Skip to content

v0.245.0

Choose a tag to compare

@github-actions github-actions released this 24 Aug 10:57
· 19 commits to main since this release

/simplify pass over the K354–K359 arc — the CLI owns the range, hooks share one resolver

Four-angle cleanup review (reuse / simplification / efficiency / altitude) over the unreleased batch; twenty-odd findings deduped to one coherent pass.

The range flag's grammar moved into the CLI. Seven workflow bash sites re-extracted --range= from prose with a copy-pasted grep one-liner — the exact caller-side dance the K354 wiring bug lived in, guarded by a "wiring did not take" backstop that only existed because extraction was caller-side. Now review-context-init parses --range= out of the --scope text itself (explicit flag wins; absent in both clears the persisted value), and changed-files, partition-lanes, and augment-impact-map default to the persisted workflow.yaml::range — the authority _activeRange()'s own contract always named. All seven bash sites collapsed; the wiring-invariant BLOCKED branch is structurally impossible and deleted; the backstop now reads the range from the receipt, not from prose. K354's marker leg flipped polarity: it now proves scope-text extraction works AND that no workflow re-greps the flag.

Hook root-resolution is one mechanism. K355 wired three hooks through devt_find_project_root; two more (pre-flight-guard, memory-auto-index) still walked with .git as a stop marker — the semantics _common.sh explicitly rejects — so in the nested-repo topology the litter fix was born from, the deny log (the stuck-detector's input) silently vanished and config was read from the wrong root. Now the guard + telemetry hooks resolve in bash via the helper and take the root as argv (non-devt projects skip their node spawn entirely — these fire on every Task event); memory-auto-index derives the root from the trigger path itself; the prompt injector's duplicate JS walk is gone. K355 gained the no-.git-marker invariant across all hook JS blocks.

Authority is consulted, not re-inferred, in three more places. _resolveGraphifyOut() returns {dir, source} so _statusBare reads borrow provenance from the resolver's decision instead of reconstructing it by negating every other precedence source; readImpactPlan() (state-graphify) is the one parser of graphify-impact-plan.json, consumed by dispatch's _impactPlanTier (whose false "read once per process" comment is corrected — K339 requires the health verdict to track the plan on disk, so it deliberately re-reads) and init's absorption/evicted branches; readLaneFilesSidecar() is the one reader of the lane-files registry sidecar, shared by dispatch's ownership map and the residue sweep.

The pinned scoring table has one source. LANE_SCORE_WEIGHTS lives in state-lanes (where the deterministic tally runs); dispatch interpolates it into the rendered <lane_scoring> block, so the rendered contract and the score-math check cannot disagree; the agent-prose copy stays smoke-pinned. The gate previously pinned two prose copies while the JS copy — the one that computes — could drift free. Also single-sourced: the corpus-suppression stub's not_applicable prefix (producer + envelope-health classifier + a pin on the workflow bash copy).

Docs currency. docs/AGENT-CONTRACTS.md's per-lane filtering corollary described the retired extension heuristic (<graphify_status> stub, prose-extension detection at dispatch_lanes) — rewritten to the shipped mechanism: one corpus-intersection probe in render-lanes, node-presence gating, the NOT_APPLICABLE_PREFIX-pinned stub, extension check as graph-unavailable fallback only.

Efficiency + dead weight. collectChangedFiles memoized per process (the scope receipt, memory-signal lane, and manifest freshness collected the same change set — 2–4 git spawns each, including on the short-circuit path); _worktreePrimaryRoot gets both git dirs from ONE rev-parse; the borrowed-graph caveat probes statusBare instead of paying status()'s freshness cost to discard it; the duplicate JSONL trace-walk inside assertGraphifyDecision extracted to one walker; the CLI entry's 15 copy-pasted help-aware exit guards collapsed to exitOnCode; the lane narrative-guard's artificial throw-to-reach-catch flattened to a plain branch; the dead scores[].findings_listed field dropped; escapeRegExp got a named home in io.cjs; validateEnvelopeHealth now validates the notice/absent arrays the producer emits. Fixed en passant (twice-flagged by review agents): the residue-sweep mode read used jq '// true', which swallows an explicit false — review.residue_sweep: false still auto-dispatched.

Residue sweep — assigned is not attended (K358)

Registration coverage catches scope files assigned to NO lane; nothing caught the quieter failure: files a lane owned and never looked at. Field: a parallel run's only release-blocker lived in an out-of-corpus file (.po/YAML class — no graph nodes, nothing anchoring reviewer attention) that its lane skimmed past while every gate reported green.

New deterministic state lane-residue: a scope file counts attended when a sidecar finding references it or a terminal lane's markdown names it (the rubric's Axis-A per-file "no issues found in <file>" attestation counts — this enforces a contract reviewers already carry; a generic "all good" line does NOT vouch for a file, and an ambiguous basename can't vouch for its path). Residue is reported with the owning lane and corpus membership (out-of-corpus = the ranked risk class; unknown when the graph is unavailable, never asserted).

Wired into the parallel path before the consolidator dispatch, so the sweep's output is one more lane the synthesis folds in: non-empty residue registers L9 (residue-sweep) and renders it through machinery that already existed — register-lane, render-lanes --only, a focus directive naming the assigned-but-unattended class, the native corpus suppression. Bounded at ONE dispatch, never recursive. Config review.residue_sweep: true (default) / "ask" / false (report-only — the list surfaces as uncovered scope). K358 proves detection, attestation clearing, the generic-clean-line non-vouch, and the wiring; the attention checks probe-reverted red-then-green.

The arch scanner adapts to the layout it lands in (K359)

Field: the python-fastapi template's scanner suite, installed into a repo with no app/ tree, walked the ENTIRE repository from --root . — parsing read-only course-year folders and producing 44 findings, all false positives — while the smells file's detect commands hardcoded app/, the temporal-markers rule flagged tier naming that IS that project's pedagogy, and quality-gates mandated mypy over the project's configured checker. Auto-wired live at setup, none of it fit the structure it landed in.

arch-scan.py now discovers its source roots — conventional app/ layout → conventional src/ layout → top-level packages (__init__.py) → repo root as a LOUD last resort naming --src as the fix — and always prints what it chose and why (a silent scope choice is how 44 false positives read as a real report). --src=dir1,dir2 overrides discovery; --service-root defaults to auto (first <src-root>/services that exists) instead of the hardcoded app/services. Every runnable detect command in common-smells.md now uses a <src-root> placeholder; the temporal-markers rule carries a domain-fit caveat (sequence-as-pedagogy, numbered migrations, versioned API modules are not temporal markers); quality-gates Gate 3 states the contract is "the project's type checker passes," not "mypy specifically." K359 runs the scanner across src-layout, app-layout, and bare fixtures — noise folders never parsed, provenance printed, override wins — with the discovery probe-reverted red-then-green.

The parallel verifier envelope carries its own type (K357)

Field (eval round 3, D6 — confirmed exactly as reported): the shared verify step embedded ONE compiled verifier envelope, so the parallel path dispatched workflow_type: code_review and pointed <rubric_path> at the single-dispatch rubric while simultaneously injecting a parallel-only lane_sample — harmless only because both rubric pins currently resolve byte-identical files; a project-local per-type rubric override would have misgraded every parallel verify. Worse, dispatch render-filled verifier:code_review_parallel fell back to the bare verifier.tmpl.md — the DEV verifier envelope.

Now: a verifier-code_review_parallel.tmpl.md twin exists (correct type + rubric-code_review_parallel.md path), the shared verify step embeds BOTH blocks MODE-labeled with the field history stated inline, and render-filled resolves the specific template. The twin is held to a two-line delta by gate K357 (diff after normalizing the type token) — the exact copy-paste-drift class that cost the parallel path four gates in the pre-partition era, made mechanically impossible rather than reviewed for.

Worktree-aware graph resolution — "present elsewhere" is not "absent" (K356)

Field, two instances: reviewing a PR in a linked git worktree silently dropped the entire graph layer — graphify-out/ is gitignored, so a worktree never has one — and the degradation (tier=skip, empty scope_trust, no blast radius) was byte-identical to a deliberately graphify-disabled project. "You have a graph, it just isn't reachable from here" was indistinguishable from "you chose not to have one," which is exactly what made it quiet. Separately, the MCP drill-down gate read an empty worktree trace and accused the operator of fabricating drill-downs when the server's records simply sat in the primary checkout.

The resolver (getGraphifyOutDir) now runs: GRAPHIFY_OUT env (existed — graphify's own multi-worktree sharing mechanism) → new graphify.out_dir config (parity for the env, so a project pins the shared home once) → local graphify-out/ → worktree borrow: local graph absent + linked worktree detected (git rev-parse --git-dir ≠ --git-common-dir) + primary has graphify-out/graph.json → use the primary's, with graph_source: "primary_checkout" + primary_root provenance on status(). A local build always wins (the borrow only fires on local absence), so a worktree opts into its own graph by building one. Freshness stays honest for free: built_at_commit compares against THIS worktree's HEAD, so branch divergence surfaces as lag. Kill-switch: graphify.worktree_graph_fallback: false.

The distinct state: with the fallback off, status() reports graph_present_elsewhere — naming the primary's graph path and all three remedies — instead of a bare graph_missing, which now means nowhere. The state string rides the existing graphify state=<state> skip-reason passthrough into plans, envelopes, and the activity surface unchanged.

Write-path safety: maybe-refresh never auto-rebuilds a borrowed graph — graphify update . scans the worktree's tree, and auto-refreshing into the shared primary graph would poison it with branch state. It skips with graph_borrowed_from_primary and names the two sanctioned paths (refresh in the primary, or a forced local build that then takes precedence). Non-ready refresh skips also stopped collapsing every state back into the graph_missing literal.

The caveat: plans generated against a borrowed graph carry worktree_graph_caveat (branch-only symbols are corpus-blind; lag measures divergence from this worktree's HEAD), riding the same graph-impact.md prepend passthrough as corpus_blind_caveat.

The gate diagnosis: attestation stays strict — foreign-id trace records are never accepted — but on a zero-record drill-down failure the gate now probes the primary checkout's trace and reports what it found (primary_trace_probe + "the MCP server is rooted there; run from the primary or restart the session inside this worktree"), turning the fabrication accusation into a named topology error with its remedy.

K356 exercises all of it against a real git worktree add fixture — borrow with provenance, elsewhere-vs-missing, local precedence, refresh suppression, primary untouched, gate diagnosis — with three fixes probe-reverted red-then-green.

Passive hooks never create project state — the .devt litter fix (K355)

Field: five scattered .devt dirs across one repo — the git root, a subfolder, and three chapter directories — in a project that never ran a devt workflow. All five carried the identical signature (status.json, subagent-events.jsonl, hook-trace/) and nothing else: pure hook residue. The creator was one line — subagent-status.sh's bare relative mkdir -p .devt/state — which fires on every subagent start/stop in EVERY Claude session, because the plugin loads globally. Whatever cwd the hook fired with (session root, a subagent's drifted shell) got a fresh .devt; the trace writer then found the new state dir on its upward walk and completed the signature. Deleting them didn't help: they re-accumulated on the next session.

The policy, now shared and gated: lifecycle hooks resolve the nearest EXISTING .devt (devt_find_project_root in _common.sh — pure bash, no subprocess for a hook that fired 260 times in one field run) and no-op when there is none. Only explicit /devt:* entry points may initialize a project, always at the resolved root. .git is deliberately not a hook marker — an explicit command creating .devt at a git root is opt-in; a hook doing it is litter (the field repo's git-root .devt was itself residue).

The read-side twin rode along: session-start.sh (project detection, setup warning, resume banner, handoff) and workflow-context-injector.sh (the every-prompt state banner + its cache key) read .devt relative to cwd — so a session started in a subfolder of a real devt project missed root workflow state and printed the not-configured banner beside it. Both now resolve the same root the writes do, falling back to cwd so .devt-less projects behave unchanged.

K355 locks both directions functionally: bare and git-only dirs stay clean through a hook fire, a nested-cwd fire writes to the root .devt and creates nothing below it, session-start and the injector see root state from a subdirectory, the bare relative path stays dead, and all three hooks share the one resolver. Both halves probe-reverted red-then-green.

Three-instance field batch — agreement over presence (K354)

Three greenfield instances evaluated the same devt version from three different angles; their calibration answers retracted two of their own claims (a jq '.exists // false' read of a key that doesn't exist reported as a CLI defect; a "no working invocation" overreach from two failed flag guesses) and receipt-proved the rest. The unifying class, named by one evaluator and validated at code level: devt's guards overwhelmingly tested presence, not agreement — "something is there" repeatedly stood in for "the right thing is there."

One scope parser. The ## Source value line of code-review-input.md ("git-diff (base: development)") survived scopeInputFiles() as a phantom scope file reaching three consumers — the governance denominator (memory_signal.files_checked), review-weight, and the graphify scope key — because the partitioner's parser was fixed for exactly this and the state-io copy never was. Both now delegate to one section-aware parseScopeArtifactBody (whole-body fallback keeps headerless bare lists legal), and writing the artifact correctly per the documented template no longer injects the defect it documents.

Skip provenance survives its own absorption. augment-impact-map absorbs graphify-skip-reason.txt into the map (content-wins invariant) — correct — but both structured consumers kept re-inferring the MCP decision from the file layout the absorption had just changed: every lane envelope carried skipped:false on a run where no MCP tier ran, and the Graphify-activity surface printed its ambiguous zero-case line instead of the precise SKIPPED line it exists for. The impact plan (which computed the decision and survives) is now the authority for both: loadGraphImpact annotates mcp_ran:false / tier:"skip" / reason from it, and the activity surface falls back to plan.skip_reason when the skip artifact is gone.

envelope_health tells a delivered map from a delivered apology. New notice classification (self-describing no-payload bodies — > note: skip provenance, not_applicable corpus stubs — plus a plan-tier consult for graph_impact), new absent[] array (a wholly missing monitored block previously vanished from the report: the arrays summed 5 of 6 with nothing indicating one was gone), a semantic check for scope_trust ({"trust":"empty"} parsed as JSON and certified "populated"), and agent_skills joins the monitored set now that render fills its token mechanically — the documented orchestrator-side injection had field-shipped the literal {injected from .devt/config.json if available} template text to five lanes.

Lane scoring is a contract, not a vibe. The rendered <lane_scoring> block deferred to "your scoring guide" — a file that does not exist (the agent body's code-reviewer/scoring-guide.md reference was dangling) — and three lanes invented three different weight schemes, materially inverting the lane rank order. The pinned table (Critical −15 / Important −7 / Minor −3 / Nit −1, floor 0) now rides in the envelope block and the agent body, lane-severity-tally recomputes every lane's score from its own severity_counts (score_math_mismatches names deviants; null scores stay contract-legal), and a per-lane prose guard checks every "(N findings)" line naming exactly one lane against the sidecar AND findings_contributed — the field case's headline totals were correct, so a totals-only guard passes the failing run.

--range reaches context-init. The CLI supported it all along; the workflow call omitted the flag, and an absent flag explicitly clears any prior value — so a range review's symbols, memory signal, and impact tier silently derived from the working-tree diff (three untracked markdown files became the review universe). The substep now extracts and passes the range, the bundle emits a files_source + scope_consistency receipt, and a three-state backstop acts on it: range resolving to zero files blocks with its own diagnosis (bad ref, not empty review), a working-tree universe under a named range blocks as a wiring-regression invariant, range-vs-tree divergence stays informational (merges/rebases legitimately differ). --scope-mismatch-ok is the escape hatch, matching --stale-ok.

Filter before the cap cuts. The 32-symbol cap evicted 4 production symbols while 19 test-shaped ones held seats (test symbols have no callers by construction — every seat dilutes the blast radius). Classification is source-path from the diff itself, never name-shape for resolvable symbols (a sku fixture collides with a production Sku model; a false positive evicts exactly what the filter protects): production symbols rank ahead of test-attributed ones before the cap, test symbols stay in topic as coverage signal, non-members fall back to test_*/Test* prefixes only, review.test_path_markers overrides the path markers, and topic_symbols_test_demoted counts the reduction.

Lens partition as a first-class mode. state partition-lanes --lenses=core!,tests,docs,arch — the ! axis (default first) is the OWNER lane that adjudicates correctness; every other axis is a lens: true lane over the SAME full file set, deferring logic rulings via <lane_lens>. The owner concept is structural: a flat list of equal lenses has no anchor to defer to, and the owner/lens discipline is what made four hand-rolled field reports compose instead of collide. The structural partitioner now reports single_lane + lens_hint when it collapses to one lane under a parallel mandate — the fallback that silently defeated an already-made parallel decision — and the workflow instructs the orchestrator to convert the mandate's enumerated axes into a lens partition instead of accepting fan-out with no parallelism.

CLI honesty. --help/-h never executes — it routes to the module's unknown-subcommand enumeration (K156 keeps those complete) and exits 0; dispatch render-lanes --help used to render all five envelopes and would today also stamp the redispatch lease. dispatch verify-envelope <path> without --sha256 reports the file's own digest (sha256sum behavior) instead of demanding a number nothing else could produce. state update-lane accepts the flag spellings operators actually guess and echoes usage on every rejection — two individually-true errors had composed into a false "no working invocation" report. The MCP drill-down gate stopped saying "fabricated": it now reports the trace path it read, whether the file exists, and its record count, with the worktree-divergence hypothesis named (the server resolves its root from its own process cwd).

Project-owned state. state.project_owned config globs classify files in .devt/state/ as the project's: never swept by state cleanup --apply, never listed or counted in the foreign-file notice — for projects whose tests hardcode a baseline or snapshot path there, where relocation to .devt/state/project/ would mean editing test code and staling the tests' own remediation messages. dispatch/ (the canonical --out home, now also the bare---out default for render-lanes) joins the canonical subdirs so devt's own rendered envelopes never classify as foreign. Plus: the python-fastapi template's quality-gates.md gains the HURL E2E gate its own hurl-reference.md implied (a rules file that never names the E2E gate cannot catch the omission class that produces silent integration regressions), and the reviewer's Axis-H instruction states that an absent warnings ledger on a clean run is the expected state, not a broken guard.

Deliberately deferred: the worktree-blindness cluster (graph resolution, MCP trace path, stale identity stamping — one topology assumption breaking three ways) goes to its own design pass; the verifier envelope's hardcoded code_review type on parallel runs stays harmless while both rubric pins are byte-identical and is queued behind it.

K354 locks the batch: eleven functional legs (one-parser fixture, usage echo, report mode, help-no-side-effects with stamp-count proof, lens registration + single-axis rejection, score-math + prose-count agreement flagging only the deviant lane, skip-provenance annotation, health arrays on a live render, range receipt, filter-before-cap demotion with the dropped-tail asserted test-only, project_owned survival through cleanup --apply) plus the prose anchors; four fixes were probe-reverted red-then-green.

Cleanup pass over the post-0.244.0 arc — and two guard holes the altitude review caught

Four-angle review of the four unpushed commits; the surviving findings, applied. Two are behavioral, not cosmetic:

  • The redispatch lease now covers dispatch run. The lease lived in the render-filled CLI case only, while dispatch run — the sanctioned one-off launcher, explicitly advertised for ad-hoc dispatch — neither consulted it nor left a stamp: a presumed-death retry routed through run reproduced the clobber with the guard green, and a dispatch issued via run left no evidence for a later render to refuse on. Both cases now share _redispatchLeaseRefusal + _appendDispatchStamp; the lease also resolves its artifact from io-contracts outputs.primary instead of restating the (code-reviewer, review.md) pair, takes its fallback window from config DEFAULTS instead of a drifting literal, and the leased-agent scope stays deliberately narrow — programmer/tester/verifier legitimately re-render within minutes in revision loops.
  • render-lanes applies the corpus-gated map suppression natively. The workflow bash gated only the hand-assembled dispatch shape; the pointer-stub path — named the sanctioned first-class form by the same workflow — still shipped the global map to zero-corpus lanes. One in-process intersection over the whole partition (each CLI spawn re-loads and re-indexes the entire graph, up to five times per review), per-lane stub replacement, and the ownership pass now retains lane files so the render loop stops re-reading every sidecar it had already read.

The rest: one config read per partition run (both refinement helpers took their own uncached getMergedConfig); the derivable laneFiles return and its O(n²) exclusion dropped in favor of the single releaseSet; the file↔node attribution walk (basename index + field-tuned suffix-disambiguation rule) extracted to one _forEachNodeFileMatch shared by laneSuggestions and corpusIntersect; the partitioner's two local path-similarity counters unified into _sharedPathSegments; M3 cohesion split into decide-then-apply phases (the mutation-during-iteration defensive copies bought nothing); the release-anchor reservation made branchless with the protected scope as a named constant; three dead conditions removed (the textShaped underscore/dot/parens disjuncts — the text-channel harvest yields pure alphanumerics, so interior-capital is the only discriminator that can fire — the lease's unreachable Infinity arm, and the --only !only guard); envelope_health statuses exported as named constants instead of order-pinned array destructuring; clobber archive copies namespaced under .devt/state/.archive/clobber/ so the snapshot ring-buffer pruner and the recovery copies stop sharing one directory under two conventions; and K353 tightened (exact-sentence neutralizer identity, plain require concatenation) plus two new legs locking the run-path lease and the native suppression in both directions.

Skipped, with reasons: the TIER-STEP gate-attribute/body wording overlap (two audiences — the attribute is the scan line, the body the execution directive); a shared dispatch-stamps reader for state-gates (would require a state-gates→dispatch import cycle); a PROTECTED_SCOPES set (one protected lane kind exists — the named constant covers the rename hazard without speculative generality).

Boundary fixes from the second full-fan-out field run — flag parsing, re-dispatch concurrency, and four noise sources

The second calibrated field evaluation landed with two of its own claims self-corrected; everything below survived code-level validation, and two items took their final shape from the evaluator's corrections rather than the original report.

--lane-<id>-focus= accepts multiline values verbatim. The parser's (.*)$ could not span a newline, so a multiline focus silently missed the map — exit 0, no block, no warning — defeating the exact retry path built to stop hand-rolled context loss. Verbatim acceptance per the evaluator's contract choice: the value lands inside <lane_focus> where newlines are harmless, and rejection would add a file-indirection step at precisely the moment a run is already degraded.

In-flight redispatch lease on code-reviewer renders. A field orchestrator read an empty agent listing as consolidator death and re-dispatched; the first consolidator was alive, wrote a complete 119KB synthesis, and the replacement's stub-first skeleton overwrote it unrecoverably. Both dispatch-time guards originally proposed evaluate false at that timeline — the collision is at write time — so the shipped fix is the evaluator's: a TTL lease keyed on dispatch-stamps.jsonl, which was already on disk with exactly the discriminating record. A code-reviewer render inside dispatch.redispatch_lease_minutes (default 15, 0 disables) with no substantive review.md is refused — "an empty agent listing is not proof of death" — and a substantive review.md inside the window is refused the other way (a re-render leads to a stub-first overwrite of finished work). --force-redispatch is the loud override; lane retries flow through render-lanes and are governed by the substance check, never leased.

Archive-before-stub, all 8 output-writing agents. The stub-first protocol now requires copying a pre-existing substantive target into the state archive before the first write — a re-dispatch must never make prior work unrecoverable. The lease prevents the incident; this makes anything that slips past survivable. (Generation-checked rename deliberately not built: heavier write-protocol change, and prevent+recover covers the class.)

Staleness reasons render a divergence window. The task-changed templates inlined both full task strings — ~4,600 characters to convey a ~70-character delta. Long changes now render as lengths + a first-divergence excerpt. The prior_completed: false half of that finding needed no fix: the record said active: true because that run predated the review-tail step-element/staged-gate fixes and had skipped the terminal transition the shared step body already instructs — the detector correctly read a lying record whose liar is already shipped out of existence.

Text-channel topic symbols require identifier shape (interior capital, _, ., or ()): a prose sentence capitalizes its first word, so the PascalCase harvest shipped "Also"/"Consider"/"Python"/"Everything" as graph anchors — 8 of 9 text-channel symbols in the field run. The evaluator's evidence ruled out the alternatives: an English-word denylist is unbounded, and node-existence can't discriminate because the graph itself holds prose pseudo-nodes (that filter caught one of nine). Known, accepted cost: a single-word class named only in prose drops from this channel — the diff channel is unaffected and remains the primary anchor source (O3 updated to pin the new contract).

Lane graph-context gating is corpus intersection, not file extensions. The all-prose-extensions rule made an all-or-nothing call, so a lane of hurl/lockfile/docs/.env files — zero code, yet not all "prose" — received 175 lines of Python symbols. New graphify corpus-intersect splits a file list by node presence in graph.json; a lane with no corpus files gets the not_applicable stub. Deterministic where majority-prose thresholds are heuristic; the extension rule survives only as the graph-unavailable fallback.

Third-party hook mandates neutralized in every graph_impact_note. Two lanes were caught between a project-local PreToolUse hook demanding graphify-before-grep (asserting subagent scope explicitly) and an envelope telling them to consume the pre-computed map. The hook is not devt's — the sweep found nothing of the kind in hooks/ — but the envelope now states that the inlined map satisfies any such orientation mandate, so lanes stop spending budget reconciling the contradiction. K353 locks all seven with per-leg falsifiability (the lease, focus, and shape legs were each probed red by reverting their fix); three suite fixtures that legitimately re-render code-reviewer gained the lease-off config or the explicit force flag.

The auto-partitioner learns what half a release branch actually is

Field receipt (first full-fan-out parallel review): partial-community mode put half the branch — changelog, lockfile, hurl suite, unit tests — into ONE "ungrouped" lane with no coherent review lens, and the CORS middleware fix landed in a different lane from its regression test because community labels describe the graph's view of the code, not the diff's intent. Three refinements in state partition-lanes, each config-overridable and generic across ecosystems:

Release-artifacts lane, pre-extracted. Changelog/VERSION/lockfile/manifest files become a dedicated lane whose concern is cross-artifact agreement — reviewing them together is what caught a wrong-branch attribution in the field run's hand-rolled equivalent. Two placement decisions came out of fixture probes, not theory: extraction happens BEFORE graphify lane-suggestions sees the scope (post-hoc extraction met the consolidation packer, which had absorbed VERSION into a community super-group with no output marker distinguishing packed files from genuinely labeled ones), while the CLI still receives the FULL scope (its skew threshold divides by total input — the shrunken input flipped a healthy 31% partition into a false 44% skew fallback). The lane is merge-protected both ways: it holds an anchor slot even when too small to earn one by size, and never receives overflow. Patterns are deliberately tighter than review-weight's logic-excludes — a loose HISTORY prefix would move history_service.py into the release lane. ≥2 matches; a lone changelog rides along in the normal flow.

The ungrouped remainder gets path grouping. What's left of the community-unlabeled bucket after extraction gets the same top-2-level path grouping the fallback branch uses (now one shared helper, not two drifting copies) — tests/hurl + docs + root instead of a grab-bag lane.

Test-mirror cohesion. A test file whose basename stem mirrors a source file in the same scope joins that source's lane even when community labels disagree — both files being in the diff IS the co-change signal, no git query needed. Ambiguity never moves: multiple stem matches resolve by longest shared directory prefix, ties stay put. On the field fixture this moves tests/test_cors.py into the middleware's lane — the exact split reported.

K352 locks all three through the full CLI path against a seeded partial-mode graph fixture (mode stays partial, release lane carries exactly its four files, the cors test co-locates, both kill-switches honored). Falsifiability: neutralizing the pre-extraction, the mirror, or re-shrinking the lane-suggestions input each turns a distinct leg red — the last one reproducing the exact skew-fallback bug the fixture first caught live. Deliberately not built: evolution co-change data as a cohesion signal (stale-by-default at partition time — needs its own receipt) and any relocation of genuinely community-labeled files (the release patterns match only non-code, which cannot carry a label).

The review-tail lessons, generalized to dev

The parallel-review silent-skip fixes were instance repairs on a class defect, and dev — the highest-traffic workflow type — carried both halves of the class. Applied now, ahead of a dev-side field receipt rather than after one:

Staged verifier gate on every post-verify dev phase. assert-verifier-ran now fires at docs/retro/autoskill/curate/finalize entry, not only at terminal complete — the same reachability hole: nothing compels an orchestrator to take the terminal transition, and state update phase=retro status=DONE with verify skipped was refused by nothing. Choreography verified before staging: STANDARD+ stamps none of these phases DONE before verify's pipeline position (the lone pre-existing finalize stamp is a BLOCKED branch, which never fires entry gates), and SIMPLE/TRIVIAL flows auto-pass via the gate's built-in tier opt-out — so a refusal here always means verify was skipped. The registry header's staged-exception rule is now stated generally instead of as a review-only carve-out.

dev's 10 TIER-STEP pointers promoted from HTML comments to <step> stubs with the tier condition in the body — the two pointer families previously encoded contradictory lessons (K349 forbids the comment form in the review parents while K210/K211 locked it into dev), leaving a future editor a coin-flip which pattern to copy. K210 repointed: the spine now carries exactly ONE step tag per relocated step (the stub) instead of zero, with residency caught by the count + K211's token check + a body-marker sweep on the flagship verify step. K351 locks both halves behaviorally (SIMPLE opt-out passes / STANDARD skip refused / healthy passes, in a fixture) and structurally (10 stubs, zero comment-form pointers). Falsifiability: un-staging the registry fails the refused-stamp leg; demoting one stub to a comment fails the pointer-form leg.

Cleanup pass over the parallel-review field batch

Four-angle review (reuse / simplification / efficiency / altitude) of the released batch; the surviving findings, applied:

  • envelope_health vocabulary single-sourced. The validator re-encoded healthy/degraded as fresh literals two modules away from the producer — the exact drift the file's own verification constants exist to prevent. ENVELOPE_HEALTH_STATUSES + a named validateEnvelopeHealth now live in the contract module; computeEnvelopeHealth produces from the constant, and the check attaches to ALL four sidecar schemas, not just review.json — the <envelope_health> block is injected into every rendered envelope, so any agent may echo the field, not only the reviewer.
  • graphify_status derivation moved into loadGraphImpact. The dispatch-side ternary re-walked the loader's four-status enum across a module boundary; a fifth status would have fallen into the catch-all unnoticed. The loader now returns the consumer-shaped object beside the content it derives from, and the dead s.graphify_status_json || override (no writer exists anywhere) is gone.
  • Orphan prune extracted to _pruneStaleLaneArtifacts beside registerLane so the write-side and delete-side artifact inventories sit adjacent — a new per-lane artifact must land in both or the watermark bug returns one artifact class over. Inside it: the can't-fire dedup guard became a Set, lane-diff pruning matches the same filesystem predicate as sidecars (cleans diffs whose other traces are already gone), and three exists-then-act double-stats became try/catch.
  • Redispatch step: one fetch, one fence. The retry block re-ran state list-lane-outputs three lines after the increment loop fetched identical data with the same jq predicate; merged into a single block (also closes the cross-fence $LANES_JSON shape K334 polices).
  • --only present-but-empty is now a loud error, not a silent render-ALL — the caller is mid-redispatch, where a wrong full fan-out does the most damage. The redundant only[] result field is dropped (registered_count alone marks a subset render; ids already ride in lanes[].id).
  • Minor: lane-invariant <lane_budget> literal and _laneAutoMemorySummary hoisted out of the per-lane render loop; the two staged-gate registry comments had landed on swapped sections (the field-receipt attribution claimed the single path — it was parallel) and now point the right way; K349/K350 use the suite's $CLI convention and K350 gains the empty-flag leg.

Deferred to its own pass, deliberately: generalizing the staged verifier gate to dev's post-verify phases and promoting dev-workflow's 10 TIER-STEP HTML-comment pointers to step elements — both are the review-tail lessons applied to a different pipeline, and enforcement expansion on the highest-traffic workflow type deserves its own choreography validation, not a cleanup slot. Also noted, not built: the full single-lock batch-registration refactor (per-call locks are pre-existing composition) and a file-wide flag-parsing sweep in dispatch.cjs.