4ee6800a8f375c60fb81b1a8a2bd82ebfa832d9a
6972 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3cef9c226e |
Drift 1/4: TaskCard planning affordances from traits, not "triage" (8→3 measured; one site needed a new wire fact, one conversion was wrong and the tests caught it) (#2558)
## Drift conversion 1 of 4 — TaskCard.tsx Taking the dashboard surfaces from the drift review. This is the board-card one; ListView, TaskDetailModal and register-task-workflow-routes follow separately so each stays revertable. ### Convergence number `task.column === / !== "todo" | "triage"` in `TaskCard.tsx`: **8 → 3** The three survivors are **one documented fallback**, not scattered checks. `getTaskColumnFlags` (Column.tsx) returns `undefined` when a card's column is absent from the resolved metadata and is not the rendering column — the pre-load window, and a card stranded in a lane its workflow dropped. Converting to bare trait reads would have removed every planning affordance in exactly those states, so the legacy ids survive **once**, at the role helpers, plus the move prompt resolving its own target. They retire with the load window, not with this change. I'd rather report 8 → 3 with the reason than 8 → 0 with a regression behind it. ### Why this file was urgent Every planning affordance was gated on `task.column === "triage"`. Land U11 — merged column keeps id `todo`, `triage` deleted — and each comparison silently becomes false: **delete button, awaiting-approval controls, planner badge, step list all vanish from planning cards.** ### One site needed a new fact on the wire, not a renamed comparison `showStartAction` was `intake === true && column !== "triage"`. That hardcoded id was standing in for *"an intake column that does not auto-triage"* — a distinction that lives in trait **config** (`intake` with `autoTriage: false`) and was invisible to every client. It also **inverts** under U11: with `triage` deleted, `column !== "triage"` is vacuously true, so a Start button would appear on **every planning card**. `describeColumns` now derives `manualIntake` server-side and the gate reads it. Renaming the comparison would have shipped the inversion. ### One conversion was wrong, and the tests caught it I first converted the move-progress prompt to the card's *own* column role. The original tests the move **destination** — moving a card *back* into a pre-implementation lane is what risks discarding step progress. `confirms preserving progress before moving` failed immediately on an `in-progress → todo` move. It now resolves the target column's flags. That is the argument for red-green per site rather than pattern-matching the comparison: the regex looks identical at both sites and means different things. ### Revert-proof New `TaskCard.u11-merged-column.test.tsx` renders cards in the **post-U11 shape** — id `todo`, traits `intake + hold`, no `triage` anywhere — and asserts Delete, the planner badge and the step list still appear; that Start does **not** (auto-triaging); and that a manual-intake lane does get it. Revert any converted site and the matching case fails, because these cards are not in `triage` and never will be again. ### Fixture updates, and why they are not weakening - Start-affordance cases now pass `manualIntake`, which the server supplies for a manual intake lane. - "omits the Start button for the triage column even when intake is flagged" → "for an **AUTO-triaging** intake column". The rule was never about the id; the title said it was. ### Verification `pnpm test:gate` (414 + 10 + 71), `pnpm lint`, both dashboard typechecks green. **No new failures**: `TaskCard.test.tsx` reports the same 2 pre-existing failures with and without the change, verified by diffing failing test *names* against a stashed clean tree rather than comparing counts. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
01a75f9edc |
fix(desktop): typecheck streamed model downloads (#2493)
## Summary - make the fetch response-body cast explicit across DOM and desktop TypeScript library definitions - preserve the existing async byte-stream runtime behavior ## Test plan - `pnpm --filter @fusion/dashboard exec vitest run src/stt/__tests__/model-manager.test.ts --reporter=dot` - `pnpm --filter @fusion/desktop typecheck` - `pnpm --filter @fusion/dashboard typecheck` - `node scripts/check-changeset-format.mjs` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved desktop build typechecking for streamed speech-model downloads by refining how streamed response bodies are interpreted for TypeScript. * Preserved runtime behavior, including streaming, integrity/hash checking, file writing, and cancellation handling. * **Maintenance** * Updated the release metadata so this fix is published with the correct patch classification. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
6721bdc652 |
U12 part 7: the List view never self-healed a card's workflow — extract Board's FN-7591 refetch and wire it up (#2530)
## U12 part 7 — the List view never self-healed a card's workflow **Stacks on #2528.** Merge that first. Paying off something I owed on #2525: greptile pointed out that a task whose `taskWorkflowIds` entry is absent — or present but resolving to a workflow that does not declare the task's stored column — gets no per-workflow move metadata, so its menu falls back to the neighbour approximation and **stays there until some unrelated refresh happens**. Board has forced one board-workflows refetch for exactly this since FN-7591. List had none. So the degraded state persisted longest precisely where it is most likely: a **just-created card**, which is when a workflow was actually chosen. I said there that porting the self-heal deserved its own change rather than riding along in a move-menu fix. This is it. ### Two commits, deliberately separable **1. Extraction — move only.** Board's ~55 lines (refs, suspect-mapping predicate, signature guard, deferred macrotask) become `useUnmappedWorkflowRefetch`. Copying them into ListView would have created a second copy of subtle race-avoidance logic to keep in sync. Evidence it is a move: with comments and the new wrapper signature stripped, the hook's **41 body lines** and the **42 removed from Board** differ by exactly one line — the `}` that closed Board's enclosing scope. Nothing added, removed or reordered. The original FNXC notes travel with the code, since they are the reason each line exists. Board's suite is green with no expectation edits. **2. Wiring — behaviour change.** ListView calls the hook. ### Revert-proof Remove the hook call from ListView and the new case fails: `fetchBoardWorkflows` is never called a second time, so the mapping never resolves. A companion case pins the other half — a fully-mapped board must **not** refetch, so the signature guard cannot turn a healthy list into a loop. It measures calls made *after* the initial load settles, because mount fetch and switcher-open legitimately call the fetcher and counting from zero would measure those instead. ### Two existing tests needed fixture corrections — neither a regression Both because the self-heal now fires **correctly** where the fixture did not expect a fetch: - `refreshes workflow columns when workflow metadata SSE arrives` chained two `mockResolvedValueOnce` payloads. The file-level cache seed maps no tasks, so first paint saw FN-001 as unmapped and the repair fetch ate the payload the test asserts on. Seeded that test's own first-paint cache, and added a trailing default — the SSE swap (`backlog` → `ready`) leaves FN-001 in a column its workflow no longer declares, so a repair fetch there is right, and without a fallback it resolved `undefined` and wiped the payload. Worth stating plainly: both fixtures had quietly depended on List *never* self-healing. That dependency is what the change removes. ### Verification `pnpm test:gate` (309 + 10 + 71), `pnpm lint`, `pnpm verify:fast` (18 steps), dashboard typecheck green. ListView + Board suites: **320 passed, 0 failed, 0 skipped**. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * List and Board views now self-recover when task-to-workflow mappings are missing or incorrect, avoiding degraded workflow UI until a later refresh. * Workflow recovery retries are more robust and coordinated to handle delayed/failed refreshes. * Recovery behavior correctly stops/reset when switching projects or unmounting. * **Tests** * Added comprehensive ListView coverage for unmapped-workflow self-heal, including retry timing, StrictMode effect replay, SSE refresh interactions, and mapped-vs-unmapped scenarios. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b85a5d4531 |
fix(core): bound compound engineering review remediation (#2532)
## Summary - cap Compound Engineering Code Review remediation at two Execute→Review repair passes - enable no-progress detection for the built-in CE workflow - preserve explicit project/workflow overrides while making the authored CE default visible in settings and docs - update stale IR/changeset language that still described Code Review as unbounded when unset ## Why The previous CE default was effectively unbounded. A reviewer that repeatedly returned `REVISE` could consume thousands of remediation cycles without terminally parking the task. The built-in workflow should fail closed after a small, explicit budget while still allowing operators to author a different numeric cap. ## Verification - `FUSION_PG_TEST_SKIP=1 corepack pnpm@10.33.0 --filter @fusion/core exec vitest run src/__tests__/builtin-workflows.test.ts` — 46 passed, 17 skipped - `corepack pnpm@10.33.0 --filter @fusion/core typecheck` - `corepack pnpm@10.33.0 --filter @fusion/dashboard exec vitest run app/components/__tests__/WorkflowSettingsPanel.test.tsx app/components/__tests__/workflow-setting-display.test.ts` — 33 passed - `corepack pnpm@10.33.0 --filter @fusion/dashboard typecheck` - `corepack pnpm@10.33.0 changeset status --since=origin/main` - `git diff --check origin/main...HEAD` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Improvements** - Compound Engineering Code Review now caps remediation attempts at 2; after two unsuccessful attempts, the process parks instead of retrying indefinitely. - Post-restart review recovery now completes in a single maintenance cycle to reduce delays. - Default post-review fix budget increased from 3 to 10. - Review revision limits now consistently honor workflow-authored defaults when settings are left empty, and `0` disables automatic remediation. - **Documentation** - Updated the workflow editor, settings reference, workflow steps, and operator panel text to clarify cap/default/disable semantics (including CE: 2). - **Tests** - Added/updated unit tests to validate the new bounded remediation behavior and messaging. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
9a8fc409ff |
fix: persist manual task pauses (#2536)
## Summary - persist an explicit `userPaused` latch when operators pause tasks through CLI, MCP, dashboard task routes, or mission stop - keep automatic/internal pauses distinct (`userPaused` remains false unless explicitly requested) - clear the latch on unpause - route the flag through in-memory and PostgreSQL task stores - add contract coverage across core, CLI, MCP, dashboard task routes, and mission stop ## Why A manually paused task could lose the reason for its pause across dashboard/runtime restart. Startup recovery then treated it like an internally interrupted task and reclaimed it, restarting automation against the operator’s intent. Manual pauses must survive restart and remain non-runnable until explicitly unpaused. ## Verification - core pause durability tests: 2 passed - CLI task/extension tests: 150 passed; PostgreSQL integration lane remains active in CI - dashboard route tests: 261 passed - `@fusion/core`, `@runfusion/fusion`, and `@fusion/dashboard` typechecks passed - full workspace build passed with pnpm 10.33.0 - changeset validation and `git diff --check` passed - live aggregate runtime verification also confirmed `paused=true,userPaused=true` survived a normal dashboard restart with zero active tasks <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Bug Fixes** - Manual task pauses now persist across application restarts and recovery. - Pauses initiated via the CLI, dashboard, MCP tools, and mission stop controls are recorded as explicit user actions. - Automatically paused tasks remain eligible for recovery. - Unpausing clears the durable manual-pause state. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
3ff98aae56 |
U12 part 8: delete the lossy normalizeColumn + behaviour ratchet — and the definitive answer on the raw flag (2 reads left, both U2b's) (#2535)
## U12 part 8 — deletes the lossy `normalizeColumn`, and ratchets it shut Independent of the #2525 → #2528 → #2530 stack; touches only `@fusion/core` exports. This closes **one of the two `@deprecated (workflowColumns, U12)` markers** the unit was named for. ### The hazard `normalizeColumn` coerced an arbitrary value to a **legacy** column, rewriting every workflow-defined custom id to `triage`. Silent data loss for any project whose workflow declares a column outside the six built-ins — and it sat one line away from `normalizeColumnId`, which sanitises structurally and passes real ids through. The dashboard picked the wrong one for its entire task-ingest path until that was diagnosed; `useTasks.ts` and `routes-trait-rekey.test.ts` still carry the notes from that fix. So this is not a hypothetical footgun — it already fired once, on the surface where it mattered most. Deleted rather than left deprecated because it has **zero callers anywhere in the workspace**. It was pure exported hazard: a lossy coercion next to its safe twin, waiting to be picked again. ### The ratchet is the point `no-lossy-column-coercion-export.test.ts` bans the **behaviour, not the identifier**: it walks every exported single-argument function whose name mentions "column" and fails if one maps a valid custom id onto a different legacy id. Re-adding `normalizeColumn` under any name trips it. Verified by actually reintroducing the function — **two of the three cases fail, including the name-agnostic one**. That last detail is what stops it being a guard that checks nothing. Coverage stated plainly: deleting an unused export has no behaviour to revert-check. The compile is the proof it had no callers; the ratchet is the proof it cannot return. --- ## Answering the standing question: does anything still read the raw `workflowColumns` flag? **Yes. Exactly two sites, and both are U2b's.** I am not able to close this out, and here is the complete list rather than a summary: ``` packages/core/src/store.ts:38,43 ← the definition packages/core/src/task-store/moves.ts:9,363 ← `useWorkflow` packages/core/src/task-store/workflow-task-create-ops.ts:11,351 ← move-policy preflight ``` That is the whole list in production code. Everything else that greps is a comment, a test that writes the flag deliberately to exercise the dead path, or the unrelated `workflowColumns.*` i18n namespace for the Columns editor panel. **Why I have not deleted the settings key.** It cannot go while those two read it — the key is what they read. And the two are not separable from each other: `workflow-task-create-ops.ts:351` computes the `movePolicyPreflight` that `moves.ts` consumes and validates, and un-gating the preflight alone would start evaluating workflow move policies (with their plugin-gate side effects) while the branch that consumes the result stays off. That is a behaviour change with no consumer, which is worse than either state. **Status of the blocker.** U2b has not landed. `main` at `919f68f9b` still has both reads; the program's merged history goes `#2466 → #2467 → #2468 (characterisation only) → #2469 → #2479 → #2500 → #2512 → #2513`, with no convergence PR. PR #2468 was Phase A2 **steps 1–2 only** — the differential characterisation — and the convergence that deletes one of the two move paths was never merged. So the honest state of the unit: everything U12 owns is done except the two reads that U2b owns, and the settings key that cannot be deleted until they are gone. If you want me to take U2b itself, say so — I have the inventory and the divergence list, and I would want the current U2b worker stood down from `moves.ts` first. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
18d654a5ff |
capacity, part 3: delete the globalMaxConcurrent setting, API and UI (#2529)
Part 3 of the capacity simplification, and the half that removes the **knob**. Enforcement (shared semaphore, runtime wiring) went in #2509; this removes everything an operator or API client can still see, so nothing is left readable-but-ignored. ## Deleted Settings key + schema default · CentralCore’s `getGlobalConcurrencyState` / `updateGlobalConcurrency` / `acquireGlobalSlot` / `releaseGlobalSlot` and the `concurrency:changed` event · the whole Global Concurrency block in `async-central-core` · `PUT /api/global-concurrency` · the Scheduling · Global settings section · the footer and Command Center global sliders · the dead `getGlobalConcurrencyLimit` reader whose only caller went in #2509. ## Kept, deliberately **`GET /api/global-concurrency` survives as telemetry only** — live `currentlyActive` / `projectsActive` from CentralCore’s side-effect-safe source. “How busy is this machine?” is still a real question once the cap that used to answer it is gone. It no longer reports `globalMaxConcurrent`/`queuedCount`: those came from the deleted cap and from slot bookkeeping production code never incremented, so publishing them was publishing zeros dressed as state. **`useGlobalConcurrency` becomes read-only.** Everything that existed to *persist* went with the cap — the 500 ms debounce, the save-state machine, the commit-on-close/unmount flush, the slider clamp, the `interactive` gate. The module-level shared store is **kept**: its original justification (two mounted consumers drift apart with private copies) holds for a polled read exactly as it did for a cap, and one fetch now serves both. The live “N running (all projects)” readout survives in both surfaces, moved onto the per-project row. ## Two sections become one Scheduling · Global existed to host exactly one control. With it deleted the section renders an empty pane, so the Global/Project pair merges back into **“Scheduling”**. An empty nav entry is a promise of settings that are not there. ## One real fix found on the way `SchedulingSection`’s `concurrencyLoading` gated the **project** concurrency inputs on the **global**-concurrency fetch — never the right source, since `maxConcurrent` and `maxWorktrees` come from the settings form. It is repointed at the form’s own load, preserving the invariant it existed for: a concurrency input stays disabled until its live value arrives, so an operator cannot overwrite a resolved limit with a blank fallback. ## Migration A stored `globalMaxConcurrent` is **ignored** — it is a project-blob key nothing reads, so dropping it needs no schema change. The `central.global_concurrency` **table** is dropped in a follow-up; this slice stops seeding and reading it first, so that drop has no live writer to race. ## Verification, and how the wider suite was controlled `pnpm lint` clean · core/engine/dashboard `tsc` clean · `pnpm test:gate` green (309 + 10 + 71) · dashboard settings/footer/command-center/hooks **2237/2237** · core `central-core-backend` 9/9. The broader dashboard suite shows failures, and I checked rather than assumed: running the suspect files on **clean main** reproduces `api-git` (49), `TaskDetailModal.rendering` (28) and `settings-mobile` (17) identically. Two were genuinely mine — `SettingsModal.scheduling-merge` (0 on main, 17 on this branch: my nav rename) and one `settings-mobile` picker case asserting `scheduling` is a scoped pair — and both are fixed. Tests for deleted behaviour are removed with it (footer confirm/cancel/flush/dedupe, global marker geometry, the hook’s PUT case, the CentralCore slot cases), each carrying a note on what it guarded and where the surviving **project-side** equivalent lives. Fixture-only references were updated, not deleted. Nothing booted. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7003dc9803 |
U12 part 6: Board re-rendered every column on every state change — one inline arrow, measured with a memo-comparator probe (#2528)
## U12 part 6 — Board re-rendered every column on every state change
**Stacks on #2525.** Merge that first.
`canDropTask` was allocated as a fresh inline arrow, per column, per
render:
```tsx
canDropTask={(taskId) => canDropTask(taskId, columnDef.id, selectedWorkflow.id)}
```
`Column` is `React.memo`, and a new function identity on any prop
defeats that entirely. So **any** Board state change — collapsing
Archived, changing Done sort, opening the workflow switcher —
re-rendered every column and every card beneath it, not just the
affected one.
Bound through a `useMemo` cache keyed by lane + column. After the fix,
collapsing Archived re-renders exactly one column: `archived`.
### Measured, not guessed
I instrumented `React.memo`'s comparator to print which props actually
change identity on a collapse toggle. For every unaffected column the
answer was exactly one:
```
PROBE todo changed: canDropTask
PROBE in-progress changed: canDropTask
PROBE in-review changed: canDropTask
PROBE done changed: canDropTask
PROBE archived changed: canDropTask,collapsed <- the one that should re-render
```
After:
```
PROBE archived changed: collapsed
```
### Why this hid, and why my first attempt failed
Two things worth recording, because both were mistakes I made in this
program:
**The test was pointed at dead code.** "keeps unaffected columns stable"
measured the **legacy single-lane board**, whose props were all stable —
so it passed for a long time while covering nothing operators use.
Deleting that board in part 1 repointed it at the real board, where it
failed 3-vs-2. I skipped it then rather than weaken it to the observed
number, and said it needed its own investigation. This is that
investigation.
**My first fix was wrong and I was right to revert it.** In part 1 I
tried a `useRef` cache invalidated by `useEffect`, it did not fix the
test, and I reverted it as unproven rather than ship it. The reason is
now clear: the effect runs *after* the render that populated the cache,
so it wipes the very bindings that render created and the next render
allocates fresh ones — the invalidation defeated the cache. `useMemo`
keyed on the resolver has no such window; the map lives exactly as long
as the closure owning it.
### Revert-proof
The test is un-skipped **with the fix, not with a new expected number**.
Restore the inline arrow at either call site and it fails 3-vs-2 again.
### Verification
`pnpm test:gate` (309 + 10 + 71), `pnpm lint`, dashboard typecheck
green. Board, Board.canDropTask, workflow-resolved-columns and
board-no-legacy-flash: 132 passed, 0 failed, **0 skipped** — the skip
introduced in part 1 is gone.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **Bug Fixes**
- Move menus now show exactly the destinations permitted by each custom
workflow, including non-adjacent moves.
- Invalid or hidden destination columns are excluded from move options.
- Older workflow data continues to use a compatible fallback behavior.
- **Performance**
- Improved board responsiveness by preventing unaffected columns and
cards from re-rendering when archived sections collapse or Done sorting
changes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
da0351857e |
U12 part 5: put real workflow adjacency on the wire — custom-workflow move menus were guessing (measured), and the VALID_TRANSITIONS shortcut is gone (#2525)
## U12 part 5 — the move menu was guessing; now it asks the graph **Stacks on #2521** (same file). Merge that first. The context menu had **no adjacency data at all**, so it did two wrong things at once: it approximated move targets from a column's **neighbours in declared order**, and — because that approximation is strictly weaker than the real graph — it kept a `VALID_TRANSITIONS` shortcut for any workflow whose column-id set matched the six built-ins. Measured, the approximation loses real operator moves: | current | workflow graph | neighbour approximation | |---|---|---| | `in-progress` | in-review, todo, triage, done | todo, in-review | | `todo` | in-progress, triage, archived | triage, in-progress | | `done` | todo, triage, archived | in-review, archived | So **every custom workflow has been offering a guess**: menu entries the store would reject, and legal moves it never offered. The built-ins were fine only because the shortcut bypassed the guess entirely. ### The fix `BoardWorkflowColumn` gains `moveTargets`, resolved by `resolveAllowedColumns` — *the same resolver `moveTaskInternal` validates against*. The menu now offers exactly what the store will accept, for any workflow. Threaded through all four metadata builders (Board, Lane, ListView, TaskDetailModal). Optional on the wire, deliberately: a client older than this field keeps the neighbour fallback rather than losing its move menu mid-upgrade. ### Why deleting the legacy shortcut is safe Not an assertion — a measurement, then a pin. `resolveAllowedColumns(BUILTIN_CODING_WORKFLOW_IR, c)` is **identical to `VALID_TRANSITIONS[c]` for all six columns, order included**: ``` triage ["todo","archived"] == VALID SAME todo ["in-progress","triage","archived"] == VALID SAME in-progress ["in-review","todo","triage","done"] == VALID SAME in-review ["done","in-progress","todo","triage"] == VALID SAME done ["todo","triage","archived"] == VALID SAME archived ["done"] == VALID SAME ``` `builtin-adjacency-matches-legacy-transitions.test.ts` pins it so the equivalence cannot drift silently — if the built-in workflow's edges change without `VALID_TRANSITIONS` following, default menus change shape and that test fails first. It compares **order** too, since the menu renders targets in the order it receives them, so a reorder is operator-visible. Default-workflow menus are therefore byte-identical. Custom ones stop guessing. ### What's left of the legacy vocabulary here `COLUMNS` is gone from `TaskContextMenu` — deleting the shortcut removed its last use. `VALID_TRANSITIONS` survives for exactly one thing: the **no-metadata load window**, documented at the site. I measured removing that in #2521 and it left Task Detail with no move options during load, which is a regression rather than a cleanup. It retires when the load window does. ### Revert-proof, two ways - Drop the `declaredTargets` branch → the custom-workflow case fails: the neighbour fallback returns `["backlog","building"]`, missing the legal `shipped` jump **and** offering `backlog`, which that graph forbids. That is exactly the defect class shipped to every custom workflow today. - A second case pins that an adjacency edge into a column the board cannot show is **dropped**, not rendered as a dead menu entry. ### Verification `pnpm test:gate` (309 + 10 + 71), `pnpm lint`, `pnpm verify:fast`, core + dashboard typechecks green. **No new test failures**: five suites report 31 failures with and without the change — an identical, pre-existing set, verified by diffing failing test *names* against a stashed clean tree, not by comparing counts. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Move menus for custom workflows now show only the destinations permitted by that workflow. * Task-specific workflow rules are applied consistently across boards, lists, lanes, and task details. * Invalid or unavailable destinations are excluded from move options. * Existing clients remain supported when workflow destination data is unavailable. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ebc89310bc |
U12 part 4: derive the move menu's "Back to" label from workflow traits (plus two legacy reads I did NOT delete, with measurements) (#2521)
## U12 part 4 — the move menu's "Back to" label followed hardcoded
column ids
`getTaskMoveTransitions` is shared by Board cards, List rows and Task
Detail. It labelled a backwards move with:
```ts
column === "in-progress" && task.column === "in-review"
? t("taskDetail.move.backToInProgress", "Back to In Progress")
```
Two hardcoded lifecycle ids **and** a hardcoded English column name. On
a workflow that renames those lanes the condition never matched, so the
affordance silently vanished — and had it matched, it would have
announced "In Progress", a column absent from that board. Same
legacy-vocabulary class U10 removed from Board and U12 removed from
ListView, surviving in the context menu all three surfaces render.
Now keyed on the traits it was approximating: the **current** column
carries `mergeBlocker`, the **target** carries `countsTowardWip`, and
the label interpolates the column's own name through a new
`taskDetail.move.backTo` key (added to all six locales).
### Scope I deliberately held back
**The set of moves labelled "Back to" is unchanged.** For
`builtin:coding` the traits resolve to exactly `in-review` and
`in-progress`.
I first generalised this to "any target earlier in the workflow's
declared order" — arguably nicer, and I had it working. Then I measured
it: it relabels moves this change never set out to touch. **18 assertion
sites across three suites** flip from "Move to" to "Back to" (e.g. a
card in In progress gets "Back to Todo", "Back to Planning").
Same-set-different-derivation is the honest scope here; widening which
moves read as backwards is a separate, visible product decision, not a
side effect of a vocabulary fix.
### Two things I chose not to delete, and why
Both are still-live `VALID_TRANSITIONS` reads in this file. Neither is
removable today, and the reason is the same missing wire field —
documented at both sites rather than left as a puzzle.
**1. The default-column-set shortcut.** `TaskContextMenuColumnMetadata`
carries id/label/flags but **no adjacency**, so the workflow branch can
only guess targets from a column's neighbours in declared order.
Measured against the real graph that is a strict loss:
| current | `VALID_TRANSITIONS` | neighbour-derived |
|---|---|---|
| `in-progress` | in-review, todo, triage, done (4) | todo, in-review
(2) |
| `todo` | in-progress, triage, archived (3) | triage, in-progress (2) |
| `done` | todo, triage, archived (3) | in-review, archived (2) |
Deleting that read is not a cleanup — it drops real operator moves
(archive from Todo, straight-to-Done from In progress). Note the guard
keys on the column **id set**, so a workflow that merely renames the six
built-ins still takes this path and still gets correct targets; only
reordering or replacing them falls through to the weaker logic.
**2. The no-metadata fallback.** I removed it first, on principle, and
measured the result: `workflowMoveColumns` is optional at both call
sites (`workflowMoveMetadata?.moveColumns`, `taskMoveColumns`) and
genuinely undefined until board-workflows resolves, so dropping it left
Task Detail with **no move options during load**. That is a live surface
degraded to satisfy a purity rule, so it is not shipped. Unlike Board
and ListView — where the legacy path was provably unreachable — this one
is reachable and useful.
Both retire the same way: put each column's allowed targets on the
board-workflows payload so the load window has real data instead of a
guess. That is a server + wire + client change and belongs in its own
slice.
### Revert-proof
The renamed-workflow fixture declares `signoff` (mergeBlocker) and
`building` (countsTowardWip). Restore the id literals and the new case
fails — `Move to Building` instead of `Back to Building` — which no
relabelling of the old hardcoded string could satisfy, since that string
names a column absent from the board. The same case asserts the forward
move keeps "Move to Shipped", so the rule stays a distinction rather
than a blanket relabel.
### Verification
`pnpm test:gate` (309 + 10 + 71), `pnpm lint`, `pnpm verify:fast`,
dashboard typecheck green.
**No new test failures**, established properly: the three suites this
touches report 30 failures both with and without the change, and I
diffed the failing test *names* against a stashed clean tree rather than
comparing counts — the sets are identical. (An earlier count-only
comparison had me chasing two failures that turned out to be my own new
assertions.)
Also regenerates `packages/i18n/src/resources.d.ts` via `pnpm
i18n:types`. That picks up **~45 lines of pre-existing drift** from
earlier merges that did not regenerate it; the file is generated, and
leaving it stale would omit the new key from the types. Flagged so the
extra lines are not mistaken for scope creep. Note
`packages/dashboard/app/locales/` is gitignored (copied from
`packages/i18n/locales/`), so only the canonical locales are committed.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
3badc244a7 |
U12 part 2: bind the three U5 reconciliation guards — USER-VISIBLE (and one path that couldn't run under PostgreSQL at all) (#2512)
## U12 part 2 — the three U5 reconciliation guards now actually fire
USER-VISIBLE. Taken on standing authority; here is exactly what changed
for operators.
All three read the RAW `experimentalFeatures.workflowColumns` key via
`store.workflowColumnsFlagOn()`. Nothing in production writes it, so all
three have been inert since the workflow-columns cutover.
| Guard | Before (every real project) | After |
|---|---|---|
| Workflow edit removing an **occupied** column | Save succeeded; cards
left in a column the workflow no longer declares | Save fails with
`OccupiedColumnsError` unless `rehomeTo` is supplied |
| Workflow **delete** | Occupant capture returned `[]`; cards sat in the
deleted workflow's columns until the next engine start | Cards move to
the default workflow's entry column as part of the delete |
| Workflow **switch** | Never reconciled; the `reconciliation` field in
the declared return type was never populated | Card in an undeclared
column moves to the resolved target; a declared column is preserved |
Both consumers already handle the new outcomes and needed no change:
`register-workflow-routes.ts` maps `OccupiedColumnsError` to a
structured 409 carrying per-column occupant counts, and
`fn_workflow_update` returns a retryable structured result. The
dashboard editor's `rehomeTo` retry flow becomes reachable for the first
time. I only updated two stale "flag-ON" comments there — that code was
correct all along and simply never fired.
### What an operator actually sees (USER-VISIBLE — read this bit)
Four changes to what the board and the API do. Nothing here is silent.
1. **Editing a workflow to remove a column that has cards in it now
FAILS.** Previously the save succeeded and the cards were left in a
column their workflow no longer declared. The dashboard shows the
existing 409 with per-column occupant counts and prompts for a re-home
target; retrying with `rehomeTo` moves the cards and saves. Removing an
EMPTY column is unaffected.
2. **Deleting a workflow moves its cards immediately** to the default
workflow's entry column, instead of leaving them until the next engine
start.
3. **Switching a task's workflow moves the card** when the new workflow
does not declare its current column. A card whose column IS declared
stays exactly where it is. The API response now carries the
`reconciliation` summary it always promised.
4. **A switch whose re-home would be REJECTED is now refused before
anything is written.** If the destination column is at its WIP limit,
the switch fails with a structured 409 (`workflow-switch-rehome-failed`)
naming the task, both columns and the reason — and **nothing changes**:
the task keeps its current workflow AND its current column. Retry after
making room. Previously this combination committed the selection and
then silently reported a move that never happened, leaving selection and
column disagreeing.
**Can a torn card still happen? Yes, in one narrow case, and here is how
you recover.** If the destination fills in the window between the
pre-flight and the move, the selection is already committed and the card
ends up in a column its new workflow does not declare. That case is not
silent: it writes a `task:workflow-switch-torn` run-audit row, and the
error carries `selectionCommitted: true` with both columns. Recovery:
make room in the destination and move the card there, or switch the task
back — and if neither happens, the R7 startup sweep
`reconcileUndeclaredTaskColumns` re-homes it on the next engine start.
The card is never lost; it is visible in a lane the board may not draw
until one of those runs.
The one thing to watch after merge: (1) converts a previously-silent
success into a visible failure, so an operator mid-edit on a busy
workflow will start seeing a 409 they never saw before. That is the
point — the alternative was stranding their cards — but it is the change
most likely to generate a "this used to work" report.
### The thing that made this more than a gate removal
Un-gating the switch guard surfaced that
`selectTaskWorkflowAndReconcileImpl` read the task through
`store.readTaskFromDb` — the **synchronous SQLite** reader, which throws
under PostgreSQL:
```
TaskStore.db: SQLite Database is not available in backend mode
```
The flag returned before that line, so the gate was hiding a path that
**could not execute at all in the production backend**, not merely a
disabled feature. Ported to the async `readTaskRow`. Found by the new
tests, not by reading the code.
### Review round 2 (both findings real, both fixed)
**Torn write with no alarm — fixed by ORDERING, not by a louder
message.** My first attempt only made the error loud, which left the
torn state intact. The real fix is that the deterministic rejection
cause (destination at its WIP limit) is now checked BEFORE
`selectTaskWorkflow` commits, by resolving the target IR straight from
`workflowId` instead of through the task's selection. Nothing commits on
that path.
For the residual race the failure is loud AND recorded: `rehomeOccupant`
now returns `{ moved, error? }` (additive; sweep callers ignore it), the
switch writes a `task:workflow-switch-torn` run-audit row, and throws
`WorkflowSwitchRehomeFailedError` with `committed: true`. Consumers
translate it: the dashboard route returns a structured 409 with
`selectionCommitted`, and `fn_task_set_workflow` returns the same fields
— no more generic "something went wrong".
**Fabricated column for a deleted task.** My first fix fell back to
`fromColumn` when the final read found no row, so a task soft-deleted
mid-switch was reported as having its old column *preserved*. Absent now
reads as absent (the optional `reconciliation` is omitted). Extracted as
the pure `buildSwitchReconciliation` seam because the window is not
reachable through the public call — `selectTaskWorkflow` rejects an
already-deleted task up front — so it is a genuine race, and I test the
decision directly rather than asserting it from reading the code.
### Revert-proof, measured
New `workflow-reconciliation-production-shape.pg.test.ts` — 6 cases,
with the flag **never written**, which is the configuration every real
project has. Each flip reverted individually:
- re-gate the edit guard → **2 failures** (OccupiedColumnsError case;
rehomeTo re-home case)
- re-gate the delete capture → **1 failure** (card stays in
`custom-hold`)
- restore the switch early return → **2 failures** (`reconciliation`
undefined; card does not move)
- all three in place → **6/6 green**
Round-2 fixes, also measured:
- restore the `fromColumn` fallback → the "row is gone" case fails
(reports `preserved: true` for a deleted task)
- drop the `!outcome.moved` throw → the capacity-blocked case fails
(resolves instead of raising)
- **move the capacity pre-flight back AFTER the commit → the case fails
on the SELECTION assertion** (expected `WF-002`, received `WF-001`),
i.e. it proves the ordering, not the wording
The pre-existing coverage in `workflow-authoritative-reads.pg.test.ts`
reached the occupied-column guard by **writing the flag ON itself** —
same pattern as the ListView/Board suites in part 1. Its flag write is
removed; it now runs in the production shape.
### Where I nearly got this wrong
My first revert harness was buggy and I briefly concluded the delete
re-home was **redundant** — I had probed the stored column and seen
`triage` with what I thought was the flip reverted. It wasn't.
`workflow-ops.ts` contains two identical `const occupantTaskIds = await
store.listWorkflowOccupantTaskIds(id, false)` lines (field-reconcile
block, delete path), so my first-match edit reverted the wrong one.
Re-run anchored on surrounding context, the delete case fails as
predicted. Recorded in the test header as a caution. I also chased and
**refuted** a scarier hypothesis along the way — that an unrelated
`updateTask` coerces a custom column back to `triage`. It does not; the
column survives.
### Deliberately NOT in this PR
The v1-IR rollback-compat persistence (`downgradeIrToV1IfPure`) on the
workflow UPDATE path. It shared the same `flagOn` variable, which is how
it surfaced: **one flag read was feeding two unrelated decisions, so the
flag has more decision sites than call sites** — my earlier 9-site
inventory undercounted. It chooses the stored *shape* of the graph
rather than gating a guard, so it is a persistence-format change with a
different blast radius. It now reads the flag explicitly, behaviour
unchanged, for a follow-up.
The `moves.ts` group remains U2b's.
### Verification
`pnpm test:gate` (307 + 10 + 71), `pnpm lint`, `pnpm verify:fast` (17
steps), both typechecks green. Full `packages/core` PostgreSQL suite:
**1042 passed, 3 failed** — `central-archive-secrets.test.ts`
(log-prefix assertion) and
`workflow-settings-project-identity.pg.test.ts` (×2, project-id
resolution). I confirmed the identical 3 failures on a stashed clean
tree: pre-existing, unrelated. No Fusion instance booted.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Bug Fixes**
* Workflow edits now prevent removal of occupied columns unless cards
are moved to a specified destination.
* Cards are automatically re-homed when workflows are deleted or
switched.
* Workflow switches now check destination capacity before committing and
provide clear conflict details when re-homing fails.
* Reconciliation results now indicate whether cards were moved or
preserved.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
743df98aa4 |
capacity, part 1: merge pinned at 1, worktrees-off mode, and one dead knob deleted (#2502)
First slice of the capacity simplification. Operator: *"just have two
capacity — overall per project agent count and max worktrees. Remove all
other capacities and counts."* Plus two later additions: **merge is
always 1, fixed**, and **worktrees off ⇒ limit by total agents only**.
Three independently revertable commits. No limiter is added anywhere;
one is deleted, one is made structurally absent, and one is pinned.
---
## 1. Merge concurrency ratcheted at 1 (test-only)
I was asked to add a limiter if merge concurrency could be raised. **It
cannot** — there is no setting, workflow property, pool or trait config
anywhere that raises it, so this adds no code and pins what already
holds.
Serialization lives in the **pump**: `drainMergeQueue`’s `mergeRunning`
re-entrancy latch, `activeMergeTaskId` as a single-slot identity, the
`mergeBodyInFlight` next-generation latch, and one `ProjectEngine` per
projectId.
**Not** in the merge-queue lease, which is a per-task ROW (`primaryKey
[projectId, taskId]`) — two tasks can hold leases simultaneously by
construction, and it has exactly one caller (the worktree-reuse
handoff). Ordinary merges never take it. A lease-level test would have
been describing an invariant that layer has never held.
The second half guards the other direction: a merge-concurrency
*setting* would not fail the pump ratchet — it would sit unread until
someone wired it up.
**Revert-proof:** deleting the latch → `expected 1 times, but got 2
times`; deleting the `finally` → latch-stuck; injecting
`maxConcurrentMerges: 2` → fails naming the key; injecting a
`maxParallelLanes` merge-trait field → fails naming the field. Sources
restored byte-identical after each injection.
## 2. `worktreesEnabled` — off means the worktree limit cannot bind
No worktrees-off mode existed (no
`worktreesEnabled`/`useWorktrees`/`worktreeMode` anywhere — only
worktree *configuration*).
**Why not `maxWorktrees: 0`, which needs no new key:** it deadlocks. `??
4` keeps `0` (not nullish), the gate is `used >= limit`, so `0 >= 0`
holds **on an empty board** and nothing ever dispatches — while the
operator-visible reason reads `gate=maxWorktrees; used=0/0`, a limiter
that looks like it is working while the board is dead. It also needs the
Command Center `{min:1}` clamp relaxed. So `0` costs the gate rewrite
*and* the clamp change *and* encodes a mode as a magic value.
**Off is absence, not a big number.** `resolveWorktreeCapacityLimit`
returns `number | null`; `ConcurrencyGateDiagnostic.maxWorktreesGate` is
now optional, so consulting a worktree limit in OFF mode does not
type-check. A gate holding `Infinity` can start binding again the moment
someone "fixes" a comparison; an absent gate cannot.
That paid for itself immediately: making it nullable surfaced a
**second, independent** worktree gate (`activeWorktrees >= maxWorktrees`
early-return) that a skip-by-convention approach would have missed
silently.
**Scope, deliberately:** this is a statement about *counting*, not
isolation. It does not make concurrent agents safe to share one checkout
and builds nothing toward that — the non-worktree paths that exist today
are fallbacks to the operator’s own tree, one of which caused FN-8600.
**Revert-proof:** a resolver ignoring the flag turns both OFF scheduler
tests red while every ON test stays green — they reuse the *same*
fixture (5 in-progress, limit 4) that pre-existing tests prove blocks,
so the pair moves in opposite directions. Removing `disabled:` reddens
the UI test.
## 3. `maxTriageConcurrent` deleted — it controlled nothing
**Measured: zero enforcement reads.** The only `.maxTriageConcurrent`
reference in the repo was a route echoing it back in `/config`. FN-8453
removed the pool it gated and left the knob shipping in
`DEFAULT_SETTINGS`, the settings type, the section registry, the API
response and six i18n catalogs, doing nothing, for releases.
Historical FNXC comments are **updated, not deleted** — they explain a
real past incident; they now say "planning admission slot" so they stop
implying a live setting. Tombstoned so it cannot return.
`/config` loses a field; safe in-repo since `fetchConfig`’s own return
type never declared it.
---
## Two corrections worth recording
- I earlier reported `maxWorktrees` had **no** Settings UI. Wrong —
`WorktreesSection.tsx:47`; my grep was truncated by `head`. It changed
the placement (toggle beside it, rather than a duplicate key in
Scheduling).
- I planned to assert the queued-reason string is rewritten in OFF mode.
Measured that it is **unreachable**: when `maxConcurrent` binds, the
sweep bails before the per-task reason and logs nothing. The test
asserts absence instead.
Two near-misses caught before commit: a pre-existing FN-7505 guard
caught my *new* key missing a description mapping; and editing i18n via
`json.load/dump` silently dropped unrelated duplicate keys
(`autoUpdateAndRestart` in `fr`) — Python keeps only the last of a
duplicated key. Redone textually, every catalog re-validated.
## Verification
`pnpm lint` clean · core/engine/dashboard/i18n typecheck clean · `pnpm
test:gate` green (309 + 10 + 71) · capacity/worktree suites 11/11 ·
engine merge-invariant + scheduler 45/45 · dashboard settings 114/114.
Rebased onto current main and re-verified.
Nothing was booted at any point.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added a project setting to enable or disable running tasks in
worktrees.
* Disabling worktrees removes worktree capacity limits from task
scheduling.
* The “Max Worktrees” setting is disabled when worktree execution is
turned off.
* **Changes**
* Removed the unused triage concurrency setting from configuration and
dashboard responses.
* Updated scheduling diagnostics and queue messages to reflect disabled
worktree capacity limits.
* Added localized labels and help text for the new setting.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
fd6d005333 |
U12 part 1: delete the legacy board path (262 ListView + 39 Board tests were measuring it; 9-site flag inventory, moves.ts group blocked on U2b) (#2500)
## U12, part 1 of 2 — and one blocker you need to route The unit's headline deletion (`isWorkflowColumnsCompatibilityFlagEnabled`) is **blocked by U2b** and is not in this PR. What is here is everything that could be deleted without making a convergence decision that belongs to another unit. ### The blocker PR #2468 landed as `b941d3cba` — but that was **Phase A2 steps 1–2 only: the differential characterization**. The convergence (pick a path, delete the other, delete the flag) has not landed; `feature/workflow-move-path-convergence` is still live. Deleting the raw flag **is** that convergence. `move-path-equivalence.pg.test.ts` says so in its own header, and its second `describe` is literally *"the flag gates MORE than side effects"*. The plan makes this a blocking unit with an equivalence *proof obligation* and an explicit "stop and escalate rather than reconcile silently" note. So I stopped. ### Inventory: every read of the raw flag, with a verdict Nine sites. All false in production because nothing writes `experimentalFeatures.workflowColumns`. **Blocked on U2b — one branch, not separable:** | Site | Silently disabled today | Visible if flipped | |---|---|---| | `moves.ts:312` `useWorkflow` | typed `TransitionRejectionError`, workflow adjacency, the shared transition invariants (merge-blocker *trait* generalization), plugin column gates, the `transitionPending` marker, `workflowId` in `task:move` run-audit, and the trait-hook side-effect path | Yes — rejections change **type and message** | | `moves.ts:931` | the in-transaction capacity gate. `resolveColumnCapacity` never runs | Yes — WIP limits begin binding | | `workflow-task-create-ops.ts:351` | `prepareWorkflowMovePolicyPreflight` returns `undefined` unconditionally → **workflow/plugin move policies have never been evaluated** | Yes — new rejections | On #2488: the pool-id sentinel fix is correct *and* still inert. Two dead layers stacked — the gate it fixed is inside `if (useWorkflow && …)`. **Not blocked, but each moves operators' cards — deferred to PR 2 per your call:** | Site | Silently disabled today | |---|---| | `workflow-ops.ts:183` | `OccupiedColumnsError` + `rehomeTo` when a workflow edit removes an **occupied** column. Today the save succeeds and strands the cards | | `workflow-ops.ts:344` | occupant re-home on workflow **delete** | | `workflow-definitions.ts:700` | workflow-**switch** reconciliation, and the `reconciliation` field in the API response | I verified these three are **not** coupled to `moves.ts`: `rehomeOccupant` reaches a custom target via the `isWorkflowDeclaredRecoveryRehome` carve-out (`moves.ts:641`), which exists because the repair "silently no-oped on every store open" before it. **Not blocked, no behaviour change for current binaries** (also PR 2): `project-store-ops.ts:687` + `lifecycle-ops.ts:1119` — `downgradeIrToV1IfPure` on persist, for *binary-downgrade* rollback. Needs a round-trip test, not an assumption. ### What this PR deletes **Dashboard.** `workflowColumnsEnabled` was a literal `true` at all three `MainContent` call sites; the server hardcodes `flagEnabled: true`. Gone: Board's legacy single-lane board (55 lines mapping the hardcoded `COLUMNS` enum — the last board surface deriving columns from the legacy vocabulary, an R8 violation that survived U10); `tasksByColumn` and its cache ref, orphaned with it; ListView's `LEGACY_LIST_COLUMNS` (the ListView copy of the synthesized-trait-flags defect U10 fixed in Board); both props; the `shouldHydrateCache` gate; TaskDetailModal's `flagEnabled` early return. **Neither Board nor ListView imports the legacy column enum any more.** **Core.** `evacuateCustomColumnsToLegacy` (#1409) — both triggers require the previous settings to have the flag ON, which no writer produces. `runWorkflowColumnsIntegrityPass` — no caller anywhere, superseded by `reconcileUndeclaredTaskColumns` (registered in startup recovery), and it read through the sync SQLite handle, so invoking it under PostgreSQL would have thrown rather than reconciled. **Migration answer:** a project with `workflowColumns: false` persisted needs no migration and no read-time drop. Nothing in this PR reads the key, and it stays in `HIDDEN_EXPERIMENTAL_FEATURE_KEYS` so Settings still suppresses it rather than resurrecting it as an unknown setting. Proven by tests, no instance booted. **`flagEnabled` stays on the wire** as a constant. Removing it changes the response shape, and a browser tab outliving a server upgrade would read the missing field as "off" and degrade. One boolean, no client branches on it, droppable a release later. ### Measured - Production sources: **-332 / +131** (net **-201**). Additions are almost entirely FNXC comments recording why each branch was unreachable. - Dashboard production only: -168 / +93. - Core: -164 / +38. ### The finding I'd actually flag `Board.test.tsx` and `ListView.test.tsx` both left `workflowColumnsEnabled` unset and stubbed `fetchBoardWorkflows` with a **never-resolving promise**. Under the old gate that rendered the **legacy** board — so **262 ListView tests and 39 Board tests were asserting against a configuration production never reached**, and a real regression in the workflow board or list would not have failed either file. Same shape as the other four: looked enforced, wasn't. Both now seed the first-paint lane cache with the default workflow's **real** columns (ids and names copied from `BUILTIN_CODING_WORKFLOW_IR`) — the same seam production uses. Repointing them surfaced assertions that encoded legacy-only values: `"In Progress"`/`"In Review"` (real IR names are `"In progress"`/`"In review"`), and Planning Mode asserted to receive `null` as the workflow id, which is only what `getTaskPlanningWorkflowId` returns when `workflowMode` is false. `"Back to In Progress"` is **not** one of those — it is a hardcoded i18n string in `TaskContextMenu:210`, not derived from the column name. Left alone, and flagged: it will not follow a renamed column. That's U11 vocabulary territory. **One test is SKIPPED, not weakened** — "keeps unaffected columns stable when archived collapse toggles". Pointed at the real board the invariant is **false**: toggling the archived column re-renders unaffected columns (measured: todo renders 3×, not 2×). Pre-existing production behaviour this deletion exposed, never covered because the test measured the dead path. I ruled out the obvious causes (every callback prop is `useCallback`; the per-column task memo's deps exclude `archivedCollapsed`; memoizing the inline `canDropTask` binding did **not** close it — I wrote that fix, could not prove it with a failing test, and **reverted it**). The reason is recorded at the test: un-skip with a fix, never with a new expected number. ### Verification `pnpm test:gate` (299 + 10 + 71), `pnpm lint`, `pnpm verify:fast` (17 steps), and both package typechecks green. `settings-defaults.test.ts > warns once per process for legacy cwd-main mode` fails — **pre-existing**, confirmed by stashing my changes and re-running. No Fusion instance was booted. ### Routing request Per your call: the `moves.ts` group and the final removal of `isWorkflowColumnsCompatibilityFlagEnabled` go to **U2b**, inside the convergence PR where the equivalence proof already lives. The divergences their characterization suite does **not** yet cover: plugin column gates, the `transitionPending` marker, `workflowId` in `task:move` run-audit, and move-policy preflight. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a271f1868f |
U10: dashboard renders workflow-resolved columns (6 legacy-vocabulary defects, incl. a silently-disabled open-PR guard) (#2492)
Phase D / **U10** of the workflow-owned-lifecycle program (**R8**). This
unit **blocks U11** (merge Todo into Planning) — the board must render
IR-resolved columns before the column shape can change.
## What was wrong
Six dashboard surfaces answered a column question from the legacy
`COLUMNS` / `VALID_TRANSITIONS` vocabulary rather than the card's own
workflow IR. Each is a defect today, and each is a way U11 would ship
visibly broken.
| # | Surface | Defect |
|---|---|---|
| 1 | Board — All workflows | Appended **every** legacy column id to the
lane union with synthesised flags → a phantom lane for a column no
workflow declares, labelled with the raw id, ordered by the enum index
with an alphabetical tie-break that scrambled a custom workflow's
declared order |
| 2 | ListView | `if (groups[column])` **silently dropped** a row whose
stored column the workflow no longer declares — no lane, no row, no
error |
| 3 | Move menu | A card stranded in an undeclared column got an **empty
move list** — the one surface that could rescue it offered nothing |
| 4 | Task Detail | Header badge rendered the raw stored id;
title/description editing gated on the literal `{triage, todo}` — a
renamed planning lane lost Edit with nothing on screen to explain it |
| 5 | `board-workflows` | The built-in lifecycle label map was an
**override**, not a fallback, so it replaced a name a built-in
deliberately chose |
| 6 | `POST /tasks/:id/move` | The open-PR backward guard used
`COLUMNS.indexOf(...)` → **-1 on any renamed board**, and the guard
treats a negative index as "allow" |
**#6 is the one worth reading twice.** The guard did not start rejecting
the wrong things — it stopped existing. On a renamed board an operator
could drag a card backward out of review with an open GitHub PR,
orphaning it, and nothing failed. This is precisely the "a converted
guard silently stops firing" row in the plan's risk table, reached
through a rename rather than a conversion.
The re-engage copy of that same guard is **deliberately left on the
legacy enum**, with a comment saying why: it is gated on literal
`in-review` / `in-progress` end to end, so converting only its indices
would make it *weaker* (a workflow declaring `in-review` but not
`in-progress` would score -1 and disable it). U5 owns that lane.
## Evidence
**Every fix has a test that fails when the fix is reverted.** With the
six production files stashed and the tests kept, **10 of the 28 tests
fail**:
- Board aggregate: phantom lane present (2)
- ListView: stranded card dropped, desktop **and** mobile (2)
- Move menu: empty move list for a stranded card (1)
- Task Detail: badge shows `staging`, Edit missing in a renamed intake
**and** hold lane (3)
- `board-workflows`: `builtin:lead-generation`'s `triage` renders as
"Planning" (1)
- Move route: backward move between renamed columns **allowed** with an
open PR (1)
The other 18 are regression pins on behaviour that must not change
(default-workflow lane order and labels, legacy `in-review →
in-progress` block, legacy editable columns, forward moves, terminal
PRs).
**Measured, not estimated.** The label-map clobber was quantified
against the built-in IRs actually in tree: **4 column names replaced — 3
case-only variants ("In progress" → "In Progress"), 1 genuine semantic
rename.** Only the rename is a user-visible defect; the fix preserves
the case normalisation rather than churning the default board.
## Surface enumeration (AGENTS.md)
Desktop **and** mobile — the breakpoint is `(max-width: 768px),
(max-height: 480px)`, so landscape phones exceed 768 wide and match on
height. Column states: empty, populated, duplicate id across two
workflows, and a column no workflow declares. Views: single-workflow
lane, All-workflows aggregate, list, move menu, task detail, move route.
## Regression check
Full dashboard suite, both sides of the change:
| | Test Files | Tests |
|---|---|---|
| Before | 41 failed / 1068 | **296 failed** / 21195 |
| After | 42 failed / 1071 | **297 failed** / 21223 |
`+28` total is exactly the tests this change adds. The single failure
delta is `register-model-routes-kimi-k3-supplemental`, which **fails
identically on this branch's base when run in isolation** — shard-order
dependent, unrelated to columns. **Zero regressions attributable to
U10.** The ~296 pre-existing dashboard failures are inherited from main
and are flagged to the coordinator, not touched here.
`pnpm test:gate`, `pnpm lint`, both dashboard typechecks
(`tsconfig.json` and `tsconfig.app.json`), `pnpm smoke:boot`, and `pnpm
check:changesets` are green.
## Not in this unit
- `Board`'s legacy single-lane `COLUMNS.map` fallback still exists.
`MainContent` passes `workflowColumnsEnabled` unconditionally, so it is
unreachable in-app, but proving that is a deletion argument and this is
not a deletion unit — flagging rather than removing.
- `ListView`'s `LEGACY_LIST_COLUMNS` fallback, same reasoning.
- `flagEnabled` on the wire (U2 noted U10 retires it once no client
reads it) — four clients still branch on it; retiring it is a
client-shape change that belongs with U11's shape work.
- `GET /api/tasks?column=` still validates against `COLUMNS`, rejecting
a workflow-declared custom column as a list filter. Server-side filter
surface, not a rendering decision.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
4819c26349 |
chore(deps-dev): bump jsdom from 27.4.0 to 29.1.1 (#2450)
Bumps [jsdom](https://github.com/jsdom/jsdom) from 27.4.0 to 29.1.1. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/jsdom/jsdom/releases">jsdom's releases</a>.</em></p> <blockquote> <h2>v29.1.1</h2> <ul> <li>Fixed <code>'border-radius'</code> computed style serialization. (<a href="https://github.com/asamuzaK"><code>@asamuzaK</code></a>)</li> <li>Fixed computed style computation when using <code>'background-origin'</code> and <code>'background-clip'</code> CSS properties. (<a href="https://github.com/asamuzaK"><code>@asamuzaK</code></a>)</li> <li>Significantly optimized initial calls to <code>getComputedStyle()</code>, before the cache warms up. (<a href="https://github.com/asamuzaK"><code>@asamuzaK</code></a>)</li> </ul> <h2>v29.1.0</h2> <ul> <li>Added basic support for the ratio CSS type. (<a href="https://github.com/asamuzaK"><code>@asamuzaK</code></a>)</li> <li>Fixed <code>getComputedStyle()</code> sometimes returning outdated results after CSS was modified. (<a href="https://github.com/asamuzaK"><code>@asamuzaK</code></a>)</li> </ul> <h2>v29.0.2</h2> <ul> <li>Significantly improved and sped up <code>getComputedStyle()</code>. Computed value rules are now applied across a broader set of properties, and include fixes related to inheritance, defaulting keywords, custom properties, and color-related values such as <code>currentcolor</code> and system colors. (<a href="https://github.com/asamuzaK"><code>@asamuzaK</code></a>)</li> <li>Fixed CSS <code>'background</code>' and <code>'border'</code> shorthand parsing. (<a href="https://github.com/asamuzaK"><code>@asamuzaK</code></a>)</li> </ul> <h2>v29.0.1</h2> <ul> <li>Fixed CSS parsing of <code>'border'</code>, <code>'background'</code>, and their sub-shorthands containing keywords or <code>var()</code>. (<a href="https://github.com/asamuzaK"><code>@asamuzaK</code></a>)</li> <li>Fixed <code>getComputedStyle()</code> to return a more functional <code>CSSStyleDeclaration</code> object, including indexed access support, which regressed in v29.0.0.</li> </ul> <h2>v29.0.0</h2> <p>Breaking changes:</p> <ul> <li>Node.js v22.13.0+ is now the minimum supported v22 version (was v22.12.0+).</li> </ul> <p>Other changes:</p> <ul> <li>Overhauled the CSSOM implementation, replacing the <a href="https://www.npmjs.com/package/@acemir/cssom"><code>@acemir/cssom</code></a> and <a href="https://github.com/jsdom/cssstyle"><code>cssstyle</code></a> dependencies with fresh internal implementations built on webidl2js wrappers and the <a href="https://www.npmjs.com/package/css-tree"><code>css-tree</code></a> parser. Serialization, parsing, and API behavior is improved in various ways, especially around edge cases.</li> <li>Added <code>CSSCounterStyleRule</code> and <code>CSSNamespaceRule</code> to jsdom <code>Window</code>s.</li> <li>Added <code>cssMediaRule.matches</code> and <code>cssSupportsRule.matches</code> getters.</li> <li>Added proper media query parsing in <code>MediaList</code>, using <code>css-tree</code> instead of naive comma-splitting. Invalid queries become <code>"not all"</code> per spec.</li> <li>Added <code>cssKeyframeRule.keyText</code> getter/setter validation.</li> <li>Added <code>cssStyleRule.selectorText</code> setter validation: invalid selectors are now rejected.</li> <li>Added <code>styleSheet.ownerNode</code>, <code>styleSheet.href</code>, and <code>styleSheet.title</code>.</li> <li>Added bad port blocking per the <a href="https://fetch.spec.whatwg.org/#bad-port">fetch specification</a>, preventing fetches to commonly-abused ports.</li> <li>Improved <code>Document</code> initialization performance by lazily initializing the CSS selector engine, avoiding ~0.5 ms of overhead per <code>Document</code>. (<a href="https://github.com/thypon"><code>@thypon</code></a>)</li> <li>Fixed a memory leak when stylesheets were removed from the document.</li> <li>Fixed <code>CSSStyleDeclaration</code> modifications to properly trigger custom element reactions.</li> <li>Fixed nested <code>@media</code> rule parsing.</li> <li>Fixed <code>CSSStyleSheet</code>'s "disallow modification" flag not being checked in all mutation methods.</li> <li>Fixed <code>XMLHttpRequest</code>'s <code>response</code> getter returning parsed JSON during the <code>LOADING</code> state instead of <code>null</code>.</li> <li>Fixed <code>getComputedStyle()</code> crashing in XHTML documents when stylesheets contained at-rules such as <code>@page</code> or <code>@font-face</code>.</li> <li>Fixed a potential hang in synchronous <code>XMLHttpRequest</code> caused by a race condition with the worker thread's idle timeout.</li> </ul> <h2>v28.1.0</h2> <ul> <li>Added <code>blob.text()</code>, <code>blob.arrayBuffer()</code>, and <code>blob.bytes()</code> methods.</li> <li>Improved <code>getComputedStyle()</code> to account for CSS specificity when multiple rules apply. (<a href="https://github.com/asamuzaK"><code>@asamuzaK</code></a>)</li> <li>Improved synchronous <code>XMLHttpRequest</code> performance by using a persistent worker thread, avoiding ~400ms of setup overhead on every synchronous request after the first one.</li> <li>Improved performance of <code>node.getRootNode()</code>, <code>node.isConnected</code>, and <code>event.dispatchEvent()</code> by caching the root node of document-connected trees.</li> <li>Fixed <code>getComputedStyle()</code> to correctly handle <code>!important</code> priority. (<a href="https://github.com/asamuzaK"><code>@asamuzaK</code></a>)</li> <li>Fixed <code>document.getElementById()</code> to return the first element in tree order when multiple elements share the same ID.</li> <li>Fixed <code><svg></code> elements to no longer incorrectly proxy event handlers to the <code>Window</code>.</li> <li>Fixed <code>FileReader</code> event timing and <code>fileReader.result</code> state to more closely follow the spec.</li> </ul> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Commits</summary> <ul> <li><a href=" |
||
|
|
af3d78d2cd |
chore(deps-dev): bump vitest from 4.1.8 to 4.1.10 (#2446)
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 4.1.8 to 4.1.10. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/vitest-dev/vitest/releases">vitest's releases</a>.</em></p> <blockquote> <h2>v4.1.10</h2> <h3> 🐞 Bug Fixes</h3> <ul> <li><strong>browser</strong>: Check fs access in builtin commands [backport to v4] - by <a href="https://github.com/hi-ogawa"><code>@hi-ogawa</code></a>, <strong>Hiroshi Ogawa</strong> and <strong>OpenCode (claude-opus-4-8)</strong> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/10680">vitest-dev/vitest#10680</a> <a href="https://github.com/vitest-dev/vitest/commit/5c18dd267"><!-- raw HTML omitted -->(5c18d)<!-- raw HTML omitted --></a></li> <li><strong>vm</strong>: Fix external module resolve error with deps optimizer query for encoded URI [backport to v4] - by <a href="https://github.com/SveLil"><code>@SveLil</code></a> and <a href="https://github.com/hi-ogawa"><code>@hi-ogawa</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/10661">vitest-dev/vitest#10661</a> <a href="https://github.com/vitest-dev/vitest/commit/bae52b511"><!-- raw HTML omitted -->(bae52)<!-- raw HTML omitted --></a></li> </ul> <h5> <a href="https://github.com/vitest-dev/vitest/compare/v4.1.9...v4.1.10">View changes on GitHub</a></h5> <h2>v4.1.9</h2> <h3>🐞 Bug Fixes</h3> <ul> <li>Fix <code>importOriginal</code> with optimizer and query import [backport to v4] - by <strong>Hiroshi Ogawa</strong>, <strong>David Harris</strong>, <strong>Codex</strong>and <strong>Vladimir</strong> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/10546">vitest-dev/vitest#10546</a> <a href="https://github.com/vitest-dev/vitest/commit/a5180190c"><!-- raw HTML omitted -->(a5180)<!-- raw HTML omitted --></a></li> <li><strong>browser</strong>: <ul> <li>Wait for orchestrator readiness before resolving browser sessions [backport to v4] - by <strong>Vladimir</strong> and <strong>Séamus O'Connor</strong> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/10555">vitest-dev/vitest#10555</a> <a href="https://github.com/vitest-dev/vitest/commit/7fb29651a"><!-- raw HTML omitted -->(7fb29)<!-- raw HTML omitted --></a></li> <li>Wait for iframe tester readiness before preparing [backport to v4] - by <strong>Vladimir</strong> and <strong>Séamus O'Connor</strong> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/10497">vitest-dev/vitest#10497</a> and <a href="https://redirect.github.com/vitest-dev/vitest/issues/10556">vitest-dev/vitest#10556</a> <a href="https://github.com/vitest-dev/vitest/commit/fbc626c40"><!-- raw HTML omitted -->(fbc62)<!-- raw HTML omitted --></a></li> </ul> </li> <li><strong>mocker</strong>: <ul> <li>Hoist vi.mock() for vite-plus/test imports [backport to v4] - by <strong>Hiroshi Ogawa</strong>, <strong>LongYinan</strong>, <strong>Claude Opus 4.8</strong> and <strong>Vladimir</strong> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/10548">vitest-dev/vitest#10548</a> <a href="https://github.com/vitest-dev/vitest/commit/2c9559c02"><!-- raw HTML omitted -->(2c955)<!-- raw HTML omitted --></a></li> </ul> </li> <li><strong>pool</strong>: <ul> <li>Prevent test run hang on worker crash [backport to v4] - by <strong>Ari Perkkiö</strong> and <strong>Jattioui Ismail</strong> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/10543">vitest-dev/vitest#10543</a> and <a href="https://redirect.github.com/vitest-dev/vitest/issues/10564">vitest-dev/vitest#10564</a> <a href="https://github.com/vitest-dev/vitest/commit/934b0f587"><!-- raw HTML omitted -->(934b0)<!-- raw HTML omitted --></a></li> </ul> </li> </ul> <h5><a href="https://github.com/vitest-dev/vitest/compare/v4.1.8...v4.1.9">View changes on GitHub</a></h5> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href=" |
||
|
|
ff58df22e4 |
chore(deps-dev): bump @vitest/coverage-v8 from 4.1.8 to 4.1.10 (#2445)
Bumps [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8) from 4.1.8 to 4.1.10. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/vitest-dev/vitest/releases">@vitest/coverage-v8's releases</a>.</em></p> <blockquote> <h2>v4.1.10</h2> <h3> 🐞 Bug Fixes</h3> <ul> <li><strong>browser</strong>: Check fs access in builtin commands [backport to v4] - by <a href="https://github.com/hi-ogawa"><code>@hi-ogawa</code></a>, <strong>Hiroshi Ogawa</strong> and <strong>OpenCode (claude-opus-4-8)</strong> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/10680">vitest-dev/vitest#10680</a> <a href="https://github.com/vitest-dev/vitest/commit/5c18dd267"><!-- raw HTML omitted -->(5c18d)<!-- raw HTML omitted --></a></li> <li><strong>vm</strong>: Fix external module resolve error with deps optimizer query for encoded URI [backport to v4] - by <a href="https://github.com/SveLil"><code>@SveLil</code></a> and <a href="https://github.com/hi-ogawa"><code>@hi-ogawa</code></a> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/10661">vitest-dev/vitest#10661</a> <a href="https://github.com/vitest-dev/vitest/commit/bae52b511"><!-- raw HTML omitted -->(bae52)<!-- raw HTML omitted --></a></li> </ul> <h5> <a href="https://github.com/vitest-dev/vitest/compare/v4.1.9...v4.1.10">View changes on GitHub</a></h5> <h2>v4.1.9</h2> <h3>🐞 Bug Fixes</h3> <ul> <li>Fix <code>importOriginal</code> with optimizer and query import [backport to v4] - by <strong>Hiroshi Ogawa</strong>, <strong>David Harris</strong>, <strong>Codex</strong>and <strong>Vladimir</strong> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/10546">vitest-dev/vitest#10546</a> <a href="https://github.com/vitest-dev/vitest/commit/a5180190c"><!-- raw HTML omitted -->(a5180)<!-- raw HTML omitted --></a></li> <li><strong>browser</strong>: <ul> <li>Wait for orchestrator readiness before resolving browser sessions [backport to v4] - by <strong>Vladimir</strong> and <strong>Séamus O'Connor</strong> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/10555">vitest-dev/vitest#10555</a> <a href="https://github.com/vitest-dev/vitest/commit/7fb29651a"><!-- raw HTML omitted -->(7fb29)<!-- raw HTML omitted --></a></li> <li>Wait for iframe tester readiness before preparing [backport to v4] - by <strong>Vladimir</strong> and <strong>Séamus O'Connor</strong> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/10497">vitest-dev/vitest#10497</a> and <a href="https://redirect.github.com/vitest-dev/vitest/issues/10556">vitest-dev/vitest#10556</a> <a href="https://github.com/vitest-dev/vitest/commit/fbc626c40"><!-- raw HTML omitted -->(fbc62)<!-- raw HTML omitted --></a></li> </ul> </li> <li><strong>mocker</strong>: <ul> <li>Hoist vi.mock() for vite-plus/test imports [backport to v4] - by <strong>Hiroshi Ogawa</strong>, <strong>LongYinan</strong>, <strong>Claude Opus 4.8</strong> and <strong>Vladimir</strong> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/10548">vitest-dev/vitest#10548</a> <a href="https://github.com/vitest-dev/vitest/commit/2c9559c02"><!-- raw HTML omitted -->(2c955)<!-- raw HTML omitted --></a></li> </ul> </li> <li><strong>pool</strong>: <ul> <li>Prevent test run hang on worker crash [backport to v4] - by <strong>Ari Perkkiö</strong> and <strong>Jattioui Ismail</strong> in <a href="https://redirect.github.com/vitest-dev/vitest/issues/10543">vitest-dev/vitest#10543</a> and <a href="https://redirect.github.com/vitest-dev/vitest/issues/10564">vitest-dev/vitest#10564</a> <a href="https://github.com/vitest-dev/vitest/commit/934b0f587"><!-- raw HTML omitted -->(934b0)<!-- raw HTML omitted --></a></li> </ul> </li> </ul> <h5><a href="https://github.com/vitest-dev/vitest/compare/v4.1.8...v4.1.9">View changes on GitHub</a></h5> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href=" |
||
|
|
a7856e8a90 |
chore(deps): bump @xyflow/react from 12.11.0 to 12.11.2 (#2447)
Bumps [@xyflow/react](https://github.com/xyflow/xyflow/tree/HEAD/packages/react) from 12.11.0 to 12.11.2. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/xyflow/xyflow/releases">@xyflow/react's releases</a>.</em></p> <blockquote> <h2><code>@xyflow/react</code><a href="https://github.com/12"><code>@12</code></a>.11.2</h2> <h3>Patch Changes</h3> <ul> <li> <p><a href="https://redirect.github.com/xyflow/xyflow/pull/5825">#5825</a> <a href=" |
||
|
|
710d56b2db |
U4 trim: delete the dependency-blocked-todo feature (unreachable in production) and revert 5a2de7d (#2477)
Stacked on #2474. Base is `feature/workflow-vocabulary-u4-dead-code` — do not merge before it. Deletes an **entire feature that has never executed in production**, and reverts `5a2de7d`, which only threaded resolved lifecycle columns through it. ## Reachability evidence — the whole basis for this ``` surfaceDependencyBlockedTodos ← in NEITHER sweep registry; no caller in └─ getDependencyBlockedTodoReporter() engine/dashboard/cli — only tests └─ engine/dependency-blocked-todo-reporter.ts ← sole caller of ↓ └─ core/computeDependencyBlockedTodoReport ``` self-healing owns two name-based sweep registries (`runStartupRecovery`, 58 entries; `runMaintenance`, 76). `surfaceDependencyBlockedTodos` is in **neither**, so nothing ever invoked the chain below it. Its four tests passed while proving nothing about production. ## Why delete rather than wire it up Wiring was the tempting option and is the riskier one. Switching on a 450-line path that has never run — whose tests therefore establish nothing about its behavior against real data — is a **behavior change with unquantified blast radius**. This program already refused exactly that move for the **pool-id sentinel**, a one-line change that would switch on dormant enforcement across every project. This is the same class of move at ~450× the size. Deleting is also the recoverable direction: git keeps the feature, and it can be resurrected deliberately — with tests that prove it *runs* — if dependency-blocked reporting is actually wanted. ## The settings keys go with it `dependencyBlockedTodoReportEnabled` defaulted `true` while driving nothing. A schema/API-visible switch that lies about what the system does is worse than no switch. (It had no dashboard UI field — the dashboard test allowlist already recorded it as *"no UI field"*.) Four sibling tuning keys are removed with it. ## Against my own earlier work `5a2de7d` threaded resolved lifecycle roles into `computeDependencyBlockedTodoReport` and its reporter, answering a review finding I confirmed as real. **The code was correct; the impact claim was not**, because the path never executes. Neither the reviewer nor I checked *reachability* before agreeing the defect mattered — only correctness. A correction is posted on that thread in #2470. **Scope limit on that admission:** the same finding also described *incorrect scheduler ordering*. That half runs through `buildUnblockWeightMap` in `task-priority.ts`, which is **live** and was already threading `terminalColumns` (B1, `434b385`). Scheduler ordering was never affected, before or after. ## What survives `blocker-fanout.ts` **stays** — it is live via `task-priority.ts`. Only the plural `holdColumns` option added by `5a2de7d` is reverted, since the deleted report was its sole consumer. `holdColumn` (singular, from B1) remains. ## Net **1,244 deletions / 5 insertions across 15 files** — ~450 production lines, ~684 test lines, 5 settings keys. ## Verification - `tsc --noEmit` clean in **core, engine, and dashboard-app**; `pnpm lint` clean - merge gate green (299 + 10 + 71) - self-healing suite: 411 passed, 1 **pre-existing** failure (`archiveStaleDoneTasks`) - dashboard settings-descriptions suite green - `settings-parity.test.ts` has one **pre-existing** failure (`agentToolOutputMaxChars` overlap) that fails identically with these changes stashed — unrelated to this deletion No changeset: `@fusion/core` and `@fusion/engine` are private. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added quiet-window backlog health diagnostics for stalled items in review, with repeat-alert suppression. * Added default thresholds for backlog-pressure alerts. * **Changes** * Removed dependency-blocked todo reporting and related alerts. * Removed the dependency-blocked todo enable/disable setting; remaining tuning options are no longer active. * Updated the workflow hold classification to use a single todo column. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
4158cf1ab7 |
Phase A: workflow-owned lifecycle foundation (U1, U2, U3) (#2467)
Phase A (Foundation) of
`docs/plans/2026-07-26-001-refactor-workflow-owned-lifecycle-plan.md`.
Three units, one commit each. No operator-visible behavior change.
## U1 — Lifecycle-column resolution seam
`resolveLifecycleColumns(ir)` returns `{ intake, hold, wip, review,
complete, archived }` — the first column carrying each trait,
`undefined` for a role no column carries.
`resolveTaskLifecycleColumns(store, taskId, cache?)` is the store-aware
form; the cache is caller-owned so a sweep reads one IR per workflow
rather than one per card.
A v1/column-less IR resolves to `undefined` for the **whole struct**
rather than a struct of undefined roles. A caller must be able to
distinguish "this workflow declares no hold column" (a real shape to
honor) from "no column vocabulary at all" (skip and log) — only the
second licenses conservative fallback.
Nothing consumes the seam yet; Phases B–D convert the ~207 hardcoded
column literals onto it.
## U2 — Delete the pre-cutover parity machinery (delete-only)
**`workflow-columns-settings.ts`** — `isWorkflowColumnsEnabled` had the
body `return true`. Six live call sites branched on it, so every
flag-OFF arm was dead code that read as a supported configuration.
Deleted; surviving side inlined at self-healing's transitionPending
sweep, the scheduler's per-column capacity diagnostic, merge-trait's
policy resolver, the board-workflows payload, two task-workflow routes,
and the CLI TUI's column enrichment.
**`workflow-parity.ts`** — asserted the default workflow's adjacency
*equals* the legacy `VALID_TRANSITIONS`. U11 deliberately breaks that
equality by merging Todo into Planning, so this is not a stale assertion
to update; it is a contract against the target state. Its emitter
(`workflow-parity-observer.ts`) is already a tombstone, so
`getWorkflowParitySummary` and `computeWorkflowColumnsGraduationReport`
aggregated run-audit rows nothing writes and had no caller outside
`TaskStore`. Both store methods go with it.
`flagEnabled` stays on the board-workflows **wire** as a constant `true`
— shipped dashboard clients still branch on it, and changing the
response shape is not a deletion. U10 retires the field once no client
reads it.
The `legacy-tombstones` ratchet is extended to both files plus seven
symbols, each with the reason it is gone.
### ⚠️ Finding: the third listed deletion was NOT dead
The plan also lists "the flag-off inline move path" in
`task-store/moves.ts`. It is **not** deleted, per U2's execution note
("any behavior change found while removing a branch means the branch was
not dead").
That path is gated on `isWorkflowColumnsCompatibilityFlagEnabled`
(`store.ts:38`) — a **different** function from the always-true public
helper. It reads the raw `experimentalFeatures.workflowColumns` setting,
which nothing in production sets (`settings-schema.ts:396` — "no default
flags are emitted"; zero non-test writers; the operator's own
`~/.fusion/settings.json` has no such key). So `useWorkflow` is false
for effectively every real project: the flag-OFF inline side effects are
the **live** default move path and the flag-ON `default-workflow-hooks`
path is the dead one. The code says so itself at `moves.ts:638`.
Deleting that branch would swap every project onto an untravelled code
path — a behavior change, not a deletion.
**Carry this into Phases B and C, stated plainly so the plan's error is
not repeated:**
> **The inline move path in `moves.ts` is LIVE.
`default-workflow-hooks.ts` (the trait-hook path) is DEAD.** KTD-6
asserted the inverse. Until the convergence unit lands, **nothing may
assume trait hooks run** — a guard, sweep, or subscriber written against
`applyDefaultWorkflowMoveEffects` would never fire in production and
would still pass its tests.
Convergence is **not** attempted here. It is its own unit (Phase A2)
with a proper equivalence proof, per operator decision.
### U3's emit point is on the LIVE path — the seam is not born dead
Worth stating explicitly because it is the failure mode that would make
every later subscriber silently never fire: the `TaskTransitioned` emit
is **not** inside the `if (useWorkflow)` branch. That block closes at
`moves.ts:1212`; the emit sits at `:1214`, beside the existing
`store.emit("task:moved", …)`, on the unconditional post-commit path. It
therefore fires on **both** the live inline path and the dead hooks
path, and the convergence unit inherits the obligation to keep it firing
on whichever path survives — same events, same order, same payloads.
The graph-side emitters (`NodeEntered`, `RunSuspended`) carry the same
risk from a different direction: the bus refuses an invalid payload
*silently* by design, so an emitter regression would stop the event with
no test failure. They are asserted end-to-end through the real bus —
"did a subscriber actually receive it", not "was emit called" — because
a spy passes on a refused payload. The `moveTaskInternalImpl` emit does
**not** yet have that end-to-end assertion against a real store move;
that proof belongs to the convergence unit, which has to build the
both-paths fixture anyway.
## U3 — Post-commit event seam with a transactional outbox
**The bus is not a queue, not a transaction participant, and not a
delivery guarantee.** Durable follow-on work uses the transactional
outbox — a `workflow_work_items` row written *inside* the transition
transaction (the shape `createCompletionHandoffWorkflowWork` already
uses). "Emit after commit, let a subscriber enqueue the work" has a
crash window where a process dies between commit and subscriber, leaving
no event *and* no work-item row, so required work is skipped permanently
with nothing to recover from. Post-commit subscribers therefore carry
only losable reactions.
Emission is consequently lossy and isolated by design: a throwing or
rejecting subscriber is caught and logged, cannot roll back the
transition, and cannot stop the others. Deliveries append to one serial
chain, so two transitions on a task deliver in commit order.
The ids/outcomes-only rule is **mechanised, not documented** —
run-audit's equivalent lives only in prose and has been violated
repeatedly. A payload carrying an object body or a prose string is
refused at the emit boundary and never reaches a subscriber or log sink.
It degrades rather than throws: the emitter is post-commit, so a shape
bug must not become a lifecycle failure.
Emit points: `TaskTransitioned` from the single post-commit point in
`moveTaskInternalImpl`; `NodeEntered` and `RunSuspended` from the graph
column boundary, the latter *after* the durable continuation is
persisted so an observed suspension implies a resumable run.
`registerWorkflowEventSubscribers` (engine) is empty on purpose —
U7/U8/U10 move real reactions onto it, each with the characterization
test proving the reaction was non-authoritative first.
## Verification
- `pnpm test:gate` — green (2/10, 16/299, 1/71).
- `pnpm lint`, `pnpm build`, `tsc --noEmit` on core and engine — green.
- U1: 20 tests in `workflow-lifecycle-traits.test.ts`, including the
fully-renamed-workflow case (fails if the resolver falls back to a
literal) and a shared-cache read-count assertion.
- U2: `legacy-tombstones.test.ts` green with the extended ratchet;
`board-workflows`, `merge-trait`, `workflow-graph-executor-parity`, and
move-hook suites green with no expectation edits.
- U3: 20 bus-invariant unit tests (isolation, ordering, the allowed-key
and required-key halves of the ids-only rule, lossiness) plus 3
end-to-end emitter-delivery tests; 5 outbox tests against a **real
PostgreSQL** work-item table (crash survival, rollback, at-least-once
redelivery on lease expiry, idempotent handler → one effect,
dropped-subscriber vs. durable work). A hand-written fake of the lease
predicate would only prove the fake redelivers.
**Not verified:** the `moveTaskInternalImpl` emit is confirmed on the
unconditional post-commit path by structure and by the surrounding
tests, but is *not* yet asserted end-to-end against a real store move on
both flag settings — that is Phase A2's fixture. The engine subscriber
registry ships empty by design, so no production subscriber exercises
the bus end-to-end yet. `settings-defaults.test.ts` has one pre-existing
failure on `main` (a logger-prefix mismatch in the
`mergeIntegrationWorktree=cwd-main` warning) — confirmed present on a
clean tree, unrelated to this branch.
🤖 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**
* Workflow lifecycle columns are now derived from workflow definitions,
supporting renamed and custom workflows.
* Added post-commit lifecycle events for task transitions, node entry,
and run suspend/resume with validated payloads.
* Follow-on processing for lifecycle emissions is now more robust
(rollback-safe, at-least-once delivery, idempotent handling).
* **Bug Fixes**
* Workflow board responses, task enrichment, and promotion no longer
depend on workflow-columns feature-flag gating.
* Subscriber failures no longer impact committed workflow transitions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
|
||
|
|
8b039a543e |
fix(desktop): advance Pi runtime pin to 0.82.1 for packaging PR lane (#2465)
## Summary - Advance the matched Pi runtime pin (`pi-ai`, `pi-coding-agent`, `pi-agent-core`, `pi-tui`) from **0.82.0 → 0.82.1** so electron-builder's production-dependency walk accepts `pi-agent-core`'s `pi-ai@^0.82.1` requirement. - Fixes the Desktop packaging PR-lane failure: `Production dependency @earendil-works/pi-ai not found for package @earendil-works/pi-agent-core` (required `^0.82.1`). - Keep the workspace override guard; update pin-policy fixtures and CLI package-config expectations. - Tighten the advisory packaging step-order test so it asserts against the real `electron-builder --dir` step (not a missing release-only step name that previously passed via `indexOf === -1`). - Run `pnpm dedupe` so the packaging lane's lockfile dedupe early-warning is clean. ## Context #2439 pinned the full Pi closure at 0.82.0 and made recent main-based packaging runs green. This advances to the current upstream patch so deploy + electron-builder stay aligned with `pi-agent-core@0.82.1`'s declared dependency range. ## Test plan - [x] `node scripts/check-pi-versions-pinned.mjs` - [x] `node --test scripts/__tests__/check-pi-versions-pinned.test.mjs` - [x] `pnpm --filter @runfusion/fusion exec vitest run src/__tests__/package-config.test.ts` - [x] `pnpm --filter @fusion/desktop exec vitest run src/__tests__/release-workflow.test.ts` - [x] `pnpm dedupe --check` - [ ] GitHub: Desktop packaging (should run full packaging walk — lockfile/package.json touched) - [ ] GitHub: PR Checks (Lint, Typecheck, Build, Gate) |
||
|
|
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> |
||
|
|
0e3d2a2265 |
refactor: delete meta-task auto-archive and automated recovery follow-ups (#2461)
Deletes two pieces of automated "meta" machinery that filed and garbage-collected cards restating state already on the task that failed. Net **-1015 lines**. ## Why **Automated recovery follow-ups.** `createAutomatedFollowup` and its dedup engine (289 lines of signature matching, 1h recurrence rate-limiting, 24h supersedes windows) existed to file recovery cards for verification-cap and merge-conflict give-ups. In both cases the parent is *already* parked `failed` with a descriptive `error` and a log entry carrying the failing command, branch, and output — the card was a second copy of that. **Meta-task auto-archive.** The sweeps that garbage-collected those cards were worse than redundant: the regex classifier matched ordinary feature work, and its positional fallback bound cards to unrelated tasks, so **live work could be archived**. They are removed together, because the auto-archive sweeps only existed to clean up after the follow-up engine. ## What changed ### Deleted - `packages/engine/src/verification-followup-dedup.ts` in full — `createAutomatedFollowup`, `decideAutomatedFollowup`, `AutomatedFollowupKind`, `computeVerificationFailureSignature`, `extractFailingTestFiles`. - `findActiveRecoveryFollowUp` — dead code, defined and never called (`tsc` independently flagged it `6133 declared but its value is never read`). - The meta-task auto-archive sweeps `autoArchiveResolvedMetaTasks` / `autoArchiveStalledMetaTasks` and helpers `classifyMetaTask` / `resolveMetaTargetTaskId` / `computeMetaChainDepth` / `archiveMetaTask` / `evaluateMetaAutoArchiveGuards`, plus settings `metaTaskStallAutoCloseMs` and `metaTaskActiveExecutionGraceMs`. - Run-audit types `task:auto-archived-meta-resolved`, `task:auto-archived-meta-stalled`, `task:auto-archive-meta-resolved-skipped`, `task:auto-archive-meta-stalled-skipped`, `verification:followup-created`, `verification:followup-deduped`. The two signature helpers were **deleted rather than relocated** — once the three call sites went they were provably unreachable: `buildVerificationFailureSignature` had exactly one caller, and it was the only caller of `extractFailingTestFiles`. ### Call sites 1 and 2 — park kept, card dropped Verification-cap and merge-conflict give-ups keep their park, audit event, operator comment, and log entry. Site 1's `error` string was reworded off `"See follow-up task for investigation."` (no follow-up will exist) to carry the guidance itself. `autoResolveDisabled` was **kept** — it still drives the outer park guard and the `reason` string; only the inner branch that guarded card creation is gone. ### Call site 3 — autostash orphan, replaced not deleted This one is a genuine data-loss guard, so it keeps a durable trail. A `live`-classified orphan is a merger stash holding **real uncommitted work**, and unlike sites 1–2 there is no parked parent — the parent may already be `done` and merged, so nothing else on the board would ever mention the stash. The card is replaced by a `logEntry` **and** an `addTaskComment` on the parent, preserving every fact the old description carried: the sha, `record.label` (the handle `git stash` recovery needs), `record.detectedByTaskId`, and `sourcePhase`. New truthful run-audit event `task:autostash-orphan-live-detected` replaces the borrowed `verification:followup-*` name, with ids/outcomes-only metadata per AGENTS.md. ### Kept unchanged: the two real product features Eval follow-ups (`eval-followups.ts`) and PR-comment follow-ups (`pr-comment-handler.ts`) only borrowed the shared engine for its dedup pass. Both keep their exact behavior, column, priority, `sourceType`, and log lines, with dedup inlined as a `listTasks` scan on `suggestionId` / `prNumber` respectively. Both fail open (create) if the listing throws, matching the old engine. ## Test changes — read this one Two tests asserted the *deleted* engine's rate-limited `"[verification recurrence]"` logEntry. Those assertions were removed, **not loosened**: both tests still assert no duplicate card is created, and the eval test still asserts the existing id is reported back. No coverage of surviving behavior was weakened. The three `meta-*` test files were deleted along with the sweeps they covered. ## Verification ``` $ pnpm test:gate Test Files 2 passed (2) Tests 10 passed (10) # core Test Files 16 passed (16) Tests 299 passed (299) # engine-core Test Files 1 passed (1) Tests 70 passed (70) # ci-shape GATE_EXIT=0 $ pnpm --filter @fusion/engine --filter @fusion/core exec tsc --noEmit -p tsconfig.json TSC_EXIT=0 (no output) ``` Plus a file-scoped run over the touched surfaces (`eval-followups`, `pr-comment-handler`, `merger-autostash-orphan-surface`, `merger-autostash-cleanup`, `run-audit`, `run-audit-secret-taxonomy`, `project-engine`, `project-engine-manager`): **213/213 passed**. A repo-wide grep confirms no surviving references to any deleted symbol, module, or audit event. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Failed tasks now retain recovery and verification details directly on the original task instead of generating separate follow-up cards. * Live autostash issues now preserve stash information in task comments and activity logs. * Existing evaluation and pull-request follow-ups continue to be reused when appropriate. * **Changes** * Removed automatic archival of meta-tasks. * Removed obsolete meta-task timing settings. * **Documentation** * Updated architecture and settings documentation to reflect these workflow changes. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d19c82b33b |
test(dashboard): align modal and locale fixtures (#2460)
## Summary - query Task Detail modal assertions from the document after its FloatingWindow portal migration - sync secondary locale catalogs with the current English key structure ## Test plan - `pnpm --filter @fusion/dashboard exec vitest run app/components/__tests__/TaskDetailModal.attachments-and-tabs.test.tsx --silent=passed-only --reporter=dot` - `pnpm i18n:status` - `pnpm --filter @fusion/dashboard build` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Added missing localization entries for workflow selection/help text, action-based report targeting, agent tool output limit hints, auto-update/restart messaging, and task refinement status labels across Spanish, French, Korean, Simplified Chinese, and Traditional Chinese. * **Tests** * Updated task detail modal tests to query the correct rendered document root so portal/modal content is asserted reliably. * Minor test formatting adjustments to keep assertions consistent without changing coverage. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
11887625f1 |
test(dashboard): align touch fixture with FloatingWindow (#2459)
## Summary - update the touch-resize browser fixture assertions for New Task's FloatingWindow migration - verify the shared nine touch targets, resize handle ID, and unified geometry persistence key ## Test plan - `corepack pnpm --filter @fusion/dashboard build` - `corepack pnpm --filter @fusion/dashboard exec vitest run --project dashboard-browser-touch --silent=passed-only --reporter=dot src/__tests__/task-modal-touch-resize-browser.test.ts` - `corepack pnpm check:changesets` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Tests** * Updated tablet touch-resize coverage for task-detail to use the shared southeast resize handle and corrected expected hit-target counts. * Refreshed hit-target detection and drag target assertions after switching the test surface to the task-detail/floating window layout. * Updated persistence/geometry validations to compare the stored unified width/height against the resized panel dimensions. * Reworked desktop vs tablet assertions, including tighter overlay padding checks and standardized shared resize-handle sizing expectations. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
d6c917d726 |
feat(dashboard): add view and settings-section enumeration API (#2453)
## Summary Gives external integrations (command palettes, plugin launchers, alternate dashboard shells) a supported way to **discover the host UI** — instead of hardcoding the dashboard's view ids, labels and settings search terms and hand-syncing them on every release. This is the read-only metadata slice of the "constrained by a stable host context and API client" idea in `docs/proposals/2026-07-01-dashboard-theme-plugin-system.md`, and the follow-on to #2415 (theme tokens + overlay layering). Two additions, both inert unless called: | Endpoint | Returns | |---|---| | `GET /api/views` | Every registered built-in view id, in dashboard order — `id`, English `label`, plus optional i18n `labelKey`, legacy `aliases` and `internal` flag. | | `GET /api/settings/sections` | Selectable Settings sections — `id`, `label`, `labelKey`, `scope`, `group`, `keywords`, `searchableKeys`, `advanced`. | Both are read-only, return static project-independent metadata, take no project id, and are mounted inside `createApiRoutes` so they sit behind exactly the same `/api` authentication as every other dashboard route — no more, no less. ## What actually changed — one source of truth The endpoints are the small part. The core of the diff is **collapsing duplicated UI metadata into two shared registries that now drive both the dashboard UI and the API**: - `packages/dashboard/src/shared/dashboard-views.ts` — canonical view ids + English labels + i18n keys + legacy aliases. - `packages/dashboard/src/shared/settings-sections.ts` — canonical settings sections + scope/group/search metadata, with `group` and `advanced` derived from the list's own structure. `LeftSidebarNav`, `SettingsModal` and `useViewState` were rewritten to consume those registries instead of carrying their own copies (net **−230 lines** in `SettingsModal` alone). Edit the registry and the rendered UI and the API move together. ## Drift protection Being precise about what each test can and cannot catch, because "no-drift" claims are easy to overstate: - `left-sidebar-nav-registry-parity.test.tsx` — the one test that catches drift the registry does not already determine. It **renders** the sidebar with a recording `t()` spy and pins each entry's translation key and English fallback to the registry (the sidebar still hardcodes its keys). It also asserts the rendered destination count equals the enrolled id list, so a newly added sidebar view fails until it is enrolled. - `ui-metadata-sync.test.ts` — pins the Settings navigation list, advanced-visibility set, persisted view list, reset-key registry and both endpoint payloads to the registries. Since those consumers are now *derived* from the registries, these assertions mainly guard against a future consumer **re-hardcoding** its own copy. Two of them do stand on their own: each section's served `group` is pinned to the group header it actually renders under, and no published `labelKey` may resolve to a non-leaf i18n node. - `register-ui-metadata-routes.test.ts` — drives the real Express router and asserts each endpoint serves the registry payload verbatim, with no filtering or reshaping. - Exactly two **existing** tests are updated, both for the same reason: they asserted that `SettingsModal.tsx`'s *source text* contains a section literal that now lives in the registry. `VoiceInputSection.modal-visibility.test.tsx` now asserts Voice Input's Basic-mode contract against `SETTINGS_SECTION_METADATA`, and `mcp-documentation.test.ts` reads the registry for the two MCP section ids. No other existing test in the package changes. ## Design notes / decisions for review - **`GET /api/views` returns the full registry, not the live menu.** It includes flag-gated / experimental ids and `internal` (non-navigable) destinations; reachability depends on flags and plugins this endpoint does not evaluate. Documented as "known view ids", not "visible nav entries". - **`labelKey` is optional and best-effort; `label` is the guarantee.** A `labelKey` is published only where the dashboard itself renders that view's title through it. `graph` (labelled from a plugin manifest) and the internal `task-detail` carry none rather than advertise a key that resolves to nothing — and `task-detail` in particular must not point at `taskDetail.title`, which is an occupied i18n *namespace* whose lookup returns an object rather than falling through to a default. A guard test now enforces that. Separately, a few published keys (`nav.ideation`, `nav.importTasks`, `nav.automations`, `pr.view.title`) are the dashboard's real keys but aren't in the shipped catalogs yet because the host supplies their English inline; the docs say plainly that consumers must fall back to `label`. - **`keywords` / `searchableKeys` are explicitly non-contractual.** `searchableKeys` exposes the raw i18n translation-key strings backing a section's searchable copy; values, ordering and presence may change between releases. Documented as best-effort search hints, never stable identifiers. - **Migration is deliberately partial.** The desktop sidebar, Settings navigation and persisted view list now come from the registries; `Header.tsx` and the mobile More sheet still hardcode a few of the same labels. They can still drift from what `GET /api/views` reports; converting them is left to a follow-up so this diff stays reviewable. - **No project scoping, deliberately.** The proposal doc rightly pushes plugin traffic through a project-scoped client — these two endpoints are the exception that proves the rule: they return static registry metadata that is identical for every project, so threading a `projectId` would imply a scoping guarantee that does not exist here. They never touch `getScopedStore` / `TaskStore`. - **Two endpoints rather than one `/api/ui-metadata` envelope.** Views and Settings sections are independent registries with different consumers, and `/settings/sections` sits naturally beside the existing `/settings/*` routes. A consumer that only needs navigation doesn't pay for settings metadata. - **The registry extraction ships with the endpoints rather than as a separate PR.** The registries *are* the mechanism that keeps the API honest — split apart, the first half is a refactor with no observable effect and the second can't land without it. - **Placement:** `packages/dashboard/src/shared/` is a new directory, and these are the first *production* `app/ → src/` imports in the package (today the only one is in `ProviderIcon.test.tsx`). They sit under `src/` because `src/`'s tsconfig cannot import `app/`, so a module both sides consume has nowhere else to go; both registries are dependency-free data leaves, and `vite build` plus `check-no-node-only-core-imports-in-dashboard` confirm the client bundle is unaffected. The considered alternative was `packages/core/src` behind the `dashboard-browser-safe-core-modules.json` allowlist, where `mobile-nav-primary-items.ts` keeps a destination→labelKey table — these stayed out of `core` because they are dashboard-owned UI ids, and because the two tables describe different surfaces (core mirrors the mobile nav's `nav.skills`/`nav.settings`; this registry mirrors the desktop sidebar's `header.skillsView`/`header.settings`). - Ships a `@runfusion/fusion` **minor** changeset (`category: feature`). Happy to adjust any of the above — shape, placement, or dropping `searchableKeys` — if you'd rather it landed differently. ## Verification - Rebased onto `main@26dcccb7c`. Two conflicts, both resolved by absorbing upstream's work rather than reverting it: - `SettingsModal.tsx` — upstream's `voice-input` section (and the `FNXC:VoiceInput` decision comment explaining it stays out of the advanced-only set) moved into the registry. The registry's section list is byte-identical to `main`'s `SETTINGS_SECTIONS` (45/45 entries, all fields), and the registry-derived `ADVANCED_SETTINGS_SECTION_IDS` is identical to `main`'s hardcoded set (19/19, same order) — both verified mechanically, not by eye. Upstream's `RUNTIME_*` hide-uninstalled-runtimes sets are untouched. - `routes/README.md` — the `mount-sequence` list regenerated from `CREATE_API_ROUTES_REGISTRAR_MOUNT_SEQUENCE`, so `registerVoiceRoutes` and `registerUiMetadataRoutes` are both in place and the contract test passes. - `DASHBOARD_VIEWS` covers exactly `main`'s `BuiltInTaskView` union, aliases included, and `BUILT_IN_TASK_VIEWS` reproduces `main`'s 27-entry array in order (`devserver` still preceding `dev-server` for the migration path). - Every one of the 20 sidebar labels the refactor rewrote was checked to be byte-identical to `main`'s hardcoded fallback, and every `FNXC:` decision comment displaced by the move was accounted for — all 75 in `SettingsModal.tsx` and all 11 in `useViewState.ts` survive, relocated onto the registry entries they document. - The full `dashboard-app` + `dashboard-api` suites were run at this commit (**20,706 passing**) and again on unmodified `main@26dcccb7c`, and the failing-file sets compared: **every file that fails here also fails on `main`** — nothing regresses. The overlap is environment-driven (Postgres-backed `*.pg.test.ts`, tests needing built `dist` artifacts, and `SettingsModalNodeRouting.test.tsx`'s `No "fetchSystemInfo" export is defined on the "../../api" mock`), none of it touched by this change. - `tsc --noEmit` clean for both dashboard projects, `eslint` clean on every changed file, and `vite build` of the client bundle succeeds (the two pre-existing `@fusion-plugin-examples/claude-runtime` / `playwright-core` module-resolution errors reproduce on unmodified `main`). - Repo gate scripts pass: `check-changeset-format`, `check-routes-modular`, `check-no-node-only-core-imports-in-dashboard`, `check-no-cwd-relative-dashboard-test-reads`, `check-mock-completeness`. - The three new assertions were mutation-tested rather than assumed load-bearing: breaking the registry's `group` derivation, dropping an enrolled sidebar id, and re-pointing `task-detail` at the `taskDetail.title` namespace each make their test fail. - Local CodeRabbit review over two passes: 3 minor findings, all addressed (parity projection missing `group`; route tests asserting partial instead of exact payloads; the `labelKey` guard not covering the settings registry). <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added authenticated, read-only APIs for discovering dashboard views and selectable Settings sections. * Added dashboard view metadata, including labels, aliases, internal status, and translation keys. * Added Settings metadata with grouping, scope, advanced status, and search-related information. * Updated navigation and Settings UI labels to use shared metadata. * **Documentation** * Documented the new metadata endpoints and integration guidance. * **Bug Fixes** * Added safeguards and automated checks to keep UI navigation and API metadata synchronized. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
3f33cb000f |
feat: per-origin workflow selection + feedback-derived refinement titles
Two task origins had no workflow picker in front of the operator and always inherited the project default: `fn task create` (CLI + the `fn_task_create` agent tool) and refinement tasks. Add a Project General setting for each, where blank/unset means "Selected workflow" (the operator's current Board lane, falling back to the project default) and a concrete id pins that origin. Because the Board lane lives in browser localStorage, non-browser callers could not resolve "Selected workflow" at all. `boardSelectedWorkflowId` mirrors the lane into project settings so they can. Note this makes the mirrored lane project-scoped: two operators on one project share it, last switch wins. The Board never reads it back, so the only effect is which workflow a newly created task inherits. Resolution is `TaskStore.resolveOriginWorkflowOverrideId(origin)`: pinned setting -> mirrored lane -> `undefined` to inherit each caller's existing default-workflow path unchanged. A deleted or fragment id degrades to inherit rather than throwing, so a stale settings value can never break task creation. An explicit `workflow_id` argument to `fn_task_create` still wins. Separately, a refinement is now titled by the operator's own feedback via the shared `deriveFallbackTaskTitle`, not `Refinement: <parent title>`. Ten refinements of one task previously rendered ten identical titles, so the board could not tell them apart while the text saying what each one asked for sat in the description. Provenance moves to a `Refines <id>` card chip alongside the existing detail-view parent link and dependency edge. Verified: merge gate (299 tests), lint, full build, and typecheck for core, CLI, and dashboard all pass. New coverage: origin resolution across both origins and the full precedence ladder, the two settings pickers, the board-lane mirror, refinement titling (including sibling distinctness), and the card chip. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e4cb957b57 |
FN-8619: migrate modal geometry to FloatingWindow
Unify migrated modal geometry and responsive behavior under FloatingWindow. - Cover Agent Detail and GitHub Import floating-window interaction and persistence contracts. - Preserve unique Agent Detail labels, mouse-only backdrop dismissal, and tablet touch layouts. - Update the dashboard modal migration inventory. Files changed: docs/dashboard-modal-inventory.md | 6 +- .../dashboard/app/components/AgentDetailView.css | 49 +++++----- .../dashboard/app/components/AgentDetailView.tsx | 6 +- .../dashboard/app/components/FloatingWindow.css | 10 +- .../dashboard/app/components/TaskDetailModal.css | 15 +++ .../AgentDetailView.floating-window.test.tsx | 102 +++++++++++++++++++++ .../__tests__/GitHubImportModal.test.tsx | 58 ++++++++++++ ...etailModal.responsive-and-dependencies.test.tsx | 24 +++++ 8 files changed, 241 insertions(+), 29 deletions(-) Fusion-Task-Id: FN-8619 Fusion-Task-Lineage: efa95e41-b12e-4e5d-aeb2-65b43c2dd8e0 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
43160a7aae |
FN-8621: migrate complex modals to FloatingWindow
Unify complex dashboard modal presentation under persisted FloatingWindow geometry. - Migrate Create Room, Task Detail, Agent Detail, and GitHub Import modal presentations. - Preserve documented embedded and docked exceptions, dismissal behavior, and nested scrolling. - Add presentation-contract coverage and publish dashboard guidance and changesets. Files changed: ...n-8619-resize-persist-modals-floating-window.md | 7 ++ .changeset/fn-8621-create-room-floating-window.md | 7 ++ docs/dashboard-guide.md | 16 +++- docs/dashboard-modal-inventory.md | 12 +-- .../dashboard/app/components/AgentDetailView.css | 16 +--- .../dashboard/app/components/AgentDetailView.tsx | 102 ++++++++++++++++----- .../dashboard/app/components/CreateRoomModal.css | 19 +++- .../dashboard/app/components/CreateRoomModal.tsx | 57 ++++++++---- .../dashboard/app/components/FloatingWindow.css | 13 ++- .../dashboard/app/components/FloatingWindow.tsx | 15 +++ .../dashboard/app/components/GitHubImportModal.css | 11 +-- .../dashboard/app/components/GitHubImportModal.tsx | 40 ++++++-- .../dashboard/app/components/TaskDetailModal.css | 52 +---------- .../dashboard/app/components/TaskDetailModal.tsx | 58 ++++++------ .../__tests__/AgentDetailView.core.test.tsx | 2 +- .../AgentDetailView.mobile-scroll.test.tsx | 6 +- .../components/__tests__/CreateRoomModal.test.tsx | 62 +++++++++++-- .../components/__tests__/FloatingWindow.test.tsx | 1 + .../__tests__/GitHubImportModal.test.tsx | 8 +- ...etailModal.responsive-and-dependencies.test.tsx | 77 +++++++--------- .../__tests__/modal-presentation-contract.test.tsx | 74 +++++++++++++++ .../dashboard/app/hooks/useEmbeddedPresentation.ts | 2 +- .../dashboard/app/hooks/useModalResizePersist.ts | 5 + 23 files changed, 441 insertions(+), 221 deletions(-) Fusion-Task-Id: FN-8621 Fusion-Task-Lineage: 04b6f3fe-d527-4a21-a0cb-489eb20f5e91 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
7715a83e6f |
FN-8624: fix task detail activity right padding
Keep Task Detail activity insets symmetric while reserving overlay clearance only for first-row content. - Move Live/Feed overlay clearance from the activity container to first-row elements. - Preserve responsive overlay spacing and the full-width Interventions timeline. - Cover modal, pop-out, embedded, and responsive activity layouts with updated tests. - Add a patch changeset for the Task Detail padding fix. Files changed: .changeset/fn-8624-task-detail-inset.md | 7 ++++ .../dashboard/app/components/TaskDetailModal.css | 41 ++++++++++------------ .../TaskDetailModal.attachments-and-tabs.test.tsx | 6 ++-- .../__tests__/TaskDetailModal.css.test.ts | 21 ++++++----- ...etailModal.responsive-and-dependencies.test.tsx | 25 +++++++------ 5 files changed, 54 insertions(+), 46 deletions(-) Fusion-Task-Id: FN-8624 Fusion-Task-Lineage: 578cfe86-3021-421d-841b-2670efc10a5d Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
d3c1955866 |
FN-8625: remove heartbeat controls from agent org charts
Keep org charts read-only for heartbeat configuration while preserving controls on board and list surfaces. - Remove heartbeat toggle props, markup, and styling from org chart nodes - Cover both org chart layouts and board heartbeat availability with regression tests - Add a patch changeset for the dashboard behavior fix Files changed: .../fn-8625-remove-org-chart-heartbeat-toggle.md | 7 +++++++ packages/dashboard/app/components/AgentsView.css | 7 ------- packages/dashboard/app/components/AgentsView.tsx | 14 +++++--------- .../__tests__/AgentsView.orgchart.test.tsx | 20 ++++++++++++++++++++ .../app/components/__tests__/AgentsView.test.tsx | 20 +++++++++++--------- 5 files changed, 43 insertions(+), 25 deletions(-) Fusion-Task-Id: FN-8625 Fusion-Task-Lineage: 454c05e6-fe31-486b-975a-80ba5e8c518c Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
89b4621a93 |
FN-8607: migrate shared modals to FloatingWindow
Standardize agent, onboarding, and utility modal behavior on the shared FloatingWindow contract. - Migrate modal hosts to shared geometry, sheet, dismissal, and focus behavior. - Make FloatingWindow the sole owner of modal ARIA semantics and protect portal-safe dismissal surfaces. - Cover migrated modal contracts and document the responsive sheet requirements. Files changed: docs/dashboard-guide.md | 6 +- packages/dashboard/app/components/AgentGenerationModal.tsx | 8 +- packages/dashboard/app/components/AgentImportModal.tsx | 4 +- packages/dashboard/app/components/AgentListModal.tsx | 4 +- packages/dashboard/app/components/AgentOnboardingModal.tsx | 4 +- packages/dashboard/app/components/DockerNodeOnboardingModal.tsx | 4 +- packages/dashboard/app/components/ExperimentalAgentOnboardingModal.tsx | 4 +- packages/dashboard/app/components/FloatingWindow.css | 36 ++++-- packages/dashboard/app/components/FloatingWindow.tsx | 12 ++ packages/dashboard/app/components/MilestoneSliceInterviewModal.tsx | 2 +- packages/dashboard/app/components/NativeShellOnboardingModal.tsx | 2 +- packages/dashboard/app/components/SetupWizardModal.tsx | 3 +- packages/dashboard/app/components/SubtaskBreakdownModal.tsx | 2 +- packages/dashboard/app/components/__tests__/AgentModals.floatingWindow.test.tsx | 134 ++++++++++++++++++--- packages/dashboard/app/components/__tests__/OnboardingModals.floatingWindow.test.tsx | 128 +++++++++++++++++--- packages/dashboard/app/components/__tests__/UtilityModals.floatingWindow.test.tsx | 124 +++++++++++++++++-- packages/dashboard/app/components/__tests__/migratedModalFixtures.tsx | 59 ++++++--- packages/dashboard/app/components/__tests__/modalFloatingWindowContract.test.tsx | 20 ++- 18 files changed, 458 insertions(+), 98 deletions(-) Fusion-Task-Id: FN-8607 Fusion-Task-Lineage: 11ba6a9c-ce89-4358-83c4-a15ef15b6128 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
bdfa5186c4 |
FN-8622: capture Chromium CDP modal screenshots
Add assertion-guarded CDP evidence for migrated Agent List and Setup Wizard modal touch geometry. - Mount production modal surfaces in the resize browser fixture - Verify tablet resize and drag behavior plus true-phone sheet behavior - Store the captured phone and tablet screenshots as regression evidence - Extend mobile sheet styling to the migrated modal hosts Files changed: .../dashboard/app/components/FloatingWindow.css | 8 ++ .../app/task-modal-touch-resize-e2e-fixture.tsx | 17 +++- .../fn-8607/phone-fullscreen-sheet.png | Bin 0 -> 17823 bytes .../fn-8607/tablet-agent-list-after.png | Bin 0 -> 21489 bytes .../fn-8607/tablet-setup-wizard-after.png | Bin 0 -> 81964 bytes .../task-modal-touch-resize-browser.test.ts | 99 +++++++++++++++++++++ 6 files changed, 120 insertions(+), 4 deletions(-) Fusion-Task-Id: FN-8622 Fusion-Task-Lineage: 3fabd950-bac0-47f4-9649-08709db31e02 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
ba24a530ef |
FN-8620: migrate dashboard modals to FloatingWindow
Standardize dashboard modal geometry and interaction behavior through the shared FloatingWindow contract. - Migrate New Task, right-dock expansion, terminal, onboarding, and utility modals to FloatingWindow. - Expand shared window resizing, positioning, accessibility, and contract coverage. - Document the modal migration inventory and publish changesets. Files changed: .changeset/fn-8607-floating-modal-contract.md | 7 + .changeset/fn-8620-bespoke-geometry-modals.md | 7 + docs/dashboard-guide.md | 47 ++ docs/dashboard-modal-inventory.md | 6 +- .../app/components/AgentGenerationModal.tsx | 14 +- .../dashboard/app/components/AgentImportModal.tsx | 7 +- .../dashboard/app/components/AgentListModal.tsx | 8 +- .../app/components/AgentOnboardingModal.tsx | 6 +- .../app/components/DockerNodeOnboardingModal.tsx | 14 +- .../ExperimentalAgentOnboardingModal.tsx | 6 +- .../dashboard/app/components/FloatingWindow.css | 21 + .../dashboard/app/components/FloatingWindow.tsx | 62 ++- packages/dashboard/app/components/MailboxModal.tsx | 14 +- .../components/MilestoneSliceInterviewModal.tsx | 12 +- .../app/components/NativeShellOnboardingModal.tsx | 6 +- packages/dashboard/app/components/NewTaskModal.css | 196 +------ packages/dashboard/app/components/NewTaskModal.tsx | 593 +++++++-------------- packages/dashboard/app/components/RightDock.css | 84 +-- .../app/components/RightDockExpandModal.tsx | 333 ++---------- .../dashboard/app/components/SetupWizardModal.tsx | 20 +- .../app/components/SubtaskBreakdownModal.tsx | 6 +- .../dashboard/app/components/TerminalModal.css | 120 +---- .../dashboard/app/components/TerminalModal.tsx | 321 ++--------- .../__tests__/AgentModals.floatingWindow.test.tsx | 26 + .../components/__tests__/FloatingWindow.test.tsx | 27 +- .../app/components/__tests__/NewTaskModal.test.tsx | 66 +-- .../OnboardingModals.floatingWindow.test.tsx | 20 + .../app/components/__tests__/RightDock.test.tsx | 18 +- .../components/__tests__/TerminalModal.test.tsx | 61 +-- .../UtilityModals.floatingWindow.test.tsx | 18 + .../components/__tests__/migratedModalFixtures.tsx | 25 + .../__tests__/modalFloatingWindowContract.test.tsx | 30 ++ 32 files changed, 726 insertions(+), 1475 deletions(-) Fusion-Task-Id: FN-8620 Fusion-Task-Lineage: 2a912260-3292-4b13-bcf0-9de5a3df8ccd Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
256c64a7bd |
chore(release): v0.74.0-beta.5
Version bump via changesets. |
||
|
|
034827f251 |
FN-8623: restore CDP touch geometry test lane
Restore a dedicated Chromium CDP lane for dashboard touch-geometry coverage. - Add an opt-in touch-geometry test command and isolated Vitest project. - Keep the browser-dependent spec out of deep and quality backfill collection. - Document browser discovery, port, and single-collection requirements. Files changed: docs/testing.md | 10 ++- packages/dashboard/package.json | 1 + .../__tests__/dashboard-test-config-guard.test.ts | 71 +++++++++++++++++++++- .../task-modal-touch-resize-browser.test.ts | 5 ++ packages/dashboard/vitest.config.ts | 28 ++++++++- scripts/lib/test-inventory-spec.json | 3 +- 6 files changed, 114 insertions(+), 4 deletions(-) Fusion-Task-Id: FN-8623 Fusion-Task-Lineage: eafd7497-9302-49a4-8e9e-aa93c9f56a6f Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
73b2a32e2b |
test: backfill dashboard test mocks for new api and viewport exports
Concurrent dashboard work added runtime exports that hardcoded module mocks did not expose, so any suite rendering the affected component threw "No <export> is defined on the mock". Adds the missing `../api` exports (system-info probe, update install/restart, cloudflared, provider key and login helpers, git remotes/branches) and the `isTabletTouchViewport` viewport helper across the 26 suites that mock those modules. Verified: all 26 files pass (1012 tests). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
beebd270bd |
fix: make Queued to plan / Ready badges agree with the planning lane
TaskCard inferred "unplanned" from steps.length === 0 while triage's todo-discovery and the scheduler's dispatch filter both decide from PROMPT.md seed-ness, so the badges disagreed with the engine in both directions: a real spec that parsed to zero steps read as "Queued to plan" while the scheduler already treated it as a WIP-slot candidate, and a re-seeded card still carrying old steps read as "Ready" while triage was about to plan it. Either way the badge sent operators to the wrong cap. Adds the shared isTaskAwaitingPlanning predicate (replan park, missing spec, seed-vs-real content) used by both triage's discovery and a new best-effort `awaitingPlanning` enrichment on GET /api/tasks. TaskCard derives both badges from that one value — strict complements — and keeps the step count only as a fallback for SSE payloads and older servers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
01b27b13f1 |
test(dashboard): add per-character typing coverage to uncovered field surfaces
Sweeping the other views for the FN-8606 typing bug found no further breakage, but it did expose why the bug shipped: almost all field coverage uses fireEvent.change, which sets a value in one shot on a node it already holds and never needs the input to stay mounted. A remount is invisible to it. Adds expectStableTyping (types character by character via userEvent, then asserts DOM node identity, accumulated value, and retained focus) and applies it to the uncovered surfaces: the FN-8606-migrated AddNode, ConnectNode, NodeDetail, Scripts, and WorkflowAddStep modals, plus the board's QuickEntryBox composer and SubtaskBreakdownModal title editing. All pass — this is a detection floor, not a fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d193dcdbb0 |
chore(lint): ban React components declared inside another component
Adds fusion-react/no-nested-component-definitions, a custom rule in the house style of the existing detached-spawn guard. A component declared in render is a new element type every render, so React remounts its subtree on each parent update and destroys focus, scroll, and local state. This pattern shipped three times without review or tests catching it: FN-8606's ModalShell left Planning Mode and Settings untypable, and MailboxModal's ReplyContextExpandable collapsed expanded reply rows. Tests missed it because fireEvent.change sets a value without needing the node to stay mounted. The rule reports PascalCase functions (including memo()/forwardRef()-wrapped) that return JSX and are declared inside another JSX-returning function. Lowercase render helpers are deliberately allowed — they are the sanctioned fix. Escape hatch: // nested-component-allowlist: <reason>. Scoped to production .tsx, with a vitest guard for the rule itself. Hoists the two pre-existing violations (ProviderStatusBadge, GitHubStatusBadge in ModelOnboardingModal) to module scope so the rule lands clean at "error". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c21fb0d1d0 |
fix(dashboard): stop Mailbox reply-context rows collapsing on unrelated re-renders
ReplyContextExpandable was declared inside MailboxModal's render, making it a new element type on every render. Any parent update remounted the whole recursive reply thread, so expanding one reply row collapsed the others and discarded their DOM identity, focus, and scroll position. Hoist it to module scope and pass the parent's reply state and handlers through an explicit env prop so the element type stays stable. Adds a regression test that expands one row, expands a second, and asserts the first keeps both its node identity and aria-expanded state — same defect class as the FN-8606 ModalShell fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7065d03895 |
fix(dashboard): keep Planning Mode and Settings typable after floating-window migration
FN-8606 declared the window shell as a component inside PlanningModeModal and SettingsModal render. A component declared in render is a new element type on every render, so React remounted the whole subtree on each keystroke, destroying the focused input: Planning Mode dropped everything after the first character and Settings text fields did the same. Replace ModalShell with a plain renderModalShell(children) call so the returned element types stay stable, and add a Planning regression test that types per-character across both the modal and embedded surfaces (fireEvent.change cannot observe this class of bug, which is why it shipped). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
0022621d22 |
chore(release): v0.74.0-beta.4
Version bump via changesets. |
||
|
|
2bb8537352 |
FN-8616: make agent tool-output limits configurable
Expose the shared agent tool-output budget as a scoped operator setting with an explicit no-limit option. - Resolve global and project output caps with a safe finite default and zero sentinel. - Propagate configured budgets through PI and plugin runtime tool wrappers. - Add settings controls, localized labels, documentation, and regression coverage. Files changed: .changeset/fn-8616-tool-output-budget-setting.md | 7 ++++ docs/agents.md | 4 +- docs/settings-reference.md | 1 + .../core/src/__tests__/tool-output-budget.test.ts | 23 ++++++++--- packages/core/src/index.gate.ts | 2 + packages/core/src/index.ts | 2 + packages/core/src/settings-schema.ts | 12 ++++++ packages/core/src/tool-output-budget.ts | 31 +++++++++++++-- packages/core/src/types/settings-scope.ts | 8 ++++ .../app/components/settings/save-split.ts | 1 + .../sections/GlobalGeneralSection.search.ts | 20 ++++++++++ .../settings/sections/GlobalGeneralSection.tsx | 26 ++++++++++++ ...lobalGeneralSection.tool-output-budget.test.tsx | 46 ++++++++++++++++++++++ .../settings-default-descriptions.test.tsx | 1 + .../src/__tests__/agent-session-helpers.test.ts | 20 ++++++++++ .../src/__tests__/runtime-resolution.test.ts | 15 +++++++ .../__tests__/tool-output-budget-wrapper.test.ts | 45 ++++++++++++++++----- packages/engine/src/agent-runtime.ts | 2 + packages/engine/src/agent-session-helpers.ts | 18 +++++++-- packages/engine/src/pi.ts | 29 ++++++++++---- packages/engine/src/runtime-resolution.ts | 10 ++++- packages/i18n/locales/en/app.json | 4 ++ packages/i18n/locales/es/app.json | 6 ++- packages/i18n/locales/fr/app.json | 6 ++- packages/i18n/locales/ko/app.json | 6 ++- packages/i18n/locales/zh-CN/app.json | 6 ++- packages/i18n/locales/zh-TW/app.json | 6 ++- packages/i18n/src/resources.d.ts | 4 ++ 28 files changed, 323 insertions(+), 38 deletions(-) Fusion-Task-Id: FN-8616 Fusion-Task-Lineage: 3ca99a61-d6ae-48ff-98d2-f14a153aa2b7 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
743dc5f46e |
FN-8612: remove tablet task modal padding
Keep task modals dense on tablets without reducing their touch resize targets. - Move the Task Detail drag target out of layout flow while preserving its 44px hit area. - Restore desktop-density New Task header and body padding on tablet resize surfaces. - Add CSS, unit, and browser coverage for tablet geometry and generic floating windows. Files changed: .changeset/fn-8612-tablet-modal-padding.md | 7 + docs/dashboard-guide.md | 8 +- .../task-detail-modal-tablet-width.test.ts | 8 + .../dashboard/app/components/FloatingWindow.css | 40 ++++- packages/dashboard/app/components/NewTaskModal.css | 14 ++ .../FloatingWindow.touch-geometry.test.tsx | 14 ++ .../app/components/__tests__/NewTaskModal.test.tsx | 8 + .../app/task-modal-touch-resize-e2e-fixture.tsx | 54 ++++++- .../task-modal-touch-resize-browser.test.ts | 177 ++++++++++++++++++++- packages/dashboard/vitest.config.ts | 6 + 10 files changed, 322 insertions(+), 14 deletions(-) Fusion-Task-Id: FN-8612 Fusion-Task-Lineage: fecd7c48-7b6c-434e-9002-f8f21241120c Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
65f6748e5b |
FN-8613: theme mailbox markdown links
Theme mailbox message anchors with dashboard tokens and verify link coverage. - Style markdown anchor default, visited, hover, and focus-visible states with design tokens - Cover inline, autolink, list, and table links while preserving file-path controls - Add a patch changeset for themed mailbox links Files changed: .changeset/theme-mailbox-markdown-links.md | 7 ++++ packages/dashboard/app/components/MailboxModal.css | 29 +++++++++++++ .../__tests__/MailboxMessageContent.test.tsx | 49 ++++++++++++++++++---- 3 files changed, 77 insertions(+), 8 deletions(-) Fusion-Task-Id: FN-8613 Fusion-Task-Lineage: a8c7e8bb-8238-4018-bf95-38193f908eb4 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
a6885b73f2 |
FN-8606: migrate core workflow modals to shared floating windows
Migrate dashboard dialogs to the shared movable and resizable FloatingWindow contract. - Move core, workflow, Git, planning, and automation modal surfaces to stable floating-window identities with persisted geometry. - Suspend geometry and floating controls for phone and short-viewport sheets, including Quick Chat. - Add accessibility wiring, tablet touch targets, migration tests, and operator documentation. Files changed: .changeset/fn-8606-floating-window-core-modals.md | 7 + docs/dashboard-guide.md | 4 + packages/dashboard/app/App.tsx | 8 +- .../dashboard/app/components/ActivityLogModal.tsx | 30 ++-- packages/dashboard/app/components/AddNodeModal.tsx | 8 +- .../dashboard/app/components/ChangesDiffModal.tsx | 35 +++-- .../dashboard/app/components/ConnectNodeModal.tsx | 9 +- .../dashboard/app/components/FloatingWindow.css | 71 ++++++++- .../dashboard/app/components/FloatingWindow.tsx | 27 +++- .../dashboard/app/components/GitManagerModal.tsx | 30 +++- .../dashboard/app/components/GroupTaskModal.tsx | 8 +- .../app/components/ModelOnboardingModal.tsx | 28 ++-- .../dashboard/app/components/NodeDetailModal.tsx | 9 +- .../dashboard/app/components/PlanningModeModal.css | 1 - .../dashboard/app/components/PlanningModeModal.tsx | 57 +++---- .../app/components/ScheduledTasksModal.tsx | 9 +- packages/dashboard/app/components/ScriptsModal.css | 1 - packages/dashboard/app/components/ScriptsModal.tsx | 32 ++-- .../dashboard/app/components/SettingsModal.css | 1 - .../dashboard/app/components/SettingsModal.tsx | 58 ++++--- .../app/components/WorkflowAddStepModal.css | 15 -- .../app/components/WorkflowAddStepModal.tsx | 46 +++--- .../components/__tests__/ActivityLogModal.test.tsx | 33 ++-- .../app/components/__tests__/AddNodeModal.test.tsx | 12 +- .../components/__tests__/ChangesDiffModal.test.tsx | 78 +++++----- .../components/__tests__/ConnectNodeModal.test.tsx | 7 + .../components/__tests__/FloatingWindow.test.tsx | 173 ++++++++++++++++++++- .../components/__tests__/GitManagerModal.test.tsx | 30 ++-- .../components/__tests__/GroupTaskModal.test.tsx | 9 ++ .../__tests__/ModelOnboardingModal.test.tsx | 18 ++- .../components/__tests__/NodeDetailModal.test.tsx | 9 ++ .../__tests__/PlanningModeModal.autosize.test.tsx | 30 ++-- .../__tests__/ScheduledTasksModal.test.tsx | 21 ++- .../app/components/__tests__/ScriptsModal.test.tsx | 15 +- .../__tests__/SettingsModal.mobileClose.test.tsx | 25 ++- .../__tests__/WorkflowAddStepModal.test.tsx | 7 + .../floatingWindowMigration.test-helpers.ts | 126 +++++++++++++++ 37 files changed, 828 insertions(+), 259 deletions(-) Fusion-Task-Id: FN-8606 Fusion-Task-Lineage: dab0df2d-73f4-4b0f-bec3-a45016310c91 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |
||
|
|
93a403af67 |
fix(dashboard): import delete-attribution constants via browser-safe subpath
The client bundle aliases `@fusion/core` to the leaf `core/src/types.ts` to keep Node-only dependencies out of the browser, so a package-root import of `FUSION_CLIENT_HEADER`/`FUSION_DASHBOARD_UI_CLIENT` typechecked but failed `vite build`: "FUSION_CLIENT_HEADER" is not exported by "../core/src/types.ts" Follow the documented pattern instead of widening the root alias: declare a `./task-delete-attribution` subpath export, add the matching Vite alias ahead of the broader `@fusion/core` key (Vite matches in order), register the module in the browser-safe-core allowlist, and import the subpath from the client. `task-delete-attribution.ts` has no imports at all, so it is a safe leaf. `app/utils/detectContentLanguage.ts` already warned about exactly this trap; the miss was mine for verifying with typecheck, lint and test:gate but not `pnpm build`, which is one of the four checks CI blocks on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ab87d0d803 |
fix(api): return 404 for missing tasks, and make task deletions attributable
Three related fixes, all originating from a `[api:error] Request failed` log line showing a 500 on `GET /api/tasks/FN-8610/runtime-fallback`. 1. Missing/deleted tasks now return 404 instead of 500. `getTaskImpl` signalled a miss with a bare `Error`, and route catches only mapped errno `ENOENT` to 404 — a leftover from the file-backed storage era. In Postgres mode nothing sets an errno code, so every unknown/missing/soft-deleted/wrong-project read returned 500. Adds a typed `TaskNotFoundError` (message byte-identical) plus a shared `task-lookup-error` mapper applied across the task, session-diff, git/GitHub, workflow and file-workspace route registrars. The same bare throw existed on both archive-lifecycle delete paths, so `DELETE /tasks/:id` was affected too. 2. 5xx logs now carry the origin stack. `rethrowAsApiError` constructed a fresh `ApiError` from the message and discarded the original, so the `FNXC:ApiErrorDiagnostics` contract logged the rethrow site rather than the throw site — the reported log entry had no stack at all. Threads `cause` through the error factories and walks the chain (bounded, cycle-guarded). 3. Task deletions are attributable, and non-operator deletes notify. `task:deleted` audit rows recorded `agentId: "system"` for every HTTP delete, making an operator click indistinguishable from a script or an agent; the calling agent's task id was accepted by the store and then never persisted. Adds a `callerKind` union recorded in audit metadata, tags every delete call site, and stamps a self-reported `x-fusion-client` header from the dashboard client. When the caller is `agent-tool` or `api-unattributed`, a best-effort notice is sent to the operator mailbox; operator and engine deletes stay silent. `x-fusion-client` is attribution, not authentication — anything can send it. No delete-blocking, gating or permission logic is added here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
827b145fa1 |
FN-8611: persist manual import translations
Persist manual GitHub and GitLab import translations across preview sessions. - Add durable translation cache reads and identity-aware translation requests. - Route manual translations through the translation budget with actionable API errors. - Restore cached previews automatically and cover API/UI behavior with tests. Files changed: .changeset/fn-8611-manual-import-translations.md | 7 ++ docs/dashboard-guide.md | 2 +- packages/dashboard/app/api/ai-text.ts | 52 ++++++-- packages/dashboard/app/api/legacy.ts | 2 + .../dashboard/app/components/GitHubImportModal.tsx | 14 ++- .../components/GitHubImportTranslateControls.tsx | 39 +++++- .../__tests__/GitHubImportModal.test.tsx | 42 +++++++ packages/dashboard/src/ai-translate.ts | 23 ++++ .../register-ai-text-assistant-routes.test.ts | 135 +++++++++++++++++---- .../routes/register-ai-text-assistant-routes.ts | 131 ++++++++++++++------ 10 files changed, 370 insertions(+), 77 deletions(-) Fusion-Task-Id: FN-8611 Fusion-Task-Lineage: 00a84f43-4469-4bc5-bbb6-8245d8d79781 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> |