Repository navigation
v0.246.0
Plugin paths agents can actually resolve — the <plugin_root> anchor
In default by-reference mode, agents had NO working path to the guardrails: the stub said Read guardrails/<name> (relative — resolves against the agent's project cwd, where no such directory exists) and the agent-body fallback said ${CLAUDE_PLUGIN_ROOT}/guardrails/... (an env var agent shells never receive — the same class the eval-7 batch fixed on the rubric and provenance surfaces). Three-part fix at the mechanism level: the by-reference guardrails stub now names the absolute file path (self-resolving, so both delivery pipelines stay byte-identical); every rendered envelope carries a universal <plugin_root> tag — the ONE resolvable anchor for any "plugin's X" reference; and all ten agent-body instructions that pointed Reads through the env var (guardrails fallbacks in programmer/code-reviewer/architect/tester, direct reads in debugger/researcher/curator/retro, the standards path, two CLI examples) now resolve through the stub paths or the envelope tag, with an explicit warning that the env var is unset in agent shells. Gate: K366 gains the anchor leg (probed red-then-green); K314's stub pin and F63's plugin-root negative re-pinned to the absolute form (both demonstrated red under the old pins).
Simplify pass over the eval-7 + cascade batches — one diff source, one predicate, one emit tail
Four-angle review (reuse / simplification / efficiency / altitude) of the two batches below, applied under the byte-identity bar: render-filled and render-lanes outputs proven byte-identical against the pre-pass HEAD.
One diff source — and a capability fix riding on it. scope-cascade hand-rolled three git diff spawns; it now delegates to changeSetText, the module's sanctioned change-set reader — which also means untracked files finally feed the git-only extractor (a brand-new module's constants previously yielded zero cascade symbols, undercutting the feature's own graphify-independence claim) and range purity is preserved. Attribution collapsed from git grep -l + a read of every hit file into one git grep -o whose output lines ARE the (file, symbol) pairs — zero file opens.
The off switch now switches off. Mode resolution moved into the CLI: review.scope_cascade: "off" previously ran the full scan in both workflows and only suppressed the echo, and the two bash blocks carried drift-prone copies of the defaulting jq (the //-swallows-off trap commented in exactly one). The CLI short-circuits on off, returns mode, and ships preformatted display[] lines — which also fixes a divergence the copies had already grown (the single path silently omitted the ubiquitous-dropped disclosure).
Render internals deduplicated. render-lanes no longer re-renders the base template to recompute the owned-token set the base render just built; the rubric fail-loud moved from the lanes-only path into cmdRenderFilled, covering single-dispatch and verifier envelopes that carry the same <rubric_path> and the same risk; render-filled's two emit branches share one stamp+telemetry tail; envelope-byte records now ride EVERY emission path (dispatch run and stdout-mode lanes included — an aggregate that misses inline-delivery renders under-reports the most expensive consumption mode) and append via the canonical appendJsonl (torn-write guarantee); the outermost-span filter dropped from O(n²) to a maxEnd sweep.
The stamp rule reaches every reader. The workflow-id stamp check moved into the shared scope-artifact parser and the tier-floor counter — a prior run's leftover was already rejected for sizing and partitioning but still set the tier floor and the residue/coverage denominator. partition-lanes reads the artifact once instead of twice. The test-path predicate joined its marker list in one shared helper (the match rule can no longer drift between diff-symbol demotion and cascade classification).
Skipped with reasons: folding the two cascade bash blocks into the SHARED-STEP partition (post-CLI-fold residue is a few lines at different pipeline positions; restructure outweighs), multi-path assert-file-quiescent (one spawn saved vs. CLI shape churn), and inverting the router's help-capability set (single entry; a second-entry-is-the-signal note added instead). Suite 1143/0 + locking 3/3.
Scope cascade + consumption-side batch — the diff is not the blast radius, and routing reads the cheapest sufficient surface
state scope-cascade — reaching what a change invalidates. Every scope mechanism stopped at the diff-derived universe (the residue sweep catches assigned-but-unattended; nothing caught never-assigned), while field data showed 17 out-of-scope files carrying 9 required fixes — docs describing changed primitives, catalogs keyed by changed action names, registries, siblings — reachable only through hand-written per-lane focus text. The new CLI unions the graph-backed symbol extractor with a git-only diff scan (definition keywords + UPPER_CASE constant assignments — the constant pattern is what reaches catalogs and registries), runs one git grep over tracked files outside the declared scope, and proposes candidates with per-file evidence (matched symbols, class docs/catalog/test/source/hyperedge); names matching 30+ files are dropped from evidence as non-discriminating, and the drop is reported. Hyperedge members ride as their own class via a new member_files projection. Config review.scope_cascade: warn (default — both review workflows surface the table and forbid silently dropping candidates), lane (the parallel path auto-registers a bounded cascade lens lane), off. Promotion of the default is receipt-gated on field candidate precision. Graphify-independent by construction; language-agnostic by construction.
dispatch bytes — the consumption-side bottleneck finder. Every render now appends a per-block byte map to envelope-bytes.jsonl (nested spans attribute to the outermost block; <context> is the wrapper, not a block), and the aggregate ranks blocks by bytes with shares. token-report reads transcripts and cannot see which block carried the cost; this records it at the one place that knows — render time. The deferred consumption items (conditional envelope narrative reads, plan-section reads) now wait on these numbers instead of a guess.
Routing reads the cheapest sufficient surface. The dev-workflow review gate routed by Reading full review.md (90KB+ field-observed) for two fields that live in the schema'd sidecar — it now routes on read-sidecar review.json (score joined the hoisted routing fields per the every-field-a-gate-routes-on rule), reading the markdown only on NEEDS_WORK branches where the findings actually feed <review_feedback>. The docs/retro pre-dispatch check instructed reading "impl-summary.md status" — a surface that deliberately carries NO status header under the sidecar-only routing contract, so the instruction was wrong, not just wasteful; it now reads impl-summary.json. The research gate becomes state read-section's first workflow consumer, reading only the ## Status section of an artifact that runs tens of KB.
Gates: K366 (cascade behavioral with in-scope-exclusion + class assertions — probed red-then-green; byte attribution + aggregate; stale read-forms pinned out so they cannot return; artifact registration both surfaces). The build also tripped two pre-existing gates honestly — the cross-module call resolver caught a name collision, and the cross-fence shell-state gate caught the first draft of the review-gate rewrite carrying a variable across fence boundaries — both fixed as flagged. Suite 1143/0 + locking 3/3.
Field-eval batch #7 — routed findings become losable no more, and the CLI stops impersonating other failures
Two greenfield calibration reports processed together (one had landed after the prior batch cut and was never seen). Validation refuted four of their claims as wrong-key jq queries against working features (is_stub, count/by_type, total_calls/by_tool — the exact selectors now ride in the relay) and one as unreproducible (state update phase=present_findings sticks at HEAD); everything below is what survived code-verification.
Lane sidecar v2 — routed_to[] + rubric_read, per-lane score retired. Two findings in one run were routed lane-to-lane as prose and filed by NEITHER sidecar — a routed item lands on a lane that has already closed its scope and has no mechanism to accept it. The write trailer now dictates {"lane","severity_counts","findings","routed_to","rubric_read"}: every cross-lane handoff must appear as a structured routed_to[] entry, and the tally's new routing guard asserts each one was adopted (target lane filed it, or the consolidated findings carry its file) — unadopted items STOP consolidation with from/to/file named. rubric_read:false makes a self-derived axis grading machine-visible (before: one lane's honest prose was the only signal, and nothing downstream could tell its grades were non-comparable); render-lanes additionally fail-louds when the rubric file the envelope names is absent or empty. The per-lane 0–100 score is retired after a third filing: pure deduction saturates at the 0 floor (a lane scored 22 while writing the review's best-reasoned Critical), lens lanes were graded on coverage of files they were told not to adjudicate, and the tally derives the honest signal — the per-lane severity distribution — from severity_counts with zero lane tokens spent. review.json drops lane_scores[] for lane_findings_kept (the prose-count check's dedup-tolerant second source); the report headline becomes verdict + severity counts + per-lane severity distribution.
The placeholder guard scans only what it owns. The unfilled-token assertion read the RENDERED envelope, so operator prose quoting {run_id} (in a mandate about run-id hygiene) or a Python parameter name in a notes file hard-blocked emission — five wasted renders across two runs, and the brace-mangling workaround corrupted the very text lanes were told to grep for. The assertion now checks only the template's own pre-fill token set: operator content renders through verbatim, while an unfilled template slot (late fills included) still refuses exactly as before.
Verified Negatives become a first-class template. Three runs of receipts (four hypotheses honoured by seven lanes with zero re-spend; a deleted-then-silently-redefined symbol steered to the top finding) for a mechanism that lived in one prose aside and depended on the operator thinking of it unprompted. The dispatch_lanes step now opens the notes file with a pinned ## Verified Negatives skeleton (HYPOTHESIS / DISPROVED BY / IMPLICATION), written before lanes dispatch.
CLI papercuts, all field-billed. check-agent-output accepts --file= and rejects unknown flags loudly (a coerced flag token read as "seven lane reviews missing" and would have invited a full seven-lane re-dispatch); render-filled --out=<dir> joins the canonical filename instead of dying EISDIR, and the dispatch stamp lands only after a successful emit (a failed render left a live lease the next attempt refused on); dispatch <sub> --help prints per-subcommand flag docs — --lane-<id>-focus=, the fix for the mandate-never-reaches-lanes trap, was previously findable only by grepping source; the consolidator's review.md/review.json get the same quiescence wait lanes already had (both were read mid-write in the field and a needless resume was sent).
Provenance ids the model can actually see. devt's graphify MCP wrapper emitted correlation_id only in protocol-level _meta, which MCP clients strip before the model sees the result — three consecutive runs of drill-downs with nothing to cite. The id now also rides inside the JSON payload; the provenance protocol is emitted only when graph-impact.md actually carries [call: markers; and the protocol's CLI citation renders plugin-root-absolute, because CLAUDE_PLUGIN_ROOT is not set in agent shells and the bare form was unexecutable exactly where it was read (a consolidator fell back to raw ls for its own rubric's mechanical check). Materialized plugin rubrics open with a machine-resolved CLI-path header for the same reason; project-local rubric overrides stay byte-verbatim.
Stale scope can no longer size a fresh review. Devt-written code-review-input.md carries a workflow-id stamp; readers ignore a stamped artifact whose id is not in the current workflow chain and fall back to changed-files (before: a prior branch's 8-file leftover survived inside the 1-hour eviction window and scope_check preferred it — an ordering accident away from partitioning a 46-file review as 8). Operator hand-written files carry no stamp and stay honored.
Guards say why. The pre-flight deny names the governing doc ids and states that covered files and ungoverned paths pass silently — the field read the one-in-six asymmetry as a flake and started satisfying it mechanically. The parallel workflow gains an explicit never-glob-for-lane-files directive (the archive ring preserves prior runs' lane files under original names; a recursive glob satisfied a completion wait with June's reviews while five live lanes were still stubs), and the knowledge-candidate prose now states what the code always did: scratchpad-direct is the contract, lane-file tags are the aggregator's rescue path, both are valid.
Template: drift baseline keys drop line. arch-scan.py's _finding_key was (category, file, line, fingerprint), so adding a constants block re-reported ten untouched accepted-debt findings as new drift and invited a --write-baseline that would have re-blessed everything current. Identity is now (category, file, fingerprint) with count-aware suppression (a Counter — each baseline entry absorbs at most one live finding, so a genuine duplicate still surfaces); line stays in entries as advisory metadata. Mirrored to greenfield additively; their 75-test internals suite + drift gate green.
Gates: K365 (eight-leg batch gate, routing-guard leg probed red-then-green); K274/K292/K354/K360/K362/F63/F66 re-pinned to the surviving contracts — each demonstrated red under the old pins before the re-pin. Suite 1142/0 + locking 3/3.
Simplify pass over the eval-4/5/6 arc — one predicate, one registry, one verdict
Four-angle review of the three field-eval batches (reuse / simplification / efficiency / altitude), applied under a byte-identity bar: consolidator, single, verifier, and render-lanes outputs proven byte-identical to pre-edit baselines, all touched gates re-probed.
One terminal-lane predicate. The on-disk terminal set (file_exists + non-foreign cid) was computed three ways in three modules — including inside the safety gate, where a drifted copy would silently diverge from the fill it guards. isTerminalLane now lives in state-lanes.cjs and all three consumers call it.
One severity-delta authority. The workflow's five-branch bash block re-derived the delta adjudication the tally already owned — the exact consumer-side re-derivation shape that produced the original upward/downward asymmetry. laneSeverityTally now emits severity_delta_verdict + severity_delta_message (ledger folded in, verdicts ok/explained_by_ledger/unexplained_excess/unchecked); the workflow echoes the message and routes on the verdict. K340's consumption pin repointed to the surviving sidecar-read literals (probed red-then-green).
One fill registry. The five conditional late fills in render-filled collapsed into a LATE_FILLS table iterated once (a null fill leaves the token for the unfilled-token assertion); _declaredScope() serves both scope consumers; render-lanes hoists the per-render invariants (graph split, global signal parse) out of the lane loop and defers the cross-lane mention scan until a lane actually matches.
Smaller fixes. Signal refresh no longer recomputes forever on a checked-and-empty "{}" cache (only null/absent/empty-string trigger recompute, and the restore persists via the sibling writers' skipGates idiom); the memory-signal matched-files cap is a named exported constant shared by producer and lane-view; correction-echo id matching normalizes case/punctuation while reporting original spellings; the suite's three copy-pasted lane-seeding blocks became one seed_lane_reviews helper.
Sixth greenfield calibration report: a silent-wrong-output cascade with every gate green — the worst failure class this system can have — plus seven adjacent defects. Validation corrected three mechanism diagnoses before code moved: the blamed path check is fine (their exists=false was mid-run timing, itself unrecorded narration); the lease refusal was a same-role render-inspect-rerender self-collision their proposed correlation-id key would not have prevented; and the god-node "self-contradiction" is a producer filling matches[] from the graph-GLOBAL list while god_node_match is diff-anchored.
No render ships a placeholder (Q1). The field consolidator synthesized from a literal {lane_files_newline_separated} — a block envelope_health deliberately doesn't monitor. Every render path now hard-fails (exit 2, token named) on any unsubstituted {snake_case_token} in the FINAL string; the grammar measured zero false positives on clean envelopes. The check immediately caught one of our own gate fixtures rendering a consolidator against an empty registry — the exact field shape.
Disk outranks status (Q2). The real cascade root: all five lanes sat in_flight (the substance-check transitions never ran) while five 30KB reviews existed on disk, and BOTH the consolidator's lane-files fill and assert-consolidator-dispatched keyed on status. The fill now falls back to the on-disk terminal set (file_exists + current cid) with an in-block warning naming the skipped step, and the gate fails loudly when outputs exist but nothing reached substance_pass — a safety gate that passes beside five on-disk reviews is not a safety gate. Truly-empty runs stay green.
The lease knows targets and says what it guards (Q3). Stamps carry target (agent:workflow_type); refusal fires only on same-target collisions (legacy stamps stay protective), and the message names the stamped correlation id and sanctions the inspect-loop override — a stamp records render intent, not a dispatch.
Stalled lanes get a deadline (Q4). A lane sat 8 minutes at a 232-byte stub with zero signal (subagent events are unusable for lane identity — 1,454 agent:"unknown" records). state lane-stall-check compares each registered lane's output substance against envelope-mtime age (config review.lane_stall_minutes, default 10) and names the resume path per stalled lane; wired at consolidate-entry. Names, never auto-dispatches.
The lens hint sees sliver skew (Q5). The hint fired only on a one-lane partition; the field's 2/14/21/24/184-LOC structural partition (75% top-lane share, four slivers) sailed past it while the mandate enumerated four review axes. A skew trigger (top share ≥ review.lens_hint_skew_share 0.6 AND ≥2 lanes at/below review.lens_hint_sliver_loc 25) now emits the same hint with a sliver-skew preamble — structural path only, unsized lanes excluded, silent on balanced partitions.
God-node matches mean matches (Q6). matches now comes from the diff-anchored god_node_matches; the global top-fan-in list rides additively as global_god_nodes. The agent contract always read matches as the matches — the producer was the liar.
Scope label says what it is (Q7). files_source: "working-tree" on a clean tree read as a defect report; the no-artifact-no-range value is now "changed-files-union" with a note naming the composition (merge-base committed + working tree + untracked). Zero consumers pinned the old string.
Corrections carry a ledger (Q8). A half-applied correction passed every internal self-check and was caught only by an external count-diff. Correction dispatches now name finding ids and the corrected sidecar echoes incorporated_ids[]; state assert-correction-applied makes partial application arithmetic (missing[]/extra[]; an absent echo fails — a completion claim without the id ledger is indistinguishable from a stale state). Same agreement-over-presence shape as the promotion ledger, applied to the repair loop.
Receipts recorded: Verified Negatives produced its strongest steering yet ("counter deleted" → "silently redefined under an unchanged name"); eval-5's promotion ledger, lane-scoped signal (an ADR-002 binding fed a Critical in the owning lane), and rubric twin header all converted on first field exposure; the run's effect-size verdict read an honest medium. Relay at .devt/state/threads/relay-greenfield-eval6-2026-08-26-5d8338aa.md.
Field eval #5 batch — change-aware anchors, honest severity, ledgered promotions (K361 + K362)
Fifth greenfield calibration report, on a run that exercised the eval-4 batch (which converted fully: signal populated 8/8, sliced map live, scope denominator present, sidecars at ZERO drift after one run of the pinned trailer). Every claim validated against their artifacts; the graph-layer charges all held — one worse than reported — while two proposed fixes were redirected to mechanisms that would actually have fired, and one "defect" resolved as designed-but-illegible.
Change-aware symbol assembly (K361). The 32-slot args cap spent 20 slots on scanner-visitor boilerplate from the exact 6 files the plan itself declared corpus-blind, and truncated the branch's two core symbols — inverting the tool's value precisely where the change was. Two assembly paths existed and only the brief path was ever hardened: the args path was a raw-order slice(0, 32). Now getDiffSymbols carries per-symbol corpus-blind attribution, and the args merge demotes blind-file symbols last, ranks the remaining pool (diff symbols + graph-resolved topic symbols together) by changed-line mention count with the brief path's test-demotion discipline, and cuts once. Plan telemetry args_symbols_blind_demoted; the brief path and its K354 pins untouched.
Anchor-quality calibration note. effect_size: "large" beside god_node_match: false is popularity-driven fan-in, not a semantic-severity verdict — the field operator hand-wrote that override into all eight envelopes in three layers. augment-impact-map now emits a calibration section on exactly that scalar pair (the report's own proposed trigger — god-node-dominated anchors — would not have fired on the run that motivated it: every anchor was is_god_node: false). Deduped against the existing census-based calibration branch — one note, never two.
Consolidator graph pointer (K362). The consolidator's rendered envelope carried no graph_impact while its template assigned severity-recalibration and drop/keep adjudication that need the map — and envelope_health's threshold correctly-but-illegibly reported absent + healthy. The template now carries a {graph_impact_pointer} block: a by-reference Read directive (with byte size) when graph-impact.md exists, the artifact's own absence notice otherwise. No inline body; no classifier change needed.
Promotion ledger. The workflow mandated upward severity promotion while the tally's upward branch was a mandatory STOP with no escape — correct synthesis was guaranteed to trip a full-severity STOP (the downward branch always had an explanation hatch). The field consolidator had already invented the fix unprompted: a well-formed severity_promotions[] ({id, from, to, reason}) that nothing consumed. Now formal: the template dictates it, lane-severity-tally reads it and emits promotion_ledger {count, net, ids} + promotion-adjusted lane totals, and both workflow branches compare against adjusted totals — the ok line names the promotions; STOP fires only on excess the ledger does not cover. Promotion is an arithmetic identity.
Lane-scoped memory signal. First receipt on the eval-4 signal refresh: populated 8/8 — and broadcast: five lanes wrote near-identical explain-aways of the same two global docs. Lane envelopes now carry a lane-scoped view (docs whose matched_files intersect the lane's set, plus cap-saturated docs that can't prove non-membership); zero-bind lanes get a one-line checkable claim, never {}. Single-dispatch and consolidator keep the full signal. Honest limit stated in code: path-based matching cannot route lens-semantic bindings — the field's L6→L7 prose routing is the designed cover for that class, and it worked.
Rubric twins announced. The md5-identical rubric pair is deliberate (one rubric grades the consolidated review; the separate pin exists as an independent override point) — but nothing said so, and a sharp reviewer read the _parallel suffix as a broken promise. The materialized parallel copy now opens with an identical-BY-DESIGN header, present only while the pins agree; a diverged project override gets its own body unannotated.
Corpus-blind list machine-readable. 26% of the run's scope had no graph nodes, knowable only from a prose blockquote. corpus_blind_files now renders one-per-line under a stable heading in graph-impact.md (sliced per lane), so membership is a grep, not an inference.
Also recorded: the run's calibration bomb — zero of 87 findings cite graph provenance — is the first on-rail graphify receipt, held with its confound (the anchors were the garbage fixed above; the next run measures the repaired configuration). The reporter's two self-disproofs (digest verification strict; residue nulls = no output yet) both check out against the implementation. Their sync --check ask is recorded, receipt-thin, deferred. Relay at .devt/state/threads/relay-greenfield-eval5-2026-08-26-5d8338aa.md.
Field eval #4 batch — the map is sliced, the signal refreshes, the denominator is injected (K360)
Fourth greenfield parallel-review report, validated artifact-by-artifact against their .devt/state/ before any code moved. The positives converted receipts this arc was waiting on: 7/7 first-dispatch lanes, one correlation-id chain, and the narrative guard catching a REAL silently-dropped finding body (47-finding run, one finding counted and indexed but bodiless while the prose claimed "listed above" — repaired in 6 calls). Three of the report's six defect diagnoses were wrong in mechanism; validation redirected each to the real defect.
Memory signal refresh (F1). The reported "split-brain" (<memory_signal>{} beside populated <memory_affects> in all 7 lanes) was a cleared cache, not a design flaw: reset-soft clears memory_signal_json, and refresh-scope-context restored the other three scope caches but not the signal — context init was its only writer. The bare {} is the shape reserved for "memory layer genuinely unavailable," so every agent burned an is-memory-broken check. The signal computation is now extracted (computeMemorySignal, one computation, two writers) and the refresher restores a null/empty signal; a populated one is never touched. Deliberately NOT a JSON-notice fallback — a brace-wrapped notice parses, would classify "populated", and would mask the degradation envelope_health reports honestly.
Per-lane graph-impact slicing (F2). The reported "duplicated verbatim" map was never duplicated — it rode UNSLICED: the full 23KB global map was ~47% of each ~50KB lane envelope, 6 of 7 lanes paying for sections covering other lanes' files. render-lanes now slices per lane: headline kept, sections kept when they mention the lane's files (path, or basename only when unique across the partition) or no partition file at all; sections anchored solely to other lanes' files are omitted with an in-block count + full-map pointer. Provenance headers survive verbatim; the corpus-suppression stub path and single-dispatch full inline are untouched. Kill-switch dispatch.lane_graph_slicing: false.
Declared-scope denominator (F3). Lane envelopes carried no total scope count anywhere, so completeness claims got seeded from whatever number a lane could find (field: "complete across 161 files" from a governance-scan count). Every lane block now carries <lane_scope_context> ("declared review scope: M files across K lanes; this lane owns N", M from the one scope parser with registry-union fallback) and the consolidator envelope carries <declared_scope> as the pinned completeness denominator.
Lane sidecar shape pinned (F4). The seven field sidecars kept the dictated core perfectly uniform and drifted on everything the write trailer never specified — the lane key spelled three ways, improvised duplicate count fields. The trailer now dictates the FULL top-level shape (pinned "lane":"<id>", no other top-level fields, null score carries score_null_reason), and lane-severity-tally emits sidecar_shape_warnings[] for improvised keys and missing/wrong lane identity — warn-only, agreement over presence.
needs_human machine-visible (F5). The reported "empty revisions[]" was refuted — the array was populated and drove the repair loop the same report praises. The real gap: NEEDS_HUMAN items existed only as prose beside a bare criteria_needs_human count with zero machine consumers. verification.json now carries needs_human[] (questions verbatim), the verifier contract requires it whenever the count is > 0, and a sidecar-schema check flags count↔array disagreement.
Staleness hardened (F6). Task comparison is whitespace-normalized (a re-typed multi-line task is the SAME task; exact-compare false-flagged taskChanged), and auto-reset-if-stale now persists its outcome ({ts, source:"auto_reset", ok, acted, reason} to gate-trace) — the field's acted=0 claim was unverifiable because the outcome lived only on stderr; the next such report is artifact-checkable.
Template + docs (F7/F8). python-fastapi template gains a "Pin Hand-Written API Examples" testing pattern + an "Example Drift" smell entry (the branch's biggest finding cluster was hand-written responses= examples drifting from response models; the pytest guard itself is project-side) — mirrored additively into greenfield's rules with baselines refreshed. GRAPHIFY.md documents the hook-yield pattern: project-installed graphify-orientation hooks should stand down during active devt workflows (the eval-2 neutralizer kills obedience, not injection tokens; the hook is the project's to scope — a paste-ready guard prefix rides in the relay).
Relay at .devt/state/threads/relay-greenfield-eval4-2026-08-25-5d8338aa.md: shipped items, the three diagnosis corrections (so their memory doesn't preserve wrong mechanisms), and their action items (hook scoping — second ask, now paste-ready; state.project_owned for their 29 root files; the pytest guard). Gate K360 covers all six code items; the F1/F2 legs probe-reverted red-then-green.
Docs-currency sweep — every doc surface audited claim-by-claim against the code
Four parallel auditors verified CLAUDE.md, README.md, and all eight docs/ references against HEAD; ~46 confirmed drifts corrected (report-then-apply, each reality re-verified before editing). The load-bearing ones: GRADER.md still taught a 5-axis rubric (current: 8 axes A–I, no F, materialized in-project — not repo-path <rubric_path>); GRAPHIFY.md listed the impact plan as evictable when it deliberately SURVIVES eviction (the "evicted" envelope state depends on that); STATE-RULES.md was missing the fifth audit bucket (project_owned), three canonical subdirs, ~18 contract filenames, and six allowed-pattern rows; AGENT-CONTRACTS.md pointed the verifier dispatches at a file they no longer live in and cited a KEEP-IN-SYNC comment that doesn't exist (the real pin is K357's normalize-diff); CLAUDE.md's release steps omitted marketplace.json — the omission that live-tripped the v0.245.0 release; README advertised a nonexistent --focus=security and one-version-old rubric defaults. Hand-maintained counts that drift every release (module/workflow/assertion totals) were replaced with countless phrasing or "1,100+"/"40+". New CLI-REFERENCE entries: state lane-residue, state lane-sample, graphify augment-impact-map; list-lane-outputs now names its literal file_exists/cid_match fields (a field evaluator misread a nonexistent .exists key). DISPATCH-RECIPES leads with dispatch run.
Surfaced by the audit and then fixed on explicit approval (DEF-013, closed): memory.preflight_mode's schema default has been block since the memory layer shipped and the config-drift banner asserts "fail-secure default = block" — but the hook carried a warn fallback and read only the raw project config, so every project that never set the key silently ran soft mode. The hook now resolves the mode per the 3-level contract its own comment always claimed (defaults ← global ~/.devt/defaults.json ← project), with block as the unconfigured fallback; memory.enabled and shared_roots_coerce ride the same layered read. Explicit warn/off still work and still trip the drift banner. A new HOME-isolated smoke leg pins the unconfigured→deny path.