## 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.
6.0 KiB
Fleet conversion reachability — measured, repo-wide
Summary
The backlog is 722 COLUMN guards. Two independent constraints gate conversion, and neither is visible from the census's per-file counts — which is what the work order sorts on.
| Constraint | Guards affected | Discovered by |
|---|---|---|
| No role helper for the role | 680 of 722 (94%) | #2685 |
| Helpers not importable from the call site's package | 572 of 722 (79%) | this note |
| Column flags not in scope at the guard | see below | #2683/#2684 (one file), this note (repo-wide) |
1. Where the guards actually live
The role helpers (isIntakeColumnRole, isPreImplementationColumnRole, isHoldColumnRole) live in
packages/dashboard/app/utils/columnRoles.ts — a dashboard-app module. Engine, core, CLI, and
the dashboard server cannot import it.
| Location | Guards | Share | Helpers importable? |
|---|---|---|---|
packages/engine/** |
316 | 43% | no |
packages/dashboard/app/** |
150 | 20% | yes |
packages/core/** |
148 | 20% | no |
packages/dashboard/src/** (server) |
78 | 10% | no |
packages/cli/** |
24 | 3% | no |
| plugins | 6 | 1% | no |
150 of 722 (20%) sit where the existing helpers can be called at all. Widening the helper set (#2685) does not change this number — it is about the module's location, not its coverage.
Core already exports resolveColumnFlags, so a core-side predicate module would be the same
abstraction made reachable rather than a new one. That is a prerequisite for the other 80%, and it
is not sufficient — see the next section.
2. The binding constraint: flags are not in scope at most guards
A role predicate needs the column's resolved trait flags. Many guards run in functions that receive a bare task row and have no workflow IR to resolve flags from. Threading one in changes a function signature and its call graph — a behavior change, out of scope for fleet conversion.
File-level proxy over the 572 non-dashboard guards (does the file reference
resolveColumnFlags / findColumn / WorkflowIr / columnFlags at all?):
- 339 in files that do reference one — possibly convertible
- 233 in files with no IR/flags reference anywhere — not convertible without threading
That proxy overstates convertibility, and the overstatement is the point. Reachability varies
within a single file, so a file-level verdict is not usable. Measured in scheduler.ts:
| Site | Context | Convertible? |
|---|---|---|
:1690 |
resolveWorkflowIrById(...) + resolveColumnFlags(c) resolved in the same block |
yes |
:231 |
isLegacyDependencySatisfied(dep: Task | undefined) — task only |
no, without a signature change |
:341 |
shouldHoldActiveFileScopeLease(...) — task only |
no, without a signature change |
scheduler.ts is the largest unclaimed cluster (28) and is already mixed. So per-site triage is
required for every cluster; "convert the file" is not a unit of work that exists here.
Worked example of a guard that must be skipped, not converted
packages/core/src/task-merge.ts:254 — getTaskMergeBlocker:
if (!options.skipColumnIdentityCheck && task.column !== "in-review") {
The parameter is Pick<Task, "column" | "paused" | "status" | "error" | "steps" | "workflowStepResults">
— no IR, by design. 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;
the literal is the fallback for callers that have not proven lane identity.
Converting it would not remove a legacy id — it would delete the fallback that the option was introduced to make explicit. Flag and skip.
3. What this implies for the work order
- Sorting by per-file guard count sorts by a number that does not predict convertibility.
- A cluster's real unit of work is (site, is-flags-in-scope), which the census does not emit.
- The two prerequisites are ordered: role-helper coverage (#2685) → a core-reachable module → then conversion. Claiming clusters before the first two land produces per-file rediscovery, which is what #2683, #2684, #2685, and this note each are.
Useful census upgrade (not done here — it changes the census, which is the shared instrument and not mine to change unannounced): 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 the baseline shrink predictable, which the fleet rules require ("the baseline must shrink by exactly your converted count").
Method
- Counts from
node scripts/lifecycle-column-census.mjs --json(byFile), the authoritative instrument — never grep. - Package attribution by path prefix on
byFile. - Flag-reachability proxy by
grepfor the four resolver identifiers, file-level, stated as a proxy because it is one. - Per-site verdicts in §2 read from source at the cited lines.
- Claim collisions checked against each open PR's merge base, not against
origin/main: diffing a branch against main reports a difference when the branch is merely stale (the file did not exist at its base), which falsely flags unrelated PRs as touching a file.feature/code-organization-wave17is excluded from collision checks — 1556 files, 150 commits behind main, alreadyDIRTY; it must rebase wholesale regardless, and treating it as a claim would mark every cluster in the backlog as taken.