refactor(git-agent): split tracker mechanics into generated GitHub references - #339
Merged
dean0x merged 121 commits intoSep 16, 2026
Merged
Conversation
…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)
…olve B29: typescript-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)
… four touched knowledge bases
…dge bases with the resolve wave
Owner
Author
Resolution SummaryFull summary withheld (public repository).
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) |
…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
deleted the
feat/324-tracker-phase-2-contract-mechanics-split
branch
September 16, 2026 11:20
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.mdwas 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 mechanics —ghinvocations, 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:
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; ficontradicts D4's STOP-the-fan-out rule (waiting out a secondary limit extends the provider's penalty window), andgh release create … --notes "$NOTES"is an inline-body recipe wherecreate-releasemandates--notes-fileafter a scrub whose failure is a hard stop. Both were invisible to every existing guard.Key Changes
expandVariants+ the(module, op)registry insrc/core/mds-variants.ts,compiledSkillRefsDir(), a third build destination inscripts/build-mds.ts, the 28-line provider-resolution preamble (ceiling 40), recursivecollectSkillRefFiles, and the byte-budget four-shape table.learn-conventions, the D10 publication gate, the marker legend), theSKILL.mdcut, andgithub-api.md's five tracker sections._partials/_tracker.mds+ five host adoptions, the{ISSUE_REF}/{ISSUE_ID}vocabulary, the### Handoff Valuesproducer/consumer seam, the marker-literal removals, and the two dispositions.formatOverlaySummaryas a named render site, and the packed-tarball manifest guard.The numbers.
chars(dist/agents/git.md)chars(skills/git/SKILL.md)max_op=manage-debt5,007)#340.+ 8#341.+ 15#339-resolve.; tracked intests/fixtures/containment-exemptions.ts), each individually justifiedgithub-api.mdThe 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}.mdandissue: 42are byte-identical undergithub, 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
dist/skills/git/referencesWAS added toALLOWED_OUTPUT_DIRS(D-SKILLREFS-ALLOWLIST), with a newHostVariantmember'skill-refs'. Routing the third destination aroundresolveOutputDirwould have made the allowlist a partial gate — true for two destinations, bypassed for the third.## Operation:anchor, because the anchor breaks a'sole'-mode lookup until that guard is repointed. T2a landed both halves in one commit, as required.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.MAX_REFERENCE_SWEEP_DEPTH = 8; empty directories are left in place (removing them races a concurrent build'smkdir).expandVariants(and(module, op)were legalised and are named in aLEGALISED_IN_PHASE2constant, asserted both present there and absent from the forbidden table, so the narrowing reads as a narrowing and not as a hurried deletion.BUDGET_LOADED_SETis a frozen literal, notTOTAL_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".git.mdalone — measured, not assumed. That is why P2-S8'sgithub-api.mdcut is load-bearing for AC-2.5 rather than tidy-up.T2a — the unsampled half
SKILL.mdcut, plus one condensing. The named cuts alone land ~700 characters over budget.## Anti-Patternswas cut (every row restates a rule stated once above it;references/violations.mdis the named authority).## When This Skill Activateswas not cut — it was condensed: the five bullets collapsed to the one-line body that still stands atSKILL.md:22-24, since the frontmatterdescription: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-Patternscut, the budget cannot be met andBUDGET_SKILL_MDmust be re-derived, never raised.references/github-api.mdare frozen by exact text inKNOWN_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
gh repo viewappears only inpublication-gate.md. SG-8 forbids movingpost-review-summary/post-resolution-summarymechanics, 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 inpublication-gate.mdand 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.node …redact-secrets.cjs …) stays inline ingit.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— themax over opsterm is taken overTRACKER_GITHUB_OPS. AC-2.5 bounds "the worst-case tracker spawn";fetch-review-threads' 15,812-character load ofgithub-api.mdpredates the split and is not a cost it introduces. It is recorded as its own table row rather than dropped.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-constructedJSON.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:31—ghdeliberately 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.code-review.mds:333restated 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 inreferences/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 wholereferences/tree is normalised to0644, not only this run's files, becausecopyDirectorypreserves source modes and a reference is read-only instruction text.sweptOrphansreuse — the prune's removals fold into the existing sweep arrays viarecordSweep(report, 'reference', …)and inheritformatSweepSummary's render site, rather than needing a third report field.T5 — this subtask
github-status-lines.txtre-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 ingit.mdcannot 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 reportbacklink-shipped-issuesstep 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 aPER_ITEM_PAYLOADconstant rather than omitted silently.D-EXTREF-SCOPE—skills/git/SKILL.md's Extended References table keeps itsreferences/tracker/{provider}/{op}.mdrow 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_MDwas not touched.D-PROOF-TRANSITION— the Phase-0 faithfulness gate (run the extractor over theb6928e5snapshot, 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-dircase 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.mdby the git-agent golden, the generated references by the containment oracle's101bda7baselines). The retarget's own non-vacuity comes fromSTATUS_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.txtre-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:
extractStatusLinessamples 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 fromc0b9860; 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:
gh→ "the tracker"; the 403/429 +X-RateLimit-Remaining < 10signal → "a provider-signalled secondary rate limit"; "GitHub's penalty window" → "the provider's"; the< 50rung → "the provider's backpressure rung"references/publication-gate.md**Process:**, blank, the**Mechanics:**pointer)**Mechanics:**pointer added, the old per-commitgh apifan-out line gone ([DR-17] commit B)git.mdwhile steps 1/3/4 moved, so the reference's contiguous slice no longer spans itdynamic-build.mds1/2resolve.mds4/6Tracked=#→Tracked={ISSUE_REF}(P2-S11)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 —
2e019a5after the contract/mechanics split and10ac94cafter 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:
learn-conventions.md47 ·tracker/github/setup-task.md24 ·ensure-traceable-issue.md15 ·post-wave-report.md11 ·publication-gate.md10 ·manage-debt.md10 ·decision-markers.md9 ·backlink-shipped-issues.md9 ·ensure-pr-ready.md6 ·fetch-issues-batch.md6 ·fetch-issue.md2 ·create-release.md1.CONTAINMENT_EXEMPTIONSrange. All ninegit-agent.mdranges are exercised by this diff, none unexercised::24-25(two lines — the D4 remote-unavailable and secondary-rate-limit sentences),:28(the< 50backpressure 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(## Principlesitem 1),:990(## Boundaries'gh pr create).82 added lines — 28 blank / horizontal rule, 54 content:
## Tracker provider resolution+## Tracker input contract)**Mechanics:**pointers, one per op whose body moved### Handoff Valuesproducer blocks insetup-taskandfetch-issue:24-25,:28,:715,:968):45,:50)references/publication-gate.md(DR-20)decision-markers.md,setup-task's 1b/1c pointer tolearn-conventions.mdensure-pr-ready's ALWAYS-ON contract line (4b),ensure-traceable-issue's untrusted-input contract (:910),## Boundaries' replacement (:990)Every hunk is a pure move or a named exemption. Nothing else is in the diff.
Per-op retained / moved table
movedcounts the lines of the op's### Processblock in its generated reference;retainedis the op's section length ingit.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).tracker/github/{op}.mdgit.mdsetup-task### Traceability'sole'over a git.md-only corpus; AC-0.10 issue-body containment file-scoped; byte-budgetnameableFromscans git.mdfetch-issue#-strip ref rule), D4 clause, Output'sole'; AC-0.10 file-scopedfetch-issues-batch#-strip, ≤50,TRUNCATED,NOT_FOUND ({refs}), containment), D4 clause, OutputTRUNCATED/## Issues Batchpins'sole'(unchanged); thegh api graphqlpin →'union'; conventions-commit arm (c)'sole'; AC-0.10 file-scopedmanage-debt'union'; D4 clause +(pending —'sole'; D11 forward/reverse'union'create-release## Closed Issuesenrichment bullet only## Commitsbullet, the 60000 cap, step 6, the D4 carve-out, Output'sole'; D11 reverse'union'gather-release-evidence≤25/ no-fan-out, reference file scanbacklink-shipped-issues'sole'; D11 forward/reverse'union'; P2-S4 one-home-per-detector, reference tree scanensure-traceable-issue'union'; AC-0.6b'sole'post-wave-report'union'; AC-0.10 external-thread file-scoped; D11 forward/reverse'union'ensure-pr-readyexists_open+render_pr_link)gh pr create … --body-file+ D11'sole'; D11 bypass scan, corpus-wideCross-cutting cuts:
git.mdreferences/learn-conventions.md(57 L, 3,627 ch)**Process:**block.devflow/conventions.mdis absent, the D4 clause, Output,**Commit boundary:**'union'; conventions-commit arm (b) stays sink-widereferences/publication-gate.md(13 L, 1,114 ch)## Publication gate (D10)sectionits unchanged on git.mdtracker/github/backlink-shipped-issues.md§ Provider signals (GitHub)ghavailability, the concrete scrub-then-post chain'union'; a new cross-cutting negative guard, section-scoped over git.mdWhy a guard's mode changed, in one sentence: a guard whose literal left
git.mdmust 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.
tests/tracker/byte-budget.test.tsreferences/tracker/naming line appended to the compiled agent4a2c11b(landed with 6 red),17ffa60(3 → green)tests/tracker/containment.test.ts['probe.md:1']; an exemption silences exactly its own range → unaccounted[2],linesScanned 108c1190INLINE_BODY_SHAPES,joinContinuations) widened on pattern and scopetests/git-agent.test.tsSKILL.md:211gh release create … --notes "$NOTES"— already in the treefcec744(landed red on purpose)## Operation:anchor vs the'sole'corpustests/git-agent.test.ts6f7230fsleep 60tests/tracker/containment.test.ts≤50 branchessingle authoritytests/git-agent.test.tstests/guards/heredoc-quoting.test.ts<<EOF,<<-MSGmatch;<<'EOF',<<"EOF",<<<do not) and a seeded unquoted heredoc through the same collector82d8b86tests/git-agent.test.tsD42row with no definition →['D42']ac9bd3etests/git-agent.test.tsgithub-api.mdexclusions went stale and the guard went red until they were deleted16baec2tests/tracker/containment.test.ts≤25absent) and a per-commitgh apiloop present9c704df; permanent probe over the baseline finds exactly 1its)tests/git-agent.test.tsexpected [] to equal ['post-resolution-summary', …]; a seeded third op naming the gate7fedbb1tests/git-agent.test.ts>= 6bcff97dtests/git-agent.test.tsexpected [] to have length 1forX-RateLimit-Remaining\header < 10`bcff97dtests/tracker/containment.test.ts['tracker/github/probe.md']7fedbb1_tracker.mdsadoption (AC-2.9, two-armed rule)tests/build-mds.test.tscaae772tests/build-mds.test.tscaae772<!-- devflow:in anydist/commands/*.mdtests/build-mds.test.tsdynamic-build.mds, andcode-review.mds— which the plan did not name); a seeded restatement in a temp copy of a real dist filebbf0936_engine.mdsneutral wording (C9/C9b)tests/build-mds.test.tsbbf0936tests/dynamic/depends-on-grammar.test.ts1953114tests/seams/command-agent-input.test.ts['ISSUE_BRANCH_TOKEN','ISSUE_ID','ISSUE_PR_LINK']tests/installer/reference-overlay.test.ts47352d6, then the naive additive-copy shape [DR-05] rejects: 11 of 17 failed47352d6+ a behavioural run against the naive shape (never committed)tests/packaging.test.tsb48919ftests/guards/capability-hoist.test.ts179ba71tests/guards/provider-scope.test.ts179ba71mcp__/ user-facing "MCP"tests/guards/provider-scope.test.tssrc/assets/agents/test.md's legitimate Chrome tool allowlist, which is why the scope is the Git spawn surface179ba71tools:key on the Git agent; AC-2.7_mcp.mdabsencetests/guards/provider-scope.test.tstracker/github/directory asserted present so the absence is an absence, not an unbuilt tree179ba71tests/tracker/containment.test.ts179ba71tests/guards/guard-census.test.ts179ba71tests/guards/retired-wording.test.ts_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 probe179ba71tests/helpers.tsref()refuses an undeclared path; the extractor refuses to return while a declared reference went unreaddd42ea1Prefix-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 the5ad16e6Verification Gate).)Zero failures. Every red this phase declared is now green: the git-agent golden (red since T1's
17ffa60) and the threegithub-status-linesderivationits (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-dirrefusal) 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: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-count11→12,issue-capture-contract-size3→6, and four new entries (generated-reference-manifest-size13,packed-reference-manifest-size13,git-agent-guard-count68,issue-pr-link-forwarding-sites14). No budget constant was touched after it was pinned.toMatchSnapshotremains 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
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.github-api.mdandSKILL.mdlines, and the exemption list's second arm (no entry may cover a range that is in fact still contained).>= 8, the literal8untouched, and no numeric literal decreased anywhere undertests/except the six baselines listed above.D{N}label lacks its definition.Release readiness
devflow:gitmust still receive the canonical GitHub references) is a release blocker, not merely an AC. AC-2.4a is GREEN — shadow SKILL.md kept, all tentracker/github/*.mdbyte-equal to the generated source, and all 13 byte-equal on the canonical branch.Could not refresh the generated references for "{provider}" ({error}) — the previously installed files were left unchanged. The install still succeeds (PF-009).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.tracker mechanics unavailableis reachable by design from P2-S14; its runtime arm is Phase 3 (P3a-S14).PR checklist (D-B)
CHANGELOG.md### Changedenumerates the internal changes_preamble.mds:75NO-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-474RETAIN disposition recorded and pinned mechanically/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 intests/integration/clause-ii-file-residue.test.tsand is green.ENOENT … snyk-macos-arm64). Not retried.Follow-ups
src/assets/skills/git/references/github-api.md— genericgh pr create --body/-f body=examples that predate D11, in a file the Git agent loads on thefetch-review-threadspath. They are loadable instruction text that shows an agent how to post an unscrubbed body. Frozen by exact text inKNOWN_GITHUB_API_INLINE_BODIESso the list can only shrink; filed as fix(git-skill): nine pre-existing D11 inline-body bypasses in references/github-api.md #340.git.mdcut (8 PR/review op bodies, 381 L / 25,175 ch) is excluded by design from every phase of this feature, with SG-8's constraint quoted verbatim:post-review-summary/post-resolution-summarymechanics may move only in a PR that moves their guards, never as a size optimisation.Also fixes #340 — D11 inline-body recipes in
references/github-api.mdThe 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 inKNOWN_GITHUB_API_INLINE_BODIESare now rewritten to the scrub-then-post chain already used elsewhere in the file: compose to$DEVFLOW_BODY_RAW→redact-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_BODIESis 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 #341below (ADR-025: widened in the same commit as the content it catches).it(count intests/git-agent.test.tsis 77 (floor 73).CONTAINMENT_EXEMPTIONSrows for the rewrittengithub-api.mdbaseline lines (88, 91, 313, 328, 334, 336, 504, 541, 545, 552, 638); baselines and goldens untouched; no ceiling raised.github-api.mdis 17,259 chars (was 15,812) — a recorded, non-gated byte-budget row (D-LOADED-SET-SCOPE).$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).### Also fixes #341below.Commits: 34b6346, c9180f2, f45d2b6, ed9b494, a715851 (KB refresh).
Also fixes #341 — the six inline sinks the D11 guard could not see
tests/git-agent.test.tsjoins backslash-continued commands before matching, names five shapes (long-flag--body|--notes|--comment;short-flag-b/-n/-con posting verbs;api-field-f/-F/--field/--raw-field body=;unscrubbed-file--body-file/--notes-filewith 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 inbaseline/github-api.md, 1 inbaseline/SKILL.md), and a corpus-reach probe on sentinel paths.KNOWN_GITHUB_API_INLINE_BODIESstays[];git-agent-guard-countfloor 68 → 73.a715851: the widened collector found exactly six inline sinks —ensure-traceable-issue.mdgh issue create … --body;manage-debt.mdgh issue close … --commentandgh issue create … --body;skills/git/references/patterns.mdgh pr create … --body;skills/review-methodology/references/patterns.mdgh 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 twogh … create --json numberdefects (a flag neither command has) are gone.post-review-summaryunder D10 and D11.git.mdsnames a comment attached to a close as a posted body (provider-neutral clause, +49 ch; one fixture-only golden regeneration,65e5470).github-status-lines.txtre-capture was needed: the fixture samplesmanage-debt.mdlines 11–18 andensure-traceable-issue.mdlines 15–19; the sinks sit at 54–70 and 26–46. Eight#341.containment exemptions (baselinegithub-api.md149, 153, 189, 191, 193, 196–200, 257, 259) bring the total to 48.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 classificationD-CROSS-CUTTING-ON-DEMANDintests/tracker/byte-budget.test.tsstands, 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);collectUnfencedH2owns the rule andtests/tracker/reference-structure.test.tsasserts 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.mdheredoc and D3 template lines), zero stray. New floormin-fenced-h2= 7. Live probes: the extractedmanage-debtsection reachesgh issue close "$old_issue";ensure-traceable-issuereaches the create recipe and the D3 template.tests/git-agent.test.ts(setup-task, fetch-issue, learn-conventions arm b, the## Issues Batchheader guard) use op-scoped extraction; the seam test'scollectMissingProducersstays body-scoped because its probes mutate the body under test.post-wave-report, the last op in git.md, had satisfied set (b) only through the shared## Principlestrailer 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 verbatimorcontent (Principle 8).(+120 chars in the core agent, not the per-op reference — PF-027). Source commit667c497red on exactly the golden byte-equality; fixture-onlyce491f9regenerated the golden.10ea0d5(origin/main33b730e, PR fix(flags): default pin-sonnet-4-6 and disable-bundled-skills to off #338); no conflicts.test-harnessandtracker-referencesrefreshed in031e1bd.Resolve wave (cycle 1, 2026-09-15/16)
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.ISSUE_PR_LINKre-check is the only gate (immaterial once the$ISSUEsink is quoted).fetch-issues-batch, state fields only, and the op projectsstate.&&chain.manage-debtappends to the backlog body, so the size check and archive path are live, with the successor number validated before promotion.github-api.mdhas one D4 stop-and-report spelling, fail-closed rate probes, observable batch truncation, a self-contained release recipe, and every expansion quoted..oldrecovery copy when a restore fails, fails loud when the compiled tree is absent, stages process-uniquely under the converged subtree, and boundschmodRecursive.HostPlan/VariantSectionare typed totally.gitOp()derives through it (byte-neutral); fence grammar is probed and unclosed fences are asserted absent.it(and refuses skipped/focused spellings; provider detectors are word-bounded; the heredoc matcher covers the shell grammar..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