A human reviewer's feedback was being thrown away.
`PrCommentHandler.handleChangesRequested` gated on `task.column !==
"in-review"` and returned early. On any board whose review lane is
renamed, a GitHub **"changes requested"** review produced **no steering
comment** and the card **never went back to work** — the feedback
vanished behind a log line nobody reads. No error, no audit row.
## Census
| file | main | here |
| --- | ---: | ---: |
| `packages/engine/src/pr-comment-handler.ts` | 1 | **0** |
## Two literals, only one countable — again
```ts
if (task.column !== "in-review") { … return; } // counted
…
await this.store.moveTask(taskId, "in-progress"); // INVISIBLE to the census
```
The census scores comparisons. The requeue **destination** is a call
argument, so nothing in the backlog pointed at it — the same pairing as
the branch-worktree auto-requeue in #2797, and the same trap: converting
the gate alone would make the handler *admit* the review and then
attempt a move into a lane the board may not declare, which `moveTask`
rejects. A half-conversion here turns a silent drop into a thrown
rejection. They convert together or not at all.
That is now the second confirmed instance of this shape. The pattern to
look for is a **counted guard whose body performs a hardcoded
`moveTask`** — the guard is the visible half and the move is the
dangerous one.
## Revert results (measured, each run independently)
| conversion | reverted → |
| --- | --- |
| review-lane gate | RENAMED case fails — `updateTask`/`moveTask` never
called; the review is dropped |
| requeue destination | RENAMED case fails — `moveTask` called with
`"in-progress"` instead of the board's wip lane |
The legacy case passes both ways, which is why both vocabularies run. A
non-vacuous companion (renamed board, card sitting in the hold lane)
keeps a gate that admits everything from passing.
## Verification
- `pnpm test:gate` — 161 + 487 + 13 + 71, green
- `pr-comment-handler.test.ts` — 34 passed
- `npx tsc -p packages/engine/tsconfig.json --noEmit` — clean
- `pnpm lint` — clean
- `node scripts/lifecycle-column-census.mjs --strict` — exit 0
(Running the census explicitly, not just `pnpm lint`: CI's Lint job runs
both, and a clean local `pnpm lint` is **not** evidence the Lint check
passes — that cost a round-trip on #2797.)
The engine mega-batch. Folds my four engine PRs and will absorb the
remaining `packages/engine` guards as commits on this branch.
**Superseded and closed:** #2722, #2741, #2766, #2770.
## Census — files converted so far
| file | before | after |
|---|---:|---:|
| `notification/notification-service.ts` | 9 | **5** |
| `runtimes/in-process-runtime.ts` | 6 | **1** |
| `eval-followups.ts` | 2 | **0** |
| `pr-comment-handler.ts` | 1 | **0** |
| `task-revert.ts` | 2 | **0** |
The last two are **census-invisible** (`Set.has(task.column)`
membership) — the class measured in #2763, which a comparison-based scan
cannot count. So the backlog number moves less than the work does,
deliberately.
## What each one actually fixed — all silent, none cosmetic
- **Notifications stopped entirely.** `handleTaskMovedAsync` compared
`data.to` to `in-review`/`done`, so on a renamed board the two
notifications operators rely on most were never sent.
- **A finished card's plan review could re-enter.** The continuation
drain's terminal test matched nothing, so a completed card's planning
continuation was handed to the executor.
- **The revert route admitted and the service refused.** The route
resolved terminal lanes; the service compared to a hardcoded pair. The
operator got a dead end from an affordance the UI and route both
offered.
- **Follow-up dedup blocked new cards forever.** A finished follow-up in
a renamed complete lane read as *open*, so the dedup matched it
permanently — defeating the intent the code documents in the line above
it.
- **The mission requeue wrote a column that may not exist**, and its
guard never matched.
## Flagged, not fixed — deliberately
- **`concurrency.ts` idle semaphore leak recovery** — the last live
caller of the running-agent predicate that does not enrich. On a renamed
board it under-counts and can reclaim a legitimately-held slot. The
enriching variant is async and this is a synchronous repair path whose
failure mode is reclaiming live work.
- **The archival `task:moved` listener** — runs on every move with no
cheap gate ahead of it; converting costs an IR resolution per move to
decide most moves are not archival.
## Notes carried from the folded PRs
Two conflicts resolved in main's favour because **main's version was
better**: `in-process-runtime`'s seam uses `terminalColumns:
ReadonlySet` (membership) where mine used `LifecycleColumns`
(first-per-role), and the test is rewritten against main's API. That
arity trap has now caught me four times, so membership is the default
shape in everything new here.
Review fixes from the folded PRs are included: the notifier's review
set, the second human-review site, the second dedup copy, the workspace
revert surface, and the file-content assertions.
## Verification
`pnpm test:gate` **GREEN** (158 + 10 + 487 + 71) · **224 passed** across
the touched engine suites · engine and dashboard `tsc` clean · `pnpm
lint` clean · census `--strict` exits 0.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
**Taking: `engine/triage.ts`, `engine/pr-comment-handler.ts`,
`engine/eval-followups.ts`, `cli/commands/task.ts`, `cli/extension.ts`**
(write class — no collision with the comparison backlog).
## A class the census does not count
The 48-guard work list tracks `=== "triage"` **comparisons**. These are
`column: "triage"` **writes** — and post-#2515 every one creates a card
directly into the state STALL 3 was about, except **manufactured
continuously** rather than left behind by the upgrade.
## Why they bite
`createTaskImpl` resolves the column as:
```ts
column: input.column || options?.resolvedEntryColumn || fallbackIntakeColumn || "triage"
```
`input.column` **wins**, so an explicit `column: "triage"` overrides the
workflow's resolved intake column entirely.
`store-create-intake-column.test.ts` already pins that a create with
**no** column lands in the default workflow's intake (now `todo`) —
these callers opted out of it.
The sharpest is `triage.ts`'s `fn_task_create` agent tool: it passed
`workflowId: params.workflow_id` **and** `column: "triage"` in the same
call. The caller chose a workflow and the column ignored it — a Coding
(Ideas) create landed in `triage` instead of `ideas`.
## Counts
**Comparison guards: unchanged by this PR.** This is the write class;
conflating the two would misreport convergence toward the zero bar.
| file | `column: "triage"` writes before | after |
|---|---:|---:|
| `packages/engine/src/triage.ts` | 1 | **0** |
| `packages/engine/src/pr-comment-handler.ts` | 1 | **0** |
| `packages/engine/src/eval-followups.ts` | 1 | **0** |
| `packages/cli/src/commands/task.ts` | 3 | **0** |
| `packages/cli/src/extension.ts` | 3 | **0** |
| **total** | **9** | **0** |
## A test that pinned the defect
`pr-comment-handler.test.ts` asserted `column: "triage"` in the
createTask call — so it would have **failed the fix and passed the
bug**. Rewritten to assert the invariant (the caller passes no column,
so the workflow's intake wins) plus an explicit `Object.hasOwn(arg,
"column") === false`, which is what actually catches a reintroduction.
## Interaction with #2591
My merged #2591 rescues these cards once created — they sit on a legacy
planner id their workflow doesn't declare and are still in planning
stage. So this isn't a *visible* stall today; the rescue absorbs it.
**That's the reason to fix it rather than leave it:** a self-healing
path silently absorbing a steady stream of malformed creates is exactly
how the underlying defect stays invisible.
## Deliberately not touched
- `{ id: "start", kind: "start", column: "triage" }` in the builtin
coding / PR / lead-generation IRs — workflow-internal **node
declarations** for workflows that still legitimately declare a `triage`
column, not lifecycle writes.
- Left for their owners: `core/task-store/project-store-ops.ts:210`,
`core/task-store/update-task-deps.ts:111` (main worker),
`dashboard/src/routes/register-gitlab.ts:108` (u12). Same defect, same
one-line shape.
## Verification
- 304 engine/CLI tests green across the affected suites
- merge gate green (482 + 132 + 10), engine + CLI tsc clean, lint clean
No changeset: `@fusion/engine` and `@fusion/core` are private; the CLI
change is a bug fix with no user-facing API change — happy to add one if
you'd rather it appear in release notes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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>
Implements deterministic followup deduplication for the project engine, routing eval and PR followups through a new `verification-followup-dedup` helper that excludes the parent task to prevent self-referential loops, with tests covering eval-followups, merge-error-recovery, PR comments, and the new
Fusion-Task-Id: FN-5232
Merges reviewState hardening (FN-3280 steps 2-3: hardened persistence, API fixes, and PR feedback sync), stepIndex reconciliation for task updates (FN-3757), roadmap route context extraction into the fusion-plugin-roadmap plugin (FN-3160), and shared state snapshots for mesh sync with autostash hard
Fusion-Task-Id: FN-3280
This merge lands three major features and a significant dashboard enhancement. FN-3276 adds a full Review tab to the task detail modal with multi-step lifecycle: review metadata persistence in the task store, new task workflow routes for refresh and same-task revision, and the review tab UI surface
Fusion-Task-Id: FN-3276
- Consolidate steeringComments and comments into unified comments field using TaskComment type
- Remove SteeringComment type, migrate all comments to TaskComment format
- Update addComment to delegate to addTaskComment with auto-refinement for done tasks
- Merge legacy steeringComments data into comments during database migration
- Update dashboard API endpoint from /steer to /comments
- Update executor to use unified comments field for real-time injection
- Update PR comment handler to use addTaskComment method
- Merge steeringComments and comments into unified comments field in Task type
- Update TaskStore to use single comments array instead of separate steeringComments
- Add database migration to convert existing steeringComments to comments
- Update executor to inject all comments into AI execution context
- Update dashboard SteeringTab to use unified comments API
- Update CLI task steer command to use comments field
- Update PR comment handler to add comments via unified API
- Optimize backup tests using fake timers instead of real timeouts
- Enable parallel file execution in core, engine, CLI, and dashboard packages
- Add inline test helpers to reduce dependencies in dashboard routes tests
- Update executor tests with exact command matching and improved assertions
- Update AGENTS.md with test optimization patterns (fake timers, unique temp dirs)
- Add database migration to merge steeringComments into comments field
- Update SQLite schema and queries to use unified comments column
- Refactor TaskStore to handle single comments field instead of dual fields
- Update dashboard components (SteeringTab, TaskCard) for unified comments
- Update engine executor and PR comment handler for new field structure
- Remove deprecated steeringComments from types and interfaces
- Rename @kb/core, @kb/dashboard, @kb/engine to @fusion/* namespace
- Update all import statements across 143+ files to use new package names
- Update workspace dependencies and root package.json references
- Fix bundler configurations (tsup, vite) for new package names
- Update test files and fix typecheck issues
- Add changeset file documenting the package rename
- Add section expansion state management with localStorage persistence
- Update section headers with chevron toggle controls
- Implement conditional task row rendering based on section state
- Add Expand All / Collapse All toolbar controls
- Add CSS styles for chevron rotation animation and section headers
- Add comprehensive tests for collapsible section behavior