Commit Graph

284 Commits

Author SHA1 Message Date
gsxdsm
9eae6b9bc5 fix(auth): a provider's first-ever login silently saved nothing
Operator could not log in to Anthropic or Codex on a fresh container: every
attempt ended "Login did not complete. Please try again.", while the same
providers worked flawlessly on their long-lived native install.

FusionAuthStorage.modify() is the seam pi persists a COMPLETED LOGIN through
(Models.login -> credentials.modify(provider.id, ...) in pi-ai models.js:198).
It resolved its write target with `creating: false` and returned before invoking
the callback whenever the provider had no credential row yet:

    const target = this.resolveWriteTarget(provider, current, false);
    if (!target || !this.credential(target, current)) return { changed: false };

So a first login completed its browser flow, exchanged the code, took and
released the lock file, wrote NOTHING, and resolved as success — leaving the
dashboard poll to see authenticated:false and report the generic failure.

It reproduces only on a store with no existing row, which is why it looked
environment-specific: an install that has logged in before takes the same path
as a refresh over an existing row and is fine, while every new container, new
machine, or wiped ~/.fusion can never complete a first login for ANY provider.

Evidence from the operator's container: flow ended with err=None (pi resolved,
no error), nothing logged, auth.json still {}, the agent directory's mtime
bumped when the lock was taken and released while auth.json itself never
changed, and an API-key write — which goes through set(), not modify() — landed
immediately.

modify() now creates when absent and updates when present; a callback returning
undefined still writes nothing, so pi's refresh-bails-out behaviour is unchanged.

auth-storage-instances.test.ts asserted the old behaviour, grouping modify() with
remove/logout/removeInstance as "non-creating". The removal guarantees are kept;
the modify() assertion is inverted, because it encoded the defect.

Also surfaces the server's own loginError through a new describeLoginFailure()
helper instead of the generic sentence, so an OAuth state mismatch reads as the
stale-tab instruction it is. Writing its test caught a bad regex of mine:
`code.*expired` matched "OpenAI Codex ... token_expired", a different failure.

Verified: the new first-login test fails against the old `creating: false` and
passes with the fix; 86 engine auth tests, 238 dashboard auth/dialog tests, and
pnpm test:gate all pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 21:39:06 -07:00
gsxdsm
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>
2026-08-17 15:47:43 -07:00
gsxdsm
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>
2026-08-15 23:14:24 -07:00
gsxdsm
7ed1c39a67 FN-9102: fix active runtime segment accounting
Prevent reopened tasks from double-counting closed execution segments while preserving pre-execution planning time.

- Clear the live execution anchor whenever a WIP segment is banked.
- Clamp legacy poisoned runtime values with separate execution and combined-work wall-clock ceilings.
- Cover cards, detail statistics, core totals, and planner metrics across WIP round trips and historical rows.
- Add a patch changeset for the published Fusion package.

Files changed:
 .changeset/fn-9102-task-runtime-double-count.md    |  7 ++
 .../src/__tests__/reopen-semantics-by-role.test.ts | 95 ++++++++++++++--------
 packages/core/src/tasks/task-timing.ts             | 20 ++++-
 .../core/src/workflows/default-workflow-hooks.ts   | 14 +++-
 .../app/components/__tests__/TaskCard.test.tsx     | 27 ++++++
 .../__tests__/TaskTokenStatsPanel.test.tsx         | 27 ++++++
 .../app/utils/__tests__/taskTiming.test.ts         | 40 +++++++++
 packages/dashboard/app/utils/taskTiming.ts         | 28 +++++--
 .../__tests__/task-planner-chat-metrics.test.ts    | 28 +++++++
 .../dashboard/src/task-planner-chat-metrics.ts     | 22 ++++-
 10 files changed, 260 insertions(+), 48 deletions(-)

Fusion-Task-Id: FN-9102

Fusion-Task-Lineage: 3b42e81f-4e11-4232-80f6-052f51b83f1c

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-15 13:39:24 -07:00
gsxdsm
284feeaf11 FN-9092: prevent model menus from dismissing host dialogs
Keep portaled model-menu interaction from propagating to host dialog dismissal.

- Mark model-menu portal surfaces and stop their pointer events from reaching dialog backdrops.
- Apply the dismissal guard across agent, chat, floating-window, and model-selection dialogs.
- Close enabled overlays on valid touch taps while suppressing duplicate compatibility mouse closes.
- Add desktop and mobile regression coverage for portal-host dismissal behavior.

Files changed:
 .../fn-9092-model-filter-dialog-dismissal.md       |   7 +
 .../ui-bugs/portaled-model-menu-host-dismissal.md  |  19 +++
 packages/dashboard/app/components/AgentsView.tsx   |   6 +
 .../app/components/ChatThinkingLevelControl.tsx    |  17 ++-
 packages/dashboard/app/components/ChatView.tsx     |  38 +++--
 .../app/components/CustomModelDropdown.tsx         |  13 +-
 .../dashboard/app/components/FloatingWindow.tsx    |  17 +--
 .../app/components/ModelSelectionModal.tsx         |  17 +--
 .../dashboard/app/components/NewAgentDialog.tsx    |  12 +-
 .../dashboard/app/components/QuickEntryBox.tsx     |   5 +-
 .../ChatThinkingLevelControl.portal.test.tsx       |   9 ++
 .../__tests__/CustomModelDropdown.test.tsx         |  24 ++-
 .../__tests__/ModelSelectionModal.test.tsx         |   3 +-
 .../NewAgentDialog.portal-dismissal.test.tsx       |  77 ++++++++++
 .../components/__tests__/NewAgentDialog.test.tsx   |  11 ++
 .../model-menu-filter-host-dismissal.test.tsx      | 163 +++++++++++++++++++++
 .../app/hooks/__tests__/useOverlayDismiss.test.tsx |   7 +-
 packages/dashboard/app/hooks/useOverlayDismiss.ts  |  26 +++-
 .../app/utils/__tests__/portalSurfaces.test.ts     |  31 ++++
 packages/dashboard/app/utils/portalSurfaces.ts     |  31 ++++
 20 files changed, 469 insertions(+), 64 deletions(-)

Fusion-Task-Id: FN-9092

Fusion-Task-Lineage: a0d0b84d-2efe-4a67-9485-77c08f7cc57e

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-15 06:42:39 -07:00
gsxdsm
0188d9427b FN-9044: group workspace tasks by acquired worktrees
Show workspace-mode task cards in their own worktree group instead of Unassigned.

- Classify workspace worktree groups with stable identifiers and repository counts.
- Render localized workspace headers and appropriate group icons.
- Cover workspace grouping across utilities, columns, lanes, and group headers.
- Document the board behavior and add a patch changeset.

Files changed:
 .changeset/fn-9044-workspace-worktree-grouping.md  |  7 ++
 docs/dashboard-guide.md                            |  2 +-
 packages/dashboard/app/components/Column.tsx       |  4 +-
 .../dashboard/app/components/WorktreeGroup.tsx     | 20 ++++--
 .../app/components/__tests__/Column.test.tsx       | 32 ++++++++-
 .../app/components/__tests__/Lane.test.tsx         | 24 ++++++-
 .../__tests__/worktree-grouping-workspace.test.tsx | 66 +++++++++++++++++
 .../app/utils/__tests__/worktreeGrouping.test.ts   | 82 ++++++++++++++++++++++
 packages/dashboard/app/utils/worktreeGrouping.ts   | 37 ++++++++--
 packages/i18n/locales/en/app.json                  |  4 +-
 packages/i18n/src/resources.d.ts                   |  4 +-
 11 files changed, 267 insertions(+), 15 deletions(-)

Fusion-Task-Id: FN-9044

Fusion-Task-Lineage: d342d4fb-9fab-4efd-813f-2a3091d7c414

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-14 20:50:55 -07:00
gsxdsm
f6518b4275 FN-9029: keep release-gate verdicts transient
Keep release-gate verdicts restricted to fresh task-list responses.

- Strip release-gate data from SSE and non-board task paths.
- Reject stale initial verdicts before rendering promote controls.
- Cover server persistence, SSE payload, and client freshness boundaries.

Files changed:
 .../dashboard/app/hooks/__tests__/useTasks.test.ts | 115 +++++++++++++++++++++
 packages/dashboard/app/hooks/useTasks.ts           |  80 +++++++++-----
 .../app/utils/__tests__/releaseGate.test.ts        |   9 ++
 .../routes-tasks-release-gate-transient.test.ts    | 103 ++++++++++++++++++
 .../src/__tests__/sse-task-deleted-payload.test.ts |  10 +-
 packages/dashboard/src/sse.ts                      |  26 +++--
 6 files changed, 305 insertions(+), 38 deletions(-)

Fusion-Task-Id: FN-9029

Fusion-Task-Lineage: cf764f3e-c6f5-4fa1-b608-ebb8d547cfdd

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-13 15:43:16 -07:00
gsxdsm
a6ce7f89ef FN-8987: expose release-gate verdicts for Promote visibility
Expose server-evaluated release-gate state so Promote controls accurately reflect approval readiness.

- Attach transient release-gate verdicts to task API responses.
- Preserve only fresh REST verdicts across task snapshots and render them in task cards.
- Align hold-release gating and document the Promote-state contract.

Files changed:
 .changeset/fn-8987-release-gate-verdict.md         |   7 +
 docs/dashboard-guide.md                            |   4 +
 docs/solutions/workflow-learnings/project-union-versus-per-task-lanes.md |   1 +
 docs/workflow-steps.md                             |   4 +
 packages/core/src/index.gate.ts                    |   2 +-
 packages/core/src/index.ts                         |   2 +-
 packages/core/src/types.ts                         |   2 +
 packages/core/src/types/task/task-core.ts          |  20 +++
 packages/dashboard/app/components/TaskCard.tsx     |   3 +-
 packages/dashboard/app/components/__tests__/TaskCard.test.tsx |  31 ++++
 packages/dashboard/app/hooks/__tests__/useTasks.test.ts |  26 ++++
 packages/dashboard/app/hooks/useTasks.ts           | 135 +++++++++++++++--
 packages/dashboard/app/utils/__tests__/releaseGate.contract.test.ts |  29 ++++
 packages/dashboard/app/utils/__tests__/releaseGate.test.ts |  49 ++++++
 packages/dashboard/app/utils/releaseGate.ts        |  26 ++++
 packages/dashboard/app/utils/reviewBudgetApproval.ts |   9 ++
 packages/dashboard/src/routes/register-task-workflow-routes.ts |  20 ++-
 packages/engine/src/__tests__/release-gate-verdict.test.ts |  40 +++++
 packages/engine/src/execution/hold-release.ts      | 166 +++++++++------------
 packages/engine/src/index.ts                       |   3 +
 20 files changed, 469 insertions(+), 110 deletions(-)

Fusion-Task-Id: FN-8987

Fusion-Task-Lineage: 6f1742bc-2b2b-4a32-9be5-92160335d90d

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-11 14:22:14 -07:00
gsxdsm
967e98194e FN-8950: hide Promote until plan gates clear
Prevent task-card promotion shortcuts when plan review or approval holds remain.

- Mirror default-on plan-review gate and approval-hold predicates in dashboard helpers.
- Suppress Promote across planning, review, and approval states.
- Add regression coverage and a patch changeset.

Files changed:
 .changeset/fn-8950-promote-plan-gate.md            |  7 ++
 packages/dashboard/app/components/TaskCard.tsx     | 29 +++++--
 .../__tests__/TaskCard.cost-badge.test.tsx         |  2 +
 .../__tests__/TaskCard.footer-wrap.test.tsx        |  2 +
 .../app/components/__tests__/TaskCard.test.tsx     | 96 ++++++++++++++--------
 .../utils/__tests__/reviewBudgetApproval.test.ts   | 77 +++++++++++++++++
 .../dashboard/app/utils/reviewBudgetApproval.ts    | 43 ++++++++++
 7 files changed, 218 insertions(+), 38 deletions(-)

Fusion-Task-Id: FN-8950

Fusion-Task-Lineage: c587b3b4-03b4-4cd2-9588-826706788eb7

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-11 04:00:16 -07:00
Victor Canô
a09e0cb87f feat(i18n): add Português (Brasil) (pt-BR) locale (#3347)
## Summary

Adds **Português (Brasil)** (`pt-BR`) as a supported locale:

- Selectable as **Translation target language** (project settings) and
as the dashboard / terminal UI language.
- Full machine-drafted catalogs (`app`, `cli`, `common`), disclosed in
`packages/i18n/locales/TRANSLATION_STATUS.md` following the pattern
#1352 established — reviewed for glossary/register consistency (0.18%
untranslated, matching only keys that are empty in `en`), but
native-speaker corrections are welcome.
- Brazilian Portuguese content-language detection (accent-stripped
stopword list — the scorer strips diacritics before matching, so
accented entries never match; `com`/`mais` deliberately omitted to avoid
bare-domain `.com` and French collisions, with regression tests for both
directions).
- `pt`/`pt-PT` browser and environment locales resolve to `pt-BR` on all
three detection paths (`FALLBACK_LNG` routing plus a `pt` branch in
`normalizeToSupportedLocale`, mirroring the existing `zh` handling).
- `README.pt-BR.md` + switcher links in all READMEs, docs updates
(`settings-reference`, `cli-reference`, `i18n-contributing`, `--lang`
help text), changeset (`minor`).

Drive-by fixes bundled: `TRANSLATION_STATUS.md` was missing the `ko`
row; the LanguageSelector endonym test was missing `한국어`;
`docs/i18n-contributing.md` now names the two compile-enforced display
maps (`LOCALE_LABELS`, `localeDisplayName`) a new locale must update;
the `--lang` CLI help text no longer drifts from its validator.

## Test plan

- `pnpm i18n:status` (key parity gate) green; catalogs are
`i18n:sync`-idempotent.
- Updated/extended suites: core `locale-settings`, i18n
`config`/`parity`/`db-banner-catalog`/`i18n-gate-coverage`, dashboard
`useLanguage`/`LanguageSelector`/`GeneralSection.importTranslate`/`detectContentLanguage`
(incl. new pt-BR detection + bare-domain regression tests), CLI
`settings`.
- `pnpm verify:fast` (typecheck, build, boot smoke), `pnpm lint`, `pnpm
check:changesets`, and the bounded `pnpm test` lane all green locally
(the three `test:pg-gate` files fail locally only for lack of a Postgres
instance; they fail identically on clean `main`).


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added Brazilian Portuguese (Português (Brasil)) across the dashboard,
terminal interface, settings, and translation tools.
  * Added Portuguese translations for common interface and CLI content.
* Added automatic Portuguese language detection, locale normalization,
and fallback support.
* Added a Portuguese (Brazil) README with product, setup, and usage
documentation.

* **Documentation**
* Updated language selectors, CLI references, settings documentation,
and translation guidance.
  * Added Portuguese README links to translated documentation.
  * Added French to the documented dashboard language options.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: gsxdsm <gsxdsm@users.noreply.github.com>
2026-08-09 13:33:16 -10:00
gsxdsm
ec52a9131a FN-8859: add native roadmap embeds to chat
Enable roadmap structures to open, preview, and embed directly from chat and dashboard views.

- Register roadmap views as native structures with chat-reference support.
- Add composer previews, in-place dashboard embedding, and drag-safe interactions.
- Document the native-structure plugin contract and add coverage across supported surfaces.

Files changed:
 .changeset/fn-8859-roadmap-native-structure-embeds.md     |   7 +
 docs/PLUGIN_AUTHORING.md                           |   1 +
 docs/dashboard-guide.md                            |   6 +-
 packages/dashboard/app/components/MessageComposer.tsx   |  16 +-
 packages/dashboard/app/components/__tests__/MessageComposer.test.tsx  | 102 ++++++++++-
 packages/dashboard/app/components/__tests__/overflowViewRegistry.test.tsx | 68 +++++++-
 packages/dashboard/app/components/dashboard/MainContent.tsx | 22 ++-
 packages/dashboard/app/components/dashboard/__tests__/MainContent.native-structure-roadmap.test.tsx | 188 +++++++++++++++++++++
 packages/dashboard/app/components/nativeStructureChatRef.ts | 4 +-
 packages/dashboard/app/components/overflowViewRegistry.tsx | 34 ++--
 packages/dashboard/app/plugins/types.ts            |  10 +-
 packages/dashboard/app/utils/__tests__/nativeStructureDrag.test.ts | 46 +++--
 packages/dashboard/app/utils/nativeStructureDrag.ts | 18 +-
 plugins/fusion-plugin-roadmap/src/dashboard-view.tsx | 7 +-
 plugins/fusion-plugin-roadmap/src/dashboard/RoadmapsView.tsx | 22 ++-
 plugins/fusion-plugin-roadmap/src/dashboard/__tests__/RoadmapsView.test.tsx | 51 ++++++
 16 files changed, 560 insertions(+), 42 deletions(-)

Fusion-Task-Id: FN-8859

Fusion-Task-Lineage: 99a7292b-2a06-4325-8131-ca74431214e3

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-08 23:54:40 -07:00
gsxdsm
1f5c44ac71 FN-8826: restore WIP lifecycle badges
Restore visible lifecycle badges for empty-status tasks in WIP lanes.

- Derive fallback badge labels from resolved workflow WIP traits and lane names
- Render the fallback consistently on board cards and desktop/mobile list rows
- Cover empty, custom-lane, populated, and paused status precedence

Files changed:
 .changeset/fn-8826-dashboard-wip-badge.md          |  7 +++
 packages/dashboard/app/components/ListView.tsx     | 37 +++++++++++--
 packages/dashboard/app/components/TaskCard.tsx     | 19 +++++--
 .../app/components/__tests__/ListView.test.tsx     | 61 ++++++++++++++++++++++
 .../app/components/__tests__/TaskCard.test.tsx     | 38 +++++++++++++-
 .../dashboard/app/utils/taskStatusBadgeLabel.ts    | 24 +++++++++
 6 files changed, 177 insertions(+), 9 deletions(-)

Fusion-Task-Id: FN-8826

Fusion-Task-Lineage: fd9c857a-a25f-44d0-ae66-502bc1bd860e

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-07 20:33:42 -07:00
gsxdsm
001bd2b97a FN-8814: prevent stale planning badges during code review
Keep planning and review lifecycle badges unambiguous across board and list views.

- Classify active non-planning optional review gates for badge precedence.
- Suppress stale Planning badges and empty list placeholders during Code Review.
- Cover card, desktop list, mobile list, and workflow helper badge states.

Files changed:
 packages/dashboard/app/components/ListView.tsx     | 32 ++++++--
 packages/dashboard/app/components/TaskCard.tsx     | 24 +++++-
 .../app/components/__tests__/ListView.test.tsx     | 56 +++++++++++++
 .../app/components/__tests__/TaskCard.test.tsx     | 66 +++++++++++++++
 .../app/utils/__tests__/taskProgress.test.ts       | 96 +++++++++++++++++++++-
 packages/dashboard/app/utils/taskProgress.ts       | 34 ++++++--
 6 files changed, 292 insertions(+), 16 deletions(-)

Fusion-Task-Id: FN-8814

Fusion-Task-Lineage: d3a9f087-02ea-4657-aa9e-30a537458cb9

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-06 08:11:22 -07:00
gsxdsm
09e4808d7e FN-8807: revalidate dashboard cards after browser resume
Refresh dashboard task data when browser resume signals arrive, while retaining live SSE mutations across delayed snapshots and cache remounts.

- Revalidate on focus, pageshow, visibility, and SSE reconnect with overlapping-request deduplication.
- Reconcile live task creates and deletes synchronously before caching or pruning mutation fences.
- Cover resume triggers, instrumentation, and cache/remount membership races.

Files changed:
 .../fn-8807-dashboard-resume-revalidation.md       |   7 +
 .../useTasks.resume-instrumentation.test.ts        |  41 +++++
 .../dashboard/app/hooks/__tests__/useTasks.test.ts |  65 ++++++++
 packages/dashboard/app/hooks/useTasks.ts           | 185 +++++++++++++++------
 .../dashboard/app/utils/resumeInstrumentation.ts   |   6 +
 5 files changed, 256 insertions(+), 48 deletions(-)

Fusion-Task-Id: FN-8807

Fusion-Task-Lineage: df8f097e-5067-4fcc-8023-92a2ea329b40

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-05 11:39:23 -07:00
gsxdsm
fa3df0f6bd FN-8798: synchronize planning status across task views
Keep board, list, and task detail planning indicators consistent with fresh lifecycle state.

- preserve authoritative task snapshots across SSE and detail refreshes
- render transient replan planning only for fresh, unpaused planner activity
- pass global pause state to every task-detail host and cover status convergence

Files changed:
 packages/dashboard/app/App.tsx                     |   2 +
 packages/dashboard/app/components/AppModals.tsx    |   3 +
 packages/dashboard/app/components/ListView.tsx     |  11 +-
 packages/dashboard/app/components/TaskCard.tsx     |   7 +-
 .../dashboard/app/components/TaskDetailModal.tsx   |  27 ++-
 .../app/components/__tests__/AppModals.test.tsx    | 201 +++++++++++++++++++--
 .../app/components/__tests__/ListView.test.tsx     |  31 ++++
 .../app/components/__tests__/TaskCard.test.tsx     |  17 ++
 .../app/components/dashboard/MainContent.tsx       |   1 +
 .../__tests__/useTasks-hydration-freshness.test.ts |  58 +++++-
 .../dashboard/app/hooks/__tests__/useTasks.test.ts |  29 ++-
 packages/dashboard/app/hooks/useTasks.ts           |  51 +++++-
 .../utils/__tests__/taskStatusBadgeLabel.test.ts   |  22 ++-
 .../dashboard/app/utils/taskStatusBadgeLabel.ts    |  36 ++++
 14 files changed, 453 insertions(+), 43 deletions(-)

Fusion-Task-Id: FN-8798

Fusion-Task-Lineage: 7defae4e-fb39-4d2c-943a-ef6c1a164bfc

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-04 21:57:00 -07:00
gsxdsm
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 -->
2026-08-04 00:20:08 -07:00
gsxdsm
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**
2026-08-03 00:20:53 -07:00
gsxdsm
bf173dad7c FN-8696: keep reverted tasks out of Done
Keep reverted tasks out of completed views and give every task-detail host a revision recovery path.

- Exclude reverted tasks from complete columns and group them in dedicated recovery lists.
- Provide Delete and Revise actions in cards, lists, docks, modals, and popped-out details.
- Add localized copy, documentation, regression coverage, and a patch changeset.

Files changed:
 .changeset/fn-8696-reverted-task-resolution.md     |  7 ++++
 docs/dashboard-guide.md                            |  4 ++
 packages/dashboard/app/App.tsx                     |  2 +
 packages/dashboard/app/components/AppModals.tsx    |  2 +
 packages/dashboard/app/components/Board.tsx        | 42 +++++++++++++++++++-
 packages/dashboard/app/components/DockTaskList.tsx | 21 +++++++++-
 packages/dashboard/app/components/ListView.tsx     | 18 +++++++++
 packages/dashboard/app/components/TaskCard.css     |  4 ++
 packages/dashboard/app/components/TaskCard.tsx     |  9 +++++
 .../dashboard/app/components/TaskDetailModal.tsx   | 18 ++++++++-
 .../app/components/__tests__/Board.test.tsx        | 46 ++++++++++++++++++++++
 .../app/components/__tests__/DockTaskList.test.tsx | 29 +++++++++++++-
 .../app/components/dashboard/MainContent.tsx       |  4 ++
 .../app/components/overflowViewRegistry.tsx        |  3 ++
 .../app/components/useRightDockController.tsx      |  9 +++++
 .../app/utils/__tests__/taskRevert.test.ts         | 16 +++++++-
 packages/dashboard/app/utils/taskRevert.ts         | 18 +++++++++
 packages/i18n/locales/en/app.json                  |  3 ++
 18 files changed, 249 insertions(+), 6 deletions(-)

Fusion-Task-Id: FN-8696
Fusion-Task-Lineage: 7c9550fb-ef8e-4422-8ec1-0de1665413a3
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-01 13:30:18 -07:00
gsxdsm
7dbcff139c fix: board lane counts and card glow never exceed live running agents
Operators summing the lane-header executing counts saw 10 active cards
under a 9-slot concurrency cap and read it as a capacity breach. The
header unioned the shared Running predicate with the card activity-chrome
predicate, which glowed needs-replan parks (FN-8494) and fresh
planner-log windows (FN-8300) that hold no concurrency slot.

isTaskAgentActive's positive arm now delegates to the shared
isRunningAgentTask predicate (footer/admission truth) and Column headers
count only that predicate, so glow and counts are a strict subset of the
live-agent population. Idle replans render the existing "Queued to
revise" waiting label instead of activity chrome.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 11:03:04 -07:00
gsxdsm
df56790c9b FN-8676: fix Quick Add upward menu anchoring
Keep Quick Add portal menus attached to their triggers when they open upward.

- Position upward menus with bottom anchors rather than height-capped top offsets.
- Apply shared positioning to Quick Add pickers and the custom model dropdown.
- Add desktop and mobile regression coverage and a patch changeset.

Files changed:
 .changeset/quick-add-upward-menu-anchor.md         |  7 ++++
 .../app/components/CustomModelDropdown.tsx         | 18 ++++++--
 .../dashboard/app/components/QuickEntryBox.tsx     | 48 +++++++++++++++-------
 .../__tests__/CustomModelDropdown.test.tsx         | 40 +++++++++++++++---
 .../components/__tests__/QuickEntryBox.test.tsx    | 46 ++++++++++++++++++---
 .../app/utils/__tests__/fixedMenuPosition.test.ts  | 12 +++---
 packages/dashboard/app/utils/fixedMenuPosition.ts  | 22 ++++++----
 7 files changed, 151 insertions(+), 42 deletions(-)

Fusion-Task-Id: FN-8676

Fusion-Task-Lineage: e48b4b2c-9675-42c6-8712-83cb4a0ca3d8

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-01 01:00:57 -07:00
gsxdsm
7e4e892fce fix(dashboard): one queued badge family — map raw 'queued', fold queued-to-plan into the status badge
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 20:33:34 -07:00
gsxdsm
500f40e65b fix: descriptive waiting badges (Queued to revise / Queued behind FN-X) + dependency-free blocked exits replan calmly
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 17:37:38 -07:00
gsxdsm
475bb2d641 FN-8637: restrict Quick Add Start to manual-intake workflows
Restrict Quick Add Start eligibility to verified manual intake lanes.

- Require the server-derived manualIntake flag instead of hold alone.
- Preserve Coding Ideas routing while hiding Start for Coding's merged planning lane.
- Cover desktop and mobile eligibility behavior and document the updated rule.
- Add a patch changeset for the corrected workflow gating.

Files changed:
 .changeset/fn-8637-quick-add-start-manual-intake.md       |  7 ++++
 docs/dashboard-guide.md                            |  2 +-
 packages/dashboard/app/components/QuickEntryBox.tsx     | 21 ++++++------
 packages/dashboard/app/components/__tests__/Column.test.tsx       | 20 ++++++++---
 packages/dashboard/app/components/__tests__/ListView.test.tsx     | 40 ++++++++++++++++++----
 packages/dashboard/app/components/__tests__/QuickEntryBox.test.tsx    | 21 +++++++++---
 packages/dashboard/app/utils/__tests__/quickAddStart.test.ts      | 38 ++++++++++++++++++--
 packages/dashboard/app/utils/quickAddStart.ts      |  9 ++++-
 8 files changed, 128 insertions(+), 30 deletions(-)

Fusion-Task-Id: FN-8637

Fusion-Task-Lineage: 9652d7d8-f954-49e8-9a76-a2421654baae

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-31 17:27:43 -07:00
Phil Larson
920d68e10f fix(dashboard): expose column roles to browser bundle (#3151)
## Summary
- export the browser-safe `@fusion/core/column-roles` subpath
- keep Vite/Vitest aliases ahead of broad `@fusion/core` aliases
- restore production dashboard builds after task undo classification
adopted shared column-role helpers

## Test plan
- `node scripts/check-no-node-only-core-imports-in-dashboard.mjs`
- `FUSION_DASHBOARD_DEEP=1 pnpm --filter @fusion/dashboard exec vitest
run app/utils/__tests__/taskRevert.test.ts --pool=threads
--maxWorkers=1`
- `pnpm --filter @fusion/core typecheck`
- `pnpm --filter @fusion/dashboard typecheck`
- `CI=true pnpm check:changesets`
- `pnpm build`


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
  * Fixed dashboard build compatibility for browser-based environments.
* Improved reliability when importing column role functionality across
supported application components.

* **Refactor**
* Made column role utilities available through a dedicated browser-safe
entry point.

* **Chores**
* Updated development and test configurations to consistently resolve
the new entry point.
* Documented the browser-safe module classification and recorded the
release patch.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-31 06:46:19 -07:00
gsxdsm
a8dae03fdb fleet(dashboard): taskRevert 2 → 0 — the recorded blocker named the wrong variable (#3129)
The largest remaining census cluster. Deferred twice, with a blocker
that turns out to be false **in the same component where its
counter-example already lives**.

## What the earlier notes got right

`detailColumnFlags` describes the **modal's own task**, and the column
classified here belongs to a **neighbour**. Supplying it would answer
*"is this neighbour finished?"* with a different row's traits — wrong on
data, not merely stale on vocabulary. That reasoning stands and I kept
it.

An earlier pass also converted this, left the parameter unsupplied, and
**reverted it** — correctly. An unsupplied optional parameter is
strictly worse than the literal: the guard is gone, the census counts a
conversion, and the behaviour is the legacy fallback forever. That rule
is why the wiring ships in this same commit.

## What the conclusion got wrong

> "A correct conversion needs per-**neighbour** flags — which the modal
does not have and should not fetch mid-render."

`columnFlagsByTaskId` is a per-task map. It is **already a prop** of
`TaskDetailModal` (declared :367, destructured :727), and the call site
at :992 sits **below** that destructure.

And `TaskDetailModal` already uses it exactly this way, for the
near-duplicate canonical:

```ts
columnFlagsByTaskId?.get(nearDuplicateCanonical.id)
```

…under a note observing that *its* blocker had been *"asserted from the
shape of the problem rather than tested against what was in scope."*
Same assertion, one function over. So the supplier the earlier note went
looking for exists, is per-neighbour, and needs no fetch.

## What it fixes

This lookup skips **finished** candidates so a done/archived prior undo
attempt never renders as an active "Undo task" link. On a board that
renames those lanes it matched neither — a finished undo task kept
rendering as open, which is precisely the stale affordance the
function's own header says it exists to prevent.

## Census

| | before | after |
|---|---|---|
| `taskRevert.ts` | 2 | **0** |
| repo backlog | 17 | **15** |

## Measured

- 4 new cases; `taskRevert.test.ts` **11/11 pass**.
- **MUTATION**: restoring the literal pair fails the renamed case.
- **The negative is load-bearing.** The map is fail-soft, so a candidate
it does not cover must still be treated as **open**, not skipped. A
conversion that skipped unknown candidates would *hide live undo links*
— failing in the direction nobody reports.
- A **control** pins that an unwired caller (no flags at all) still
skips the legacy ids, so the optional parameter cannot regress default
boards.
- `TaskDetailModal` suites — **31 files / 664 tests pass**.
- `tsc --noEmit -p tsconfig.app.json` clean; census `--strict`,
`check-lane-wiring`, `check-fnxc-future-dates` clean.

## Pattern worth noting

This is the fourth deferral this session whose stated blocker had
dissolved or misidentified itself, and the second where the
counter-example was already in the same file. The common shape: a note
records *why* something is blocked, is accurate when written, and is
never re-checked — so the block outlives its cause. Re-reading them cost
minutes each and returned two real conversions.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 05:58:44 -07:00
gsxdsm
7763063cbd docs(task-revert): the recorded conversion "blocker" is a correctness boundary, not a cost (#2868)
`findOpenUndoTaskForSource` carries a note saying its conversion is
blocked on **hoisting the flags derivation** in `TaskDetailModal` —
framed as a hook-ordering cost in a 5000-line component, i.e. something
somebody could pay. I went to pay it, and the framing is wrong in a way
that would have produced a **worse defect than the literal**.

## Why hoisting would not unblock it

This function scans the `tasks` list for **other** tasks pointing back
at the source, so the column it classifies belongs to a **neighbour**.
`detailColumnFlags` describes the **modal's own** task — its own FNXC
note says exactly that, and it is guarded by `detailFlagsAreForThisTask`
*precisely because* using it for anything else is wrong.

So supplying it here would answer *"is this neighbour finished?"* with
the modal task's traits. On a project where two workflows reuse a column
id, an open undo task gets classified by a workflow it does not belong
to, and the "Undo task" affordance vanishes or persists wrongly.

**Wrong flags are worse than a stale vocabulary.** The literal is at
least uniformly legacy; this would be wrong *on data*. It is the
flags-for-the-wrong-row shape — the same question I have been applying
all program: *do these flags describe the column of the row this guard
is about?*

## What a correct conversion needs

Per-**neighbour** flags: the caller would have to resolve each
candidate's own workflow, which the modal does not have and should not
fetch mid-render. Until a per-task lane map exists at that call site,
**the literal is the right answer**.

## Why this is worth a PR rather than silence

The previous note is an invitation. Somebody following it would land a
plausible-looking conversion, drop the census count by one, and
introduce a data-dependent bug that only appears on multi-workflow
projects — the exact "conversion that scores as a win" pattern
documented in `docs/solutions/workflow-learnings/`. The census entry
stays and is now labelled as **accurate debt** rather than a missed
conversion.

No behaviour change; comment only. Dashboard app `tsc` clean, `pnpm
lint` clean, `app/utils` suites 59 files / 689 tests green.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 16:17:37 -07:00
gsxdsm
109204c590 fix: the query class — three sweeps that never ran on a renamed board (#2818)
Three sweeps that **never ran at all** on a renamed board, plus the
shared answer the rest of the class needs. Consolidated from three
handoff branches so the helper appears once. #2811 merged, so this is my
only open PR.

`#2800` measured this class and shipped evidence deliberately without
conversions: `listTasks({ column: "<literal>" })` filters in the store,
so on a renamed board the read returns an **empty array** and the sweep
it feeds does nothing. The census scores the comparison *inside* the
loop, never the query above it.

## What was broken

| file | census count | what actually happened on a renamed board |
|---|---|---|
| `backlog-pressure-reporter.ts` | **0** | both reads empty, ratio
computed as 0/0 — **the alert never fired**, on a board that may be
under exactly the pressure it reports |
| `stale-task-reporter.ts` | **0** | both reads empty — **no stale-task
signal ever raised**, where work is most likely sitting unnoticed |
| `restart-recovery-coordinator.ts` | flagged | sweep never ran — **an
engine restart left interrupted tasks stuck with no requeue** |

Two of the three have a census count of **zero**. They contain no
lifecycle comparison at all, so they have never appeared in the backlog,
in a per-file list, or in any "N → 0" claim — and were completely inert.
**A file at zero is not evidence of anything.**

## The shared answer, and what it is not

Every existing resolver answers a **per-task** question. A query has no
task in hand, so it needs the project-level one: every column any
workflow declares for a role, unioned with the legacy ids so a board
mid-rename still finds rows under the old ones. The set is never empty,
so a caller cannot accidentally query nothing.

The header states what it is **not**: answering a per-card question from
the union would mark a card as review because some *other* workflow
calls its column review — the flat-set mistake this program has made
four times.

## The finding that generalises: the query is rarely the whole defect

`stale-task-reporter` **still reported zero after the query was fixed**
— `getTaskAgeStalenessSignal` defaults to the legacy pair, so a card the
query now returned was refused inside the signal. Converting only the
query would have looked like a fix and changed nothing.

That is a caveat on #2800's approach, offered as refinement rather than
correction: **asserting the query ARGUMENT is right when pinning a known
defect** (the outcome is 0 either way) **and insufficient when proving a
fix**, because the outcome is the only thing that distinguishes a real
conversion from a deeper one. All three conversions here assert
outcomes.

`restart-recovery` had three layers — query, a redundant re-assertion
(deleted; a test pins the `paused` guard it did contribute), and a move
destination that was **already** resolved but whose warning comment was
stale. A stale warning is its own hazard: it told the next reader a
defect existed where none did.

## Verification

- helper **8 passed** · three reporter/coordinator suites **29 passed**
- `pnpm test:gate` **161 / 13 / 487 / 71** · lint clean · `--strict`
exits 0 · four `tsc` targets clean
- each conversion revert-proven independently; the failing case is named
in each test header

## Two mistakes worth recording

**The helper's own test caught a bug in it.** My first draft wrapped the
definition loop in one `try`, and `parseWorkflowIr` **validates** rather
than parses — one malformed row would have returned legacy-only lanes
for *every* workflow, indistinguishable from the bug it exists to fix.
Now isolated per definition.

**I clobbered the core barrel** by taking `index.ts` wholesale from a
handoff branch, dropping two exports `main` had added since; three
packages stopped compiling. Taking a file from another branch takes its
whole contents, including what is now stale — for a barrel that is
nearly always wrong. Re-applied as a single edit on top of `main`.

## Not included

`self-healing.ts`'s 49 — actively owned and mid-conversion; an outside
refactor there produces conflicting halves of one sweep.
`project-engine.ts` (7) and `executor.ts` (2) need their own read of
what each sweep does with the rows, which these three are the argument
for.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 13:55:10 -07:00
gsxdsm
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>
2026-07-30 12:46:12 -07:00
gsxdsm
e84e9d7f60 fix: the caller audit — five unwired parameters, five defects in their callers (#2803)
Seven fixes that were sitting on separate handoff branches with no owner
while `main` moved. Consolidated, rebased onto current `main`, and
verified **together** rather than only per-branch. The individual
branches remain if a subset is preferred.

This is the same consolidation that got `batch-core` and #2787 adopted.
**Close it if it breaks queue policy** — the branch keeps the work safe
either way.

## Where these came from

#2787's review found an optional parameter whose production caller never
passed it. That is a class, so I ran it against everything I had landed
and found five more. **All five turned out to have their real defect in
the CALLER, not the parameter** — in four of them the parameter was
unreachable:

| unwired parameter | what was actually wrong |
|---|---|
| `blocker-fanout.escalationColumns` | the hold default made the count
zero — **no bottleneck warning was emitted at all** |
| analytics `columnFlagsByName` | routes never built a map — **0
in-progress / 0 in-review beside correct cost totals** |
| `isLegacyAutoMergeStampCandidate` | the read **queried a column a
renamed board does not have**, so the backfill iterated nothing |
| `rankAssignedTasksForWakeDelta` | `getTasksByAssignedAgent`'s
`excludeArchived` used the literal — **archived cards returned as open
work** |
| `duplicate-intake.columnFlagsByColumnId` | intake could **archive or
soft-delete a newly created task** as a duplicate of finished work |

The heuristic worth keeping: **an optional parameter no production
caller fills is a marker pointing at an unexamined caller.** The census
cannot see any of these five — every gate is a `Set`/array literal or a
query filter, i.e. a definition rather than a comparison.

## Also included

- **`executor.ts`** — the stale-spec guard did the exact thing its own
comment forbids: on a renamed board it ran on a LIVE task and pulled it
out of execution into replan. `activeMergeStatuses` protected merging
cards *by accident*, which is why the symptom looked arbitrary.
- **`register-project-routes.ts`** — project health reported **0 active
tasks**; its list also still contained `triage`, dead since U11.
- **`dashboard/app/utils/taskTiming.ts`** — a **second copy** of
`getTotalAgentActiveMs`. Core's was converted; the card chip imports
this one, so the census counted the site as done while the rendered
number stayed keyed on `"in-progress"`.

## Verification

Verified as a set: `pnpm test:gate` **161 / 13 / 487 / 71** · core
suites **15 passed** · engine **7** · dashboard **12** · four `tsc`
targets clean · lint clean · census `--strict` exits 0.

Each fix is revert-proven individually; the specific case that fails is
named in each test header.

## Two honesty notes

**Three guards here are structural, not behavioural, and say so in their
headers.** `sanitizeAgentTaskLinks` is a closure inside
`createApiRoutes`; the analytics aggregators need a live
`AsyncDataLayer`; the stale-spec guard sits deep inside `execute()`.
Each ratchet fails on revert — verified — but none is an end-to-end
proof, and the headers state which half they cover.

**One of my behavioural test sets would have lied.** The intake-dedup
cases drive `findSameAgentDuplicates` directly; I removed the wiring to
measure the revert and **they stayed green**, because they pin the
predicate and not the caller. That is the exact illusion this audit was
chasing, reproduced in my own file. The forward now has its own
structural check.

## Deliberately not included

`worktree-pool.ts:1205` — it **fails safe** (a missed match protects a
branch from cleanup rather than deleting it) and sits in the merger's
branch-reaping path where the opposite error destroys work. That
deserves its owner's judgement, not a drive-by conversion.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 12:02:53 -07:00
gsxdsm
76d77da4d1 fleet: taskSorting + TaskReviewTab 8 → 0 — and Board was faking a column id to force done-sorting (#2744)
Two app-side clusters, 8 → **0**, plus a caller-side hack retired.

## What was broken

**`TaskReviewTab.tsx`** — three of its four questions were `task.column
=== "in-review"`, driving the **Create-PR button**, the **"frozen on
entry to review"** auto-merge hint, and **PR-feedback addressing**. On a
renamed review lane all three took their non-review branch: the button
was absent, and the hint claimed the effective auto-merge value was
*not* frozen when it was.

**`taskSorting.ts`** — `isReviewColumn` decides whether merging cards
float to the top of a lane. Keyed on the id it silently stopped doing
that on any renamed review lane, so the operator loses the "what is
merging right now" ordering with nothing failing.

Both follow the shape this code already established: **caller supplies
the trait, default to the legacy id**. `columnFlags` on the review tab
is optional and wired from `TaskDetailModal`, which already resolved it
for `canEdit` and the actions menu.

## A synthetic column id, retired

`Board.tsx` forced done-sorting by passing the **literal `"done"`** as
the column argument for any complete-flagged lane:

```ts
grouped[column.id] = isWorkflowDoneLikeColumn
  ? sortTasksForDisplayColumn(grouped[column.id] ?? [], "done", doneSortMode)
  : sortTasksForDisplayColumn(grouped[column.id] ?? [], column.id as ColumnType, ...);
```

A synthetic id standing in for a trait — so a custom complete lane
sorted correctly only because its caller **lied about its name**. Both
call sites now pass the real column id and state the trait. (Board's own
census count stays at 2: those two literals were the synthetic ids and
are gone; the 2 remaining are different sites.)

## Revert proof

| reverted | failure |
|---|---|
| `task.column === "in-review"` on the Create-PR guard | `Unable to find
an element by: [data-testid="task-review-create-pr"]` |
| same, on the auto-merge hint | `expected 'Effective: Auto-merge off'
to contain 'frozen on entry to review'` |

A third case pins that the widened test does not treat *every* column as
review.

**None of the 45 existing `TaskReviewTab` cases could have caught this**
— `columnFlags` is optional and they all omit it, so they assert the
legacy fallback. That is the same blind spot as the reconciler's 33 in
#2737, and it keeps recurring: an optional-flags seam means the existing
suite stays green through the conversion *and* through a broken one.

## A process failure worth recording

**I lost this conversion once and had to redo it.** I overwrote four
files with their `origin/main` versions to check whether a failing test
was pre-existing, then "restored" with `git checkout HEAD -- <dir>`.
HEAD was still `origin/main` because I had not committed, so that
**discarded the work**.

Same class as the shared-stash incident two PRs back: an implicit or
positional restore reference. The fix is ordering, not care — **commit
before any baseline comparison**, so `git checkout HEAD -- <file>`
restores my work rather than main's. This PR's commit was created before
the comparison for exactly that reason, and the note is in the commit
message so the next person hits it there too.

## Verification

`pnpm test:gate` **GREEN** (158 + 10 + 487 + 71) · **232 passed** across
TaskReviewTab / taskSorting / Board suites · dashboard `tsc -p
tsconfig.app.json` clean · `pnpm lint` clean · census `--strict` exits
0.

The 1 `board-mobile` failure is **pre-existing** — verified by swapping
in clean `origin/main` copies of all four files and reproducing it.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 08:06:04 -07:00
gsxdsm
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>
2026-07-30 02:47:26 -07:00
gsxdsm
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.
2026-07-30 02:38:58 -07:00
gsxdsm
cef1b08af3 U12: the census baseline follows the count down — and goes in the merge gate (#2661)
Coordinator item 2. The census had the right mechanism and no teeth.

## The gap

`--strict` already fails on a rise **and** on an unrecorded drop — that
logic was correct. But nothing blocking ran it, so the baseline drifted
to **854 while the tree held 787**. That is **67 guards of regression
that would have merged silently**: a high-water mark wearing a ratchet's
name.

This is the same shape as the ceilings I tightened in #2647, one level
up. Worth saying plainly: I fixed the vitest ratchet's slack by hand and
did not check whether the *authoritative* instrument had the same
problem. It did, and by a much larger margin.

## Three changes

1. **`--strict` runs in `test:gate`.** The baseline cannot go stale
again without a red gate.
2. **Baseline re-recorded: 854 → 785** across 14 files (`triage` 38 →
9).
3. The single RISE is resolved honestly rather than absorbed.

## The +3 investigation

One file rose: `register-task-workflow-routes.ts` **22 → 23**. #2621
replaced one `task.column === "todo"` with `task.column === "triage" ||
task.column === "todo"` — a net **+1** that also reintroduced a `triage`
literal, while the PR title reported *"count 0 → 0"*.

Not an accusation. There was no gate for the author to check against,
and a hand-counted claim in a PR title is exactly the thing that goes
wrong without one. Change 1 is the fix.

**The literal is justified and stays**, marked `DELIBERATE-LITERAL`
rather than converted. It is the **v1-IR arm**: a v1 workflow yields no
role assignments, so `resolveLifecycleColumns` returns nothing and the
legacy pre-implementation ids are the only pre-WIP signal available. The
`else` branch directly below already resolves intake/hold for every v2
workflow. Converting this arm would not finish anything — it would
delete the only answer v1 boards have and admit
`in-progress`/`in-review` cards into a rebound that clears worktree,
branch and retry counters, which is the regression #2621 was fixing.

## Both directions proven

| direction | probe | result |
|---|---|---|
| rise | add `t.column === 'in-review'` | `live-agent-count.ts: 6 -> 7`,
exit 1 |
| drop | convert one guard | `self-healing.ts: allows 111, tree has
110`, exit 1 |

**The drop probe took three attempts to test honestly, and the first two
"passed" while proving nothing:**

1. I renamed a receiver (`task.column` → `Probe`) — the classifier is
**fail-closed**, so an unknown receiver is still counted and the number
never moved.
2. I targeted a site in `hold-release.ts` that carries a
`DELIBERATE-LITERAL` marker — not counted as a column guard at all, so
removing it changed nothing.

Only removing a counted comparison outright moved the number. Both false
negatives came from me assuming the probe worked because the command
exited the way I expected.

## On auto-rewrite vs fail-and-instruct

You offered either. The script already does **fail-and-instruct**, with
`--update-baseline` as the explicit re-record, and I kept it that way
rather than making the test rewrite the baseline during a run.

Reason: a silent downward rewrite means a conversion PR's own diff never
shows the number moving, so "census before/after in the PR body" becomes
unverifiable — the reviewer would have to re-derive it. Failing with the
new number in the message puts it in the diff where a human sees it, and
it costs one command.

## Verification

`pnpm lint` clean. `pnpm test:gate` green with the census in it — `every
file matches its baseline exactly` (10 / 132 / 487 / 71).

Note for the fleet launch: with `--strict` gating, **every** conversion
PR must now re-record the baseline in the same PR. That is the intended
cost, and it makes the fleet's "baseline must shrink by exactly the
converted count" rule mechanically enforced instead of a review
instruction.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 00:47:31 -07:00
gsxdsm
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>
2026-07-30 00:13:58 -07:00
gsxdsm
890d588891 Phase B — Column.tsx 2→0 and taskActivity.ts 2→0 (U11's cluster to zero) (#2636)
**Claimed:** `Column.tsx`, `taskActivity.ts` — both to zero.

| file | before | after |
|---|---:|---:|
| `packages/dashboard/app/components/Column.tsx` | **2** | **0** |
| `packages/dashboard/app/utils/taskActivity.ts` | **2** | **0** |

## `Column.tsx` — two different fixes, because the two sites are
different problems

**The preserve-progress prompt** routed through
`isPreImplementationColumnRole`. This is the *same* question that helper
was written for — ListView asks it about a move target, Column asks it
about itself — and the degraded id sets are identical (`{todo,
triage}`), so the consolidation is exact.

I verified the sets matched **before** consolidating, because the
sibling case is not interchangeable: `isPreExecutionHoldColumn` in
`TaskContextMenu` drives the Plan affordance and its degraded set is
`{triage}` alone. Routing *that* through this helper added `plan` to
flagless `todo` cards, caught by an existing test. **Same shape,
identical trait path, non-interchangeable fallbacks.**

**The legacy-board arm** (`workflowMode || column === "triage"`) —
deleted, on the third attempt.

I deleted it twice before and reverted both times because four Column
tests render without `workflowMode`. That was the delete-only rule
working, but **my conclusion from it was wrong**: a behaviour change
means the branch was not dead *for those callers*, and the callers are
**fixtures, not production**. Board is Column's only consumer and passes
`workflowMode` at all three render sites. Defending an unreachable arm
so four tests keep passing preserves the tests, not the behaviour.

Two notes for anyone converting the remaining dashboard files:

- I did **not** default `workflowMode` to `true`, which was the tempting
one-liner. `isArchived`, `isHoldColumn` and `isWipProcessingColumn` all
switch on that same flag, so a global default would silently reinterpret
every other fixture in an 85-test file.
- **"Four tests break" was itself an underestimate.** Two more FN-770
fixtures surfaced only after the first two were fixed, because they
render their own explicit `column="triage"` block instead of using
`defaultProps`. The blast radius only became accurate by fixing it in
waves.

## `taskActivity.ts` — composed, not copied

The degraded arm now composes `utils/columnRoles`' predicates instead of
naming ids. **No local copy** — which is the failure mode #2625 hit from
the other direction.

Equivalent *by construction*:

| lane | composition | resolves to |
|---|---|---|
| intake | `isIntakeColumnRole(undefined, col)` | `triage` |
| hold | `isPreImplementationColumnRole(...)` **and not** intake |
`todo` |

reproducing `col === "triage" || (col === "todo" && isReplanning)`
exactly, since the shared pre-implementation set is `{todo, triage}` and
the shared intake id is `triage`.

Deliberately phrased as *"pre-implementation and not intake"* rather
than a second id list: if either shared set changes, this composition
follows it instead of silently disagreeing with the file next door. That
disagreement is precisely what bit the `TaskContextMenu` consolidation
above.

**I previously reported this site as blocked on `TaskCard.tsx` (U12's)**
— on the theory that the arm could only die once every caller supplied
resolved flags. Wrong framing: the arm doesn't need to become
*unreachable*, it needs to stop *naming ids*. Composing the shared
predicates does that without touching any caller.

## Verification

**1139 of 1141** green across `app/utils`, `Column` and `TaskCard`
suites. The two `TaskCard` failures are **pre-existing** — verified by
stashing this change and re-running, where they fail identically.
Dashboard app typecheck and lint clean.

Takes U11's cluster to zero except `TaskContextMenu.tsx`, whose
remaining site is covered in **#2626** and whose second site is a
documented non-consolidation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 22:37:43 -07:00
gsxdsm
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>
2026-07-29 21:23:46 -07:00
gsxdsm
bad35775a1 Drift 3/4: Task Detail intake affordances from traits — the UI half of the #2571 approve/reject stall (4→3) (#2577)
## Drift conversion 3 of 4 — Task Detail, the UI half of the #2571 stall

**Stacks on #2566.** Merge order: #2558 → #2566 → this. (#2571 is the P0
and is independent — merge it first regardless.)

### Convergence number

Live-code `column === / !== "todo" | "triage"` in `TaskDetailModal.tsx`:
**4 → 3**

All three survivors are the documented no-metadata fallback, same shape
as TaskCard and ListView: `workflowMoveMetadata` is `null` until the
detail payload resolves, and a bare trait read would drop these controls
during that window.

### This is the UI half of the P0

`isAwaitingApproval` and the standalone Delete button were both gated on
`task.column === "triage"`. On the merged lineage (#2515) that is false
for every card, so a task parked `awaiting-approval` **loses its
Approve/Reject controls in the one surface that shows them**.

#2571 fixes the routes that *reject* those actions. This fixes the UI
that stops *offering* them. Either half alone leaves the operator stuck
— one with buttons that 400, the other with no buttons at all.

### Three conversions

| site | was | now |
|---|---|---|
| `isAwaitingApproval` + standalone Delete | `column === "triage"` |
resolved column's `intake` |
| `requiresExecutionModeReplan` | `todo \|\| in-progress` | `hold \|\|
countsTowardWip` |
| move-progress prompt | source column ids | **target** column's flags |

The replan rule is "this card may already hold a plan or a live
execution context" — which the traits state directly;
`todo`/`in-progress` was the Default workflow's spelling of it.

The move prompt is the mistake I made first in TaskCard, where its
regression test caught that the site tests the move **destination**, not
the card. Carried the lesson here rather than repeating it.

### Tested through a pure seam, and why

`requiresExecutionModeReplanForTest` is exported so the rule can be
asserted as a function of (column id, flags).

Asserting it through the modal means booting async detail loading to
observe one boolean — and an earlier DOM-level attempt at exactly this
class of assertion (in #2566, ListView) **passed with the conversion
reverted**, because the text it matched also appears in a column header.
I am not repeating that. A seam discriminates; that DOM test did not.

Revert-proof: restore `column === "todo" || column === "in-progress"`
and the merged-column case fails, because that column is `intake + hold`
and carries no `countsTowardWip`. The suite also pins that the rule
still **narrows** (a complete lane needs no replan) and that the legacy
fallback is unchanged when flags are absent.

### Verification

`pnpm test:gate` (414 + 10 + 71), `pnpm lint`, dashboard typecheck
green.

**No new failures**: `TaskDetailModal.rendering.test.tsx` reports the
same 28 pre-existing failures with and without this change, diffed by
test *name* against a stashed clean tree.

### Drift set status

| file | before | after | PR |
|---|---|---|---|
| `TaskCard.tsx` | 8 | 3 | #2558 |
| `ListView.tsx` | 5 | 3 | #2566 |
| `taskActivity.ts` (found underneath) | 1 | 1 | #2566 |
| `TaskDetailModal.tsx` | 4 | 3 | this |
| `register-task-workflow-routes.ts` | 10 | 11 | #2571 (P0, widened on
purpose) |

Survivors are no-metadata fallbacks except the routes, where the guards
deliberately accept resolved-intake **or** `triage` so a P0 fix cannot
reject anything previously allowed. Those retire together once the
legacy id is gone board-wide.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 10:26:21 -07:00
gsxdsm
99c9f14ee0 feat: run Plan Review in the planning lane with a Plan Review badge (#2462)
## What

Plan Review, planning, and the replan loop move from the implementation
column into the **planning lane** (`todo`), so a task under
specification never holds a WIP slot. The card crosses into
`in-progress` exactly once, at `parse`, released by the scheduler.

Operators also finally see a **Plan Review** badge while the gate runs —
it was previously invisible on the default workflow.

## The part that made it possible

Moving the node is ten lines. It was attempted three times and reverted
each time, because a graph run with no durable continuation replayed
from `start` and dragged an in-progress card *backward* out of the WIP
column, firing `abort-on-exit` and stranding it in a pre-WIP column with
no releaser.

So this PR adds the graph **entry contract** —
`resolveColumnResumeNode`:

| Card is in | Resumes at |
|---|---|
| `triage` | `start` |
| `todo` | `plan` |
| `in-progress` | `parse` — never re-plans, never moves backward |
| `in-review` | first review node — gates are not skipped |

`ir.columns` is ordered and that order is the lifecycle order; rework
and failure edges are excluded so the entry point is always the main
path. The proof it's the right fix: **`executor-task-done-invariant`
passes unmodified** after failing every previous attempt.

## Also in here

- **Release gate narrowed twice.** `isUnplannedForExecution` applies its
pre-release plan-review gate only when the node's column equals the
card's column *and* the group is enabled for the task. The enablement
check fixes a real deadlock — a task with Plan Review toggled off was
held forever waiting for evidence nothing would ever write.
- **Badge cleanup.** Gate badge reads "Plan Review" instead of the
ambiguous "Reviewing" and no longer hides behind a lane restriction; the
status badge stops duplicating it; `planning` renders as "Planning"
instead of the raw engine token.
- **Coding (Ideas)** renames its planner column to "Planning" (id `todo`
unchanged) and loses its private planning-node re-home — the graph it
clones is already plan-in-place.
- **New sweep** `reconcileUndeclaredTaskColumns` re-homes a row whose
column its workflow no longer declares. Written for a follow-up, kept
because it makes any column edit survivable.

## Test changes

Scheduler and release fixtures now model a card whose Plan Review passed
— the state every real card is in when the capacity sweep sees it. A
held unreviewed card is the gate working, and that path stays owned by
`pre-release-plan-review.test.ts`.

New `workflow-graph-entry-contract.test.ts` covers the invariant at
every lifecycle position, plus the gap-column and remediation-node
cases.

## Verification

Gate 299 + 70 + 10, dashboard badge suites 672, engine
workflow/entry/executor suites 147, core 122. Lint and typecheck clean.
Full engine suite sits at the pre-existing baseline (notifier /
plugin-runner / notification-service, untouched by this).

## Follow-up

Removing the Todo column entirely is a separate ~207-site
lifecycle-vocabulary refactor — planned in
`docs/plans/2026-07-26-001-refactor-workflow-owned-lifecycle-plan.md`
(companion docs PR).

🤖 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**
* Plan Review now runs in the Planning lane before implementation
begins.
* Cards resume from their current workflow column without replaying
earlier steps.
* Added automatic recovery for cards stranded in outdated workflow
columns.
* **Improvements**
  * Renamed the Coding (Ideas) planner column to “Planning.”
* Refined Plan Review gating to respect enabled settings and the card’s
current column.
* Updated planning and Plan Review badges for clearer, consistent labels
across cards and lists.
* **Bug Fixes**
* Improved workflow transitions and release behavior around planning,
review, and execution.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 23:42:46 -07:00
gsxdsm
0d40bc41d1 fix(dashboard): stop badging the merge-blocker in-review stall
A pre-merge check reporting a blocker is the ordinary in-review resting state
rather than an exceptional one, so badging it marked routine cards abnormal.
Operator-requested removal.

Suppression is expressed as a code list next to the existing
no-worktree-no-merge-confirmed entry, so both surfaces that gate on
shouldShowInReviewStallBadge -- the card header badge and the Task Detail
diagnostic block -- drop it from one place. The previous carve-out only
suppressed merge-blocker while isActiveMergeStatus(task.status) held; that is
gone, and the test row that used to expect a badge for status undefined now
asserts the suppression is unconditional.

task.inReviewStall keeps being computed and stored -- only the affordance is
withheld -- so the Review tab, run-audit, and self-healing are unaffected.
No dead CSS: the shared .in-review-stall classes still serve the remaining
codes and no --merge-blocker rule existed. The card test asserts no empty
badge shell is left behind.

Two TaskDetailModal cases used merge-blocker only as a fixture for the
diagnostics row and jump-to-activity-entry behavior; repointed at
transient-merge-status-no-owner so they still guard what they were written for.

Note for follow-up: this badge was the board's only signal for a card blocked
on a failed pre-merge step. self-healing's needsOperatorBypass comment already
flags that such cards "sit silently" behind a generic badge; with the badge
gone they show nothing at all on the board.

Verified: tsc -p tsconfig.app.json clean, pnpm lint clean, 693 tests passing
across the 6 affected suites.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 11:47:04 -07:00
gsxdsm
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>
2026-07-26 11:17:52 -07:00
gsxdsm
f5163d8351 fix(dashboard): resync SSE state on reopen and bound the service-worker cache
Follow-up to f157bf7460, fixing the regressions an adversarial review found in
the mobile tab-discard work.

- SSE hidden-suspend dropped events silently: the per-task/run log streams are
  live-only with no replay, and many subscribers had no onReconnect, so a 60s+
  hidden window left invisible gaps in logs, a never-rendered approval banner,
  a diverged chat transcript, and a missed merge advance notice. Every
  subscriber now resyncs authoritative state on reopen.
- useAgentLogs refetches its authoritative page on reconnect and reports
  hasMore truthfully once paging reaches the first entry.
- Agent run logs are windowed rather than discarded, so the head of a long run
  stays reachable.
- lastFetchTimeMs is seeded from the cached envelope's savedAt, so a hydrated
  stale snapshot no longer renders every in-progress card as stuck.
- MAX_IMMUTABLE_CACHE_ENTRIES lands as 200; it was committed as Infinity, which
  left the cache-first bucket unbounded and the cap dead code.
- useAgentLogs.ts held a literal NUL byte that made git treat the file as binary
  and grep skip it; replaced with an escape sequence so it stays reviewable.

Verified: tsc -p tsconfig.app.json clean, pnpm lint clean, pnpm check:changesets
clean, 25 scoped test files / 1337 tests passing. The xterm scrollback constants
and several components still lacking onReconnect remain untested.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 10:13:39 -07:00
gsxdsm
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.
2026-07-26 09:50:44 -07:00
gsxdsm
d4aa79b66c FN-8598: preserve legacy task cost badges
Restore cost badges for tasks with valid legacy token totals.

- Preserve usage records when optional timestamps and cache-write totals are absent
- Use task creation time to satisfy legacy usage timestamp requirements
- Cover card badge rendering, unpriced mixed usage, and mobile visibility
- Add a patch changeset for the restored badge behavior

Files changed:
 .changeset/fn-8598-cost-badge-fix.md               |   7 +
 .../task-token-usage-serialization.test.ts         |  45 +++++++
 packages/core/src/task-store/serialization.ts      |  16 ++-
 .../__tests__/TaskCard.cost-badge.test.tsx         | 146 +++++++++++++++++++++
 .../app/utils/__tests__/taskTokenCost.test.ts      |  11 ++
 5 files changed, 221 insertions(+), 4 deletions(-)

Fusion-Task-Id: FN-8598

Fusion-Task-Lineage: 83fb4051-8e0f-4ee9-9f00-0e4d5cb8661e

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-26 09:06:41 -07:00
gsxdsm
10ebd0e025 fix(dashboard): keep quick-entry portal menus attached when space is tight
Extract shared fixed-menu positioning so deps/agent/node/priority pickers
clamp max-height without detaching from the trigger when free space is
shorter than the preferred dropdown height.
2026-07-25 21:01:25 -07:00
gsxdsm
3f976e3dca FN-8538: give Planning Mode a dedicated collaborative prompt
Planning Mode now uses a standalone collaborative prompt rather than inherited task-triage instructions.

- Resolve the Planning Mode default independently of workflows and triage assignments.
- Preserve explicit full-system prompt overrides and update prompt-setting documentation.
- Cover dedicated prompt behavior and add the published-package changeset.

Files changed:
 .changeset/fn-8538-planning-prompt.md              |  7 +++
 docs/dashboard-guide.md                            |  3 +-
 .../core/src/__tests__/prompt-overrides.test.ts    |  7 +++
 packages/core/src/prompt-overrides.ts              |  8 ++-
 packages/dashboard/app/utils/builtinPrompts.ts     |  8 ++-
 .../__tests__/planning-infinite-interview.test.ts  | 60 +++++++++++++++++++++-
 .../__tests__/planning-prompt-resolution.test.ts   | 47 ++++++++++-------
 packages/dashboard/src/planning.ts                 | 46 +++++------------
 8 files changed, 130 insertions(+), 56 deletions(-)

Fusion-Task-Id: FN-8538

Fusion-Task-Lineage: 36e0665d-7995-4bc5-9c73-948f7f4e9131

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-23 11:41:15 -07:00
gsxdsm
fd9e4b2a30 FN-8509: create Coding Ideas Start tasks in Todo
Coding Ideas Start now atomically creates tasks in the validated Todo column.

- Resolve Todo only from captured, visible Coding Ideas workflow metadata.
- Preserve explicit Start destinations through Board and List create hosts without a follow-up move.
- Add regression coverage, documentation, and a patch changeset.

Files changed:
 .changeset/fn-8509-coding-ideas-start-todo.md      |  7 +++++
 docs/dashboard-guide.md                            |  4 +--
 packages/dashboard/app/components/Column.tsx       |  8 +++++-
 packages/dashboard/app/components/ListView.tsx     |  5 ++++
 .../dashboard/app/components/QuickEntryBox.tsx     | 17 +++++++-----
 .../app/components/__tests__/Column.test.tsx       | 16 ++++++++++-
 .../app/components/__tests__/ListView.test.tsx     | 31 +++++++++++++++++++++-
 .../components/__tests__/QuickEntryBox.test.tsx    | 31 ++++++++++------------
 .../app/utils/__tests__/quickAddStart.test.ts      | 25 ++++++++++++++++-
 packages/dashboard/app/utils/quickAddStart.ts      | 21 ++++++++++++++-
 10 files changed, 134 insertions(+), 31 deletions(-)

Fusion-Task-Id: FN-8509

Fusion-Task-Lineage: 951599a0-f277-4499-aaea-90371ddeda72

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-22 19:08:47 -07:00
gsxdsm
c469f90b12 FN-8500: add Xiaomi provider icons
Show Xiaomi branding for direct provider names and MiMo model labels.

- Add an accessible, tokenized Xiaomi provider mark
- Infer boundary-safe MiMo labels through the shared provider icon key
- Reuse shared inference in usage cards and cover Xiaomi mappings
- Add a patch changeset for Xiaomi provider branding

Files changed:
 .changeset/fn-8500-xiaomi-provider-icon.md         |  7 +++
 packages/dashboard/app/components/ProviderIcon.tsx | 24 +++++++++
 .../dashboard/app/components/UsageIndicator.tsx    | 58 +++-------------------
 .../app/components/__tests__/ProviderIcon.test.tsx | 48 ++++++++++++++++++
 .../components/__tests__/UsageIndicator.test.tsx   | 17 +++++++
 packages/dashboard/app/styles.css                  |  1 +
 .../app/utils/__tests__/providerIconKey.test.ts    |  9 ++++
 packages/dashboard/app/utils/providerIconKey.ts    |  8 +++
 8 files changed, 120 insertions(+), 52 deletions(-)

Fusion-Task-Id: FN-8500

Fusion-Task-Lineage: 1784a180-cf41-4666-a59e-10aa24f47b72

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-22 17:29:20 -07:00
gsxdsm
da616e1cf9 FN-8498: add Quick Add Start menu
Add gesture-only Quick Add Start promotion for eligible hold-based workflows.

- Open a Start menu with right-click or touch/pen long-press while preserving ordinary Save and Enter behavior.
- Validate and snapshot workflow routing before creating, then move matching tasks through host callbacks.
- Cover workflow guards, promotion outcomes, and Board/List callback wiring.

Files changed: .changeset/fn-8498-quick-add-start-menu.md         |   7 ++
 docs/dashboard-guide.md                            |   4 +
 packages/dashboard/app/components/Column.tsx       |   1 +
 packages/dashboard/app/components/ListView.tsx     |   1 +
 .../dashboard/app/components/QuickEntryBox.css     |  30 +++++
 .../dashboard/app/components/QuickEntryBox.tsx     | 115 ++++++++++++++++-
 .../app/components/__tests__/Column.test.tsx       |  10 +-
 .../app/components/__tests__/ListView.test.tsx     |  12 ++
 .../components/__tests__/QuickEntryBox.test.tsx    | 137 +++++++++++++++++++++
 .../app/utils/__tests__/quickAddStart.test.ts      |  38 ++++++
 packages/dashboard/app/utils/quickAddStart.ts      |  51 ++++++++
 11 files changed, 400 insertions(+), 6 deletions(-)

Fusion-Task-Id: FN-8498

Fusion-Task-Lineage: 30d8e7d4-59f1-4c4f-9224-1a9d8c27141b

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-22 16:31:24 -07:00
gsxdsm
b6135f4bd5 FN-8494: keep task cards active while replanning
Keep task-card activity chrome visible throughout durable and fresh replanning states.

- Treat needs-replan tasks as agent-active on triage and todo lanes without changing lock policy.
- Cover board, list, mobile, pause, and freshness behaviors with regression tests.
- Add a patch changeset for the replanning activity indicator.

Files changed:
 .changeset/fn-8494-replan-active-glow.md           |  7 +++++
 .../app/components/__tests__/ListView.test.tsx     |  4 +++
 .../app/components/__tests__/TaskCard.test.tsx     | 31 ++++++++++++++++++++++
 .../app/utils/__tests__/taskActivity.test.ts       | 23 ++++++++++++++++
 packages/dashboard/app/utils/taskActivity.ts       |  7 ++++-
 5 files changed, 71 insertions(+), 1 deletion(-)

Fusion-Task-Id: FN-8494
Fusion-Task-Lineage: a910a22a-cff7-423f-82ee-359830beb104
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-22 13:57:02 -07:00
gsxdsm
395f1364d6 FN-8493: show revising status badges
Rename the needs-replan badge to Revising across dashboard task views.

- Map needs-replan status badges to the Revising translation.
- Update English resources and generated resource types.
- Cover board cards and desktop/mobile list rows with regression tests.
- Add a patch changeset for the operator-facing copy fix.

Files changed:
 .changeset/fn-8493-revising-status-badge.md        |  7 ++++++
 .../app/components/__tests__/ListView.test.tsx     | 25 ++++++++++++++++++++++
 .../app/components/__tests__/TaskCard.test.tsx     | 13 +++++++++++
 .../utils/__tests__/taskStatusBadgeLabel.test.ts   |  6 ++++--
 .../dashboard/app/utils/taskStatusBadgeLabel.ts    |  8 +++----
 packages/i18n/locales/en/app.json                  |  4 ++--
 packages/i18n/src/resources.d.ts                   |  4 ++--
 7 files changed, 57 insertions(+), 10 deletions(-)

Fusion-Task-Id: FN-8493

Fusion-Task-Lineage: 569d271b-48f9-42f4-83f8-4e9f86fdab68

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-22 09:32:02 -07:00