Commit Graph

2707 Commits

Author SHA1 Message Date
gsxdsm
f2f6795010 FN-8635: keep worktree slider visible
Keep Command Center capacity controls visible and correctly editable across settings states.

- Render Max worktrees in the shared full-width range wrapper.
- Preserve capacity values after load failures and explain disabled worktree limits.
- Add control tests, browser geometry coverage, documentation, and a release changeset.

Files changed:
 .changeset/fn-8635-worktrees-slider.md             |   7 +
 docs/dashboard-guide.md                            |   2 +-
 .../command-center/CommandCenterControls.css       |   6 +
 .../command-center/CommandCenterControls.tsx       |  63 +++++---
 .../__tests__/CommandCenterControls.test.tsx       |  76 ++++++++-
 packages/engine/e2e/fn-8635-worktrees-slider.mjs   | 180 +++++++++++++++++++++
 6 files changed, 310 insertions(+), 24 deletions(-)

Fusion-Task-Id: FN-8635

Fusion-Task-Lineage: fb9c2a46-3c5c-4871-bab4-c43af20cb4de

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-31 18:03:13 -07:00
gsxdsm
475bb2d641 FN-8637: restrict Quick Add Start to manual-intake workflows
Restrict Quick Add Start eligibility to verified manual intake lanes.

- Require the server-derived manualIntake flag instead of hold alone.
- Preserve Coding Ideas routing while hiding Start for Coding's merged planning lane.
- Cover desktop and mobile eligibility behavior and document the updated rule.
- Add a patch changeset for the corrected workflow gating.

Files changed:
 .changeset/fn-8637-quick-add-start-manual-intake.md       |  7 ++++
 docs/dashboard-guide.md                            |  2 +-
 packages/dashboard/app/components/QuickEntryBox.tsx     | 21 ++++++------
 packages/dashboard/app/components/__tests__/Column.test.tsx       | 20 ++++++++---
 packages/dashboard/app/components/__tests__/ListView.test.tsx     | 40 ++++++++++++++++++----
 packages/dashboard/app/components/__tests__/QuickEntryBox.test.tsx    | 21 +++++++++---
 packages/dashboard/app/utils/__tests__/quickAddStart.test.ts      | 38 ++++++++++++++++++--
 packages/dashboard/app/utils/quickAddStart.ts      |  9 ++++-
 8 files changed, 128 insertions(+), 30 deletions(-)

Fusion-Task-Id: FN-8637

Fusion-Task-Lineage: 9652d7d8-f954-49e8-9a76-a2421654baae

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-31 17:27:43 -07:00
gsxdsm
95410b5de6 FN-8638: add Factory Light dashboard theme
Add a daylight industrial theme that persists across dashboard and desktop startup.

- Register Factory Light in persisted theme types, selectors, and bootstrap validators.
- Define Factory Light tokens and preview swatches for light and dark modes.
- Cover theme registration and rendered token contracts, and document the new option.

Files changed:
 .changeset/fn-8638-factory-light-theme.md          |   7 ++
 docs/dashboard-guide.md                            |   3 +-
 docs/settings-reference.md                         |   2 +-
 packages/core/src/types/execution-and-ui.ts        |   2 +
 .../app/__tests__/factory-light-theme.test.ts      | 106 +++++++++++++++++++++
 .../dashboard/app/components/ThemeSelector.css     |  14 +++
 .../components/__tests__/ThemeDropdown.test.tsx    |   2 +-
 .../components/__tests__/ThemeSelector.test.tsx    |   2 +-
 .../__tests__/CommandCenterControls.test.tsx       |   2 +-
 packages/dashboard/app/components/themeOptions.ts  |   1 +
 packages/dashboard/app/index.html                  |   2 +-
 packages/dashboard/app/public/theme-data.css       |  86 ++++++++++++++++-
 packages/desktop/src/renderer/index.html           |   1 +
 13 files changed, 223 insertions(+), 7 deletions(-)

Fusion-Task-Id: FN-8638

Fusion-Task-Lineage: 3b78bc31-0f03-4299-8f5f-1a69ac7c604a

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-31 17:13:53 -07:00
gsxdsm
29eb512d57 docs(solutions): the general shape — a green that answers a different question (#3273)
Extends the doc merged in #3255 with two more instances of the same
pattern, both found this session, **neither involving a ratchet**. Four
instances now, from four unrelated directions:

| what was read as "pass" | what the green actually meant |
| --- | --- |
| `node scripts/check-*.mjs` exits 0 | report-only mode — the failure
path needs `--strict` |
| a census reports 0 for a new file | the file is untracked, so it was
never scanned |
| a backgrounded `cmd > log; grep …` reports exit 0 | that is `grep`'s
status; the suite inside had 8 failures |
| a rebased branch's tests pass | the rebase never started, so it ran on
the **old** base |

The two new ones are worth writing down because they are not about
tooling anyone built here — they are about how results are read.

**Exit codes belong to the last command in the pipeline.** A
backgrounded `run_tests > log 2>&1; echo done; grep X log` exits with
`grep`'s status, so the harness reported "completed, exit code 0" for a
dashboard suite that had 8 failures. I nearly recorded that suite as
green. Read the summary out of the log; never infer a suite's result
from a wrapper's exit code.

**A failed rebase leaves you on the old base, and the tests still pass
there.** `git rebase` refused with `cannot rebase: You have unstaged
changes`, so the branch never moved. `git diff origin/main` then listed
20+ files including other workers' commits — which reads exactly like my
branch had reverted their work — and a full test run on that tree came
back green. Both signals were true about a tree nobody cared about.

```
git merge-base --is-ancestor origin/main HEAD
```

said STALE while the tests said pass. That is the only check that
separates the two, and it belongs before any claim of "verified on
current main".

The shared tell, stated once: **a result too clean, or too alarming, for
what changed.** Every probe shape passing including ones that obviously
should not; a two-file branch appearing to revert twenty. When the
answer does not fit the size of the question, find out what was actually
measured before believing it.

## Verification

Docs only; no code paths change. `lifecycle-columns`,
`move-target-literals`, `inert-sync-lanes`, `quarantine-ledger` all exit
0. No changeset — AGENTS.md excludes internal docs.

**Pre-existing red, not from this branch:** `check:fnxc-future-dates`
currently fails on main from a `2026-08-01-00:50` stamp in
`packages/core/src/task-store/lifecycle-ops.ts` (commit `e52da740a5`) —
a timezone-ahead clock writing tomorrow's date, at 23:45 UTC. Already
claimed by **#3269 and #3270**, so I have not touched it; flagging only
so this branch's CI result is not misattributed. It is the same
recurring class this doc's sibling rule addresses: take the stamp from
`date -u`, not the local clock.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Documentation**
* Added guidance for identifying misleadingly successful CI and test
results.
* Documented checks for report-only runs, untracked files, masked
failures, and tests running on an outdated code base.
* Included recommendations for reviewing logs and verifying branch
ancestry.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 17:09:24 -07:00
gsxdsm
d14294b6cb docs(solutions): add the count-based probe, which the report-only trap cannot fool (#3257)
## What

Adds one technique to #3255. Docs only.

#3255 records that probing a ratchet **by exit code** can read green
because the tool is report-only without `--strict` — a real trap that
nearly got a healthy gate reported as dead. There is a second technique
that sidesteps it entirely and is strictly more informative: **parse the
tool's own per-file count.**

```bash
node scripts/check-move-target-literals.mjs 2>&1 | grep -a "my-probe-tmp" \
  | grep -aoE "^ +[0-9]+" | tr -d ' '
```

**Immune to the report-only trap** — a report-only run still *prints*
the count, so the number moves 0 → 1 whether or not `--strict` was
passed.

**It measures which shapes, not just whether something fired.** An exit
code is one bit for the whole run. Auditing a detector means asking *"of
these five spellings, which are seen?"*, and five separate binary runs
cannot distinguish **partial** detection from a probe file that failed
to compile. The move-target audit read `direct 1 / backtick 1 / ternary
0 / const 0` in a single run, which named the gap immediately.

## Both belong

| question | technique |
|---|---|
| **can this ratchet fail at all?** | `pnpm check:*` — ask this first
(#3255 §1) |
| **what can it see?** | per-file counts — an exit code is too coarse |

I also added a caveat that applies to both: confirm the probe is
actually being scanned by watching the tool's **scanned-file total**
move. A probe that never compiled and a probe the tool never discovered
both report zero hits, and neither is a finding — that one cost me a
wasted measurement before I noticed the total had stayed at 1961.

## Why this is worth a follow-up rather than a comment

#3255's rule as written — *"use `pnpm check:*`, not a bare `node
scripts/...`"* — would have made the shape-coverage audits impossible,
since `--strict` collapses five distinct per-form answers into one bit.
The rule is right for its question and wrong for the other one, and the
distinction is easy to lose once only the rule survives in someone's
memory.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Documentation**
  * Added guidance for evaluating ratchets using per-file output counts.
* Documented report-only and shape-coverage limitations, count-based
versus failure-based checks, and verifying that probe files were
scanned.
  * Included a command example for probing ratchet behavior.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 16:03:58 -07:00
gsxdsm
37d891e879 FN-8633: improve tablet terminal dragging
Give floating tablet terminals a dedicated drag grip while preserving tab-strip panning.

- add a touch-sized tablet-only header drag grip and pop-out hit target
- preserve floating geometry at the tablet breakpoint and document the gesture
- cover grip availability, dragging, and horizontal tab-panning CSS isolation

Files changed:
 .changeset/fn-8633-tablet-terminal-drag.md         |  7 ++
 docs/dashboard-guide.md                            |  4 +-
 packages/dashboard/app/components/TerminalModal.css | 61 +++++++++++++
 packages/dashboard/app/components/TerminalModal.tsx | 16 ++++
 packages/dashboard/app/components/__tests__/TerminalModal.test.tsx | 99 ++++++++++++++++++++++
 5 files changed, 185 insertions(+), 2 deletions(-)

Fusion-Task-Id: FN-8633

Fusion-Task-Lineage: f1c442a8-3302-4f6a-98e9-f1efa4083c12

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-31 15:47:58 -07:00
gsxdsm
9690f46439 docs(solutions): probe the instrument the way CI runs it (#3255)
Records two instrument-level defects found this session. Both were in
the tools the program uses as ground truth, and both looked exactly like
a pass.

## 1. A ratchet that could not fail from the command I typed

`check-move-target-literals` is report-only unless given `--strict`,
which `package.json` supplies. Probed bare, it returned **exit 0 for
every probe** — including a blatant `moveTask(id, "in-review")` pasted
into `scheduler.ts`.

That is the exact signature of a dead ratchet, and I nearly reported
another worker's guard as inert on the strength of it. The guard was
fine; my invocation could not fail. What makes it dangerous is the
output: a report-only run prints its normal summary line and exits 0, so
the terminal is indistinguishable from a genuine pass.

## 2. A ratchet that could not see the file I had just written

`lifecycle-column-census` and `check-move-target-literals` discovered
files with `git ls-files` — **tracked only** — while the other five walk
the filesystem.

| new file with a plain legacy guard | result |
| --- | --- |
| same guard in an already-tracked file | caught |
| new file, untracked | **missed, exit 0** |
| identical file, `git add`ed | caught, exit 1 |

The detectors are fine. The blindness is discovery, and it lands at the
one moment the number is consulted: add a helper, check your own work,
read zero, commit — and it surfaces later in someone else's CI run,
attributed to a push instead of to the edit. The tool was answering
about the last commit while being asked about the working tree.

## 3. Why it is worth a doc rather than two one-line fixes

Individually these are cheap. Together they cost a day.

Because `check-inert-sync-lane-conversions` walks the filesystem and the
census did not, the **same probe file** was caught by one and missed by
the other. I read that differential as a claim about expression walking
and investigated it as one — the real cause was that two instruments in
the same program disagreed about which files exist.

When the measuring tools disagree about their own domain, every
differential between them is unreadable until someone notices. That is
the transferable lesson, and it is not visible from either fix alone.

## Status of the fixes

- Census discovery scope: **#3254** (open).
- Type-assertion blind spot in the sync-lane ratchet: **#3252** (open).
- `check-move-target-literals` discovery scope: reported to **#3253**,
whose author is already in that file — not touching it.

## Verification

Docs only; no code paths change. All eight ratchets exit 0. No changeset
— AGENTS.md excludes internal docs.
2026-07-31 15:42:55 -07:00
gsxdsm
59dfc4678b docs(solutions): record what each lifecycle ratchet cannot see, measured (#3251)
## What

This note already prescribes: *"Before trusting a ratchet: mutate the
shape it claims to catch and confirm it exits non-zero."* This is that
checklist item **executed against all five lifecycle gates** on one
tree, one staged probe file per form. Docs only.

**Two of the five were wrong.**

| gate | catches | does NOT catch |
|---|---|---|
| `lifecycle-column-census` | `===` / `!==` | ~~membership, switch~~
**fixed (#3247)** |
| `check-move-target-literals` | direct + backtick destinations |
~~ternary~~ **fixed (#3250)**; still misses a destination bound to a
local |
| `check-sql-column-literals` | `"column"` comparisons — **including
plain template literals**, not only drizzle `sql` tags | nothing; the
one miss probed was an identifier the schema never uses |
| `check-inert-sync-lane-conversions` | lane reads via the
`resolvePlannerLanes` helper | a **direct**
`store.resolveTaskWorkflowIrSync(...)` read feeding
`resolveLifecycleColumns` — inert by the same mechanism, untracked |
| `check-fnxc-future-dates` | future stamps | nothing — it caught this
table's author, twice |

## The two lessons the table encodes

**A ratchet's blind spot is invisible in exactly the way its subject
is.** Both fixed gaps sat next to a printed zero *and a sentence
promising nothing could land silently*. The count was true. The sentence
was true only for the forms the parser happened to visit. That is the
same shape as the conversions this program spent weeks finding — code
that looks converted because the instrument cannot see the difference.

**Probe correctness is its own trap.** The first census probe measured
nothing: the scanner enumerates git-tracked files, the probe was
untracked, and the scanned-file count staying flat reads *exactly* like
"no gap". A `DELIBERATE-LITERAL` probe likewise read as a broken escape
hatch until the marker moved to its own line — mid-expression it
attaches to the wrong node, which is the documented gotcha, and it still
caught the person who had just written it down.

## Reported, not fixed

The inert-sync gap is left open deliberately: it is one narrow shape,
the only in-tree instance (`replan-target.ts:95`) is documented, new
conversions would use the tracked helper, and that gate has uncommitted
work from another worker. Recording it beats editing a file someone else
is mid-change on.

```
lint clean; fnxc-future-dates: none added; all five gates --strict green on this tree
```
2026-07-31 15:21:53 -07:00
gsxdsm
bcaa48390b FN-8627: add Sage color theme
Add the Sage palette across persisted dashboard and desktop theme selection paths.

- Register Sage in core, dashboard bootstrap, desktop, and selector metadata.
- Add dark and light Sage tokens plus independently resolvable swatches.
- Cover registration, token, selector, and documentation updates.

Files changed:
 .changeset/fn-8627-sage-theme.md                   |   7 ++
 docs/dashboard-guide.md                            |   3 +-
 packages/core/src/types/execution-and-ui.ts        |   2 +
 .../dashboard/app/__tests__/sage-theme.test.ts     | 101 +++++++++++++++++++++
 .../dashboard/app/components/ThemeSelector.css     |  14 +++
 .../components/__tests__/ThemeDropdown.test.tsx    |   2 +-
 .../components/__tests__/ThemeSelector.test.tsx    |   2 +-
 .../__tests__/CommandCenterControls.test.tsx       |   2 +-
 packages/dashboard/app/components/themeOptions.ts  |   1 +
 packages/dashboard/app/index.html                  |   2 +-
 packages/dashboard/app/public/theme-data.css       |  86 +++++++++++++++++-
 packages/desktop/src/renderer/index.html           |   1 +
 12 files changed, 217 insertions(+), 6 deletions(-)

Fusion-Task-Id: FN-8627

Fusion-Task-Lineage: fd4353b3-1e0c-4c7e-84dd-bcad2815178c

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-31 14:58:53 -07:00
gsxdsm
478b15d7ec docs(solutions): add the CI failure-rate method, and a fourth instance (#3244)
## What

Follow-up to #3243. That note said *"take a second measurement of a
different kind"* — true, and useless at 2am without the technique. This
adds the one that actually settled every case, plus a fourth instance
that occurred after #3243 was written. Docs only.

## The technique

Enumerate recent failing CI runs and compute a **per-file failure
rate**. Seven runs separated three populations that are
indistinguishable from a single local run:

| rate on CI | meaning | action |
|---|---|---|
| **7/7** | consistent, real | fix, or diagnose and hand off with
evidence |
| **1/7** | intermittent | flake or race; two in one subsystem is a
product-race smell |
| **0/7** (fails only locally) | environment | fix your sandbox, change
**nothing** in the repo |

Measured on this repo's main while writing it: `planning-browser-e2e`
**7/7**, `postgres/schema-applier` **1/7**, `report-store.pg` **1/7**.

## The fourth instance

#3243 documented three reversals. A fourth happened after it merged: a
component test with **2 failing cases locally, 0/7 on CI**. That makes
**three separate local-only failures in a single session** — a
model-routes test hanging offline, a component test with four failing
cases, and a set of assertions I was ready to call a regression.

Each felt like a finding. All three were my sandbox. That is frequent
enough to be a habit rather than bad luck, which is why it is worth a
row in a table rather than a mention.

## The cost asymmetry, which should drive the default

Acting on a **0/7** by quarantining **deletes coverage that is green
everywhere else**. Acting on a **7/7** by investigating costs an hour.
The errors are not symmetric, so when unsure which row you are in, the
cheap move is always more samples from the *other* environment — not
more confidence about the one you have.

This is the concrete form of the point the standing quarantine rule
already encodes with *"without a corresponding real bug"*: **"I saw it
fail" is not that clause**, and the failure-rate table is how you tell
the difference before acting.

```
lint clean; fnxc-future-dates: none added (exit code checked before piping)
```
2026-07-31 14:12:51 -07:00
gsxdsm
5365746d37 docs(solutions): record "one sample is not a diagnosis" (#3243)
## What

A `docs/solutions` note recording three diagnoses I reversed **in one
session**, all wrong the same way. Docs only.

## The three

| observed | my story | what it was |
|---|---|---|
| `planning-browser-e2e` fails at width **769**, passes at **768** |
layout regression at the tablet breakpoint, from FN-8606 | a **race** —
5 passes in 6 runs; on every pass the control sits inside the viewport
at 769 (`right: 753 ≤ 769`) |
| a model-routes test fails **3 of 3** locally | red on main; quarantine
candidate | **green on CI**; a sandbox interaction. The fixture is
configured offline, so a sandbox should not have changed the outcome —
the tell was there from run one |
| one approach could not cover a resolver | the site is **unpinnable** |
a *different shape* covered it — a helper that **resolves** rather than
one that **receives** |

Each was plausible, mechanistic, and consistent with the evidence I had.
That is what made each dangerous: **a diagnosis that explains your one
data point feels finished.**

Each survived exactly until a second measurement **of a different kind**
— another environment, more samples, an instrumented probe. Re-running
the same command is not a second measurement.

## The reusable part

| observation | tempting story | check first |
|---|---|---|
| fails at boundary X, passes at X−1 | structural bug at the boundary |
run it 5 more times — boundaries are where races surface |
| **consistent** locally, green on CI | main is broken | the
environment; consistency is not universality |
| **intermittent** locally, consistent on CI | flaky test | a race the
slower runner loses every time |
| one approach failed | the site cannot be done | whether a different
*shape* of the approach works |

## Why it matters beyond debugging hygiene

Two of the three would have caused real damage if acted on:

- Quarantining the model-routes test — the action the standing rule
seems to license on "observed failing" — would have **deleted coverage
that is green everywhere else**. The rule's *"without a corresponding
real bug"* clause is load-bearing, and a local observation does not
satisfy it.
- "Unpinnable" hardened a single failed approach into a property of the
site. Left standing, it becomes a permanent excuse not to look — the
same failure I corrected in an inherited note earlier today, which had
recorded four resolvers as unmeasurable for environment reasons that did
not hold here.

Hence the last rule: **record cautions as environment-scoped, not as
properties of the code.** Say where you measured.

```
lint clean; fnxc-future-dates: none added (exit code checked before piping)
```
2026-07-31 14:00:04 -07:00
gsxdsm
24ef266e48 FN-8628: add Factory Dark dashboard theme
Add a low-light industrial dashboard color theme with first-paint support and release documentation.

- Register Factory Dark across persisted theme types, selector metadata, and desktop/dashboard bootstrap validators.
- Define dark and light Factory Dark tokens, swatches, and selector styling.
- Cover theme registration, tokens, bootstrap behavior, and UI theme-option counts.
- Add a minor @runfusion/fusion changeset and document the theme.

Files changed:
 .changeset/fn-8628-factory-dark-theme.md           |   7 ++
 docs/dashboard-guide.md                            |   3 +-
 docs/settings-reference.md                         |   2 +-
 packages/core/src/types/execution-and-ui.ts        |   2 +
 .../app/__tests__/factory-dark-theme.test.ts       | 106 +++++++++++++++++++++
 .../dashboard/app/components/ThemeSelector.css     |  14 +++
 .../components/__tests__/ThemeDropdown.test.tsx    |   2 +-
 .../components/__tests__/ThemeSelector.test.tsx    |   2 +-
 .../__tests__/CommandCenterControls.test.tsx       |   2 +-
 packages/dashboard/app/components/themeOptions.ts  |   1 +
 packages/dashboard/app/index.html                  |   2 +-
 packages/dashboard/app/public/theme-data.css       |  86 ++++++++++++++++-
 packages/desktop/src/renderer/index.html           |   1 +
 13 files changed, 223 insertions(+), 7 deletions(-)

Fusion-Task-Id: FN-8628

Fusion-Task-Lineage: 6f3c7cd9-0130-482d-8aa8-ca47d48b134f

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-31 13:56:55 -07:00
gsxdsm
851369a480 docs(solutions): record "silence is not success" (#3241)
## What

A new `docs/solutions` note recording a failure that hit **three
different tools in one session**, each time reading as a pass. Docs
only.

## The three costumes

| what happened | looked like | was |
|---|---|---|
| `git stash --keep-index` swept the new test file out of the tree | "45
passed" | the pre-existing count; the new test never ran |
| a blinding script hit an unmapped role and `sys.exit(2)` **with no
message**; `&&` skipped the check, `;` let the run proceed | "375/375
green under blinding" | nothing blinded — run was against unmodified
source |
| a gate piped to `tail -1`, printing a blank line | "gate ran, no
complaints" | exit code 1; the FNXC stamp check had failed, and **CI
caught it in #3238** |

## Why it deserves its own note

**A passing run and a run that never happened produce the same evidence:
no failure text.** Every other bug announces itself; this one is defined
by the absence of an announcement. The instinct that catches ordinary
bugs — *"nothing looks wrong"* — is precisely the instinct that
certifies this one.

It gets worse under automation, where output is piped and skimmed. `|
tail -1`, `| grep "Tests"`, `>/dev/null 2>&1` all discard the part that
would have said `No test files found` or `command not found`.

## The five rules, each paid for above

1. **Assert the exit code before any pipe.** A pipeline's status is the
*last* stage's — `cmd | tail -1` reports `tail`'s success, never
`cmd`'s.
2. **Confirm the run did the work.** "Test Files 1 passed" when you
expected 16 is a finding, not a pass.
3. **A tool that can no-op must say what it did** — print the
substitution and location, fail loudly where it cannot act.
4. **Verify the mutation, not the tool's promise** — `git diff --stat`,
not the exit code.
5. **Break the guard on purpose once** and watch it fail. A guard never
observed failing has not been shown to work — the standard this repo
already applies to product ratchets, turned on your own verification.

## The uncomfortable part, kept in

The third instance was a rule **I added to AGENTS.md myself in #3174**,
broken for the second time. I ran the gate. I read `tail -1`. I moved
on.

Writing a rule down does not make you follow it. The only reason it was
caught is that **CI read the output when I did not** — an argument for
the gate existing, not for me having been careful.

Cross-linked from the resolver-audit note, whose every wrong reading
came from a run that never happened rather than from the blinding
itself. That connection is the point: I spent this session auditing a
program whose subject is defects hiding behind green results, and
reproduced the same class three times in my own tooling.

```
lint clean; fnxc-future-dates: none added (exit code checked before piping this time)
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Documentation**
* Added workflow guidance explaining why silent or seemingly successful
output does not confirm that a test, script, or validation gate ran.
* Documented verification practices including checking exit codes, work
counts, no-op detection, post-run changes, and intentional failure
checks.
* Added a case study highlighting how filtered output can conceal
verification failures.
* Added cross-references connecting resolver interpretation, test
execution, and conversion coverage.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 13:47:08 -07:00
gsxdsm
4bcaddafc5 docs: index workflow-owned-lifecycle-closing-verification.md in Audit Reports
Found during routine docs orphan scan. The file (added 2026-07-30,
commit 13bf7e001d) was not linked from the README.md index. It is a
verification runbook and recorded-pass history for the workflow-owned
lifecycle cutover programme.
2026-07-31 13:43:27 -07:00
gsxdsm
05f09c29f8 docs(solutions): complete the repo-wide resolver audit; correct a superseded note (#3236)
## What

Completes the repo-wide resolver audit and **corrects a note of mine
that had gone stale**. Docs only.

Every `resolveProjectColumnsForRoles` call site in the repository has
now been blinded individually.

## Final results

| package | sites | outcome |
|---|---|---|
| `engine` | 10 files | scheduler, triage, evaluator uncovered → pinned;
executor, restart-recovery, notification already covered; self-healing
21 pinned / 1 inert |
| `core` | 14 | 9 covered, **5 uncovered → all 5 pinned** (#3225, #3227,
#3233, #3234, #3235) |
| `dashboard` | 4 | `register-task-workflow-routes.ts:1268` covered;
`server.ts` ×3 flagged |
| `cli` | 1 | flagged |

## The correction

A note recorded `workflow-analytics.ts` and `team-analytics.ts` — 4
resolvers — as **unmeasurable**, because `pgDescribe` probes TCP and the
`.pg` suites skip without it.

The caution is real and stays: a skipped suite reads exactly like a
passing one. But on an environment where those suites **do** run, all 4
were measured, and `team-analytics.ts` turned out to have a half-covered
pair — `completeLanes` covered, **`activeLanes` not** — in a file named
`team-analytics-renamed-lanes`. That is now pinned (#3227, merged).

Left standing, the note converts a real finding into a **permanent
excuse for not looking**. It now says: confirm the suite actually skips
*here* before recording a site as unmeasurable for environment reasons.

## A fourth measurement failure mode — the opposite direction

The three already recorded all produce false *uncovered*. This one
produces false *covered*:

**A COVERED verdict needs a baseline.** The dashboard sweep reported 5
failing files under the global blind. **4 of them fail on clean `main`**
and have nothing to do with lanes — a docs-inventory test and a
model-routes test among them. Read as-is, that is four resolvers falsely
credited as covered. Only
`register-task-workflow-routes.awaiting-planning.test.ts` passes clean
and fails blinded, so it is the sole real detector.

Second time today a baseline changed a conclusion (the first found a
genuine red on main, #3229).

## Why 4 sites are flagged rather than pinned

- **`server.ts:1922/1923/1938`** — inside the `/api/health/reliability`
route closure. No route-level test exists, and the only way in is
booting `createServer(store)` behind a mock-the-world shell, which the
slow-test rule forbids. The alternative is a refactor to expose a seam —
its own commit, since moving code and changing behaviour do not ride
together. (A note already in this doc reached the same conclusion
independently; this confirms it by measurement.)
- **`cli/commands/task.ts:660`** — worth its own warning. Extracting a
pure helper and testing it **would look like coverage and would not
be**: blinding the resolver leaves such a test green, because the helper
*receives* the lane set rather than resolving it. The uncovered thing is
the resolve call, not the decision it feeds. Its sibling test file
already records the same limit honestly for `boardColumnsForDisplay`.

## Reported, not fixed: 4 pre-existing red dashboard files on main

`lazy-loaded-views-docs.test.ts` (AGENTS lazy-view inventory drifted —
24 actual vs 18 documented), `ResearchView.test.tsx`,
`planning-browser-e2e.test.ts`,
`register-model-routes-kimi-k3-supplemental.test.ts` — 7 failing tests,
all in the non-blocking suite.

I am not fixing them here: the lazy-views inventory is a curated list
other workers are actively adding to, and rewriting it mid-flight would
collide. Flagging so it is visible rather than silently absorbed into my
blind's noise.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Documentation**
* Updated workflow guidance to require baseline comparisons and
verification that all relevant tests run.
* Added safeguards for detecting ineffective changes and distinguishing
pre-existing failures.
* Expanded PostgreSQL audit documentation with measured coverage
results, including uncovered resolver paths.
* Recorded completed coverage sweeps across core, dashboard, and CLI
areas, including pinned and non-pinnable sites.
* Clarified limitations when testing extracted decision helpers instead
of resolver calls.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 13:23:31 -07:00
gsxdsm
01ab2400d0 docs(learnings): blinding measures the instrument you picked — rule 5, and where the measurement cannot be taken (#3222)
Extends `blind-the-resolver-to-find-uncovered-conversions.md` rather
than forking a second doc on the same technique.

## Rule 5: blinding measures the instrument you picked, not the site

A suite that never reaches the blinded site reports `0 failed` for the
same reason a covered one does. The outputs are identical. This produced
a **wrong answer twice in one sweep**, both times reading as a finding:

| blinded | suite run | said | actually |
|---|---|---|---|
| `reads.ts` ×3 | `search-excludes-renamed-archive-lane.test.ts` | 3
uncovered | that file unit-tests `liveSearchPredicate` and never runs
`reads.ts`; against `cold-storage-renamed-archive-lane.test.ts` one of
the three is covered |
| `server.ts` ×3 | `reliability-metrics.test.ts` | 3 uncovered | that
file imports `../reliability-metrics`; nothing executes the route at all
|

The `reads.ts` case is the one to remember, because **the misleading
suite was written for that exact conversion**. It proves the
collaborator honours a resolved set — which says nothing about whether
the caller passes one, and can never fail when the call site is blinded.
That gap shipped as a real hole and was closed in #3220.

Doc adds the cheap guard: make the blinded edit obviously fatal (`throw
new Error("x")`) and re-run. Still green means the suite does not reach
the site and the measurement is void.

## Where the measurement cannot be taken

Per #3212's stance that recording *why* something cannot be pinned is a
result, three groups are written down so nobody re-derives them:

- **No TCP PostgreSQL** — `workflow-analytics.ts` / `team-analytics.ts`
(4 resolvers) keep renamed-lane coverage in `.pg` suites. `pgDescribe`
probes **TCP**; `pg_isready` succeeding on a **Unix socket** is not the
same thing. I made exactly this mistake and reported PG as reachable one
round before correcting it — mistaking the two turns 4 skipped suites
into 4 false "uncovered" readings.
- **No injectable seam** — `reads.ts`'s incremental-sync scan composes
Drizzle conditions against `layer.db`. A test there asserts the query
built, not the rows excluded: green, and blind to the bug.
- **Logic inside a route closure** — `server.ts`'s three resolvers sit
in the `/api/health/reliability` handler, which has no route-level test.
The only harness in that package is a mock-the-world shell the slow-test
rule forbids; the alternative is a refactor to expose a seam, which is
its own commit.

## Census

**Unchanged — `CONVERSION QUEUE EMPTY`, `AVAILABLE: 0`.** Documentation
only.

Gates verified green (`check-fnxc-future-dates`,
`lifecycle-column-census --strict`).

No changeset: internal docs, per AGENTS.md.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Documentation**
* Added guidance for verifying the test instrument used during blinding.
  * Documented fatal-edit reachability checks.
* Added troubleshooting guidance for situations where resolver coverage
cannot be measured.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 12:28:20 -07:00
gsxdsm
206ff11874 docs(solutions): record the blinding audit's own failure modes (#3223)
## What

Extends
`docs/solutions/workflow-learnings/blind-the-resolver-to-find-uncovered-conversions.md`
with what this session's audit work paid for. Docs only — no code, no
changeset (internal doc).

## The main addition: the audit's own failure modes

**Every wrong reading this method has produced came from test
*selection*, not from the blind.** Three in one session, each of which
reads exactly like coverage:

| what I ran | why it lied |
|---|---|
| `vitest run src/__tests__ -t "executor"` | `-t` filters test
**names**, not files. Reported two `executor.ts` resolvers uncovered;
**both are covered.** |
| `blind3.py <file> <var>` with an unmapped role | exited non-zero
**silently**; `&&` skipped the check and `;` let vitest run against
**unmodified source**. Reported "375/375 green under blinding" with
nothing blinded. |
| `vitest run src/__tests__/notification` | missed
`src/notification/__tests__/` — a nested `__tests__` the glob never
reached. Reported covered code as uncovered. |

The rule that follows: an UNCOVERED verdict is a claim about the whole
tree and needs the whole tree's tests. Confirm the blind actually
modified the file with `git diff --stat` — *not* the tool's exit code —
and that the run included every file importing the module.

I am documenting my own instrument failing the standard I have been
applying to product guards all phase: *a guard that reports success
without checking anything is worse than no guard.* Mine reported success
without checking anything. It now echoes what it substituted and where,
and fails loudly on an unmapped role or missing variable; I self-tested
both directions before trusting any number in #3219 and #3221.

## Rule 5: the resolver must be able to answer differently in the
harness

`resolveProjectColumnsForRoles` returns **legacy ids and nothing else**
when the store has no `listWorkflowDefinitions` — an intentional degrade
so an unreadable workflow list cannot fail a sweep. A harness omitting
it makes the resolved set and the literal set **equal by construction**,
so the conversion is unobservable however good the assertion is.

This is not a test bug. It is correct production behaviour that erases
the difference the test is trying to measure — and it alone left both
the `scheduler.ts` and `triage.ts` conversions unpinnable.

## A correction to my own earlier rule

I had "seed-then-union sites hide defects" too broad. Such a site hides
a defect **only while every lane you assert on is already in the seed**.
On a renamed board the resolver is the sole contributor of the renamed
lane, so the legacy blind is *not* a no-op — I predicted it would be and
it failed. Also: expand roles to legacy ids **per role** from
`LEGACY_COLUMN_IDS_BY_ROLE`; `intake` is `["todo","triage"]`, not
`["triage"]`, and a stricter-than-real blind manufactures failures that
read as coverage.

## Inventory, so the gap is legible

**116 non-test call sites across 30 files** — core 17, engine 10,
dashboard 2, cli 1. Audited so far, all in engine: `self-healing.ts` (64
mapped / 21 pinned / 1 inert by construction), `executor.ts` (2,
covered), `scheduler.ts` (uncovered → pinned in #3219), `triage.ts`
(uncovered → pinned in #3221), `restart-recovery-coordinator.ts`
(covered), `notification-service.ts` (covered).

**`packages/core`'s 17 files are entirely unaudited.** Stated as a gap
rather than left implied, so nobody reads engine's coverage as a
repo-wide clean bill.

## Flagged, not guessed

- `evaluator.ts`'s archived read is uncovered — **no test file imports
that module at all.** Left unpinned deliberately: it is a thin
pass-through into `collectDeterministicSignals`, which is testable
directly, and it affects eval signal quality rather than task lifecycle.
Recorded in the doc rather than silently skipped.
- I did not audit core; it is outside my package and I am not claiming
anything about it either way.
2026-07-31 12:08:59 -07:00
gsxdsm
2868eb4797 docs(learnings): blind the resolver to find uncovered conversions (#3214)
Sibling to #3203 (`a-falling-count-is-not-evidence`), which records that
a metric moving is not proof the system moved. **This is the positive
procedure**: how to find out whether a landed conversion is held by
anything, and how to write a test that holds it.

## The measurement it is written from

Of **64 resolved lane sets in `self-healing.ts`, 26 had no test that
could distinguish them from the literal they replaced** — including
three conversions I shipped that same day, and two halves of sweeps I
had already recorded as covered.

## The procedure

```
- const reviewColumns = await resolveProjectColumnsForRoles(this.store, REVIEW_ROLES);
+ const reviewColumns = new Set<string>(["in-review"]);
```

Suite fails → covered. Suite passes → nothing in the tree can tell the
conversion from the literal. One resolver, one 17-second run — cheaper
than writing the conversion was.

## Why the census cannot answer this

| instrument | question |
|---|---|
| census / lane-wiring ratchet | is this site written in the resolved
vocabulary? |
| blinding | does anything break if it stops being? |

Neither substitutes for the other. A conversion merged with 204 green
tests behind it and zero able to see it.

## Four rules, each paid for by a test that proved nothing

1. **Blind each resolver separately** — coverage is per-resolver, not
per-sweep. Twice a sweep recorded as done was half-done, because control
flow short-circuited before the second guard.
2. **The fixture must reach the branch the resolver gates.** A card in a
renamed *wip* lane cannot exercise a *terminal* skip — it is caught by
the wip∪review set first.
3. **Assert a path-specific side effect, never a return value.**
`outcome === "reclaimed"` is reachable without the guarded branch.
4. **A store fake must honour `options.column`.** Flat and call-order
stubs answer identically whatever column is requested — a fake that
ignores its own filter cannot see a filter bug.

## The two shapes a ratchet cannot distinguish

- **resolved gate, literal branch** — reads as *unwired*, was a live
defect (#3208: a working agent lost its task link)
- **passed-but-unread** — reads as *wired*, is dead code (#3212)

A ratchet counting call sites scores the first as debt and the second as
done. Both wrong.

## Why a doc and not more PR comments

Everything above currently lives in ~20 PR descriptions. The next person
to touch a lane conversion will not read those. `docs/solutions/` is
where this project already keeps the things it learned the expensive
way, and the frontmatter (`applies_when: deciding whether a lane
conversion is actually protected by a test`) is what makes it findable.

## Verification

`pnpm test:gate` 13 + 161 + 499 + 71 · lint · fnxc-dates (TZ=UTC) ·
`self-healing-docs` 2 passed. Docs only; no changeset, per the AGENTS.md
rule for internal docs.
2026-07-31 11:18:23 -07:00
gsxdsm
e9a57ca8ba docs(learnings): a falling count is not evidence that anything changed (#3203)
## The metric counterpart to #3200

#3200 (merged) records the **shapes** an inert conversion takes, and its
grammatical tell is the portable one: *if a claim can be written without
running anything, it has not been tested.* I offered this material there
and said I would write it as a sibling rather than bloat that doc; it
merged without it, so here it is.

That doc is about **claims**. This one is about **numbers**.

## The tell

**A count that falls is not evidence that anything changed.** Every gate
here reports a number, and a number goes down three ways — work
happened, the code got denser and the scan stopped matching, or someone
lowered the allowance. Only the first is progress, and from inside the
check all three look identical.

Four times in one phase:

| what moved | what actually happened |
|---|---|
| census 12 → 2 (`scheduler.ts`, #3051) | ten guards routed through
`resolveTaskWorkflowIrSync`, which answers with the DEFAULT board under
PostgreSQL. Byte-identical. Refuted in #3058 |
| census 45 → 44 (`triage.ts`, #3114) | converted the exact arm #3108
flagged hours earlier. #3126 reverted it — three PRs for one line |
| ratchet 20 → 15 | not a conversion: #3065 rewrote `a === x \|\| a ===
y` as `set.has(a)`. It printed *"total fell — re-record"*, which would
have **permanently retired live guards** |
| ratchet 22 → 9 | `scheduler.ts` reported **0** while 13 guards still
fell back to the default board |

Rows three and four are the dangerous shape: **the gate went quiet
exactly when someone improved the code**, and the remedy it suggested
was to lower the allowance.

## Also recorded

- **One defect, four spellings** (#3062, #3068, #3079, #3181) — each fix
correct about the shape in front of it and blind to a respelling. The
lesson is not "write a better regex": enumerating consuming syntax is a
losing game, and the durable form keys on the *source*.
- **An instrument that runs nowhere and one that cannot fail are the
same defect.** `check:inert-sync-lanes` was invoked by nothing for six
PRs; `check:quarantine-ledger` ran nowhere *and* omitted `--strict`, so
wiring it alone would have been theatre. Includes the mechanical audit
that finds both.
- **Base drift makes branch numbers incomparable** — three false alarms,
one of them mine, from comparing against a remembered figure. The
procedure that works is extracting both scripts and running them against
one tree; that is how #3169 and #3181 were shown additive (22 = 13 + 7 +
2), which decided merge order and collapsed one into six lines inside
the other.
- **A pick-work list at 100% false positives**, because under-reporting
deferrals is the direction that manufactures the #3108 → #3114
collision.

## Every claim is a measurement

No mechanism here is derived from reading. Nine PRs cited, each the one
that produced or refuted the finding — including the ones where I was
wrong: a stale number I mistook for a regression, and two future-dated
stamps of my own that the full ratchet set caught before they shipped
(one earlier one it did not, and that broke `main`).

## Census before / after

```
before:  COLUMN guards (the backlog):   12
after:   COLUMN guards (the backlog):   12
```

Docs only.

## Verification

`test:gate` exit 0 · `fnxc-future-dates`, `lifecycle-columns`,
`inert-sync-lanes`, `quarantine-ledger`, `inert-flag-seams`,
`lane-wiring`, `sql-column-literals` — all exit 0 · `pnpm lint` clean.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Documentation**
* Added guidance for interpreting workflow metrics and avoiding
misleading conclusions from declining counts.
* Documented detection blind spots, branch comparison issues, false
positives, and validation procedures.
* Included a practical checklist for reviewing metrics, quality gates,
comparisons, and potential conversions.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 10:44:21 -07:00
gsxdsm
5f97fbcb06 docs(learnings): a blocker described four times, wrong twice — instrument before you file (#3200)
Records the method that moved a `triage.ts` site flagged unconvertible
for four cycles. The method transfers; the three conversions do not.

## Four mechanisms, split by derivation rather than care

| # | claimed mechanism | derived from | held? |
|---|---|---|---|
| 1 | merged intake/hold vocabularies | reading | no |
| 2 | orphan arm scoped to `source === "selection"` | reading + one test
run | partly |
| 3 | provenance verifies by `ir.id`, which builtins lack | reading a
**comment** | **no — filed as #3187, closed as wrong** |
| 4 | two test harnesses cannot answer a selection query | instrumented
isolation | **yes** |

(3) is the expensive one. The text I quoted was **historical prose
describing code that had been removed**, sitting directly above a
paragraph saying exactly that. I read a rationale as an implementation,
and it reached an issue other lanes could have acted on.

## The isolation took three runs

```
flag only, no conversion             8 passed   -> the orphan arm is not the cause
flag + conversion                    5 failed   -> the conversion is
same, with a realistic mock store    8 passed   -> the mock was the cause
```

Change one variable, let the suite answer. Available from cycle one.

## Why this is not just "test more"

Every wrong mechanism was plausible, specific, and consistent with the
code as read. **Plausibility is what made them dangerous** — each was
good enough to write down, publish and act on. The failure mode is not
sloppiness; it is that a careful reading of a large file *feels* like
evidence.

The tell is grammatical: **a claim that can be written without running
anything is a hypothesis, not a measurement.** "This cannot be converted
because X" versus "reverting X fails these 3 of 8 cases."

## The corollary, including its negative result

Once the harness was the suspect, a class fell out: a test that stubs a
reader **broken in production** proves the call site's logic while
unable to see that production resolves nothing. Eight files stubbed
`resolveTaskWorkflowIrSync` — one masking a live defect, four redundant
(#3198), one legitimate.

The doc also records that the obvious generalisation **fails**:
`getTaskWorkflowSelection` is equally degraded under PostgreSQL but
stubbing it masks nothing, because the resolver prefers the async twin
and both answer the same. The distinguishing property is that the reader
returns something *incorrect*, not merely *unused*. Written down so
nobody repeats the 120-file sweep.

Docs only; `check-fnxc-future-dates` exit 0.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Documentation**
  * Added a case study for diagnosing an unconvertible workflow site.
* Documented controlled-run findings identifying the realistic mock
store as the cause.
* Clarified the difference between reading-based hypotheses and
instrumented evidence.
* Added guidance for distinguishing conversion, orphan-arm, and
mock-store issues.
* Recorded an audit of related test stubs, including redundant, masking,
legitimate, and unresolved cases.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 10:07:07 -07:00
gsxdsm
984b3ed0c1 docs(learnings): a seventh inert shape — reusing an already-resolved local (my #3114 turned main red) (#3130)
Records the failure shape behind #3126, from the person who caused it.

## What is new about it

The three inert conversions this program had catalogued — #3051, #3062,
#3068 — all called `resolveTaskWorkflowIrSync` **at the call site**,
where the sync resolver is visible in the diff. That is what the
existing entries describe, and it is why they read as avoidable.

Mine was not that. #3114 converted a `triage.ts` arm to
`disposeLanes.wip`, reusing a value `resolvePlannerLanes` had produced a
few lines above. It merged, and `main` went red: `triage.ts: 7 -> 8`.

My reasoning at the time, verbatim from the PR body:

> `resolvePlannerLanes` already called immediately above — no new
resolution/await.

That sentence checks the **cost** question and skips the **correctness**
one. I confirmed I was not adding an `await` to a synchronous listener —
the usual blocker, and a real one — and never asked what kind of
resolver had produced the local I was reusing.

**Reusing an already-resolved value reads as strictly safer than
resolving.** No new work, no new await, no new failure mode. That
intuition is correct about cost and silent about correctness, and the
sync-ness sits one hop away inside the helper, where a call-site
reviewer does not see it.

So the check is not *"am I calling a sync resolver here?"* but **"what
produced every lane value I am about to compare against,
transitively?"** A local is not evidence; the resolver behind it is.
#3122 widened the gate to follow wrappers for precisely this reason —
and I walked through the door it was widened to cover, during the same
phase I was adding it.

## Two corollaries recorded with it

1. **A gate that catches the defect but does not block is a report.**
`check-inert-sync-lanes` fired correctly and the PR merged anyway,
because it is not in the blocking set. #3127 fixes that, and I would
prioritise it over any individual conversion — this is the second time
this phase a correct non-blocking signal was ignored.
2. **`triage.ts`'s remaining 7 are not backlog.** The revert takes it to
7, and those seven are the same shape: they need the emitter-side /
async-threading work tracked in #3082, not another conversion pass.
`--claims` (#3124) now marks sync-resolver files as inert-risk and keeps
them out of the start-here list for exactly this reason.

## Scope

Docs only — one section appended to the existing learnings file, placed
with the other numbered shapes and before "The rule that produced every
fix above". No code, no gate, no changeset (internal docs).

The revert itself is #3126, which I confirmed on a clean detached
`origin/main` checkout rather than on a branch; I did not open a
competing fix.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 05:38:20 -07:00
gsxdsm
20e3731eb1 docs(workflow-learnings): a deferral's stated blocker is a claim, and it decays like a measurement (#3026)
Two pieces of work were filed rather than fixed in one session, each
with a specific technical reason. **Both reasons were wrong**, and in
both cases the real obstacle was smaller than the stated one.

| filed rationale | reality |
|---|---|
| "the plugin has no scaffolding for faking its stores" (#3020) |
`_harness.ts` builds a real `PluginContext` over a live PostgreSQL
layer; the gap was **two missing readers on a stub** — fixed in #3022 |
| "supplying this needs a published-API change" (#3003) | the type is
dashboard-internal, `@fusion/plugin-sdk` is `private: true`; the actual
obstacle is stale type declarations between two in-repo packages |

The first one matters most: the filed issue was a **pipeline that stalls
forever** on a renamed board. The cost of that excuse would have been a
real stall sitting open behind a plausible-sounding note.

## The shape

Both times the blocker was asserted **from the shape of the problem**
rather than tested. *"This needs infrastructure that doesn't exist"* and
*"this crosses a published boundary"* are each checkable in about five
minutes, and neither was checked before I wrote a paragraph explaining
why the work couldn't proceed.

## Why it's worth writing down

Filing is often right — someone else owns the contract, the fix needs a
decision, the data genuinely isn't there. What makes it wrong is filing
on an **untested** blocker, because a filed issue with a confident
rationale is the one thing nobody re-derives. It reads as settled.

That's the same mechanism as a stale "do not re-probe" note (which this
document already records, and which I had to correct in #3018), one
level up: there a *measurement* went stale, here a *decision* did.

## The rule

**Before writing the blocker down, spend five minutes trying to hit
it.** If it's real you'll hit it immediately and can describe it
precisely — which makes the issue more useful. If it isn't, you have the
fix instead of the issue.

Docs only. No code, no baselines.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 01:17:19 -07:00
gsxdsm
eecc87c31e docs(workflow-learnings): the "named legacy-id collections are clean" entry was wrong (#3018)
It hid two real defects — and it explicitly told the next reader not to
re-probe them.

## What the entry did

Counted **declarations** (48, then 49) and concluded the population was
benign because each one is a fallback vocabulary, a builtin column list,
or an already-converted seam.

All true of the declarations. **The declaration isn't where the defect
lives.**

## Measure the use, not the declaration

A collection used as a **membership gate against a column**. Nine exist,
and two were live user-visible defects sitting inside a population this
doc had marked clean:

| site | defect |
|---|---|
| `TIME_INDICATOR_COLUMNS.has(task.column)` — `TaskCard` | elapsed-time
indicator never rendered on a renamed board (#3014) |
| `PLANNER_ACTIVITY_COLUMN_IDS.has(task.column)` — `useTasks` | planning
border and pulsing badge never appeared (#3017) |

The other seven are genuinely fine, and the reasons are kept because
they're the shapes worth recognising: the no-flags fallback *inside* a
role helper, a seam that seeds the legacy pair then unions resolved
lanes, a marked `DELIBERATE-LITERAL` fallback chain, and a plugin with
no trait source at all.

## The tell

One question separates the two groups: **does a flags path exist in this
file at all?**

Both defects had none — the gate was the only decision, with nothing to
degrade from. Every benign case had a resolved path sitting right next
to the literal.

## Why this is worth its own PR

A "do not re-probe" note that is wrong is **worse than no note**: it
converts one person's incomplete measurement into everybody's blind
spot. That's the same failure this document already records for
`sortTasksForDisplayColumn`, one level up — there an annotation told
readers to skip a *row*, here it told them to skip a *population*.

I wrote the original entry, and I'd read past it twice myself before
#3014 forced the re-measurement.

Docs only. No code, no baselines.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 00:49:56 -07:00
gsxdsm
e9f587c363 docs(workflow-learnings): correct the "bounded" heuristic — a clock-shaped dep is not a fast one (#3012)
The severity heuristic I wrote in #2998 sorted dependencies **by name**,
and #3007 is the counterexample.

## What I got wrong

I classified `lifecycleDates` as *bounded* because its dep list contains
`lifecycleNowMs`, and deferred it in #3001 with the line *"any wrong
answer there survives only until the next update."*

That value is driven by a **local-midnight boundary timer** — one tick
per card per day. So a finished card shows no completion date for up to
**twenty-four hours**. @gsxdsm found it after I'd written it off.

`nowMs`, `Ticker` and `lastFetchTimeMs` span a live 30-second ticker, a
per-fetch stamp, and a daily boundary. Sorting them by name puts a
day-long defect in the same bucket as a 30-second one.

## The sharper half

A card in a **completion lane doesn't subscribe to the shared live
ticker at all** — that's exactly what the ticker's eligibility check is
for, and what #2996 fixed. So the "fast" dependency that would have
rescued this population is the one thing that population never receives.

The corrected question is: **which dependencies refresh *for this
population*** — not which ones appear in the list. Two of my three
severity calls in that sweep leaned on a dep that the affected cards
structurally never get.

## Why this is worth a PR rather than a quiet edit

The doc is what the next person triages against. #3001 explicitly told
them the four "bounded" sites were deprioritised **by design** — on
reasoning that was wrong for at least one of them. Leaving that in place
means someone defers a day-long defect on my say-so.

Docs only. No code, no baselines.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 00:33:25 -07:00
gsxdsm
e78bf20d55 docs(workflow-learnings): a sixth shape — the resolved value arrives after a memo has answered (#2998)
## The shape

Three defects this session, all the same, none visible to any instrument
here:

A lane value resolved **asynchronously** (the board fetches workflow
traits after first paint) is read inside a `useMemo`/`useCallback` whose
dependency list omits it. The first computation runs with the flags
`undefined`, the role helpers correctly fall back to legacy ids, and on
a **renamed** board that answer is wrong. When the flags arrive nothing
in the dep list changed, so the memo never recomputes.

| defect | severity |
|---|---|
| blocker fan-out trait index (#2993) | permanent — empty index for the
mount |
| card live elapsed-time indicator (#2996) | permanent — never
subscribes |
| near-duplicate chip (#2997) | bounded — self-heals on the next task
refresh |

A legacy board hides all three: there the fallback already answers
correctly on the first paint, so the stale list costs nothing. **Every
instance is renamed-board-only**, which is why they accumulated — and
this repo has no `react-hooks/exhaustive-deps` rule, so the class is
invisible to lint.

## Two properties decide severity, both readable off the dep list

1. **Does any dependency refresh quickly?** `allTasks`, a live clock, a
task identity — any of them rebuilds the closure on the next update,
making the wrong answer a bounded window. The chip keys on `allTasks`
and recovers; the indicator keys on `task.column`, which never changes,
so it never does.
2. **Is the value covered transitively?** A dependency that itself lists
the flags gets a new identity when they arrive, and that propagates.

## A gate was built and rejected — the part worth writing down

The scanner reports **19 sites; two were real.** Property 2 is why:
transitive coverage is invisible to any purely syntactic check and would
need a real dependency graph.

`TaskCard`'s context-menu memo omits all three role flags and is
**nonetheless correct** — it depends on `taskActionMenuModel.actions`,
and that model lists `taskColumnFlags`, so the whole chain recomputes. I
checked that before filing it, which is the only reason this PR isn't a
bug report about missing Archive/Revert menu entries.

Freezing 19 would have baselined mostly noise and trained everyone to
skip the report — the exact failure this document already records for
`sortTasksForDisplayColumn`, where an annotation saying "ignore these"
hid a real defect for days. **A good investigative tool is not
automatically a good ratchet**, and the next person deserves to know the
turn was considered rather than missed.

The triage that does work is cheap: run the scan, then ask the two
questions above. Nine of nineteen survive question 1; hand-checking
those is an afternoon, not a project.

Docs only — no code, no baselines.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 00:00:06 -07:00
gsxdsm
6a465e1006 docs(workflow-learnings): probe harnesses lie more often than the gates do (#2983)
## What

Probing four gates with unimagined shapes this session (#2979, #2980,
#2981) produced **two rounds of silently invalid results** — both from
the harness rather than the instrument, and both agreeing with what I
expected, which is why neither was noticed on the spot.

1. **`node gate.mjs | tail` then `echo $?` reads *tail's* exit status.**
Every probe reported "caught". The gate was in fact failing on `main`
for an unrelated reason, so the runs proved nothing. That fictional
evidence nearly shipped a double-counting change to the SQL gate.
2. **A gate that lists files with `git ls-files` cannot see an untracked
probe file.** Six census probes reported "missed" — including the shape
the census is explicitly built for, which was the tell.
Filesystem-walking gates (`check-sql-column-literals`,
`check-inert-flag-seams`) see untracked files; the census does not.

The rule that catches both in one step, now written down:

> **A probe run needs its own control.** Include one shape the
instrument is known to catch and one it must not flag. If the known-good
shape doesn't come back caught, stop — you're measuring your harness.

Worth stating plainly because the two failure modes have opposite costs:
a probe that wrongly reports *caught* retires a real hole; one that
wrongly reports *missed* sends you rewriting an instrument that was
already correct.

## Two measured negative results, recorded so nobody re-runs them

Added to the existing "Surfaces that were checked and are CLEAN"
section:

| shape | population |
|---|---|
| `switch (task.column)` with legacy `case` labels | **0 sites** |
| a legacy id hoisted into a single const, then compared | **1 site —
and it is correct code** |

The one site is `self-healing.ts:2992`, which seeds `let holdColumn =
"todo"` as its documented legacy floor and then overwrites it from
`resolveLifecycleColumns(...).hold`. The census is right not to flag it;
a naive version of this probe reports it as a defect.

The second shape was worth measuring precisely because **the same shape
had a real population in SQL** — it's what #2980 fixed. It did not
transfer. Population is a property of how people write that particular
kind of code, so each instrument has to be measured on its own rather
than by analogy to a sibling that just turned something up.

## Why this is docs and not a gate change

The census's comparison-only scope is adequate for this codebase: every
blind shape I could construct has an effectively empty real population.
Demanding new detection would have forced a large baseline change across
the program's central instrument for **zero defects** — the same mistake
as filing "48 uncounted sites" that the existing section already warns
about.

Docs only. No code, no baselines touched. All five gates green.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 23:16:26 -07:00
gsxdsm
4f929acc10 fix(dashboard): stop over-aggressive component unmounts (keep-alive for planning, terminals, popups) (#2420)
Implements
docs/plans/2026-07-22-001-fix-dashboard-remount-churn-plan.md: every
confirmed source of unnecessary unmount/remount churn in the dashboard,
plus a keep-alive layer for conversation- and terminal-bearing surfaces.

## What changed

**Keying / component identity (U1–U3)**
- Streaming chat segment key no longer embeds `entries.length` — an
expanded thinking block stays expanded while entries stream into it
(R1).
- Dock task list keys `TaskCard` rows by `task.id` (occurrence suffix
only for the duplicate-id anomaly) instead of `id-index` — no remount on
reorder/filter/status change (R2).
- `ProviderStatusBadge` / `GitHubStatusBadge` hoisted out of
ModelOnboardingModal's render body (R3); MCP server rows key by
`server.name` alone (R4).

**Keep-alive layer (U4–U6)**
- New shared `KeepAliveView` wrapper: visible = in-flow flex child;
hidden = out-of-flow `position:absolute; inset:0` with
`visibility:hidden; pointer-events:none` + `aria-hidden` (never
`display:none`, so xterm geometry never collapses).
- Planning Mode renders as a kept-alive sibling of the MainContent
switch after first open (per-project latch mirroring Quick Chat). While
hidden, the session-list SSE, recovery poll, and elapsed ticker suspend
via a new `active` prop; reveal re-subscribes and refreshes the sessions
list once. Payload-carrying entry points (initial-plan handoff, resume)
and project switches remount via a new
`modalManager.planningEntryGeneration` key, preserving pre-keep-alive
fresh-open semantics. `recordResumeEvent` instrumentation records
`remount` on first activation and `route-active` on reveal.
- Task-detail Terminal / Worktree-terminal / Planner-chat tabs stay
mounted-but-hidden after first open (per-task latches; task switch/close
still disposes fully). `SessionTerminal` gains `active`: reveal refits +
forces a font remeasure, and if the WS died while hidden it re-runs the
full attach lifecycle (dead-socket recovery).
- Popped-out task windows hide via FloatingWindow `hidden` instead of
leaving the render array; `TaskDetailContent` gains `active` so hidden
popups close their SSE/EventSource channels while the terminal WS stays
open. `visiblePoppedOutTaskEntries` remains the Escape-shortcut
consumer.

**Planning Mode internal-transition audit (U7)**
- Audit findings: session-list mode and mobile list/detail flips are
CSS-class transitions over one always-mounted detail pane (no
state-discarding unmounts); re-selecting the active session is an
early-return visibility restore; session switching intentionally reloads
from the session row (stream re-attach for generating sessions);
remaining index keys are on stateless lists. No product-code defects
found; regression tests now lock the always-mounted invariant on desktop
+ mobile.

**Cheap-view state (U8)**
- CommandCenter (active sub-tab + date range) and DevServerView
(selected script/task + typed-but-unsent command) persist per project
via `modalPersistence` and restore after their (intentional) unmount
round-trips. Also fixed the candidate auto-fill effect clobbering a
customized non-empty command.

## Symptom Verification
- **Original symptom:** streaming thinking blocks collapsed mid-stream;
terminals reconnected and lost scroll/input on tab flips; Planning Mode
lost in-flight interviews on navigation; popped-out windows vanished
off-view; dock cards remounted on reorder.
- **Exact reproduction:** (1) expand a thinking block during a stream;
(2) run a command in the Terminal tab, flip to Plan and back; (3) start
a planning interview, navigate Board and back; (4) pop out a task with
board/list-only scoping and switch views; (5) change a dock task's
status.
- **Assertion it is gone:** component-identity/instrumentation tests in
TaskChatTab, SessionTerminal, TaskDetailModal
(worktree/planner-chat/tabs), PlanningModeModal keep-alive +
internal-transitions, App keep-alive round-trip, and
App.taskPopupViewGating assert no remount and preserved state for each
repro, across desktop and mobile breakpoints.

## Verification
- File-scoped vitest: 23 files / 1091 tests green (all touched suites
plus FloatingWindow, TerminalModal, TaskPlannerChatTab,
lazy-loaded-views guard, App suites).
- `pnpm verify:fast`: PASS (13 steps — scoped typecheck/build, CLI
build, boot smoke).
- `pnpm check:changesets`: passes; changeset
`fix-dashboard-remount-churn` (`@runfusion/fusion` patch, labeled
format).
- Known pre-existing failures NOT caused by this branch (verified
failing at base a224c1111 in a clean worktree): 7 tests in
`TaskDetailModal.oversight-controls/oversight-mobile/models-progress-workflow`.
- jsdom cannot prove rendered-grid correctness for xterm reveal; per the
plan's risk note, manual browser verification of terminal reveal remains
recommended.

🤖 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**
* Switching views or tabs no longer resets Planning Mode, task details,
terminals, planner chats, or popped-out task windows.
  * Streaming content remains expanded and stable as new entries arrive.
* Hidden views suspend background activity and resume correctly when
shown.
  * Terminal sessions reconnect automatically when needed.

* **Improvements**
  * Command Center and Dev Server selections persist per project.
  * Custom Dev Server commands are preserved while browsing suggestions.
* Improved stability when reordering task lists and updating server
states.

* **Documentation**
* Updated dashboard guidance for hidden, retained task pop-ups and view
transitions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 21:50:47 -07:00
gsxdsm
fd795883c5 feat(missions): per-mission taskPrefix override for triaged task ids (#2347)
## Summary
Maintainer re-land of
[#2334](https://github.com/Runfusion/Fusion/pull/2334) (fork
`flexi767:feat/per-mission-task-prefix`) after resolving merge conflicts
with current `main`.

Fork push was unavailable despite `maintainerCanModify`, so this branch
carries the conflict resolution.

### Feature
- Optional per-mission `taskPrefix` for triaged task ids (inherits
project prefix when unset)
- Dashboard MissionManager + routes + store/triage plumbing
- Postgres migration for `project.missions.task_prefix`

### Conflict resolution
- Main claimed migration **0026** (bigint counters) and **0027**
(workflow IR pin)
- Mission task-prefix migration renumbered **0026 → 0028**
- Baseline `0000_initial.sql` includes `task_prefix` on missions
- `legacy.ts` keeps code-org re-exports; `missions.ts` carries
`taskPrefix` on create/update types

## Test plan
- [ ] CI green (lint/typecheck/build/gate)
- [ ] Create mission with custom prefix; triage feature → task ids use
that prefix
- [ ] Clear mission prefix via PATCH null; new tasks inherit project
prefix

Closes / supersedes #2334 once this lands (or re-point the fork PR).

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Missions can now set an optional per-mission task ID prefix
(overriding the project default).
* Added task prefix support to mission create/edit UI and dashboard
APIs, including normalized uppercase values and validation.
* **Bug Fixes**
* Improved commit hook generation for custom prefixes and special
characters, with safer shell handling to prevent unsafe interpretation.
* **Chores**
* Added PostgreSQL migration and schema-applier support to persist and
propagate mission task prefixes, including upgrade/backfill coverage.
* **Tests**
* Added backend and UI/API test coverage for task-prefix creation,
clearing, and ID minting behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-30 21:35:23 -07:00
gsxdsm
b1bd571682 batch-sql-ratchet: the census / gate-ratchet family — collection branch, fold here (#2941)
## Family branch for consolidation directive item 4

`batch-sql-ratchet` did not exist and ~10 open PRs are waiting for a
collection point, so this establishes it. **Fold your census/ratchet
commit here and close your own PR as superseded.**

```bash
git fetch origin batch-sql-ratchet
git checkout -B batch-sql-ratchet origin/batch-sql-ratchet
git cherry-pick <your-sha>
# verify scoped, not full suite:
pnpm --filter @fusion/core exec vitest run src/__tests__/archived-column-gate-parity.test.ts --silent=passed-only --reporter=dot
git push origin HEAD:batch-sql-ratchet
```

**Candidates I can see open right now** (owners: please fold + close):

| PR | branch |
|---|---|
| #2938 | `fix/comments-ops-sentinel` |
| #2935 | `fix/task-artifacts-sentinels` |
| #2933 | `chore/commit-tightened-census-baseline` |
| #2931 | `fix/async-comments-sentinels` |
| #2928 | `fix/audit-ops-sentinel-marker` |
| #2925 | `live-task-column-lanes` |
| #2923 | `fix/task-id-integrity-sentinel` |
| #2921 | `fix/plugin-store-migration-marker` |
| #2894 | `gate/sql-literals-match-census-placement` |

That is **10 → 1** once folded. I have not cherry-picked anyone else's
commits — folding someone's work without them verifying it is how a
batch lands broken.

---

## What is in it so far (mine, from #2924)

**Clears a live main red:** `archived-column-gate-parity` fails on
`origin/main` today.

```
AssertionError: TypeScript encoding changed.
  async-comments-attachments.ts: 8 → 5
```

#2886 fixed a real bug — archived-document guards failing in *opposite*
directions on a renamed lane — by replacing three `column ===
"archived"` comparisons with `isArchivedLane(column, archivedColumns)`.
The AST scan counts raw comparisons, so the tally dropped.

**What I did not do is record it as three sites converted**, because
measured, it is not:

```
grep -rn "archivedColumns:" packages/core/src packages/engine/src --include="*.ts" | grep -v __tests__
→ (no matches)
```

No caller passes it. The parameter defaults to `LEGACY_ARCHIVED_LANES =
new Set(["archived"])`, so every call resolves to the literal it
replaced — byte-identical behaviour, resolved branch dead.

That matters for this guard's whole argument: its header warns that
converting the TypeScript half while the Drizzle and raw-`sql` halves
still compare the string is a split brain *"no test would catch, because
every builtin workflow spells the column `archived` so the two halves
agree by accident on every board we ship."* **There is no split brain
today precisely because the resolved half is unwired** — it becomes one
the moment a caller threads real lanes in without the SQL sides moving.
Recorded inline so `5` cannot be read as "3 sites done"; flagged on
#2886.

Verified not a split brain: the Drizzle and raw-sql inventories are
unchanged and both pass — worth stating because those assertions run
*after* the TypeScript one, so a plain red says nothing about them.

Scoped edit to `AUDITED_TS_SITES` by line range: these paths appear in
more than one inventory here, and an unscoped replace would quietly edit
the raw-sql side too, making the parity guard agree with itself (the
trap I hit in #2817).

Guard still bites: appending a real `task.column === "archived"` to an
audited file fails it. Core **4852 passed / 0 failed**, lint clean,
test-only.

Closing #2924 as superseded by this.

🤖 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**
* Improved task delegation messages when workflow pickup cannot be
confirmed.
* Delegation results now clearly indicate when a task has not been
verified for pickup.

* **Quality Improvements**
* Added validation checks to catch future-dated markers and inconsistent
SQL-column usage.
* Refined workflow checks to distinguish stale configuration from
incomplete configuration.

* **Documentation**
* Updated lifecycle conversion guidance with more accurate audit
findings and limitations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 19:59:14 -07:00
gsxdsm
7b68f20501 batch(docs): fold the three workflow-learnings / annotation PRs into one (#2942)
## Family batch — replaces #2926, #2892, #2887

Per the consolidation directive: the u9/e2e **docs family**, folded into
one branch and one CI run. Three PRs, five commits, **five files,
comment and markdown only**.

| folded PR | commits |
|---|---|
| #2892 `docs/union-vs-per-task` | the project union and the per-task
answer are not ranked; date correction |
| #2926 `docs/date-my-measured-claims` | date the measured claims (one
was wrong); date the grep-vs-AST measurement in the SQL gate header |
| #2887 `docs/archived-state-literals` | mark the three archived STATE
literals as deliberate |

Cherry-picked in original order with authorship preserved; all five
applied clean, no conflicts.

## Scope is provably comment-only

```
docs/solutions/workflow-learnings/lifecycle-conversions-that-score-as-wins.md
docs/solutions/workflow-learnings/project-union-versus-per-task-lanes.md
packages/core/src/task-store/async-maintenance.ts        ← FNXC DELIBERATE-LITERAL annotation
packages/core/src/task-store/workflow-definitions.ts     ← FNXC DELIBERATE-LITERAL annotation
scripts/check-sql-column-literals.mjs                    ← header prose only
```

Every added line in `packages/` and `scripts/` is inside a comment —
checked by filtering the diff for declarations, conditionals and
returns, which returns nothing. The two core files gain
`DELIBERATE-LITERAL` markers explaining that `'archived'` is a **state**
marker there, not a lane: the sweep collects rows Fusion itself archived
or soft-deleted, so widening to the resolved archived set would pull
live cards into a cleanup pass.

## Verification (scoped, per the directive — not the full suite)

- `pnpm lint` — clean
- `check-sql-column-literals` — exit 0 (the file it annotates)
- `check:lifecycle-columns` — exit 0 (the markers it adds are
census-visible)
- `sync-workflow-ir-callsite-allowlist.test.ts` — 3/3

## A correction worth recording

Mid-fold I saw a changeset, `self-healing.ts` and a test file in `git
diff origin/main..HEAD` and nearly reported the batch as impure. They
were **main's own commits** — `origin/main` advanced between branch
creation and the diff, so the comparison was against a stale base.
Rebasing onto current `main` reduced it to the five files above. Worth
flagging for anyone else folding a family today: with `main` moving this
fast, diff the branch **after** rebasing or the file list will lie to
you.

## Closing the originals

#2926, #2892 and #2887 are superseded by this and are being closed. I
hold no PRs of my own in this family — all mine merged — so this fold is
on behalf of the family rather than a rollup of my own work.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 19:12:26 -07:00
gsxdsm
b3b377d367 docs(workflow-learnings): two lane-literal classes no tool of ours can see (#2877)
Docs only. Two findings from this unit that cost real time to derive and
would otherwise be re-derived by whoever reaches these files next.

## 1. `=== "archived"` is usually a SENTINEL

`packages/core/src/task-store/async-comments-attachments.ts` carries
**9** census guards — the second-largest single-file count outside
`self-healing.ts`. Reading all nine: **exactly one** is a board-column
comparison. The other eight compare against a value `getLiveTaskColumn`
*manufactures*:

```ts
if (row.column === "archived" || row.deletedAt != null) return "archived";  // ← fabricated
return row.column;
```

Converting those eight to `isArchivedColumnRole` would keep passing on
the built-in board and start **failing** on a renamed one — a
soft-deleted parent's documents would become readable. **The conversion
makes the renamed board worse**, which is the opposite of what the
census count implies.

The rule that separates them: look at where the compared value *came
from*, not at its type. From `task.column` or a DB field → a board lane.
From a function that *returns* `"archived"` as a documented outcome → a
sentinel.

Consequence worth stating plainly: **a file's census count is an upper
bound on convertible sites, not a work estimate.**

## 2. Lane literals inside raw `sql` are in no total at all

The Reliability panel had three inputs. Two were call arguments and
converted routinely (#2861). The third encoded its lanes in a `sql`
fragment:

```sql
metadata->>'to' = 'in-review' OR (metadata->>'from' = 'in-review' AND metadata->>'to' = 'done')
```

The census scans `===`/`!==` comparisons; the unwired-lane-parameter
guard scans declarations. **Neither can see a string inside a `sql`
template**, so this class is not in the backlog number — a second,
independent reason the total is a floor. Second known instance after the
archived gate in PR #2724, which makes it a pattern rather than an
accident.

Fixed in #2875, and the doc says so rather than leaving it described as
outstanding — a learnings doc that reports a fixed defect as open sends
the next reader to a dead end. `scripts/check-sql-column-literals.mjs`
(#2841) is the detector for the class and freezes the surface at 30
sites; the two are complementary.

## 3. Sibling files

The GitLab importer's `column: "triage"` was fixed in #2843. The Linear
importer — written from the same template, with **two tests pinning the
bug** — still had it, and was found only by re-grepping an area I had
already declared clean (#2860). When a defect is found in a file that
has a sibling, the sibling is the next place to look, and no tool will
tell you that.

## Verification

`pnpm lint` clean. No source change.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 17:05:01 -07:00
gsxdsm
6099f028e4 docs: correct every number in the self-healing sweep doc — all of mine were wrong, three different ways (#2865)
CodeRabbit flagged #2838's doc as saying four sweeps converted when the
PR converted more. It merged before I could answer, so this is the
fix-forward — and re-measuring found the count itself was wrong, along
with **every intermediate number I published**.

## Measured, comments stripped

Literal column queries in `self-healing.ts`: **47 before, 36 now.**
Eight sweeps converted, all eight named in the doc.

## Three distinct errors, each recorded because the next worker re-runs
this

1. **The per-commit "N remaining" counts (44, 43, 42, 41, 40) were
arithmetic on an assumed starting point.** I decremented a number
instead of measuring one — in a program whose central discipline is that
measurement beats assumption, in commit messages that also said
"measured".
2. **A raw `grep -c` counts explanatory comments that quote the old
query form** — including the ones these conversions *add*. So converting
a sweep could leave the count unchanged, which is exactly what it
appeared to do for six of the eight.
3. **The obvious comment filter (`startsWith("//") || startsWith("*")`)
misses block-comment lines beginning with ordinary prose**, which is
most of them here. That is why my first correction said 45 and was still
wrong.

The doc now carries the strip-comments-then-count command, so the number
is **reproducible rather than quoted**.

## Also corrected

The activation-risk list is **2 sweeps, not 4** —
`finalizeNoOpReviewTasks` and `recoverCompletionHandoffLimbo` were
converted in the same PR and are no longer risky. A stale list naming
specific sweeps and line numbers is worse than a stale count: it reads
as a work queue, and I nearly "fixed" a guard I had already wired from
exactly that kind of row.

## Verification

`pnpm lint`, `check:changesets`, census `--strict` — clean. Docs-only;
no code change.
2026-07-30 16:34:45 -07:00
gsxdsm
5792452f0a docs(workflow-learnings): mutation testing has one blind spot — your own imagination (#2858)
The most transferable thing this lane produced, and it is a correction
to advice I wrote earlier in the same document.

## The gap

Every other section here says *"watch the guard go red before you trust
it."* That rule is necessary and **not sufficient**, and the way it
fails cost the most.

Three instruments were written during this program. Each was
mutation-tested in both directions before shipping. Each was green.
Reviewers then found, in those same instruments:

- a **file-level pre-filter** that skipped whole files, so a forbidden
site added to a file with no other SQL was invisible;
- an **anchored pattern** that missed qualified and compound fragments
(`t."column" = 'done'`);
- a scan over **SOURCE text**, where a double-quoted TS string still
spells `\"column\"` with the backslashes in it;
- an operator list of `= != <>` that never considered **`IN (...)`**;
- and worst, a template scan that joined only the **static spans** — so
a Drizzle query, which puts the COLUMN in the interpolation hole and the
legacy id in the static text, matched nothing. **That gate was blind on
the exact files it was built to freeze.** Enabling that one shape took
the population from 14 to 31 and revealed five previously invisible
files.

## Why the mutation tests could not catch any of them

All five are **false negatives**, and the reason is structural rather
than sloppy:

> A mutation you write is a mutation you already imagined, so it lands
inside the space your scanner understands.

Reintroducing a defect the checker was designed around proves the
checker still handles that defect. It says nothing about shapes you
never modelled.

## What does find them

1. **Run the instrument against the code it was written for and read the
hits by hand.** The Drizzle blindness was obvious the moment someone
asked *"why is the merge-queue query — the reason this exists — not in
the output?"*
2. **Prefer one unanchored pattern over a fast pre-filter plus a precise
one.** Every false negative above came from two patterns disagreeing
about whether to run the real check at all. A pre-filter is a second,
weaker specification of the thing you are testing.
3. **Treat a guard's own count as a claim to verify, not a result.** "14
sites" read as coverage for days; it was the subset one scanner happened
to model.

A false positive is loud and gets fixed. **A false negative prints a
baseline and reads as coverage.**

Docs only — no changeset.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 15:55:00 -07:00
gsxdsm
7784cb1fe8 self-healing: six recovery sweeps that never ran on a renamed board — and the guards widening their queries activates (#2838)
**Six self-healing sweeps did not run at all on a renamed board. Each is
a recovery path — the thing that unsticks a card when something has
already gone wrong.**

#2800 measured this class and could not fix it: a read happens *before*
any task is in hand, so there is nothing to resolve a per-task lane
from. `resolveProjectColumnsForRoles` (landed separately) is the seam
that was missing.

## What was silently dead

| sweep | what stayed broken on a renamed board |
| --- | --- |
| `reconcileDoneTaskIntegrity` | a landed card kept **no commit sha**,
forever |
| `recoverAlreadyMergedReviewTasks` | a card whose merge **succeeded**
stayed parked with `status: "failed"` |
| `recoverStuckMergeDeadlocks` | **doubly blind** — no candidates *and*
no dependents |
| `recoverInterruptedMergingTasks` | a task interrupted mid-merge sat in
`merging` indefinitely |
| `recoverMergeableReviewTasks` | a card ready to merge was never
re-enqueued |
| `recoverReviewTasksWithFailedPreMergeSteps` | a card parked on a
failed review step was never revived |

The census scored the `task.column === "..."` re-assertion *inside* each
loop, never the query above it. Converting those comparisons would have
dropped six counts and changed nothing — the loop bodies were already
unreachable.

## The conversion shape — five parts, three of which review taught me

Documented in `self-healing-sweeps-are-blind-on-a-renamed-board.md`,
because the second sweep **drifted from the first**: I wrote it from the
pre-review version and reproduced a flaw already fixed one commit
earlier.

1. **Read** — project union, query each column, dedupe by id. Legacy ids
unioned so a board mid-rename is not skipped.
2. **Verdict** — per card against **its own** workflow. Widening the
read and widening the verdict are different decisions: *a missed row is
invisible, a wrong row is a write.* Using the project union as a
per-card test claims a card because some **other** board calls its
column that role.
3. **Provenance** — the resolver **substitutes** the built-in IR rather
than failing, so `length > 0` reads as "this card answered" when nobody
did. It does not change the verdict (measured: identical) — it makes the
unrepaired card **reportable**.
4. **The log strings** — widening a query invalidates every message
naming the old literal. One logged `"stale merging task(s) in
in-review"` after its read covered several lanes.
5. **The guards the query ACTIVATES.**

## Part 5 is the one that bites

A guard downstream of a literal query is **unreachable** on a renamed
board — and unreachable is indistinguishable from correct. That is why
these sit unwired indefinitely.

`recoverReviewTasksWithFailedPreMergeSteps` filters on `blocker !==
"task has failed pre-merge workflow steps"` — an **exact string match**.
Unwired, the blocker returns `"task is in 'checking', must be in
'in-review'"`, so widening the query alone would have made the sweep
**find every card and reject every card**.

Measured: **6 sweeps hold both a literal query and an unwired lane
guard**; 30 hold a literal query with no such guard. All six are named
in the doc.

**One of the six was my own already-converted sweep.** I widened
`recoverAlreadyMergedReviewTasks` two commits before noticing its
`getTaskHardMergeBlocker` was unwired — so for two commits it found
renamed-board cards and declined them. The scan must run **before**
widening; I did it after, and only caught it because the next sweep
forced the question. `getTaskHardMergeBlocker` was the blind spot for
four of the six: a wrapper, no lane parameter at all, every caller
behind a literal query.

## Corrections to my own work, kept visible

- The project union used as a **per-card verdict** — the flat-set
mistake `project-lane-vocabulary.ts` warns about in its own header,
which I quoted while writing it.
- A **provenance fix that was a no-op**: measured identical verdicts in
every state, revert passed its own new test, so it was thrown away
rather than shipped with a comment claiming otherwise.
- The second sweep **reproducing the first's pre-fix shape**.
- Three assertions that were **vacuous until the revert exposed them** —
including one where the write needed a real git repo, so `commitSha`
could not distinguish accepted from rejected.

## Verification

- `pnpm test:gate` — 161 + 487 + 13 + 71
- `self-healing.test.ts` 412, query-blindness suite 12
- `tsc` on core and engine; `pnpm lint`; `check:changesets`; census
`--strict` — all clean, each run explicitly
- Every conversion revert-measured, **each direction independently**
where a sweep has two (read and guard)

## Scope

**42 queries remain**, 5 of the 6 activation-risk sweeps among them.
Each is per-sweep work — its own filter semantics, its own downstream
guards, its own log strings — so they land one at a time with the
pattern proven, never swept.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Bug Fixes**
- Self-healing workflows now work correctly on boards with renamed
lifecycle columns.
- Improved recovery for completed, in-review, interrupted, stalled, and
failed-merge tasks.
- Prevented tasks from being incorrectly classified using another
workflow’s columns.
  - Added warnings when a task’s workflow lanes cannot be resolved.

- **Documentation**
- Expanded guidance on renamed-board recovery behavior and related
diagnostic limitations.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 15:38:10 -07:00
gsxdsm
89aaf341d0 the unwired-seam audit: 9 defects the census cannot see, incl. a reviewed card that cannot merge (#2820)
**Nine operator-visible defects in a class the census cannot see, plus
the audit method that found them.**

The census scans for lifecycle-column **comparisons**. This PR is about
guards that have no literal to find: a helper takes an optional
*resolved* lane set, its own test passes it, the census entry is gone —
and the callers pass nothing. **A resolved seam nobody wired is
indistinguishable from no seam at all.**

## What was broken

| defect | operator sees |
| --- | --- |
| `getTaskMergeBlocker` unwired in `mergeTaskImpl` | `Cannot merge FN-1:
task is in 'checking', must be in 'in-review'` — **a reviewed card
cannot merge** |
| …and in the completion move | `Cannot move FN-1 to done: …` — **and
cannot complete** |
| `isParkedTaskColumn` unwired ×2 (`agent-heartbeat`) | a durable agent
keeps claiming a parked card; **Health Check renders it RUNNING** |
| `resolveLinkSyncColumnRoles` first-per-role | link hygiene skips a
**second hold lane** entirely |
| `executor` active-task predicate first-per-role | a card in a **second
wip lane reads as INACTIVE**; its prompt file becomes reclaimable |
| `isPlanningContinuationTaskDispatchable` partially threaded | a board
declaring `done` as *non-terminal* stalls its cards — **stalled by a
lane name** |
| `default-workflow-hooks:72`, `executor:2404` | resolved gate admits
the move, unresolved blocker refuses it |

## The recurring shape, which is sharper than "a caller forgot an
argument"

Four sites resolve the lane and then re-ask with the literal, **a few
lines apart in the same function**:

- `task-artifacts-ops` resolves `completeColumn`, then asks the blocker
with the literal.
- `default-workflow-hooks:72` gates on `lifecycleColumns?.review`, then
the literal.
- `executor:2404` compares `resolveResumeLanes(…).review`, then the
literal.
- `resolvePlanningContinuationCandidate` applies the caller's terminal
set, then delegates without it.

**Grep for the helper, not the literal.** The literal is one function
away, correctly annotated as a fallback — which is exactly why the
census is blind to all of it.

## The arity trap, named and measured (six occurrences, one caught by
review here)

`resolveLifecycleColumns` answers *"which column is **the** hold
lane?"*. A `.includes()`/`.has()` test asks *"is this **any** hold
lane?"*. Nothing distinguishes them — same types, no literal.

**A default-vs-renamed differential cannot catch it**, because the
default board declares one column per role and therefore cannot express
the failing shape. It needs a *structurally* different fixture. That is
a sharper rule than "test both vocabularies", and it would have caught
all six.

Scanned: 12 candidate sites. **4 fixed · 3 blocked (2 on the inert sync
IR reader; `triage:833` also query-shaped) · 1 needs a hook-contract
change · 3 not defects (a returned tuple; an ordering-sensitive
precedence list) · 1 false positive of my own scan.**

A sweep over all twelve would have broken the ordering-sensitive pair,
delivered nothing at the sync-blocked ones, and "fixed" a site that was
already correct.

## Two traps in fixing this class — I hit both here

1. **The legacy id is a FALLBACK, not a member.** Pre-seeding
`"in-review"` admits a board that *declares* `in-review` as its WIP
column — a card mid-implementation merges prematurely. A real resolved
answer must **replace** the default. (Caught by review; it is the same
unscoped-legacy-acceptance the glasses plugin's review caught earlier,
which I had read and reintroduced.)
2. **Two guards, one assertion.** `toContain("must be in")` passed with
`mergeTaskImpl` reverted, because the *completion* guard caught the card
instead. The assertion now names the site (`Cannot merge` vs `Cannot
move … to done`) so the two fail independently.

## Corrections I made to my own work, recorded rather than quietly fixed

- My first PG test was **vacuous three ways**:
`saveWorkflowDefinition?.()`/`setTaskWorkflowSelection?.()` do not exist
(the `?.` swallowed both, so the task kept the builtin workflow),
`updateTask({column})` does not move a card, and a two-node IR made
every setup move illegal. Premise is now **asserted**, not assumed.
- My doc claimed the audit was complete. It enumerated **helpers**, not
every **caller** — `getTaskMergeBlocker` alone has 13 call sites.
Corrected in place, with the still-unwired ones listed by file and line
and a note to distrust any "audit complete" claim including mine.
- A severity correction to another worker's E2E:
`selectActionablePlanningContinuations` has **no production caller**, so
its stated consequence is latent, not live.

## Verification

- `pnpm test:gate` — 161 + 487 + 13 + 71
- `tsc` on core and engine; `pnpm lint`; `check:changesets`; census
`--strict` — all clean, each run explicitly
- Every fix revert-measured; each has a non-vacuous companion. The
two-hold-lane and repurposed-`in-review` cases exist because the default
board cannot express those shapes.

## Deliberately not done, with reasons in
`resolved-seams-nobody-wired.md`

`isTaskReadyForMerge` (dead in production — wiring it would be the
anti-pattern itself); `getTaskHardMergeBlocker` (3 of 4 callers are
query-gated sweeps); `getInReviewStallReason` (needs a **batch
prefetch**, not a per-task resolve — its callers decorate every task on
every list read; the in-review stall badge is wrong on renamed boards
until then); `default-workflow-hooks` planning/live-work sets (needs
`DefaultWorkflowMoveContext` to carry the IR — a shared contract
change).
2026-07-30 15:08:01 -07:00
gsxdsm
7927c7b58a docs(testing): probe the DOM before theorising about a missing element (#2850)
Earned the hard way this week: three separate causes in `App.test.tsx`
and `board-mobile-view-switch.test.tsx` all presented **identically** as
a missing element, each with a DOM that looked healthy.

| what the test said | what was actually wrong |
|---|---|
| `Unable to find "+ New Task"` | `ListView` rendered its workflow
**skeleton**, which carries the same `list-view` class as the real body
— so the preceding `waitFor(".list-view")` passed |
| `Unable to find role="heading" "New Task"` | `NewTaskModal` **threw**
— an incomplete `vi.mock` was missing `isShortViewport` — and an
`ErrorBoundary` swallowed it |
| `Unable to find [data-testid="switch-to-board"]` | an uncaught render
error **unmounted the entire React root**; the DOM was already empty
three lines earlier |

The part worth recording is the hit rate. **Three theories were offered
before any probe — "the board renders nothing", "i18n is returning
keys", "the FloatingWindow rework" — and all three were wrong.** Three
probes each landed the cause on the first try. Those wrong theories cost
days; the probe is four lines.

The doc records the snippet, what each signal means (`error-boundary` in
the DOM = a swallowed throw, and its text names a missing mock export
exactly; empty DOM with no boundary = unmounted root, so trust the
*first* failing assertion not the reported one; container present but
contents absent = a skeleton standing in), and the corollary for writing
assertions:

> Wait on a marker only the real thing has.

A class shared with a loading or empty state turns *"the list rendered"*
into *"something rendered"*, and the test then fails one line later
against a DOM that looks fine. That is why `list-view-body` exists
(#2834).

Placed under the dashboard testing sections in `docs/testing.md`. Docs
only — no changeset.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 15:07:34 -07:00
gsxdsm
be12ca905b docs(workflow-learnings): the fifth shape — converted consumer, literal-passing producer (#2835)
Found by the operator reviewing **my own** shipped fix (#2823). It
belongs in this document because it is the only shape so far that every
instrument in the program reported as done.

## The defect

`clearNearDuplicateReferencesTo` was converted to resolve the
canonical's column flags, and my test proved that by supplying `column:
"shipped"`. Both production call sites in `moves.ts` gated on the
**resolved** complete lane and then passed the literal `column: "done"`.
The consumer was correct; the producer was never converted; and the
hand-supplied fixture value is exactly what hid it.

Why nothing caught it:

- the **census** counts comparisons — a value passed as an argument is
not a comparison;
- the **seam check** asks whether callers *supply* the argument — these
did, with a literal;
- the **test** supplied the interesting value itself, so it exercised
the consumer and never the producer.

And the part I would have got wrong: driving the flow end to end still
does **not** distinguish them. The consumer looks the passed column up
in the canonical's IR, finds nothing for `done` on a renamed board, and
falls through to the legacy predicate where `done` *is* terminal — right
answer, wrong reason. It only bites on a board that declares a `done`
column **without** the complete trait.

## The rule

> A differential test must vary the value the **production** code
computes, not one the test hands in.

If the fixture passes the lane name, it has tested the consumer. Who
computes that argument in production, and do they compute it or spell
it, is a separate question — and the one that was wrong here.

## Measured, so nobody builds the wrong instrument

An AST probe for call arguments shaped `{ column: "<legacy id>" }` finds
**79 sites** across `packages/`. It correctly flags the two real
`moves.ts` offenders — but most of the rest are legitimate: `set({
column: "archived" })` writing the archive state, `listTasks({ column:
"todo" })` filtering a query.

So a blocking gate on this shape needs a curated list of consumers that
interpret a column as a **role**, as opposed to storing or filtering it
— a per-consumer judgment call, not a mechanical check. **Recorded
rather than built**, and deliberately not attempted on top of five
unmerged PRs in packages I do not own.

I also verified one nearby call site that a naive version of this probe
flags and which is **not** a defect: `archive-lifecycle-2.ts:353` passes
`column: "archived"`, but that path sets `task.column = "archived"`
unconditionally, so it is passing the column the card actually reached.
Exactly what the operator's fix is about.

Docs only — no changeset.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 14:25:59 -07:00
gsxdsm
ed6d54485b glasses plugin: the review actions could never resolve a review lane (4 guards + 3 invisible destinations) (#2816)
Four agent actions still keyed on literals, with three census-invisible
`moveTask` destinations between them. `agent-actions.ts` already had
`laneContext`/`destination` from an earlier partial conversion — these
were simply never migrated.

## Census

| file | main | here |
| --- | ---: | ---: |
| `plugins/fusion-plugin-even-realities-glasses/src/agent-actions.ts` |
4 | **0** |

Plus 3 hardcoded `moveTask` destinations the census cannot see
(`requestReview` → `in-review`, `returnToAgent` → `todo`, `retryTask` →
`todo`).

## The real finding: this plugin could never resolve a review lane

`resolveLifecycleColumns` keys its `review` role on the
**`mergeOrchestration` trait alone**. A board whose review column
carries only `merge-blocker` and/or `human-review` — the common custom
shape, since `merge` is opt-in — resolves **no review lane at all**.

So every review-gated action here (`requestReview`, `acceptReview`,
`returnToAgent`, `retryTask`) compared against `undefined` and **refused
every card**, and `requestReview` had nowhere to move one. This is not a
regression from converting them; it is why they *could not* be converted
with `lanes.review` as-is.

Converting the four guards without noticing would have shipped four
actions that fail closed on exactly the boards this program exists to
support — a conversion that looks complete, passes its suite, and makes
the plugin useless on a custom board.

**Widened in `laneContext`, not in the shared resolver.**
`resolveLifecycleColumns` is consumed well beyond this plugin, and its
`review` role deliberately means "the merge-orchestration column" for
the merge queue. The gap is already recorded in
`notification-renamed-lifecycle-columns.test.ts` and in #2807 —
reconciling the two definitions is a core-level decision, not one to
take from a plugin. `mergeBlocker` is preferred over `humanReview`
because a card cannot leave a merge-blocking column until the gate
clears, which is the closer analogue of the legacy `in-review`.

## The suite caught an over-reach of mine

My first version put a blanket `if (degraded) conflict(...)` at the top
of `retryTask`, which broke a pinned invariant the test names outright:
**"a degraded workflow does not block retries that move nothing."** The
status-only retry just clears fields; refusing it because the workflow
could not be read breaks a recovery that needs no lane at all.

Degraded now blocks only the branches that actually **move**. Same
reasoning applied to `acceptReview`, which also moves nothing. The
existing `startWork` convention — conflict on degraded — is right
precisely *because* it moves.

## Ordering

`returnToAgent` and `retryTask` now resolve their destination **before**
the field clear. Both cleared first, so a rejected move left the
assignee and status — or the worktree, branch and base refs — nulled
with the card exactly where it was. That is the fifth instance of this
half-applied shape in the audit, and it is rule 3 in the class doc.

## Revert results (measured, each independently)

| conversion | reverted → |
| --- | --- |
| `requestReview` destination | 1 failed — moves to the literal
`in-review`, which this workflow does not declare |
| `returnToAgent` destination | 1 failed — moves to the literal `todo`,
same |

Plus a non-vacuous companion: a renamed card *not* in the wip lane must
still be refused by `requestReview`, so a gate admitting everything
would not pass.

## Verification

- Plugin suite — **186/186**
- `pnpm test:gate` — 161 + 487 + 13 + 71, green
- `tsc` on the plugin — clean
- `pnpm lint`, `check:changesets`, census `--strict` — all clean (run
explicitly)
2026-07-30 12:55:38 -07:00
gsxdsm
ba40942a10 batch-dashboard-app: 75 → 2 across packages/dashboard/app — the last two are deliberate, not missed (#2772)
**Batch branch is live: `batch-dashboard-app`.** Push conversions here
as commits rather than opening per-file PRs — that is the CI-run
bottleneck this model removes.

**One-line ownership note for you to arbitrate:** you have addressed me
as U11, U12 and U7 at different points, so the `u12 worker ->
batch-dashboard-app` mapping is ambiguous from my side. I claimed it
because `dashboard/app` is where I have done the most work this session
(TaskContextMenu, Column, TaskCard, TaskDetailModal, columnRoles,
taskActivity) and I know which of its guards are load-bearing fallbacks.
**If another worker is the intended owner, say so and I will hand the
branch over rather than both of us pushing to it** — two workers on one
shared branch is exactly what silently discarded a reviewed fix in #2645
today.

## The work order (measured at branch point, tests excluded)

**75 guards across 32 files.** Largest: `TaskContextMenu.tsx` 9 ·
`Column.tsx` 7 · `ListView.tsx` 6 · `TaskDetailModal.tsx` 4 · then a
long tail of 3s, 2s and 1s. Full per-file list is in the committed work
order so feeders can claim without re-measuring.

## Two rules this surface keeps tripping on

**1. A literal after `??`, or in the `else` of a `flags ?` ternary, is a
DEGRADED-MODE answer — not an unconverted guard.** Two real states reach
it: the **pre-load window** (board renders before the workflows fetch
resolves) and a card stranded on an id its workflow no longer declares.
In both, `columnFlagsById` has no entry at all. Deleting the fallback
does not remove a decision — it substitutes "no role" silently, and
affordances vanish during first paint.

Those sites reach 0 by **marking**, not deleting. Expect
`TaskContextMenu.tsx` and the `utils` files to be **mostly marks**. A "9
→ 0" that deleted 9 fallbacks is a regression wearing a green census.

**2. A marker excuses ONLY the construct it is attached to** — the
statement or function holding the literal, not a sibling declaration.
This has cost three passes, two of them mine; my first attempt on
`reliability-metrics.ts` scored **1 of 6**. **Verify by the count
moving, not by the comment existing.** With the ratchet gate-blocking, a
mis-marked batch either wedges the gate or locks the miss into a
re-recorded baseline.

## Status

Opening commit is the work order only — **0 of 75 converted so far.** I
am near the end of my context, so I am establishing the branch and the
shared list rather than starting conversions I cannot finish cleanly.
Feeders can begin immediately; I will keep the branch rebased.

My other PR **#2762** (`live-agent-count.ts` 6 → 0) is green and
unconflicted — per your rule it should land rather than fold into a
batch, and it is `packages/core` so it belongs to batch-core anyway.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Task UI now resolves workflow “column roles” per task to drive
diffs/merge details, routing/steering, progress/runtime visibility, and
review badges.
* Right-dock/overflow views and dev-server now use per-task column
traits for “executing” behavior and dependency-based “Up Next”
eligibility.
* **Bug Fixes**
* Fixed bulk action selection/delete/archive eligibility and prevented
cross-workflow role leakage.
* Made in-review/stale-paused-review, stuck, and effective
executor/validator model logic role-aware.
* **Tests**
* Added regression coverage for degraded-flag behavior and ensured
resolved-flag props aren’t ignored.
  * Added a static check to fail builds on inert optional flag seams.
* **Documentation**
  * Updated batch work-order and mega-batch branch guidance.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---

## Late addition: the seam gate was masking a real offender

`scripts/check-inert-flag-seams.mjs` matched call sites by NAME, so two
same-named functions in
different modules were conflated. I had documented that as a known
false-positive source and moved
on — reports mentioning `sortTasksForDisplayColumn` are noise, read past
them.

That annotation was the damage. Core's `sortTasksForDisplayColumn`
genuinely never receives its
`columnFlags` argument outside its own tests. The dashboard's separate
function of the same name
(`app/components/taskSorting.ts`), called with up to five arguments from
`Lane`/`Board`/`ListView`,
was raising the arg-count max and clearing core's seam. The offender was
behind a row everyone had
been told to skip.

The gate now records the module each callee is imported from and matches
it against the seam's
declaring module.

**Measured, by reverting the change:** the scan prints `17 seams, all
supplied` and emits **no row**
for the function. With the change, it is reported. Both directions
watched.

Reported on #2783 rather than fixed from outside — core owns it, and
"wire the flags" vs "drop the
parameter and let the literal stay counted" is their judgment call.
TEMPORARY allow-list entry
carries it meanwhile; the existing staleness check fails the moment the
site becomes supplied, so
the entry cannot outlive the fix.

Two known limits remain, both inherent to name matching and both
documented in the script: the
one-supplier floor, and the `__tests__` exclusion (hence the two
permanent `ALLOWED` entries).


## And the one-supplier floor, closed the same way

I wrote in the section above that the floor "hasn't cost anything yet."
That is verbatim the
reasoning that kept the imported-shadow bug alive, so I closed it
instead of leaving the note.

`best < arity` asked only whether SOME caller supplied the argument. One
correct call site cleared
the seam while every sibling took the legacy fallback — the
`isTaskStuck` defect class, where two of
three sites omitted the flags and the gate stayed green because the
third was right. Review caught
that one. A partially-supplied seam is the harder of the two:
wholly-unsupplied is uniformly wrong,
this works on the board you tested and degrades on the column you did
not.

**Measured:** dropping the flags argument at `Column.tsx`'s supplied
call site produces
`supplied by 5/6 call sites; omitted at
packages/dashboard/app/components/Column.tsx:1 (of 2)`;
restoring returns `all supplied at every call site`. Red and green both
watched.

Two real omissions found, both on `isNearDuplicateCanonicalInactive`:

- **`TaskDetailModal.tsx`** — deliberate, and it **corrects a note I
left at that site**. The old
note said hoisting the flags state was "the actual fix." It is not, for
this call: the flags in
scope describe the *modal's* task, and the canonical is a **different
task** on a column this
component never resolves. Passing them would type-check, read as a
conversion, and answer about
the wrong task — exactly what `column-role-degraded-flags.test.ts`
exists to catch. Supplying it
  correctly needs a fetch, which is a data change and out of scope.
- **`core/task-store/branch-group-ops.ts`** — genuinely wireable (the
impl is async and already
holds `store` and `canonicalId`). Reported on #2783, not edited from
outside.

Exemptions for this class are keyed by **call site**
(`<file>::<function>`), not by function name.
A name-level entry would waive every site of a partially-supplied seam,
which is backwards — its
other sites are correct and are the reason the omission is worth
reporting. Both entries carry the
same staleness check as the name-level list and cannot outlive their
fix.

Remaining known limit, now the only one: the `__tests__` exclusion,
which makes a test-only export
read as having no callers. That is what the two permanent `ALLOWED`
entries are.


## The `__tests__` exclusion, and two allow-list entries built on false
reasons

Named as the "last remaining limit" above, so it got closed too. The
scan now reads test files for
call sites — but counts them **separately**, and a test never clears a
seam. That direction is the
dangerous one: counting test callers as suppliers would have re-hidden
core's
`sortTasksForDisplayColumn`, whose only suppliers are its own tests.
Measured by lifting its
exemption: still reported.

Both permanent allow-list entries claimed the scanner couldn't see their
callers. **Both reasons
were false**, and reading tests is what proved it:

- **`evaluateMergeBlockerGuard`** — zero callers in tests either. Its
only reference in the repo is
its own declaration; never registered as a trait hook; the
`evaluateDefaultWorkflowGuards` reader
its file header credits does not exist. The `lifecycleColumns`
conversion went onto dead code, and
its note describes a crossing the guard cannot make. Reported on #2783,
including the two things I
am explicitly *not* concluding (no `"guard"` hook is registered in
production; whether that is
  residue or a dropped registration needs core's intent).
- **`isRecoverableMissingWorktreeReviewFailure`** — 5 test call sites.
It wraps
`...WithProgress`/`...NoProgress`, the live pair called from
`self-healing.ts`, both supplying
  `reviewColumns`. Entry kept, true reason recorded.

### A wrong turn, recorded because it is the failure mode this PR is
about

I first classified no-production-caller seams as *informational* when
they weren't re-exported from
a package index, reasoning that a public export might be called
externally. That silently downgraded
`sortTasksForDisplayColumn` — a confirmed real offender — from failing
to a footnote. Publication
status has nothing to do with whether there is production behaviour to
be wrong. Reverted to the
simple rule: no production caller means inert, and it fails.

It is worth stating plainly because it is the exact shape of everything
else in this PR: a change
that made the gate read *cleaner* while making it catch *less*, and it
type-checked, passed every
test, and would have reviewed fine.

### Where that leaves the check

Every blind spot named in this PR has now been closed, and **each one
produced a real defect within
minutes of closing it** — imported shadows, the one-supplier floor, the
`__tests__` exclusion. Four
verified findings went to core, one to engine. I would not read the
remaining ~240 guards' green
gates as evidence that they are clean; I would read them as untested.


## Two guards for one question, one of them worse

Having hardened the script, I checked its older twin rather than
assuming it was fine.
`resolved-flags-seams-have-suppliers.test.ts` carried its own copy of
the trailing-flags-parameter
check — written before the script existed — with **all three** holes the
script has since closed.

**Measured on one reintroduced defect** (dropping the flags argument at
`Column.tsx`'s supplied
`isNearDuplicateCanonicalInactive` call):

| | result |
|---|---|
| `scripts/check-inert-flag-seams.mjs` | `supplied by 5/6 call sites;
omitted at .../Column.tsx:1 (of 2)` |
| this test's arity half | **3 passed** |

Deleted the arity half. Redundancy between a strong and a weak check
isn't redundancy — it's a green
result available to whoever runs the weak one, and there was no signal
at the call site telling you
which you were looking at.

The **props-shape half stays**: it has no twin in the script, and I
confirmed it still fires by
reintroducing the original `PrPanel` defect (outer component stops
destructuring `taskColumnFlags`)
— it reports `PrPanel declares taskColumnFlags but never takes it`.

Dashboard app suite: **113 files / 3921 tests** (was 3922 — the deleted
case is the difference).


## The gate started catching defects as they landed

Syncing with main brought in three fresh conversions from other workers.
The hardened check flagged
all three immediately — the first time these guards have fired on
someone else's landed code rather
than on my own.

- **`TaskCard`** — `getRunningOptionalGateBadge(task)` omitted flags
while *both* `ListView` sites
supplied. Fixed, and `taskColumnFlags` added to the `useMemo` deps: no
`exhaustive-deps` rule here,
so a memo that reads flags without listing them keeps the first-paint
`undefined` answer and
  reproduces the bug through staleness instead of omission.
- **`TaskTokenStatsPanel`** — `getTotalAgentActiveMs` omitted while
`TaskCard` supplied, so the same
runtime number came from the real column on a card and from legacy ids
in the detail modal. Now
takes `columnFlags`, supplied from `detailColumnFlags` — correct here
because the panel renders the
  modal's **own** task, unlike the near-duplicate canonical above.
- **`ListView` ×2** — passed `columnFlagsById.get(task.column)`, the
cross-workflow **union**. A task
whose own workflow doesn't declare that column gets a *neighbour
workflow's* traits. The landed
comment justified it as "this list already owns `columnFlagsById`" —
exactly the reasoning
`column-role-degraded-flags.test.ts` exists to reject. It failed on
merge and is how I found this.

Also: the `getTotalAgentActiveMs` exemption I was carrying
**self-retired**. Main wired the seam, the
staleness check failed the entry, and I removed it. That mechanism has
now paid for itself once.

### Pre-existing, NOT from this PR: `App.test.tsx` is red on main

`app/components/__tests__/App.test.tsx` fails **10 of 141** identically
with my changes, with my
changes stashed, and with main's own `App.tsx` restored. Not mine, and
not in the merge gate.

**Bisected on clean `main` checkouts, so this is measured rather than
inferred:**

| commit | date | result |
|---|---|---|
| `main~400` (`41d60f0355`) | 2026-07-25 | **140 passed** (140 tests) |
| `main~275` (`74d6513fae`) | 2026-07-27 | 3 failed / 141 |
| `main~210` (`d2ce1ba8b5`) | 2026-07-29 | 10 failed / 141 |
| `main` (`6fc98fd6c7`) | 2026-07-30 | 10 failed / 141 |

So it is **not one regression** — it degraded in two stages across
2026-07-25 → 07-29, and the test
file itself changed in that window (140 → 141 tests). Three commits
touched it there:
`73b2a32e2b`, `f26cbedf4f`, `f157bf7460`. That window overlaps the
workflow-owned lifecycle
migration, which is suggestive but not something I confirmed.

The failures are render-level, not assertion-level — `Unable to find an
element with the text: + New
Task`, `Unable to find role="dialog"`, `Unable to find ... Back nav
task`. The board appears to
render nothing. That reads like a real regression or a harness mismatch
after the lifecycle
migration, not a flake, so I have deliberately **not** quarantined it —
quarantine is for flakes, and
using it here would hide the signal. Flagging for whoever owns
`App.tsx`.

My suites: `app/__tests__` **113 files / 3921 tests** green, `tsc` 0,
lint 0, census `--strict` 0,
seam gate 0.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 12:46:12 -07:00
gsxdsm
6fc98fd6c7 the third census-invisible class: 51 hardcoded moveTask destinations, measured — and duplicates never archived on a renamed board (#2808)
A third census-invisible class, measured — plus the two worst instances
fixed.

## The shape

```ts
if (task.column !== "in-review") { … return; }     // the census counts THIS
await this.store.moveTask(taskId, "in-progress");  // and cannot see THIS
```

The census is an AST scan for **comparisons**. A `moveTask` destination
is a **call argument**, so no backlog entry ever points at one.
Converting the guard alone is *worse than converting neither*: the
handler starts admitting work on a renamed board and then tries to move
the card into a lane that board may not declare.

This bit twice in one week — #2797 (`branch-worktree` requeued into a
lane that may not exist) and #2807 (a GitHub "changes requested" review
dropped, then a move to a hardcoded `in-progress`). Both times it was
found only because the guard *next to it* happened to be under
conversion. So I went looking.

## Measured

Across `core`/`engine`/`dashboard`/`cli`/`plugins`, excluding
`__tests__`/`*.test.*` and comment lines:

| | count |
| --- | ---: |
| hardcoded `moveTask` destinations in production | **51** |
| …passing `recoveryRehome: true` — **deliberate**, not defects | 22 |
| …plain, rejected on a board that does not declare the target | **29**
|

**The 22 must not be "fixed".** `moves.ts` exempts them on purpose
(#1411): a card stranded in an undeclared column has to stay rescuable
to a legacy safe-landing column, or it can never be recovered at all. A
sweep that converts them deletes the rescue path. That distinction is
the reason this is 29 and not 51, and it is why I measured before
writing.

## Why this got sharper recently

The `workflowHasColumn(workflowIr, toColumn)` rejection used to sit
inside a block gated on `isWorkflowColumnsCompatibilityFlagEnabled` — a
settings key **nothing in production writes** — so it never executed and
the legacy `VALID_TRANSITIONS` table decided instead. U12 hoisted it out
of that dead branch and it is now live, proven on a real store by
`live-move-path-undeclared-target.test.ts`:

```
moveTask(card in "todo" -> "triage")  now REJECTS: /Unknown column for this workflow/
```

That changed the failure mode of all 29 from *"silently lands the card
in an undeclared column"* to *"throws"*.

**29 is not a crash count.** Whether a throw surfaces or disappears
depends on whether the caller catches, which is per-site and I did
**not** measure it — the doc says so explicitly rather than letting the
number imply severity it hasn't earned.

## Fixed here: 9 of the 29

`duplicate-intake` and `duplicate-guard` both archive a duplicate. On a
renamed archive lane the move is rejected, so **the duplicate is never
archived and keeps sitting on the operator's board as live work** — and
in `duplicate-guard` the row has already been stamped
`deterministicDuplicateOf`, so it is *marked* a duplicate while
occupying an active lane. Half-applied, which is the same trap as
#2797's branch clear.

Both now resolve the `archived`-trait column from the task's own
workflow through one shared helper, unioned with the legacy id.

**`cli/commands/task-lifecycle`** — `finalizePullRequestMerge` and
`finalizeNoOpMergeTask` both move the card to a hardcoded `"done"`, and
both run `updateTask({ status: null, mergeRetries: 0 })` *first*. On a
rejection the merge has already landed and the bookkeeping is already
cleared while the card never reaches its complete lane: the operator
sees a merged branch, a card still sitting in review, and a reset retry
counter. Same half-applied shape as #2797's branch clear. Both now route
through one resolver so they cannot drift.

**`contamination` / `foreign-only-contamination` (×2) /
`restart-recovery-coordinator`** — four recovery requeues to a hardcoded
`"todo"`, none of them a `recoveryRehome` escape. On a board without
that column the move is rejected and **the recovery never completes** —
the card stays contaminated or stranded, which is precisely the state
these paths exist to clear.

**Consolidation.** `resolveReboundTargetForTask` and
`resolveArchiveTargetForTask` now live beside
`resolveTaskLifecycleColumns` in `workflow-lifecycle-traits`, already
the store-dependent resolution seam. My first pass put the archive
helper inside `duplicate-intake` and had `duplicate-guard` import it
from there — wrong home, and it would have grown a copy per caller as
more sites converted. Seven call sites now share two definitions.

**Plain (non-`recoveryRehome`) destinations: 29 → 21.**

**Coverage on the CLI pair is scoped, and I'd rather say so than imply
more:** the test covers the *resolver*, not the two call sites. Both
enclosing functions are private and reachable only through
`processPullRequest`, which needs a live GitHub surface — exporting them
purely to test wiring is a worse trade than stating what is covered.
Three cases: renamed lane resolves, no-workflow falls back to the legacy
id (which also pins that a default board is byte-identical), and a
throwing lookup falls back.

## Revert result (measured)

| conversion | reverted → |
| --- | --- |
| duplicate archive destination | new case fails — `moveTask` called
with `"archived"` on a board whose archive lane is `boxed` |
| CLI complete-lane resolver | replacing the body with a bare `return
"done"` fails the renamed case |
| both move-target resolvers | replacing either body with a bare return
of its legacy id fails 5 cases across the resolver suite and
`duplicate-guard` |

Each resolver has a **non-vacuous companion** asserting it does *not*
return the legacy id on a renamed board — without it, a resolver
returning any string would pass. The fallback cases are load-bearing
rather than padding: `resolveWorkflowIrForTask` degrades to the built-in
IR rather than throwing, and the built-in rebound/archive lanes *are*
`todo`/`archived`, so those cases also pin that a default board is
byte-identical.

The pre-existing case asserting the legacy `"archived"` passes both
ways, which is exactly why it could not detect this and why the new one
supplies a workflow.

## Ownership note

`packages/core` was `batch-core`'s territory and `packages/cli` was
`batch-cli-plugins`'. Both batches have landed, and this is
newly-discovered work in the class documented here rather than leftover
conversion backlog. Four sites, two shared helpers — happy for either
half to move if those owners would rather carry it.

## Verification

- `pnpm test:gate` — 161 + 487 + 13 + 71, green
- `duplicate-guard` + `duplicate-intake` — 40 passed
- `tsc` on core and engine — clean
- `pnpm lint`, `check:changesets`, census `--strict` — all clean (run
explicitly; a clean `pnpm lint` alone is not evidence the CI Lint check
passes)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Bug Fixes**
- Duplicate tasks are now archived to each workflow’s configured archive
lane.
- Completed tasks are moved to the workflow-specific completion lane,
with a safe fallback for older workflows.
- Recovery and requeue actions now use each workflow’s configured
rebound lane instead of assuming a fixed destination.

- **Documentation**
- Added guidance on avoiding failures caused by hardcoded workflow
destinations and incomplete lifecycle conversions.

- **Tests**
- Added coverage for renamed workflow lanes, fallback behavior,
duplicate archiving, and recovery destinations.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 12:25:00 -07:00
gsxdsm
ed0df8b0e2 evidence: the self-healing sweeps do not RUN on a renamed board — 49 hardcoded column QUERIES, and 17/30 fakes hide it (#2800)
**Evidence only — no conversions, no behaviour change.** One doc, one
test. It changes how the fleet should read the largest remaining file in
the backlog.

## The finding, measured on `origin/main`

`packages/engine/src/self-healing.ts` carries:

- **97** lifecycle-column comparisons the census counts, and
- **49** calls of the shape `this.store.listTasks({ column: "<literal>",
… })`.

`listTasks`' option is `column?: ColumnId` — **one literal column**,
applied as a filter in the store. On a workflow whose lanes are renamed,
every one of those 49 queries returns an **empty array**, so the sweep
it feeds does nothing at all.

**The self-healing sweeps are not
mostly-correct-with-some-unconverted-guards. They never execute.** The
`in-review` family alone is roughly half the calls: merge recovery,
wedged merges, branch rebind, pending-step reconciliation.

## Why this matters to the census specifically

```ts
const tasks = await this.store.listTasks({ column: "done", slim: true });
const candidates = tasks.filter((task) =>
  task.column === "done" &&        // <-- the census counts THIS
  …
);
```

The census scores the **comparison**, not the query. Converting it is a
legal-looking change that drops a count and changes **nothing an
operator can observe** — the loop body still never runs, because the
list was already empty.

Roughly **31** of self-healing's remaining comparisons are this shape.
Driving `self-healing.ts` to 0 would report the subsystem as converted
while it stays inert on custom boards. In this file the census total is
not merely a floor — it is actively misleading, and I'd rather the fleet
know that before someone spends a week on the 97.

## Why the existing suite cannot see it

Measured across `packages/engine/src/__tests__/self-healing*.test.ts`:

- **30** files define a `listTasks` on their store fake.
- **17** ignore the `column` option entirely.

```ts
// representative of the 17
listTasks: vi.fn(async (options?: { limit?: number; offset?: number }) => {
  const all = [...tasksById.values()];   // options.column is never read
  return all.slice(offset, offset + limit);
}),
```

The fake is **more permissive than production**. The sweep receives rows
the real query would have filtered out, so the test proves the sweep's
*logic* while saying nothing about whether the sweep is ever *reached*.
A green self-healing suite is not evidence that self-healing runs.

This is the mirror image of
`store-fake-defects-that-masquerade-as-production-bugs.md`: there a fake
is *missing* something production needs and the code looks broken; here
it supplies *more* and the gap looks fixed.

## About the test

It **pins a known defect** and is labelled as such in the file header —
it asserts what the engine does today, which is the wrong thing.

It asserts the **query argument**, not the outcome. The outcome is `0`
either way, so an outcome assertion cannot distinguish *"nothing to do"*
from *"asked the wrong question"*. Asserting the argument also avoids
standing up the git-evidence path these sweeps enter once they have
candidates.

- **Ratchet proven to fire:** repointing `reconcileDoneTaskIntegrity`'s
query at the renamed lane makes it fail — `1 failed | 2 passed`. A guard
that reports success without checking anything is worse than no guard,
so I ran it.
- **Guard on the guard:** a first case asserts the renamed fixture
really does resolve a complete lane that is not `done`. Without it,
every later assertion could pass vacuously if the fixture ever collapsed
to the default vocabulary.
- **Control case:** shows the ignoring fake hands back a row whose
column is `shipped` from a query that asked for `done` — the mechanism
by which the suite stays green.

When the query layer is fixed this test will fail, forcing an update.
That is the intent.

## What I did NOT do, and why

I did not fix it. `column?: ColumnId` takes one id, and the resolution
is circular at the query layer — you need a task to know its workflow,
and you are querying to find the tasks. A real fix is either a
multi-column query option (`columns?: readonly ColumnId[]`) plus a
resolved union across live workflow definitions, or dropping the filter
and post-filtering by role in the engine.

Either is a **behaviour change to a shared store API across 49 call
sites**. That is a coordinator-level decision, not something a
conversion PR should take unilaterally — the same reasoning that kept
membership predicates out of the census. I'd take it on if you want it;
it needs to be a deliberate call, not a side effect of a conversion
sweep.

## Verification

- `pnpm test:gate` — 161 + 487 + 13 + 71, green
- `npx tsc -p packages/engine/tsconfig.json --noEmit` — clean
- `pnpm lint` — clean
2026-07-30 11:15:43 -07:00
gsxdsm
9b573268a4 docs(u9): audit every non-scheduler execute() call site — exactly ONE ignores pause (#2747)
## What this closes

`executor-prompt.test.ts` has 3 failures asserting that `execute()`
refuses to dispatch a user-paused card during a global pause. The guard
lives in the **scheduler** (`scheduler.ts:1515`, a hard stop that never
reaches `.execute(`), so those tests call a layer that has never
enforced it.

I flagged this in #2719 without being able to say how large the real gap
was. This answers that — and the answer is much narrower than "execute()
is unguarded".

## Every non-scheduler call site

Enclosing method and guard status resolved **programmatically**, not by
reading nearby lines:

| Site | Enclosing method | Guarded? | Reading |
|---|---|---|---|
| `executor.ts:3333` | `dispatchUnpauseResume()` | no | **Correct
as-is** — it *is* the unpause path; a guard here is self-contradictory |
| `executor.ts:3495` | `constructor()` — `task:moved` sub, `to ===
"in-progress"` | **no** | **The one real gap** |
| `executor.ts:5702` | `resumeTaskForAgent()` | yes | `globalPause \|\|
enginePaused` + `!task.paused` |
| `executor.ts:5858` | `resumeOrphaned()` | yes | same guard earlier in
the method |
| `in-process-runtime.ts:2255` | `drainWorkflowContinuations()` |
indirect | gated by `status !== "active"`; engine pause is expected to
leave the runtime non-active |

**Exactly one path can reach `execute()` without consulting pause
state.** So the open question is not *"does `execute()` need a guard"*
but *"can a `task:moved` → `in-progress` event fire while paused"* —
narrow, and answerable by whoever owns the pause contract.

## A measurement correction worth recording

My first pass used a 40-line window above each call and **mis-attributed
two sites**: `:5702` and `:5858` looked unguarded because their guard
sits earlier in the same method, above the window. Resolving the
enclosing method properly flipped both to guarded and cut the apparent
gap from three sites to one.

That is the difference between reporting "3 of 5 paths ignore pause" and
the truth. A proximity heuristic is not an enclosing-scope analysis.

## Still not decided, deliberately

Three options, and they are not equivalent:

1. **Guard the `task:moved` subscription** — narrowest; keeps the
scheduler as the single pause authority. Does *not* make the three tests
pass, since they call `execute()` directly.
2. **Give `execute()` its own guard** — makes the tests pass, but must
not refuse the legitimate internal re-dispatch paths.
`dispatchUnpauseResume()` would break outright: it exists to resume a
card the operator just unpaused.
3. **Retire the three direct-`execute()` assertions**, covering the
invariant at the scheduler layer where it is enforced.

(2) and (3) both touch coverage of **user pause** — a safeguard this
program re-ratified and told workers not to narrow. Choosing either
silently inside a test-repair PR is how a safeguard gets weakened by
accident.

## What is NOT verified

Whether that subscription is actually **reachable** while paused.
Proving it needs a trace of who emits `task:moved` with `to ===
"in-progress"` under a global pause; if every emitter is itself gated,
the gap is theoretical. That trace is the remaining work before
preferring option 1 over the status quo.

Docs-only — no source, no tests. Lint clean.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Documentation**
* Added an audit documenting workflow pause behavior and identifying an
event-driven execution path that can create sessions during a global
pause.
* Recorded findings from existing test failures and reviewed available
enforcement points for pause handling.
* Documented trade-offs and the remaining decision on where pause guards
should be applied.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-30 08:51:32 -07:00
gsxdsm
245086dad6 docs: the census total is a floor — 25 membership predicates it structurally cannot see, one a live defect (#2763)
Docs only, extending the entry #2748 landed. Opening it because the
fleet reads the census total as its completion bar, and that total
excludes a whole predicate class — a measurement that should not live in
a chat reply.

## Measured on `origin/main`

- **47** array/Set literals of two or more lifecycle ids, in 35 files.
- **25 are membership predicates against a task's column** —
`SET.has(task.column)` / `ARRAY.includes(task.column)` — in 19 files.
Two are documented fallbacks behind a resolved primary, so **~23 are
unconverted guards**.
- The census scans `===` / `!==` against a column. **None of these is a
comparison, so none is counted.**

| file | constant |
| --- | --- |
| `cli/src/commands/task.ts` (3) | `retryReviewColumns` |
| `dashboard/app/components/TaskCard.tsx` (2) | `TIME_INDICATOR_COLUMNS`
|
| `engine/src/eval-followups.ts` (2) | `OPEN_COLUMNS` |
| `engine/src/merger.ts` (2) | `sourceTerminal` |
| `engine/src/task-revert.ts` (2) | `REVERTABLE_COLUMNS` |
| `core/src/agent-role-policy.ts` (1) | `IMPLEMENTATION_TASK_COLUMNS` |

## One is a proven live defect

`isImplementationTask` is
`IMPLEMENTATION_TASK_COLUMNS.has(task.column)`, and
`evaluateImplementationTaskBind` short-circuits to `allowed: true` when
it returns false. **On a renamed board every agent is bind-compatible
with every task** — the role check that stops a liaison being handed
implementation work (the NEXT-871 loop FN-7851 fixed) does not apply.

It surfaced only because a reviewer questioned a coverage claim in one
of my dispatch tests (#2739). Passing an agent wasn't proof the
evaluator ran, so I asserted a `custom`-role agent must be *refused* —
and that test failed against production. Flagged at the site in #2739,
not fixed: `isImplementationTask` is a sync pure predicate with no
store, and making the routing policy async is a behaviour change to
agent admission.

## What this does and does not argue

The census is the right instrument — AST-based, honest about what it
measures, and it has caught real drift in both directions (it failed on
me in #2724 when merged conversions moved an inventory *down*). This is
not an argument against it.

It is an argument against reading **"backlog: N" as "N guards remain"**.
The same shape already appeared in the archived gate (#2724), where the
rule is additionally encoded in Drizzle predicates and raw `sql`
templates that no comparison scan can see. Two independent classes now,
found the same way — by looking at what the instrument's definition
excludes.

**Extending the census to count membership predicates is deliberately
left to you, not done here.** It would move every worker's number
mid-fleet, and deciding which sets are lifecycle guards versus
board-config definitions or type unions is exactly the judgement
`DELIBERATE-LITERAL` exists for — 47 collections would each need that
call.

## Verification

`pnpm lint` clean · census `--strict` exits 0 · no code changes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 08:12:51 -07:00
gsxdsm
1322a1bb11 docs(solutions): the optional-flags seam kept four green suites blind to their own conversion — and why I did not ship a ratchet for it (#2748)
Docs only. No code, no census movement.

## The finding, measured

Four consecutive files in this program had **fully green suites at
conversion time** that could not have detected the conversion — correct
or broken:

| file | pre-existing cases blind to the change |
| --- | --- |
| `github-tracking-reconciler.ts` | **33** (fake store had no workflow
reader) |
| `TaskReviewTab.tsx` | **45** (`columnFlags` omitted everywhere) |
| `plan-approval-hold-invariant` drain | **25** (`opts.lifecycle`
omitted everywhere) |
| `task-age-staleness.ts` | **12** (`context.lifecycle` omitted
everywhere) |

The cause is structural. Every conversion here uses the same seam — the
caller passes resolved flags, the helper falls back to the legacy id
when they are absent — and every pre-existing test omits the flags. So
the suite passes **before** the conversion, **after a correct one**, and
**after a wrong one**, as long as the fallback is intact. "The suite is
green" carries no information about the change.

I reported this observation four times in PR bodies. Restating it a
fifth time is worth less than writing it where the next worker will
actually find it.

## It also corrects the obvious test

The natural property is "hold the traits fixed, change the id, behaviour
is identical". That is only half the invariant. It does not catch:

```ts
// Not a fallback — an OVERRIDE. The id wins even when traits disagree.
return column === "in-review" || flags?.mergeBlocker === true;
```

Renaming `in-review` → `checking` leaves that correct, because the trait
arm answers. The defect appears in the **converse** direction — a column
that still *carries* a lifecycle name while its traits say otherwise,
which is what you get by repurposing a default column rather than
renaming one. That is the direction that found a live **"Merge & Close"
offered on a mid-implementation card** in #2718.

## Why this is not a ratchet — a negative result, recorded

I tried to automate it, and I am shipping the reason it failed rather
than a guard I do not trust.

The **consumer scan is sound**: AST-based, 31 files, 66 role-helper call
sites. The **coverage half is not**. The renamed ids this program uses —
`building`, `checking`, `converted`, `published`, `backlog` — are
ordinary English words that appear in unrelated test prose, and a test
merely *importing* the module under test does not prove it exercises the
role path. My scan reported `TaskCard.tsx` as covered by
`Column.test.tsx` on a **filename coincidence**.

A guard built on that reports coverage that does not exist, which is
worse than no guard, so it is not shipped.

A sound alternative — pin the consumer set and make each new file
declare its status — was also rejected: a 31-entry status inventory
would conflict with every concurrent fleet PR that adds coverage. That
is the same churn already removed from the census baseline by dropping
its derived aggregates.

The attempt is written down so the next person does not repeat it from
scratch, and the requirement lives as a review criterion until someone
finds a sound signal.

## What it asks for

1. **A flags-supplying case** — if every case omits the new parameter,
the conversion is untested in both directions.
2. **Both directions where both are reachable** — renamed lane, and
repurposed column.
3. **A non-vacuous companion** — assert what the widened predicate must
still *exclude*, or a predicate matching every column satisfies your new
cases. (Both `TaskReviewTab` and the dispatch filters needed this.)
4. **Run the revert and record the failure text.** Twice in this program
a new case passed with the change reverted: once because the branch was
gated behind an unwired handler (`refine` needs `onOpenRefine`), once
because the hook was dispatched by trait and the test IR did not declare
that trait, so it never ran at all.

Cross-linked both ways with the adjacent `store-fake-defects` entry,
with the distinction stated so the two are not confused: **there** a
fake is missing a method so a branch never runs and production looks
wrong; **here** the fake is complete and the test is correct, but a
parameter is absent so production takes its documented fallback.

## Verification

`pnpm lint` clean · census `--strict` exits 0 (unmoved — this PR changes
no code).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 07:11:22 -07:00
gsxdsm
11aba0394e docs(solutions): converting a column literal to a role makes it async — the four forms that ship green (#2710)
Four review rounds across `TaskCard.tsx` and `TaskDetailModal.tsx` each
found a **real defect**. None was in the conversion itself — every one
came from the same property change. The fleet has ~600 guards left to
convert against the same helpers, so this is written down rather than
left in four commit messages.

## The property that changes

```ts
task.column === "in-progress"   // stable for the lifetime of the render tree
isWipColumn                     // derived from fetched trait flags — CHANGES after first paint
```

Column trait flags arrive from a board-workflows fetch. Until it lands
they are `undefined` and every role helper falls back to the legacy id.
So a converted role is `false`, then `true`, within one mounted
component.

## The four forms

| | form | symptom |
|---|---|---|
| 1 | **stale memo** — deps still keyed only on `task.column` | timers,
labels, completion dates frozen at first-paint values (4 instances in
TaskCard) |
| 2 | **frozen `useState` initializer** | the section does not start
collapsed — it *appears later, already collapsed*, on a card nobody
touched |
| 3 | **eager action on a guess** — effect mutates state before flags
resolve | a tab opens and instantly bounces; the correction never lands
because the action destroyed the state it would have corrected |
| 4 | **stale identity** — flags resolved, but for the *previous* entity
| roles resolve from another task's workflow: confidently wrong rather
than merely stale |

**Form 4 defeats the obvious fix for form 3.** A `metadata === null`
guard asks whether data *loaded*, not whether it describes the entity
currently open — and it only appears in components that stay **mounted
across entity changes**, which is why TaskCard never showed it and the
modal did.

## Why a doc rather than four commit messages

All four ship **green**: types pass, existing tests pass, and the
**default board behaves identically** — because on the default lineage
the legacy fallback and the resolved role agree. They diverge only on a
**renamed board**, which is precisely the case the conversion exists to
support.

So the failure mode is: census count reaches zero, everything is green,
and the feature is broken exactly where the programme was meant to fix
it. A reviewer catching these one at a time is the expensive path, and
it has now cost four rounds on two files.

Also relevant: **this repo has no `react-hooks/exhaustive-deps` rule**,
so form 1 has no automated backstop at all.

## Contents

A checklist a converter can run against a component file, and the
concrete fix shape for each form — including tagging fetched metadata
with the id it describes, and applying that guard to the **role
bindings** rather than only the effects (reordering effects fixes the
call sites you noticed and leaves the bindings stale for everything
else).

Follows the convention already established by the engine-side scoping
note in `architecture-patterns/fleet-self-healing-cluster-scoping.md`,
which records the equivalent hazard for sync workflow reads.

## Verification

`pnpm test:gate` green (10 / 158 / 487 / 71). `pnpm
check:lifecycle-columns` exits 0. `pnpm lint` clean.

Docs-only; no changeset.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 04:37:19 -07:00
gsxdsm
bb3bdab999 The ratchet follows the count down — a drop tightens instead of reddening the gate (coordinator item 2) (#2679)
Taken after asking twice for reassignment with no reply, and after the
same failure bit a **third** time. No open PR touches the census CLI, so
this is unowned in practice — **U12, say so if you have started and I
will close this in favour of yours.**

## What changed

A **drop** now tightens the baseline instead of failing. Failing hard
was defensible in isolation — a stale allowance is a hole, since those
guards can return up to the old count while the check stays green. What
it missed:

**The drop is almost never the failing author's to fix.** Eleven files
dropped during one merge wave, none of those PRs re-recorded, and none
of their authors did anything wrong. Measured three times since CI began
gating this: `columnRoles.ts` 0 → 1, then `executor.ts` twice.

A permanently-red gate is a bigger hole than a stale allowance, because
it gets ignored and then nothing is guarded at all. **The rise check —
the ratchet's actual purpose — is untouched and still fails hard.**

## The residual, named rather than glossed

In CI the write is discarded with the runner, so the committed baseline
stays stale until someone commits a tightened one. The exposure is
bounded (regrowth only up to the old count), printed on every run, and
strictly smaller than the exposure from a check people route around.
`--strict --exact` restores hard failure for the pinned end state.

**One writer:** the write is now a named `writeBaseline()` shared by the
tighten path and `--update-baseline`, rather than a second
`writeFileSync`. Two writers for one artifact is how they drift — a
lesson this file already learned once.

## Exercised end to end

| scenario | result |
|---|---|
| drop, `--strict` | exit **0**, `TIGHTENED`, allowance rewritten 9 → 6
|
| drop, `--strict --exact` | exit **1**, baseline untouched |
| rise, `--strict` | exit **1** |
| clean | exit **0** |

Pinned through the real CLI with an isolated baseline. Revert proof:
restoring the hard failure fails **1 of 32**.

## Two of my own mistakes, recorded

**A vacuous assertion, in the case that guards against vacuity.** I
first wrote `expect(allowedAfter).toBeLessThan(4 + allowedAfter)` — true
for every number. Replaced with a comparison against the inflated value
the fixture started from. This file documents that trap repeatedly and I
still walked into it, which is the argument for the mechanical revert
check over careful reading.

**The env override is `FUSION_CENSUS_BASELINE_PATH`**, not the
`FUSION_CENSUS_BASELINE` I used in the first draft — so the first
version of these cases silently ran against the **real** baseline and
passed for the wrong reason. A test whose fixture never took effect is
the same failure as a test whose fixture can't fail.

## Verification

32/32 census suites, `pnpm test:gate` **71/71**, `--strict` exits 0,
`pnpm lint` clean, `docs/testing.md` updated.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---

## Update — the base-ref ratchet (review round 2, commit `4895845579`)

The first version of this PR shipped a **named residual**: the
tightening write dies with the CI runner, so the committed allowance
stays high and a later PR can regrow guards up to it while `--strict`
prints green. I called the exposure bounded and moved on. Greptile
flagged it P1 and was right — naming a hole is not closing one.

`--strict` now stops trusting the committed number for files the branch
touched. It measures each **changed** file at the base commit
(`FUSION_CENSUS_BASE_REF`, else the PR base branch, else `origin/main`)
and fails if the file carries more guards than the base ref has. **The
enforced ceiling is what main has today**, so a stale, missing, or
long-unrecorded baseline no longer opens a window.

| decision | why |
|---|---|
| changed files only, `<ref>...HEAD` | untouched files have main's
counts by construction; censusing all ~400 at the base ref is ~400 `git
show` calls to re-derive numbers that cannot have moved. Three-dot also
stops charging this branch for guards that landed on main after the
fork. |
| a new file's base allowance is **0** | "absent at the base ref" as
unbounded would make a new file the cheapest place to hide a fresh guard
|
| fails **open** on an unresolvable ref, printing `SKIPPED` | a shallow
clone cannot produce an honest comparison; a degraded run must not read
as a clean one. The baseline comparison still applies. |
| merged into the existing `regressions` list | one failure per file,
and `--update-baseline` keeps working as the deliberate escape hatch. No
new exit path. |

**Revert proof, measured both ways.** With the base-ref block removed,
the regrowth fixture — base commit 2 guards, HEAD 5, baseline allowing 9
— exits **0** with `TIGHTENED`, which is precisely the reported
scenario. With it: exit **1**, `column-guard count ROSE`, `above its
count on the base ref`, baseline left at 9. **3 of the 4** end-to-end
cases go red on revert. The fourth passes without the fix by design — it
is the genuine-conversion case the auto-tighten exists to keep green,
and a case that reddens either way proves nothing.

The end-to-end suite builds a throwaway two-commit `git init` repo under
the temp dir, because this exploit is a property of the **plumbing**,
not of the comparison: resolving a ref, working out the changed set,
reading base source through `git show`. The comparator itself is pure
with the reader injected (`findRegrowthAgainstBase`), with its own cases
in `lifecycle-column-census-ast.test.ts` — including the one that would
silently pass everything, looking up the wrong key in
`summarize().byFile`.

**Rebased onto `origin/main` @ bc782d8d92** (the branch was forked
before the recent merge wave; its baseline read 746 against a tree of
722).

Verification on the rebased branch: census **722** / `--strict` exit 0 ·
**70/70** across both census suites · `pnpm test:gate` **71/71** · `pnpm
lint` clean.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 02:56:08 -07:00
gsxdsm
ae23be79f7 fleet: scheduler.ts 28 → triaged (NOT converted) + repo-wide reachability measurement — the work order sorts on a number that doesn't predict convertibility (#2687)
## Claim

`packages/engine/src/scheduler.ts` — the largest **unclaimed** cluster
(28). Triaged, **not converted**, for the reason below. Census
unchanged: **722 → 722**. No baseline movement is claimed, because
nothing was converted.

## Why not converted

Three of us independently hit the same wall on our first file — #2683
and #2684 (`self-healing.ts`), #2685 (helper coverage). This measures
the whole backlog **once** so the remaining workers don't each pay that
cost.

Two constraints gate conversion. Neither is visible in the per-file
counts the work order sorts on.

### 1. Location — the helpers aren't importable from 80% of the backlog

`isIntakeColumnRole` / `isPreImplementationColumnRole` /
`isHoldColumnRole` live in
`packages/dashboard/app/utils/columnRoles.ts`, a dashboard-**app**
module.

| Location | Guards | Share | Importable? |
|---|---:|---:|---|
| `packages/engine/**` | 316 | 43% | no |
| `packages/dashboard/app/**` | 150 | 20% | **yes** |
| `packages/core/**` | 148 | 20% | no |
| `packages/dashboard/src/**` | 78 | 10% | no |
| `packages/cli/**` | 24 | 3% | no |
| plugins | 6 | 1% | no |

**150 of 722 (20%)** can call them at all. Widening the helper *set*
(#2685, correctly) does not move this number — it's the module's
location, not its coverage. Core already exports `resolveColumnFlags`,
so a core-side predicate module would be *the same* abstraction made
reachable, not a new one. It is a prerequisite for the other 80% and
**not sufficient** — see below.

### 2. Flag scope — the binding constraint

A role predicate needs resolved trait flags. Most guards run in
functions handed a bare task row with no IR to resolve from. Threading
one in changes a signature and its call graph: a **behavior change, out
of scope**.

File-level proxy over the 572 non-dashboard guards: **339** in files
that reference an IR/flags resolver, **233** in files with none.

**That proxy overstates convertibility, and the overstatement is the
finding.** Reachability varies *within* one file, so a file-level
verdict is unusable. In my claimed cluster:

| Site | Context | Convertible? |
|---|---|---|
| `scheduler.ts:1690` | `resolveWorkflowIrById(...)` +
`resolveColumnFlags(c)` in the same block | **yes** |
| `scheduler.ts:231` | `isLegacyDependencySatisfied(dep: Task \|
undefined)` | no — task only |
| `scheduler.ts:341` | `shouldHoldActiveFileScopeLease(...)` | no — task
only |

So "convert the file" is not a unit of work that exists in this backlog,
and the rule *"the baseline must shrink by exactly your converted
count"* cannot be satisfied per-file until the count is per-site.

## A guard that must be skipped, not guessed

`packages/core/src/task-merge.ts:254`:

```ts
if (!options.skipColumnIdentityCheck && task.column !== "in-review") {
```

The parameter is `Pick<Task, "column" | "paused" | ...>` — no IR,
deliberately. The in-source FNXC comment records that callers who *have*
resolved the `merge-blocker` trait pass `skipColumnIdentityCheck` rather
than spoofing `{ ...task, column: "in-review" }`. The trait-aware path
already exists *beside* this literal.

Converting it wouldn't remove a legacy id — it would delete the fallback
the option was introduced to make explicit. **Flagged and skipped.**

## Suggested census upgrade (not done here)

Emit per-site whether trait flags are resolvable in the enclosing scope.
That turns the work order from "largest file" into "largest
**convertible** cluster" and makes baseline shrinkage predictable. I did
not touch `scripts/lifecycle-column-census.mjs` — it is the shared
instrument and changing it unannounced would invalidate everyone's
in-flight before/after numbers.

## Method correction worth propagating to every fleet worker

Claim-collision scans must compare a branch to its **merge base**, not
to `origin/main`. `git diff origin/main origin/<branch> -- <file>`
reports a difference when the branch is merely *stale* (the file didn't
exist at its base) — it flagged dashboard and CLI PRs as touching engine
E2E files. I nearly skipped a free cluster on that false signal.

`feature/code-organization-wave17` is excluded from collision checks:
**1556 files, 150 commits behind main, already `DIRTY`**. It must rebase
wholesale regardless, and counting it as a claim marks *every* cluster
in the backlog as taken.

Docs-only — no source, no test, no census change.
2026-07-30 02:53:18 -07:00
gsxdsm
bb30d37e59 fleet: self-healing.ts 110 → scoped (NOT converted) — sync workflow reads make this cluster unsafe to batch (#2683)
Claiming the largest unclaimed cluster per the work order, then
**handing it back sized rather than half-converted.** Docs only; census
unchanged (722 / triage 0).

## The cluster

`packages/engine/src/self-healing.ts` — **110 guards**, largest single
file in the order.

```
by column:   in-review 48 · in-progress 20 · done 17 · todo 13 · archived 12
by receiver: column 100 · to 7 · from 3
```

## Why the mechanical conversion is unsafe here

**The engine has no synchronous way to learn a task's workflow.**
`resolveTaskWorkflowIrSync` returns the DEFAULT IR for every task in
production — `getTaskWorkflowSelection` returns `undefined`
unconditionally (a PG-cutover stub), so the reader always takes its
`!workflowId` branch. It is typed non-optional, so **no caller can
detect the substitution.**

A conversion routed through it: compiles, reads better than the literal,
**counts as census progress**, and is wrong for every custom workflow,
silently. That is strictly worse than leaving the literal — the literal
is at least honest about being one. It is the "guard that cannot fire"
pattern wearing better clothes, and the ratchet would score it as a win.

The correct form uses `resolveTaskLifecycleColumns(store, taskId)`
(async, store-aware), which needs resolved lanes **in scope per
method**. Sampled sites (926, 932, 984) do sit in `async` methods so it
is reachable — but that is a per-sweep restructuring, not a per-line
substitution, and these sweeps iterate task lists, so a naive per-task
resolve turns one sweep into N store reads.

**In-tree precedent:** `triage.ts` `discoverReadyPlanningTasks` solved
this exact problem — store-free `couldBeCandidate` prefilter, bounded
(8) concurrent resolve over the survivors, decision stays synchronous
over a resolved map. Any batch here should follow that shape per sweep.

## Recommended split, by SWEEP not by column

110 sites cannot honour *"census before/after, baseline shrinks by
exactly the converted count"* while also restructuring six-plus sweeps
in one PR.

1. **the review/merge sweeps** (`in-review` 48) — largest, and the one
where a wrong lane silently changes **merge eligibility**. First and
alone.
2. **WIP/rebound sweeps** (`in-progress` 20, `todo` 13).
3. **terminal sweeps** (`done` 17, `archived` 12) — read
`complete`/`archived`; most mechanical of the three.
4. **the 10 `from`/`to` sites** — these are MOVE-transition arms, not
task-column reads. Different question (*"is this transition into a
review lane?"*), so they must not ride along with the `task.column`
work.

## Why I am not doing item 1 myself

I am near the end of a long session — this is the same context in which
I produced a confidently-wrong structural finding earlier today
(retracted in #2667, where I trusted a hand-rolled brace counter over a
comment in the file). A 48-site restructuring of the merge-eligibility
sweeps is exactly the work that should not be done by a worker in that
state, and the fleet rules' *flag-and-skip* discipline is the right call
over guessing.

**What a fresh worker gets from this PR:** the site census, the
async-scope survey, the hazard with its root cause, the in-tree pattern
to copy, and a four-way split with the risky piece isolated. That is the
expensive part of the job already done.

## Fleet rule this cluster proves, worth adding to the brief

**Never resolve a workflow synchronously in a converted guard.** Use
`resolveWorkflowIrForTaskWithProvenance` (branch on `source`) or
`resolveTaskLifecycleColumns`; if neither is reachable at the site, flag
and skip.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 02:44:27 -07:00
gsxdsm
13bf7e001d Closing-bar verification pass on origin/main — one tree, one report (+ the E2E red it found) (#2660)
**Closing-bar item 4, run on one clean tree at `origin/main`
(`be63e72f1`).** Nobody was assigned this and my own work is merged, so
I took it.

> **This PR is now REPORT-ONLY — net zero file changes.** I found the
planning-lane E2E red, fixed it, then discovered **#2658 (gsxdsm) makes
byte-for-byte the same change** to the same helper and was opened first.
I reverted mine rather than leave two identical edits to one function to
conflict. **The E2E result below depends on #2658 landing** — on
`origin/main` without it, that suite is 2 failed / 5 passed.
>
> The duplication is worth one note for the fleet: two workers
independently hit the same control-card failure and independently traced
it to FN-7648's unplanned-seed gate plus a fixture that never wrote a
spec. Independent confirmation of the diagnosis, but also ~an hour spent
twice — the census-style work order exists to stop exactly that, and E2E
fixture defects are not on it.

## Report — all four, one tree

| Check | Result |
|---|---|
| `pnpm test:gate` | **PASS** (132 + 10 + 487 + 71 tests) |
| `pnpm verify:fast` | **PASS** — 13 steps green in 89.9s, boot smoke
`GET /api/health 200`, clean shutdown |
| E2E families | **13 files / 109 tests PASS** — *after* the fix below;
**2 failed** before it |
| census | total **787**, triage **10** |

## Two corrections to the bar itself

**1. It is not "all-8 E2E" any more — there are 13 families.** The suite
grew while the bar was being written:

```
agent-count · agent-link · lease-rebound · lifecycle · merge-family · merge-rebound
merge-safeguards · merged-board · planner-lane · planner-lane-resolution
planning-lane · rebound-family · stranded-column
```

A verification pass scoped to 8 would have skipped 5 families —
including the one that was red. Worth fixing the number in the bar so
the final pass globs rather than counts.

**2. `DELIBERATE-LITERAL (reviewed)` reads 3, and I chased it —
RESOLVED, no gap.** I flagged the drop from an earlier "7" as a possible
fleet-safety hole. It is not one. Reconciled against `--json byFile`:

| File | markers | counted `deliberate` | counted `column` |
|---|---|---|---|
| `hold-release.ts` | 2 | **2** | **0** |
| `live-agent-count.ts` | 1 | **1** | 6 |
| `replan-target.ts` | 2 | 0 | 4 |

`deliberate: 3` = hold-release 2 + live-agent-count 1, which is exactly
the set of marker-covered **comparisons**. `replan-target.ts`'s two
markers sit above `return "triage"` **return-value** literals, not
comparisons — the census correctly does not count those as guards at
all, so they are neither `deliberate` nor `column`. The earlier "7" was
simply a different tree state before conversions landed; I was quoting a
stale number.

Worth noting the marker matcher is already hardened for the subtle case:
`hasDeliberateMarker` walks every **ancestor** rather than the enclosing
statement, because the real markers sit above the enclosing *function*
while the comparison is a `return` inside it — a statement-only lookup
"silently reclassified three reviewed literals as backlog". That is the
guard-cannot-fire pattern, already caught and fixed by whoever wrote the
AST version.

**Consequence for the fleet: the census's categories are trustworthy
as-is.** No pre-launch action needed on this.

## The red it found

`workflow-planning-lane-live-e2e.pg.test.ts` — **2 failed / 5 passed**,
including its own **control** case:

```
releases an ordinary held card on a default board (the control)
  → AssertionError: expected [] to include 'FN-OK'
```

`seedHeldTask` never wrote a `PROMPT.md`, so task creation's bootstrap
seed stood, and FN-7648's `isUnplannedForExecution` correctly refused to
release an unspecified card. **The sweep was right; the fixture was
asking it to release a card that had never been specified.**

**This is the second instance of the identical defect** — same cause and
same fix as `workflow-lifecycle-live-e2e`'s `seedTask` in #2634. This
suite was written after that fix and did not inherit it. The graph-entry
contract doc already states the rule: *"Scheduler/release test fixtures
must model a card that cleared the gate ... A held unreviewed card is
the gate working."*

Both failures had one cause — the mid-sweep approval-park case was
downstream of the control never releasing. **5 → 7 passed**, and cards
that are *supposed* to be held still are, held by their own
status/marker, which is what those cases assert.

Given it has now happened twice, a shared `seedPlannedTask` helper in
the E2E fixture module would prevent a third. I did not add one here: it
touches suites owned by U7 and U11 mid-consolidation, and this PR should
stay the verification pass plus its one finding.

## Bar status after this

- **gate / verify:fast / E2E** — green on one tree, with this commit.
- **triage → 0** — still **10**, all in U12's `moves.ts` (4) and
`register-task-workflow-routes.ts` (1) per file scan; flag resolution in
flight.
- **ratchet tightened (item 2)** — not done, U12's.
- Once triage hits 0 and the ratchet lands, re-running this exact pass
is a ~4-minute job and I can produce the final report.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 01:38:29 -07:00
gsxdsm
dca20496f4 consolidate/u7: plugins to zero + 8 executor rebound guards + resume lanes (supersedes #2607, #2635, #2640) (#2644)
Consolidation branch for U7, per the new one-branch working mode.
**Supersedes #2607, #2635, #2640** — the three of my PRs that were stuck
on review threads. My other seven (#2602, #2605, #2606, #2611, #2621,
#2628, #2633) are green with **zero unresolved threads** and are
deliberately left alone for the merge sweep.

## What is in here, file by file

| file | change | guards before → after |
|---|---|---|
| `plugins/…/glasses/src/agent-actions.ts` | gates, destinations and
degraded-resolution refusal all resolve from the task's own workflow | 2
→ 0 |
| `plugins/…/glasses/src/quick-capture.ts` | accepted capture columns
come from the board; default no longer names the deleted column | 1 → 0
|
| `plugins/…/glasses/src/settings.ts` | quick-capture default was
`triage`, the column #2515 removed | (assignment, uncounted) |
| `plugins/…/dependency-graph/src/GraphTaskNode.tsx` | redundant column
condition deleted | 1 → 0 |
| `packages/engine/src/executor.ts` | 8 rebound guards compare the
resolved column; 4 resume-eligibility literals share one resolver | 151
→ 143 (+4 off-bar) |
| `packages/engine/src/__tests__/` | 4 new suites, 26 cases | — |

`plugins/` reaches **zero** column guards with this branch.

## The three threads it closes

**#2607 — five findings, all mine, all the same rule.** I kept
*qualifying* a legacy-id fallback instead of removing it:

| attempt | rule | hole review found |
|---|---|---|
| 1 | fall back to `todo` when the role is missing | moved cards to
phantom columns |
| 2 | …only if the workflow **declares** `todo` | aliased **review**
lane named `todo` |
| 3 | …and only if no other role is assigned to it | **traitless**
parking column named `todo` |

The qualifications were the mistake. Once `resolveLanes` returns a lane
set the workflow *has* a column vocabulary, so "no column carries the
hold trait" is a complete answer — refuse. `destination()` is two lines
now, with no aliasing surface left to qualify.

Plus a sixth, which is a genuinely different state: **degraded
resolution is indistinguishable from the default board.**
`resolveWorkflowIrForTask` is total by design — a missing definition
silently returns the *default* coding IR — so a card on a custom board
whose definition could not be read resolved to `todo`/`in-progress`.
`undefined` lanes cannot express that (it means "no workflow at all",
where the legacy ids *are* the answer). The actions now refuse with 409.
#2618 would replace this check with resolver provenance; it is not
merged, so this does not depend on it.

**#2635 — "seven rebound sites remain untested."** Fair; my "same shape"
note was an assertion, not coverage. Seven of the eight need a live
graph run to reach, so the *shape* is pinned instead: a static check
that no guard in front of a rebound move compares against a column
literal, with a vacuity case (the same detection run against the
original shape) and a match-count floor (≥8), because a guard reporting
success on zero matches is worse than no guard.

**#2640 — duplicate workflow resolution.** Framed as I/O; it is also a
correctness bug. Eligibility and re-entry are two halves of one decision
and resolved the workflow separately, so a workflow edit landing between
them has the halves reading *different boards*. Now one caller-owned
memo per decision — caller-owned because a process-lifetime cache would
have to guess when a mid-flight workflow edit invalidates it.

## Behavioural findings, not tidying

- **The last-resort recovery for completed-but-stranded work did not
exist off the default lineage.** `promotedFromPlannerColumn` was false
on a renamed board, so finished work resting in planning was never
promoted; the code fell through to a review handoff that role adjacency
rejects, and the card stayed stuck with its work complete.
- **Rebound guards could not see the column their own move targeted.**
U5b converted the move target; the eight `column !== "todo"` checks in
front of it were left literal, so on a renamed board the engine moved a
card into the column it was already in — and `moveTaskInternal` runs
reset-on-entry on every real move, so at the `preserveProgress: false`
site it reset step progress a second time.
- **The FN-1404 `task:move` audit row was lying**, recording `to:
"todo"` while the move target was resolved. A run-audit trail that
disagrees with the move it describes is worse than none. Not a
comparison, so no census counts it.
- **A task interrupted by an engine pause never resumed on a renamed
board** (off-bar, `in-review`/`in-progress` literals): four comparisons
decided one question and had to agree; two of them disagreed on a
renamed board, so re-entry silently never fired.

## Revert proofs, isolated per site

| reverted | result |
|---|---|
| `destination()` back to attempt 3 | 3 of 38 fail |
| degraded-resolution refusals removed | 2 of 42 fail |
| capture set back to the legacy five | 2 of 3 fail (renamed-board
suite) |
| forward exclusions → literals | 1 of 14 fails |
| missing-wip refusal removed | 2 of 14 fail |
| `promotedFromPlannerColumn` → literals | 3 of 7 fail |
| promotion target → `"in-progress"` | 3 of 7 fail |
| one rebound guard → `!== "todo"` | 1 of 3 fails (static shape) |
| resume lanes → legacy trio | 1 of 5 fails |

Every conversion is paired with a negative — a forward move, a
not-a-planner-lane card, a default-lineage card, an unresolvable
workflow — so neither "always fire" nor "never fire" can pass for
"resolve the role".

## Commit discipline

Twelve commits, each one thing: the code move (`resolvePlannerLanes` out
of `triage.ts`) is separate from every behavior change, and each review
fix is its own commit with its own revert proof.

## Verification

- `pnpm test:gate` **71/71**
- 162/162 across the glasses plugin's 19 files; 26/26 across the four
new engine suites
- engine + glasses typecheck clean; `pnpm lint` clean

🤖 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**
* Engine recovery and retries now work correctly with renamed or
customized workflow columns.
  * Tasks in manual-intake columns are no longer automatically planned.
* Agent actions and quick capture now respect each board’s declared
columns and lifecycle stages.
* Awaiting-approval tasks are recognized regardless of their current
column.
* Command Center SDLC funnel stages now accurately reflect customized
workflows.

* **Documentation**
* Added guidance for safely changing workflow-column logic and
interpreting lifecycle-column checks.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 00:52:55 -07:00