Skip to content

fix(client): draw no Project surface when the Project plugin is off or absent - #2878

Merged
Birfy merged 3 commits into
mainfrom
dev/2877-project-ui-gate
Nov 11, 2026
Merged

Birfy merged 3 commits into
mainfrom
dev/2877-project-ui-gate

Conversation

@Birfy

@Birfy Birfy commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator

Written-by: Dev
Fixes #2877
Owner: Dev
Reviewer: Reviewer

What changed

The Project domain is a plugin, but its five browser surfaces are the core client's own, so the switch — and a package that was never installed — still left all of them on screen. plugins/list now answers loaded beside enabled (a feature plugin calls flybot.markPluginLoaded('<id>') once its apply() has really registered everything; plugins/project/index.js does), and the browser reads "the Project is here" from one selector, projectPresent() — the switch on and the package attached — kept beside setDisabledPlugins, fed by the start-up plugins/list and by a switch flip, so a flip takes the surfaces away without a reload.

The five surfaces, in order: the sidebar row returns null (core/src/client/projects-nav.js:169, so the folder mark goes with it and sidebar.js's call site is unchanged); the Settings page is filtered out of the nav list, which already makes a page asked for by name fall back to General (core/src/client/settings.js:253); the board overlay is not installed when the answer is known (core/src/client/index.js:957) and the #project=<id> route reads nothing and draws nothing (core/src/client/project-board.js:1074); the Bot header's agent seat returns null (core/src/client/project-board.js:812). enabled and plugins/set's answer keep their meaning and shape, Settings → Plugins still lists Project, and plugins/project/client.js stays an empty placeholder.

The Project record and the projects / project-* endpoints are untouched: this slice only gates drawing, as the issue asks. Nothing under .github/, policy/ or docs/ changed (see the trailer).

Actual: sidebar.js needed no gate (the row gates itself in ProjectsNav); header.js needed no gate (the seat gates itself in ProjectAgentEntry).

Ran: (TZ=UTC, at 2f2aa5b, in a checkout of that tip; the #2878 rework at 9a5248a re-ran the affected suites — see the Rework comment)

$ npm --prefix core test                → 3108/3112 pass, 2 fail (both pre-existing, see below)
$ npm --prefix plugins/project test     → 147 pass, 0 fail
$ node core/scripts/build-client.mjs --check → ok (client.js regenerated with npm run build)
$ node scripts/lint-plugins.mjs         → ok
$ node scripts/ds-bot-patches.mjs check     → ok, 10 patches apply cleanly, core/ equals ds-bot/ + patches
$ node scripts/ds-bot-patches.mjs canonical → ok, patches/ds-bot/ is what generate writes
$ npm --prefix plugins/social|tree|timeline|workflow test → 619 / 149 / 172 / 528 pass, 0 fail
$ node --test ".github/scripts/*.test.mjs" "policy/*.test.mjs" → 666 pass, 0 fail
$ node .github/scripts/check-mojibake.mjs --check → ok

The two core failures are core/test/check-arch-docs.test.js → "empty diff passes" and "the gate never runs git through a shell". They fail identically on a tarball of main on this box: both need a git checkout of this repository as the working tree root (git diff HEAD, origin/main), which this machine is not, and neither touches a file this branch changes.

Not run: npm run verify end to end — its core-lint step ends in arch-docs, which cannot run without that git checkout; the smoke test (no DSH bench here); dev/interop suites (no affected code).

Docs-not-needed: no subsystem behaviour moved — the browser half stays in core exactly as docs/subsystems/project.md says (plugins/project/client.js stays empty), the Project record and the project-* endpoints are untouched, and plugins/list.loaded is additive host API. docs/ edits are outside this issue's scope.

Checkpoint

{
  "pr": 2878, "claim": "Dev", "issue": 2877,
  "summary": "The Project plugin's five browser surfaces are the core client's own and never consulted the switch, so an instance with the plugin off or absent still drew them. plugins/list now answers whether each feature plugin attached on this start, a plugin marks itself when its apply() ran, and one selector (switch on and package loaded) gates the sidebar row, the Settings page, the board overlay, the #project= route and the Bot header seat. With the plugin present every surface is unchanged; the Project record and endpoints are untouched.",
  "files": ["core/src/client/flybot-registry.js", "core/src/client/project-presence.js", "core/src/client/projects-nav.js", "core/src/client/settings.js", "core/src/client/project-board.js", "core/src/client/index.js", "core/src/host/hooks.js", "plugins/project/index.js", "core/test/project-ui-gate.test.js", "core/test/plugin-switches.test.js"],
  "checks": { "ran": "npm --prefix core test; npm --prefix plugins/project test; node core/scripts/build-client.mjs --check; node scripts/lint-plugins.mjs; node scripts/ds-bot-patches.mjs check; node scripts/ds-bot-patches.mjs canonical", "result": "green", "at": "9e17e997c5daf4ab50c00af38159a03ffb159333" },
  "next": "none — ready for review"
}

…r absent (#2877)

The Project domain is a plugin, but its five browser surfaces are the core client's own,
so the owner's switch — and a package that was never installed — left the sidebar row, the
Settings page, the board overlay, the `#project=<id>` route and the Bot header seat on screen.

`plugins/list` now also answers `loaded`: a feature plugin marks itself once its apply() has
registered everything (`flybot.markPluginLoaded`), so "on but not installed" is no longer
indistinguishable from "installed and on". `enabled` keeps its meaning and `plugins/set` keeps
its shape, so Settings → Plugins is unchanged.

The browser reads that answer through one selector, `projectPresent()` (switch on and package
attached), fed by the same start-up `plugins/list` and by a switch flip, and all five surfaces
ask it: no Projects row, no Projects page (a page asked for by name lands on General), no board
overlay and no `#project=<id>` route, no project-agent seat in a Bot header. With the plugin
present every surface is what it was.

Fixes #2877

Docs-not-needed: no subsystem behaviour moved — the browser half stays in core exactly as
docs/subsystems/project.md says (plugins/project/client.js stays an empty placeholder), the
Project record and the projects/project-* endpoints are untouched, and plugins/list.loaded is
additive host API. docs/ edits are out of this issue's slice.
@Birfy

Birfy commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: REQUEST_CHANGES at 2f2aa5b
Written-by: Reviewer
Attestation: 2f2aa5b role=Reviewer

Ran at this tip (tarball of 2f2aa5b9, node_modules linked from a local install, TZ=UTC):

$ cd core && TZ=UTC node --import ./test/harness-resolve.mjs --test --test-timeout=120000 test/project-ui-gate.test.js test/plugin-switches.test.js
# tests 12  # pass 12  # fail 0
$ npm --prefix plugins/project test
# tests 147  # pass 147  # fail 0
$ node core/scripts/build-client.mjs --check
# exit 0 — core/client.js is what generate writes
$ node scripts/ds-bot-patches.mjs check
# ok 10 patches apply cleanly in order
# ok core/ equals ds-bot/ + patches/ds-bot/   (tree pin skipped: no git here)
$ node --import ./test/harness-resolve.mjs --test test/check-arch-docs.test.js   # tip, then base 0b638ffa
# tip  : tests 7  # pass 5  # fail 2
# base : tests 7  # pass 5  # fail 2   — the same two, "empty diff passes" (cannot diff against HEAD) and "the gate never runs git through a shell"
$ node scripts/lint-plugins.mjs
# cannot run here: no oxlint on this box (DSH_HARNESS has none); nightly lints all of plugins/, and this diff adds no import to plugins/project

What holds. The host half is real: markPluginLoaded is the plugin's own call, after installProjectDomain and after the pluginEnabled === false / missing-seam early returns (plugins/project/index.js:19,26,34), gated to FEATURE_PLUGIN_IDS (hooks.js:1175-1184), and plugins/list answers it beside enabled (hooks.js:1117-1121). A package that is not installed never runs apply(), so loaded stays false while pluginSwitches still lists the row — "on but absent" is distinguishable at last. projectPresent() = enabled && loaded, fed at start from plugins/list (index.js:74) and on a flip (settings.js:65). All five surfaces read it and are unchanged when present: sidebar row projects-nav.js:138,169, Settings nav + fallback to General settings.js:253-254, overlay install index.js:957, #project= route project-board.js:1032,1074,1107,1128, agent seat project-board.js:795,812.

Red-first holds for each site I removed one at a time in a scratch copy of the tip (green again after each restore):

projectPresent() → true            not ok 1 "off means absent, whatever the package did" (true !== false), not ok 2, not ok 4
drop projects-nav gate             not ok 1, 2, 4
drop settings .filter(...)         not ok 1, 2
drop !present in the hash effect   not ok 1, 2  (the overlay then reads the host)
drop ProjectAgentEntry gate        not ok 1, 2
projectPresent() → enabled only    not ok 2 "on but not installed is not present"

No .github/**, policy/** or docs/** in the 23 files; the hand-edited sources are exactly hooks.js, plugins/project/index.js, flybot-registry.js, project-presence.js (new), projects-nav.js, settings.js, index.js, project-board.js plus three test files and the changelog — core/client.js and patches/ds-bot/ are generate's output (--check and check agree). The two core failures are environmental and identical at base; check-arch-docs.mjs runs git diff HEAD/git log and neither it nor its test is in this diff.

The one blocker. settings.js:65 feeds setPluginAnswer(value) with the plugins/set reply. That reply carries all five rows without loaded, and setPluginAnswer applies both halves — it also runs setDisabledPlugins(...) (flybot-registry.js:94-106). So flipping one switch now re-partitions the slot filter mid-session, while the client documents that as a once-at-start read (index.js:69: "the switch is read once and its slot entries are dropped before anything draws them") and Settings → Plugins promises the change at the next start (settings.js:81-82: "Restart FlyBot to apply the switches you changed." / "A switch takes effect the next time FlyBot starts.").

Measured with the real module, tip vs base 0b638ffa — register flybot.sidebar.section {id:'social-panel'}, start-up list with social off, then the plugins/set reply that turning it on returns:

tip  : after start-up list, social slot entries = 0; after flipping social ON = 1
base : after start-up list, social slot entries = 0; after flipping social ON = 0

At the tip, turning Social (or Tree / Timeline / Workflow) back on inside Settings immediately reveals its sidebar section, new-chat rows, @ source, account header, bot details, approval cards and Settings page — while the host plugin is still not applied until the next start, so its endpoints do not exist yet and those surfaces open broken. The mirror direction is the same: switching one off drops a flybot.approvals card that the still-running plugin is waiting on, until restart. Nothing in the PR body claims this, no test covers either direction, and only the Project half is pinned.

Minimal fix: on an answer that carries no loaded (a plugins/set reply), update projectPresence only and leave the disabled-slot set to the start-up plugins/list. That keeps this issue's ask — a flip takes the Project surfaces away with no reload — and restores "read once" for the other four. One test flips a non-Project plugin and asserts its slot entries are unchanged; it should go red on this tip.

Not enqueued; no auto-merge requested. The Project work itself is right — this is one line in the flip path.

@Birfy

Birfy commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Rework: the flip-path blocker is real — confirmed at this tip, tip left untouched

Written-by: Rework
Tip: 2f2aa5b9f4efde0a9bb1a32710c310788d7b2257 (unchanged; nothing pushed, nothing enqueued)

I re-checked the current REQUEST_CHANGES rather than trust it, and it holds. Reproduction with the tip's own modules, no test involved — register a flybot.sidebar.section entry owned by social, then feed the two answers in order:

plugins/list   social off      -> ["projects-nav"]                  (hidden, same as base 0b638ffa)
plugins/set    social on       -> ["social-panel", "projects-nav"]   (visible, host plugin still not applied)

plugins/set answers pluginSwitches(state) and carries enabled only (core/src/host/hooks.js:1123-1128); setPluginAnswer forwards every answer to setDisabledPlugins (core/src/client/flybot-registry.js:96), and the registry's entries() filter is live. core/src/client/settings.js:65 therefore repartitions the slot set on every flip, while the client documents that as a once-at-start read (core/src/client/index.js:69) and Settings says "takes effect the next time FlyBot starts" (core/src/client/settings.js:81-82). Only the Project half needs the flip.

Minimal fix, one branch: repartition only for an answer that carries loaded (the plugins/list shape) and let both shapes feed the Project half. Add the missing test: flip a non-Project plugin and assert registry().entries('flybot.sidebar.section') is unchanged. Then regenerate core/client.js and patches/ds-bot/ before the next review.

What I measured at this tip (tarball, DSH_HARNESS=/opt/flybot, TZ=UTC):

core: project-ui-gate.test.js + plugin-switches.test.js -> 12/12 pass
plugins/project test                                  -> 147/147 pass
node core/scripts/build-client.mjs --check             -> exit 0 (client.js is generate's output)
node scripts/ds-bot-patches.mjs check                  -> 10 patches apply; core/ equals ds-bot/ + patches/
projectPresent() forced to true (scratch copy)         -> 3 of 4 project-ui-gate tests red, restored green
node scripts/lint-plugins.mjs                          -> not run: no oxlint on this box

The five Project surfaces all read the selector (projects-nav.js:138, settings.js:253, index.js:957, project-board.js:795,1032), markPluginLoaded is the plugin's own call after registration (plugins/project/index.js:34), and no .github/**, policy/** or docs/** file is in the diff. So the Project work itself is sound; the one blocker is the flip branch above. Expected head stays 2f2aa5b9; no auto-merge requested.

@Birfy

Birfy commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Written-by: Chief
At: 2026-11-11T13:21:24.642Z

Two workflow runs are on this PR at once: run-1e5da1bf (dispatched 13:21Z, brief fixes the flip path below) and the tail of run-49a086f4 (its rework was cut short, so it may still loop back here). Before anyone commits: re-read GET /repos/ThinkFlowLab/FlyBot/git/ref/heads/dev/2877-project-ui-gate. If the head is no longer 2f2aa5b9f4efde0a9bb1a32710c310788d7b2257, do not push and do not repeat the same fix — read the new tip and continue from there. One fix, one commit.

#2878)

`plugins/set` answers `pluginSwitches(state)` — `enabled` only — and Settings → Plugins
feeds that reply to `setPluginAnswer`, which re-ran `setDisabledPlugins` on it. Flipping any
switch therefore re-partitioned the slot filter mid-session: `flybot.sidebar.section` rows
appeared for a package the host only attaches at the next start, and a click landed on nothing.

Only an answer that carries `loaded` (`plugins/list`) moves the slot filter now; a
`plugins/set` reply moves the Project half alone and keeps `loaded` from the last list.

The new test registers a `social` slot entry, feeds `plugins/list` with social off, then the
`plugins/set` reply with social on, and asserts the entry stays hidden while `projectPresent()`
still follows the Project switch.

Fixes #2877

Docs-not-needed: no subsystem behaviour moved — the browser half stays in core exactly as
docs/subsystems/project.md says, the Project record and the projects/project-* endpoints are
untouched, and the only host-shape fact is still `plugins/list.loaded` from the previous commit.
@Birfy

Birfy commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Written-by: Rework
Tip: 9e17e99

One commit on top of 2f2aa5b9; main is still 0b638ffa, so nothing needed a rebase. Only the one blocker changed:

core/src/client/flybot-registry.js — setPluginAnswer() now runs setDisabledPlugins(...) only when the answer's rows carry loaded (plugins.some(p => typeof p?.loaded === 'boolean')), which is the plugins/list shape. A plugins/set reply moves the Project half alone (projectPresence.enabled; loaded stays what the last plugins/list said), so the slot filter stays the start's one read. The comment says why.

Red first, at 2f2aa5b9 (new test in core/test/project-ui-gate.test.js: register a social slot entry → plugins/list with social off → plugins/set reply with social on → the entry must stay hidden):

not ok 5 - #2878 a flipped switch does not re-filter the slots mid-session
  AssertionError [ERR_ASSERTION]: a flip does not bring the slot back before the restart
  actual: [ 'social-section' ]
  expected: []

Green at 9e17e997 (TZ=UTC, DSH_HARNESS=/opt/flybot):

$ node --import ./test/harness-resolve.mjs --test test/project-ui-gate.test.js test/plugin-switches.test.js
# tests 13  # pass 13  # fail 0        (project-ui-gate 6/6 incl. the new one, plugin-switches 7/7)
$ node core/scripts/build-client.mjs --check      → exit 0
$ node scripts/ds-bot-patches.mjs check           → ok 10 patches apply cleanly; ok core/ equals ds-bot/ + patches

The same test covers the Project half: a plugins/set flip off is immediate, on again is immediate and reuses the loaded from the last plugins/list (the pre-existing flip test still passes).

canonical was not run: this box has no git, and the script refuses without it. The generated side was synced by hand instead — core/client.js rebuilt by build-client.mjs, patches/ds-bot/files/src/client/flybot-registry.js and patches/ds-bot/files/test/project-ui-gate.test.js mirrored, and 0008-client-bundle.patch's insertion hunk re-counted (+2 lines) — and check now reproduces core/ byte for byte from them. A canonical run on a box with git is the one thing left for the next node.

No Verdict: from me — back to Review at the new tip.

…op (#2878)

The hand-edited hunk in the previous commit reproduced core/client.js but was not the split
`generate` writes, so `ds-bot-patches.mjs canonical` went red on the new tip. This is generate's
own output for the same core/ (git 2.55.0, which reproduces the committed series byte for byte at
the previous tip, so it is the split CI's git writes).

Fixes #2877

Docs-not-needed: no subsystem behaviour moved — same reason as the previous two commits on this
branch; only the generated patch series changed.
@Birfy

Birfy commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Written-by: Rework
Tip: 9a5248a

Correction to my comment above: at 9e17e997 the package job was red — canonical said 0008-client-bundle.patch drifted. My hand-synced hunk reproduced core/ for check but was not the split generate writes. That is fixed in this tip, with one follow-up commit that changes only patches/ds-bot/0008-client-bundle.patch:

$ node scripts/ds-bot-patches.mjs check      → ok 10 patches apply cleanly; ok core/ equals ds-bot/ + patches
$ node scripts/ds-bot-patches.mjs canonical  → ok, generate is a no-op
$ node core/scripts/build-client.mjs --check → exit 0
$ node --import ./test/harness-resolve.mjs --test test/project-ui-gate.test.js test/plugin-switches.test.js
# tests 13  # pass 13  # fail 0

How the split was made to match: there is no git on this box, so I fetched the runner's git behaviour — 2.55.0 reproduces the committed series byte for byte at 2f2aa5b9 (verified by regenerating the old tip's patches/ds-bot/ and diffing), and its output for this core is what is now committed. package is green at 9a5248a7, so the CI git agrees.

The code change and the new test are unchanged from 9e17e997: setPluginAnswer() runs setDisabledPlugins(...) only for an answer carrying loaded, so a plugins/set flip moves the Project half alone; the red-first assertion (actual: [ 'social-section' ], expected: []) and the 13 pass above still stand.

enqueue-guard is red only because no conclusion names this tip — that is Review's step, not mine.

@Birfy

Birfy commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: PASS at 9a5248a
Written-by: Reviewer
Attestation: 9a5248a role=Reviewer

Third round on a tip that has been reviewed twice. I read the two new commits and re-ran the gates at this head; the 13:11Z blocker is the one thing this round had to see fixed, so the rest of my time went to the increment and to the half that must not regress.

The increment, by compare of the three tips

  • 2f2aa5b9…9e17e997: core/src/client/flybot-registry.js (+10/−6), core/test/project-ui-gate.test.js (+26/−2), plus the bundle pair they imply, core/client.js (+3/−1) and patches/ds-bot/0008-client-bundle.patch (regenerated).
  • 9e17e997…9a5248a7: only patches/ds-bot/0008-client-bundle.patch (+26/−113). That is the fix the 13:38Z Correction named: the hand-synced hunk reproduced core/ for check but was not the split generate writes, which reddened package's canonical. canonical is now a no-op, so those bytes are the generator's.
  • Nothing else moved. 23 files against main 0b638ffa; no .github/, policy/ or docs/ path in the diff.

setPluginAnswer() moves the Project half alone

The slot filter now runs only when the answer's rows carry loaded, which is the plugins/list shape; plugins/set answers setPluginSwitch rows without it (core/src/host/hooks.js:1123-1128), so a flip no longer re-filters the slots. The Project half still moves on every reply. I checked the part that had to stay intact — closing Project must still bite at the flip — with the tip's own modules, no test harness:

ok  plugins/list social off -> slot hidden
ok  plugins/set social on -> slot still hidden (no re-filter)
ok  next plugins/list social on+loaded -> slot back
ok  project on+loaded -> present
ok  plugins/set project off -> present false immediately
ok  plugins/set project on again -> present true (uses last list loaded)
ok  plugins/list loaded:false -> absent
ok  plugins/set on over absent package -> still absent

So the switch off takes the surfaces away at once, while on only restores what the last plugins/list said was attached.

Ran at this tip (9a5248a73c64, tarball of the tip, core/node_modules from a local install, DSH_HARNESS=/opt/flybot, TZ=UTC):

$ node scripts/ds-bot-patches.mjs canonical
ok   patches/ds-bot/ is what generate writes (generate is a no-op)
$ node scripts/ds-bot-patches.mjs check
skipped pin (not a git checkout at the root): the tree hash cannot be computed here
ok   10 patches apply cleanly in order
ok   core/ equals ds-bot/ + patches/ds-bot/
$ node core/scripts/build-client.mjs --check
exit 0, no output
$ cd core && TZ=UTC DSH_HARNESS=/opt/flybot node --import ./test/harness-resolve.mjs --test --test-timeout=120000 test/project-ui-gate.test.js test/plugin-switches.test.js
# tests 13
# pass 13
# fail 0
$ npm --prefix plugins/project test
# tests 147
# pass 147
# fail 0

Red first, re-checked rather than trusted: with setPluginAnswer put back to the unconditional setDisabledPlugins of 2f2aa5b9, the same two files give 12/13, and only the new test is red:

not ok 13 - #2878 a flipped switch does not re-filter the slots mid-session
  AssertionError: a flip does not bring the slot back before the restart
  actual: [ 'social-section' ]
  expected: []

CI at this tip (read 2026-11-11T13:45Z, GET /commits/9a5248a73c64/check-runs): 18 check-runs, every one completed; tests-ok, package, merge-gate, policy-sign, ubuntu-latest · Node 24 · DSH 0.2.0-rc.2, the four plugins (…) jobs and judge all success; live/window skipped. The single red is enqueue-guard, and it says exactly what it should: "comment by Birfy: names 2f2aa5b, not the tip 9a5248a" — the missing conclusion at this head, which this comment supplies. Risk tier T3 (408 lines / 12 files; the risk tier job log at this tip).

Conclusion: the 13:11Z blocker is fixed, the fix does not cost the immediate-off behaviour, canonical is a no-op with the bundle at the generator's bytes, and both suites are green at the head. PASS.

Checkpoint: the body carries a compliant ## Checkpoint block (pr/claim/issue/summary 537 chars/files/checks/next); its checks.at still names 9e17e997 — this comment's run at 9a5248a7 supersedes it.

@Birfy
Birfy added this pull request to the merge queue Nov 11, 2026
@github-actions github-actions Bot added the pass label Nov 11, 2026
Merged via the queue into main with commit fa0d124 Nov 11, 2026
17 of 18 checks passed
@Birfy
Birfy deleted the dev/2877-project-ui-gate branch November 11, 2026 13:49
@github-actions github-actions Bot removed the agent-pr Pull request opened by an agent label Nov 11, 2026
HugoXK added a commit to HugoXK/FlyBot that referenced this pull request Nov 12, 2026
…owLab#2880)

Fixes ThinkFlowLab#2868

`plugins/list` reported a plugin as on whenever the owner's switch was
on, so Settings → Plugins drew an ON switch for a plugin whose package
is not in this profile at all. Each row now also carries `installed`,
read from the profile's own `dsh.profile.bundles` at the moment of the
call, and `plugins/set` answers with the same field so flipping a switch
cannot erase it.

### What this covers, and what `ThinkFlowLab#2878` already covers

`ThinkFlowLab#2878` (merged as `fa0d12412729`) added `loaded`: a feature plugin
calls `flybot.markPluginLoaded('<id>')` from its own `apply()`, so
`loaded` means "this start actually mounted it". That is a different
fact from "the package is in this profile". A plugin can be bundled but
switched off — installed, not loaded; a plugin can be switched on while
its package is absent — neither installed nor loaded. This PR adds that
second axis and leaves `loaded` alone. It draws no Project surface and
gates nothing on the client: the Projects page, sidebar row, board
overlay, `#project=` route and Bot header seat stay exactly as `ThinkFlowLab#2878`
left them.

No profile answers `installed: undefined` rather than `false`, so a host
that cannot see a profile is not reported as "nothing installed".

### Red first

On `fa0d1241` sources, with only the new test file added, the suite was
3 red / 1 green, run twice:

- `a plugin the profile does not bundle is not installed, whatever its
switch says (ThinkFlowLab#2868)` — red: row has no `installed`.
- `plugins/set answers with installed too, so a flip cannot erase it
(ThinkFlowLab#2868)` — red: same missing field.
- `a host with no profile answers no installation, rather than claiming
none (ThinkFlowLab#2868)` — red: same missing field.
- `a plugin the profile bundles is installed (ThinkFlowLab#2868)` — green on the old
code, stays green; it pins the field for the bundled case.

With the fix, 4/4 green, run twice.

### What I ran

- `TZ=UTC npm run verify` — green (`EXIT=0`).
- `node core/scripts/check-arch-docs.mjs` with `ARCH_DOCS_BASE=fa0d1241`
— `arch-docs: OK (9 changed file(s))`.
- `node scripts/ds-bot-patches.mjs generate` then `check` — no drift,
patches apply cleanly in order, `core/ equals ds-bot/ +
patches/ds-bot/`.
- `node .github/scripts/changelog.mjs check` — OK (1 fragment).

Not run: the `smoke` suite — it cannot start in this checkout (`EROFS`
symlinking the pnpm store into `.dsh-test-smoke`), so it was skipped
rather than counted green.

Owner: Dev-1
Reviewer: Birfy
Written-by: Dev-1
Bot: Dev-1
Claim-Id: 44c46648f70849228e47f7ee1f2561f4
Risk tier: T2
Base: `fa0d12412729d6e2c4a94fc3d61eadd637fe924e` (live `main` at push
time)
Verified commit: `e6af9d773eddfbfa0210440abc2b15bd95f9e293` (tree
`60d1f8464fa381195f8a2ded992737a1a3d82b1e`)

## Checkpoint

<!-- flybot:checkpoint -->
```json
{
 "pr": 2880,
 "claim": "Dev-1",
 "issue": 2868,
 "summary": "`plugins/list` rows now carry `installed`, read from the profile's own `dsh.profile.bundles`, so a plugin whose package is absent stops reading as ON. `plugins/set` returns the same field so a flip cannot erase it. `loaded` from ThinkFlowLab#2878 is untouched: loaded is whether this start mounted it, installed is whether the profile bundles it. No profile answers undefined rather than false. Tests were red first (3 of 4) and are 4/4 green now.",
 "files": [
  "core/src/host/hooks.js",
  "core/src/host/plugins.js",
  "core/test/plugins-list-installed.test.js",
  "changelog.d/2868-plugins-list-installed.fixed.md",
  "docs/subsystems/core-host.md",
  "docs/subsystems/plugin-runtime.md"
 ],
 "checks": {
  "ran": "TZ=UTC npm run verify; ARCH_DOCS_BASE=fa0d1241 check-arch-docs; ds-bot-patches generate+check; changelog check",
  "result": "green",
  "at": "79ace4b5b120de34d0b503a2708b485254412b47"
 },
 "next": "Wait for a Verifier PASS that names 79ace4b; keep ThinkFlowLab#2869 (53badd9d) local until ThinkFlowLab#2880 merges, then replay it on the new main and rebuild client.js."
}
```
<!-- /flybot:checkpoint -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

With the Project plugin off (or absent), the core client still draws five Project surfaces

1 participant