U11: merge Todo into Planning on the default lineage (+ the migration mechanism, and a measured safety audit that cuts the work list 32%) (#2515)
**Merges Todo into Planning on the operator's real default workflow.** Held from merge pending the `triage` literal audit below — see *Gating*. ## The board change `builtin:coding` → `BUILTIN_STEPWISE_FINAL_REVIEW_CODING_WORKFLOW_IR` → clones `BUILTIN_STEPWISE_CODING_WORKFLOW_IR`. That IR now declares **five** columns, and `plan`, `plan-review`, `plan-replan` and `start` all live in the merged Planning column: ``` columns: todo="Planning", in-progress, in-review, done, archived start -> todo plan -> todo plan-review -> todo plan-replan -> todo parse -> in-progress (first implementation node) ``` The id stays `todo`, the display name becomes "Planning". That is the cheaper half: `todo` was already the hold column, so every trait lookup, task row, stored selection and the 121 `column === "todo"` guards keep their meaning, and **no stored row needs re-homing**. Promoting `triage` instead would have produced the same board while making those guards workflow-*dependent* — live for Coding (Ideas), silently dead for Coding. `builtin:legacy-coding` keeps its six-column shape, per the operator's decision. It exists to be the old thing. ## Entry contract, before and after each IR edit | | result | |---|---| | before the default-lineage edit | **15 passed** | | after the edit | **13 passed, 2 failed** | | after reading both | **15 passed** | Neither failure was routed around. One was a genuine expectation change (two planning entry points became one); the other was my own `mergeTodoIntoPlanning` helper throwing *"source IR is not the split-column shape this merge transforms"* — because production **is** the merged shape now. I **deleted** the helper rather than making it tolerant: a transform that has silently become a no-op asserts nothing. ## The safety argument, proven not asserted Entering at `start` is exactly what dragged cards backward in the three earlier reverted attempts. `merged-planning-start-node-no-move.test.ts` proves against the **real** boundary controller and **real** default IR that entering `start` performs no move (`moveTask` is never *called*), reaches no hold→wip capacity seam, and **still moves on a genuine crossing** so the no-op is same-column rather than a disabled boundary. Removing the controller's same-column short-circuit turns exactly the two no-move tests red. ## The migration mechanism A card can outlive its column. `resolveAllowedColumns` derives targets from graph adjacency, and an undeclared source has none — so it returned `[]` and **every** move was rejected with "Valid targets: none", including the one that would rescue the card. An undeclared source now resolves to the workflow's rebound target. Escape hatch, not relaxation: declared columns are untouched, and it offers the rebound target *only*, so a stranded card gets back **into** the lifecycle rather than a free jump past review. ## A real regression this surfaced `isDefaultWorkflowColumns` matched the legacy **six** ids as a set. The merged default declares five, so the match stopped firing and the default board fell through to neighbor-only adjacency, which **drops legal moves and invents an illegal one**: | edge | effect | |---|---| | `in-progress → done` | **dropped** — the mission-validation cross edge | | `in-review → todo` | **dropped** — review work back to planning | | `todo/done → archived` | **dropped** — the FN-4892 direct-archival edges | | `done → in-review` | **invented** — a backward edge no rule allows | Adjacency now derives from lifecycle **roles**. The load-bearing assertion: the legacy six still reproduce `VALID_TRANSITIONS` **verbatim**. Applied only when a workflow declares the full role set, so custom boards keep neighbor adjacency. ## Failure accounting (core package, vs a 49-failure baseline) | stage | failed | new | |---|---:|---:| | after the merge | 65 | 18 | | after the escape hatch | 52 | 5 | | after role-derived adjacency | 53 | 4 | The 4 remaining are 3 `builtin-workflows` expectations encoding the pre-merge shape and 1 create-intake expectation naming `triage` on `builtin:coding`. Two `schema-applier` and two `workflow-reconciliation-production-shape` failures appeared in intermediate runs and are **not mine** — both files pass in isolation (75/75 and 7/7). I re-ran each before attributing them, which is why the earlier "priority" flag on the reconciliation pair was withdrawn. Gate: **309/309**. Lint clean. ## Gating: the `triage` audit (`docs/solutions/architecture-patterns/u11-triage-literal-safety-audit.md`) Program tracking cited **58** `triage` comparisons. Measured with the same pattern: | | count | |---|---:| | raw comparisons | 87 | | inside comments | 1 | | **not a lifecycle column at all** | **15** | | column comparisons | 71 | | OR-paired with `"todo"` in the same expression | 32 | | **exclusive `triage` — the real work list** | **39** | **15 do not compare a column.** `role === "triage"`, `surface === "triage"`, `sessionPurpose === "triage"`, `entry.agent === "triage"` name the planning **agent**. Converting them would be actively wrong, and the failure — a planning agent that can't resolve its prompt template — would look nothing like a column bug. **One site changes an operator-visible affordance**, which is why per-site review beat a sweep: `TaskCard.tsx:1927` — `taskColumnFlags?.intake === true && task.column !== "triage"`. The literal is a **narrowing**, not a match. After the merge a Planning card has `intake === true` and `column === "todo"`, so the narrowing stops applying and **Start begins rendering on default Planning cards where it previously did not.** A sweep would have "converted" the literal and shipped the new affordance silently. These guards do not go **dead**, they go **workflow-dependent** — `triage` stays live for legacy-coding, Ideas, every linear built-in and any user workflow (R11) — which is harder to detect than dead. Work list and ownership are in the audit doc. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
---
|
||||
category: architecture-patterns
|
||||
module: "@fusion/core, @fusion/engine, @fusion/dashboard"
|
||||
date: 2026-07-29
|
||||
problem_type: migration_audit
|
||||
component: workflow-columns
|
||||
severity: high
|
||||
applies_when:
|
||||
- "Landing U11's deletion of `triage` from the default coding workflow"
|
||||
- "Deciding whether a `column === \"triage\"` site is broken by that deletion"
|
||||
- "Counting the remaining lifecycle-literal conversion surface"
|
||||
tags:
|
||||
- workflow-columns
|
||||
- u11
|
||||
- migration
|
||||
- literal-conversion
|
||||
related_components:
|
||||
- workflow_graph
|
||||
- triage
|
||||
- self-healing
|
||||
- dashboard
|
||||
---
|
||||
|
||||
# The `triage` literal surface, measured per site
|
||||
|
||||
U11 merges Todo into Planning by keeping the id `todo` and **deleting `triage`** from the default
|
||||
coding lineage. Every surviving `column === "triage"` comparison is therefore a candidate for
|
||||
silent breakage — a guard that stops matching does not fail a test, it disables a path.
|
||||
|
||||
This audit exists because the headline count is misleading in both directions, and shipping the
|
||||
deletion on top of an estimate is how a green merge produces a broken board.
|
||||
|
||||
## The count, and why the headline number is wrong
|
||||
|
||||
Program-level tracking cited **58** `triage` comparisons. Measured directly with the same pattern
|
||||
across `packages/*/src` + `packages/dashboard/app`, excluding tests:
|
||||
|
||||
| | count | meaning |
|
||||
|---|---:|---|
|
||||
| Raw `=== "triage"` / `!== "triage"` | **87** | the grep everyone quotes |
|
||||
| — inside comments | 1 | not code |
|
||||
| — **not a column at all** | **15** | `role`, `agentType`, `sessionPurpose`, `surface`, `entry.agent` |
|
||||
| **Column comparisons** | **71** | the only ones the deletion can reach |
|
||||
| — OR-paired with `"todo"` in the SAME expression | **32** | a merged Planning card still matches |
|
||||
| **Exclusive `triage`, needing individual proof** | **39** | the real work list |
|
||||
|
||||
Two things this changes:
|
||||
|
||||
1. **15 of the 87 are not lifecycle columns.** `agent-prompts.ts`'s `role === "triage"`,
|
||||
`tool-availability.ts`'s `surface === "triage"`, `skill-resolver.ts`'s
|
||||
`sessionPurpose === "triage"`, `TaskChatTab.tsx`'s `entry.agent === "triage"` — these name the
|
||||
**planning agent**, not the planning column, and are unaffected by any IR change. Converting
|
||||
them would be actively wrong. A file-level count cannot see this distinction.
|
||||
|
||||
2. **32 more are already safe** because the branch accepts `todo` in the same expression. A card
|
||||
that used to be in `triage` is now in `todo`, so it still matches. These need no change and no
|
||||
test.
|
||||
|
||||
So the surface that actually gates U11 is **39 sites**, not 58 and not 87.
|
||||
|
||||
## Why "not a column" is not a technicality
|
||||
|
||||
`triage` is overloaded in this codebase: it is a column id, an agent role, a session purpose, and
|
||||
a prompt-template family. Only the first is affected by the IR. A conversion sweep driven by the
|
||||
raw grep would rewrite the other three, and the resulting failure — a planning agent that no
|
||||
longer resolves its own prompt template — would look nothing like a column bug.
|
||||
|
||||
## The safety rule
|
||||
|
||||
After the merge, a default-workflow card that used to rest in `triage` rests in `todo`. So:
|
||||
|
||||
- **SAFE** — the site's `triage` branch also accepts `todo`, or resolves by trait
|
||||
(`flags.intake`/`flags.hold`), or compares a resolved entry column rather than a literal.
|
||||
- **NEEDS PROOF** — the `triage` branch is exclusive. Then ask: *when this stops matching for a
|
||||
default-workflow card, what does the operator lose?* If the answer is nothing, the site is dead
|
||||
for that lineage and safe. If the answer is an affordance or a recovery path, it is breakage.
|
||||
|
||||
`triage` remains a **live column id** for `builtin:legacy-coding`, Coding (Ideas), every linear
|
||||
built-in, and any user-authored workflow (R11). So these guards do not go dead — they go
|
||||
**workflow-dependent**, which is harder to detect than dead. That is the reason for per-site proof
|
||||
rather than a sweep.
|
||||
|
||||
## The 39, by owner
|
||||
|
||||
Ownership follows the program's file assignments; this audit does not claim them.
|
||||
|
||||
| File | sites | owner |
|
||||
|---|---:|---|
|
||||
| `engine/src/self-healing.ts` | 7 | capacity worker |
|
||||
| `dashboard/src/routes/register-task-workflow-routes.ts` | 6 | U12 worker |
|
||||
| `dashboard/app/components/TaskCard.tsx` | 6 | U12 worker |
|
||||
| `core/src/task-store/task-creation.ts` | 3 | see note — already mitigated |
|
||||
| `engine/src/replan-target.ts` | 3 | U7 worker (2 are comments) |
|
||||
| `dashboard/app/components/ListView.tsx` | 3 | U12 worker (1 is a comment) |
|
||||
| `dashboard/app/components/TaskDetailModal.tsx` | 2 | U12 worker |
|
||||
| `dashboard/app/components/TaskContextMenu.tsx` | 2 | already trait-paired — safe |
|
||||
| remaining 7 files | 1 each | scattered |
|
||||
|
||||
### Already resolved or safe on inspection
|
||||
|
||||
- **`TaskContextMenu.tsx:143`** — `column === "triage" || flags?.intake || flags?.hold`. Already
|
||||
trait-paired (U10). The literal is a legacy fallback, not the decision.
|
||||
- **`task-creation.ts:493/863`** — `isIntakeColumn` ORs the literal with the **resolved** entry
|
||||
column, so a merged Planning card matches through the resolved half. The expression-level pairing
|
||||
check misses this because it pairs on the literal `"todo"`, not on a resolved variable. Mitigated
|
||||
further by the `resolveDefaultWorkflowIntakeColumn` fix already on this branch.
|
||||
- **`replan-target.ts:100`, `ListView.tsx:659`** — prose inside comments describing the old
|
||||
behavior. No code effect.
|
||||
|
||||
### Flagged as a real behavior change, not yet owned
|
||||
|
||||
- **`TaskCard.tsx:1927`** — `taskColumnFlags?.intake === true && task.column !== "triage"`. The
|
||||
literal here is a **narrowing**: it suppresses the Start affordance on the legacy intake column.
|
||||
After the merge a default Planning card has `intake === true` and `column === "todo"`, so the
|
||||
narrowing stops applying and **Start begins rendering on default Planning cards where it
|
||||
previously did not**. That is an operator-visible affordance change, and it is the kind that the
|
||||
Surface Enumeration rule exists to catch. It needs an explicit decision, not a mechanical
|
||||
conversion.
|
||||
|
||||
## Do not land the deletion on an estimate
|
||||
|
||||
The instruction that produced this audit was correct: verify per site, do not assume. The measured
|
||||
result is that the work list is **32% smaller** than the tracked figure, that **15 sites must not
|
||||
be converted at all**, and that at least one site changes an operator-visible affordance in a way
|
||||
no column-conversion sweep would have surfaced.
|
||||
Reference in New Issue
Block a user