aedee4b8231bf050c3240a00ab6645ede5d87ee9
486 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2eae0b2507 |
feat: remove stuck-task tagging from the dashboard; fix liveness-ratchet scan path
Removes the dashboard's stuck-task tagging per operator request: the Stuck card/status badges, stuck row styling, the footer Stuck segment and stuckTaskCount stat, utils/taskStuck.ts, the isStuck agent-activity gate, and the taskStuckTimeoutMs prop plumbing (App -> Board/Lane/Column/ WorktreeGroup/MainContent -> TaskCard/ListView/ExecutorStatusBar). Stuck-task tests are deleted or reconciled. The taskStuckTimeoutMs setting and the engine's recovery sweeps (including the stuck-killed status) are unchanged — the setting is engine-side only now. Also repoints the FN-6756 liveness-gate ratchet's facade scans at executor/task-executor-session-facades.ts, where the wave20 extraction moved hasLiveSessionSurface/clearPhantomExecutorBinding (the two pre-existing red tests on main). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9a9e591b72 |
FN-9115: clarify and expand agent skill selection
Clarify automatic skill availability while making forced-reading selections easier to manage. - Replace the skill picker with a searchable multi-select checkbox list and explicit loading, error, empty, and unavailable states. - Label forced, disabled, and undiscovered skills consistently across agent detail and list views, including a clear None state. - Update localized guidance, documentation, regression coverage, and the published package changeset. Files changed: .changeset/fn-9115-skills-ui.md | 7 + docs/dashboard-guide.md | 4 + .../agent-detail-settings-theme-styling.test.ts | 3 +- .../dashboard/app/components/AgentDetailView.css | 7 +- .../dashboard/app/components/AgentDetailView.tsx | 35 ++- packages/dashboard/app/components/AgentsView.css | 4 + packages/dashboard/app/components/AgentsView.tsx | 13 +- .../dashboard/app/components/NewAgentDialog.tsx | 5 +- .../dashboard/app/components/SkillMultiselect.css | 258 +++------------------ .../dashboard/app/components/SkillMultiselect.tsx | 178 +++++--------- .../AgentDetailView.mobile-scroll.test.tsx | 4 +- .../AgentDetailView.skills-procedure.test.tsx | 59 +++-- .../app/components/__tests__/AgentsView.test.tsx | 19 +- .../components/__tests__/SkillMultiselect.test.tsx | 81 ++++--- .../__tests__/useDiscoveredSkillsCache.test.ts | 16 ++ .../app/hooks/useDiscoveredSkillsCache.ts | 13 +- .../app/utils/__tests__/agentSkills.test.ts | 28 +++ packages/dashboard/app/utils/agentSkills.ts | 69 +++++- packages/i18n/locales/en/app.json | 21 +- packages/i18n/locales/es/app.json | 21 +- packages/i18n/locales/fr/app.json | 21 +- packages/i18n/locales/ko/app.json | 21 +- packages/i18n/locales/pt-BR/app.json | 21 +- packages/i18n/locales/zh-CN/app.json | 21 +- packages/i18n/locales/zh-TW/app.json | 21 +- 25 files changed, 493 insertions(+), 457 deletions(-) Fusion-Task-Id: FN-9115 Fusion-Task-Lineage: 18229b87-d0dc-41e7-9333-2d12df852486 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
59d53b42a8 |
test(dashboard): repair stale engine/hook/icon mocks and moved-file source scans
Dashboard bare-run repair, mock-drift cluster. Six route suites' inline @fusion/engine mocks predated FN-8902's DEFAULT_MODEL_REGISTRY_REFRESH_TIMEOUT_MS import and now wrap the canonical createEngineMock fallback proxy; App.test's useTasks mock becomes an importOriginal spread so the real mergeTaskSnapshot reaches components, its Todo/graph tests move to FN-8762's plugin-view enablement invariants (with a mockReset fix for a stale mockResolvedValueOnce queue the clearAllMocks reset never drops); the Mailbox lucide mock learns FN-9014's Archive icon; and two source-scan tests repoint files moved by the #2398 domain-folder refactor (app/api/agents/agents.ts, engine healing/self-healing-constants.ts), both verified via git log --follow. Verified 10 files / 252 tests green under their standard lane projects; both dashboard typechecks clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5c5ffb6f48 |
test(dashboard): update component expectations to intentional post-FN-87xx behavior
Dashboard bare-run repair, component-drift cluster. All nine suites were stale expectations behind cited intentional commits — no UI regressions found: FN-8826's always-on WIP lifecycle badge (TaskCard.oversight now asserts no overseer element instead of an empty header), FN-8762's Todo Lists plugin extraction (MobileNavBar, MainContent.graph-popout, useAppSettings repurposed to the pluginDashboardViews contract, with a regression pin that the removed todosEnabled field never returns), FN-8796's clock-arbitrated mergeTaskSnapshot, FN-8797's prompt-only planning refresh, FN-8702's 767.98px phone-sheet boundary, FN-8764's primary-role split, and FN-8947's added touch-target selector. Every update carries an FNXC comment citing the causing commit. Verified 9 files / 293 tests green, tsconfig.app.json typecheck clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
59dc5df5f4 |
fix(dashboard): show a retryable message when Planning Retry hits a down server
Gateway 502/503/504 non-JSON bodies (for example Traefik "no available server") were dumped as content-type diagnostics into the Planning error banner. |
||
|
|
e9089ee8a1 |
FN-9110: contain GitHub pull request imports on mobile
Keep GitHub pull-request rows and previews readable within narrow import layouts. - Let mobile pull-request rows grow with wrapped titles, badges, and branch metadata. - Break long branch names within list rows and detail previews. - Cover modal and embedded presentations with layout and interaction regressions. - Add a patch changeset for the mobile layout fix. Files changed: .changeset/fn-9110-github-import-pulls-mobile.md | 7 +++ .../github-import-pulls-mobile-layout.test.ts | 54 ++++++++++++++++++++++ .../dashboard/app/components/GitHubImportModal.css | 27 ++++++++++- .../__tests__/GitHubImportModal.test.tsx | 32 +++++++++++++ 4 files changed, 119 insertions(+), 1 deletion(-) Fusion-Task-Id: FN-9110 Fusion-Task-Lineage: 572b5fb3-076e-448a-9f6f-185b836b4161 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
401b057965 |
FN-9025: align Quick Add tablet actions edge-to-edge
Make Quick Add action controls use the mobile edge-to-edge layout at tablet widths. - Add a tablet-specific responsive action-row layout. - Preserve centered desktop controls and existing mobile behavior. - Cover tablet spacing declarations and add a patch changeset. Files changed: .changeset/fn-9025-quick-add-tablet-spacing.md | 7 ++++ .../quick-entry-action-row-centering.css.test.ts | 35 +++++++++++++++--- .../dashboard/app/components/QuickEntryBox.css | 41 ++++++++++++++++++++-- 3 files changed, 75 insertions(+), 8 deletions(-) Fusion-Task-Id: FN-9025 Fusion-Task-Lineage: 8850dd57-c99e-4f18-98d5-576c0bc7e665 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
0e98d9b213 |
FN-9024: center Quick Add action row on tablet
Center Quick Add actions as a single desktop and tablet cluster while preserving the mobile layout. - Center the base action row and remove greedy split alignment. - Retain the <=768px edge-to-edge mobile layout. - Add a regression test and a patch changeset. Files changed: .changeset/fn-9024-quick-add-center-action-row.md | 7 ++ .../quick-entry-action-row-centering.css.test.ts | 138 +++++++++++++++++++++ .../dashboard/app/components/QuickEntryBox.css | 28 +++-- 3 files changed, 166 insertions(+), 7 deletions(-) Fusion-Task-Id: FN-9024 Fusion-Task-Lineage: e6c6d9b5-e9cf-4d37-9d62-fedd1986d7cc Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
f7bf3f91d1 |
FN-9022: add task recommendations to Insights
Expose actionable task recommendations in the Insights view. - Add recommendation list API, client hook, and Insights rendering states. - Localize recommendation content and document the Insights workflow. - Cover recommendation retrieval, routes, hooks, API, and view behavior. Files changed: .changeset/fn-9022-insights-recommendations.md | 7 ++ docs/architecture.md | 1 + docs/dashboard-guide.md | 3 +- .../__tests__/task-recommendations-list.test.ts | 136 +++++++++++++++++++++ packages/core/src/index.ts | 2 +- packages/core/src/store.ts | 5 +- packages/core/src/task-store/reads.ts | 48 +++++++- packages/core/src/types.ts | 4 + packages/core/src/types/task/task-core.ts | 19 +++ packages/dashboard/app/__tests__/api-tasks.test.ts | 24 ++++ packages/dashboard/app/api/legacy.ts | 3 + packages/dashboard/app/api/tasks/tasks.ts | 22 ++++ packages/dashboard/app/components/InsightsView.css | 27 ++++ packages/dashboard/app/components/InsightsView.tsx | 47 +++++-- .../app/components/__tests__/InsightsView.test.tsx | 117 ++++++++++++++++++ .../hooks/__tests__/useTaskRecommendations.test.ts | 130 ++++++++++++++++++++ .../dashboard/app/hooks/useTaskRecommendations.ts | 116 ++++++++++++++++++ .../__tests__/task-recommendation-routes.test.ts | 26 ++++ .../src/routes/register-task-workflow-routes.ts | 33 +++++ packages/i18n/locales/en/app.json | 11 +- packages/i18n/locales/es/app.json | 11 +- packages/i18n/locales/fr/app.json | 11 +- packages/i18n/locales/ko/app.json | 11 +- packages/i18n/locales/pt-BR/app.json | 11 +- packages/i18n/locales/zh-CN/app.json | 11 +- packages/i18n/locales/zh-TW/app.json | 11 +- packages/i18n/src/resources.d.ts | 9 ++ 27 files changed, 835 insertions(+), 21 deletions(-) Fusion-Task-Id: FN-9022 Fusion-Task-Lineage: b83f1074-37af-4f87-9ec0-2bd9bb9bf64d Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
e0af6ddd93 |
FN-8952: derive Quick Add Save smoke fixtures from locales
Keep 300px Quick Add Save browser smoke coverage synchronized with shipped translations. - Derive board and list Save fixtures from supported locale catalogs. - Escape fixture labels and reject missing Save translations. - Add fast fixture parity, derivation, and escaping coverage. Files changed: docs/testing.md | 3 + .../__tests__/browser-layout-smoke-fixture.test.ts | 75 +++++++++++++- .../dashboard/scripts/browser-layout-smoke.mjs | 110 ++++++++++++++------- 3 files changed, 147 insertions(+), 41 deletions(-) Fusion-Task-Id: FN-8952 Fusion-Task-Lineage: 31daec25-5d9a-43bb-9ccf-667702006e76 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
313eea1461 |
FN-8935: make Agents Overview scrollable on mobile
Make long Active Agents lists reachable within constrained mobile and short desktop layouts. - Establish a bounded, touch-friendly Active Agents scroll owner. - Add unit and Chromium layout coverage for populated and empty overview states. - Document the required browser smoke coverage and add a patch changeset. Files changed: .../fn-8935-agents-overview-mobile-scroll.md | 7 ++ docs/testing.md | 2 + .../__tests__/browser-layout-smoke-fixture.test.ts | 26 +++++- .../dashboard/app/components/AgentsOverviewBar.css | 14 ++++ .../AgentsOverviewBar.mobile-scroll.test.tsx | 96 ++++++++++++++++++++++ .../dashboard/scripts/browser-layout-smoke.mjs | 86 +++++++++++++++++++ 6 files changed, 230 insertions(+), 1 deletion(-) Fusion-Task-Id: FN-8935 Fusion-Task-Lineage: 43b793b9-3629-45a8-b6c1-d8100467e960 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
d450dbe971 |
FN-8829: add task recommendations
Add persistent task recommendations that agents can create, resolve, and display in task details. - Persist recommendation state and expose task recommendation API routes. - Generate recommendations from executor task completions with duplicate suppression. - Add localized dashboard recommendation tab and settings control with coverage. Files changed: .changeset/fn-8829-recommendations.md | 7 + docs/dashboard-guide.md | 1 + docs/settings-reference.md | 1 + .../postgres/settings-persistence.pg.test.ts | 10 + .../postgres/task-recommendations.pg.test.ts | 191 +++++++++ .../core/src/__tests__/settings-parity.test.ts | 2 + packages/core/src/config/settings-schema.ts | 2 + packages/core/src/index.ts | 2 +- .../0047_fn_8829_task_recommendations.sql | 3 + packages/core/src/postgres/schema-applier.ts | 32 +- packages/core/src/postgres/schema/project.ts | 2 + packages/core/src/store.ts | 12 +- packages/core/src/task-store/persistence.ts | 4 +- packages/core/src/task-store/serialization.ts | 1 + packages/core/src/task-store/settings-ops.ts | 22 + packages/core/src/task-store/task-mutation-ops.ts | 78 +++- packages/core/src/task-store/task-row-mappers.ts | 2 +- packages/core/src/task-store/task-update.ts | 51 ++- packages/core/src/types.ts | 4 + packages/core/src/types/settings/settings-scope.ts | 6 + packages/core/src/types/task/task-core.ts | 18 + .../__tests__/App.openTasksInRightSidebar.test.ts | 3 +- packages/dashboard/app/__tests__/api-tasks.test.ts | 31 ++ packages/dashboard/app/api/legacy.ts | 1 + packages/dashboard/app/api/tasks/tasks.ts | 24 ++ .../dashboard/app/components/TaskDetailModal.tsx | 43 +- .../app/components/TaskRecommendationsTab.css | 71 ++++ .../app/components/TaskRecommendationsTab.tsx | 127 ++++++ .../__tests__/SettingsModal.general.test.tsx | 11 + .../TaskDetailModal.recommendations.test.tsx | 111 +++++ .../app/components/settings/section-keys.ts | 1 + .../settings/sections/GeneralSection.tsx | 14 + .../settings-default-descriptions.test.tsx | 1 + packages/dashboard/app/hooks/useModalManager.ts | 6 + packages/dashboard/app/plugins/types.ts | 7 +- .../__tests__/task-recommendation-routes.test.ts | 472 +++++++++++++++++++++ .../src/routes/register-task-workflow-routes.ts | 263 +++++++++++- .../executor-task-recommendations.test.ts | 128 ++++++ packages/engine/src/executor.ts | 66 ++- packages/i18n/locales/en/app.json | 15 +- packages/i18n/locales/es/app.json | 16 +- packages/i18n/locales/fr/app.json | 16 +- packages/i18n/locales/ko/app.json | 16 +- packages/i18n/locales/zh-CN/app.json | 16 +- packages/i18n/locales/zh-TW/app.json | 16 +- packages/i18n/src/resources.d.ts | 12 + 46 files changed, 1908 insertions(+), 30 deletions(-) Fusion-Task-Id: FN-8829 Fusion-Task-Lineage: 5f60a1fb-9cf8-4577-9bfd-c20a2d402333 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
35810666be |
FN-8797: stabilize task detail refreshes
Keep task-detail state and controls mounted while Definition content refreshes. - Add a prompt-only task endpoint and client request path. - Preserve task lifecycle, workflow badges, and actions during prompt refreshes and workflow updates. - Cover prompt refresh stability, late responses, and route behavior. Files changed: packages/dashboard/app/__tests__/api-tasks.test.ts | 14 ++ packages/dashboard/app/api/legacy.ts | 3 + packages/dashboard/app/api/tasks/tasks.ts | 15 ++ .../dashboard/app/components/TaskDetailModal.tsx | 102 ++++++---- .../__tests__/TaskDetailModal.rendering.test.tsx | 211 ++++++++++++++++++--- .../__tests__/TaskDetailModal.test-helpers.ts | 1 + ...gister-task-workflow-routes.task-prompt.test.ts | 93 +++++++++ .../src/routes/register-task-workflow-routes.ts | 17 ++ 8 files changed, 388 insertions(+), 68 deletions(-) Fusion-Task-Id: FN-8797 Fusion-Task-Lineage: 57d571b4-4a02-4c96-9986-444c525d8405 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
3071018593 |
fix: prepare browser smoke fixture before launch (#3333)
## Summary - prepares emitted dashboard CSS and binds the fixture server before starting Chrome - prevents cold client builds from consuming the supervised browser lifetime - adds a regression that holds browser launch until fixture preparation resolves ## Test plan - `FUSION_DASHBOARD_DEEP=1 pnpm --filter @fusion/dashboard exec vitest run --project dashboard-app-quality-foundation-ui --pool=vmThreads --maxWorkers=1 --silent=passed-only --reporter=dot app/__tests__/browser-layout-smoke-fixture.test.ts` - `pnpm --filter @fusion/dashboard typecheck` - `pnpm --filter @fusion/dashboard build` - `FUSION_BROWSER_SMOKE_REQUIRE=1 pnpm --filter @fusion/dashboard test:browser-smoke` - `pnpm exec eslint packages/dashboard/scripts/browser-layout-smoke.mjs packages/dashboard/app/__tests__/browser-layout-smoke-fixture.test.ts` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved browser smoke test setup to ensure fixture preparation completes before the browser launches. * Added cleanup handling when browser startup fails, preventing leftover test resources. * Preserved the primary browser launch error when cleanup also fails, while recording the cleanup issue. * **Tests** * Added coverage for fixture startup order, launch failures, and cleanup behavior. * Preserved existing HTML fixture validation. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
9939897aab |
fix: prevent stale planning approvals and review churn (#3327)
## Summary Planning can no longer approve or execute against evidence from a superseded dependency episode. Dependency mutations, approval decisions, recovery, and execution admission now share serialized lifecycle rules, so stale planner work cannot restore an invalid approval or release an unplanned task. Review also converges instead of discovering one blocker per round. Planning performs a repository-grounded completeness pass up front; Plan Review batches all independently discoverable blockers and carries an episode-scoped decision ledger across revisions; code review traces changed invariants through production consumers and tests. Repeated feedback still advances the safety budget, while provider failures and superseded episodes stay outside the remediation ledger. The dashboard now exposes manual approval only for the intended exhausted-review state, and refusal/recovery audit events make rejected lifecycle transitions diagnosable without leaking prompt content. ## Validation - `pnpm verify:fast` — scoped typechecks/builds, CLI build, and boot smoke passed. - Focused Core and Engine regression suites — 511 tests passed. - `pnpm lint`, strict changeset validation, Core/Engine typechecks, and package builds passed. Fixes #3325. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Improved Plan Review approvals, rejections, and replan-cap handling across task workflows. * Added cumulative feedback and attempt tracking across repeated planning reviews. * Added safer recovery for stalled planning handoffs and interrupted approval updates. * **Bug Fixes** * Prevented stale approvals and unplanned execution after dependency changes. * Improved concurrent approval handling, retryability, and refusal-record deduplication. * Refined dashboard approval indicators and responsive approval views. * **Quality Improvements** * Strengthened planning and code-review completeness checks and blocking-finding coverage. * Preserved review history while clearly marking outdated approvals. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
5b2b31d2c9 |
FN-8762: extract Todo Lists into bundled plugin
Move Todo Lists into a bundled, project-enabled plugin package. - Move Todo UI, client API, and server routes into the plugin package. - Register and bundle Todo as an enabled plugin dashboard view rather than a static host feature. - Preserve Todo route validation and server-error semantics, including task assignment agent lookup. - Keep disabled and legacy Todo views out of project navigation and main content. Files changed: .changeset/fn-8762-todos-plugin.md | 7 + AGENTS.md | 3 +- docs/PLUGIN_AUTHORING.md | 4 + docs/dashboard-guide.md | 4 + docs/todo-view.md | 151 +---- .../cli/src/plugins/staged-bundled-plugin-ids.ts | 1 + packages/cli/tsup.config.ts | 8 + .../core/src/board/mobile-nav-primary-items.ts | 2 - .../__tests__/bundled-plugin-install.test.ts | 2 + .../core/src/plugins/bundled-plugin-install.ts | 1 + packages/dashboard/app/App.tsx | 18 +- .../app/__tests__/lazy-loaded-views-docs.test.ts | 9 +- packages/dashboard/app/api/legacy.ts | 15 - packages/dashboard/app/api/system/index.ts | 1 - packages/dashboard/app/api/system/todo.ts | 85 --- packages/dashboard/app/components/Header.tsx | 23 +- .../dashboard/app/components/LeftSidebarNav.tsx | 1 - packages/dashboard/app/components/MobileNavBar.tsx | 4 - .../dashboard/app/components/SettingsModal.tsx | 2 - .../app/components/__tests__/App.test.tsx | 7 - .../app/components/__tests__/Header.test.tsx | 42 -- .../app/components/__tests__/RightDock.test.tsx | 18 +- ...skDetail.mobile-transition.board-panel.test.tsx | 1 - .../__tests__/TaskDetail.swipe-back.test.tsx | 1 - .../__tests__/TodoView.mobile-css.test.ts | 66 --- .../app/components/__tests__/TodoView.test.tsx | 649 --------------------- .../__tests__/navigation-history.test.tsx | 3 - .../__tests__/overflowViewRegistry.test.tsx | 118 +--- .../app/components/dashboard/MainContent.tsx | 27 +- .../dashboard/app/components/dashboard/types.ts | 6 +- .../app/components/overflowViewRegistry.tsx | 21 +- .../app/hooks/__tests__/useTodoLists.test.ts | 291 --------- .../app/hooks/__tests__/useViewState.test.ts | 11 + packages/dashboard/app/hooks/useAppSettings.ts | 5 - packages/dashboard/app/hooks/useViewState.ts | 5 + .../__tests__/registerBundledPluginViews.test.tsx | 14 + packages/dashboard/app/plugins/bundled-todos.d.ts | 5 + .../app/plugins/registerBundledPluginViews.ts | 18 + packages/dashboard/app/plugins/types.ts | 4 + .../src/__tests__/todo-documentation.test.ts | 68 --- .../dashboard/src/__tests__/todo-routes.test.ts | 577 ------------------ packages/dashboard/src/registry-manifest.json | 101 +++- packages/dashboard/src/routes.ts | 1 - .../src/routes/plugin-bundled-runtimes.ts | 1 + .../src/routes/register-integrated-routers.ts | 2 - packages/dashboard/src/shared/dashboard-views.ts | 6 - packages/dashboard/src/todo-routes.ts | 342 ----------- packages/dashboard/vite.config.ts | 8 + packages/dashboard/vitest.config.ts | 8 + packages/desktop/scripts/workspace-tools.ts | 1 + plugins/fusion-plugin-todos/README.md | 20 + plugins/fusion-plugin-todos/manifest.json | 6 + plugins/fusion-plugin-todos/package.json | 38 ++ .../fusion-plugin-todos/src/dashboard-interop.d.ts | 12 + plugins/fusion-plugin-todos/src/dashboard-view.tsx | 4 + .../src/dashboard/LoadingSpinner.tsx | 1 + .../src/dashboard}/TodoView.css | 0 .../src/dashboard}/TodoView.tsx | 16 +- plugins/fusion-plugin-todos/src/dashboard/api.ts | 15 + .../src/dashboard/projectStorage.ts | 3 + .../fusion-plugin-todos/src/dashboard/swrCache.ts | 6 + .../src/dashboard/useConfirm.ts | 1 + .../src/dashboard}/useTodoLists.ts | 4 +- plugins/fusion-plugin-todos/src/index.ts | 4 + .../fusion-plugin-todos/src/todo-routes.test.ts | 46 ++ plugins/fusion-plugin-todos/src/todo-routes.ts | 156 +++++ plugins/fusion-plugin-todos/tsconfig.json | 28 + plugins/fusion-plugin-todos/vitest.config.ts | 9 + pnpm-lock.yaml | 64 +- pnpm-workspace.yaml | 1 + 70 files changed, 671 insertions(+), 2531 deletions(-) Fusion-Task-Id: FN-8762 Fusion-Task-Lineage: 3beb502c-3793-451b-b357-50c243395410 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
5d7fa7c6b6 |
fix(dashboard): align List browser smoke fixture (#3316)
## Summary - mark the native mobile List fixture with the production single-pane class introduced by FN-8754 - add fixture coverage so the production selector cannot drift silently ## Test Plan - `pnpm --filter @fusion/dashboard exec vitest run app/__tests__/browser-layout-smoke-fixture.test.ts --silent=passed-only --reporter=dot` - `pnpm check:changesets` - `pnpm build` - `pnpm --filter @fusion/dashboard test:browser-smoke -- --require-browser` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Updated the mobile list layout fixture to include the production single-pane styling class, improving alignment with the app’s mobile layout behavior. * **Tests** * Added a browser smoke test to verify the expected mobile list layout class is generated reliably. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
cb57093d03 |
refactor: domain folder layout (types, API, core, engine) (#2398)
## Summary Wave 17 organizes Fusion into **domain folders** (stacks on #2397). ### Layout - **core/types/** — board, task, agents, settings, merge, workflow, mesh, … - **core/src/** — agents, ai, async-stores, workflows, tasks, config, db, … - **dashboard/app/api/** — client, tasks, agents, git, missions, planning, … - **engine/src/** — agents, auth, execution, merge, missions, overseer, worktree, … Root keepers retained for large entrypoints (`store.ts`, `executor.ts`, `merger.ts`, …). Public barrels (`@fusion/core`, `@fusion/engine`, `app/api.ts` → legacy) stay stable. ## Test plan - [x] `@fusion/core` typecheck - [x] `@fusion/engine` typecheck (pre-existing playwright-core noise only) - [ ] CI merge gate **Stack:** #2394 → #2397 → **this PR** |
||
|
|
5931e10345 |
FN-8730: add Midnight dashboard theme
Add a persisted Midnight palette with consistent first-paint behavior across dashboard surfaces. - Register Midnight in shared theme metadata and web/Electron startup validators - Define light and dark Midnight tokens plus selector swatches - Cover selection and bootstrap validation, and document the new preset Files changed: .changeset/fn-8730-midnight-theme.md | 7 ++ docs/dashboard-guide.md | 5 +- docs/settings-reference.md | 2 +- packages/core/src/types/execution-and-ui.ts | 5 ++ .../dashboard/app/__tests__/midnight-theme.test.ts | 94 ++++++++++++++++++++++ .../dashboard/app/components/ThemeSelector.css | 14 ++++ .../components/__tests__/ThemeDropdown.test.tsx | 54 ++++++++++++- packages/dashboard/app/components/themeOptions.ts | 1 + packages/dashboard/app/index.html | 3 +- packages/dashboard/app/public/theme-data.css | 86 +++++++++++++++++++- packages/desktop/src/renderer/index.html | 2 + 11 files changed, 267 insertions(+), 6 deletions(-) Fusion-Task-Id: FN-8730 Fusion-Task-Lineage: 8e2d81bc-37f1-4381-8985-01e83cad06bd Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
f21c07929a |
fix(dashboard): align mid-tablet shell chrome with JS viewport mode
Tablet-class devices at ≤768 CSS px were mode=tablet (left sidebar on, no MobileNavBar) while pure max-width:768 rules hid the sidebar and elevated the footer for a phantom tab bar. Publish data-viewport-mode and key shell CSS off it so tablet keeps the left sidebar and a true bottom footer. |
||
|
|
4fb873f465 |
FN-8722: fix GitHub Import sheet spacing
Fix standalone GitHub Import content alignment on mobile and short sheet viewports. - Remove the standalone resize-handle gutter when the FloatingWindow sheet contract applies. - Add fixture and browser-smoke coverage for standalone, embedded, and detail import layouts. - Document the shared short-height sheet geometry behavior and add a patch changeset. Files changed: .changeset/fn-8722-github-import-mobile-spacing.md | 7 ++ docs/dashboard-guide.md | 3 +- .../__tests__/browser-layout-smoke-fixture.test.ts | 17 +++++ .../dashboard/app/components/FloatingWindow.css | 12 ++++ .../__tests__/GitHubImportModal.test.tsx | 18 +++++ .../dashboard/scripts/browser-layout-smoke.mjs | 78 ++++++++++++++++++++++ 6 files changed, 134 insertions(+), 1 deletion(-) Fusion-Task-Id: FN-8722 Fusion-Task-Lineage: cc812078-b0b5-4223-9435-ed77d80f2b47 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
1e67e87321 |
FN-8702: fix Git Manager phone sheet spacing
Make Git Manager use the standalone phone layout only below the 768px boundary. - Remove the hidden resize-handle gutter from Git Manager phone sheets - Preserve embedded, tablet, and desktop Git Manager geometry - Add responsive layout smoke fixtures and regression coverage - Document the phone-sheet behavior and add a patch changeset Files changed: .changeset/fn-8702-git-manager-mobile-spacing.md | 7 + docs/dashboard-guide.md | 5 +- .../__tests__/browser-layout-smoke-fixture.test.ts | 23 ++++ .../dashboard/app/components/FloatingWindow.css | 12 ++ packages/dashboard/app/components/ScriptsModal.css | 10 +- .../components/__tests__/GitManagerModal.test.tsx | 31 +++-- packages/dashboard/scripts/browser-layout-smoke.mjs| 143 +++++++++++++++++++++ 7 files changed, 211 insertions(+), 20 deletions(-) Fusion-Task-Id: FN-8702 Fusion-Task-Lineage: 52473a8a-d3a7-48d2-b768-fc2f6fcd18b9 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
7dc6c4b4ec |
FN-8711: preserve credential instances in settings actions
Preserve selected credential-instance identity across dashboard authentication actions. - Route named-account OAuth and API-key actions with their instance IDs. - Retain sibling credential accounts during targeted OAuth polling. - Document the default-versus-named credential contract and extend desktop/mobile coverage. Files changed: ...n-8711-settings-credential-instance-contract.md | 7 ++ docs/settings-reference.md | 2 +- .../app/__tests__/settings-moved-keys.test.ts | 2 + .../dashboard/app/components/SettingsModal.tsx | 49 +++++++++++--- .../__tests__/SettingsModal.models-auth.test.tsx | 78 ++++++++++++++++++++++ .../components/__tests__/settings-mobile.test.tsx | 14 ++-- .../settings/sections/AuthenticationSection.tsx | 15 +++-- .../settings-default-descriptions.test.tsx | 19 ++++++ 8 files changed, 165 insertions(+), 21 deletions(-) Fusion-Task-Id: FN-8711 Fusion-Task-Lineage: f43eaa27-4d36-4a3d-acf3-caba54e31857 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
52310c9b65 |
FN-8698: test independent Board and List task popups
Add regression coverage for independently retained task popups across Board and List views. - Exercise popup identity, refresh, visibility, and close behavior for the same task. - Cover real desktop and mobile Board/List interaction paths. Files changed: .../app/__tests__/App.taskPopupViewGating.test.tsx | 69 ++++++++++++- .../app/components/__tests__/App.test.tsx | 113 ++++++++++++++++++++- 2 files changed, 176 insertions(+), 6 deletions(-) Fusion-Task-Id: FN-8698 Fusion-Task-Lineage: 857db4cb-0dd5-45ea-a7b7-9cb95f6f5fef Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
c0ddadd746 |
FN-8634: balance Task Detail and terminal right padding
Balance perceived right-edge spacing across Task Detail and terminal shells. - Move content padding outside scrollbar-owning Task Detail surfaces. - Account for injected terminal viewport scrollbar tracks across modal variants. - Add layout regression coverage and a patch changeset. Files changed: .changeset/fn-8634-task-detail-right-padding.md | 7 + .../__tests__/detail-body-mobile-overflow.test.ts | 8 +- .../__tests__/task-detail-inset-symmetry.test.ts | 271 +++++++++++---------- .../dashboard/app/components/SessionTerminal.css | 16 +- .../dashboard/app/components/SessionTerminal.tsx | 14 +- .../dashboard/app/components/TaskDetailModal.css | 49 ++-- .../dashboard/app/components/TaskDetailModal.tsx | 48 ++-- .../dashboard/app/components/TerminalModal.css | 11 +- .../components/__tests__/TaskChangesTab.test.tsx | 23 +- ...etailModal.responsive-and-dependencies.test.tsx | 16 +- 10 files changed, 273 insertions(+), 190 deletions(-) Fusion-Task-Id: FN-8634 Fusion-Task-Lineage: 58cce580-5da0-47cf-a325-20b3707dbeef Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
ad15113872 |
test(dashboard): pin the typed-title regression MY archived-lane fix caused (#3290)
#3286 fixed a real user-facing regression and **shipped no test**, so nothing stops it returning. The regression was mine. ## The bug #3215 (mine) added `isArchivedColumn` to an effect's dependency list to keep the task fetch honest. That effect **also owned four `setState` calls**, and `isArchivedColumn` is a `useMemo` over `useBoardWorkflows()` — which revalidates asynchronously. Every revalidation re-ran the reset over whatever the operator had typed. A title entered before the workflows settled silently reverted to `Research: <heading>`, and the task was created with a title nobody wrote. ## Why my own four tests could not see it Every existing case in this file asserts the **filtered task list** — render, await `fetchTasks`, read the datalist. **None types into the form.** I tested what I added and not what I touched. That is why the regression belongs in this file rather than a new one: the gap is this file's. ## The case Renders with `boardWorkflows: null` — the state when an operator opens the modal and starts typing — types a title with per-character `userEvent`, then rerenders with a resolved workflow set (a **new object identity**, which is the entire mechanism) and asserts the typed text survived. Two details that each cost a cycle, recorded at the site: - **`fetchTasks` is not awaited.** It runs only in enrich mode, while the title field exists only in create mode — so the reset effect, not the fetch, is under test. My first version waited on it and failed for the wrong reason. - **`userEvent.type`, not `fireEvent.change`.** The documented failure is state overwritten between renders; a single synthetic change event can land after the reset and mask it. ## Measured both directions, on main `6834ba35bd` | state | result | |---|---| | fixed main | **5 passed** | | dependency re-added to the reset effect (my bug) | **1 failed / 4 passed** — and only that case | The second row is the point: it fails on precisely the mutation that recreates the defect, and leaves the four archived-lane cases green — so it pins the regression without duplicating what is already covered. `eslint` clean, `check-fnxc-future-dates` 0. Test-only. ## Note on provenance Getting this measurement took three attempts: a `git checkout` of the PR branch silently failed (stderr suppressed), so I twice ran against the wrong tree and nearly concluded the fix did not work. HEAD and dirty-count are printed beside every number above for that reason. |
||
|
|
95410b5de6 |
FN-8638: add Factory Light dashboard theme
Add a daylight industrial theme that persists across dashboard and desktop startup. - Register Factory Light in persisted theme types, selectors, and bootstrap validators. - Define Factory Light tokens and preview swatches for light and dark modes. - Cover theme registration and rendered token contracts, and document the new option. Files changed: .changeset/fn-8638-factory-light-theme.md | 7 ++ docs/dashboard-guide.md | 3 +- docs/settings-reference.md | 2 +- packages/core/src/types/execution-and-ui.ts | 2 + .../app/__tests__/factory-light-theme.test.ts | 106 +++++++++++++++++++++ .../dashboard/app/components/ThemeSelector.css | 14 +++ .../components/__tests__/ThemeDropdown.test.tsx | 2 +- .../components/__tests__/ThemeSelector.test.tsx | 2 +- .../__tests__/CommandCenterControls.test.tsx | 2 +- packages/dashboard/app/components/themeOptions.ts | 1 + packages/dashboard/app/index.html | 2 +- packages/dashboard/app/public/theme-data.css | 86 ++++++++++++++++- packages/desktop/src/renderer/index.html | 1 + 13 files changed, 223 insertions(+), 7 deletions(-) Fusion-Task-Id: FN-8638 Fusion-Task-Lineage: 3b78bc31-0f03-4299-8f5f-1a69ac7c604a Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
bcaa48390b |
FN-8627: add Sage color theme
Add the Sage palette across persisted dashboard and desktop theme selection paths. - Register Sage in core, dashboard bootstrap, desktop, and selector metadata. - Add dark and light Sage tokens plus independently resolvable swatches. - Cover registration, token, selector, and documentation updates. Files changed: .changeset/fn-8627-sage-theme.md | 7 ++ docs/dashboard-guide.md | 3 +- packages/core/src/types/execution-and-ui.ts | 2 + .../dashboard/app/__tests__/sage-theme.test.ts | 101 +++++++++++++++++++++ .../dashboard/app/components/ThemeSelector.css | 14 +++ .../components/__tests__/ThemeDropdown.test.tsx | 2 +- .../components/__tests__/ThemeSelector.test.tsx | 2 +- .../__tests__/CommandCenterControls.test.tsx | 2 +- packages/dashboard/app/components/themeOptions.ts | 1 + packages/dashboard/app/index.html | 2 +- packages/dashboard/app/public/theme-data.css | 86 +++++++++++++++++- packages/desktop/src/renderer/index.html | 1 + 12 files changed, 217 insertions(+), 6 deletions(-) Fusion-Task-Id: FN-8627 Fusion-Task-Lineage: fd4353b3-1e0c-4c7e-84dd-bcad2815178c Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
f86d758f9b |
FN-8630: balance Task Detail scrollbar insets
Keep Task Detail content symmetrically inset when its body scrolls. - Reserve stable scrollbar gutters on both inline edges of the scrollable detail body. - Add deterministic coverage for modal, pop-out, and embedded detail inset symmetry. - Publish a patch changeset for the layout correction. Files changed: .changeset/fn-8630-task-detail-right-padding.md | 7 + .../__tests__/task-detail-inset-symmetry.test.ts | 222 +++++++++++++++++++++ .../dashboard/app/components/TaskDetailModal.css | 15 ++ 3 files changed, 244 insertions(+) Fusion-Task-Id: FN-8630 Fusion-Task-Lineage: 9fd0c2bd-3370-4f86-ad12-9f06e5172c5b Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
24ef266e48 |
FN-8628: add Factory Dark dashboard theme
Add a low-light industrial dashboard color theme with first-paint support and release documentation. - Register Factory Dark across persisted theme types, selector metadata, and desktop/dashboard bootstrap validators. - Define dark and light Factory Dark tokens, swatches, and selector styling. - Cover theme registration, tokens, bootstrap behavior, and UI theme-option counts. - Add a minor @runfusion/fusion changeset and document the theme. Files changed: .changeset/fn-8628-factory-dark-theme.md | 7 ++ docs/dashboard-guide.md | 3 +- docs/settings-reference.md | 2 +- packages/core/src/types/execution-and-ui.ts | 2 + .../app/__tests__/factory-dark-theme.test.ts | 106 +++++++++++++++++++++ .../dashboard/app/components/ThemeSelector.css | 14 +++ .../components/__tests__/ThemeDropdown.test.tsx | 2 +- .../components/__tests__/ThemeSelector.test.tsx | 2 +- .../__tests__/CommandCenterControls.test.tsx | 2 +- packages/dashboard/app/components/themeOptions.ts | 1 + packages/dashboard/app/index.html | 2 +- packages/dashboard/app/public/theme-data.css | 86 ++++++++++++++++- packages/desktop/src/renderer/index.html | 1 + 13 files changed, 223 insertions(+), 7 deletions(-) Fusion-Task-Id: FN-8628 Fusion-Task-Lineage: 6f3c7cd9-0130-482d-8aa8-ca47d48b134f Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
f0a13745b2 |
fix(test): lazy-views doc parser stops at any heading — six phantom views came from the H2 that follows
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
0bdc9bf4fb |
fix(dashboard): archived tasks stayed in the research picker on a renamed board (#3215)
## The defect The enrich-mode task picker filtered with `task.column !== "archived"`. On a board whose archive lane is renamed, that matched nothing — so filed-away tasks stayed in the picker and an operator could attach research findings to work they had deliberately archived. ## Census before / after | | before | after | |---|---|---| | COLUMN guards (backlog) | 10 | **9** | | `ResearchTaskActionModal.tsx` | 1 | **0 — converted** | Baseline re-recorded in the same commit; `--strict` green. ## This site was declined twice, and I wrote the second wrong estimate #3213 left it counted, correctly, on the note that was here — which was mine. Both prior cost estimates were wrong, so this corrects my own work: 1. **"Needs a data-fetch change"** — reasoned about `columnFlagsByTaskId`, a per-**task** map built from board-resident rows. Right that such a map can't help (archived rows are exactly what a board map omits), but this guard asks a per-**column** question, so it never needed one. 2. **"Needs prop threading, MainContent → ResearchView → here"** — right that the answer is column-keyed, wrong about where it lives. `ListView` builds `columnFlagsById` *inline*, which made it look like the owner. The data is `useBoardWorkflows`, a hook already called from `App`, `Board`, and `HeaderWorkflowSwitcherSlot`. **Measured cost: one file.** The modal already takes `projectId`, and `ResearchView` renders it only when a finding is open (`open` hardcoded beside `if (!finding) return null`) — so the hook cannot fetch for a closed modal, which was the one real objection to calling it here. Union across workflows keyed by column id, first declaration wins — the same convention `ListView` uses, so the two cannot disagree about a shared id. `isArchivedColumnRole` fail-softs to the legacy id when a column has no flags, so an unresolved workflow behaves exactly as the literal did. ## Tests — the invariant, not the repro Per the surface-enumeration rule, four cases: renamed archive lane, legacy id, unresolved workflow (fail-soft), and a second workflow's archive lane through the cross-workflow union. A repro-only test would pass on the legacy board and prove nothing about the case the guard exists for. **Anti-vacuity control:** | | renamed lane | union | legacy id | fail-soft | |---|---|---|---|---| | pre-fix literal | **FAIL** | **FAIL** | pass | pass | | converted | pass | pass | pass | pass | The legacy and fail-soft cases hold in both directions **on purpose** — they pin that this conversion did not change the pre-resolution answer. Flagging that so 4/4 isn't read as four independent proofs. ## Measured | check | result | |---|---| | `census --strict` / `check-fnxc-future-dates` | exit 0 / exit 0 | | `eslint` | clean | | `tsc -p tsconfig.app.json` (the config that actually covers `app/`) | exit 0 | | new tests | 4/4 | | `pnpm test:gate` | exit 0 (744 tests) | ## Note on process My first attempt at the control silently did nothing — the revert script threw a `SyntaxError`, so the "pre-fix" run was the fixed code and reported 4/4. Caught it because the error printed. The table above is from the re-run. |
||
|
|
d541c3154e |
fix(dashboard): retire the dead .agent-detail-overlay CSS (#2915) (#2985)
Closes #2915. `.agent-detail-overlay` has had **no renderer** since FN-8619 moved Agent Detail onto `FloatingWindow`, whose `modal` host owns the scrim (`.floating-window-overlay--modal`). Four CSS sites plus one inert mobile `@media` rule. ## Why this sat unfixed, and what unblocked it I filed this earlier and deliberately did **not** delete the CSS, because two *passing* guards pinned a selector list naming the class: ``` dashboard-overflow-containment.test.tsx:295 mobile-horizontal-pan-containment.test.ts:90 ".modal-overlay:not(.confirm-dialog-overlay),\n .agent-detail-overlay,\n .agent-dialog-overlay,\n .workflow-output-modal-overlay" ``` Neither list mentions `.floating-window-overlay--modal`. If that list were the mechanism keeping modal scrims inside mobile horizontal-pan containment, then FN-8619 moved every migrated modal's scrim out from under the guard and the dead entry was **masking a live defect** — deleting it would have been the wrong move twice over. I said at the time I couldn't settle it without rendering at a phone breakpoint. **That was wrong — it is answerable by reading**, and I only went back because the same mistaken conclusion cost me a day on the Planning coverage in #2982. **The containment lockdown is global**, on the mobile `html, body` block in `styles.css`: ```css @media (max-width: 768px) { html, body { overflow-x: hidden; overscroll-behavior-x: none; touch-action: pan-y; } ``` `FloatingWindow.css` says so itself at its mobile breakpoint — *"Mobile keeps the global `styles.css` pan-y lockdown so the dashboard cannot drift"*. The per-overlay list only reasserts it for overlays that are themselves scroll containers. Migrated modals are covered by the global rule, so **no hole, and no masked defect**. ## Verified the guards still guard something The risk in editing a pinned selector string is turning a real guard into a string-equality formality: | state | result | |---|---| | after this change | 15/15 pass | | global lockdown broken (`touch-action` / `overscroll-behavior-x` removed from `html, body`) | **2 failed / 13 passed** | They fail on the mechanism, not the text. ## Scope note Two of the four `styles.css` sites are **grouped selectors shared with `.agent-dialog-overlay`, which is still live** (`NewAgentDialog.tsx:415`). So this is a selector-list edit, not a block deletion — easy to get wrong in a bulk sweep, which is why it is called out here and in the FNXC note replacing the deleted rule. The retired mobile rule set `padding: 0; align-items: stretch` on the overlay. Not a lost feature: `.floating-window-overlay` is `position: fixed; inset: 0` with no flex context, so those declarations had nothing to act on — FloatingWindow positions the panel by geometry. **Verified:** 120/120 across `agent-modals-mobile`, `core-modals-mobile`, `AgentDetailView.core`, and both containment guards; `tsc -p tsconfig.app.json` 0 errors; lint clean; FNXC gate exit 0. Dead-CSS removal with no behaviour change, so no changeset. Main health while I was here: engine **11475 passed / 0 failed**. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
4f929acc10 |
fix(dashboard): stop over-aggressive component unmounts (keep-alive for planning, terminals, popups) (#2420)
Implements
docs/plans/2026-07-22-001-fix-dashboard-remount-churn-plan.md: every
confirmed source of unnecessary unmount/remount churn in the dashboard,
plus a keep-alive layer for conversation- and terminal-bearing surfaces.
## What changed
**Keying / component identity (U1–U3)**
- Streaming chat segment key no longer embeds `entries.length` — an
expanded thinking block stays expanded while entries stream into it
(R1).
- Dock task list keys `TaskCard` rows by `task.id` (occurrence suffix
only for the duplicate-id anomaly) instead of `id-index` — no remount on
reorder/filter/status change (R2).
- `ProviderStatusBadge` / `GitHubStatusBadge` hoisted out of
ModelOnboardingModal's render body (R3); MCP server rows key by
`server.name` alone (R4).
**Keep-alive layer (U4–U6)**
- New shared `KeepAliveView` wrapper: visible = in-flow flex child;
hidden = out-of-flow `position:absolute; inset:0` with
`visibility:hidden; pointer-events:none` + `aria-hidden` (never
`display:none`, so xterm geometry never collapses).
- Planning Mode renders as a kept-alive sibling of the MainContent
switch after first open (per-project latch mirroring Quick Chat). While
hidden, the session-list SSE, recovery poll, and elapsed ticker suspend
via a new `active` prop; reveal re-subscribes and refreshes the sessions
list once. Payload-carrying entry points (initial-plan handoff, resume)
and project switches remount via a new
`modalManager.planningEntryGeneration` key, preserving pre-keep-alive
fresh-open semantics. `recordResumeEvent` instrumentation records
`remount` on first activation and `route-active` on reveal.
- Task-detail Terminal / Worktree-terminal / Planner-chat tabs stay
mounted-but-hidden after first open (per-task latches; task switch/close
still disposes fully). `SessionTerminal` gains `active`: reveal refits +
forces a font remeasure, and if the WS died while hidden it re-runs the
full attach lifecycle (dead-socket recovery).
- Popped-out task windows hide via FloatingWindow `hidden` instead of
leaving the render array; `TaskDetailContent` gains `active` so hidden
popups close their SSE/EventSource channels while the terminal WS stays
open. `visiblePoppedOutTaskEntries` remains the Escape-shortcut
consumer.
**Planning Mode internal-transition audit (U7)**
- Audit findings: session-list mode and mobile list/detail flips are
CSS-class transitions over one always-mounted detail pane (no
state-discarding unmounts); re-selecting the active session is an
early-return visibility restore; session switching intentionally reloads
from the session row (stream re-attach for generating sessions);
remaining index keys are on stateless lists. No product-code defects
found; regression tests now lock the always-mounted invariant on desktop
+ mobile.
**Cheap-view state (U8)**
- CommandCenter (active sub-tab + date range) and DevServerView
(selected script/task + typed-but-unsent command) persist per project
via `modalPersistence` and restore after their (intentional) unmount
round-trips. Also fixed the candidate auto-fill effect clobbering a
customized non-empty command.
## Symptom Verification
- **Original symptom:** streaming thinking blocks collapsed mid-stream;
terminals reconnected and lost scroll/input on tab flips; Planning Mode
lost in-flight interviews on navigation; popped-out windows vanished
off-view; dock cards remounted on reorder.
- **Exact reproduction:** (1) expand a thinking block during a stream;
(2) run a command in the Terminal tab, flip to Plan and back; (3) start
a planning interview, navigate Board and back; (4) pop out a task with
board/list-only scoping and switch views; (5) change a dock task's
status.
- **Assertion it is gone:** component-identity/instrumentation tests in
TaskChatTab, SessionTerminal, TaskDetailModal
(worktree/planner-chat/tabs), PlanningModeModal keep-alive +
internal-transitions, App keep-alive round-trip, and
App.taskPopupViewGating assert no remount and preserved state for each
repro, across desktop and mobile breakpoints.
## Verification
- File-scoped vitest: 23 files / 1091 tests green (all touched suites
plus FloatingWindow, TerminalModal, TaskPlannerChatTab,
lazy-loaded-views guard, App suites).
- `pnpm verify:fast`: PASS (13 steps — scoped typecheck/build, CLI
build, boot smoke).
- `pnpm check:changesets`: passes; changeset
`fix-dashboard-remount-churn` (`@runfusion/fusion` patch, labeled
format).
- Known pre-existing failures NOT caused by this branch (verified
failing at base
|
||
|
|
ba40942a10 |
batch-dashboard-app: 75 → 2 across packages/dashboard/app — the last two are deliberate, not missed (#2772)
**Batch branch is live: `batch-dashboard-app`.** Push conversions here as commits rather than opening per-file PRs — that is the CI-run bottleneck this model removes. **One-line ownership note for you to arbitrate:** you have addressed me as U11, U12 and U7 at different points, so the `u12 worker -> batch-dashboard-app` mapping is ambiguous from my side. I claimed it because `dashboard/app` is where I have done the most work this session (TaskContextMenu, Column, TaskCard, TaskDetailModal, columnRoles, taskActivity) and I know which of its guards are load-bearing fallbacks. **If another worker is the intended owner, say so and I will hand the branch over rather than both of us pushing to it** — two workers on one shared branch is exactly what silently discarded a reviewed fix in #2645 today. ## The work order (measured at branch point, tests excluded) **75 guards across 32 files.** Largest: `TaskContextMenu.tsx` 9 · `Column.tsx` 7 · `ListView.tsx` 6 · `TaskDetailModal.tsx` 4 · then a long tail of 3s, 2s and 1s. Full per-file list is in the committed work order so feeders can claim without re-measuring. ## Two rules this surface keeps tripping on **1. A literal after `??`, or in the `else` of a `flags ?` ternary, is a DEGRADED-MODE answer — not an unconverted guard.** Two real states reach it: the **pre-load window** (board renders before the workflows fetch resolves) and a card stranded on an id its workflow no longer declares. In both, `columnFlagsById` has no entry at all. Deleting the fallback does not remove a decision — it substitutes "no role" silently, and affordances vanish during first paint. Those sites reach 0 by **marking**, not deleting. Expect `TaskContextMenu.tsx` and the `utils` files to be **mostly marks**. A "9 → 0" that deleted 9 fallbacks is a regression wearing a green census. **2. A marker excuses ONLY the construct it is attached to** — the statement or function holding the literal, not a sibling declaration. This has cost three passes, two of them mine; my first attempt on `reliability-metrics.ts` scored **1 of 6**. **Verify by the count moving, not by the comment existing.** With the ratchet gate-blocking, a mis-marked batch either wedges the gate or locks the miss into a re-recorded baseline. ## Status Opening commit is the work order only — **0 of 75 converted so far.** I am near the end of my context, so I am establishing the branch and the shared list rather than starting conversions I cannot finish cleanly. Feeders can begin immediately; I will keep the branch rebased. My other PR **#2762** (`live-agent-count.ts` 6 → 0) is green and unconflicted — per your rule it should land rather than fold into a batch, and it is `packages/core` so it belongs to batch-core anyway. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Task UI now resolves workflow “column roles” per task to drive diffs/merge details, routing/steering, progress/runtime visibility, and review badges. * Right-dock/overflow views and dev-server now use per-task column traits for “executing” behavior and dependency-based “Up Next” eligibility. * **Bug Fixes** * Fixed bulk action selection/delete/archive eligibility and prevented cross-workflow role leakage. * Made in-review/stale-paused-review, stuck, and effective executor/validator model logic role-aware. * **Tests** * Added regression coverage for degraded-flag behavior and ensured resolved-flag props aren’t ignored. * Added a static check to fail builds on inert optional flag seams. * **Documentation** * Updated batch work-order and mega-batch branch guidance. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --- ## Late addition: the seam gate was masking a real offender `scripts/check-inert-flag-seams.mjs` matched call sites by NAME, so two same-named functions in different modules were conflated. I had documented that as a known false-positive source and moved on — reports mentioning `sortTasksForDisplayColumn` are noise, read past them. That annotation was the damage. Core's `sortTasksForDisplayColumn` genuinely never receives its `columnFlags` argument outside its own tests. The dashboard's separate function of the same name (`app/components/taskSorting.ts`), called with up to five arguments from `Lane`/`Board`/`ListView`, was raising the arg-count max and clearing core's seam. The offender was behind a row everyone had been told to skip. The gate now records the module each callee is imported from and matches it against the seam's declaring module. **Measured, by reverting the change:** the scan prints `17 seams, all supplied` and emits **no row** for the function. With the change, it is reported. Both directions watched. Reported on #2783 rather than fixed from outside — core owns it, and "wire the flags" vs "drop the parameter and let the literal stay counted" is their judgment call. TEMPORARY allow-list entry carries it meanwhile; the existing staleness check fails the moment the site becomes supplied, so the entry cannot outlive the fix. Two known limits remain, both inherent to name matching and both documented in the script: the one-supplier floor, and the `__tests__` exclusion (hence the two permanent `ALLOWED` entries). ## And the one-supplier floor, closed the same way I wrote in the section above that the floor "hasn't cost anything yet." That is verbatim the reasoning that kept the imported-shadow bug alive, so I closed it instead of leaving the note. `best < arity` asked only whether SOME caller supplied the argument. One correct call site cleared the seam while every sibling took the legacy fallback — the `isTaskStuck` defect class, where two of three sites omitted the flags and the gate stayed green because the third was right. Review caught that one. A partially-supplied seam is the harder of the two: wholly-unsupplied is uniformly wrong, this works on the board you tested and degrades on the column you did not. **Measured:** dropping the flags argument at `Column.tsx`'s supplied call site produces `supplied by 5/6 call sites; omitted at packages/dashboard/app/components/Column.tsx:1 (of 2)`; restoring returns `all supplied at every call site`. Red and green both watched. Two real omissions found, both on `isNearDuplicateCanonicalInactive`: - **`TaskDetailModal.tsx`** — deliberate, and it **corrects a note I left at that site**. The old note said hoisting the flags state was "the actual fix." It is not, for this call: the flags in scope describe the *modal's* task, and the canonical is a **different task** on a column this component never resolves. Passing them would type-check, read as a conversion, and answer about the wrong task — exactly what `column-role-degraded-flags.test.ts` exists to catch. Supplying it correctly needs a fetch, which is a data change and out of scope. - **`core/task-store/branch-group-ops.ts`** — genuinely wireable (the impl is async and already holds `store` and `canonicalId`). Reported on #2783, not edited from outside. Exemptions for this class are keyed by **call site** (`<file>::<function>`), not by function name. A name-level entry would waive every site of a partially-supplied seam, which is backwards — its other sites are correct and are the reason the omission is worth reporting. Both entries carry the same staleness check as the name-level list and cannot outlive their fix. Remaining known limit, now the only one: the `__tests__` exclusion, which makes a test-only export read as having no callers. That is what the two permanent `ALLOWED` entries are. ## The `__tests__` exclusion, and two allow-list entries built on false reasons Named as the "last remaining limit" above, so it got closed too. The scan now reads test files for call sites — but counts them **separately**, and a test never clears a seam. That direction is the dangerous one: counting test callers as suppliers would have re-hidden core's `sortTasksForDisplayColumn`, whose only suppliers are its own tests. Measured by lifting its exemption: still reported. Both permanent allow-list entries claimed the scanner couldn't see their callers. **Both reasons were false**, and reading tests is what proved it: - **`evaluateMergeBlockerGuard`** — zero callers in tests either. Its only reference in the repo is its own declaration; never registered as a trait hook; the `evaluateDefaultWorkflowGuards` reader its file header credits does not exist. The `lifecycleColumns` conversion went onto dead code, and its note describes a crossing the guard cannot make. Reported on #2783, including the two things I am explicitly *not* concluding (no `"guard"` hook is registered in production; whether that is residue or a dropped registration needs core's intent). - **`isRecoverableMissingWorktreeReviewFailure`** — 5 test call sites. It wraps `...WithProgress`/`...NoProgress`, the live pair called from `self-healing.ts`, both supplying `reviewColumns`. Entry kept, true reason recorded. ### A wrong turn, recorded because it is the failure mode this PR is about I first classified no-production-caller seams as *informational* when they weren't re-exported from a package index, reasoning that a public export might be called externally. That silently downgraded `sortTasksForDisplayColumn` — a confirmed real offender — from failing to a footnote. Publication status has nothing to do with whether there is production behaviour to be wrong. Reverted to the simple rule: no production caller means inert, and it fails. It is worth stating plainly because it is the exact shape of everything else in this PR: a change that made the gate read *cleaner* while making it catch *less*, and it type-checked, passed every test, and would have reviewed fine. ### Where that leaves the check Every blind spot named in this PR has now been closed, and **each one produced a real defect within minutes of closing it** — imported shadows, the one-supplier floor, the `__tests__` exclusion. Four verified findings went to core, one to engine. I would not read the remaining ~240 guards' green gates as evidence that they are clean; I would read them as untested. ## Two guards for one question, one of them worse Having hardened the script, I checked its older twin rather than assuming it was fine. `resolved-flags-seams-have-suppliers.test.ts` carried its own copy of the trailing-flags-parameter check — written before the script existed — with **all three** holes the script has since closed. **Measured on one reintroduced defect** (dropping the flags argument at `Column.tsx`'s supplied `isNearDuplicateCanonicalInactive` call): | | result | |---|---| | `scripts/check-inert-flag-seams.mjs` | `supplied by 5/6 call sites; omitted at .../Column.tsx:1 (of 2)` | | this test's arity half | **3 passed** | Deleted the arity half. Redundancy between a strong and a weak check isn't redundancy — it's a green result available to whoever runs the weak one, and there was no signal at the call site telling you which you were looking at. The **props-shape half stays**: it has no twin in the script, and I confirmed it still fires by reintroducing the original `PrPanel` defect (outer component stops destructuring `taskColumnFlags`) — it reports `PrPanel declares taskColumnFlags but never takes it`. Dashboard app suite: **113 files / 3921 tests** (was 3922 — the deleted case is the difference). ## The gate started catching defects as they landed Syncing with main brought in three fresh conversions from other workers. The hardened check flagged all three immediately — the first time these guards have fired on someone else's landed code rather than on my own. - **`TaskCard`** — `getRunningOptionalGateBadge(task)` omitted flags while *both* `ListView` sites supplied. Fixed, and `taskColumnFlags` added to the `useMemo` deps: no `exhaustive-deps` rule here, so a memo that reads flags without listing them keeps the first-paint `undefined` answer and reproduces the bug through staleness instead of omission. - **`TaskTokenStatsPanel`** — `getTotalAgentActiveMs` omitted while `TaskCard` supplied, so the same runtime number came from the real column on a card and from legacy ids in the detail modal. Now takes `columnFlags`, supplied from `detailColumnFlags` — correct here because the panel renders the modal's **own** task, unlike the near-duplicate canonical above. - **`ListView` ×2** — passed `columnFlagsById.get(task.column)`, the cross-workflow **union**. A task whose own workflow doesn't declare that column gets a *neighbour workflow's* traits. The landed comment justified it as "this list already owns `columnFlagsById`" — exactly the reasoning `column-role-degraded-flags.test.ts` exists to reject. It failed on merge and is how I found this. Also: the `getTotalAgentActiveMs` exemption I was carrying **self-retired**. Main wired the seam, the staleness check failed the entry, and I removed it. That mechanism has now paid for itself once. ### Pre-existing, NOT from this PR: `App.test.tsx` is red on main `app/components/__tests__/App.test.tsx` fails **10 of 141** identically with my changes, with my changes stashed, and with main's own `App.tsx` restored. Not mine, and not in the merge gate. **Bisected on clean `main` checkouts, so this is measured rather than inferred:** | commit | date | result | |---|---|---| | `main~400` (`41d60f0355`) | 2026-07-25 | **140 passed** (140 tests) | | `main~275` (`74d6513fae`) | 2026-07-27 | 3 failed / 141 | | `main~210` (`d2ce1ba8b5`) | 2026-07-29 | 10 failed / 141 | | `main` (`6fc98fd6c7`) | 2026-07-30 | 10 failed / 141 | So it is **not one regression** — it degraded in two stages across 2026-07-25 → 07-29, and the test file itself changed in that window (140 → 141 tests). Three commits touched it there: `73b2a32e2b`, `f26cbedf4f`, `f157bf7460`. That window overlaps the workflow-owned lifecycle migration, which is suggestive but not something I confirmed. The failures are render-level, not assertion-level — `Unable to find an element with the text: + New Task`, `Unable to find role="dialog"`, `Unable to find ... Back nav task`. The board appears to render nothing. That reads like a real regression or a harness mismatch after the lifecycle migration, not a flake, so I have deliberately **not** quarantined it — quarantine is for flakes, and using it here would hide the signal. Flagging for whoever owns `App.tsx`. My suites: `app/__tests__` **113 files / 3921 tests** green, `tsc` 0, lint 0, census `--strict` 0, seam gate 0. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2a293ee1e0 |
fleet: TaskDetailModal.tsx 30 -> 7 (#2698)
## Census before / after | | before | after | |---|---:|---:| | `TaskDetailModal.tsx` | **30** | **7** | | repo backlog | 721 | **698** | Backlog dropped by **23** — the converted count, nothing else moved. **Not 30 → 0.** The seven survivors are enumerated below with reasons rather than absorbed into the number. ## Converted (23) Four role bindings declared immediately after `workflowMoveMetadata` — their source — and 20 in-component comparisons collapsed onto them. Two module-scope helpers (`resolveDefaultTab`, `requiresExecutionModeReplan`) take a bare column id with no flags in scope, so they use the fallback-only form. That is **centralisation, not trait resolution**, and each is labelled as such at the site so it stays greppable as "still needs its flags threaded". ## Not converted (7), each with a reason | count | site | why | |---:|---|---| | 2 | `showNearDuplicateWarning` (~881) | Sits **above** `workflowMoveMetadata`, so referencing the role bindings is a temporal-dead-zone error. Needs the state declaration hoisted — behaviour-safe, but it reorders hooks in a 6500-line component, which is not a vocabulary edit. | | 2 | two `useEffect` dep arrays (~930, ~936) | Same problem, subtler: the callback *bodies* could reference the bindings, but a **dep array is evaluated eagerly** at the hook call, which is above the declaration. Same hoist. | | 2 | `overlapBlockerTask.column` (~3559) | A **different task's** column. The modal holds flags for its own card only; resolving the blocker's role means fetching its flags. Out of scope. | | 1 | `session.agentState === "done"` (~353) | An **agent state, not a column**. Already reclassified in #2692 — this file drops to 6 counted when that lands. | ## Late-arriving flags — checked, not assumed `currentColumnFlags` is `null` until the workflow fetch resolves, so every role here flips after first paint. That is the hazard that produced four stale memos in `TaskCard` (#2688 review), so I ran an AST pass over every `useMemo` / `useEffect` / `useCallback` in the file. **None needed a new dependency** — the 20 converted sites are all render-path expressions rather than memoised closures. Worth stating explicitly, because "no dep changes" in a conversion PR usually means nobody looked. ## Verification `pnpm test:gate` green (10 / 158 / 487 / 71). `pnpm check:lifecycle-columns` exits 0 with the baseline re-recorded here. `tsc -p tsconfig.app.json` clean. `pnpm lint` clean. Targeted `TaskDetailModal` suites pass (5/5). Note: the full `TaskDetail*` glob exceeds a 10-minute run locally, so I verified with the targeted suites plus typecheck rather than reporting a number I did not measure. No changeset: no user-visible change. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c6c947c1c2 |
test(fleet): prove resolved traits beat a matching legacy id (#2694)
Review finding on #2688, landing separately because the helper set merged in #2685 (so the test file is on `main`) and #2688's branch is checked out in another worktree. ## The gap My false cases for the four new role helpers all passed a **non-matching** id: ```ts expect(isCompleteColumnRole(undefined, "shipped")).toBe(false); ``` A broken `trait || legacyId` implementation satisfies that just as well — no trait **and** no id match, so `false` either way. Those cases prove the fallback fires. They do not prove **precedence**. ## The discriminating shape Trait says `false`, id says `true`: ```ts expect(isCompleteColumnRole({ complete: false }, "done")).toBe(false); expect(isArchivedColumnRole({ archived: false }, "archived")).toBe(false); expect(isWipColumnRole({ countsTowardWip: false }, "in-progress")).toBe(false); expect(isReviewColumnRole({ mergeBlocker: false, humanReview: false }, "in-review")).toBe(false); ``` Flags-first returns `false`; an OR returns `true`. That is the only shape separating the two implementations, and it is the one that matters in production — **a resolved column whose trait is explicitly off must not be overridden by its name.** **Mutation-verified:** rewriting `isCompleteColumnRole` as `flags?.complete === true || columnId === LEGACY_COMPLETE_COLUMN_ID` now fails with `expected true to be false`. Under the previous cases it passed. ## Worth naming This is the same one-sided-test defect I have been flagging in other people's work, in mine. The helpers were already correct — only the evidence was weak, which is the harder version to catch, because everything is green and the assertion *looks* thorough. It generalises to the fleet: any converted site tested only with "no flags, non-matching id" proves the fallback and nothing about precedence. ## Verification `columnRoles.test.ts` **14 → 15**. `pnpm test:gate` green (10 / 158 / 487 / 71). `pnpm check:lifecycle-columns` exits 0. `pnpm lint` clean. Test-only; no changeset. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e9e63d8e0f |
consolidate/capacity: --strict was red on main (my #2621), 14 stale baselines, routines seeding a deleted column, worktrees-off audit (#2652)
Capacity unit consolidation. Three coherent themes, small commits inside. ## Census before/after (`node scripts/lifecycle-column-census.mjs`) | | before | after | |---|---:|---:| | triage column guards (the bar) | 10 | **10** | | `--strict` on main | ❌ **RED** | ✅ green | | baseline staleness | 14 files stale | **0** | This branch does **not** move the triage bar — its remaining 10 are moves.ts (dies with the flag), the dashboard cluster, and one deliberate site. It fixes the instrument that measures the bar, plus a live defect the comparison count cannot see. --- ## 1. `--strict` was RED on clean `origin/main`, and it was my fault ``` packages/dashboard/src/routes/register-task-workflow-routes.ts: 22 -> 23 ``` My merged #2621 added a v1-IR pre-WIP fallback answering a greptile P1 and shipped no marker or baseline update, so the program's measuring instrument has been failing on main since it landed. Fixed **at the site** with a `DELIBERATE-LITERAL` marker, not by bumping the baseline. That branch runs only when the IR declares no columns and no nodes, so there is no role to resolve — `resolveLifecycleColumns` returns nothing and the legacy pre-implementation ids are the only pre-WIP signal that exists there. It is *unconvertible*, not unfinished; the sibling `else` two lines down is the trait path for every IR that can answer. A rise that is genuinely correct belongs where a reader will see it. ## 2. The baseline was stale for 14 files — a hole, not cosmetics A stale allowance lets converted guards return while the check stays green. Measured gaps: ``` self-healing.ts allows 126, tree has 111 executor.ts allows 112, tree has 104 moves.ts allows 44, tree has 39 default-workflow-hooks allows 25, tree has 7 mission-feature-sync allows 5, tree has 0 MissionControlPanel allows 4, tree has 0 (+8 more) ``` **Only two of the fourteen are mine.** The other twelve are already-merged conversions by other workers where nobody re-recorded. Re-recorded all fourteen here rather than waiting for twelve PRs, because until it happens the ratchet is not holding the 779 it exists to hold. Flagging it plainly: those drops are other people's work being locked in, not mine being claimed. ## 3. Routines created tasks into the column U11 deleted The routine editor's "Target Column" defaulted to `triage`. That value is submitted as the create step's `taskColumn`, and an **explicit** column bypasses the workflow entry-column resolution added for column-less creates (#2589) — so every routine saved with the untouched default seeded its tasks into a column the board does not declare. Defaulting to `todo` would be the same mistake one column over: a custom workflow declaring no `todo` is seeded into an undeclared column just as surely, because an explicit column overrides entry resolution whatever its value. So the default sends **nothing** and each workflow's own intake resolution decides. The `triage` **option** is removed too, not merely un-defaulted — fixing the initializer alone left the operator able to pick the deleted column one click away, and it was the option labelled "Planning", the name the merged `todo` column now displays. Removing it retires that label inversion as well. Found by scanning **membership** forms rather than comparisons: the comparison census cannot see a `?? "triage"` default, so no count showed this and nobody was looking. Revert-proof — restoring the default fails with *"the default must not name a column at all"*. ## 4. "Worktrees off is INERT" had one unaudited reader The constraint was that `maxWorktrees` become genuinely inert, "not set very high and not skipped by convention". `resolveWorktreeCapacityLimit` returns `null` for that, and its unit tests can only prove the **resolver** is right — they cannot see a second reader, which is the only way the constraint breaks. Audited every `maxWorktrees` read that bounds anything. **Exactly two:** `scheduler.ts` (the admission gate, via the resolver, single call site, optional gate snapshot) and `self-healing.ts`'s `enforceWorktreeCap` — `(settings.maxWorktrees ?? 4) * 2`, a **raw** read. The second is **not a bug** and is left alone: it bounds worktree *directories on disk* and only removes *idle* ones. Worktrees still exist in OFF mode, so that bound must keep applying or idle directories accumulate unbounded. Recorded consequence: in OFF mode the number still governs disk retention while gating no admission — an edge you scoped out. The note says explicitly **not** to unify the two readers: routing hygiene through the resolver returns `null` in OFF mode and silently removes the disk bound, which is a leak dressed as a simplification. New ratchet requires every file bounding on `maxWorktrees` to be named with a reason, and rejects a **stale** allowlist entry. Proven by injecting `active >= (settings.maxWorktrees ?? 4)` into `hybrid-executor.ts`. --- ## Deliberately NOT included - **My own census script.** #2633 landed the canonical one, and it is better than mine — an AST classifier *plus* an independent text classifier with `--compare`, and a baseline that fails on unrecorded **drops** as well as rises. Mine only caught rises. I deleted mine rather than ship a second measuring instrument; three copies of "strip comments" is the drift shape this program keeps paying for, so the worktree ratchet now imports #2633's `stripComments`. - **My TaskContextMenu fix.** Superseded, and by a better answer: main's `isPureIntakeColumn` (intake *without* hold) keeps the merged Planning column shown and suppresses only a bare Ideas capture, which resolves the exact hold-lane objection coderabbit raised against my version. I briefly clobbered that merged work by checking my old file out wholesale, caught it in the diff, and reverted. ## Verification `pnpm lint` clean · core + dashboard `tsc` clean · census suite 23/23 · worktree ratchet 8/8 · RoutineEditor 49/49 · `routes-task-retry-planning-column` 16/16 · `lifecycle-column-census --strict` exits 0. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- ## Added after review (all four greptile threads were real, and two of them mattered) **The routine fix was half a fix.** `routine-runner.ts:515` *and* `cron-runner.ts:982` both did `column: (step.taskColumn as Column) || "triage"` **after** the step is read, so every routine — including ones saved through the fixed editor — still created tasks into the deleted column. Both now omit it. **The advanced steps editor MANUFACTURED the defect.** `ScheduleStepsEditor.tsx` had three `triage` defaults: the new-step template (`:64`), the per-step initializer (`:95`), and the select still offering it (`:344`). So the path I had *not* fixed produced the bug by default, on fresh data. Template names no column; initializer coerces a persisted `triage`; `triage` removed from the options; empty submits `undefined`. **Four pre-existing tests pinned the defect** and are rewritten to the corrected invariant rather than appeased: | test | asserted | |---|---| | `cron-runner`: "defaults column to triage when taskColumn is not set" | `column: "triage"` | | `ScheduleStepsEditor`: "adds a create-task step..." | `taskColumn` toBe `"triage"` | | `ScheduleStepsEditor`: "allows saving create-task step..." | the legacy column is **resubmitted** | | plus the explicit-column case added beside each, so the fix cannot swallow a deliberate choice | **The allowlist hole was the worst finding.** `AUDITED_BOUNDS` was keyed by FILE, so every bounding expression in an allowlisted file was exempt — a second raw bound in `scheduler.ts` stayed green, the one case that ratchet exists for. Per-expression now, and making it so **immediately surfaced a real second bound the file-level version was hiding** (`maxWorktreesGate.used >= maxWorktreesGate.limit`, safe by construction since the snapshot is `undefined` in OFF mode). Proven by injection. ## Found while re-reading my own deletion, not reported A **rendered tooltip** still named a deleted cap. The "Queued to plan" badge read *"planning starts when a concurrency slot frees up (maxConcurrent / globalMaxConcurrent)"*. The cross-project cap is gone — capacity is two numbers per project — so it told operators their planning waited on a limiter they can no longer find a setting for. Names the surviving dimension only now. ## Coding (Ideas): enforcing #2651 rather than repeating it I took the unowned coding-ideas IR merge, concluded it must not be done, then found **#2651 had already implemented, reverted and documented exactly that** — with better grounding than my own argument. It added no test, so nothing stops the next person reaching the same dead end. So this ships their reasoning as a ratchet, not a second opinion: triage discovery keys on the column's `autoTriage`, so a merged column is either never scanned (cards sit on a bootstrap stub until the **capacity hold** releases them, sending **unplanned** work into in-progress — worse than stalling) or scanning wins and the manual gate is gone. Their scope caveat is kept: `autoTriage` is a general trait field, so only *this preset's* collapse is dead, not manual intake as a concept. The registry does not reject the merged shape, which is why prose was not enough. ## Verification (re-run) `pnpm lint` clean · core + engine + dashboard-app `tsc` clean · `lifecycle-column-census --strict` exits 0 ("every file matches its baseline exactly") · routine-runner 24/24 · cron-runner 156/156 · ScheduleStepsEditor 41/41 · RoutineEditor 49/49 · worktree + coding-ideas 12/12. TaskCard has 2 failures **pre-existing on main** — confirmed identical with my changes stashed. --- ## Bears directly on the closing bar: this PR already removes the 67-guard ratchet slack Measured on current `origin/main` with the census itself: ``` tree total: 787 baseline total: 854 SLACK: 67 FILES ABOVE BASELINE (1): +1 packages/dashboard/src/routes/register-task-workflow-routes.ts (22 -> 23) FILES BELOW BASELINE: 13, totalling 68 unrecorded conversions -18 core/default-workflow-hooks.ts (25->7) -15 engine/self-healing.ts (126->111) -8 engine/executor.ts (112->104) -5 core/task-store/moves.ts (44->39) -5 engine/mission-feature-sync.ts (5->0) -4 core/live-agent-count.ts (10->6) ``` **The slack is not regression — it is 13 files of merged conversions nobody re-recorded**, against exactly **one** rise. This PR re-records the baseline **854 → 782 across 140 files**, which closes it. **And the "+3 that slipped in" is +1, and it is mine.** `register-task-workflow-routes.ts 22 → 23` is the v1-IR pre-WIP fallback my #2621 added; it is justified (that branch runs only when the IR declares no columns or nodes, so there is no role to resolve) but it shipped with no marker and no baseline update — which is why `--strict` has been **red on main since it merged**. Fixed here at the site with a `DELIBERATE-LITERAL` marker rather than by bumping the baseline, because a rise that is genuinely correct belongs where a reader will see it. Sequencing note for the auto-lowering change: if this lands first, that work is purely the mechanism (auto-lower, or fail with tighten instructions) rather than a cleanup, and the two re-records will not collide in the same file. Also worth carrying into that mechanism, from building the same guard here: **`--update` must refuse to RAISE.** An earlier version of mine wrote current counts verbatim, so a developer who added a literal and ran the documented update command locked the regression in as the new ceiling — the mirror of the high-water problem. Lowering can be unattended; raising should be a hand edit with the reason recorded. ## Third piece of residue from my own deletion `updateGlobalConcurrency` in the dashboard API client PUT to `/api/global-concurrency`, a route removed when the machine-wide cap went. Zero callers; the only reference was the `legacy.ts` barrel re-export. Deleted both. `fetchGlobalConcurrency` **survives on purpose** — the GET route remains and serves live utilization telemetry to the footer and Command Center; nothing gates on it. That is the third: after the second raw `maxWorktrees` reader and the "Queued to plan" tooltip. A deletion is not finished when the enforcement goes — the client, the label and the tooltip outlive it. --- ## Re-greened the dashboard API tests: 117 failures on main, ONE root cause These would have polluted the closing verification pass, and nobody owned them. `api()` builds headers via `new Headers(...)` and returns `Object.fromEntries(headers.entries())` — and `Headers.entries()` **lowercases every key**, so the object reaching `fetch` is `content-type`, not `Content-Type`. `ab87d0d80` then added `x-fusion-client: dashboard-ui` for run-audit attribution. Both changes are correct; neither is visible at a call site, so **114 assertions across 7 files** kept asserting the old shape and went red together. Fixed by naming the shape **once** in `app/test/apiRequestHeaders.ts` rather than patching 114 literals — restating a shared fact 114 times is what made a two-line client change look like 117 failures. Deliberately not a loose `objectContaining`: these tests are the only thing pinning that the attribution header is sent *at all*. **117 → 4.** The remaining 4 are unrelated pre-existing CSS failures (`task-detail-modal-tablet-width` ×3, `space-token-defined` ×1) — confirmed identical on clean main with my changes stashed. ### A gap this surfaced, recorded not papered over Three routes failed in the *opposite* direction — they send the old shape because they call `fetch()` **directly**, bypassing `api()`, so they never get the attribution header. `client.ts` claims the opposite: > "Applied once here rather than per-call so no future mutation route has to remember it." That does not hold for a route that bypasses the helper it is applied in. **Measured in `app/api/`: 8 files make direct `fetch()` calls and 7 include mutations (POST/DELETE)** — among them `ai-sessions.ts`'s DELETE, which is the same class as the four-delete incident the header was added for. So the attribution fix has a hole in exactly its motivating case. Not fixed here: routing those onto `api()` is a behaviour change across the API layer and belongs to its owner, not to a test re-green. Those assertions use a separate `API_JSON_HEADERS_NO_ATTRIBUTION` constant so the gap stays **visible** — if a route is later moved onto `api()`, its test fails and points at the note explaining why. --- ## This branch takes the triage bar 10 → 5, and makes `--strict` green `node scripts/lifecycle-column-census.mjs` on this branch reports **triage 5**, against **10** on `origin/main`. The five removed are the ScheduleStepsEditor template/initializer/option and the RoutineEditor default/option — the automation paths that were creating tasks into the deleted column. **`--strict` was also RED on clean main, twice over, and both causes were the same mistake:** a thorough written rationale the tool cannot read, because the marker was not where the census looks. The census reads a comparison node's **leading comments**; a `DELIBERATE-LITERAL` in the JSDoc above the enclosing function or declaration does not reach the comparison inside it. | site | why it is legitimate | why the tool could not see it | |---|---|---| | `columnRoles.ts:80` `isHoldColumnRole` | degrades to `columnId === "todo"` only when a column has **no resolved traits** — identical in kind to `LEGACY_PRE_IMPLEMENTATION_COLUMN_IDS` directly above, which escapes counting only because a Set is a membership form | rationale written, **no marker token** | | `MissionControlPanel.tsx` ×3 | the SDLC funnel **alias table** — maps `to-do`/`ready`/`review`/`shipped` onto one display stage with an explicit `other` bucket, and nothing branches on it | marker in the JSDoc; the comparisons are arrow bodies **inside the array literal**, which it does not reach | The second only surfaced because converting the `triage` stage to a Set removed its count and exposed the siblings — red gate, justification sitting three lines above, unreachable. Both are markers, no behaviour change. Neither is a conversion candidate: resolving the funnel table to traits would **drop the non-column aliases it exists to accept**. **For the auto-lowering work:** the marker-placement rule is now the recurring trap — three instances, three different authors, including me. A marker that does not register is indistinguishable from no marker, and the failure mode is a red gate with a written explanation nobody can act on. If the census accepted a marker anywhere in the enclosing declaration's comments, none of the three would have happened. Baseline re-recorded per the tool's own instruction ("Re-record the baseline in the SAME PR that lowered the count"). <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Bug Fixes** - Task “Actions” menus no longer appear on bare cards in the Planning column. - Routines, scheduled tasks, and create-task steps now respect each board’s configured workflow intake column instead of using a retired default. - Legacy tasks saved with the retired intake column are migrated to automatic workflow resolution. - Target-column selection now offers only “Automatic (workflow intake)” and “Planning,” removing the obsolete option. - Capacity/planning messaging and related UI tooltip text were clarified; concurrency cap updates are managed per project. - **Tests** - Added/updated coverage for workflow intake resolution, create-task target column behavior (including legacy coercion), capacity safeguards, and API request consistency. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d75de0fb80 |
fleet: complete the column-role helper set — 680 of 722 guards had no helper to convert to (#2685)
**Fleet blocker, measured before claiming a file — this unblocks 94% of the work order.** ## The gap The work order says *"conversion pattern: the existing role helpers ONLY; no new abstractions"*. Measured against the census, those helpers cover **42 of 722** backlog guards: | role | guards | helper? | |---|---:|---| | `intake` / `hold` → `todo` | 42 | ✅ `isIntakeColumnRole`, `isPreImplementationColumnRole`, `isHoldColumnRole` | | `in-review` | 200 | ❌ | | `done` | 195 | ❌ | | `archived` | 147 | ❌ | | `in-progress` | 138 | ❌ | **680 guards — 94% — had no helper to convert to.** Every fleet worker hits this on their first file. I hit it claiming `TaskCard.tsx`, whose 42 guards are `done` 13, `archived` 12, `in-progress` 9, `in-review` 7, `todo` 1. ## Why this is not "a new abstraction" It is the **same** abstraction — flags-first, legacy id only as the documented no-metadata fallback — applied to the roles it did not yet cover. The alternative is inlining a flags-plus-fallback expression at 680 sites, which recreates exactly the copy-paste drift these helpers exist to remove: **three inline copies in `ListView` are what started this file.** Widening `ColumnRoleFlags` threads nothing new through any call site. Callers already pass these flags — `TaskContextMenuColumnFlags` declares all of them — the interface had only *declared* the two the earlier helpers needed, so the type was dropping the rest on the floor. ## A correction I made mid-change My first draft of that comment claimed the flags were "already carried on `ColumnRoleFlags`". `tsc` disproved it immediately — `complete`, `archived` and `countsTowardWip` did not exist on the type. Corrected rather than quietly patched, because that claim *was* the justification for calling this a completion rather than an addition. ## Each distinction is asserted, not just documented - **`isCompleteColumnRole` does not count `archived`** — an archived card is finished but not *completed*; surfaces counting throughput would double-count it. - **`isWipColumnRole` keys on `countsTowardWip`**, the same flag capacity arithmetic uses, so a board cannot have a column that counts toward WIP for capacity but not for this predicate. - **`isReviewColumnRole` accepts either `mergeBlocker` or `humanReview`** — separable traits, but every converted caller asks "is this card in review", for which both qualify. A caller needing one and not the other should read the flag directly rather than widen this. Both directions are asserted in every case, so a helper returning `false` unconditionally cannot pass. ## Verification `columnRoles.test.ts` **10 → 14**. `pnpm test:gate` green (10 / 158 / 487 / 71). `pnpm check:lifecycle-columns` exits 0. `tsc -p tsconfig.app.json` clean. `pnpm lint` clean. No census movement — this adds capability, converts nothing. My `TaskCard.tsx` conversion (42 → 0) follows on top of it. No changeset: internal helpers, no user-facing change. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
dc50425e98 |
docs: correct 104 future-dated FNXC timestamps across 61 files (#2680)
## What The FNXC convention exists so a reader can place a note against the change that motivated it. A stamp dated *after* the edit landed defeats exactly that. This is program-wide drift, not one author's slip — I contributed to it in my own commits this week, which is how I noticed it. ## Measured, on this tree **104 stamps across 61 files** dated later than the day they were written, from one day ahead to **2026-10-19 (81 days)**: | count | date | count | date | count | date | |---|---|---|---|---|---| | 50 | 2026-07-31 | 6 | 2026-08-05 | 3 | 2026-08-13 | | 17 | 2026-08-01 | 1 | 2026-08-07 | 1 | 2026-08-19 | | 7 | 2026-08-02 | 1 | 2026-08-12 | 2 | 2026-08-26 | | 11 | 2026-08-03 | | | 3 | 2026-10-19 | An earlier number I circulated was ~70. That came from a narrower pathspec and was wrong; **104** is the measurement. ## How Each stamp is rewritten to the date of the commit that introduced **that line**, via per-line `git blame` — deliberately *not* stamped uniformly with today's date. A uniform stamp swaps a wrong date for a different wrong date and flattens the ordering that makes these comments navigable; blame preserves it. Times of day are untouched, and a blame date in the future is clamped rather than trusted. ## Why the verification is listed A docs sweep across 61 files is precisely where a stray edit hides, so the safety claims are mechanical rather than asserted: - every changed line begins with a comment marker — **no code touched**; - **no test asserts an FNXC date later than today**, so no `toContain` assertion on embedded source text can be silently invalidated (several such assertions do exist); - CSS files, which carry several of those assertions, are outside the pathspec. ## Verified lint clean · merge gate green (487 + 158 + 10 + 71) · `census --strict` exit 0 · tsc clean for core, engine, and dashboard (`tsconfig.app.json`). **No behavior change.** Comment text only. ## Not done here A guard preventing recurrence. A check that rejects an FNXC stamp dated after the commit would stop this returning, but it needs a decision about where it runs (lint rule vs. gate) and it is a behavior change to CI — it does not belong riding inside the sweep it would police. |
||
|
|
642a4fa264 |
consolidate/u12 — U12 consolidation: 4 live defects, the AST ratchet fail-closed, and the moves.ts flag scoped (#2647)
One branch, one PR, per the consolidation directive. Contents file-by-file below. **Supersedes #2625** (its overlapping conversions landed via U11's #2624/#2626/#2636; only the parts nobody else did are folded here). **#2630 and #2639 stay open** — both green with zero threads, per rule 3. ## Four live defects, each measured **1. Every planning card renders an actions menu.** `TaskContextMenu.tsx` still had `shouldShowActionsMenu: task.column !== "triage"` on main *after* the rest of that file was converted. Since #2515 removed the id, the condition is TRUE for every card, so the suppression stopped applying anywhere — including on cards whose menu is empty, the orphaned click target the Surface Enumeration rule exists to catch. Found **twice independently**: by reading the guard, and again by the invariance test below, which failed on main with `shouldShowActionsMenu` true on one lineage and false on another. That is the argument for an invariance property over per-site conversion — the file had already been converted "2 → 1" and the survivor was the live one. **2. Worktree upcoming-work list empty on renamed boards.** `groupByWorktree` filtered `t.column === "todo"`. On the default board the id and the role coincide so every existing test passed; renamed, it matched nothing and a whole panel read as idle. **3. Hold-lane FIFO ordering lost on renamed boards.** `sortTasksForDisplayColumn` gated priority-then-FIFO on `column === "todo"`, degrading to the generic id-ordered sort elsewhere. Cards simply appear in the wrong order, silently. **4. The AST ratchet still failed open** — fourth time in that file, third found by review. `receiverName` understood only one-level property access and bare identifiers, so `task["column"]`, `metadataColumn(entry, "to")`, ternaries, `(task!.column)` and backtick literals were dropped. **Measured on main: `in-progress` 196 → 197, `in-review` 211 → 213** — three real guards nobody counted, including `metadataColumn(entry, "to") === "in-review"` in `reliability-metrics.ts`. Now walks wrappers, resolves calls to the callee name, and emits a `<SyntaxKind>` **sentinel** for anything unnameable: counted *and* trips the classification guard, so a human judges it instead of it vanishing. ## Per-file guard counts | file | before | after | |---|---:|---:| | `app/components/TaskContextMenu.tsx` | 1 | **0** | | `app/utils/worktreeGrouping.ts` | 1 | **0** | | `app/components/taskSorting.ts` | 1 | **0** | The other dashboard files I had converted reached 0 via U11's PRs; where our work overlapped I took theirs during the rebase, including two places where theirs was **stronger** than mine — they deleted Column's unreachable quick-create arm outright (with fixtures migrated) where I had converted it, and they verified the same `isPreExecutionHoldColumn` degraded-set asymmetry I did, independently. ## Flip precondition: the moves.ts flag is scoped, not flipped `move-target-declared-census.test.ts` answers precondition 2 with measurement. 41 engine `moveTask` calls have literal targets — `todo` 27, `in-progress` 7, `done` 6, `archived` 1 — and **all four are declared by the default lineage**, so the default board is not the exposure. `triage` appears only in a comment noting `replan-target.ts` used to hardcode it. My own grep had said `todo=29`; the AST says 27, because grep counts comments. The exposure is **custom** lineages: 20 of the 41 carry no `recoveryRehome` and would reject with unknown-column post-flip; 21 are exempt via the #1411 carve-out, which makes that carve-out load-bearing. I did not flip the flag. It is six seams, not the `789`/`837` pair every summary including mine described, and seam 2 turns on *new refusals* rather than swapping equivalent implementations — a green suite says nothing about that. #2639 pins the blast radius. ## Tests - `column-role-id-invariance.test.tsx` — hold traits fixed, vary only the column id across MERGED / LEGACY / RENAMED; every decision must agree. Drives the real consumers, so a component keeping an inline comparison fails it. Includes a unanimous-and-**false** case so it can't be satisfied by a predicate hardwired to true. **This is the test that caught defect 1 on main.** - `worktreeGrouping.test.ts` — includes two cards both in a column named `staging`, one hold and one not, asserting opposite answers. That assertion is impossible under a board-wide column-id set, which is why hold resolution is keyed per task via `getEffectiveTaskWorkflowId` (#2625 review). - `taskSorting.test.ts` — discriminates on the **tiebreak**, not priority: both branches sort by priority, so my first version passed for the wrong reason. Equal-priority cards whose `createdAt` order disagrees with their id order. - `no-hardcoded-lifecycle-columns.test.ts` — 16 detector cases: 11 shapes counted, 4 legitimate ignored, one asserting the sentinel path. Revert checks, all run: menu suppression → diff names the field; worktree → `expected [] to include 'FN-50'`; sort → `FN-2, FN-9` instead of `FN-9, FN-2`; ratchet → the 3 recovered guards disappear. ## One site that should never be converted `MissionControlPanel.tsx:46` — `{ id: "triage", match: (c) => c === "triage" || c === "signal" || c === "backlog" }` is a deliberate name-similarity heuristic for the SDLC funnel; it matches synonyms and folds unknown columns into an "other" bucket so custom columns still contribute. Converting it changes what the funnel displays. Like the `live-agent-count` fallbacks, it belongs in a documented floor — **the ratchet's target is that floor, not zero.** `DocumentsView.tsx:73` is convertible but the file has no column flags at all, so a real fix means plumbing board-workflow metadata into a view that doesn't fetch it — its own unit of work. ## Verification `pnpm lint` clean. `pnpm test:gate` green (10 / 482 / 71). `tsc -p packages/dashboard/tsconfig.app.json` and `packages/core/tsconfig.json` clean. Core ratchet + seam suites 24/24. Dashboard target suites 37/38 — the one failure is the pre-existing `"Back to In Progress"` label casing, confirmed identical on the base. --- ## Added after the initial push **5. `TaskCard` lost inline editing on renamed boards; `TaskDetailModal` kept it.** Still live on main: the modal resolved field editability from traits in U10/R8, the card used a hardcoded `{triage, todo}` set with **no trait path at all** — even though `taskColumnFlags` was already in scope. On a renamed board the title was editable in the modal and the pencil was missing from the card. Body moved unchanged into `isFieldEditableColumnRole` so the two surfaces cannot drift again. The veto traits are the substance: a column can legally carry `hold` **and** a WIP or review trait, and a plain `intake || hold` check would let an operator rewrite a description while a session executes against it. Coverage gap **measured, not assumed**: mutating `canEdit` back to the hardcoded set left `TaskCard*` at the same failure count as the unmutated run — nothing caught it. The four render cases assert the real `aria-label`; that mutation now fails with `Unable to find an accessible element ... name 'Edit task'`. **6. The ratchet's target is a documented FLOOR, not zero** — and this changes the completion bar. Zero is not reachable, and chasing it means breaking working code. Two categories are permanent, now protected as positive assertions so a future sweep cannot "finish the job" by deleting them: - `MissionControlPanel.tsx`'s `FUNNEL_STAGES` is a deliberate **name-similarity** heuristic — it matches `signal`, `backlog`, `to-do`, `ready`, `shipped` and folds unrecognised columns into an "other" bucket so a custom board still contributes counts. It is not asking whether a column has the intake trait; it buckets arbitrary column *names* for display. Asserted on the **synonym list**, because the synonyms are what prove it is name matching — if they disappear the site has changed character and the exemption stops applying. - `live-agent-count.ts`'s no-flags arm is reachable (a remote store is deliberately given an empty flag map; a card in an undeclared column has no flags at all) and deleting the literal makes such a card match **no** arm, so the queued total silently under-reports a stranded card. A count with an undocumented floor invites someone to drive it to zero. **Not done, and why:** `DocumentsView.tsx:73` is convertible but that file has no column flags anywhere, so a real fix means plumbing board-workflow metadata into a view that does not fetch it — its own unit of work, not something to smuggle into a conversion. **Re-verified after these commits:** `pnpm lint` clean, `pnpm test:gate` green (10 / 482 / 71), `tsc` clean on core and `tsconfig.app.json`, core ratchet suite 26/26, `columnRoles` 10/10, `TaskCard.test.tsx` 384/386 (the 2 are pre-existing CSS assertions). `TaskDetail*` is 130 failed / 551 passed **both with and without** this change — verified by stashing, so pre-existing and unrelated. --- ## Flag resolution: preconditions 1 and 2 are now DISCHARGED. Precondition 3 is blocked, and by evidence. **Precondition 1 — the side-effect equivalence proof — done.** `moves-flag-equivalence.test.ts` runs the same journey under both flag states against live PG and diffs the persisted row. **Result: identical** — whole-row equality across 128 fields plus an equal timing shape, over `todo → in-progress → in-review → todo → in-progress`. That test was **wrong twice** before it meant anything, and both times it was passing: 1. **It proved nothing.** `experimentalFeatures` is **global-only**, and `moves.ts` reads `getSettingsFast()`, which filters global-only keys out of the project layer. My `updateSettings` write was silently discarded, `useWorkflow` was false in *both* runs, and the "proof" compared the legacy path against itself. Found by stamping the flag-ON branch and observing the test still passed. Now written via `updateGlobalSettings`, and the helper **asserts the flag took effect** before the journey runs. 2. **The journey was forward-only**, so it never reached the reopen hook's field resets (`status`, `error`, `blockedBy`, pause clearing) — a mutation there passed. Extended with a backward move and a re-entry. Mutation-verified after both fixes: stamping seam 3, and diverging the reopen hook, each fail the comparison. **Precondition 2 — done, and its answer is a blocker.** The census says the default board is safe: all 41 literal engine move targets are declared by the default lineage. But **20 of those 41 carry no `recoveryRehome`**, so on a custom lineage that does not declare `todo` / `in-progress` / `done`, seam 2 would start rejecting them with unknown-column. That is a user-facing break on custom boards, not a theoretical one, and it is not fixed by the equivalence proof — seam 2 adds *new refusals* rather than swapping implementations. **So the flip is one step away, and the step is not mine to take alone:** those 20 call sites need to resolve their target from the task's workflow (or justify `recoveryRehome`), and they live across engine lanes in `moves.ts` caller territory — U2b/MAIN. Flipping before that trades a dormant flag for broken custom boards. What remains for precondition 3 once those land: flip both readers **atomically** (`moves.ts` + `workflow-task-create-ops.ts`, since the latter computes the preflight the former consumes), delete the flag-OFF branch with its guards, and drop the settings key. --- ## CORRECTION: seam 2 is not a blocker. My earlier claim was wrong. I stated in #2639 and above that "with the flag off there is **no** target-column validation on the move path", so flipping would introduce new refusals. **That is not what happens.** Reproduced against live PG: the identical custom-lineage move rejects with the flag **OFF** as well — ``` Error: Invalid transition: 'backlog' -> 'todo'. Valid targets: building ``` Transition validation is already in force on the flag-OFF path. So for the shape in question — an engine move to a column the task's own workflow does not declare — **the move already fails today**, and seam 2 introduces no new break for it. The 20 census sites lacking `recoveryRehome` are broken on a custom lineage *now*, not broken by the flip. I found this because the discriminator I added to prove "the flag is the cause" failed. Had I written the test to my assumption it would have passed and the false claim would have shipped — the same way the equivalence test passed while proving nothing until I tried to make it fail. **Revised precondition status:** | precondition | status | |---|---| | 1 — side-effect equivalence | **discharged** — identical rows, mutation-verified both directions | | 2 — seam-2 exposure census | **discharged, and it is not a blocker** — the rejection predates the flag | | 3 — flip both readers atomically, delete the flag-OFF branch, drop the settings key | **the remaining work** | So the flip is no longer gated on fixing 20 engine call sites. What it is still gated on is precondition 3 being done atomically across `moves.ts` and `workflow-task-create-ops.ts` (the latter computes the preflight the former consumes), which is `moves.ts` caller territory. Three cases now cover seam 2: the flag-ON rejection, the flag-OFF rejection (asserting the error *message*, so a change in which guard rejects stays visible rather than reading as agreement), and the #1411 `recoveryRehome` carve-out succeeding — pinning why that carve-out is load-bearing and must not be tidied away. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
31e49b684a |
TAKING default-workflow-hooks.ts + executor.ts + live-agent-count.ts + 6 dashboard files: reopen semantics by role, and the census's blind spot in both directions (13 sites) (#2628)
Batched conversion of every lifecycle-column guard I hold, plus the three the census could not see. **Six files to zero, repo-wide 60 → 49 by a comment-stripped unanchored sweep.** Each conversion has an isolated revert proof and a paired negative case, and the one code move is a separate commit from the behavior changes. ## Per-file before → after Counts from a comment-stripped, unanchored `(===|!==) ["']triage["']` sweep over `packages/*/src` + `plugins/*/src`, excluding tests. | file | before | after | note | |---|---:|---:|---| | `core/default-workflow-hooks.ts` | 4 | **0** | | | `core/task-store/moves.ts` | 5 | **4** | only the flag-ON mirror converted; the flag-OFF inline block is the parity reference and stays | | `engine/executor.ts` | 3 | **0** | **absent from the 45-guard list** — see below | | `core/live-agent-count.ts` | 2 | **0** | duplication removed; answer deliberately unchanged | | `engine/replan-target.ts` | 2 | **0** | both were comment prose, not guards | | `core/agent-prompts.ts` | 3 | **0** | ROLE comparisons, never column guards | | `engine/usage-limit-detector.ts` | 2 | **0** | ROLE comparisons | | `dashboard/app/components/DocumentsView.tsx` | 1 | **0** | real column guard | | `dashboard/app/components/TaskChatTab.tsx` | 2 | **0** | ROLE | | `dashboard/app/components/AgentLogViewer.tsx` | 1 | **0** | ROLE | | `dashboard/app/components/effective-model-resolution.ts` | 1 | **0** | ROLE | | `dashboard/app/hooks/useTasks.ts` | 1 | **0** | ROLE | | `dashboard/…/command-center/MissionControlPanel.tsx` | 1 | 1 | alias table, marked `DELIBERATE-LITERAL` with its reason | ## The census errs in BOTH directions This is the finding I would most like carried into the remaining work. - It **flagged 10 sites that were never column guards.** `role === "triage"` / `agentType === "triage"` compare an **AGENT ROLE**. The planner *lane* is named `triage` and keeps that name — U11 removed the *column*. Worse than noise: the obvious "finish the migration" edit is to rename the role, and that silently empties the planner's prompt template and mis-binds its model markers. `PLANNER_AGENT_ROLE` now names it, so the two vocabularies are distinguishable by grep and a rename fails loudly (revert proof: 4 tests, two of them pre-existing). - It **missed 3 real guards in `executor.ts`**, because the pattern matches `column`/`toColumn`/`fromColumn` and those locals are named `from` and `originColumn`. A census keyed on variable names will keep missing guards wherever a local was named for its role in the function. ## Two real defects, not tidying **1. A renamed board could merge with its re-review never run.** `default-workflow-hooks.ts` is named for the default workflow, but the store runs it on the flag-ON path for *every* workflow — the trait registry resolves hooks by trait id, not by workflow. Its reopen predicates listed the default lineage's column names, so on a renamed board **no reopen effect fired at all**. One of them clears `workflowStepResults`, which `getTaskMergeBlocker` reads: a card bounced out of review carried its old `passed` result back in, and that satisfies the merge gate. Same regression the graph-owned-crossing carve-out exists to prevent, arriving through the other door. (Two smaller ones rode along: failure state never cleared on a renamed reopen, and an operator dragging a card back to the queue never parked it, so the scheduler re-dispatched what they had just pulled back.) **I forgot the carve-out on my first pass, and that was worse than not converting.** A role-resolved clear plus a *name*-matched exemption means a renamed board takes the clear and never the exemption, destroying the remediation input the graph had just written. My own paired negative test caught it. **2. The last-resort recovery for completed-but-stranded work did not exist off the default lineage.** In `recoverCompletedTask`, `promotedFromPlannerColumn` was false on a renamed board, so finished work resting in the planning lane was never promoted — the code fell through to `handoffTaskToReview` straight from the planning column, and role adjacency has no planning → review edge, so the handoff was rejected and the card stayed stuck with its work complete. I converted the promotion **target** too: resolving the lane and then moving to a literal `in-progress` is the half-conversion I have already been burned by twice this program, where the guard starts admitting cards and the move then sends them to a column the board does not declare. ## E2E evidence `renamed-board-reopen.pg.test.ts` drives a **real PostgreSQL store** and a real `moveTask` on a workflow whose columns carry the standard traits under non-default names. The unit tests cannot show this: if `moves.ts` passed `undefined`, every unit case still passes via the no-basis fallback while the real board keeps the old behavior. **Proof it is load-bearing: forcing `moveLifecycleColumns` to `undefined` fails 2 of 3.** The executor suite covers both the split-role and the MERGED post-U11 shape. ## Revert proofs, isolated per site | change reverted | result | |---|---| | reopen predicate → literal names | 4 of 10 fail | | reopen field clears → literal names | 2 of 10 fail | | `userPaused` hold lane → literal `todo` | 1 of 10 fail | | graph carve-out → literal names | 1 of 10 fail | | store passes `undefined` lifecycle columns | 2 of 3 fail (real PG) | | `promotedFromPlannerColumn` → literals | 3 of 7 fail | | two-hop condition → `=== "triage"` | 1 of 7 fails | | promotion target → `"in-progress"` | 3 of 7 fail | | `isPlannerColumnFor` → literals | 1 of 7 fails | | live-agent-count: one arm dropped | 2 of 11 fail | | DocumentsView: trait branch removed | 3 of 7 fail | | planner role renamed to `"planner"` | 4 fail (2 pre-existing) | Every conversion is paired with a negative case (a forward move, a not-a-planner-lane card, a default-lineage card, a renamed column with no traits), so neither "always fire" nor "never fire" can pass for "resolve the role". ## Deliberately NOT converted, with reasons - **`moves.ts` flag-OFF inline block (4).** That branch *is* the legacy path, kept verbatim so the two can be parity-checked. Converting it erases the reference implementation. - **`live-agent-count.ts`'s no-flags fallback.** Reachable, and there is nothing to resolve from — `enrich…FromFlags` exists for callers with board flags rather than an IR, so a column missing from that map is the renamed case. "Not intake" is as much a guess as "todo is intake", and Running/Waiting are complements, so a card matching neither arm is reported as neither and the footer's queued total under-reports it. The real fix is at the caller; four new cases pin that flags override the legacy answer **in both directions**. What did change is the duplication: two hand-written copies of one rule now call one named function. - **`MissionControlPanel`'s `FUNNEL_STAGES`.** An alias table of column *names* where `triage` sits beside `signal` and `backlog`. Command Center aggregates across projects, so there is no single workflow to resolve traits from — the honest conversion is a data change, not a predicate change. - **`DocumentsView` with no traits.** Same no-basis rule; the documents list is full of historical columns absent from the current board. A case asserts a renamed column with no traits still reads as "working", documenting the gap rather than hiding it. ## Fixture findings Each cost a red run that looked like the code under test: - a `merge-blocker` column needs a reachable merge-class node, or `parseWorkflowIr` rejects the workflow; - a back-edge must be `kind: "rework"`, and a rework edge is legal only **into** a node with `config.reworkRegion: true`; - a workflow gets role-level transitions only when it declares wip + review + complete + **archived** plus a planning lane — without the archived column, adjacency falls back to order-derived neighbours and `checking -> queued` is not a legal move at all; - `recoverCompletedTask` only *reaches* the promotion seam when nothing is left to gate; without passed `plan-review`/`code-review` rows it re-enters the workflow graph and returns first, so a naive fixture silently tests the wrong branch and every assertion reads "no moves happened" for an unrelated reason. ## Verification - `pnpm test:gate` **71/71** - new suites: 10/10 reopen-semantics, 3/3 renamed-board-reopen (real PG), 7/7 executor-planner-lanes, 7/7 documents-status-dot, 4/4 planner-role-is-not-a-column - neighbours: 132 + 10 + 482 (gate shards), 350/351 engine planning/replan suites, 64/64 agent-prompts, 51/51 usage-limit-detector, 11/11 live-agent-count, 11/11 dashboard hook/log suites - the single engine failure (`executor-fast-mode-workflows.test.ts` › "raw fast mode still invokes non-executable review seam nodes") **reproduces with my changes stashed** — pre-existing on `origin/main` - typechecks clean for core, engine, and dashboard-app (`tsconfig.app.json`; `tsconfig.json` checks nothing under `app/`); `pnpm lint` clean 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
1469d57477 |
U12 drift: ListView.tsx — one tested column-role helper (5 -> 0) (#2620)
**File claimed: `packages/dashboard/app/components/ListView.tsx`.** Per-file lifecycle-column guard count: **5 → 0** (3 live, 2 in comment prose that described the deleted code). ## What was actually wrong All three live sites were *already* flags-first. The defect was that each carried its own inline copy of the same fallback: ```ts targetFlags ? Boolean(targetFlags.intake || targetFlags.hold) : column === "todo" || column === "triage" ``` Three copies, none reachable from a test, each reading like a lifecycle rule rather than the degraded mode it is. A fourth copy was the natural next step. ## Why the fallback survives instead of being deleted `columnFlagsById` is legitimately empty in two states: the pre-load window before the workflows fetch resolves, and a card stranded in a column its workflow no longer declares. A bare `flags.intake === true` returns false in both, and **both failures are silent** — the Planning badge stops appearing, and a backwards move stops asking whether to preserve step progress, so the operator loses completed steps with no prompt and no error. Deleting the fallback is not the cleanup it looks like. So it is kept, named (`isPreImplementationColumnRole`, `isIntakeColumnRole` in `app/utils/columnRoles.ts`), defined once, and documented with that reason at the definition. The legacy ids now live in a named `LEGACY_PRE_IMPLEMENTATION_COLUMN_IDS` set — a last-resort guess, not a comparison masquerading as a rule. ## Tests, and the case that never had one `app/__tests__/columnRoles.test.ts` (6). The degraded branch is now covered for the first time — it was unreachable while inline inside two `handleMove` closures and a `useCallback`. It also pins the **inversion** a fourth copy would eventually get wrong: a resolved column whose traits say it is *not* pre-implementation must not be overridden by an id that happens to be `todo` or `triage`. That is the direction that trains operators to dismiss the prompt. Mutation-checked, measured: | mutation | result | |---|---| | ignore the flags argument (`return LEGACY_….has(columnId)`) | **4 failed / 2 passed** | | ignore the id fallback (`return Boolean(flags?.intake \|\| flags?.hold)`) | **4 failed / 2 passed** | ## Behaviour preservation `ListView.test.tsx` + `workflow-resolved-columns.test.tsx`: **260 passed**, unchanged. The extraction is a pure move — the two helper bodies are the inline expressions verbatim, with the id set hoisted. `pnpm lint` clean. `tsc -p tsconfig.app.json` clean (the app config, not the root one that silently skips `app/`). No changeset: behaviour-preserving refactor. ## Backlog measured on `origin/main` at time of writing 48 total. `self-healing.ts` (10) is the capacity worker's; `register-task-workflow-routes.ts` (7) is my #2614. Remaining unowned in this area after this PR: `TaskCard.tsx` 4, `TaskDetailModal.tsx` 3, `TaskContextMenu.tsx` 2, `Column.tsx` 2, `taskActivity.ts` 2. Several of those hold the *same* fallback pattern and can now call this helper rather than grow another copy. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
743df98aa4 |
capacity, part 1: merge pinned at 1, worktrees-off mode, and one dead knob deleted (#2502)
First slice of the capacity simplification. Operator: *"just have two
capacity — overall per project agent count and max worktrees. Remove all
other capacities and counts."* Plus two later additions: **merge is
always 1, fixed**, and **worktrees off ⇒ limit by total agents only**.
Three independently revertable commits. No limiter is added anywhere;
one is deleted, one is made structurally absent, and one is pinned.
---
## 1. Merge concurrency ratcheted at 1 (test-only)
I was asked to add a limiter if merge concurrency could be raised. **It
cannot** — there is no setting, workflow property, pool or trait config
anywhere that raises it, so this adds no code and pins what already
holds.
Serialization lives in the **pump**: `drainMergeQueue`’s `mergeRunning`
re-entrancy latch, `activeMergeTaskId` as a single-slot identity, the
`mergeBodyInFlight` next-generation latch, and one `ProjectEngine` per
projectId.
**Not** in the merge-queue lease, which is a per-task ROW (`primaryKey
[projectId, taskId]`) — two tasks can hold leases simultaneously by
construction, and it has exactly one caller (the worktree-reuse
handoff). Ordinary merges never take it. A lease-level test would have
been describing an invariant that layer has never held.
The second half guards the other direction: a merge-concurrency
*setting* would not fail the pump ratchet — it would sit unread until
someone wired it up.
**Revert-proof:** deleting the latch → `expected 1 times, but got 2
times`; deleting the `finally` → latch-stuck; injecting
`maxConcurrentMerges: 2` → fails naming the key; injecting a
`maxParallelLanes` merge-trait field → fails naming the field. Sources
restored byte-identical after each injection.
## 2. `worktreesEnabled` — off means the worktree limit cannot bind
No worktrees-off mode existed (no
`worktreesEnabled`/`useWorktrees`/`worktreeMode` anywhere — only
worktree *configuration*).
**Why not `maxWorktrees: 0`, which needs no new key:** it deadlocks. `??
4` keeps `0` (not nullish), the gate is `used >= limit`, so `0 >= 0`
holds **on an empty board** and nothing ever dispatches — while the
operator-visible reason reads `gate=maxWorktrees; used=0/0`, a limiter
that looks like it is working while the board is dead. It also needs the
Command Center `{min:1}` clamp relaxed. So `0` costs the gate rewrite
*and* the clamp change *and* encodes a mode as a magic value.
**Off is absence, not a big number.** `resolveWorktreeCapacityLimit`
returns `number | null`; `ConcurrencyGateDiagnostic.maxWorktreesGate` is
now optional, so consulting a worktree limit in OFF mode does not
type-check. A gate holding `Infinity` can start binding again the moment
someone "fixes" a comparison; an absent gate cannot.
That paid for itself immediately: making it nullable surfaced a
**second, independent** worktree gate (`activeWorktrees >= maxWorktrees`
early-return) that a skip-by-convention approach would have missed
silently.
**Scope, deliberately:** this is a statement about *counting*, not
isolation. It does not make concurrent agents safe to share one checkout
and builds nothing toward that — the non-worktree paths that exist today
are fallbacks to the operator’s own tree, one of which caused FN-8600.
**Revert-proof:** a resolver ignoring the flag turns both OFF scheduler
tests red while every ON test stays green — they reuse the *same*
fixture (5 in-progress, limit 4) that pre-existing tests prove blocks,
so the pair moves in opposite directions. Removing `disabled:` reddens
the UI test.
## 3. `maxTriageConcurrent` deleted — it controlled nothing
**Measured: zero enforcement reads.** The only `.maxTriageConcurrent`
reference in the repo was a route echoing it back in `/config`. FN-8453
removed the pool it gated and left the knob shipping in
`DEFAULT_SETTINGS`, the settings type, the section registry, the API
response and six i18n catalogs, doing nothing, for releases.
Historical FNXC comments are **updated, not deleted** — they explain a
real past incident; they now say "planning admission slot" so they stop
implying a live setting. Tombstoned so it cannot return.
`/config` loses a field; safe in-repo since `fetchConfig`’s own return
type never declared it.
---
## Two corrections worth recording
- I earlier reported `maxWorktrees` had **no** Settings UI. Wrong —
`WorktreesSection.tsx:47`; my grep was truncated by `head`. It changed
the placement (toggle beside it, rather than a duplicate key in
Scheduling).
- I planned to assert the queued-reason string is rewritten in OFF mode.
Measured that it is **unreachable**: when `maxConcurrent` binds, the
sweep bails before the per-task reason and logs nothing. The test
asserts absence instead.
Two near-misses caught before commit: a pre-existing FN-7505 guard
caught my *new* key missing a description mapping; and editing i18n via
`json.load/dump` silently dropped unrelated duplicate keys
(`autoUpdateAndRestart` in `fr`) — Python keeps only the last of a
duplicated key. Redone textually, every catalog re-validated.
## Verification
`pnpm lint` clean · core/engine/dashboard/i18n typecheck clean · `pnpm
test:gate` green (309 + 10 + 71) · capacity/worktree suites 11/11 ·
engine merge-invariant + scheduler 45/45 · dashboard settings 114/114.
Rebased onto current main and re-verified.
Nothing was booted at any point.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added a project setting to enable or disable running tasks in
worktrees.
* Disabling worktrees removes worktree capacity limits from task
scheduling.
* The “Max Worktrees” setting is disabled when worktree execution is
turned off.
* **Changes**
* Removed the unused triage concurrency setting from configuration and
dashboard responses.
* Updated scheduling diagnostics and queue messages to reflect disabled
worktree capacity limits.
* Added localized labels and help text for the new setting.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
73b2a32e2b |
test: backfill dashboard test mocks for new api and viewport exports
Concurrent dashboard work added runtime exports that hardcoded module mocks did not expose, so any suite rendering the affected component threw "No <export> is defined on the mock". Adds the missing `../api` exports (system-info probe, update install/restart, cloudflared, provider key and login helpers, git remotes/branches) and the `isTabletTouchViewport` viewport helper across the 26 suites that mock those modules. Verified: all 26 files pass (1012 tests). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d193dcdbb0 |
chore(lint): ban React components declared inside another component
Adds fusion-react/no-nested-component-definitions, a custom rule in the house style of the existing detached-spawn guard. A component declared in render is a new element type every render, so React remounts its subtree on each parent update and destroys focus, scroll, and local state. This pattern shipped three times without review or tests catching it: FN-8606's ModalShell left Planning Mode and Settings untypable, and MailboxModal's ReplyContextExpandable collapsed expanded reply rows. Tests missed it because fireEvent.change sets a value without needing the node to stay mounted. The rule reports PascalCase functions (including memo()/forwardRef()-wrapped) that return JSX and are declared inside another JSX-returning function. Lowercase render helpers are deliberately allowed — they are the sanctioned fix. Escape hatch: // nested-component-allowlist: <reason>. Scoped to production .tsx, with a vitest guard for the rule itself. Hoists the two pre-existing violations (ProviderStatusBadge, GitHubStatusBadge in ModelOnboardingModal) to module scope so the rule lands clean at "error". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
743dc5f46e |
FN-8612: remove tablet task modal padding
Keep task modals dense on tablets without reducing their touch resize targets. - Move the Task Detail drag target out of layout flow while preserving its 44px hit area. - Restore desktop-density New Task header and body padding on tablet resize surfaces. - Add CSS, unit, and browser coverage for tablet geometry and generic floating windows. Files changed: .changeset/fn-8612-tablet-modal-padding.md | 7 + docs/dashboard-guide.md | 8 +- .../task-detail-modal-tablet-width.test.ts | 8 + .../dashboard/app/components/FloatingWindow.css | 40 ++++- packages/dashboard/app/components/NewTaskModal.css | 14 ++ .../FloatingWindow.touch-geometry.test.tsx | 14 ++ .../app/components/__tests__/NewTaskModal.test.tsx | 8 + .../app/task-modal-touch-resize-e2e-fixture.tsx | 54 ++++++- .../task-modal-touch-resize-browser.test.ts | 177 ++++++++++++++++++++- packages/dashboard/vitest.config.ts | 6 + 10 files changed, 322 insertions(+), 14 deletions(-) Fusion-Task-Id: FN-8612 Fusion-Task-Lineage: fecd7c48-7b6c-434e-9002-f8f21241120c Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
f26cbedf4f |
fix(dashboard): close the code-review findings on the mobile tab-discard work
An 11-reviewer pass over f157bf7460..f5163d8351 found defects in the mobile tab-discard change set itself. This fixes them. Silent data loss (the recurring defect class): - AgentDetailView reconnect refetched limit:100 and replaced wholesale, so 380 displayed lines vanished with no "Load older" and no indicator; it now reconciles through the shared logStreamReconcile helper. - useActivityLog.loadMore past the cap discarded the page it had just fetched while advancing the cursor and leaving hasMore true, so the feed silently stopped paginating behind a live-looking button. - useAgentLogs: loadMore and resyncFromServer had no mutual exclusion, a no-overlap resync discarded explicitly paged-back history, a resync outliving the reconnect delay left an unmarked gap, and the live-tail trim could evict the gap marker itself. - useLiveTranscript's resync overwrote live entries that raced the refetch. The premise itself was not fully delivered: - useProjects, useNodes, and useMeshState never called clearInterval, so they polled the whole time the tab was hidden. useProjects is mounted for the entire session, so the page never went idle -- the primary mechanism this work depends on. All three now use the shared visibility gate. - sse-bus fired onReconnect twice per reconnect cycle and fanned out ~28 subscribers in one tick, against a ~6-connection-per-origin cap on a waking radio. The successful open is now the single authority, and the fan-out uses the same exported stagger primitive as the polling path rather than a second copy of the slot formula. - A channel first subscribed during the hidden window opened a live EventSource and keepalive; suspension is now a module-level condition openChannel consults, and a channel opened inside the grace window re-arms it. Credentials and correctness: - The service worker persisted every GET /api/* to durable Cache Storage, including /api/settings with daemonToken, githubAuthToken, gitlabAuthToken and ntfyAccessToken in plaintext, with no exclusion and no purge path -- "Clear all cached data" only walked localStorage. Now gated, bounded, and genuinely purgeable. - useTasks cleared its own snapshot when the mount revalidation failed on a waking radio, so the board blanked and the next restore was empty too. Suspension-class failures no longer destroy the cache. - A single-row SSE update reset lastFetchTimeMs to now while an hours-old hydrated snapshot was on screen, re-marking every in-progress card stuck. - ListView's "Select all visible tasks" acted on the full filtered set while only 50 rows rendered, so a bulk delete reached rows the operator could not see. Column's search window reset keyed on a boolean, so refining a query kept the expanded window. Tests that could not fail: - App.test.tsx mocked TerminalModal as isOpen ? <div/> : null, making the unmount-on-close invariant unobservable; MockEventSource kept its listeners after close(), so cases passed with their onReconnect handlers deleted. - The SSE resync ratchet scanned only hooks/, exempting ~13 component call sites -- the exact regression it exists to prevent. - MissionControlPanel's bespoke poll and the xterm scrollback constants and WebGL disposal had no coverage at all. Verified: tsc -p tsconfig.app.json clean, pnpm lint clean, pnpm check:changesets clean, 877 tests passing across 36 scoped files. Known unrelated red: MailboxView.test.tsx's FN-8407 CSS guard fails at HEAD too -- this diff adds no @media rule and no .mailbox-view--mobile selector, the only two things that assertion inspects. Left alone deliberately. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f5163d8351 |
fix(dashboard): resync SSE state on reopen and bound the service-worker cache
Follow-up to
|
||
|
|
f157bf7460 |
fix(dashboard): harden visibility suspension, log caps, and mobile board UX
Suspend poll/SSE work when the tab is hidden, cap log buffers, restore board scroll more reliably, and improve list windowing/live tickers with related tests and a mobile-tab retention changeset. |