From eb874f3da35d42aff82686ef5b579371e3874bdc Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Wed, 29 Jul 2026 22:04:59 -0700 Subject: [PATCH] =?UTF-8?q?convert(cli/commands/task.ts):=20triage=20guard?= =?UTF-8?q?=201=20=E2=86=92=200=20(+=20a=20live=20main=20regression=20in?= =?UTF-8?q?=20the=20lifecycle=20E2E=20release=20path)=20(#2627)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Batched push, gate open.** One conversion; the two E2E commits are held back and the reason is below — it is the more important half of this PR body. ## Conversion | File | triage column comparisons before | after | |---|---|---| | `packages/cli/src/commands/task.ts` | **1** | **0** | `pnpm test:gate` green, `pnpm lint` clean. All four non-terminal columns rendered the **same** glyph, so the four id comparisons were only ever asking "is this column terminal?". Naming `triage` made it a lifecycle-vocabulary site for no behavioural reason — the merged Planning column dropped that id, so the comparison silently stopped matching while the output stayed correct **by accident** (the fallthrough gave it the same glyph). Behaviour-identical **only** because the loop iterates the legacy `COLUMNS` constant (`types/board.ts:27` — exactly the six ids), so `col` can never be a custom id. Stated because the forms **diverge** outside that set: the old chain fell through to the terminal glyph for an unrecognised id, the new form returns the non-terminal one. If this ever iterates workflow-resolved columns that difference becomes live, and the right answer is a trait lookup, not this. **Deeper bug deliberately untouched, for U12:** because the loop iterates the legacy enum, a card in a workflow-renamed column **is not rendered at all**. That is R8's surface change, far bigger than this glyph. *(Note: this file is not on the 45-guard list, so it will not move your count. Flagging so the numbers reconcile.)* --- ## ⚠️ Live regression on origin/main — the lifecycle E2E release path While rebasing to push, the flagship lifecycle E2E went red. **I verified it on `origin/main` alone, with none of my commits: 2 failed / 18 passed.** ``` scenario 1 — DEFAULT vocabulary → AssertionError: expected [] to include 'FN-E2E-1' (r.sweep.released is EMPTY) scenario 2 — RENAMED vocabulary → audit trail differential broken: renamed produced [{end},{review}], default produced [] ``` Both are pre-existing tests I have never touched. **The capacity release sweep is releasing nothing.** **Likely cause, from reading rather than bisecting** — so treat it as a lead, not a verdict: `moves.ts:1059` now resolves a capacity pool id and enforces `enforcePooledColumnCapacity` **inside the move transaction** (#2488 "bind the in-transaction capacity gate", made user-visible by #2499 "make the capacity gate actually bind for real projects"). The E2E drives with `settings = { experimentalFeatures: { workflowGraphExecutor: true } }` — **no `maxConcurrent`** — while the fixture's wip column declares `{ trait: "wip", config: { limitSetting: "maxConcurrent", countPending: true } }`. If the resolved limit is finite and the pooled count meets it, the hold→wip move is rejected on capacity and the sweep correctly reports nothing released. If that is right, it is a **test-harness/production interaction, not a product break** — but it means the program's primary end-to-end evidence for the capacity boundary is currently inert on main, which matters for completion criterion 3. It needs the capacity worker's eyes, since #2488/#2499 are theirs and I would be guessing at the intended pool/limit contract. ## Why my two E2E commits are held They add scenario 3 (merged intake+hold board) and scenario 6 (REVISE → rework), both of which **depend on the same release leg**. On current main they fail for main's reason, taking the file from 2 failures to 4. Pushing them would add red to the count you are tracking and obscure whose regression it is. Both are complete, mutation-attributed, and green against the commit I wrote them on: | Scenario | Proves | Mutation that fails it | |---|---|---| | 3 — merged intake+hold | capacity release works from a dual-role column | `isHeldTask` treating intake/hold as exclusive → exactly its 2 tests | | 6 — REVISE → rework | `InReview → InProgress` on renamed *and* merged boards | disabling rework re-entry → exactly its 2 tests | They go out in the next batch the moment the release path is green. ## Also not shipped, twice attempted, deleted both times Safeguard 2 (`autoMerge:false` terminal-until-human) still has **no** graph-level E2E. Attempt 1 passed and then survived mutating `merge-gate` to ignore `task.autoMerge` — the card was parking on the review column's `merge-blocker` trait, not the gate. Attempt 2 removed that trait to isolate the gate, and then the *control* case parked too, so the flag still was not the discriminator. A fixture that can isolate it needs a merge path mirroring the builtin (`merge-gate → merge node → end`) rather than a direct edge to `end` — a real redesign, not a speculative edit. The enforcement that actually holds today is `allowInReviewMergeProcessing` in `project-engine` (unit-mutation verified, NEW=9; gated via #2526). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) --- packages/cli/src/commands/task.ts | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/commands/task.ts b/packages/cli/src/commands/task.ts index cccc69f719..1df973c98c 100644 --- a/packages/cli/src/commands/task.ts +++ b/packages/cli/src/commands/task.ts @@ -591,11 +591,28 @@ export async function runTaskList(projectName?: string) { if (colTasks.length === 0) continue; const label = COLUMN_LABELS[col]; - const dot = - col === "triage" ? "●" : - col === "todo" ? "●" : - col === "in-progress" ? "●" : - col === "in-review" ? "●" : "○"; + /* + FNXC:CliBoardGlyph 2026-07-29-22:40 (lifecycle-column vocabulary): + All four non-terminal columns rendered the SAME glyph, so the four id comparisons + were only ever asking "is this column terminal?". Naming `triage` here made it a + lifecycle-vocabulary site for no behavioural reason — the merged Planning column + dropped that id, and this comparison silently stopped matching while the output + stayed correct by accident (the fallthrough gave it `●` anyway). + + Asking the terminal question directly removes the vocabulary dependency. It is + behaviour-identical ONLY because this loop iterates the legacy `COLUMNS` constant + (types/board.ts:27 — exactly the six ids), so `col` can never be a custom id. Worth + stating because the two forms DIVERGE outside that set: the old chain fell through + to `○` for an unrecognised id, this returns `●`. If this ever iterates + workflow-resolved columns, that difference becomes live and the right answer is a + trait lookup, not this. + + NOT claimed as trait-resolved, and the deeper bug is left alone: because the loop + iterates the legacy enum, a card in a workflow-renamed column is not rendered AT + ALL. That is the R8/U10 surface change (no surface derives its column set from the + legacy enum) and a far bigger fix than this glyph. + */ + const dot = col === "done" || col === "archived" ? "○" : "●"; console.log(` ${dot} ${label} (${colTasks.length})`); for (const t of colTasks) {