Skip to content

refactor(git-agent): split tracker mechanics into generated GitHub references - #339

Merged
dean0x merged 121 commits into
mainfrom
feat/324-tracker-phase-2-contract-mechanics-split
Sep 16, 2026
Merged

dean0x merged 121 commits into
mainfrom
feat/324-tracker-phase-2-contract-mechanics-split

Conversation

@dean0x

@dean0x dean0x commented Sep 14, 2026

Copy link
Copy Markdown
Owner

Tracker Phase 2 of four. Internal refactor: the Git agent's GitHub mechanics move out of the always-loaded prompt into generated skill references, addressed skill-relatively through a single resolution point. No user-visible behaviour changes.

Problem Being Solved

dist/agents/git.md was 65,677 characters (66,180 bytes, 992 lines, 18 operations) re-sent on every Git spawn — a shared agent prompt file is billed per spawn (PF-026). Roughly 9,400 of those characters are GitHub-specific mechanicsgh invocations, header names, rate-limit detectors — interleaved with the provider-independent contract: each op's **Input:**, its **Output:** template, its **Degradation (D4):** clause, and the D4/D11 invariants.

Three live consequences:

  1. PF-023 had ~30 filename-composition sinks instead of one convergence point (GAP-10, CRITICAL). There was no single place a provider was resolved, so any future provider token would have to be threaded through every sink.
  2. The always-loaded D4/D11 cross-cutting text was GitHub-specific while the contract declares it provider-independent (GAP-03, CRITICAL) — two authorities on the secret-redaction path.
  3. src/assets/skills/git/SKILL.md (283 L / 9,205 ch, preloaded on the same spawns) carried two live safety contradictions: if [ "$REMAINING" -lt 10 ]; then sleep 60; fi contradicts D4's STOP-the-fan-out rule (waiting out a secondary limit extends the provider's penalty window), and gh release create … --notes "$NOTES" is an inline-body recipe where create-release mandates --notes-file after a scrub whose failure is a hard stop. Both were invisible to every existing guard.

Key Changes

Subtask What landed
T1 The generation substrate: expandVariants + the (module, op) registry in src/core/mds-variants.ts, compiledSkillRefsDir(), a third build destination in scripts/build-mds.ts, the 28-line provider-resolution preamble (ceiling 40), recursive collectSkillRefFiles, and the byte-budget four-shape table.
T2a/T2b The 10-op split (15 commits, one per op), the D4/D11 invariant-vs-detector separation, the three cross-cutting cuts (learn-conventions, the D10 publication gate, the marker legend), the SKILL.md cut, and github-api.md's five tracker sections.
T3 _partials/_tracker.mds + five host adoptions, the {ISSUE_REF}/{ISSUE_ID} vocabulary, the ### Handoff Values producer/consumer seam, the marker-literal removals, and the two dispositions.
T4 The installer's converge-not-merge reference overlay with an atomic per-unit swap, a recursive path-keyed prune, formatOverlaySummary as a named render site, and the packed-tarball manifest guard.
T5 This subtask: the guard battery, the extractor retarget, both fixture regenerations, and the docs sweep.

The numbers.

Measure Before After Gate
chars(dist/agents/git.md) 65,677 55,664 (56,075 bytes, 913 L) ≤ 55,750 ✅ (headroom 86 — ceiling lowered from 55,900 after the Mechanics-pointer condensing pass; ceilings lower-only)
chars(skills/git/SKILL.md) 9,205 6,581 (213 L) ≤ 6,600 ✅ (headroom 19)
tracker-scoped worst-case loaded set 77,824 (monolith) 77,719 ≤ 77,824 ✅ (headroom 105; max_op = manage-debt 5,007)
Provider-resolution preamble 28 lines ≤ 40 ✅
Generated references 0 13 (10 GitHub ops + 3 cross-cutting)
Containment exemptions 63 (29 + 11 #340. + 8 #341. + 15 #339-resolve.; tracked in tests/fixtures/containment-exemptions.ts), each individually justified asserted non-empty, still-needed and ≥ 40-char rationale
MDS reference modules 0 2
Partials / hosts 11 / 15 12 / 16 manifest entries, never bumped literals
github-api.md 16,052 15,812

The four-shape table was re-recorded multiple times as real content arrived. The per-provider single-file shape's disqualification margin now stands at +13.6% over the per-op loaded set (shape 3, 88,302 vs shape 2, 77,719) and +35.5% over the preloaded shape (shape 1, 65,187) — decisive rather than an artefact of stubs.

Breaking Changes

None user-visible. New files appear under the installed skill directory (~/.claude/skills/devflow:git/references/). Zero new files in any user's project tree; zero new prompts. Tracked = #{n}, Depends on: #{n}, 42-jwt-auth.{ts}.md and issue: 42 are byte-identical under github, each pinned by its own assertion. No Jira or Linear literal exists anywhere outside the provider map.

Decisions made where the plan was ambiguous

Every one, so a reviewer can reject any of them explicitly rather than discovering it.

T1 — substrate

  • (a) dist/skills/git/references WAS added to ALLOWED_OUTPUT_DIRS (D-SKILLREFS-ALLOWLIST), with a new HostVariant member 'skill-refs'. Routing the third destination around resolveOutputDir would have made the allowlist a partial gate — true for two destinations, bypassed for the third.
  • (b) ADR-007's "four neutral values" — AMBIGUITY RECORDED. ADR-007's body does not enumerate four neutral values; its subject is retiring an on-demand script. The transferable part is its discipline: a missing artifact degrades to a neutral value, never to a fallback path. The preamble states four outcomes, of which only the first two are neutral. If a reviewer holds that "four ADR-007 neutral values" meant something else, this is the block to change — 5 of the 28 preamble lines.
  • (c) T1's reference stubs deliberately carried no ## Operation: anchor, because the anchor breaks a 'sole'-mode lookup until that guard is repointed. T2a landed both halves in one commit, as required.
  • (d) No new printed build line. An output line nothing asserts is an artifact with no consumer (ADR-003).
  • (e) output-name: is REFUSED on a reference module, not ignored: its filenames come from the op registry, so silently dropping a key honoured on the other two variants is the authoring trap the empty-value refusal already guards against.
  • (f) Reference outputs are pruned recursively, bounded at MAX_REFERENCE_SWEEP_DEPTH = 8; empty directories are left in place (removing them races a concurrent build's mkdir).
  • (g) AC-1.2 scope-fence narrowingexpandVariants( and (module, op) were legalised and are named in a LEGALISED_IN_PHASE2 constant, asserted both present there and absent from the forbidden table, so the narrowing reads as a narrowing and not as a hurried deletion.
  • (h) BUDGET_LOADED_SET is a frozen literal, not TOTAL_CHARS. Those are equality baselines that move in every regeneration commit; a budget derived from them would follow the artifact down and end up asserting "the current size is the current size".
  • (i) The loaded-set budget could not be met by cutting git.md alone — measured, not assumed. That is why P2-S8's github-api.md cut is load-bearing for AC-2.5 rather than tidy-up.

T2a — the unsampled half

  • One BEYOND-TABLE SKILL.md cut, plus one condensing. The named cuts alone land ~700 characters over budget. ## Anti-Patterns was cut (every row restates a rule stated once above it; references/violations.md is the named authority). ## When This Skill Activates was not cut — it was condensed: the five bullets collapsed to the one-line body that still stands at SKILL.md:22-24, since the frontmatter description: already drives activation. No rule was removed — only a second or third copy, or a restated one. The file lands at 6,581 of the 6,600-char budget, so no further cut is owed. If a reviewer rejects the ## Anti-Patterns cut, the budget cannot be met and BUDGET_SKILL_MD must be re-derived, never raised.
  • Nine pre-existing D11 bypasses in references/github-api.md are frozen by exact text in KNOWN_GITHUB_API_INLINE_BODIES (D-INLINE-BODY-EXCLUSIONS) rather than silenced by narrowing the guard back. Both arms are asserted — a tenth offender goes red, and an entry that stops matching goes red — so the list can only shrink. Follow-up issue: fix(git-skill): nine pre-existing D11 inline-body bypasses in references/github-api.md #340.

T2b — the sampled half

  • DR-20 deviation — ACCEPTED. The artifact says gh repo view appears only in publication-gate.md. SG-8 forbids moving post-review-summary / post-resolution-summary mechanics, and their step-3 probe lines are exactly such mechanics, so the literal reading is unsatisfiable without a move the phase prohibits. Implemented as "only in publication-gate.md and the two operations that name it" — the original scope property plus the new file, strictly stronger than "the literal exists somewhere". The literal reading requires lifting SG-8, which is a plan change.
  • D11 chain deviation — ACCEPTED. The scrubber invocation (node …redact-secrets.cjs …) stays inline in git.md; only the && gh … half became && <the resolved provider's post command>. Making the containment control a file the spawn might not have loaded is PF-027's failure mode; making the post command provider-specific is the point of the split. Narrower than P2-S4's table wording, deliberately.
  • D-LOADED-SET-SCOPE — the max over ops term is taken over TRACKER_GITHUB_OPS. AC-2.5 bounds "the worst-case tracker spawn"; fetch-review-threads' 15,812-character load of github-api.md predates the split and is not a cost it introduces. It is recorded as its own table row rather than dropped.
  • Conflict C13 — the containment oracle was authored in T2 rather than T5, so the invariant existed before the text that had to satisfy it.

T3 — the command layer

  • _preamble.mds:75 (ADR-008's Iron Rule) — EXPLICIT NO-CHANGE. It is provider-agnostic in substance: it names no vendor, and every noun in it ("issue reading", "dependency reasoning") is already tracker-neutral. The disposition line is the deliverable; no code edit and no test — a faked mechanical assertion for a process-only row is what the plan forbids.
  • dynamic-build.mds:466-474 — RETAINED. The double-wrap premise does not hold: the op-side containment wraps the issue body, while the skeleton's wrap covers the command-constructed JSON.stringify(remainingTickets) the Design reader prompt quotes each round, which never passes through the op's Output block. It is the only containment at that site; removing it is PF-058's failure mode. Pinned mechanically so a later reader cannot delete it as redundant.
  • _preamble.mds:31gh deliberately KEPT. It is the concrete CLI an author reaches for, and naming it is what makes the sandbox denial legible. What changed is the scope: the denial is over any tracker CLI rather than reading as gh-only.
  • A second marker site the plan did not name — code-review.mds:333 restated the review-summary marker literal. §14.9's denylist row is unqualified, so it was neutralised the same way rather than allowlisted. An allowlist there would re-open GAP-20 for that marker.

T4 — the installer overlay

  • D-OVERLAY-FLAT-UNIT — the flat cross-cutting set is ONE unit, not one per file. The documents land directly in references/ beside hand-authored files the overlay must never touch, so there is no directory to rename; they get the same decision rule as a provider directory and are promoted by one rename per document. Recorded honestly at the code site: the promotion loop is the one place a mid-flight I/O error could leave the flat set partly refreshed. One unit per flat file was rejected — three documents always generated and read together would report three independent outcomes.
  • D-OVERLAY-MODE-SCOPE — the whole references/ tree is normalised to 0644, not only this run's files, because copyDirectory preserves source modes and a reference is read-only instruction text.
  • sweptOrphans reuse — the prune's removals fold into the existing sweep arrays via recordSweep(report, 'reference', …) and inherit formatSweepSummary's render site, rather than needing a third report field.

T5 — this subtask

  • The github-status-lines.txt re-capture (option A), user-authorised 2026-09-14. See its own section below.
  • D-STRADDLE-SPLIT — the two straddling samples are split into two samples each rather than repointed to one side. between() slices one string, so a start anchor in a reference and an end anchor in git.md cannot be expressed; dropping either half would silently shrink coverage of that operation.
  • D-CAPABILITY-PROBE-SCOPE — the capability-hoist guard's probe verbs are the session-scoped half of §14.3's capability column. The per-item capabilities (fetch by key, list the comments of one issue, comment, edit body) are the loop's payload; a guard that called them probes would report backlink-shipped-issues step 1 as a violation, and the only way to satisfy it would be to stop fetching the data the loop exists to fetch. They are named in a PER_ITEM_PAYLOAD constant rather than omitted silently.
  • D-EXTREF-SCOPEskills/git/SKILL.md's Extended References table keeps its references/tracker/{provider}/{op}.md row and gains no row for the three flat cross-cutting documents. Each is named from the agent at its point of use, which is the reachable consumer ADR-003 asks for, so a row is documentation — and ~120 characters of it is a real per-spawn cost in the one file preloaded on every Git spawn. Headroom is 19 characters; BUDGET_SKILL_MD was not touched.
  • D-PROOF-TRANSITION — the Phase-0 faithfulness gate (run the extractor over the b6928e5 snapshot, require the old fixture back byte-for-byte) cannot be re-run across a deliberate re-capture: the old bytes are the ones the retarget changes. The standing proof is the derivation test — the --unfreeze --out-dir case re-derives the whole fixture from the live tree on every run and compares byte-for-byte — and the inputs it derives from are themselves frozen (git.md by the git-agent golden, the generated references by the containment oracle's 101bda7 baselines). The retarget's own non-vacuity comes from STATUS_LINE_REFERENCE_FILES, enforced in both directions. A future extractor rewrite inherits the Phase-0 obligation unchanged, with the re-capture commit as its baseline tree.

The github-status-lines.txt re-captures (option A)

Two re-captures, both authorised and both spent.

First — authorised by the user on 2026-09-14 (e4876e0). §14.7 said the fixture was frozen "NEVER through Phase 3". Preserving it and doing P2-S4 are mutually exclusive: P2-S4's split line runs through the middle of two sentences the fixture samples, so no relocation of verbatim text can reconstruct the sampled bytes, and one sampled anchor's disappearance made the extractor throw rather than diff.

What the fixture actually measured is why this is affordable: extractStatusLines samples prompt-internal process steps (3. Extract items to add:, 1. Find last tag:, ## Version Names) — precisely the bytes this phase is chartered to relocate — not the user-visible surface. AC-2.10's four genuinely user-visible byte-identity claims are pinned separately and are untouched.

AC-2.1's second half and prefix-shippability clause (iv) are contradicted deliberately and are superseded by commit e4876e0.

This first authorisation was spent after e4876e0: the fixture was frozen again, and §14.7's successor text (recorded here and in the test file's header) said a further re-capture would need its own explicit authorisation.

Second — authorised by the user on 2026-09-15 (c0b9860), for the resolve-wave Mechanics-pointer condensing pass. That pass shrank two Mechanics-pointer lines the fixture samples; only fixture lines 134 and 161 changed, every other sampled line is untouched. This second authorisation is also spent. The fixture is frozen again from c0b9860; any further re-capture needs its own explicit authorisation.

Fixture diff: 11 insertions, 8 deletions. 17,914 → 17,709 bytes; 246 → 249 newlines. Per sample:

# Sample Outcome
1 D4 contract block 3 lines rewritten — P2-S4: gh → "the tracker"; the 403/429 + X-RateLimit-Remaining < 10 signal → "a provider-signalled secondary rate limit"; "GitHub's penalty window" → "the provider's"; the < 50 rung → "the provider's backpressure rung"
2 D10 step 2 bytes unchanged, now read from references/publication-gate.md
10 manage-debt split; moved half unchanged, retained half +3 lines (**Process:**, blank, the **Mechanics:** pointer)
13 gather-release-evidence +2 / −1 — the **Mechanics:** pointer added, the old per-commit gh api fan-out line gone ([DR-17] commit B)
14 learn-conventions split; moved half unchanged, one blank line lost at the join
19, 20 backlink-shipped-issues, ensure-traceable-issue bytes unchanged, read from their references
21 post-wave-report body −1 line — step 2 (local path resolution) stayed in git.md while steps 1/3/4 moved, so the reference's contiguous slice no longer spans it
24 post-wave-report marker bytes unchanged, read from the reference
dynamic-build.mds 1/2 anchor retargeted; line text changed by P2-S12 (the caller no longer restates the marker)
resolve.mds 4/6 Tracked=#Tracked={ISSUE_REF} (P2-S11)
the other 14 samples byte-identical

Golden-diff hunk classification (AC-2.1, git diff main...HEAD -- tests/fixtures/golden/git-agent.md)

Re-issued over the whole branch diff, not a single commit: the golden was regenerated twice — 2e019a5 after the contract/mechanics split and 10ac94c after the Scrutinize-pass agent fixes — so a classification of either commit alone is not a classification of what ships. 252 changed lines; 82 insertions, 170 deletions.

170 removed lines — 10 blank / horizontal rule, 160 content:

  • 150 are PURE MOVES — byte-identical in a generated reference or the git skill: learn-conventions.md 47 · tracker/github/setup-task.md 24 · ensure-traceable-issue.md 15 · post-wave-report.md 11 · publication-gate.md 10 · manage-debt.md 10 · decision-markers.md 9 · backlink-shipped-issues.md 9 · ensure-pr-ready.md 6 · fetch-issues-batch.md 6 · fetch-issue.md 2 · create-release.md 1.
  • 10 are NOT moves, and each falls inside a CONTAINMENT_EXEMPTIONS range. All nine git-agent.md ranges are exercised by this diff, none unexercised: :24-25 (two lines — the D4 remote-unavailable and secondary-rate-limit sentences), :28 (the < 50 backpressure rung), :45 (the D11 "to GitHub" scope sentence), :50 (the && gh … half of the scrub chain), :541 ([DR-17] commit B's per-commit fan-out), :715 (resolve-review-threads' D4 clause — extended, not cut), :910 (ensure-traceable-issue's D3 pointer — repointed after P2-S7 deleted the section it named), :968 (## Principles item 1), :990 (## Boundaries' gh pr create).
  • 0 unaccounted.

82 added lines — 28 blank / horizontal rule, 54 content:

Category Lines
The provider-resolution preamble — a new section (## Tracker provider resolution + ## Tracker input contract) 21
**Mechanics:** pointers, one per op whose body moved 11
The ### Handoff Values producer blocks in setup-task and fetch-issue 8
D4 rewrites — invariant kept inline, GitHub signal deferred to the provider (:24-25, :28, :715, :968) 5
D11 rewrites — the scope sentence and the provider-post placeholder in the shell-discipline fence (:45, :50) 2
The two summary ops naming references/publication-gate.md (DR-20) 2
Cross-cutting reference namings — the legend pointer to decision-markers.md, setup-task's 1b/1c pointer to learn-conventions.md 2
Scrutinize-pass and detector-strip rewrites — ensure-pr-ready's ALWAYS-ON contract line (4b), ensure-traceable-issue's untrusted-input contract (:910), ## Boundaries' replacement (:990) 3
Unclassified 0

Every hunk is a pure move or a named exemption. Nothing else is in the diff.

Per-op retained / moved table

moved counts the lines of the op's ### Process block in its generated reference; retained is the op's section length in git.mds. Each guard that reads the op is listed with the extraction mode it uses ([DR-18] — there is no default; 'sole' throws on a second authority, 'union' concatenates and counts).

Op Moved into tracker/github/{op}.md moved / retained Stayed in git.md Guards, and their mode
setup-task steps 1b, 1c, 2, 3 (conventions probe, issue-first lookup, branch-convention detection, branch-token derivation) 27 / 58 heading, prose, Input, steps 1a/4/4b/5, Output incl. ### Traceability conventions-commit arm (a) 'sole' over a git.md-only corpus; AC-0.10 issue-body containment file-scoped; byte-budget nameableFrom scans git.md
fetch-issue steps 2, 3 4 / 43 Input, step 1 (the #-strip ref rule), D4 clause, Output conventions-commit arm (d) 'sole'; AC-0.10 file-scoped
fetch-issues-batch step 2 — the single bounded GraphQL batch query, verbatim [DR-07] 10 / 56 Input, steps 1/3/4/5 (#-strip, ≤50, TRUNCATED, NOT_FOUND ({refs}), containment), D4 clause, Output ≤50 / TRUNCATED / ## Issues Batch pins 'sole' (unchanged); the gh api graphql pin 'union'; conventions-commit arm (c) 'sole'; AC-0.10 file-scoped
manage-debt the whole seven-step Process 12 / 28 Input, the D4 clause, Output 60000 archive threshold 'union'; D4 clause + (pending — 'sole'; D11 forward/reverse 'union'
create-release the ## Closed Issues enrichment bullet only 5 / 36 1a/1b/2/3/4, the ## Commits bullet, the 60000 cap, step 6, the D4 carve-out, Output AC-0.6b remote-I/O + D4 'sole'; D11 reverse 'union'
gather-release-evidence A: step 4 verbatim · B: rewritten in the reference, batch-first 7 / 35 Input, steps 1/2/3/5, the D4 clause (H12-asserted), Output H12 assertion, git.md section scan; [DR-17] ≤25 / no-fan-out, reference file scan
backlink-shipped-issues the viewer-login hoist and steps 1–4, plus the GitHub rate-limit and D11 chain signals from P2-S4 16 / 41 Input, step 0 (validation + VERSION normalisation), the ≤50 loop bound, the D4 clause, Output ≤50 bound 'sole'; D11 forward/reverse 'union'; P2-S4 one-home-per-detector, reference tree scan
ensure-traceable-issue the whole three-step Process 18 / 27 Input, the D4 clause, the D3 section list, the untrusted-input contract, Output D11 forward/reverse 'union'; AC-0.6b 'sole'
post-wave-report steps 1, 3, 4 (marker probe, body composition + cap, scrubbed post) 15 / 57 Input + input descriptions, step 2 (local path resolution), the D4 clause, Output 60000 cap pin 'union'; AC-0.10 external-thread file-scoped; D11 forward/reverse 'union'
ensure-pr-ready step 4b verbatim (exists_open + render_pr_link) 12 / 48 steps 1/2/3/4a (the D11 PR-body sink)/4c/5/6, a one-line ALWAYS-ON contract, Output gh pr create … --body-file + D11 'sole'; D11 bypass scan, corpus-wide

Cross-cutting cuts:

Cut Moved to Size Stayed in git.md Guards, and their mode
P2-S5 cut 1 references/learn-conventions.md (57 L, 3,627 ch) the whole **Process:** block heading, prose, Input, a pointer saying it loads only when .devflow/conventions.md is absent, the D4 clause, Output, **Commit boundary:** Guard 2's four bound pins 'union'; conventions-commit arm (b) stays sink-wide
P2-S5 cut 2 references/publication-gate.md (13 L, 1,114 ch) the whole ## Publication gate (D10) section nothing — the two summary ops name the file "section exists" → joined corpus; the [DR-20] successor pair (new); the other eight D10 its unchanged on git.md
P2-S4 tracker/github/backlink-shipped-issues.md § Provider signals (GitHub) the D4 rate-limit signals, gh availability, the concrete scrub-then-post chain every D4 and D11 invariant the two threshold pins 'union'; a new cross-cutting negative guard, section-scoped over git.md

Why a guard's mode changed, in one sentence: a guard whose literal left git.md must read the union (GAP-21 — guard classes move with the text); a guard whose literal stayed must not widen, because 'sole' throwing on a duplicated ## Operation: anchor is the signal that a contract acquired a second authority.

Test inventory

Every guard, the known-bad sample it must fail on, and the commit in which it was proven red.

Guard File Known-bad sample Proven red in
Byte budget: six assertions (3 budgets, preamble ceiling, single-naming-line, seeded second line) tests/tracker/byte-budget.test.ts a seeded second references/tracker/ naming line appended to the compiled agent 4a2c11b (landed with 6 red), 17ffa60 (3 → green)
Containment oracle — zero unaccounted lines tests/tracker/containment.test.ts a baseline line present in no target → ['probe.md:1']; an exemption silences exactly its own range → unaccounted [2], linesScanned 1 08c1190
Inline-body guard (INLINE_BODY_SHAPES, joinContinuations) widened on pattern and scope tests/git-agent.test.ts SKILL.md:211 gh release create … --notes "$NOTES" — already in the tree fcec744 (landed red on purpose)
## Operation: anchor vs the 'sole' corpus tests/git-agent.test.ts the live collector: "operation 'setup-task' not found in corpus" ×3 6f7230f
GAP-25 sleep 60 tests/tracker/containment.test.ts committed probe over the pre-split baseline: exactly 3 sites permanent probe (H10)
GAP-25 ≤50 branches single authority tests/git-agent.test.ts a seeded second statement → count 2 (live count 1) permanent probe
Unquoted-heredoc guard tests/guards/heredoc-quoting.test.ts pattern probe (<<EOF, <<-MSG match; <<'EOF', <<"EOF", <<< do not) and a seeded unquoted heredoc through the same collector 82d8b86
AC-2.13 legend set relation tests/git-agent.test.ts a seeded D42 row with no definition → ['D42'] ac9bd3e
Frozen-exclusion ratchet (an exclusion that stops matching is red) tests/git-agent.test.ts fired for real: two github-api.md exclusions went stale and the guard went red until they were deleted 16baec2
[DR-17] batch-first release evidence tests/tracker/containment.test.ts "the bounded sequential fallback must name its own limit" (≤25 absent) and a per-commit gh api loop present before 9c704df; permanent probe over the baseline finds exactly 1
[DR-20] successor pair (4 its) tests/git-agent.test.ts expected [] to equal ['post-resolution-summary', …]; a seeded third op naming the gate before 7fedbb1
P2-S4 no provider detector in a cross-cutting section tests/git-agent.test.ts 6 live sites listed by the collector; permanent probe over the baseline asserts >= 6 before bcff97d
P2-S4 one home per moved detector tests/git-agent.test.ts expected [] to have length 1 for X-RateLimit-Remaining\ header < 10` before bcff97d
[DR-19] shared-literal registry, both arms tests/tracker/containment.test.ts a seeded restatement in a provider file → ['tracker/github/probe.md'] 7fedbb1
_tracker.mds adoption (AC-2.9, two-armed rule) tests/build-mds.test.ts 10 violations across 5 hosts, both defines missing before caae772
Per-define non-emptiness (GAP-44) tests/build-mds.test.ts a hollowed-out define body through the same slicer caae772
No <!-- devflow: in any dist/commands/*.md tests/build-mds.test.ts 2 live sites (dynamic-build.mds, and code-review.mds — which the plan did not name); a seeded restatement in a temp copy of a real dist file before bbf0936
_engine.mds neutral wording (C9/C9b) tests/build-mds.test.ts the exact pre-neutralisation literal before bbf0936
AC-2.10 byte-identity ×4 + the two-sided grammar pairs tests/dynamic/depends-on-grammar.test.ts 15 failed / 5 passed against the stashed pre-change tree before 1953114
Seam Direction 3 (GAP-15 producers) tests/seams/command-agent-input.test.ts the real collector over the committed pre-split baseline reports exactly ['ISSUE_BRANCH_TOKEN','ISSUE_ID','ISSUE_PR_LINK'] permanent probe
Reference overlay, 20 tests tests/installer/reference-overlay.test.ts import-level red at 47352d6, then the naive additive-copy shape [DR-05] rejects: 11 of 17 failed 47352d6 + a behavioural run against the naive shape (never committed)
Packed-tarball reference manifest (Guard 6e) tests/packaging.test.ts a packed list with one manifest entry removed, through the same collector b48919f
capability-hoist [DR-11] tests/guards/capability-hoist.test.ts two seeded bad fixtures (identity probe in a loop; a per-item issue-type metadata lookup) plus a hoisted-form fixture that must not fire; and a position-blind collector run listing the corpus's 7 real hoisted probe sites 179ba71
provider-scope: Jira/Linear outside the provider map tests/guards/provider-scope.test.ts seeded foreign literals in a module and a dist command; a probe proving the allowlist silences its own block and nothing beyond it; a stale-allowlist arm 179ba71
provider-scope: mcp__ / user-facing "MCP" tests/guards/provider-scope.test.ts seeded vendor literals; the first live run found src/assets/agents/test.md's legitimate Chrome tool allowlist, which is why the scope is the Git spawn surface 179ba71
provider-scope: no tools: key on the Git agent; AC-2.7 _mcp.md absence tests/guards/provider-scope.test.ts frontmatter-key collector; the tracker/github/ directory asserted present so the absence is an absence, not an unbuilt tree 179ba71
AC-2.7 reachability, both directions tests/tracker/containment.test.ts a seeded emitted file outside the registry; a mutated load-instruction template turns all three arms red 179ba71
guard-census (AC-2.6) tests/guards/guard-census.test.ts floor raised to 69 → red at 68; a seeded extra op name → roster red; a removed declaration must lower the count 179ba71
retired-wording Phase-2 denylist, scoped tests/guards/retired-wording.test.ts with the scope short-circuit removed the collector reports 7 live sites (_github.mds, github-api.md, git.mds, synthesize.md) — the scopes are load-bearing, not decoration; plus a dead-scope arm and an in/out-of-scope probe 179ba71
Extractor retarget non-vacuity tests/helpers.ts ref() refuses an undeclared path; the extractor refuses to return while a declared reference went unread dd42ea1

Prefix-shippability (§F.5-G)

(Suite figures below — Test Files/Tests — re-confirmed at dd30e3f (pre-merge scrutiny pass: comment, knowledge-base and CHANGELOG edits only since the 5ad16e6 Verification Gate).)

$ npm run build
  12 partial(s) skipped (no output-dir:)
  16 host(s) to compile:
  …
  compiled: src/assets/mds/git/_references.mds → dist/skills/git/references/ (3 file(s))
  compiled: src/assets/mds/tracker/_github.mds → dist/skills/git/references/ (10 file(s))
MDS: 16 compiled, 0 error(s), 0 warning(s)
  copied:  src/assets/commands/release.md → dist/commands/release.md
MDS commands build complete!
build exit=0

$ npx tsc --noEmit
tsc exit=0

$ npm test
 Test Files  130 passed (130)
      Tests  4541 passed (4541)
   Duration  43.94s
EXIT=0

$ npm run test:integration
 Test Files  5 passed (5)
      Tests  50 passed (50)
   Duration  38.49s
EXIT=0

$ npm pack --dry-run
npm notice total files: 394
npm notice package size: 1.0 MB / unpacked 3.3 MB
13 generated skill reference files packed (dist/skills/git/references/**)
exit=0

$ git status --porcelain
(empty)

Zero failures. Every red this phase declared is now green: the git-agent golden (red since T1's 17ffa60) and the three github-status-lines derivation its (red since T2b). No flake re-run was needed — none of the known load-sensitive tests (hud-render, capture-hooks memory-worker, compliance-e2e S16b, eager-memory-refresh, redact-secrets, ledger-ops, shell-hooks, decisions-usage-scan, goldens --out-dir refusal) failed. Nothing was excluded or skipped.

No threshold lowered (AC-2.3 / H3, belt-and-braces). git diff main...HEAD -- tests/, every numeric literal that decreased:

DECREASED  GIT_AGENT_BYTES:  66180 -> 56075     (regeneration commits 2e019a5, 10ac94c, then the resolve-wave Mechanics-pointer condensing pass at c0b9860)
DECREASED  GIT_MD_CHARS:     65677 -> 55664     (same commits)
DECREASED  GIT_MD_LINES:       992 -> 913       (same commits)
DECREASED  SKILL_GIT_CHARS:   9205 -> 6581      (T2a 82d8b86, same commit as the SKILL.md cut)
DECREASED  SKILL_GIT_LINES:    283 -> 213       (same commit)
DECREASED  FIXTURE_BYTES:    17914 -> 17709     (re-capture commit e4876e0)
raised     FIXTURE_NEWLINES:   246 -> 249

All six are equality baselines, each moved in the same commit as the file it measures — the only sanctioned moves. No floor was lowered: partial-count 11→12, issue-capture-contract-size 3→6, and four new entries (generated-reference-manifest-size 13, packed-reference-manifest-size 13, git-agent-guard-count 68, issue-pr-link-forwarding-sites 14). No budget constant was touched after it was pinned. toMatchSnapshot remains at 0 occurrences repo-wide.

Snyk: no scan was run. The Snyk MCP server has failed to connect for every subtask of this branch (ENOENT … snyk-macos-arm64 — the local wrapper binary is broken, a known issue). Not retried.

Reviewer Focus Areas

  1. The per-op retained/moved table above — the Phase-2 gate deliverable. Verify each op's contract lines stayed and only mechanics moved, and check each guard's declared extraction mode.
  2. The whole-branch golden diff (git diff main...HEAD -- tests/fixtures/golden/git-agent.md), read line by line. Not either regeneration commit alone — the golden was regenerated twice. The classification is above; the claim is that every hunk is a pure move or one of the 29 named exemptions. Anything else in that diff is a defect.
  3. The containment oracle's zero-unaccounted-lines result, including the moved github-api.md and SKILL.md lines, and the exemption list's second arm (no entry may cover a range that is in fact still contained).
  4. The D11 floor is still >= 8, the literal 8 untouched, and no numeric literal decreased anywhere under tests/ except the six baselines listed above.
  5. AC-2.13 — no surviving D{N} label lacks its definition.
  6. The overlay's shadow-independence, atomic per-unit swap and prune behaviour. AC-2.11 makes UAC-28 a release blocker.
  7. The two accepted deviations and the option-A re-captures — each is written above so it can be rejected explicitly rather than discovered.

Release readiness

  • AC-2.11 — release blocker. UAC-28 (a shadowed devflow:git must still receive the canonical GitHub references) is a release blocker, not merely an AC. AC-2.4a is GREEN — shadow SKILL.md kept, all ten tracker/github/*.md byte-equal to the generated source, and all 13 byte-equal on the canonical branch.
  • Two new install-time failure modes, both reported rather than silent:
    1. A unit that could not be refreshed — the previously installed files are left byte-unchanged and the install summary warns Could not refresh the generated references for "{provider}" ({error}) — the previously installed files were left unchanged. The install still succeeds (PF-009).
    2. A declared reference absent from the build — the one throw path in the overlay: Generated skill reference not found for declared reference "{relPath}": {absolute}. Run \npm run build:mds` to regenerate dist/skills/git/references/ before install.` Shipping an installer that silently omits mechanics the agent is instructed to load would move the failure to every user's first spawn.
  • The DEGRADED reason tracker mechanics unavailable is reachable by design from P2-S14; its runtime arm is Phase 3 (P3a-S14).
  • Phase 2 delivers no user-facing value. Record it in the release notes as an internal refactor.

PR checklist (D-B)

  • Test-inventory table present, with a red proof per guard
  • CHANGELOG.md ### Changed enumerates the internal changes
  • Prefix-shippability command sequence run and pasted
  • Release-blocker items named in the release-readiness section
  • _preamble.mds:75 NO-CHANGE disposition recorded (the disposition line is the deliverable; no code edit and no test — a faked mechanical assertion for a process-only row is what the plan forbids)
  • dynamic-build.mds:466-474 RETAIN disposition recorded and pinned mechanically
  • Clause (ii)'s live five-command walk-through (/plan → /implement → /code-review → /resolve → /release) is formally waived per issue Pluggable issue tracker (GitHub + Jira + Linear) — tracking #321 (decision 2026-09-13). Its mechanised file-residue half runs in tests/integration/clause-ii-file-residue.test.ts and is green.
  • Clause (iv) is REPLACED by the option-A statement above.
  • Snyk scan — not run; the Snyk MCP server failed to connect (ENOENT … snyk-macos-arm64). Not retried.

Follow-ups

Also fixes #340 — D11 inline-body recipes in references/github-api.md

The nine inline-body recipes (gh pr create … --body "…", gh pr review … --body "…", gh api … -f body=…, the GraphQL reply mutation) that Phase 2 had frozen by exact text in KNOWN_GITHUB_API_INLINE_BODIES are now rewritten to the scrub-then-post chain already used elsewhere in the file: compose to $DEVFLOW_BODY_RAWredact-secrets.cjs&& gh … --body-file "$DEVFLOW_BODY" / -F body=@"$DEVFLOW_BODY" (release notes stay on $DEVFLOW_NOTES / --notes-file). A head-of-file D11 note mirrors the existing D4 note and defers to git.md's ## Comment-sink scrub (D11) — the recipes implement the rule, they do not compete with it. The # VIOLATION: teaching samples keep only the defect each one teaches.

  • KNOWN_GITHUB_API_INLINE_BODIES is now empty. Both guard arms remain and are named collectors (collectUndeclaredOffenders, collectStaleExclusions) driven by the main guards and by known-bad probes seeded with synthetic offenders — including one at a sibling reference path to prove the file-scoping half — so the reverse arm is non-vacuous over an empty list (PF-018, ADR-024). The guard's matcher and corpus are described under ### Also fixes #341 below (ADR-025: widened in the same commit as the content it catches). it( count in tests/git-agent.test.ts is 77 (floor 73).
  • Eleven CONTAINMENT_EXEMPTIONS rows for the rewritten github-api.md baseline lines (88, 91, 313, 328, 334, 336, 504, 541, 545, 552, 638); baselines and goldens untouched; no ceiling raised. github-api.md is 17,259 chars (was 15,812) — a recorded, non-gated byte-budget row (D-LOADED-SET-SCOPE).
  • Quality gates: Validate PASS (4485/4485), Simplify, Scrutinize (fixed one P0: the head note paired release notes with the wrong file — now $DEVFLOW_NOTES--notes-file), Evaluate ALIGNED 6/6, QA PASS 7/7 including two mutation checks (an injected inline body and an injected stale exclusion each turn exactly one arm red).
  • fix(git-skill): three D11 inline-body sinks that INLINE_BODY_RE cannot see (continuation lines and --comment) #341 is fixed in this PR — see ### Also fixes #341 below.

Commits: 34b6346, c9180f2, f45d2b6, ed9b494, a715851 (KB refresh).

Also fixes #341 — the six inline sinks the D11 guard could not see

  • The D11 inline-body guard in tests/git-agent.test.ts joins backslash-continued commands before matching, names five shapes (long-flag --body|--notes|--comment; short-flag -b/-n/-c on posting verbs; api-field -f/-F/--field/--raw-field body=; unscrubbed-file --body-file/--notes-file with any argument other than the quoted scrubber variable; unscrubbed-api-file -F body=@ likewise), and scans every installed agent (dist-first), every generated tracker/git reference, every skill file, every compiled command and every rule (~225 files). Three probes prove each arm live: a shape-table probe, a known-bad probe over the never-regenerated 101bda7 baselines (17 hits in baseline/github-api.md, 1 in baseline/SKILL.md), and a corpus-reach probe on sentinel paths. KNOWN_GITHUB_API_INLINE_BODIES stays []; git-agent-guard-count floor 68 → 73.
  • RED proof at a715851: the widened collector found exactly six inline sinks — ensure-traceable-issue.md gh issue create … --body; manage-debt.md gh issue close … --comment and gh issue create … --body; skills/git/references/patterns.md gh pr create … --body; skills/review-methodology/references/patterns.md gh api … -f body=; skills/git/references/github-api.md --notes-file CHANGELOG.md. All six post the scrubber's output at this head. The tech-debt archive is one && chain (scrub → create → one scrubbed archive comment carrying the real successor number → bare close, no comment body), and the two gh … create --json number defects (a flag neither command has) are gone.
  • The review-methodology skill holds no posting recipe: reviews write reports; publication is the Git agent's post-review-summary under D10 and D11.
  • The D11 contract in git.mds names a comment attached to a close as a posted body (provider-neutral clause, +49 ch; one fixture-only golden regeneration, 65e5470).
  • No github-status-lines.txt re-capture was needed: the fixture samples manage-debt.md lines 11–18 and ensure-traceable-issue.md lines 15–19; the sinks sit at 54–70 and 26–46. Eight #341. containment exemptions (baseline github-api.md 149, 153, 189, 191, 193, 196–200, 257, 259) bring the total to 48.
  • Gates: full suite 130 files / 4,541 tests green (at 5ad16e6); loaded set 77,719 vs 77,824; mutation checks confirmed each guard arm goes red on exactly the named file/shape.

Shape 2b, confirmed 2026-09-15. The cross-cutting glossary references/decision-markers.md (1,681 chars) is an on-demand lookup, not a per-spawn load — the core agent inlines D4 and D11 because those must be loaded before the agent acts and says every other marker "is defined in" the glossary — so the classification D-CROSS-CUTTING-ON-DEMAND in tests/tracker/byte-budget.test.ts stands, the 78,824 figure remains a recorded row, and the per-spawn ceiling stays as a regression alarm that is lowered after each condensing pass (the devflow-wide diet is #342).

Also fixes: fence-aware section extraction and the post-wave-report containment gap (PF-063, AC-0.10)

  • extractOpSectionFromCorpus (tests/helpers.ts) terminates a section only at a ## line outside a fenced code block (backtick or tilde fences; an unclosed fence runs to end of text); collectUnfencedH2 owns the rule and tests/tracker/reference-structure.test.ts asserts zero stray unfenced ## across all 13 generated references, with known-bad probes in both directions. Census: 20 column-0 ## lines, 13 file headings, 7 fenced (manage-debt.md ## Items; ensure-traceable-issue.md heredoc and D3 template lines), zero stray. New floor min-fenced-h2 = 7. Live probes: the extracted manage-debt section reaches gh issue close "$old_issue"; ensure-traceable-issue reaches the create recipe and the D3 template.
  • Former file-scoped workarounds in tests/git-agent.test.ts (setup-task, fetch-issue, learn-conventions arm b, the ## Issues Batch header guard) use op-scoped extraction; the seam test's collectMissingProducers stays body-scoped because its probes mutate the body under test.
  • Guard 10 (AC-0.10 containment) is strictly op-scoped. Making it so exposed that post-wave-report, the last op in git.md, had satisfied set (b) only through the shared ## Principles trailer swept in by a file-scoped slice (true in the pre-split baseline too). Its step 2 now carries - The wave report MUST NOT reproduce verbatim or content (Principle 8). (+120 chars in the core agent, not the per-op reference — PF-027). Source commit 667c497 red on exactly the golden byte-equality; fixture-only ce491f9 regenerated the golden.
  • Branch aligned with main by merge commit 10ea0d5 (origin/main 33b730e, PR fix(flags): default pin-sonnet-4-6 and disable-bundled-skills to off #338); no conflicts.
  • Knowledge bases test-harness and tracker-references refreshed in 031e1bd.

Resolve wave (cycle 1, 2026-09-15/16)

  • Review 2026-09-15_2146 — ten focus reviewers, 109 findings triaged: 3 escalated (decided by the user), 73 FIX_NOW, 1 BY_DESIGN, 7 FIX_SEPARATE, 25 duplicates. All 76 fixable findings (73 FIX_NOW + 3 escalated) are fixed, across 33 commits.
  • Escalation decisions:
    1. The commands no longer claim a Git-agent grammar re-check — the Code agent's ISSUE_PR_LINK re-check is the only gate (immaterial once the $ISSUE sink is quoted).
    2. The wave round refresh names fetch-issues-batch, state fields only, and the op projects state.
    3. The tracker input contract states the merged-step-order rule.
  • Headline fixes:
    • Every compose→scrub→post recipe is one && chain.
    • manage-debt appends to the backlog body, so the size check and archive path are live, with the successor number validated before promotion.
    • github-api.md has one D4 stop-and-report spelling, fail-closed rate probes, observable batch truncation, a self-contained release recipe, and every expansion quoted.
    • The installer overlay reports four real end states, keeps the .old recovery copy when a restore fails, fails loud when the compiled tree is absent, stages process-uniquely under the converged subtree, and bounds chmodRecursive.
    • The manifest derivation and skill name live in core; HostPlan/VariantSection are typed totally.
    • The section extractor anchors both ends through the fence index, gitOp() derives through it (byte-neutral); fence grammar is probed and unclosed fences are asserted absent.
    • Guard census counts only live it( and refuses skipped/focused spellings; provider detectors are word-bounded; the heredoc matcher covers the shell grammar.
    • CHANGELOG/KB figures are corrected.
  • 7 deferred findings tracked on Tech Debt Backlog #23.
  • Full resolution summary at .devflow/docs/reviews/feat-324-tracker-phase-2-contract-mechanics-split/2026-09-15_2146/resolution-summary.md (local artifact).

Related Issues

Closes #324
Closes #340
Closes #341
Tracking: #321

…dule

Adds the Phase-2 generation substrate (P2-S13, P2-S2):

- src/core/mds-variants.ts gains expandVariants + splitVariantSections
  (DR-16), a closed VARIANT_MODULES registry with the 10 tracker ops, and a
  third allowlist entry / HostVariant for dist/skills/git/references. Both new
  functions are pure and return Result (applies ADR-013, avoids PF-014); the
  pair list is >= 8 from its first commit so parity over it is not vacuous
  (GAP-42, avoids PF-018).
- src/core/assets.ts gains compiledSkillRefsDir(), spelled from the build's own
  allowlist constant rather than a second hardcoded path.
- scripts/build-mds.ts compiles src/assets/mds/tracker/_github.mds into
  dist/skills/git/references/tracker/github/{op}.md. The reference strip
  verifies BOTH ends like the generator strip (avoids PF-061), every output is
  written tmp+rename (avoids PF-011), the plan pass sees every fanned-out
  destination, and orphaned references are pruned recursively after a clean
  build.
- tests/skill-references.test.ts: collectSkillRefFiles walks references/
  recursively, in the same commit that introduces the nested layout (AC-2.12).
  Its depth arm is proven on a seeded tree, not borrowed from the frameworks/
  entries, so one arm cannot carry a floor the other never touches.
- tests/guards/dist-agents.test.ts: the AC-1.2 scope fence is narrowed
  deliberately. expandVariants( and (module, op) are legalised and named in
  LEGALISED_IN_PHASE2; @if, the tracker-<provider>.md filename token,
  {provider}.md, variants: and the agent-host MDS directives stay forbidden.
- Manifest/harness follow-through: MDS_REFERENCE_MODULES + ALL_DISCOVERED_HOSTS,
  copyCommittedSources covers src/assets/mds, the dist/-staleness compare walks
  dist/skills recursively, and the allowlist refusal text is asserted against
  the exported table instead of a retyped literal (applies ADR-024).

Refs #324, tracking #321.
Authored deliberately RED — the budget is this phase's progress meter, and a
guard written after the cut measures the cut rather than steering it.

Six assertions are red at this commit, in two groups:

  EXPECTED RED UNTIL T2 (the meter):
    chars(dist/agents/git.md)  <= BUDGET_GIT_MD      65_677 > 55_900
    chars(skills/git/SKILL.md) <= BUDGET_SKILL_MD     9_205 >  6_600
    worst-case tracker spawn   <= BUDGET_LOADED_SET  78_408 > 77_824

  RED UNTIL THE PREAMBLE LANDS (next commit, P2-S3):
    the preamble sits between the D4 block and the publication gate, <= 40 lines
    exactly one line names a references/tracker/ path, inside the preamble
    the seeded-second-line probe for that collector

Every constant carries its derivation in a comment; none is a bare number, and
none may be raised to meet the artifact (§14.5). Characters throughout, `wc -m`
semantics, stated once at the top so chars and bytes are never confused.

The four-shape table is RECORDED, not asserted pass/fail — monolith, per-op
GitHub path, per-provider single file (disqualified), per-op without _mcp.md —
with learn-conventions.md and publication-gate.md as named 0 rows that T2
re-measures [DR-12].

The formula ↔ nameable-set check runs in BOTH directions from two independent
derivations: the declared MODEL_CROSS_CUTTING_REFS on one side, a scan of the
compiled agent on the other. One source for both would be a tautology. Modelled
on the compliance-compose bidirectional registries, with a count floor and a
seeded-extra-file probe (applies ADR-024, avoids PF-018).

dist reads are fail-loud; the preamble helper throws rather than returning a
sentinel, so a missing block can never be measured as zero lines. tests/tracker/
joins the literal-agent-paths SCAN_DIRS in the commit that creates it.

Refs #324, tracking #321.
Inserts a 28-line block (ceiling 40) at the blank line between the Degradation
contract (D4) block and `## Publication gate (D10)` in the generator host. It is
the ONE convergence point PF-023 requires, replacing a design in which a provider
token would have had to be threaded through every filename-composition sink.

What it establishes:

- Normalisation stated ONCE: trim, strip one pair of surrounding quotes, reject
  any character outside [A-Za-z], ASCII-lowercase, exact membership in
  {github, jira, linear}. Reject, never repair — `jira-cloud` does not become
  `jira`.
- A static token to directory map. The validated token SELECTS a hardcoded
  directory; it is never concatenated into a path, so no path is ever composed
  from an unvalidated value.
- Phase scope: the slot resolves manifest-only and defaults to `github`. The
  per-repo key, the reference-grammar corroboration and the configuration-file
  sink validators are Phase 3 and are named as absent, not implied.
- The neutral values, applying ADR-007's discipline that a missing artifact
  degrades to a neutral value rather than to a fallback path: the GitHub path is
  silent (no DEGRADED, no file read, no spawn), an unresolvable token and an
  absent generated reference each name their canonical DEGRADED reason and
  continue per D4.
- A `## Tracker input contract` block carrying the DR-11 capability hoist —
  capabilities and identity resolved once per spawn, before any loop, never
  probed inside one (EC-38).
- The Read-tool rule: absolute path, never `~` (the Read tool does not expand
  it), never cat/head/tail (avoids PF-035), with the size bound reading fully
  anyway rather than partially (EC-45).
- ONE load instruction, addressed skill-relatively on the existing
  `devflow:git` -> `references/...` form, and the never-fabricate literal
  adapted from src/core/compliance-compose.ts.

Turns three of the six red assertions in tests/tracker/byte-budget.test.ts
green: the preamble ceiling, the single-naming-line clause [DR-27(c)] and its
seeded-second-line probe. Adds the P2-S3 hostile-provider table — the
normalisation rule implemented exactly as written, run against
../../../etc/passwd, github/../../rules/devflow, jira-cloud, backticked and
interpolating tokens, empty, blank, 200 chars and multi-token input — proving
every one is refused and no rejected token yields a path.

EXPECTED RED and untouched: the three budget gates (until T2), and
tests/goldens/git-agent-golden.test.ts byte-equality (until T5's dedicated
regeneration commit). A golden mismatch means the source is wrong, never the
fixture. The frozen github-status-lines.txt fixture is unchanged — the insertion
point is outside every sampled range.

Refs #324, tracking #321.
…C13, AC-2.1)

The split's failure mode is text that is lost rather than moved. This lands the
oracle BEFORE any text moves: every non-blank, non-rule line of the branch's
starting tree must survive byte-identically in dist/agents/git.md, a generated
reference, or the git skill — or be named in CONTAINMENT_EXEMPTIONS with a reason.

Baselines are byte copies of `101bda7` captured once with `git show`; the test
itself never shells out. They are never regenerated: they must outlive T5's
regeneration of tests/fixtures/golden/git-agent.md, which after the split can no
longer answer "what did the branch start with".

Also seeds structural parity over TRACKER_GITHUB_OPS (>= MIN_VARIANT_PAIRS) and
per-reference non-emptiness (byte floor + the file must name its own operation).

Non-vacuity: two known-bad probes drive the real collector — a baseline line
present in no target, and an exemption that must silence exactly its own range.

Refs #324
…corpus

Each generated references/tracker/github/{op}.md now opens with `## Operation: {op}`
instead of a bespoke title. The anchor is load-bearing: the D11 forward/reverse
guards locate moved mechanics through extractOpSectionFromCorpus in 'union' mode,
which keys on exactly that heading, so a reference titled anything else is invisible
to the sink-class guards the moment mechanics arrive (T1 §7(c)).

Adding the anchor makes every sink-wide 'sole' lookup match twice, and 'sole' throws
by design. collectConventionsCommitPlacementViolations therefore takes two corpora:

  contractCorpus (git.md alone) for arms (a)/(c)/(d) — these pin an operation's
    CONTRACT, and git.md is its single authority; the text they pin never moves,
    so their corpus must not widen;
  sinkCorpus (git.md ∪ references) for arm (b) — a NEGATIVE check ("no commit --only
    in learn-conventions") that must not go blind when that body moves.

The seam test's gitCorpus was already git.md-only; the reason is now written down
next to it rather than inferred.

RED before this commit (the T1-predicted failure), on the same collector:
  operation 'setup-task' not found in corpus — cannot verify placement
  operation 'fetch-issues-batch' not found in corpus — cannot verify placement
  operation 'fetch-issue' not found in corpus — cannot verify placement

Refs #324
… (P2-S7)

RED ON PURPOSE — the fix lands in the next commit (R1: guard, then red, then change).

Pattern: `release` joins `pr`/`issue` and `--notes` joins `--body`, so
`gh release create … --notes "$NOTES"` is visible at all. `[^`\n]*` keeps a match on
one line, so `--body-file` / `--notes-file` still never match.

Scope: the hand-authored src/assets/skills/git/SKILL.md and its references/*.md join
the compiled corpus. SKILL.md is PRELOADED on every Git spawn, which made it the worst
possible blind spot.

D-INLINE-BODY-EXCLUSIONS: widening surfaced nine distinct pre-existing inline recipes
in references/github-api.md — generic gh pr/issue/api examples that predate D11 and sit
outside every Phase-2 cut table. They are frozen by exact text rather than silenced by
narrowing the scope back, with both arms asserted: a tenth goes red, and an entry that
stops matching goes red too, so the list can only shrink. A named exception is not a
weakened guard; a narrowed scope would have been.

RED proof, after the exclusions are applied (the single genuinely-new offender):
  D11 bypass: inline body form(s) found …: expected [ Array(1) ] to deeply equal []
  + [
  +   ".../src/assets/skills/git/SKILL.md: gh release create \"v${VERSION}\" --title \"v${VERSION}\" --notes "
  + ]

Non-vacuity: two inline probes now, one per arm — `gh pr create … --body "unscrubbed"`
and `gh release create v1 --notes "unscrubbed"`.

Refs #324
…7, GAP-25)

9,205 → 6,581 characters; BUDGET_SKILL_MD (6,600) goes GREEN here. SKILL.md is
preloaded on every Git and Code spawn, so everything in it is a per-spawn cost.

Moved (byte-identical unless an exemption says otherwise):
  D3 traceability template          → the generated ensure-traceable-issue reference,
                                      via a define in src/assets/mds/tracker/_github.mds.
                                      Its section headings are GitHub's Markdown, so it
                                      was provider-VARYING content in a provider-blind
                                      preloaded file.
  Standard Throttling / PR Comments
  / Releases                        → references/github-api.md

Rewritten, each with a CONTAINMENT_EXEMPTIONS entry giving the reason:
  :196  `sleep 60` DELETED — it contradicts D4's "STOP the fan-out, report THROTTLED".
  :190  the same contradiction in prose ("remaining < 10 wait 60s") — rewritten to D4's
        rule. Fixing the recipe and leaving the sentence would have been half a fix.
  :211  inline `--notes "$NOTES"` → `--notes-file` after the D11 scrub. This is the
        known-bad sample the previous commit's widened INLINE_BODY_RE went red on.
  :73   protected-branch list → a pointer to devflow:worktree-support, which owns it.
  :152  Related Issues row → {ISSUE_REF} vocabulary (the `#N` rendering is unchanged).

github-api.md carried the same D4 contradiction twice more (check_rate_limit and
batch_api_calls both slept 60s and continued) and posted release notes inline in
Complete Release Flow. All three are fixed in this commit: the `sleep 60` rule is
scoped to `git.md ∪ skills/git/**`, so a half-fix would have left the guard red.

BEYOND THE PLAN'S CUT TABLE, flagged in the exemption rationales: `## Anti-Patterns`
deleted and `## When This Skill Activates` compressed. P2-S7's `9,204 − 2,604 = 6,600`
derivation budgeted nothing for the pointers P2-S7 itself mandates (~700 ch of
add-back), so the named cuts alone land at ~7,300. Both removed sections restate rules
already stated once in the same file — and references/violations.md, already listed
under Extended References, is the named authority for git/PR anti-patterns. No rule was
removed, only a second and third copy of one. Headroom is 19 characters.

New guards, each with a known-bad probe that drives the real collector:
  `sleep 60` absent and `≤50 branches` stated exactly once across git.md ∪
    skills/git/** — proven non-vacuous against tests/fixtures/tracker/baseline/,
    the committed pre-split bytes (3 sleep-60 sites there), so the RED evidence is
    permanent rather than a reverted fix (H10);
  tests/guards/heredoc-quoting.test.ts — no unquoted heredoc delimiter under
    src/assets/, with the three deliberate hook-script sites frozen by file:line and
    both arms asserted, so a fourth goes red and a stale entry goes red.

SKILL_GIT_CHARS / SKILL_GIT_LINES re-baselined in the same commit as the file they
measure. CONTAINMENT_EXEMPTIONS is now non-empty and that is asserted.

Refs #324
…references (P2-S8)

github-api.md is loaded by fetch-review-threads and is the phase's worst non-tracker
one-spawn load. Its issue-shaped sections are tracker mechanics living in a
provider-blind file, so they go where the op that uses them will look:

  ### Fetch Issue with All Details   → tracker/github/fetch-issue.md  (keeps ISSUE_NUMBER)
  ### Extract Issue Data             → tracker/github/fetch-issue.md  (it parses ONE issue
                                       body; fetch-issues-batch's mechanics are a single
                                       GraphQL query, not per-issue body parsing)
  ### Create Issue with Labels …     → tracker/github/ensure-traceable-issue.md
  ### Tech Debt Issue Management     → tracker/github/manage-debt.md
  ## Branch Name from Issue          → tracker/github/setup-task.md

Every recipe moved byte-identically. Four lines did not, and each has a
CONTAINMENT_EXEMPTIONS entry:

  :137  `## Issue Operations` — a container heading with four destinations.
  :184  `gh issue comment … --body "$new_item"`      → --body-file "$DEVFLOW_BODY"
  :202  `gh issue comment … --body "**Continued in:"` → --body-file "$DEVFLOW_BODY"
        manage-debt is a D11 posting sink. Moving the inline forms verbatim would have
        created NEW D11 bypasses inside the tracker tree: the widened INLINE_BODY_RE
        freezes the pre-existing github-api.md sites by exact text, so a moved copy is a
        new offender by construction. Fixing them then made the two frozen entries stale
        and the guard's second arm went red until they were deleted — the ratchet working.
  :283  `## Branch Name from Issue` moved DEMOTED to `###`. extractOpSectionFromCorpus
        slices an op section at the next `\n## `, so a second level-2 heading inside a
        generated reference truncates every union-mode guard from that point on.

D-LOADED-SET-SCOPE, recorded at worstCaseReferenceLoad(): the `max over ops` term is
taken over TRACKER_GITHUB_OPS. AC-2.5 bounds "the worst-case TRACKER spawn" — whether
the split makes a tracker op cost more than the monolith. fetch-review-threads' load of
github-api.md is a cost that predates the split and is not one it introduces. It is
RECORDED as its own table row rather than dropped.

Four-shape table re-recorded on real content (T1's stubs made shape 3 look cheap):
  git.md 68,447 · SKILL.md 6,581 · worktree 2,942 · max_op 1,679 (ensure-traceable-issue)
  worst one-spawn TRACKER 1,679 · worst one-spawn NON-tracker 15,227 (fetch-review-threads,
  down from 16,052) · sum of all 10 references 6,164
  shape 1 monolith 77,970 · shape 2 per-op 81,328 (+4.3%) · shape 3 per-provider 84,134
  (+7.9%) · shape 4 = shape 2 in Phase 2

Refs #324
…, E10)

The Decision Marker Legend was 11 rows of glossary preloaded on every Git spawn.
Two of them are not glossary: D4 (degradation contract) and D11 (comment-sink scrub)
define labels whose controls the agent must already have loaded before it can act, so
making either definition a file the spawn might not have is PF-027's failure mode.
Those two stay inline and are now the ONLY definitions of their labels. D1–D3 and
D5–D10 re-home verbatim to a generated references/decision-markers.md (1,681 ch).

The legend names the file by its skill-relative path, so the reference has a reachable
consumer (ADR-003). That path is not under references/tracker/, so the single-naming-
line assertion — exactly one line of git.md composes a tracker mechanics path — is
untouched, and it is still green.

NEW: a second reference module, src/assets/mds/git/_references.mds → the root of
dist/skills/git/references/. Two mechanics it needed:

  `subdir: ''` — cross-cutting documents are provider-independent, so they land in the
    references directory itself. expandVariants skips segment validation for the empty
    subdir (splitting it yields one empty segment every name rule rejects) and emits a
    flat relPath. Both arms are now asserted, so neither is untested.

  `kind: 'fanout' | 'named'` on VariantModule, defaulting to the STRICT 'fanout' so a
    module cannot dodge the floor by forgetting a field. MIN_VARIANT_PAIRS is unchanged
    at 8 and now applies PER MODULE (the build already expanded per module, so this
    matches what it enforced) and to fan-out modules only. A count proves nothing about
    a named document set: nothing ranges over it, each document is named at exactly one
    site, and a floor there would forbid the first cross-cutting document rather than
    sharpen any assertion. `named` is not an escape hatch — a new test pins that every
    module under tracker/ is 'fanout'.

AC-2.13 lands as a SET RELATION over named collectors, with a known-bad probe:
  every D-label used in git.md is defined in the inline legend ∪ decision-markers.md;
  the inline legend defines exactly {D4, D11}; the two definition sets are disjoint.
The AC was drafted as "referenced ⊆ defined in git.md's inline legend", which this cut
makes unsatisfiable by construction — moving those definitions out IS the cut. The
relation above is its stated property ("no surviving label lacks its definition") over
the places a definition may now live. Definition rows are stripped before collecting
references, so a definition never counts as its own use.

Refs #324
…e (P2-S6, P2-S10)

Steps 1b/1c/2/3 move verbatim into the generated setup-task reference; the
contract (heading, prose, Input, steps 1a/4/4b/5, Output) stays in git.md.
The Output block gains the P2-S10 producer lines so T3's issue_capture_contract
has a producer for PR link, branch token and ISSUE_ID.
…ce (P2-S6, P2-S10)

Steps 2 and 3 move verbatim; step 1's `#`-stripping rule is ref-grammar
contract and stays in git.md, with the Output block gaining the P2-S10
producer lines.
…reference (P2-S6, DR-07)

Step 2 — the single bounded GraphQL batch query — is the op's only
provider-specific mechanic and moves verbatim. The `#`-strip, the ≤50 bound,
TRUNCATED and NOT_FOUND stay in git.md as contract. The [DR-07] pin follows the
text to the union corpus, literals unchanged.
…ce (P2-S6)

The seven-step Process body moves verbatim; Input, the P0-S9 D4 clause and the
Output block stay in git.md. First commit to disturb a github-status-lines
sample (#8, "3. Extract items to add:"): the three fixture-derivation `it`s are
EXPECTED RED until T5's authorised re-capture; every sibling baseline `it` in
that file still passes.
…reference (P2-S6)

Only the `## Closed Issues` enrichment bullet moves; tag creation, the release
create, the notes composition, the D4 carve-out and the Output stay in git.md.
…(P2-S6 commit A)

DR-17 commit A: step 4 — the merged-PR closingIssuesReferences lookup — moves
byte-identically into the generated reference. Input, the D4 clause and the
Output stay in git.md. The batch-first rewrite lands separately in commit B.
…S6 commit B)

DR-17 commit B / GAP-26: the moved step resolved closing references with one
`gh api` call per commit — up to 100 remote calls for a 100-commit range.
Replaced in the reference with a batch-first `closing_refs_for_commits` query,
PR-number dedup and a ≤25 bounded sequential fallback. The rewritten baseline
range is named in CONTAINMENT_EXEMPTIONS; its RED proof and the H12 assertion
that the D4 item-degradation clause stays in git.md land with it.
…ference (P2-S6)

The viewer-login hoist, the marker check, the back-link post and the throttle
move verbatim. Step 0's input validation, the VERSION normalisation and the ≤50
loop bound are contract and stay in git.md alongside the D4 clause and Output.
…erence (P2-S6)

The three-step Process body moves verbatim; Input, the D4 clause, the D3
section list and the Output stay in git.md with the untrusted-input contract
restated on the contract side. The moved `## Traceability Issue Template (D3)`
heading is demoted to `###` so it no longer truncates union-mode extraction of
that reference — the T2a hazard — with a containment exemption for the change.
… (P2-S6)

The marker probe, the body composition and the post move verbatim; the local
WAVE_REPORT_PATH resolution, the Input descriptions, the D4 clause and the
Output stay in git.md. The 60000-char cap pin follows the text to the union
corpus (§14.3 classes size_cap as a provider fact), literal unchanged.
…ence (P2-S6)

Step 4b moves verbatim — the open-PR lookup and the `Closes #{n}` link line are
provider mechanics. Steps 4a (the D11 PR-body sink) and 4c, and the Output,
stay; git.md keeps a one-line ALWAYS-ON contract in their place.
…erence (P2-S5 cut 1)

DR-15: the bounded scan, its untrusted-string discipline, the heuristics, the
file template and the post-composition verbatim check move into the generated
references/learn-conventions.md, so Phase 3's Tracker agent can NAME the block
instead of keeping a second hand-maintained copy of security-relevant text.
The retained op says it is loaded only when .devflow/conventions.md is absent.
Guard 2's four bound pins follow the text to the union corpus in this same
commit, literals unchanged, and MODEL_CROSS_CUTTING_REFS gains the two rows the
bidirectional check needs.
…ference (P2-S5 cut 2, DR-19)

The `## Publication gate (D10)` section moves into references/publication-gate.md,
named from the two summary operations and from nowhere else. The negative-scope
D10 `it` is replaced by the [DR-20] successor pair (named-from-exactly + probe
scope), both non-vacuous, one `it` becoming three so AC-2.6's guard count rises.
The [DR-19] shared-literal registry lands with it: seven normative sentences of
the three cross-cutting references, positive and negative arms, with a seeded
restatement probe.
…tectors (P2-S4, GAP-03)

The always-loaded contracts keep every rule — STOP on a secondary limit,
THROTTLED, the item-degrade rules, the unconditional fail-closed scrub, the
mktemp-per-invocation rule, the &&-never-a-pipeline discipline and the
scrubber invocation itself (PF-027). What leaves is the SIGNAL: the 403/429
rate-limit body, the `X-RateLimit-Remaining` < 10 and < 50 rungs, GitHub's
penalty window, the `gh` availability condition and the concrete post command.
They are stated once in the GitHub reference of the tracker op that owns the
fan-out. A new negative guard holds the cross-cutting sections at zero detector
sites, proven red over the committed pre-split baseline; the two threshold pins
follow the text to the union corpus with their literals unchanged.
… in all ten ops

ensure-pr-ready carried the pointer as a clause of step 4b while the other nine
used the standard sentence. One wording, ten ops, no path composed in any of
them — the single `references/tracker/` naming line stays in the preamble.
… (P2-S9)

Two zero-arg defines replace five divergent inline issue-parse rules:

- issue_ref_grammar()      L1 command grammar: permissive, provider-blind
                           token scan; ISSUE_REFS forwarded VERBATIM. Ships
                           the two-armed GitHub foreign-shape rule from day
                           one (AC-2.9) — a well-shaped ref renders #{n}; any
                           other shape is neither coerced nor dropped, the
                           Git agent emits the canonical §14.2 DEGRADED
                           reason. Bare-number adjudication stays in the
                           agent (the Note: device).
- issue_capture_contract() the six keys a host reads from the Git agent
                           Output, each with a greppable producer in git.md:
                           ISSUE_REF, ISSUE_ID, ISSUE_CONTENT,
                           ACCEPTANCE_CRITERIA, ISSUE_PR_LINK,
                           ISSUE_BRANCH_TOKEN.

Adopters (named as a set in the manifest, never a count): debug,
dynamic-build, dynamic-plan, implement, plan.

Guards: all-hosts adoption (hostsScanned === 5) mirroring the P0-S22
compliance_gate guard; per-define non-emptiness — required phrase AND a
600-byte floor AND the Note: paragraph, because mds::undefined_var catches
an omitted define but not a hollowed-out one (GAP-44); a seeded
placeholder-body probe drives the same slicer. GAP-31: a new ordering
assertion proves the compliance gate still resolves before its first
consumer in all 6 importers, with **Produces:**/**Requires:** excluded as
the phase-ordering DAG (PF-039) and a seeded consumer-above-gate probe.

RED before this change (tests/build-mds.test.ts):
  x every adopting host carries both defines' expanded bodies (AC-2.9)
  AssertionError: _tracker.mds adoption violations:
  debug.md: issue_ref_grammar() body missing ...
  ... 10 violations across the five hosts ...
  expected [ ...(10) ] to have a length of +0 but got 10

partial-count floor raised 11 -> 12 (floors rise, never fall).

Refs #324
…-15)

GAP-15: ISSUE_PR_LINK and ISSUE_BRANCH_TOKEN were consumed with no producer,
so the ALWAYS-ON `Closes #{n}` rule died silently on the GitHub path too.
T2b added the producers; this wires the consumer.

code.md:
- the ISSUE_NUMBER input describes its VALUE as provider-canonical and ties
  it to the producer line `- **Issue ID**: {ISSUE_ID}`. The KEY NAME is kept
  at all 14 spawn sites (§14.5) — only the value changes.
- a new paste rule: ISSUE_PR_LINK is pasted verbatim only AFTER a shape
  re-check for the resolved provider (github: ^Closes #[1-9][0-9]{0,8}$).
  On mismatch it is neither repaired nor dropped — the canonical §14.2
  DEGRADED reason is emitted and the body falls back to ISSUE_NUMBER. The
  re-check runs at the sink as well as the source because a value that was
  well-formed when returned is still attacker-influenceable at paste time.

All three extractStatusLines samples in code.md (:93, :95, :99) are
BYTE-IDENTICAL — the rule is an insertion between :95 and :97, not an edit.

New two-sided seam tests/seams/pr-link-handoff.test.ts, modelled on
tests/resolve/duplicate-verdict.test.ts: producer describe over git.md,
consumer describe over code.md, and a joint describe asserting both sides
spell the same labels. Both files are read through resolveAgentSource, never
a literal agent path (AC-0.7). Includes an order assertion (re-check precedes
the DEGRADED line) — presence alone would pass on a paste-then-check body.

Seam Direction 3 extended from 3 keys to 6 (ISSUE_ID, ISSUE_PR_LINK,
ISSUE_BRANCH_TOKEN), with the collector extracted so a permanent known-bad
probe can drive it over tests/fixtures/tracker/baseline/git-agent.md: exactly
those three are missing from the pre-split baseline and the other three are
not, which is the RED->GREEN record without un-landing anything (H10). A new
parity test asserts the compiled issue_capture_contract() define and the
checker table name the same six keys, in both directions.

issue-capture-contract-size floor raised 3 -> 6. ISSUE_URL still has no
producer and stays out (ADR-003).

Refs #324
…GAP-27/47)

Replaces GitHub-bound reference literals in the command layer with the two
§14.1 identifiers, leaving every github rendering byte-identical (AC-2.10).

- docs-framework/SKILL.md :45/:106/:144 — {issue} -> {ISSUE_ID}. The example
  `42-jwt-auth.2026-04-07_1430.md` is byte-unchanged, which is what makes the
  rename provably a no-op.
- plan.mds — the design-artifact paths use {ISSUE_ID} and now name the same
  worked example as the docs-framework record; `Closes #{issue number}` ->
  `Closes {ISSUE_REF}` with the github rendering spelled out beside it;
  ISSUE_INPUT is described as the raw candidate token. `issue: 42` untouched.
- _ticket_template.mds — `**Depends on:** {ISSUE_REF}, {ISSUE_REF} (or "none")`
  with explicit cardinality on the writer side: zero or more, comma-separated,
  or `none`, and the github rendering `Depends on: #{n}, #{n}` stated.
- _wave.mds (reader) — the same cardinality; a `Depends on:` entry of foreign
  shape is NOT a blocker and emits the canonical §14.2 reason
  `TRACEABILITY: DEGRADED (foreign issue reference {ref})`. GAP-26/ADR-005:
  the wave pre-fetch becomes MANDATORY and exactly ONCE per wave for the
  immutable fields, and the per-round refresh is state-only via
  `list_by_filter` — ONE call per round rather than T, declared explicitly as
  an API bound and NOT a fan-out cap. That collapse is also what keeps the
  untrusted-body path to a SINGLE wrapping site, asserted by count.
- resolve.mds — `Tracked` carries {ISSUE_REF} in prose, in the phase diagram
  and in the resolution-summary template, each with the github rendering
  `Tracked = #{n}` retained verbatim.

New tests/dynamic/depends-on-grammar.test.ts (20 tests), modelled on
tests/resolve/duplicate-verdict.test.ts: writer<->reader for `Depends on:`,
writer<->reader for artifact naming, the wave fetch-discipline block, and the
AC-2.10 byte-identity battery ×4 pinned against the DEPLOYED dist text. The
foreign-shape reason is asserted present on the reader and ABSENT on the
writer — one authority, not two (PF-023). tests/dynamic/ joins the
literal-agent-paths SCAN_DIRS in this same commit.

RED before this change, run against the stashed pre-change tree:
  Tests  15 failed | 5 passed (20)
  x both sides name the same grammar token
  x the retired GitHub-bound placeholder is gone from the writer
  x both sides state cardinality explicitly
  x the foreign-shape DEGRADED reason is named on the READER side only
  x the pre-fetch is mandatory and once per wave
  x per-round refresh is state-only, one call, via list_by_filter
  x the bound is declared an API bound, not a fan-out cap (ADR-005)
  x the untrusted-body path has exactly one wrapping site
  x 1/4 `Tracked = #{n}` / 2/4 `Depends on: #{n}` / 3/4 `42-jwt-auth...`
  ... 15 total

SAMPLED BYTE CHANGED (1 of the 11 command-side extractStatusLines samples):
  resolve.mds sample 4/6, anchor '├─ Phase 9: Git agent (manage-debt)'
  old: ... -> backfill Tracked=# (or TRACEABILITY: DEGRADED on failure)
  new: ... -> backfill Tracked={ISSUE_REF} (or TRACEABILITY: DEGRADED on failure)
  The anchor itself is unchanged, so extractStatusLines still resolves it; only
  the sampled bytes differ. The other 5 resolve.mds samples and all 3 code.md
  samples are byte-identical. T5's §26 map needs this one row updated before
  the authorised re-capture.

applies ADR-005 · avoids PF-023

Refs #324
…rules (P2-S12)

Four dispositions from the P2-S12 table.

1. `dynamic-build.mds` — the restated wave-report marker literal is GONE. The
   caller now says only that "The Git agent deduplicates via its own marker",
   passes WAVE_ID, and states that the format belongs to the operation
   (GAP-20 / §14.3 marker_format). `code-review.mds` restated the
   review-summary marker for the same reason and is neutralised the same way;
   the DIST_FILES-wide guard below found it, which is the guard working.

   build-mds.test.ts C10's `toContain('<!-- devflow:wave-report wave:')` is
   REPLACED by a negative guard (§23): no `<!-- devflow:` literal in ANY
   dist/commands/*.md, distFilesScanned === 14, with a seeded restatement in a
   temp copy driving the same collector. A third arm asserts the three marker
   literals still live in the Git agent sink corpus, so the rule reads as
   RELOCATED, not deleted — without it, deleting dedup everywhere would turn
   the negative guard green.

   RED before the fix:
     x no dist command restates a `<!-- devflow:` marker literal
     AssertionError: dedup-marker literals restated in commands
     (GAP-20 — the operation owns the marker): expected [ ...(2) ] to have a
     length of +0 but got 2   [dynamic-build.md, code-review.md]

2. `_engine.mds` invariant #6 — NEUTRALISED WITH A GUARD (Conflict C10). It is
   a safety rule: read literally after Phase 3 it stops applying to a
   non-GitHub tracker. Now "No unauthorized tracker or remote side-effects …
   issues/PRs on the tracker … This applies to whatever tracker is resolved,
   not to one vendor." C9 asserts the neutral wording AND that the rule kept
   its force (`Sub-agents NEVER create issues/PRs on the tracker`,
   `beyond the ticket-authorized branch`) AND that the old literal is absent.

   RED before the fix:  x C9: no unauthorized side-effects doctrine — stated
   provider-neutrally (P2-S12, GAP-41)

3. `_preamble.mds` sandbox note — neutralised in SCOPE, not in content.
   `gh` stays named: it is the concrete CLI an author reaches for, and naming
   it is what makes the denial legible (dropping it would weaken a real
   sandbox statement to protect a vocabulary rule). What changed is that the
   denial is now over "no tracker CLI of any kind, `gh` included" rather than
   reading as gh-only. New C9b guard, non-vacuous against the old literal.

   RED before the fix:  x C9b: the sandbox note does not read as gh-only

4. `_preamble.mds` ADR-008 IRON RULE — explicit NO-CHANGE disposition (GAP-41 /
   Conflict C10). "Author ZERO deterministic feature code" is provider-agnostic
   in substance; the disposition line is the deliverable, not an edit. No test,
   by design — a faked mechanical test for a process-only row is what §0.10
   forbids.

dist-files-count occurrences raised 3 -> 5 (the floor VALUE 14 is unchanged;
the new marker guard spells it at two more sites).

SAMPLED BYTE CHANGED — and the anchor with it (dynamic-build.mds sample 1/2):
  anchor 'The Git agent deduplicates via marker' NO LONGER EXISTS.
  old: The Git agent deduplicates via marker `<!-- devflow:wave-report wave:{WAVE_ID} -->` — skips if already present. …
  new: The Git agent deduplicates via its own marker — it skips if a report for this `WAVE_ID` is already posted. …
  T5 must retarget this singleLine anchor (helpers.ts:571) before the
  authorised re-capture; extractStatusLines already throws earlier at sample 2,
  so this is a second retarget, not a new class of problem. dynamic-build
  sample 2/2 ('In WAVE mode, if no tracking-issue number') is byte-identical.

avoids PF-023 (one authority per marker format)

Refs #324
…P2-S12)

DISPOSITION for dynamic-build.mds:466-474: RETAINED, and the plan's
double-wrap premise does not hold. P0-S10 moved containment into the
fetch-issues-batch Output block, which wraps the ISSUE BODY. The skeleton's
wrap covers different bytes — the command-constructed `remainingTickets` /
`quarantined` JSON that the Design reader prompt quotes each round, which
never passes through the op's Output block. Removing it would leave that
site with no containment at all, which is PF-058's failure mode.

Pinned mechanically rather than left as prose so a later reader cannot
delete it as "redundant with the op-side wrap".

Refs #324
…obes (P2-S14)

Lands RED on purpose: overlayGeneratedReferences, generatedReferenceManifest,
formatOverlaySummary and sweepOrphanedReferences do not exist yet.

Covers AC-2.4a (shadow-independent overlay), AC-2.4b [DR-05] (atomic per-unit
swap, loud failure on an absent generated reference), AC-2.4c (shadow-injected
file pruned) and GAP-24 (stale prune, symlink skip, 0644 normalisation).

tests/installer/ is added to the literal-agent-paths SCAN_DIRS in this same
commit so that guard is non-vacuous over the directory from its first file.
…erm, and share the max reducer (resolve B14: performance-01, performance-04, complexity-10)

performance-01: the gate's exclusion of references/github-api.md
(D-LOADED-SET-SCOPE) is correct, but the recorded row it owes in return had
no anchor and drifted 280 ch inside this branch (17,259 recorded, 17,539
live). Add GITHUB_API_MD_CHARS as an equality baseline, measured and
asserted with toBe, re-pinned only by the commit that edits that file's
bytes, and give the file its own row in the table.

performance-04: the budget is denominated in characters and prices no
round trip, while each **Mechanics:** pointer converts cached prompt bytes
into a fresh sequential Read (PF-026). Record the pointer-site count, the
Reads added per spawn (max over tracker ops), and the three smallest
generated references in their own table with their own units. Recorded for
0342, not a gate: no ceiling, no floor, and no reference re-inlined.

complexity-10: the same 5-line max-reducer stood three times over
different op sets; extract maxOver(ops, measure) and leave three
one-liners. Printed figures unchanged.
…e (resolve B15: complexity-11)

The 510-line exemption table was two thirds of a 1,400-line file that also
holds structural parity, [DR-17]'s batch-first probe, the shared-literal
registry and AC-2.7 reachability. It is data, so it now lives beside the
other typed fixtures in tests/fixtures/ and containment.test.ts holds the
oracle that reads it.

Pure relocation: all 48 entries move byte-identically and in the same order
(sha256 3158cd62…b6dd over the extracted table, unchanged), the interface
moves with them, and the three arms that police the list — zero unaccounted
baseline lines, no entry for a range still fully contained, and the
MIN_RATIONALE_CHARS floor — are untouched and stay in the test file. No
floor, no ratchet, no numeric-floors.json edit: the only manifest entry
naming this file pins `const MIN_REFERENCE_CHARS = 80;`, which does not move.

The fixture's header records how to add a row, since #340/#341-style
rewrites append to it.
…air, interleave rule, and per-op containment pointers (resolve B31: complexity-06, complexity-01, security-04, security-10, consistency-08, regression-05)

complexity-06 is the funding cut: the 147-char `**Mechanics:**` boilerplate,
repeated identically at 10 op sites, collapses to a 56-char marker + deixis
("load this operation's provider reference"). The `## Tracker input contract`
section already defines what that pointer means and how to resolve it, so the
per-op restatement carried no information the always-loaded contract lacks.
`learn-conventions`' pointer is NOT boilerplate (it states a conditional load
and the ALREADY_EXISTS early return) and is left unchanged.

Funded by that cut, in the same commit:
- complexity-01 option 1 (user-chosen): one `## Tracker input contract` bullet
  states the merge rule once for all ten split ops — a loaded reference's steps
  carry the operation's own step numbers and interleave with the steps stated
  in the agent, executed in numeric order.
- security-04: `$DEVFLOW_NOTES_RAW`/`$DEVFLOW_NOTES` now have a producer in the
  D11 section, under the same "never a fixed path" mktemp rule as the body pair
  (added as its own sentence so the baseline D11 line stays byte-identical and
  owes the containment oracle no exemption row).
- security-10: the single-load sentence now reads "no other line composes a
  path from the provider token" — the flat cross-cutting pointers
  (decision-markers.md, learn-conventions.md, publication-gate.md) are static
  paths, not provider-composed, and a literal reading no longer degrades them.
- consistency-08: blank line before `**Mechanics:**` at `fetch-issue` and
  `fetch-issues-batch`, so all 11 pointers render as their own paragraph.
- regression-05: the per-op Principle-8 marker-neutralisation pointer is
  restored to `setup-task` and `fetch-issue`, the two highest-traffic issue
  producers, mirroring the form `fetch-issues-batch`/`fetch-review-threads`
  still carry. PF-058 records that this per-op pointer exists because the
  global principle alone once proved insufficient for exactly these ops.

Measured `dist/agents/git.md` (npm run build:mds):
  before  55,896 chars / 56,305 bytes / 905 newlines  (4 ch under BUDGET_GIT_MD)
  after   55,577 chars / 55,988 bytes / 913 newlines  (323 ch under the ceiling)
  freed   910 ch (91 x 10 pointer sites)
  spent   591 ch (merge rule 232, regression-05 148+138, notes pair 57,
          security-10 14, consistency-08 2)
  net     -319 ch. BUDGET_GIT_MD stays 55,900 — a later batch lowers the
          ceiling to the new size; a budget raised to fit the artifact is not
          a budget.

Expected RED until the fixture-only lane regenerates the golden (3 tests, all
in the golden-ritual file group):
  tests/goldens/git-agent-golden.test.ts  byte-equality vs the golden fixture
  tests/goldens/github-status-lines.test.ts  derivation byte-equality, and the
    `--unfreeze --out-dir` acceptance arm (same derivation)
GIT_MD_CHARS/GIT_MD_LINES stay green here because they measure the golden, not
dist; they move with the regen.

The frozen fixture was NOT re-captured. `--unfreeze --out-dir <scratch>`
derivation against tests/fixtures/golden/github-status-lines.txt touches
exactly the two authorised lines and nothing else:

  @@ -131,7 +131,7 @@
  -**Mechanics:** the provider reference for this operation carries the steps that talk to the tracker; load it as the tracker input contract directs.
  +**Mechanics:** load this operation's provider reference.
  @@ -158,7 +158,7 @@
  -**Mechanics:** the provider reference for this operation carries the steps that talk to the tracker; load it as the tracker input contract directs.
  +**Mechanics:** load this operation's provider reference.

applies ADR-025, ADR-003; avoids PF-026, PF-027, PF-057, PF-058, PF-063
…as no unclosed fence

resolve B13: testing-04, regression-02

testing-04 — the fence grammar `collectUnfencedLines` documents had three rules
no probe could see: a backtick fence's info string may not contain a backtick, a
closing run must be at least as long as the opening one, and a closing line must
carry nothing after the marker but whitespace. Inverting any one of them left
all 187 tests in tests/guards, tests/tracker and tests/seams green, which made
the function owning PF-063's structural remedy unfalsifiable (PF-018). Adds one
synthetic corpus per rule — plus the <=3-space indentation bound the open and
close rules share — each paired with the control document that engages the rule,
each proven red against the inverted rule.

regression-02 — nothing asserted that shipped files close their fences, even
though the grammar documents that an unclosed one runs to end of text. Past such
a delimiter every column-0 `## ` is payload, so union-mode section extraction
runs to EOF, absence assertions over the tail pass for the wrong reason, and the
fenced-`## ` non-vacuity floor counts UP as the corpus degrades. Exposes
`collectUnclosedFences` and asserts over the always-loaded set (the compiled
agent, its skill contract, and all 13 generated references), with a known-bad
probe seeding an unclosed fence into every corpus file so the absence cannot go
green on a dead collector.

Both collectors now read one private `scanFences` pass rather than a second
scanner that would drift from the first (PF-018); `collectUnfencedLines` keeps
its signature and behaviour unchanged.
…the successor number, quote issue expansions

reliability-01/security-03: every scrub-then-post recipe had compose → scrub →
post with only the last two links &&-chained, so a failed compose let the
scrubber scrub — and the chain publish — whatever the RAW file last held. All 11
compose sites are now the chain's first link; heredoc sites are wrapped in
{ ... } so the compose keeps its exit status, with the `## ` body lines still at
column 0 (PF-063).

reliability-03: the successor issue number parsed out of `gh issue create`'s URL
is checked to be a digit run BEFORE it becomes TECH_DEBT_ISSUE, so a malformed
value can no longer reach the archive comment, later post_scrubbed targets or the
Tracked field (PF-023 — the invariant belongs at the sink every caller passes
through). Folds in reliability-02's residual: archive failure now reports
TRACEABILITY: DEGRADED and returns without aborting, so the item still lands on
the still-open predecessor (KNOWLEDGE.md:156 — BY_DESIGN, no early return).

security-06: the four unquoted expansions the split relocated verbatim are quoted
where they now live; each changed baseline line has its own CONTAINMENT_EXEMPTIONS
row.

regression-06: ensure-pr-ready.md's header claimed it held "the open-PR lookup and
PR-link rendering of step 4b only ... the publication sink stays with the
operation" while the body carries all of 4b including the scrub-then-edit. The
header now describes the body; the D11 scrubber invocation stays inline in
git.md (PF-027), untouched.

GITHUB_API_MD_CHARS re-pinned 17,539 → 17,935.

(resolve B20: reliability-01, reliability-03, security-06, regression-06)
…e shared depth constant (resolve B19: typescript-08)

ref() took the closed list's union type and then tested membership against it,
so the refusal could never fire under its own signature and needed a widening
cast to be written at all. It now takes a string and narrows through an
isStatusLineReference type predicate: the arm that refuses is the arm that
produces the value the rest of the function uses, and the cast is gone. tests/
is outside `tsc -p tsconfig.json` today (#337), so the runtime arm was always
the operative gate — it is now reachable under the signature as well.

walkFiles was the third walk over the generated reference tree still carrying
its own depth literal and its own convention. It imports MAX_REFERENCE_SWEEP_DEPTH
from src/core/reference-sweep.ts and adopts that module's `depth > bound`
semantics with the walked root at depth 0, so the permitted depth is unchanged
and a breach is loud: the build throws, the sweep reports in `failed`, and this
one throws rather than handing a collector a corpus smaller than the tree it
claims to cover. An explicitly narrowed maxDepth stays a silent per-call-site
scope — that is what those callers asked for — and may only narrow.
… recovery copy when a restore fails (resolve B21: architecture-02, architecture-08)

OverlayFailure rendered every unhealthy outcome as "the previously installed
files were left unchanged", which was true of one of four reachable states.
It now carries an OverlayFailureState discriminant populated from what the run
actually did — installed-unchanged, not-installed, partially-refreshed (the
flat-set gap D-OVERLAY-FLAT-UNIT documents, with the refreshed/stale split),
and restore-failed — and formatOverlaySummary renders one true sentence per
state, exhaustively.

The displaced-unit restore no longer swallows its own failure through
.catch(() => undefined), so a failed recovery is distinguishable from a
successful one; the tracker prune then yields to a recovery copy this run is
still relying on, rather than deleting tracker/{provider}.old in the same run
that named it as the way back. Skipping is reported through the sweep's own
failed channel, so nothing claims convergence over ground it did not cover
(avoids PF-009, PF-015). The successful path keeps the identical prune call,
arguments and position.

The '(cross-cutting)' sentinel is gone: OverlayUnit/OverlayUnitRef are a
discriminated union, and a provider is identified by the registry's own subdir
(tracker/github) rather than a trailing path segment two modules could share.
… and document every throw path (resolve B24: architecture-01, typescript-06)

buildUnitStagingTree reads a unit's source directory before it can reach its
throw, so the whole-tree-absent case (build:cli alone, an interrupted build)
never reached it: every unit degraded to a reported failure and devflow init
returned success with an agent instructed to load references that were never
installed. requireGeneratedTree stats the compiled root ONCE before the unit
loop and refuses on ENOENT only, naming the path and `npm run build:mds` in
the shape the agent resolver already uses for the same root cause. Per-file
and per-unit failures stay reported through the B21 states (PF-009): a unit
directory absent under a root that exists is still a per-unit report.

overlayGeneratedReferences' @throws documented one path while the default
manifest and this refusal are two more; all three are now named with their
conditions in one block.
…rpora (resolve B25: testing-03, performance-07)

testing-03: PROVIDER_DETECTORS matched three substrings with line.includes(),
so 'gh ' fired inside `through `, `high `, `enough ` and `although `. git.md
carries none of those words today, which made the GAP-03 toEqual([]) green by
luck rather than by the property it claims — and a generated reference already
ships "reaches GitHub through `$DEVFLOW_BODY`". Replaced with a word-bounded
regex table carrying a justification per row (the LOOP_MARKERS/PROBE_MARKERS
shape), keeping the three detectors' intent: a backticked `gh`, the bare `gh`
command word, and the X-RateLimit header prefix. The collector now names the
matching row in every hit, and a new probe drives both directions — each row
against its own shape, and the four English words against none.

performance-07: gitAgentSinkCorpus() was rebuilt 18x and inlineBodyCorpus() 2x
per run for pure functions of an on-disk tree no guard writes to. Memoised at
MODULE scope (not inside a describe) so every guard, the fence-aware collectors
included, reads one cached corpus. The builders in tests/helpers.ts are
untouched — they keep their injectable root.

Counts: cross-cutting scan unchanged at live 0 hits / baseline 8 hits (no hit
lost, none gained); ~704 -> ~226 corpus file reads per run.
…be the ref() and walkFiles refusals (resolve B22: testing-09)

testing-09: extractStatusLines' gitOp() still sliced "to the next
`## Operation:`, else EOF" by hand — the construct Guard 10 was rewritten
to escape in 667c497, where a region wider than the operation let
post-wave-report satisfy a containment assertion through the shared
`## Principles` trailer. It now delegates to extractOpSectionFromCorpus
in 'sole' mode, so the boundary is the shared line-bounded, fence-aware
rule (D-FENCE-AWARE-BOUNDARY / PF-063) that the extractor and the
reference-structure guard already share. The stale justification comment
and the two call-site comments that restated it are replaced by what the
rule now is. PF-057: the old slice pinned layout, not semantics.

HARD GATE (byte-neutral): derivation-to-derivation, not against the
frozen fixture — `test:golden:update -- github-status-lines --unfreeze
--out-dir <scratch>` before and after, with every derivation input
(dist/agents/git.md, the generated references, code.md, dynamic-build.mds,
resolve.mds) checksummed identical across both runs. diff is EMPTY, 17377
chars both sides. tests/fixtures/ was never written.

Probes for the two refusals B19 (6b253b8) left unasserted:

- ref()'s refusal fired nowhere: every call site inside the extractor
  passes a declared path, so "ref() refuses an undeclared path" was a
  claim about unexercised code (PF-018). The reader is hoisted to a
  module-level statusLineRefReader() so the probe drives the real gate,
  not a copy of its membership test — RED on a real-but-undeclared
  reference (tracker/github/fetch-issue.md, so the refusal is proven to
  be about DECLARATION, not absence), GREEN control on a declared one.
- walkFiles' MAX_REFERENCE_SWEEP_DEPTH throw had no probe: RED one level
  past the bound, asserting the message names the directory and the
  bound; GREEN control at exactly the bound. Both derive their depth from
  the imported constant, so a literal cannot outlive a bound change.

Probes live in agent-source-resolver.test.ts rather than beside the
fixture: tests/goldens/ is the golden ritual's fixture-only lane, and a
behavioural probe parked there would ride along with a fixture-only
commit.
…hive path is live, project issue state for the wave refresh, state the 4b sink inline (resolve B32: performance-03 + E1/B20 follow-ons)

manage-debt's add_tech_debt_item now composes the fetched body plus the new item
into $DEVFLOW_BODY_RAW, scrubs, and edits the backlog BODY via `gh issue edit
--body-file "$DEVFLOW_BODY"` — one && chain from the first link. The body
therefore grows, so the MAX_SIZE=60000 probe is live work and
archive_tech_debt_issue's successor path is reachable, which is what the op's own
Process step 6 has always said. post_scrubbed stays for the archive comment; the
validate-then-promote chain and the report-only DEGRADED are unchanged. A failed
body read returns 1 rather than composing an empty body, which on this path would
have replaced the whole backlog with the single new item.

fetch-issues-batch's per-issue GraphQL aliases now project `state`, and the
mechanics render it as a `**State**:` line outside that issue's
<untrusted-issue-body> wrapper — a tracker-computed enum, not remote prose — so a
wave round can see a ticket closed out of band. ensure-pr-ready step 4b states
its D11 sink inline in the contract, provider-neutral, instead of only in the
generated reference a spawn can decline to load (PF-027).

Measured dist/agents/git.md: 55,664 chars / 56,075 bytes / 913 lines
(BUDGET_GIT_MD 55,900, headroom 236). max_op is now manage-debt at 5,007 ch;
worst-case loaded set 77,719 ch (BUDGET_LOADED_SET 77,824).

Frozen-fixture derivation (`test:golden:update -- github-status-lines --unfreeze
--out-dir <scratch>`) differs from tests/fixtures/golden/github-status-lines.txt
at exactly lines 134 and 161 — the two Mechanics-pointer lines B31 already
changed — and nowhere else; the live fixture was not written.

tests/goldens/* stay red until the fixture-only lane regenerates them.
…complexity-05)

promoteUnitStagingTree held two unrelated promotion strategies under one
name: the flat cross-cutting rename loop, whose guarantee is weaker because
its directory is shared with hand-authored references, and the provider
directory displace/rename/restore swap. The single JSDoc had to explain both,
which was the tell.

Extract promoteCrossCuttingUnit and promoteProviderUnit, each carrying the
half of the JSDoc that explains it (D-OVERLAY-FLAT-UNIT travels with the flat
half). promoteUnitStagingTree stays the exported entry point and keeps what
the two halves genuinely share: the recorded OverlayFailureState, the single
catch, and the staging-tree discard. It now dispatches on unit.kind with an
exhaustive never default.

Structural extraction only. Every statement moves verbatim -- the
displace-to-.old-then-rename order, the state advancement points (now written
through a RecordPromotionState callback so the dispatcher still owns the
value), and every rendered message are unchanged. Applies ADR-003: no
transitional names or tombstones left behind.

tests/installer/reference-overlay.test.ts passes unedited at 27 tests.
…rd the D11 and authority-scan non-goals (resolve B27: testing-06, testing-12, testing-14)

testing-06: UNQUOTED_HEREDOC_RE read only `<<` followed directly by an
uppercase word, so `<<eof`, `<<Body` and the spaced `<< EOF` form — the
shape `run-hook` already opens with, quoted — were unreadable. Widened to
the shell's own grammar (optional `-`, optional blanks, a delimiter whose
first character is neither quote nor backslash), with `(?<!<)`/`(?!<)` so a
bare-word here-string (`<<< value`) is not read as a heredoc that does not
exist. Non-goals recorded: expansion-formed delimiters, quoting that starts
after the first character, a delimiter split across a continuation, and a
digit-led delimiter. The probe now names each spelling in both directions.
No new live hit: KNOWN_UNQUOTED_HEREDOCS stays at its three entries.

testing-12: IN_COMMAND excludes `&`, `|` and `;` with no quoting state, so
`gh issue create --title "A & B" --body "x"` is cut at the `&` and matches
no shape. Recorded in the shape table's non-goals with the example, per the
reviewer's minimal disposition — the matcher is unchanged.

testing-14: gitAuthorityCorpus() cannot reach the generated tree where the
operative bound now lives as `head -50`. Non-goal recorded beside the
assertion; the corpus is NOT widened, which would only prove the literal
exists somewhere and would lose the scope property (ADR-025).

Also corrects the P2-S4 baseline probe's site census to the measured eight.
…nes.txt after the resolve-wave contract edits

Fixture-only regeneration for the git.mds contract edits landed by
4e672e2 (B31: complexity-06, complexity-01, security-04, security-10,
consistency-08, regression-05) and c1fec6c (B32: performance-03 plus the
E1/B20 follow-ons).

Measured figures, re-derived from the regenerated artifacts:

  dist/agents/git.md == tests/fixtures/golden/git-agent.md
    GIT_MD_CHARS      55,664  (JS .length)
    GIT_MD_LINES         913  (newlines)
    GIT_AGENT_BYTES   56,075  (stat -f %z)
  src/assets/skills/git/SKILL.md          6,581 ch / 213 L  (unchanged)
  src/assets/skills/worktree-support/SKILL.md 2,942 ch / 92 L  (unchanged)
  Total (all three)                      65,187 ch / 1,218 L
  tests/fixtures/golden/github-status-lines.txt
    FIXTURE_BYTES     17,527
    FIXTURE_NEWLINES     249  (unchanged)

The status-lines re-capture is the one authorised 2026-09-15: fixture
lines 134 and 161 only — the two `**Mechanics:**` pointer lines B31
rewrote. The diff was checked against that authorisation before the
fixture was kept. That authorisation is now spent; the fixture is frozen
again from this commit and any further change needs a new explicit one.
…wn the resolved tsx binary (resolve B33: testing-10, testing-11)

testing-10: TOTAL_CHARS/TOTAL_LINES were DEFINED as the sum of their parts and
asserted against that same sum, so the guard held for every state of the tree —
including one where all three parts had drifted. They are now pinned literals
(65,187 ch / 1,218 L) and the guard MEASURES the three preloaded files and
compares the measurement to them, which is the equality baseline the constants
were always meant to be. Known-bad probe: perturbing TOTAL_CHARS to 65,188 and
TOTAL_LINES to 1,219 turns the guard red (`expected 65187 to be 65188`);
restored before commit.

  tests/fixtures/golden/git-agent.md           55,664 ch / 913 L
  src/assets/skills/git/SKILL.md                6,581 ch / 213 L
  src/assets/skills/worktree-support/SKILL.md   2,942 ch /  92 L
  Total                                        65,187 ch / 1,218 L

testing-11: the four subprocess guards spawned `npx tsx`, which re-resolves the
binary per spawn and on a cold cache fetches it from the registry — registry
reachability was an unstated precondition of four guards whose subject is a
local script, inside 10-30s timeouts. All four now spawn the repo's resolved
node_modules/.bin/tsx, the same spelling tests/helpers.ts and
tests/build-mds.test.ts already use. The file's runtime drops 2,595ms -> 765ms.

Incidental to the above, not a separate change: the two SKILL.md paths and the
newline-count expression are each named once rather than respelled at the new
measurement site.

No assertion semantics changed and the guard count is unchanged at 14.
…d the fence-grammar guard to the roster (resolve B34: documentation-01/02/03/04/05)

documentation-01 (+consistency-04, regression-03): re-measure every drifted
figure at HEAD c0b9860 and state the BUDGET_GIT_MD ceiling beside the
measurement so the bullet cannot rot the same way. KB: the BUDGET_LOADED_SET
derivation's SKILL.md term 9_204 -> 9_205 (65_677 + 9_205 + 2_942 = 77_824,
the arithmetic it already claimed); github-api.md cited as pinned by
GITHUB_API_MD_CHARS rather than a number that rots; shape 2b recomputed from
the live formula with its components stated. Frozen ceilings (55,900 / 6,600
/ 77,824 / 40) left spelled out.

documentation-02: the provider's rate-limit signal is not "stated exactly
once" - the preloaded SKILL.md keeps the < 10 STOP threshold as a verified
load-bearing mitigation and each fan-out op's D4 clause names it inline.
Correct the CHANGELOG claim to what shipped; SKILL.md is not edited.

documentation-03: drop the directional word pointing "below" at a bullet
that renders above.

documentation-04 (+consistency-10, regression-04): fold the stranded
"Internal refactor" paragraph into the split bullet it belongs to, scoped to
that bullet, so ### Changed is one continuous list and no sentence disclaims
the user-visible entries after it.

documentation-05: replace "Zero user-visible change" on the scrub-then-post
bullet - it changes what the product does with a user's secrets - and add a
### Fixed entry for #340/#341.

Also appends fence-grammar (added by B13, 829f905) to the tests/guards/
roster in CLAUDE.md and docs/reference/file-organization.md, which both
listed eleven of twelve files.
…ounded chmod, honest module boundary (resolve B28: security-08, reliability-08, architecture-03, performance-05, security-09)

security-08: stagingDirFor used fixed names while build-mds.ts's tempPathFor is
pid-based, so two concurrent `devflow init` runs each pre-cleaned the other's
half-built staging tree and promoted whatever survived. Both names now carry a
per-process token (pid + base-36 timestamp), the same idiom tempPathFor uses.

reliability-08: the flat set staged at `references/.cross-cutting.tmp`, outside
the tracker/** subtree the prune converges, so a crash between mkdir and
promotion stranded a partial copy inside the installed skill indefinitely — and
chmodRecursive normalised its modes on every later install. Both staging names
now resolve under tracker/, where the prune reaches them; the prune has to be
what removes them, since the per-process token means no later run's pre-clean
looks at that name again. The successful path is byte-identical.

performance-05 (+ reliability-07): chmodRecursive walked unbounded while its
three siblings bound the same tree at MAX_REFERENCE_SWEEP_DEPTH. Imported, never
re-spelled, on the shared convention (root = 0, `depth > bound` is the breach),
and a breach throws into the overlay's existing mode-normalisation warn rather
than returning quietly over ground it never covered. Consistency, not an exploit
closure: Dirent.isDirectory() is lstat-based, so no symlink loop can be entered.

architecture-03: "converge, not merge" was stated unqualified while only the
tracker/ subtree converges. Qualified to the subtree, with the flat root's gap
named (a retired GIT_CROSS_CUTTING_DOCS entry keeps its installed copy) and a
prunable flat root recorded as a Phase-3 candidate. CHANGELOG.md already scopes
the shipped claim and is untouched.

security-09 (+ architecture-09): the stated boundary "must never touch" reached
further than the implementation — modes ARE normalised across the whole
references tree (D-OVERLAY-MODE-SCOPE), which ADR-024 corollary (b) permits
because the ownership guard protects deletion, not overwrite. Corrected to
"must never replace or delete". No behaviour change.

Probes: staging paths observed at the fs boundary carry the pid and lie under
tracker/; a cross-cutting staging tree stranded by a "crashed run" is gone after
the next overlay and named in pruned.removed; the chmod bound's in-bounds twin
normalises while its known-bad probe keeps 0600 and reports the breach once.
The flat-set mid-flight probe and the jira rename spy were re-spelled off the
retired fixed staging names.
…bes, observable batch truncation

security-05/reliability-06: `### Standard Throttling` read `gh api rate_limit`
with no fallback, so an unauthenticated, offline or already-limited probe left
REMAINING empty, `[ "" -lt 10 ]` errored, the branch that exists to stop the
fan-out was skipped, and the calls went out unthrottled — the exact condition D4
names first. All three probes in this file now pin the empty string and are read
through a digit-run `case` before they are compared; an unreadable one reports
TRACEABILITY: DEGRADED (rate-limit probe failed) and stops. The two optimistic
`|| echo "100"` fallbacks went with it: answering a failed probe with "plenty of
quota" is the same fail-open one layer down.

consistency-12: one D4 rule had three control-flow answers (`exit 1`, `return 1`,
`break`). The convention is now stated once in the head-of-file D4 note and every
site conforms — inside a function, echo the DEGRADED line then `return 1`, never
`exit`, which kills the shell that called the helper; at top level the echo IS
the response, and the call sits in the branch a healthy probe reaches, so a stop
cannot fall through to it. `check_rate_limit || exit 1` becomes
`check_rate_limit && for issue in ...`.

reliability-04: `batch_api_calls` broke out correctly per D4 but returned 0 and
printed only what it had collected, so a batch truncated at 3 of 40 was
byte-indistinguishable from a complete batch of 3. It now tracks attempted vs
total, still prints the collected results, and closes with
TRACEABILITY: DEGRADED ({reason}) — THROTTLED ({n} not processed) plus a non-zero
return, so the comment's "the caller reports THROTTLED" is something a caller can
actually detect.

consistency-05: `### Releases` was the only rewritten posting recipe whose fence
was not self-contained — it posted `--notes-file "$DEVFLOW_NOTES"` with nothing
in the fence producing it. Compose, scrub and create are now one `&&` chain like
every sibling; the prose that already carried the rule is unchanged.

B20 hand-off: the sibling unquoted expansions the same file still carried are
quoted where they live — `gh issue view $ISSUE`, `-F line=$LINE_NUMBER`,
`gh pr diff $PR_NUMBER` x2, `gh pr review $PR_NUMBER` x2, `gh pr view $PR`,
`gh run watch $RUN_ID`. The `### Query Violations` examples stay unquoted; they
exist in order to be wrong.

Ten new CONTAINMENT_EXEMPTIONS rows (six under the unquoted-expansions banner,
four under a new D4 banner, per the table's own "a new cause opens a new banner"
rule); the github-api.md:24 rationale is re-stated for the call site that
replaced `|| exit 1`. GITHUB_API_MD_CHARS re-pinned 17,935 -> 19,576.

(resolve B23: security-05, reliability-04, consistency-12, consistency-05)
…n, BUDGET_GIT_MD lowered after the condensing pass (resolve B35: documentation-06, documentation-08, consistency-13)

documentation-06: the KB claimed PR #339 "landed on `main`". It has not —
origin/main is 33b730e (PR #338) and #339 is open. The branch is only
*aligned* with main by merge commit 10ea0d5. Replaced with an explicit
"Status — NOT landed" note, since seven workflow commands load this KB up
front and could otherwise treat Phase 2 as shipped (PF-010, PF-025).

documentation-08: the per-provider disqualification margin was stated three
incompatible ways with no denominator ("+3.3% -> +8.0% -> +30.3%" in the KB,
"+31% to +41%" in this test). Replaced with one statement on a named basis,
read off the live table rather than hand-typed: shape 3 (88,302 ch) vs the
shipped shape 2 (77,719 ch) = +13.6%; vs shape 1 (65,187 ch) the same rows
read +35.5% and +19.2%. The table now prints BOTH percentage columns
("vs shape 1 (preloaded set)", "vs shape 2 (per-op loaded set)") so a margin
can no longer be lifted from it without its basis, and shape 1's label no
longer calls itself "today's monolith" — it is the current preloaded set and
has shrunk with every mechanics move since T1.

consistency-13 (+documentation-13): the `## Operations` table's "Fetch GitHub
issue" wording is recorded as a Phase-3 reservation in the KB's handoff
contract, together with SKILL.md:188's rate-limit literal. Nothing renamed in
git.mds — that is contract prose and every character lands against thin
headroom.

BUDGET_GIT_MD 55_900 -> 55_750. A ceiling is a regression alarm, re-derived
only DOWNWARD after a pass that actually cut the artifact; B31's Mechanics-
pointer condensing left 236 ch of stale slack, so the alarm was no longer
armed. 55_750 leaves 86 ch over the measured 55_664. The `budget-git-md`
ceiling entry in numeric-floors.json moves down with it (value + pattern
re-pinned together) — the permitted direction for a ceiling, not a floor
lowering; the manifest guard's probe still proves an increment goes red.

Carried hand-offs from earlier batches, all in the KB:
- B28/architecture-03: a prunable flat cross-cutting root recorded as a
  Phase-3 candidate beside consistency-13's row.
- B34: the stale "headroom is 4 chars as of ce491f9" gotcha re-measured.
- B31: the retired 147-char Mechanics pointer replaced by the shipped
  56-char line.
- B20/B32: the tech-debt archive chain brought to its end state — the issue
  number is validated into a local before promotion, and add_tech_debt_item
  appends to the issue BODY via `gh issue edit --body-file`, which is what
  keeps the size check and the archive path reachable.

Verified: npx vitest run tests/tracker/byte-budget.test.ts
tests/guards/numeric-floor-manifest.test.ts tests/guards/retired-wording.test.ts
-> 36/36 pass.
…en scratch-dir helper

CHANGELOG.md and the numeric-floor-manifest JSDoc example still quoted the
pre-B31/B35 55,900 ceiling after byte-budget.test.ts lowered BUDGET_GIT_MD to
55,750 — both now match the live constant. recordSweep's JSDoc said
"thrice-repeated" when a fourth call site (the reference-overlay sweep)
already existed; restated as the end state (shared by every call site, no
count to go stale again). github-status-lines.test.ts had the same
mkdtemp/spawnSync/cleanup shape typed out twice for the two --out-dir tests;
extracted into runUnfreezeToScratchDir.
…mment

runMdsBuild had no importers outside tests/helpers.ts (buildCommittedTree is
its sole caller) — dropped the export and noted why every other test file
spawns the build through its own runBuild instead. capability-hoist.test.ts's
PROCESS_CLOSE comment kept a trailing parenthetical describing the line-at-a-
time scan it replaced; kept the forward-looking classification rule and
dropped the implementation narration.
…the probe doctrine (resolve B37: Simplify follow-ons)
@dean0x

dean0x commented Sep 16, 2026

Copy link
Copy Markdown
Owner Author

Resolution Summary

Full summary withheld (public repository).

Metric Value
Total Issues 109
Fixed 76
False Positive 0
By Design 1
Deferred 7
Blocked 0
Escalated 0
Duplicates Collapsed 25

Full report: .devflow/docs/reviews/feat-324-tracker-phase-2-contract-mechanics-split/2026-09-15_2146/resolution-summary.md (not committed; ask the author)
Posted by devflow

…CHANGELOG

Pre-merge scrutiny of PR #339 (ADR-003: leave the end state, not the transition).

- Rewrite the comments that narrated what a line replaced or used to do
  (installer overlay, overlay-failure renderer, variant registry, reference
  sweep, build planner, and their tests) as statements of the current design.
- Move the VARIANT_MODULES doc comment onto VARIANT_MODULES; it sat above the
  cross-cutting docs block and left the registry undocumented.
- Drop the 'was the monolith at T1' history from the byte-budget table label.
- Knowledge bases: strip 'moved out of' / 'replaced' / 'Fixed in <sha>' narration
  from installer-shadowing, test-harness and feature-knowledge-system; the Guard 10
  lesson now states the scoping rule rather than the fix history.
- CHANGELOG: the status-lines fixture was re-captured twice, not once, and the
  containment exemption count is 63, not 48.

Comment and prose only; no behaviour change. Gate: build clean, tsc clean,
130 files / 4,541 tests passed.
@dean0x
dean0x merged commit ecfc141 into main Sep 16, 2026
2 checks passed
@dean0x
dean0x deleted the feat/324-tracker-phase-2-contract-mechanics-split branch September 16, 2026 11:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant