U11 fallout: disprove the coding-ideas column collapse, and correct a U11 note that recorded the merge backwards (#2651)

Two findings, no behavior change. Both are about **recorded reasoning
that was wrong** — the kind that sends the next person the wrong way.

## 1. The coding-ideas column collapse does not work (IR change
reverted)

I implemented it — deleted `ideas`, moved its `intake`/`autoTriage:
false` onto Planning, repointed the `start` anchor, updated the IR
suites to the merged shape (they went green, 44/44). Then the wider
suites failed and showed why it cannot work.

**The manual gate IS the column boundary.** `replan-target.ts` names the
discriminator in its own comment: *"The real discriminator is which lane
the triage service SCANS, which depends on the intake column's
`autoTriage` config."* So `ideas` is unscanned, `todo` is scanned, and
"promote" means moving the card from one into the other. Merge them and
one column must be both:

| if… | consequence |
|---|---|
| `autoTriage: false` wins | never scanned → nothing is ever planned →
the capacity hold releases an **unplanned** card into `in-progress`,
violating FN-7648 |
| scanning wins | `autoTriage: false` is meaningless → the manual gate
is gone → the preset duplicates the default Coding workflow |

**8 tests fail, and they are not fixtures** — they encode the promotion
flow itself, e.g. `store-create-intake-column.test.ts` › *"promotes an
Ideas-parked task to todo without planning it (still bootstrap-stub
PROMPT.md)"*. Rewriting them would have meant inventing what "promote"
means with no destination column, which is how a broken flow gets
blessed by a green suite.

**What it would actually take:** a promoted flag the triage scan reads,
so one column can hold both "not yet promoted" and "being planned". That
is a new lifecycle signal, not a column merge — the same shape as the
deferred `needs-replan` follow-up. Happy to scope it.

**I also corrected my own earlier checklist** in this doc, which said to
delete the now-dead `isUnplannedStartCreate` arm. Wrong: `autoTriage` is
a general trait field (`builtin-traits.ts`), so any custom workflow can
declare a manual intake with `intake !== hold`. The arm is dead only for
this preset.

## 2. `replan-target.ts` recorded the U11 merge backwards

The note claimed U11 deletes `todo` and keeps `triage`. It is the
reverse — Shape B kept the id `todo` and deleted `triage`, precisely so
the ~120 `column === "todo"` guards kept their meaning and no data
migration shipped. The default lineage now declares `todo, in-progress,
in-review, done, archived`.

The lookups are correct today, but **for the opposite reason to the one
recorded**: the default lineage falls *through* the `triage` lookup and
lands on `todo`, its merged planning column. `triage` still matches the
workflows that genuinely declare it (Lead generation, PR review).

Also flagged without changing (it would be a behavior change): the
`return "triage"` fallbacks on the no-match and throw paths name a
column the default lineage no longer declares, so a workflow with
neither `triage` nor `todo` gets a nonexistent target.

## Census

**Unchanged: 781 total, triage 5.** This PR adds no guards and converts
none — `workflowHasColumn(ir, "triage")` is a call argument, not a
comparison, so it is outside what the census counts either way.

## Verification

41/41 engine replan-target suites (including the existing
`replan-target-merged-planning-column` suite that covers the corrected
behavior) · engine typecheck clean · the reverted IR restores the tree
to main's content for those three files, verified by `git checkout --`.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-07-29 23:26:49 -07:00
committed by GitHub
parent 22a66c3a51
commit 0c07584d51
2 changed files with 51 additions and 4 deletions

View File

@@ -251,3 +251,40 @@ Four passing cases pin what a fix must NOT break: every declared lifecycle move
undeclared column on purpose (the path that rescues already-stranded cards). Plus a premise test
asserting the compatibility flag really is unset, so the suite fails loudly if that ever changes
rather than silently testing a different code path.
### CORRECTION (same day): the collapse is NOT mechanical — it is contradictory
I implemented the checklist above, ran the suites, and it does not work. Recording the disproof
because the checklist made it look like a fixture update.
The two-column shape works because the triage service SCANS one lane and skips the other.
`replan-target.ts` names the discriminator exactly:
> The real discriminator is which lane the triage service SCANS, which depends on the intake
> column's `autoTriage` config, not on the intake/hold roles alone.
So in Coding (Ideas): `ideas` (intake, `autoTriage: false`) is NOT scanned, `todo` IS, and "promote"
means moving the card from the unscanned lane into the scanned one. That move is the gate release.
Merge them and one column must be both the unscanned manual intake AND the scanned planning lane.
There is no consistent answer:
- `autoTriage: false` wins -> the column is never scanned, so nothing is ever planned. Cards sit
with a bootstrap-stub PROMPT.md until the capacity hold releases them, which sends an UNPLANNED
card into `in-progress` — the FN-7648 invariant ("no unplanned card enters a processing column").
- scanning wins -> `autoTriage: false` means nothing, the manual gate is gone, and the preset is a
duplicate of the default Coding workflow.
Evidence: 8 tests fail, and they are not fixtures — they encode the promotion flow itself, e.g.
`store-create-intake-column.test.ts` › "promotes an Ideas-parked task to todo without planning it
(still bootstrap-stub PROMPT.md)". Rewriting them would have required inventing what "promote" means
with no destination column.
**What the collapse actually requires:** a release mechanism for a manual gate that is not a column
move — a per-task promoted flag the triage scan reads, so one column can hold both "not yet
promoted" and "promoted, being planned". That is a new lifecycle signal, not a column merge, and it
is the same shape as the deferred `needs-replan`-to-purpose-built-signal follow-up.
Also correcting step 4 of the checklist above: do NOT delete the `isUnplannedStartCreate` arm.
`autoTriage` is a general trait field (`builtin-traits.ts`), so any custom workflow can declare a
manual intake with `intake !== hold`. The arm is dead only for THIS preset, not in general.

View File

@@ -332,10 +332,20 @@ export async function resolveReplanTargetColumn(store: TaskStore, taskId: string
alone. Resolving that correctly is its own change with its own evidence; it is
deliberately not smuggled into a conversion PR.
U11 IMPACT, flagged rather than assumed: the second lookup asks for `"todo"`,
which U11 deletes. For builtin coding the first lookup still matches `triage`
(which U11 keeps), so the builtins stay correct — but a workflow relying on the
`todo` branch loses it. That is a real follow-up, not a blocker for this PR.
U11 IMPACT — CORRECTED 2026-07-29-23:59. The note here previously said U11 deletes `todo` and
keeps `triage`. It is the other way round: U11 chose "keep the id `todo`, delete `triage`" so
that the ~120 `column === "todo"` guards kept their meaning and no data migration shipped. The
default lineage now declares `todo, in-progress, in-review, done, archived` and NO `triage`.
The lookups below are therefore correct today, but for the opposite reason to the one recorded:
the default lineage FALLS THROUGH the `triage` lookup and lands on `todo`, which is its merged
planning column and the right replan target. `triage` still matches for the workflows that
genuinely declare it (Lead generation, PR review — where it is the intake/planning lane).
STILL A REAL FOLLOW-UP, unchanged by the correction: the `return "triage"` fallbacks on the
no-match and throw paths name a column the default lineage no longer declares, so a workflow
with neither `triage` nor `todo` is handed a nonexistent target. Behavior change, so it is
flagged rather than fixed in a comment correction.
*/
if (workflowHasColumn(ir, "triage")) return "triage";
if (workflowHasColumn(ir, "todo")) return "todo";