feat(tracker): selection-scoped install bundle + tracker gap closure - #351
Merged
Merged
Conversation
…l closure guard
Plan step A1 (steps 1-2) of the selection-scoped install bundle.
Skills become plugin-scoped, as rules already are. Each plugin declares the
skills it USES but does not OWN in a new `requires:` field; what a selection
installs is `skills u requires`. The 13 non-trivial tables are evidence-derived
over the corpus a selection actually installs - dist/commands, agent sources,
and SKILL.md + references/** of every skill already in the closure, iterated to
a fixed point (design review C1). The default non-optional selection drops from
40 skills to 32; the eight language skills install only with their plugin.
New pure surface in src/core/plugins.ts:
skillsOf / skillOwners / buildScopedSkillsMap / resolveSkillInstallPlan
PRESENCE_GATED_SKILLS (derived) / TEMPLATE_SKILL_REFS
buildFullSkillsMap becomes a thin wrapper over the scoped map.
resolveSkillInstallPlan is the pure install/remove/dormant-shadow decision
(design review H4) so installViaFileCopy consumes a decision rather than
growing a fourth set of inline set-arithmetic. A --plugin run removes nothing
(AC-22); FEATURE_OWNED_SKILLS are never swept (applies ADR-024); a shadow
outside the selection is reported dormant and never deleted.
Language skills are never required (AC-25): /code-review now presence-gates
each language Review focus on ~/.claude/skills/devflow:{focus}/SKILL.md, the
same gate COMPLIANCE_SKILL_INSTALLED already uses. TEMPLATE_SKILL_REFS declares
the one classified exception - the prefix-less focus-skill template - at the
declaration site of the field it exempts, and the guard imports it rather than
restating it (applies PF-067, design review M1).
Guards:
tests/guards/requires-closure.test.ts - forward (every reference resolves in
scope), reverse (no dead requires entry), structural, and the presence-gate
counterpart, each with synthetic known-bad probes (applies PF-018). Token
floor 44 registered in numeric-floors.json.
tests/guards/retired-wording.test.ts - new class rule over src/**: wave
coordinates in source comments are transition narration (applies ADR-003).
Seven sites rewritten to state the rule instead of the phase; the corpus is
widened to the .ts/.cjs trees the entry names, and the pattern form is
reserved for residue whose spellings are not enumerable (applies ADR-025).
Refs #350
…}, _mcp.md iff MCP-backed
Plan step A2 (step 3) of the selection-scoped install bundle.
The build emits every provider (34 files) so the tarball can serve any
selection without a rebuild. An install should not: a github user gains 21
files / 38 KB of jira and linear mechanics they cannot reach. installedReference
Manifest({provider}) is the narrower manifest the overlay converges to —
{github} u {selected provider} plus the provider-independent cross-cutting
documents, with tracker/_mcp.md riding the same MCP_BACKED_PROVIDER_SUBDIRS
gate its generation already rides.
The GitHub tree is the FLOOR under every provider, not an optional extra: PR
hosting stays on GitHub whatever the issue tracker is. PR_HOST_TRACKER_SUBDIR
states that as a fact about where pull requests live rather than deriving it
from "the one CLI-backed module", which would read as a rule and silently
promote the next CLI-backed provider into everyone's install.
Derived from the registry, never from a provider table: a provider registered
with a tracker/{id} subdir is installable by construction. Keeps its sibling's
posture and asserts on a registry that does not expand (design review M3) —
the registry is a compile-time constant, so a refusal is a programming error no
caller could sensibly continue past.
Measured 2026-09-19: github 13 files / 31,399 B; jira 24 / 69,252 B;
linear 24 / 73,501 B. Registered as installed-reference-count-github=13 and
installed-reference-count-provider=24; the two build-time 34-floors are
annotated as build/tarball scope with their values untouched.
Refs #350
…t file and the scoped reference overlay Plan step A3 (step 4) of the selection-scoped install bundle. Two artifacts converge with the tracker provider, and they get two owners on purpose (design review M2). convergeTrackerArtifacts owns the Tracker agent file alone and carries no overlay mode flag; overlayInstalledReferences is the installer's export, because the installer already has exactly one overlay spelling and a flag would have made it two. `devflow tracker --set` calls both explicitly, in the fixed order. src/targets/claude-code/tracker-install.ts mirrors compliance-install.ts: absolute-claudeDir precondition asserted in production code, warn-not-throw per PF-009, converged=false on any warn path so the sentinel write can be gated on it, and the biconditional converging in BOTH directions per PF-015 — selecting jira or linear installs the agent, returning to github removes it. The agent is re-copied on every converging run, so a truncated or hand-edited file self-heals rather than being trusted because it exists. `tracker` stays declared in devflow-core-skills.agents and is filtered here at install time. A FEATURE_OWNED_AGENTS set would cost three new union sites and a rewrite of the pinned agent-roster floor (17, unmoved) to solve a problem the agent sweep does not have: the sweep keys on the full registry, so it never sees this file as an orphan. overlayInstalledReferences resolves the install manifest and the target from a claudeDir and a provider and changes nothing else. Its `referencesRoot` option is the seam that makes the absent-generated-tree refusal provable without deleting dist/ out from under a concurrent run (applies PF-013); the refusal still lands before the target directory is created, so a refused overlay leaves the install as it found it. Refs #350
…rovider-scoped overlay, tracker-aware summary Plan steps A4 (steps 5-6) plus design-review C2, H4, H6, L2, M5 and M6. installViaFileCopy now takes a REQUIRED trackerProvider. Required rather than defaulted because the overlay converges: a caller that forgot to pass one would not install a slightly wrong set, it would delete the previous provider's mechanics on every install. There is no safe default for a destructive convergence, so the type refuses to guess. The skill decision is made in full, before anything is touched, by the pure resolveSkillInstallPlan (H4) — install set, removal set, dormant shadows. The removal set is registry arithmetic (`skillsOf(all) \ skillsOf(selected) \ FEATURE_OWNED`), never a readdir of the install tree, so a directory a user put there by hand is not swept as a deselection (applies ADR-024); a name the registry has lost entirely still leaves through the orphan sweep, which reports it as the different fact it is. The pre-clean is now SCOPED to what the run reinstalls and gated on a full install, while the orphan sweep stays UNSCOPED. The opposite scoping is deliberate and is recorded at the site: the sweep removes names the registry no longer has, which is true regardless of selection; the pre-clean empties a directory this run is about to rewrite, so widening it past the install set would delete a selected plugin's skill and never put it back. init.ts resolves the effective plugin list BEFORE the install rather than at manifest-write time, so the skills install set and the recorded selection have one binding and cannot disagree. Skills move from buildFullSkillsMap to buildScopedSkillsMap(effectivePlugins); agents and rules stay on the selected plugins as before. persistManifestThenConvergeTracker gains the fourth owner. convergeTracker Artifacts runs SEQUENTIALLY and strictly before the rearm/sentinel pair, which stays a Promise.all rather than being flattened to make room (H6). C2: the sentinel WRITE is gated on the agent converging — nothing advertises a provider whose agent is missing — while the sentinel REMOVAL is always attempted, because a stale sentinel costs every future session a fork for a provider the user has left and a failed agent removal is not a reason to keep paying it. M5: formatOverlaySummary and describeOverlayFailureState move to the new src/cli/commands/install-report.ts, joined by formatSkillScopeSummary and formatTrackerAssetSummary. `devflow tracker --set` renders the same overlay outcomes, and a CLI command importing a renderer out of a 2,450-line sibling couples two commands through a file neither owns. L2: the upgrade-removal line fires only when a prior manifest exists AND its plugin set is unchanged — a first install removed nothing, and a user who deselected a plugin asked for it. M6: session-start-context's directive path gate becomes the positive shape ^[A-Za-z0-9/._-]+$ instead of a denylist of four characters someone thought of. An allowlist refuses the next escape nobody has thought of by construction. The narrowing is real — a home directory with a space now suppresses the directives rather than interpolating an unproven value — and that is the fail-closed direction. New: tests/installer/install-shape.test.ts (install set, removal gating, shared- requires survival, sweep-vs-pre-clean scoping, shadow dormancy, summary lines). Refs #350
…--status reports installed mechanics Plan step A5 (step 7) plus design-review C2, C3, H1 and M2. `devflow tracker --set` now converges the whole bundle, not just the three ~/.devflow files. The step order is the invariant and is asserted as an order, through an injected TrackerSetIO the way init.ts's TrackerLifecycleIO already is: probe devflow:git -> overlay references -> rename stale conventions -> persist manifest -> converge the agent file -> rearm -> sentinel The overlay precedes the manifest write and the agent file follows it. That asymmetry is the point: the reference subtree is INERT — nothing loads it until a Git spawn resolves a provider, and resolving a provider reads the manifest — so installing it early costs nothing and lets a failure abort cleanly with the previous provider whole. The agent file and the sentinel ADVERTISE a provider and must never get ahead of the manifest recording it. init's order differs and stays differing: two call paths, one principle, and a shared signature would hide the reordering rather than document it. C3: an absent devflow:git aborts with exit 1 and the same end state as an overlay failure — no manifest, no sentinel, and no references/ husk under a skill nothing installed. H1: the overlay-failure message says the manifest, sentinel and conventions are unchanged and that the overlay is atomic per unit, so the units that succeeded are converged and only the failed ones are reported. "Nothing else changed" is the more comfortable sentence and the false one. C2: the sentinel WRITE is gated on the agent converging; the sentinel REMOVAL is always attempted, because a stale sentinel costs every future session a fork for a provider the user has left. M2: --set calls overlayInstalledReferences and convergeTrackerArtifacts explicitly, in order — no mode flag on either. Repeating the current provider is not an early return: it re-runs the overlay so a damaged subtree self-heals, and reports `(unchanged)` when nothing moved. `--status` gains a Mechanics row with three arms — installed (n) / MISSING / unreadable (errno). Three, not two: "no files" and "could not look" have different remedies, and collapsing them tells a user to re-run devflow init over a permissions problem init cannot fix. The single-owner source guards move with the bindings: each file-lifecycle owner is bound exactly once in buildTrackerSetIO, the orchestrator reaches every one of them through io (asserted, so a direct call cannot bypass the order test), and the CLI still never touches .tracker.enabled or .tracker.attempts itself. Refs #350
…rom the manifest, not the registry
Plan step A6 (steps 8-9) plus design-review L3.
skills.ts. Once install is scoped, the question `devflow skills list` has to
answer is "which plugin do I select to keep this?", and a first-wins owner
column names one plugin out of several that would each do. Every row now names
every declarer and says whether the skill is actually installed — for a skill
that is not, that is the only useful thing the row can say. L3: the declarers
are resolved ONCE into a Map before any row is rendered, rather than walking the
registry per row for an answer that does not change between rows.
`devflow skills shadow <name>` no longer refuses a skill outside the selection.
"Not installed" is now an ordinary state for a registry skill nobody selected,
so the shadow is seeded from the shipped source and reported as dormant: it
exists, every future install preserves it, and it applies to nothing until the
plugin that uses it is selected. The warning names that plugin.
uninstall.ts. computeAssetsToRemove took the whole REGISTRY as the retained set.
Once skills are plugin-scoped the registry and the manifest stop agreeing, and
retaining on behalf of plugins the user never installed means
`devflow uninstall --plugin=X` keeps X's skills alive because some unselected
plugin also declares them — leaving the user with exactly the files they asked
to remove. It now takes what the manifest records, resolved once and handed to
BOTH the dry-run and the real removal, because a preview computed from a
different list is a preview of a different uninstall. Retention is across the
closure (`skills ∪ requires`) for the same reason the install set is: a skill an
installed plugin merely requires is still a skill it needs. No manifest, or one
naming nothing this registry still has, falls back to the registry — retaining
too much is the safe direction for a removal.
The three tracker runtime basenames come from src/core/tracker.ts instead of
being retyped as literals.
Two audit items resolved as no-change, with the reason recorded at the site:
- TRACKER_GITHUB_OPS is kept. It has a live consumer (the github row of
VARIANT_MODULES) and its JSDoc is what stops the roster reading and the
provider-scope reading being confused at the guard sites that mean one and
not the other.
- trackerConventionsBackupName/Path keep their exports, now stated as
deliberate: the .bak filename is a user-visible contract, and un-exporting
would force its pin to re-derive the name beside the real one, which is the
shadow reimplementation a guard is worth nothing without (PF-018).
Refs #350
Also lands tests/scoped-install-e2e.test.ts, the end-to-end proof for the whole
of Part A: it drives the real dist/cli.js (init per provider, tracker --set both
directions, --plugin, uninstall --plugin) against mkdtemp HOMEs bound into the
spawn env (PF-060). The unit tests all call installViaFileCopy with hand-built
maps and so cannot see a wiring mistake between the CLI and the installer, which
is the only place every claim this wave makes is actually true or false. It sits
in tests/ rather than tests/integration/ because the default vitest config
excludes that directory, and a proof nobody runs is not a proof.
Folds in one cross-file fix the full-suite run surfaced: three synthetic probe
names in the new guard and install-shape suites were spelled as `devflow:`
source literals, and tests/skill-references.test.ts scans every tests/**/*.ts
for exactly that — a `devflow:` name that is not a registry skill. The probes
need those names on DISK and in the strings they feed the collector, not in the
source text, so the prefix is now composed. The probe input is byte-identical;
only this file's spelling moved, so the global scanner stays strict rather than
gaining an exemption for the two files whose subject is invalid references.
…ion registry Five of the seven describes in tests/tracker/containment.test.ts are LIVE controls over the shipped tree — they read dist/, not the 101bda7 baseline. They move ahead of the file's deletion so nothing is silenced in between. New homes: tests/tracker/single-authority.test.ts - MIN_RATIONALE_CHARS, and a named collectUnderJustified() both registries read (the constant arrives with the two registries it governs) - SHARED_LITERAL_REGISTRY + "one authority per normative sentence" - MCP_SHARED_LITERAL_REGISTRY + "tool-call contract: one authority" tests/tracker/reference-reachability.test.ts - structural parity (every op has a file, every file has an op, every file opens with its own `## Operation:` anchor) - gather-release-evidence batch-first - AC-2.7 reachability on the gh path Two changes, both classified rather than swept (applies ADR-025): 1. SHARED_LITERAL_REGISTRY's justification check was emptiness-only (`length === 0`); it now reads the same MIN_RATIONALE_CHARS floor the MCP registry reads. The constant's own doc argues that `justification: 'x'` records a keystroke rather than a reason, and leaving the weaker check two screens below that doc is how the two drift. STRENGTHENED, with a probe driving collectUnderJustified over seeded entries so the floor is proven live (PF-018). 2. The gather-release-evidence known-bad probe read the pre-rewrite fan-out line out of tests/fixtures/tracker/baseline/git-agent.md. That tree is deleted in the next commit, so the one line it contributed here is now a named PER_COMMIT_FANOUT_SAMPLE constant, quoted byte-exact, plus a discrimination arm proving a batch-first line is NOT reported. Same collector, same shape, no whole pre-rewrite document kept to hold one line. Exemption-registry partition (PF-067): (i) transition-era — all 75 CONTAINMENT_EXEMPTIONS entries. Each addresses a 1-based line range of a baseline fixture; with the baseline gone the ranges address nothing. Deleted with the oracle in the next commit. (ii) exemptions backing a live prohibition — EMPTY, and stated as empty rather than filled. The rationales that cite PF-027 and PF-063 cite them as the REASON a line was cut or demoted; neither prohibition is enforced from this table. PF-027's D11 comment-sink control is asserted in tests/tracker/byte-budget.test.ts ("written exclusions"), and its inline-body exemption registry (KNOWN_GITHUB_API_INLINE_BODIES, asserted in both directions and deliberately empty) already sits beside the prohibition in tests/git-agent.test.ts. PF-063's heading-grammar prohibition in tests/tracker/reference-structure.test.ts is absolute over unfenced `## ` and carries no exemption list at all. Nothing to move. tests/tracker/reference-floor.ts and the min-reference-chars manifest entry now name the GitHub tree's floor at its new site. RED proof: shortening one justification in each registry to a token string fails both first arms on the shared floor; probe reverted. Refs #350
…d guard census
Deleted (2,703 lines):
tests/tracker/containment.test.ts the baseline oracle; its five live
describes were re-homed in the
previous commit
tests/fixtures/containment-exemptions.ts 75 exemptions, every one addressing a
line range of a baseline fixture
tests/fixtures/tracker/baseline/ byte copies of git-agent.md, SKILL.md
and github-api.md at 101bda7
tests/guards/guard-census.test.ts the git-agent.test.ts guard COUNT
The oracle answered one question — "did the split lose a line?" — against a tree
the branch no longer resembles. Two of its seven describes read the baseline; the
other five read dist/ and have moved. The exemption table is meaningless without
the line numbers it addresses, so the two go together (ADR-003).
Same-commit ripple, all of it:
- `git-agent-guard-count` RETIRED from tests/fixtures/numeric-floors.json (30
floors -> 29). The ratchet's own rationale was that the assertion could not
live in the file it counts; with the census gone the floor has no site to be
spelled at, and numeric-floor-manifest.test.ts would fail on the dangling
sourceFile. A count of `it(` declarations was always a proxy for the guard
surface (PF-064) — what replaces it is the roster claim below, which names
the property instead of a number.
- The census's SECOND claim survives, in the file it is about:
`PHASE0_OPERATION_NAMES` -> `GIT_OPERATION_ROSTER` in tests/git-agent.test.ts,
with both `it()`s kept verbatim. The 18 names and the `collectOperationNames`
collector move with it. Registry Guard 6 stays green across a coordinated
rename, so this is still the only thing that would see one.
- Three known-bad probes read the baseline tree for their RED evidence. Each is
re-homed, none is dropped, and each still drives the SAME collector the live
arm does (PF-018):
git-agent.test.ts D11 inline-body — now UNSCRUBBED_POSTING_SAMPLES, the 18
posting recipes from that tree quoted byte-exact, asserted one-by-one
rather than as a per-file count. Verified red by breaking one
INLINE_BODY_SHAPES row.
git-agent.test.ts GAP-25 `sleep 60` — now RATE_LIMIT_SLEEP_SAMPLES, the
three sites, plus a discrimination arm on a literal the corpus lacks.
git-agent.test.ts cross-cutting detectors — now
CROSS_CUTTING_DETECTOR_SAMPLE, a seeded always-loaded header, and the
probe additionally asserts the hits are LOCATED in more than one section
and that the `## Operation:` body is left alone.
- tests/seams/command-agent-input.test.ts's Handoff Values probe no longer
needs a snapshot at all: it strips the three producer lines from a COPY of
the live git.md body and asserts the same six pairs. Self-maintaining, and a
strictly better probe than the one it replaces.
- tests/guards/no-control-bytes.test.ts:263 named "the guard-census shape" for
a pattern that now has no file to point at.
Net: the suite keeps every prohibition and loses only the corpus that could
answer questions about a tree that no longer exists.
Refs #350
End-state prose, no wave coordinates (ADR-003). No behaviour, no value and no
assertion changes anywhere in this commit.
tests/fixtures/numeric-floors.json — 16 `description` fields rewritten:
dist-host-count, partial-count, dist-files-count, slow-test-timeout-ms,
d11-posting-ops, agent-roster-count, issue-capture-contract-size,
containment-issue-body-floor, containment-external-thread-floor,
generated-reference-manifest-size, packed-reference-manifest-size,
capability-hoist-block-floor, min-hostile-refs, budget-git-md,
budget-loaded-set-jira, budget-loaded-set-linear
Each said WHEN it was raised and by which commit of which wave ("RAISED 29 ->
39 in the Phase-3 3b commit", "P2-S9", "c7bff85", "on main") rather than what
the number IS. They now derive the value from the thing it counts — 34 = ten
ops per provider plus three cross-cutting documents plus the contract; 49 =
git.md's eighteen plus ten per provider; 6 = three original keys plus the three
Handoff Values — which is the form that survives the next raise. `§14.x`
tokens are gone.
`floor` / `ceiling` / `pattern` / `occurrences` / `sourceFile` are byte-
identical for all 37 entries, verified field-by-field against HEAD. The
budget-* ceilings keep their P2/P3 pairing prose: that pairing is still true
today and the collapse is a separate, separately authorised change.
tests/guards/retired-wording.test.ts — the `phase` field is dropped from
RetiredEntry, from all 25 rows and from the failure message; the per-wave group
banners become what each group IS (scoped literals, retired DEGRADED synonyms,
retired status headings). EVERY entry is kept — the denylist never shrinks —
and the `pattern?: RegExp` field stays: the phase-label class rule is the one
entry whose members cannot be enumerated, and it is the reason a pattern exists
at all. The header's per-wave inventory goes: `removedFrom` and `justification`
already carry each literal's provenance per row, which is where it belongs.
tests/tracker/reference-structure.test.ts — PF-063's remedy stated as two halves
that both hold (demote on the move; assert the prohibition) rather than as one
half a past phase shipped and one it did not. The dangling
`CONTAINMENT_EXEMPTIONS` reference goes with it.
tests/helpers.ts — the extractor's proof-obligation note claimed the generated
references were frozen by "the containment oracle's 101bda7 baselines". They
are pinned by their committed `.mds` sources and a deterministic build. Only
that sentence is touched; collectTrackerNamingLines is not rescoped.
Refs #350
…and the ADR/PF/D4 restatements Pure cuts to the always-loaded agent. Nothing gains a rule; seven restatements lose one, and every rule they restated is stated in full somewhere the spawn already reads. - The Operations index loses its third column. Eighteen parameter lists, each a second copy of the operation's own `**Input:**` line two screens below it, free to drift against the authority they duplicate. - `**Neutral values**` drops the ADR anchor and keeps the rule it annotated. - The frontmatter-mismatch bullet drops the tail arguing why tracker.md survives uninstall. That argument belongs to the uninstall classification and is recorded at the code site that makes it; the agent needs the invariant. - Both summary ops' publication step 2 becomes a pointer to the gate they already name. Step 3 - the fail-closed visibility probe - stays spelled inline in both, because a containment control is never a file the spawn might not have loaded (PF-058). - Principles 1 and 2 stop paraphrasing the D4 block that sits 860 lines above them; principle 7 drops its pitfall anchor. Eight principles, unchanged count. - post-resolution-summary step 4 drops a delivery coordinate for a phase numbering that no longer exists. Measured: git.md 58,818 -> 56,779 ch (-2,039). Preamble 6,781 -> 6,566; non-preamble 52,037 -> 50,213 against the unraised allowance of 52,365. Loaded set: github 80,873 -> 78,834, jira 88,633 -> 86,594, linear 90,961 -> 88,922. Jira headroom 27 -> 2,066 ch, which is what funds the rest of this phase. Guard literals classified (applies ADR-025), 1 file, 1 pin: RE-POINTED 1 - git-agent.test.ts's D10 three-value pin. The enum was spelled in both op sections AND byte-identically in references/publication-gate.md; it is now asserted once, in the reference, plus a new arm per op that the op still names the gate. Strictly more sites asserted than before. KEEP-EXACT 0, SOFTEN 0, STRENGTHEN 0. retired-wording gains four entries: `ADR-007`, `PF-003` (both scoped to the two files carrying the prompt - they stay legitimate in CLAUDE.md, docs/, the other agents and the ledger), `Phase 5/9` and `Key Parameters`. Expected red from this commit until the golden re-capture: tests/goldens/git-agent-golden.test.ts byte-equality, by design. Refs #350
…ub; nothing else names it
The tool-call contract carries the transport prohibition, the capability-selection
rule and the trust discipline for every tracker call a non-github spawn makes. It
was named by its consumers: five of the ten per-operation mechanics files. The
other five - fetch-issue, fetch-issues-batch, ensure-pr-ready, create-release,
gather-release-evidence - ran tracker calls with none of it.
An extraction that turns a universal obligation into per-consumer opt-in is the
defect, not the saving (PF-058), and "some shipped file names it" could never have
caught it: five namers satisfy that as completely as ten do.
The preamble now names it, on the SAME physical line that composes the
per-operation mechanics path. That keeps PF-023's single convergence point at
exactly one line, and it is sound rather than a loophole: what PF-023 counts is
where a path is BUILT, and this one is a fixed literal built from nothing - the
validated provider token selects the mechanics directory and never reaches this
name. `_mcp.mds`'s preamble, which argued the opposite, is rewritten in the same
commit rather than left self-contradicting.
`posting_gate_head` and both setup-task Setup bullets now invoke the contract by
name in prose instead of composing its path; ten generated op files lose the path
and none gains one. `aggregate_call_budget` drops its `[DR-09]` anchor - an
internal label in text a model reads.
Guard literals classified (applies ADR-025), 4 files, 6 pins:
STRENGTHEN 2 - jira-module / linear-module "every posting mechanic invokes the
tool-call contract". Each was one claim (the file names the path); each is now
two (it invokes the contract by name AND does not compose its path).
RE-POINTED 3 - reference-reachability's contract rule, from "a shipped consumer
names it" to "the preamble names it", plus the two reachability set-builders
that read it. The old rule was structurally incapable of seeing the defect.
KEEP-EXACT 1 - provider-literals' aggregate_call_budget row, re-pinned exact on
the new spelling.
SOFTEN 0.
New arms: byte-budget splits the single-naming-line claim into composition (the
line builds the mechanics path from the provider token) and fixed literal (the
contract is named on that same line); reference-reachability gains the inverse
prohibition - no generated op file names the contract - with a discriminating
probe. budget-model declares MODEL_CROSS_CUTTING_ASSERTED beside
MODEL_CROSS_CUTTING_ON_DEMAND so the scope check sees both halves while the
recorded-cost table keeps counting only the glossary.
Measured: git.md 56,779 -> 56,987 ch (+208). Loaded set: github 78,834 -> 79,042,
jira 86,594 -> 86,782 (headroom 1,878), linear 88,922 -> 89,110 (headroom 1,890).
Expected red: tests/goldens/git-agent-golden.test.ts byte-equality, by design.
Refs #350
…ad-both, mismatch DEGRADED split by cause
Three defects in the provider-resolution preamble, one commit, because each fix
is unsound without the others.
ONE ALPHABET. A project key was shape-gated in three places that never see each
other, and two of them disagreed with the third: the Git preamble and the Tracker
agent's schema table admitted `^[A-Za-z][A-Za-z0-9_]{0,9}$` while every tool-call
provider's `KEY-N` grammar required `^[A-Z][A-Z0-9_]{1,9}$`. A key the preamble
resolved and the writer recorded could be one that no reference the agent then
rendered would accept, and the user sees that as an unparseable ref rather than as
a bad key. Both sites now state the provider grammar's own alphabet, with the
ASCII-upper normalisation happening ONCE at the key's own boundary. That is a
behaviour change: a lowercase key is now rejected at the gate rather than quietly
carried to a reference that refuses it.
READ BOTH RUNGS. "First hit wins" was wrong for rung 1 by construction: the
per-repo `tracker` key NARROWS the manifest's provider, so it cannot be evaluated
without reading rung 2 first. The order is otherwise unchanged.
SPLIT BY CAUSE. One DEGRADED spelling covered two different mistakes in two
different files with two different remedies - a per-repo override that narrows to
a provider the manifest does not carry, and a configuration file whose frontmatter
provider disagrees with the resolved one. A user who reads the unsplit reason
cannot tell which file to edit, which is what naming a reason is for.
The fourth project-key rung, "the documented neutral default", is deleted: it was
defined nowhere, and the absent-key outcome the schema table already documents is
`tracker not configured`. `$ARGUMENTS` - slash-command vocabulary in an agent
prompt - becomes "the task inputs".
LIVE_REASONS 18 -> 21 net so far (+2 -1 here): adds
`tracker configuration mismatch (repository override)` and
`(conventions file)`, retires the unsplit `tracker configuration mismatch`.
`PRE_PHASE3_REASONS` -> `GITHUB_ONLY_REASONS`: the list holds github-op wording,
which is a scope, not a date.
Guard literals classified (applies ADR-025), 4 files, 5 pins:
STRENGTHEN 2 - `collectDegradedReasons` now balances one level of nesting. Its
existing alternation was written for exactly this and could never fire: the
leading `[^)]*` admits `(`, so a split reason came back truncated at the inner
parenthesis and was reported as an unregistered spelling of a registered row.
The provider-sources seam's MISMATCH_REASON moves onto the per-repo half, with
a new arm proving the conventions-file half does NOT satisfy that rung.
SOFTEN 1 (scope only) - hostile-values' "no normalisation pipeline of its own"
now excludes the project-key schema row. Its subject is the PROVIDER TOKEN,
which arrives validated and is copied verbatim; the key is a different value
with a different provenance. The arm asserts the row it excludes exists, and
a seeded provider-token pipeline is still reported.
KEEP-EXACT 2 - hostile-values' laxened-validator probe, re-pinned exact on the
new alphabet and gaining the lowercase-rejected / uppercase-admitted pair.
New arm: single-authority gains the one-alphabet claim over all three readers, by
extraction from each shipping file rather than against a constant here - a
constant in a test would be a fourth authority and the one nobody ships.
Measured: git.md 56,987 -> 57,482 ch (+495). Loaded set: jira 86,782 -> 87,277
(headroom 1,383), linear 89,110 -> 89,605 (headroom 1,395), github 79,042 ->
79,537 (headroom 1,407). Non-preamble unchanged; the growth is all preamble.
Expected red: tests/goldens/git-agent-golden.test.ts byte-equality, by design.
Refs #350
…bition
Blocker 1. `gather-release-evidence` parsed `#[0-9]+` out of commit messages and
then kept "only digit-only entries" - GitHub's issue grammar, stated in the
provider-NEUTRAL body of the agent. Under jira or linear it matches nothing, so
`SHIPPED_ISSUES` came back empty on every release and the whole
`backlink-shipped-issues` path plus both providers' `create-release` closed-issues
mechanics were dead by construction. Nothing reported it, because an empty
evidence set looks exactly like a release that closed no issues.
The step splits along the line the rest of this surface already uses. The agent
extracts CANDIDATES - tokenise on whitespace, keep what follows `refs`/`closes`/
`fixes` on the same line, bounded at 200 tokens (M7) inside the existing
100-commit bound - and states no grammar at all. The resolved provider's Mechanics
own what a reference is, and each now says so: `#[0-9]+` for github, the project
form for jira, the TEAM-KEY form only for linear (an internal id carries no team,
so nothing distinguishes one workspace's from another's). A `KEY-N` provider
additionally requires KEY to equal the resolved project key; a well-formed
reference on another project is `foreign issue reference {ref}`, not a shipped
issue. Dedup moves AFTER the grammar gate, where it was before the gate and could
therefore collapse two different projects' references into one.
Also stated, because the rewrite must not silently re-acquire them: a
`Merge pull request` subject and a trailing parenthesised reference carry no
keyword and were never candidates; an empty `SHIPPED_ISSUES` is empty, not
degraded.
The corroboration scan is replaced by the OD-9 prohibition. It named no command,
left "closing refs" undefined, gave a 60% share no denominator, and could not tell
jira from linear with a `KEY-N` grammar both providers speak - so what shipped was
an unexecutable rung. What survives is the part that is load-bearing and stated as
a prohibition: the remote, the hosting platform and the PR host are NEVER tracker
signals. Pull requests stay on GitHub under every provider, so a rule that reads
the remote would resolve `github` for exactly the users this feature is for.
Branch names go with it as a reference source in both provider modules.
New guard: `collectGithubGrammarInNeutralBody` in provider-scope. The existing
scan is one-directional - it looks for a FOREIGN provider's literal in a file that
is not that provider's - and by construction it cannot see the incumbent's grammar
left behind in neutral text, because `#[0-9]+` names no provider (avoids PF-072).
The new arm scopes to the documents that are provider-neutral by contract (the
agent and the tool-call contract), allowlists PR-host lines because pull requests
really do stay on GitHub, and was verified RED against this commit's parent on
exactly the two lines rewritten here.
Measured: git.md 57,482 -> 57,962 ch (OD-9 -737, release evidence +475 net of the
line it replaces). Loaded set: jira 87,757 (headroom 903), linear 90,085
(headroom 915), github 80,017 (headroom 927).
Deviation from the plan's ledger, recorded: the OD-9 cut measured -737 against a
budgeted -595, and the release-evidence rewrite +475 against a budgeted +310. Six
normative clauses do not fit in +310; the larger cut pays for most of the
overrun, and the two-server subsection in a later commit is written to the
remainder rather than to its own budgeted +900.
Expected red: tests/goldens/git-agent-golden.test.ts, and now also
github-status-lines (fixture line 165 - a sampled region) and the --unfreeze
round-trip that compares against it. All three by design until the recapture.
Refs #350
…freeze is retired
The always-loaded reader block said every rendered issue ref takes
`## Reference Rendering`'s form and is "never `#`-prefixed" - while nine Output
templates two hundred lines below spelled `#{number}`. The rule had to be a
PROHIBITION because the templates were frozen byte-for-byte by the Phase-0 golden
capture and the `#` could not be edited out, so the only available fix was to
reclassify a character as "github's rendering, not a literal" and hope a spawn
read the reclassification before it read the template.
The recapture authorised for this wave makes the bytes editable, so the templates
carry the token instead: nine issue slots become `{ISSUE_REF}` and the rule states
the POSITIVE github rendering, which the prohibition could never supply.
The eight PR slots are deliberately untouched. Pull requests stay on the PR host
under every provider, so `- **PR**: #{number}` is correct provider-neutral text,
and a rewrite that swept them along would render a PR reference no host resolves.
That discrimination is now asserted in both directions rather than left to care.
The command layer's capture contract and the github batch mechanics follow the
heading they read.
Decision recorded: `tech-debt archive failed for #${old_issue}` in the github
manage-debt mechanics is EXCLUDED. It is a shell expansion inside an executable
`||` chain in a file that only runs under github, where `#N` is the correct
rendering; `{ISSUE_REF}` is a token the agent substitutes into an Output template,
and substituting it into a shell recipe replaces a live variable with a literal
brace pair. The exclusion is written at the registry entry, not only here.
Guard literals classified (applies ADR-025), 2 files, 5 pins:
RE-POINTED 2 - schema-scope claim 6 clause 2, from the prohibition
(`never `#`-prefixed`) to the positive github rendering; and the
command-agent-input seam's ISSUE_REF producerPattern, which keyed on the `#`.
DELETED 2 - claim 6 clause 3 (the reclassification rule, retired with the freeze
that forced it) and its companion arm, which asserted the templates STILL
carried the `#` and would otherwise now assert the opposite of the fix.
STRENGTHEN 1 - that companion is replaced rather than dropped: every issue slot
renders through the token, no issue slot spells the bare `#`, and the PR slots
are asserted to keep theirs.
Measured: git.md 57,962 -> 57,976 ch (+14). Loaded set: jira 87,771 (headroom
889), linear 90,099 (headroom 901), github 80,031 (headroom 913).
Expected red: the two goldens and the --unfreeze round-trip, by design.
Refs #350
…e named-4xx signal once Two rate-limit holes, one on each path. GITHUB REGRESSION. `resolve-review-threads` fans out over up to 50 threads and lost its rate-limit rungs when they moved into `references/github-api.md` - a file this operation never loads. Its D4 clause covered "other 4xx" and 5xx and said nothing about backpressure, so the op's own text left the fan-out with no stop condition. It now points at the rungs, by the same route `fetch-review-threads` already uses. The rungs themselves are NOT restated: the always-loaded agent must not name GitHub's header, because two of three providers publish no such count, and `provider-literals` holds that line. TOOL-CALL PROVIDERS. Linear's backpressure is an HTTP `400` carrying `RATELIMITED`, which a status-shaped detector classifies as a generic 4xx - and D4 answers a generic 4xx with "degrade this item and continue", so the fan-out runs straight into the penalty window. The correction was stated in exactly one place: `backlink-shipped-issues`. Every other Linear operation - every single-call one included - applied the generic rule. The tool-call contract now carries `### Rate-limit signals`: a named error inside an ordinary 4xx is D4's STOP rung wherever the resolved provider's mechanics name one, read from the error TEXT rather than the status, binding on every operation and not only the one that fans out. It is read once per spawn under every non-github provider, which is what makes it reach the other nine. The provider file keeps what is genuinely its own - the literal, the status code and the transport form - and drops the reasoning the contract now states once. budget-model gains `resolve-review-threads: ['github-api.md']`, so the bidirectional file-set check stays honest about a reference the op now names. The worst-case term does not move: the non-tracker maximum was already that file. Measured: git.md 57,976 -> 58,100 ch (+124). _mcp.md 6,399 -> 6,907 (+508). max_op linear 7,636 -> 7,479 (-157). Loaded set: jira 88,403 (headroom 257), linear 90,574 (headroom 426), github 80,155 (headroom 789). Budget warning, recorded rather than absorbed: at 257 ch of jira headroom the remaining Part B additions do not all fit. See the phase handoff - the two-server subsection is the item that does not, and it is reported rather than compressed below the content its acceptance criterion names. Expected red: the two goldens and the --unfreeze round-trip, by design. Refs #350
…d an end to the permanent DEGRADED
Blocker 4. `## Reference Rendering` had no probe, no documented default and no way
to be filled. The Tracker agent wrote `# UNRESOLVED:` into it because that is what
its schema says to write when it looked and could not tell; the Git agent reads
that sentinel as `tracker.md required fields incomplete`. So `ensure-pr-ready` and
`create-release` degraded on every run, for every jira and linear user, over a
field that was never going to resolve - and the remedy the reason names ("edit
~/.devflow/tracker.md") pointed at a section with nothing to put in it.
The fix is a split, and the split is forced by `provider-scope`. The RULE is
provider-independent and lives in the contract's rendering gate: an absent
section, an absent file and a discarded token are the SAME outcome - the resolved
provider's documented default - and this gate never yields `# UNRESOLVED:`. The
VALUE cannot live there: a provider-keyed table inside `_mcp.md` would put `jira`
and `linear` literals in a file that guard scans and no provider owns. So each
provider states its own, once, as a define beside the comment cap - the other
provider fact that is stated once and invoked everywhere - and renders it at each
gate site: `Refs {KEY}-{n}` for jira, `Refs {REF}-{n}` for linear.
The lookup key is "the resolved provider", in those words. "This file's provider"
would let a hand-edited `provider:` frontmatter route around the mismatch guard
and pick the rendering of a tracker nobody resolved.
The writer moves with the reader, in the same commit, because the two halves are
only correct together: the Tracker agent's schema row now says never to write
`# UNRESOLVED:` there. A sentinel means "the writer looked and could not tell",
and for this row the honest answer is a value the reader already has.
Guard literals classified (applies ADR-025), 1 file, 1 pin:
KEEP-EXACT 1 - jira-module's MODULE_DEFINES gains `pr_link_default`, owned by
the two tool-call providers exactly as `comment_cap` is, with a body shape.
Without the row the parity scan reads a deliberate two-provider ownership as
an asymmetry; with it, a third provider omitting the define is still reported.
New arms in single-authority: the rule is authored ONCE in the define that
expands into every gate site and is keyed on the resolved provider; each provider
states exactly one default and the emitted contract states none; a discarded
token - `pr-link: $(whoami)`, denied by the gate's own `$` rule - routes to the
default and a `### Substitutions` row and never to the incomplete-fields
degradation; and the writer's row carries the matching prohibition.
Measured: no gated term moves. The gate define is authoring-only, so `_mcp.md`
stays 6,907 ch, and the two sites that gained the value - create-release and
ensure-pr-ready - are neither the max_op term nor the worst-case op. Loaded set
unchanged: jira 88,403 (headroom 257), linear 90,574 (headroom 426).
Scope deviation, recorded: the two-server per-capability scoping subsection
planned for this commit is NOT here. It is a `_mcp.md` addition, billed per spawn
to both providers, and cannot be written in 257 ch without cutting the clauses its
acceptance criterion names. Reported with the arithmetic in the phase handoff
rather than compressed or funded by raising the ceiling.
Expected red: the two goldens and the --unfreeze round-trip, by design.
Refs #350
…osts none of it
Gate-0 decision. On a provider whose comment format has no collapsed-block
analogue, the design artifact degraded to a POINTER: a comment naming
`{PLAN_ARTIFACT_PATH}`, a local file that is not committed. That resolves for its
author and for nobody else, so the traceability comment - which exists for the
reader who was not in the room - handed them a filename. The plan is now the
comment: posted through the same gate as every other body, line 1 the marker,
referenced from `## Implementation Plan`.
The over-cap branch is the part that needed stating, because truncating is the
obvious thing to do with a body that is too long and it is the wrong thing here.
A truncated plan reads as a whole plan; nothing in the comment tells the reader
which half is missing. So over the cap the operation posts NONE of the plan,
falls back to the pointer sentence, and emits
`TRACEABILITY: DEGRADED (plan artifact exceeds comment cap)` - no `{provider}`
token, because the cap is the provider's and the failure is not. The cap is
measured AFTER redaction: the scrubber's replacement tokens can make a body that
fitted before the scrub too long after it.
LIVE_REASONS 19 -> 20 with the new row, registered in the commit that authored its
emitting site.
New arms in single-authority: per provider, the artifact section states content
rather than a pointer, both the old heading and the sentence that argued for it
are gone, and the over-cap branch refuses rather than truncates - with a probe
that discriminates the two shapes, since the truncation form is CORRECT for the
D3 comment sitting three lines below it.
Measured: no gated term moves. `ensure-traceable-issue` is neither the max_op term
nor the worst-case op for either provider. Loaded set unchanged: jira 88,403
(headroom 257), linear 90,574 (headroom 426), github 80,155 (headroom 789).
Order deviation, recorded: this is the plan's ninth P3a commit, landed eighth. It
has no dependency on the `ref_preflight_head` hoist that was to precede it, and
the hoist rewrites emitted text at both gated terms while 257 ch of headroom is
left - so the fix with user-visible behaviour went first rather than being put
behind a refactor that may not fit.
Expected red: the two goldens and the --unfreeze round-trip, by design.
Refs #350
…ladder as shared rules Two of the eight authoring-only defines in the tool-call contract had no SHARED_RULES row, so the guard that says "declared in the authoring module and in no provider module" was silent about them. Both are rules a per-op file would plausibly restate, and both decide something a second copy would decide differently: - `reference_rendering_gate` carries the shape gate, the metachar denylist and the fallback a discarded token routes to. The token is interpolated into a branch name and into a PR body from a hand-editable machine-wide file, so a second author is a second denylist. - `dedup_ladder` carries the four rungs and the rule that a recorded hint may only NARROW the probe order, never raise it. The rung a provider lands on decides whether a release back-link is suppressed on evidence or on a coincidence, and that qualification has to live in the same sentence as the rungs it qualifies. Both rows are proven live by the arm that already asserts every registered rule reaches every tool-call provider's generated tree. Expected red: the two goldens and the --unfreeze round-trip, by design. Refs #350
The floor and its message had drifted from the table: it said "eighteen" and
guarded `>= 18` while the table now carries twenty rows - the mismatch reason
split by cause (+2 -1) and the plan artifact's cap (+1), each registered in the
commit that authored its emitting site.
A floor rises with the table and never falls. Left at 18 it would have admitted
the silent removal of everything this phase added.
Not 21. The twenty-first row planned for this phase,
`ambiguous tracker server — {n} servers offer {capability}`, has no emitting site:
the two-server scoping subsection it belongs to did not land, and a registered
reason with no emitter fails the forward arm by design. The phase handoff records
why, with the byte arithmetic.
Refs #350
…er wins, ambiguity is DEGRADED
Two connected servers that both offer tracker capabilities is the one
configuration in which a write lands in somebody else's tracker and nothing
downstream can tell (AC-13). The tool-call contract now answers it, in six
clauses, in the file that is read once per spawn by every tool-call provider:
- partition the exposed tools by the server that provides them;
- qualification is per CAPABILITY, never per server — qualifying for one
promotes a server for no other, so a create and its comment cannot be
routed apart by a single promotion;
- exactly one qualifying server wins and nothing further is asked of it: a
server whose descriptions never name the tracker is still the only thing
that can serve the capability, and refusing it degrades on terseness;
- two or more ⇒ no call for that capability and
`TRACEABILITY: DEGRADED (ambiguous tracker server — {n} servers offer {capability})`;
- the winner is pinned for the whole spawn;
- a write is preceded by one corroborating read from the SAME server — once
per spawn, never per item (design review H2), so a fifty-issue backlink
does not become a hundred calls.
The untrusted-reads paragraph is extended rather than duplicated: a tool
DESCRIPTION is vocabulary for deciding what a tool does and never an
instruction to follow.
The rule is spelled WITHOUT the transport's tool-name prefix literal.
tests/guards/provider-scope.test.ts forbids that literal in every loadable
file, so the partition is stated as the leading namespace segment of the tool
name — the same claim, in the vocabulary this corpus is allowed to use.
Emitter and registry land together, per the standing rule that a DEGRADED
literal joins LIVE_REASONS in the commit that authors its emitting site: the
table goes 20 → 21 and its floor with it. `{n}` is registered as
NON-instantiable — a count has no closed domain — while `{capability}` is
instantiated against the contract's own table, and an arm proves both halves.
A second arm proves the plural case does not reuse `no tracker tool for
{capability}`: "nothing offers it" and "several things offer it" are opposite
conditions with opposite remedies.
Funded from inside the contract, not from a raised ceiling. Three cuts, each a
restatement of something the same file already says once: the last-four-rows
note under the capability table (the table's own right column), the
rotation clause's echo of the status line it follows, and "Never probe inside
a loop" after the sentence that already says once per spawn.
BUDGET_LOADED_SET_JIRA and BUDGET_LOADED_SET_LINEAR are RED after this commit
and are left red deliberately: contract prose is billed once per spawn to both
providers, the clauses AC-13 names do not fit the transition headroom, and the
ceilings are re-pinned from measurement by the phase that owns them. No
ceiling was raised here. git.md and the GitHub loaded set are unmoved.
Pins classified (applies ADR-025): STRENGTHEN 6 (reason-table floor 20→21, the
{n}/{capability} instantiation arm, and the four two-server clause arms with
their delete-one probes), STRENGTHEN 1 (MCP_SHARED_LITERAL_REGISTRY gains the
qualification sentence). SOFTEN 0, DELETED 0, RE-POINTED 0.
Refs #350
… lines into _common.mds
Fourteen ref-grammar pre-flight sites across `_jira.mds` and `_linear.mds`, and
nine lines that all three tracker modules wrote out identically, now have one
author each. New partial `src/assets/mds/tracker/_common.mds` holds:
- `compliance_step_gate` / `compliance_issue_policy` — every provider's,
the CLI one included;
- `state_batch_line(subject, subject_ref, ref_token)` — the `**State**:` batch
rendering line, parameterised by the three words that differed;
- `bare_number_rule` and the four ref pre-flight heads — `single`, `list`,
`entry`, `branch` — the tool-call providers', with the grammar, the
normalisation step, the alternation warning and the per-site tail arriving
as arguments.
THE PRE-FLIGHT HEADS ARE NOT IN `_mcp.mds`, WHERE THEIR SUBJECT PUTS THEM, AND
THE REASON IS MEASURED. Compiling `_jira.mds` against `_mcp.mds` costs 3.3 s at
nine defines there, 4.9 s at ten, 8.3 s at eleven, and does not finish inside
twelve seconds at twelve — measured with defines whose entire body is one
character, so it is the define COUNT against that module's size and not the
content. The same five defines cost 1.2 s in `_common.mds`. The ownership split
is therefore stated as: `_mcp.mds` owns the emitted contract and the defines the
sink-bypass guard requires every posting mechanic to spell; `_common.mds` owns
every other shared line, with each define naming its own audience. Both module
headers say so, and the cliff is recorded at `_mcp.mds` so the next hoist does
not rediscover it by hanging a build.
FOUR DEFINES, NOT ONE TAKING A MODE, for a second mechanical reason: MDS `@if`
is block-structured and its expansion ends the line, while all fourteen call
sites are fragments in the middle of a markdown list item. A mode parameter
would have broken the list each site lives in.
The compliance and `**State**:` hoists are emitted BYTE-IDENTICALLY — the whole
generated reference tree was diffed against its pre-hoist copy and only the
fourteen pre-flight sites moved. The pre-flight wording is unified where the
hoist merged two spellings of one rule; there are no jira or linear goldens, and
the two gated terms were held: jira's `setup-task` and `backlink-shipped-issues`
are byte-unchanged, linear's max_op moved +1 and its worst-case op +11.
One line is DELIBERATELY not hoisted and now says so in a test rather than in a
comment: the marker-neutralisation bullet is byte-identical in content and NOT in
indentation — five spaces in the GitHub module, three in the other two, because
the list nests differently. One define emits one string, so hoisting it would
re-indent one of the three, and a bullet re-indented out of its parent stops
qualifying the step it belongs to (PF-063).
`tests/fixtures/mds-manifest.ts` is touched exactly once, here, as the plan's
fourth sequencing constraint requires: partial discovery is widened from a
listing of `_partials/` to a repo-wide walk classifying by the build's own rule
(no `output-dir:` key), `MDS_REFERENCE_PARTIALS` and `ALL_MDS_PARTIALS` join the
manifest, and `partial-count` goes 12 → 13 in the same commit — a count-only
bump would have gone red naming no file.
`dist-host-count`'s pattern is narrowed to name its receiver in the same commit,
as a forced ripple: `partial-count` now pins the same number in the same file,
and two entries sharing one bare pattern have no per-entry accounting — each
one's decrement probe would keep finding the other's site.
Pins classified (applies ADR-025): STRENGTHEN 12 (repo-wide partial set-equality
and its probe, the output-dir arm widened to every manifest partial, both floor
patterns narrowed to their receivers, four SHARED_RULES rows for the hoisted
heads, the declaration arm now checked in BOTH directions against BOTH authoring
modules, the import arm against both, and the three containment-indent arms),
RE-POINTED 3 (the `_partials/` floor to the repo-wide roster, the printed-count
expectation, the tarball shipped-source count). SOFTEN 0, DELETED 0.
Still red by design and only these: the three goldens, and
BUDGET_LOADED_SET_JIRA / BUDGET_LOADED_SET_LINEAR from the preceding commit.
Refs #350
…olds The first line called the module "the provider-NEUTRAL lines every tracker module writes identically", which the body then contradicts three paragraphs later: five of its eight defines are the TOOL-CALL providers' only, and they are there for a measured compile reason rather than because they are neutral. A module header that disagrees with its own module is the thing the next reader trusts first. Module-level prose; the build emits it nowhere and no artifact byte moves. Refs #350
…in, one heartbeat, positive path shape The claim gate echoed nothing: `:` on the winning branch and `exit 0` on the losing one are indistinguishable outputs, so a run could not classify its own outcome and neither could anything reading it. It now echoes CLAIMED / LOST and exits 3 on the loss, with "absent output ⇒ LOST" as the fail-closed default for a run killed between the create and its echo. The claim PRIMITIVE is unchanged — `set -o noclobber` is already exclusive (PF-068); this commit is observability. The compose step was an unchecked first link: `cat > "$RAW" <<'EOF' … EOF` is its own statement and the shell discards its status, so a full disk or a vanished $RAW produced an empty composition that every later link happily scrubbed. It is brace-grouped and `&&`-joined into the same chain the placement hangs off, which keeps the chain single and unbroken (PF-066). The shape gate bracketed the composition at two anchors and said nothing about the lines between or after them — which is where `### Substitutions` sits, and every row of that section is a value that already failed its own shape gate. A third, negated `grep` validates the whole range against the `## Reference Rendering` denylist (backtick, `$`, `;`). Double quote and backslash are deliberately NOT at this link and both reasons are stated in the prompt: the scrubber may re-quote an assignment it redacted, and a backslash in a bracket expression is an escape to some greps and a literal to others. The heartbeat instructed a per-unit cadence (~19 touches). It is now ONE touch at the probe → compose boundary: a cadence in a prompt has no observable count, so nothing distinguishes a run that followed it from one that touched once, while one refresh at the boundary re-arms the bound for the only phase that could outlive it. Also: every path is bound in the `## Environment` fence and referenced only as "$VAR" (`TRACKER_ATTEMPTS_FILE` joins the three); a closed-enum global-safe section whose default is one of its own values is never sentinelled (with `## Dedup Strategy` explicitly carved out, its default being a live probe); the release of the claim is stated for write-less exits too; and `[DR-02]`, `PF-003` and the test-file path are dropped from the prompt — a model cannot follow an anchor, and each rule they annotated is stated in place. Pins classified per literal (ADR-025), counts per class per file: tests/tracker-agent.test.ts KEEP-EXACT 5 · RE-POINTED 2 · STRENGTHEN 7 tests/shell-hooks-tracker.test.ts STRENGTHEN 1 tests/seams/tracker-claim-staleness.ts STRENGTHEN 1 tests/guards/heredoc-quoting.test.ts STRENGTHEN 2 tests/guards/retired-wording.test.ts STRENGTHEN 2 KEEP-EXACT are the literals bound to a shared constant or carrying the protocol: the two schema-oracle grep anchors, `FINAL act`, and the two new outcome literals (`Absent output ⇒ LOST`, `a write-less exit still deletes the claim`). RE-POINTED are the two bounded-window regexes whose SUBJECT survived and whose address moved (`[DR-02]` → the claim it cited; `.tracker.attempts` → the bound variable) — same strength, new address. STRENGTHEN replaces prose with an executed or counted check: the two-claim sequence becomes 8 concurrent claimants (exactly one CLAIMED, every loser LOST with status 3, and rename-to-claim driven through the same harness to prove it reports many winners); `/\btouch\b/` becomes a counted collector that reports a repetition; the shape gate's two greps become three named links; `collectScrubChain`'s statement splitter becomes quote-aware (the range link quotes a bracket expression containing `;`, which the old splitter would have cut the chain at — a false positive on a security control); `breakScrubChain` now breaks the `&&` AFTER the scrubber rather than the first on the line, which the compose join had made the wrong one; and the hook's source-level [DR-10] arm now reports READS before the sentinel, not only forks. No pin was widened to make a red test green. The hook's path positive-shape control landed in Part A and is unchanged here; this commit only verifies it adds no read ahead of the sentinel gate. Refs #350
Two github assumptions sat on the path a non-github issue has to travel, one at
each end of /implement.
The setup-task spawn forwarded ISSUE_INPUT only when `$ARGUMENTS` started with
`#`. That is github's spelling; `PROJ-12` and `ENG-12` are the other two. Under
either of them the argument fell through to TASK_DESCRIPTION, so the branch was
derived from prose, no issue was fetched, and nothing reported a problem. The
first token is now forwarded verbatim and the only test the command makes is the
one that holds under every provider: a path ending in `.md` is a plan document.
`setup-task` is where a provider has been resolved, so classification belongs
there and nowhere earlier.
The PR step rendered `Closes #{ISSUE_NUMBER}` whenever a number was known, with
no provider in the condition — under jira or linear that number is the tail of a
key, so the line closed whichever GitHub issue carried the same digits: a wrong,
silent, GitHub-visible write. The body now uses `ISSUE_PR_LINK` verbatim when the
Git agent captured one (it is already rendered in the resolved grammar, and
re-rendering is a second rendering site), and composes the `Closes` line only
under github.
The Code agent's paste gate had a single github arm, so the one value this seam
exists to carry was rejected as malformed for two of the three providers. It is
now one full-line-anchored arm per provider, with the two bounds an anchor cannot
express written out (a newline is refused — `$` is end-of-LINE in some engines —
and the line is capped at 60 characters). `(none)` is explicitly not a mismatch;
a mismatch names the RESOLVED provider in the DEGRADED reason; and a bare number
under a non-github provider reuses the already-registered
`ambiguous issue reference` rather than minting a spelling. LIVE_REASONS is
unchanged at 21.
The jira and linear arms OVERLAP on a plain uppercase key — they part only on
jira's underscore and linear's single-character keys — which is stated in the
prompt and recorded in the payload table, and is the reason the resolved provider
picks the arm instead of the arms picking between themselves.
`provider-scope`'s block allowlist becomes a registry of allowlisted REGIONS
rather than one constant, with the paste gate as its second row and a
justification floor on every row. The alternative was a hand-written second
condition beside the first — two mechanisms for one rule, which is how one goes
stale (PF-067). The Code agent is outside the Git spawn surface and loads no
provider mechanics file it could defer to, so a per-provider sink check has to
enumerate the closed set once, inside the gate; it reads the arm for the provider
already resolved rather than resolving one, which is what keeps it a sink check
and not a second convergence point (PF-023).
Pins classified per literal (ADR-025), counts per class per file:
tests/seams/pr-link-handoff.test.ts RE-POINTED 2 (+ 6 new executed arms)
tests/guards/provider-scope.test.ts STRENGTHEN 2
tests/guards/retired-wording.test.ts STRENGTHEN 1
tests/build-mds.test.ts (3 new arms, no pin reclassified)
RE-POINTED are the two `does not match github reference grammar` pins, whose
subject survived and whose address moved to the `{provider}` template — same
strength, new address. The retired-wording addition is a CLASSIFIER, not a
spelling: the literal survives at exactly one site and the entry's pattern
admits it only while it stays github-gated, so the permitted occurrence is
registered rather than excluded by hand. The new pr-link arms execute the
grammars read OUT of the prompt against a payload table rather than re-typing
them, so a missing anchor is caught by running it instead of by reading it.
No pin was widened to make a red test green.
Refs #350
…rdinates from the hook `references/github-api.md` stated the thread-resolution condition in its own words, and the words were wrong: it admitted FALSE_POSITIVE and BY_DESIGN as resolvable, where D9 says those two are the thread author's call to close and devflow replies with cited evidence but leaves the thread unresolved. That is what a second authority costs — not drift in the abstract, a live contradiction between the document an operation loads and the gate the agent applies. The reference now names the gate and holds the mutation. A new guard keeps it that way: the resolve PREDICATE is stated exactly once across the reference tree, by the marker registry that defines D9. The corpus is the tree, not the agent prompt — the Git agent states the gate three ways (a table, a sentence, the step that applies it) and those are one document's internal business; what a reference cannot do is repeat a rule it has no way to notice moving. The detector matches the predicate, not a byte-exact sentence, because the defect it exists for WAS a paraphrase, and a registry of spellings only catches the spelling somebody already wrote. `GITHUB_API_MD_CHARS` is re-measured and re-pinned in this commit, 21,218 → 21,166, which is that constant's own rule: the only commit that may move it is the one that edits the file's bytes. The hook's two `(§14.9 constraint N)` comments are dropped. A hook comment is read during a live debugging session by someone who has the script in front of them and not the design artifact, so the coordinate is cost with no reader, while the rule each annotated — reject never repair, and the closed model domain — is stated in the same sentence. Registered in `retired-wording` as a pattern scoped to the hook tree: the `src/core` JSDoc sites carry the same coordinates for a maintainer reading the source with the repo checked out, which is a different argument, and sweeping both under one entry would settle it by omission. Pins classified per literal (ADR-025), counts per class per file: tests/guards/retired-wording.test.ts STRENGTHEN 1 tests/tracker/byte-budget.test.ts 1 equality baseline re-measured and re-pinned tests/git-agent.test.ts (4 new arms, no pin reclassified) Refs #350
…ired agent name `Validator` is a retired agent name, and GAP-5's guard forbids it in any shipped artifact — including as an ordinary English word. The new closed-enum rule introduced one in B7. The schema table's own column is "Shape gate at the sink", so naming the gate the way the table names it is both correct and the spelling the rest of the section already uses. Refs #350
…nd git-agent.md
Both goldens re-captured under the third — and final — one-time authorisation
(Gate 1, 2026-09-19). Measured first with
`scripts/update-golden.ts github-status-lines --unfreeze --out-dir <scratch>` and
the scratch output diffed against the live fixture before anything was written;
the fixtures are byte copies of that measurement, never hand-edited.
Nine fixture lines moved, each mapping to a named Part-B edit, and the line
count is unchanged (250) — no offset drift:
48, 52, 61, 86, 99 the {ISSUE_REF} rendering rule and its template sites
(8322372)
165, 166 the release-evidence candidate gate and grammar gate
(1ccaf45)
239, 240 the provider-neutral PR link line in code.md (d9e71a3)
Equality baselines re-set to the measurement in the same commit as the bytes:
GIT_MD_CHARS 58,818 -> 58,100; TOTAL_CHARS 68,341 -> 67,623;
FIXTURE_BYTES 17,527 -> 18,270; GIT_AGENT_BYTES 59,279 -> 58,549.
GIT_MD_LINES, TOTAL_LINES, FIXTURE_NEWLINES, SKILL_* and every PRE_PHASE0_*
are unchanged. No assertion logic changed.
The header now records all three spends (e4876e0, c0b9860, this commit) and
states that the authorisation is spent and none is outstanding.
Refs #350
…ings onto one re-baselined number each Gate 1 authorised a one-time re-baseline of the byte ceilings: set just above the post-change measurement, record what the raise bought, retire the companion gate, re-derive downward thereafter. Every number below is read off the byte table this file prints, never hand-derived (PF-057). BUDGET_LOADED_SET 77_824 -> 80_200 (measured 80_155, headroom 45) BUDGET_LOADED_SET_JIRA 88_660 -> 89_500 (measured 89_459, headroom 41) BUDGET_LOADED_SET_LINEAR 91_000 -> 91_700 (measured 91_642, headroom 58) PREAMBLE_MAX_LINES 40 -> 36 (measured 35, headroom 1 — LOWERED) BUDGET_LOADED_SET_P3 is deleted: the GitHub row is now one pinned number gating the shipped shape rather than the frozen pre-split set plus a computed phase companion. The two provider rows' "not a free number" companion arms are retired with it — they held the delta over a GitHub ceiling that was itself derived, and neither row is derived any more. What stops a provider ceiling being a free number now is the thin headroom recorded in its JSDoc plus the downward-only rule, both of which the surviving gate's message states. What the provider raise bought is entirely references/tracker/_mcp.md, 6_907 -> 7_963 ch: two-server per-capability scoping, the rate-limit signals section, the Reference Rendering rule, and the plan artifact posted as content with an over-cap DEGRADED path. Each JSDoc and each numeric-floors.json description records this, dated, and says "LOWERED THEREAFTER, NEVER RAISED AGAIN" (PF-071 — a one-time re-baseline is recorded as one). RED proved before the pin, then the probes removed: the collapsed GitHub ceiling refuses the pre-B1 measurement (80,873 > 80,200); each provider ceiling refuses its measurement plus 61 ch (89,520 > 89,500; 91,703 > 91,700); the preamble ceiling refuses a 37-line preamble. HELD, and NOT in this commit: the git.md pair. BUDGET_GIT_MD / BUDGET_GIT_MD_P3, PREAMBLE_CHARS_P2, their arms and their numeric-floors.json values are byte-unchanged — measured git.md is 58,100 against the plan's bounded collapse target of 56,400, so the collapse is the plan owner's decision, not this phase's. The two held descriptions lose only the sentences that named the now deleted BUDGET_LOADED_SET_P3; their values and patterns are untouched. Refs #350
…state FROZEN_LIFECYCLE_RULE said the fixture was "frozen at Phase 0, never regenerated through Phase 3", which read as a constraint that expires. It does not: the fixture is frozen, and regenerating it takes --unfreeze AND a fresh explicit authorisation naming the bytes it permits. Three have been granted and all three are spent — the log lives in tests/goldens/github-status-lines.test.ts, which the rule now names rather than restates. No behaviour change: the refusal path, its exit code and the --unfreeze/--out-dir parsing are untouched. The refusal guard's pin moves with the wording, and is strengthened while it moves. It matched /frozen at Phase 0|never regenerated through Phase 3/ over the whole combined output; the replacement matches the rule sentence's own clause, because the override hint printed two lines below it carries both "--unfreeze" and "authorisation" and would satisfy any looser pattern on its own. Proved red against a rule sentence missing the authorisation half, then green. Refs #350
…end state CLAUDE.md's single ~9.5 KB Tracker paragraph becomes three end-state paragraphs — selection and bundle, how operations run, lifecycle — at 3,458 characters with no internal identifier in any of them. A citation a reader of CLAUDE.md cannot follow is decoration, and a mis-numbered one is worse than none. "Universal Skill Installation" is retired as a description because the install is no longer universal: skills install for the selected plugins plus their declared `requires:`, the default plugin set carries 32 of the 40 plugin-owned skills, and a shadow outside the selection is dormant rather than deleted. README, CHANGELOG (Unreleased), the CLI reference, the skill catalog and the file-organization doc follow the same end state, including the two user-visible behaviour changes this branch carries: Jira issue keys are ASCII-upper-normalised, and the install footprint drops to 13 reference files for github against 24 for jira/linear. Lines naming deleted files are removed rather than annotated: the containment exemption registry, the baseline reference tree and the guard census no longer exist, so the inventories no longer list them. Git holds the history. New guard tests/docs/claude-md-tracker-section.test.ts holds the block to 3,500 characters and to zero internal identifiers, with both collectors driven against seeded known-bad input. The ceiling is registered in numeric-floors.json so it can be lowered and never raised. RED proved first: 5 of 6 assertions failed against the previous CLAUDE.md. Refs #350
…ences and test-harness knowledge Four knowledge bases rewritten as end state rather than annotated with a transition. Phase labels, the "3b/3c adds" narration and every reference to a file this branch deleted are gone; git holds the history. New surface the four now describe: `requires:` per plugin with `skillsOf` / `skillOwners` / `buildScopedSkillsMap` / `resolveSkillInstallPlan` / `TEMPLATE_SKILL_REFS`; `installedReferenceManifest` as the INSTALL-side sibling of `generatedReferenceManifest` (13 for github, 24 for jira/linear, `_mcp.md` iff MCP-backed) with the standing trap named — the loaded set is per-spawn and is neither of the other two numbers; `overlayInstalledReferences` as the one wrapper; `convergeTrackerArtifacts` in the new `tracker-install.ts`, including why the Tracker agent is copied and then removed on a github install; `install-report.ts` as the new home of the summary renderers; `runTrackerSet` with its fixed converge order and two exit-1 branches; `readTrackerMechanics`' three outcomes; `resolveInstalledPlugins` and retention-from-manifest on uninstall. Constraints recorded so nobody rediscovers them: `_mcp.mds` is at a measured define cliff (12 defines do not finish compiling), so `_common.mds` is where anything further hoists; `collectDegradedReasons` is parenthesis-balanced, which makes a naive `\(([^)]*)\)` parse of a DEGRADED reason wrong; `LIVE_REASONS` is 21 and `PRE_PHASE3_REASONS` is now `GITHUB_ONLY_REASONS`; the golden fixture's three authorisations are all spent; the ceilings are stated at their current values with the one-time authorised re-baseline recorded as one. `code.md` is named in tracker-feature as a THIRD authority for each provider's reference grammar — accepted because the Code agent is outside the Git spawn surface with no mechanics file to defer to, and recorded with the condition under which those arms move. Also in this commit: the one-line frozen-list note in legacy.ts. The `*_V2` names are the frozen spelling of an era, not a version to bump (avoids PF-012). Refs #350
…ches Clarity and de-duplication only; no behaviour change, no assertion or guard arm weakened. - src/cli/commands/init.ts: four copies of the "log each SummaryLine at its own severity" loop collapse onto one `logSummaryLines` helper. The helper keeps the `switch` + `const _exhaustive: never` arm the overlay-summary copy carried, so a level added to `SummaryLine` is still a compile error rather than a silent degrade to `info` at four call sites. - src/core/mds-variants.ts: `contractRelPath` takes the already-expanded pairs instead of re-expanding the registry. `installedReferenceManifest` has already thrown on a registry that does not expand, so the second expansion was duplicated work behind a refusal branch that can never fire. - src/targets/claude-code/tracker-install.ts: `firstExisting` reuses `pathExists` instead of repeating the same try/access/catch. - src/cli/commands/tracker.ts: the `resolveTrackerCliAction` doc comment now says why the resolver stays standalone (unit-tested over the (current, requested) matrix) rather than describing which branch reaches it. - tests/tracker-install.test.ts: drop the local `refsRoot` duplicate of the file-level `refsTarget`. - tests/tracker/single-authority.test.ts: three identical inline filters for "the tool-call providers" become one `toolCallProviderIds` helper. Refs #350
…re graph stays linear
`@mdscript/mds@0.2.0`'s resolver deep-copies the captured scope of a SELECTIVE
import and then re-snapshots the whole captured set once per `@define` in the
importing module, so the imported graph is copied once per define. `_jira.mds`
and `_linear.mds` are the two modules that selectively imported 8 names from
`_mcp.mds` plus 7 from `_common.mds`, and they paid for it: ~4.6 s each to
compile, roughly doubling `npm run build:mds`. That is invisible locally and
fatal on CI, where one vitest run spawns ~52 full builds (47 in
tests/build-mds-generator-hosts.test.ts, 3 in tests/build-mds.test.ts, plus the
memoised buildCommittedTree in tests/helpers.ts) and a 2-core runner then pushes
a single build past the 60 s spawnSync timeout. Run 35472010050 failed exactly
there, as three unrelated-looking ETIMEDOUTs in build-mds.test.ts,
build-mds-generator-hosts.test.ts and seams/pr-link-handoff.test.ts.
Alias imports (`@import "./_mcp.mds" as mcp`, call sites `{mcp.name()}`) capture
shallow. They change lookup, not expansion, so the emitted bytes do not move.
Timings (in-process compileFile, same machine):
module before after
_mcp.mds 1 ms 1 ms (unchanged — imports nothing)
_github.mds 13 ms 13 ms (unchanged — 3 selective names)
_jira.mds 4634 ms 22 ms
_linear.mds 4643 ms 21 ms
npm run build:mds 9967 ms -> 873 ms
`diff -r` of dist/skills, dist/commands and dist/agents, built from the
pre-change sources versus the post-change sources: empty, all three.
- src/assets/mds/tracker/_jira.mds, _linear.mds: both imports converted to the
alias form, every call site of an imported rule prefixed, and a header
paragraph stating why the alias form is load-bearing so the next editor does
not "tidy" it back.
- tests/build-mds-compile-time.test.ts (new): budgets each reference module at
1,500 ms — ~60x the measured ~22 ms, ~3x under the 4.6 s cliff — and names the
cliff in the failure message. Its known-bad probe reconstructs the selective
spelling from the shipped module, proves the two emit identical bytes, and
requires the reconstruction to blow the budget, so the guard cannot go vacuous
(PF-018). Proven RED against the pre-fix sources: _jira 4591 ms, _linear
4509 ms over budget.
- tests/provider-literals.test.ts: the "imports the authoring module and invokes
every rule" arm matched one of MDS's two import spellings and the bare call
site only. Same claim, now held against the `@import` directive and against a
call site with or without an alias prefix, via two named predicates with their
own seeded negatives. No assertion relaxed — a module merely NAMED in prose
still fails the import check, and a sibling rule still fails the invocation
check.
Refs #350
The dedup-ladder seam requires each tool-call provider module to invoke the
shared `dedup_ladder` define exactly once, and counted that by splitting on the
literal `{dedup_ladder()}`. The provider modules now reach `_mcp.mds` through an
alias import, so the one call site is spelled `{mcp.dedup_ladder()}` and the
count read 0.
The claim is unchanged and unrelaxed: exactly one invocation, bare or
alias-prefixed, counted by a named collector whose seeded negatives show it
still reports two call sites as two and still refuses to count the define's own
declaration or a mention of its name in prose.
Refs #350
…hen there is none `converged` answers "did this run copy the agent", and both call sites gated the presence sentinel on it. That is wrong in both directions. SUPPRESSING A WRITE IS NOT FAIL-CLOSED. On github -> jira the sentinel is absent anyway, so "skip the write" and "leave nothing advertising a missing agent" coincide, and that is the only transition either call site tested. On jira -> linear they come apart: jira's sentinel is already on disk, so skipping the write leaves it advertising a provider whose agent the run just failed to install — the exact state the suppression exists to prevent — while the message told the user "nothing advertises a provider whose agent is missing". READING `converged` ALSO DISABLES WORKING INSTALLS. A re-copy that fails over an already-installed agent (transient EACCES, a full disk) reports converged=false with a perfectly spawnable file still on disk. Suppressing there turns a transient I/O error into a silently disabled provider. So the gate is PRESENCE, not success. `ConvergeTrackerArtifactsResult` gains `agentPresent`, probed at the target on each failure path rather than inferred (the github arm answers it without a probe — it knows what it removed). Both call sites write the sentinel when the provider is advertisable and REMOVE it otherwise, through `applyTrackerSentinel`, which is the one sentinel owner (D-TRACKER-OWNER) — never an inline fs.rm at a call site. The warning now says what happened. RED proved first: the two new runTrackerSet arms and the two new persistManifestThenConvergeTracker arms fail against the parent, the first pair on `sentinel:github` never being called and the second on `sentinel:linear` never being called. Pins classified (applies ADR-025): RE-POINTED 2 — the two "unconverged agent suppresses the WRITE" arms keep their subject (the write does not happen) and gain the removal that makes the claim true; the message pin moves from `sentinel not written` to `sentinel removed`. STRENGTHEN 5 — four new arms covering the transition both existing arms could not see, plus convergeTrackerArtifacts' own agentPresent arms including the failed-re-copy-over-a-present-agent case. SOFTEN 0, DELETED 0. Refs #350
The positive shape gate was applied to the CONCATENATION "$PROJECT_ROOT$TRACKER_DEVFLOW_DIR", and only Section 3 interpolates both. Section 2 embeds $PROJECT_ROOT alone (the Learning spawn directive's `Project root: $PROJECT_ROOT`), so a ~/.devflow path outside the allowlist suppressed the Learning directive over a value that directive never carries. That is a regression against the denylist it replaced, and it is silent and permanent: the old list was `"`, `\`, CR, LF, none of which a space matches. A home directory containing a space — or `+`, `~`, `@`, `,`, `:` — now disabled the entire learning pipeline for that machine, every session, with `dbg` the only trace. Reproduced with a clean project root and a DEVFLOW_DIR carrying a space: the directive is absent; with a clean DEVFLOW_DIR it is emitted. Fail-closed is only the safe direction when the gate refuses a sink its value actually reaches. So it is one gate per VALUE: DIRECTIVE_ROOT_SAFE $PROJECT_ROOT consulted by Section 2 DIRECTIVE_PATHS_SAFE + $TRACKER_DEVFLOW_DIR consulted by Section 3 seeded from the narrower flag, so the wide one can only ever be narrower and the two cannot drift apart. No character is newly admitted at either sink: a hostile project root still refuses both directives, which the four OUTSIDE_ALLOWLIST arms continue to prove. The `''` arm also stops being dead code — under the concatenation an empty $PROJECT_ROOT could only reach it if $TRACKER_DEVFLOW_DIR were empty too, which lines 45-57 make unreachable, so a test asserting the arm's presence was pinning a control that controlled nothing. Also corrected in the same block: the comment claimed the range rejects "any non-ASCII character". A `case` bracket range collates under LC_COLLATE, and `A-Za-z0-9` admits an accented letter under a UTF-8 locale (verified: /Users/cafe-with-an-accent/p is admitted under en_US.UTF-8, refused under C). Every byte the gate exists to refuse is outside the range in both locales, so the security property is unaffected — but a comment stating a guarantee the code does not provide is the comment a future reader relies on. Pins classified (applies ADR-025): STRENGTHEN 3 — collectGuardedSections now names a flag PER SECTION, so a section reading the wider flag is reported rather than passing; its known-bad probe drives that exact coupling (and keeps the original no-flag-at-all defect, plus an underived-preamble case); and the allowlist arm requires two matchers and both `case` heads, so one gate over a concatenation fails by shape. RE-POINTED 1 — the gate-block slice anchor moves to the first flag. New behavioural arm: a hostile ~/.devflow suppresses the tracker directive and spares Learning, proved RED against the coupled gate. SOFTEN 0, DELETED 0. Refs #350
Each arm below produced its expected result whether or not the thing it names was true, which is the PF-018 shape the files they live in claim to avoid. REQUIRES CLOSURE, REVERSE ARM. The known-bad probe built a synthetic plugin with no commands, no agents and no skills, then asserted that its corpus does not reference 'nonexistent-skill'. An empty corpus references nothing, so the assertion was a tautology: it passed for a gutted collectPluginRefs, never constructed a violation string, and never ran the reverse loop or its `requires.length === 0` skip. The loop is now a named collector the live arm calls, and three probes drive it — a REAL plugin's corpus with its requires list kept whole plus one entry nothing can reference (so the collector must discriminate one dead entry from several live ones, not just report everything), a plugin whose requires entry has no corpus that could reach it, and the empty list. Keeping the base plugin's requires whole is load-bearing: the scan's scope is `skills ∪ requires`, so dropping entries shrinks the corpus and reports live entries dead for the wrong reason. THE N=8 CLAIM RACE. `mv src dst` wins unconditionally, so the rename-to-claim control reports N winners whether the claimants raced or ran one after another; `set -o noclobber` reports exactly one winner under both too. The two arms were therefore indistinguishable under a serialised harness, while the comment on the probe said a single winner there would mean the harness was not concurrent — it would not, and the harness comment's "`mv` would also lose the second time" is the same error (it wins every time, which the probe's own result shows). Nothing observed the race. `runShellAsync` now reports its wall-clock window and a named `overlapWindow` collector returns the interval during which every run was simultaneously alive; both arms assert it is positive before reading their own result, and the collector has its own probe over seeded overlapping and back-to-back windows. No claimant assertion is relaxed. THE DEGRADED-REASON PARSER'S BLIND SPOT. `collectDegradedReasons` bounds nesting at one level and requires balance, and both limits fail by matching NOTHING — so a reason with unbalanced or doubly-nested parentheses is invisible to the registry arm rather than reported by it, and the arm certifies a corpus it did not fully read (avoids PF-064). `collectUnregisteredReasons` now counts `DEGRADED (` sites against parses and reports the shortfall as its own violation. Verified over the shipped corpus first: 0 dropped today, so this adds a guard rather than a green-to-red change. Its probe drives both unreadable shapes through the collector and confirms a readable nested reason is not reported by the same check. Refs #350
…that outlived their subject
`resolveTrackerCliAction` was live on main and is not live here: `runTrackerSet`
replaced it and builds its own messages, so nothing but its own unit tests
imports it. Its JSDoc argued for keeping it as a testable resolver, and that is
the trap — the two strings it produces (`Tracker provider set to jira`,
`Tracker provider already jira`) reach no user, so four tests were pinning a
message contract with no consumer, and a correction made to either string would
have had no observable effect. Deleted with `TrackerCliActionResult` and its
tests; `TrackerCliActionMessage` stays, because `runTrackerSet` uses it. The
(current, requested) matrix it covered is covered by runTrackerSet's own arms,
against the messages users actually see (ADR-003).
Four references whose subject this branch moved or deleted:
installer.ts OverlayFailureState pointed at `formatOverlaySummary`
in init.ts; the renderers moved to install-report.ts,
and the sentence-per-arm rule it means is
describeOverlayFailureState's.
byte-budget.test.ts the jira ceiling's JSDoc named `BUDGET_LOADED_SET_P3`,
deleted when the GitHub row collapsed onto one number,
and restated that row's headroom as 107 ch — the
collapse re-measured it at 45, which the same file's
own JSDoc and numeric-floors.json both say. Two
documents in one file disagreeing about one number is
worse than neither stating it, so it now points at the
printed table instead of carrying a copy.
build-mds-compile-time the warm-up comment said it used "a module this file
does not time"; `_mcp.mds` is on the timed roster.
Warming on the cheapest roster module is correct — the
comment's reasoning was not.
core/tracker.ts a `/* */` block inserted between the JSDoc and
`trackerConventionsBackupName` detached that JSDoc
from the function, and spoke of "the two helpers
below" from above only one. Merged into the JSDoc,
naming the sibling it also covers.
No value, assertion, ceiling or floor moves.
Refs #350
…viders ship
The Code agent's per-provider paste arms are a FOURTH reader of each provider's
reference grammar, and the only one no guard covered. The project-key alphabet
already has an explicit one-authority claim over three readers — the Git
preamble, the Tracker agent's schema table and every `KEY-N` grammar in the
provider mechanics (tests/tracker/single-authority.test.ts) — and this gate was
outside its corpus, because it spells the whole ref grammar rather than the bare
key alphabet the collector there matches.
Being a fourth READER is correct and stays: the Code agent is outside the Git
spawn surface and loads no mechanics file it could defer to, so a sink check has
to enumerate the closed set once inside the gate (PF-023). Being a fourth
AUTHORITY is what nothing was stopping. The arms agree with the mechanics today,
byte for byte; what was missing is anything that would notice if a provider's
grammar were edited and this table were not, and the dangerous direction is
silent — a widened arm admits a link line the provider's own mechanics refuse,
at the one gate between an attacker-influenceable value and a GitHub-visible
sink.
`collectProviderRefGrammars` reads each provider's grammar out of the shipped
generated tree and requires it to be the single distinct `KEY-N` shape that
provider states; the arm is then asserted to be exactly that grammar with the
rendered verb spliced in after `^`. Nothing is re-typed here: a literal in this
file would be the fifth authority and the only one nobody ships (PF-018).
Linear's internal-id form is excluded by the collector's shape, which matches
the provider's own rule — an internal id carries no team, so the history grammar
admits the TEAM-KEY form only and a rendered PR link is always that form.
Two non-vacuity arms: the collector must find exactly one grammar per provider
(or the comparison is between two absences), and the two providers' grammars
must differ (or the mix-up the jira/linear overlap makes plausible could not be
reported). RED proved by widening the jira arm's key alphabet from `{1,9}` to
`{0,9}` in a copy of the prompt — reported with both strings named, then
reverted.
No prompt asset, golden or ceiling is touched.
Refs #350
Removes remaining references to tests/tracker/containment.test.ts, CONTAINMENT_EXEMPTIONS, tests/fixtures/tracker/baseline/, tests/guards/guard-census.test.ts, git-agent-guard-count, PRE_PHASE3_REASONS, BUDGET_LOADED_SET_P3, and resolveTrackerCliAction across the six affected KNOWLEDGE.md files and index.md, rewritten as end-state prose pointing at their live successors (single-authority.test.ts / reference-reachability.test.ts, reference-floor.ts, GITHUB_ONLY_REASONS, BUDGET_LOADED_SET, runTrackerSet). Completes commit ab612e0's claim, which missed these sites.
…nged (F-1, AC-23) overlayGeneratedReferences pushed every promoted unit's files into overlaidRefs on every call, so `moved` in `tracker --set` was true on every run and the `(unchanged)` branch was unreachable — the only test of it stubbed `overlaid: []`, a state the real overlay never produced. Compare each staged unit against the installed one between the build and the promotion, skip the promotion when they match, and report those files in a new `unchangedRefs`. `overlaidRefs` now names what a run WROTE, which is what `tracker --set <same provider>`'s `(unchanged)` line, formatOverlaySummary and init's `Tracker assets: +N` all read. The comparison is against what the promotion would DO, not the manifest alone: a provider unit is swapped whole, so an installed file the manifest no longer names makes the unit changed — converge-not-merge is not downgraded to a merge. File modes are deliberately out of scope; D-OVERLAY-MODE-SCOPE normalises the whole directory every run regardless. RED: `runTrackerSet` through the real `buildTrackerSetIO` printed `Tracker: github - 13 installed, 0 removed` on a repeat set; the overlay run twice into a mkdtemp target re-promoted every unit (new inodes). Refs #350
…d (F-2, M-1) The setup-task spawn forwarded the first $ARGUMENTS token as ISSUE_INPUT unless it ended in .md, so `/implement fix the login bug` reached the Git agent as `ISSUE_INPUT: fix` with TASK_DESCRIPTION omitted — and setup-task fetches whatever it is handed, with no shape gate of its own. The request was dropped and a branch derived from a failed lookup. Gate on shape instead: a single whitespace-delimited token that does not end in .md is ISSUE_INPUT, two or more tokens are TASK_DESCRIPTION, a .md path is PLAN_ARTIFACT_PATH. Token count keeps the provider neutrality the `#`-only gate was removed for — `#42`, `42`, `PROJ-12` and `ENG-7` are each one token, and no prose description is. The tests stop pinning the sentence and assert the routing each SHAPE produces. The line this command shipped becomes the known-bad probe for a new labelled collector rule: an unconditional first-token forward is perfectly provider-neutral and still loses the request. RED: all three new arms failed against the pre-change compiled command, the collector reporting `ISSUE_INPUT: {the first $ARGUMENTS token verbatim, ...} - an unconditional first-token forward`. Refs #350
…t (F-3, M-2, AC-3)
gather-release-evidence stated GitHub's stop rung inline — "Secondary
rate limit (403/429 or `X-RateLimit-Remaining` < 10)" — while jira and
linear both point at `### Provider signals ({Provider})` in their
backlink-shipped-issues reference. Two statements of one threshold, free
to drift; AC-3 asks for one.
Replace the restatement with the same pointer shape the other two
providers use. The outcome the operation owns (stop, report the
remainder as THROTTLED) stays where it is; only the threshold moves to
its authority.
The exact-string guard could not see this: it pinned the two spellings
the authority file uses, and the restatement used neither. Widen it to
the bare `X-RateLimit-Remaining` token and assert backlink-shipped-
issues.md is its only home, with a non-vacuity arm so a deleted rung
cannot read as a clean tree.
RED: the widened arm reported `github/gather-release-evidence.md`
alongside the authority.
Refs #350
…ture count (F-4, M-3) Three [Unreleased] statements this branch falsified: - the Git-agent bullet described a 4-rung first-hit-wins resolution with a 60% ref-grammar corroboration scan. Shipped: three rungs, the per-repo `tracker` key NARROWS the manifest rather than beating it, rungs 1 and 2 are read before anything is decided, and the issue grammar is not a rung at all. No corroboration scan exists. - the byte-budget bullet called the pre-split 77,824 a live ceiling. The loaded set is priced per provider now: 80,200 GitHub, 89,500 jira, 91,700 linear. - the fixture bullet said the golden "was re-captured twice". This branch spent the third authorisation. Also drops the same bullet's claim that github-status-lines.txt is byte-identical to the pre-change capture, which the fixture bullet three entries below already contradicted. Guard, so the next drift is caught rather than reviewed: CHANGELOG.md joins the retired-wording corpus, reduced to the [Unreleased] section's After-halves — a released section is a record, and a before/after bullet's title and before-half exist to NAME what was removed. The four phrases are registered as scoped entries. `close milestone` gets an explicit scope for the same reason: the changelog's end-state sentence for that fix has to name it. The scope known-bad probe now runs over EVERY scoped entry against a synthetic outside path, rather than the first entry found against `docs/` — a later entry whose scope named docs/ broke it. RED: the four literals reported against the pre-change CHANGELOG. Refs #350
…itle to its constant (F-7, F-5)
F-7 (M-6): CLAUDE.md stated one default, `Refs {KEY}-{n}`. That is Jira's
token; Linear's documented default is `Refs {REF}-{n}`. The shape is
stated once, provider-neutrally — the per-provider token is a provider
fact and lives in that provider's own mechanics, which is where both
`create-release` and `ensure-pr-ready` state it. Naming both here would
have cost 61 of the block's 42 remaining characters; the cap is not
raised to fit the sentence. Block measures 3,477 of 3,500.
F-5 (M-4): the preamble test's title said `<= 40 lines` while
PREAMBLE_MAX_LINES is 36. Interpolated, so the two cannot disagree again.
Refs #350
(a) AC-17 — backlink-shipped-issues had no executed gate evidence. New
tests/tracker/compliance-gate.test.ts asserts the matrix for github,
jira and linear: /release step 4b spawns the operation exactly once and
carries the compliance condition, step 2b gates the SHIPPED_ISSUES it
consumes on the same one, and no provider's mechanics file states a
compliance condition of its own. The ungated half is not a bare absence
— each provider file must still carry the entry gate and the
never-COMPLETE-over-zero rule it actually ships, so an empty file cannot
pass. RED shown by mutating dist: a stripped gate and a seeded
per-provider condition each failed their own arm.
(b) AC-12 — two payloads added to the EXECUTED paste matrix:
`pr-link: $(whoami)` (the value is pasted into a PR body a shell
composes) and a 74-character line whose only defect is length. RED
shown by claiming github accepts the substitution: the extracted arm
`^Closes #[1-9][0-9]{0,8}$` rejected it.
(c) AC-26 — the dry run's plan and the real removal each resolve
installedPlugins from the manifest independently, and nothing compared
them. One seeded manifest, one seeded skill tree, both paths run: what
the removal took must equal what the plan named, and what remains must
be the complement. RED shown by handing the real path DEVFLOW_PLUGINS:
it removed nothing while the plan named three skills.
(d) L2 — the init.ts predicate behind pluginListUnchanged was inline and
unexecuted while its renderer was covered exhaustively. Extracted as
isPluginListUnchanged in install-report.ts, beside the renderer that
consumes it, and unit-tested in both directions: a null prior manifest
is never unchanged (the empty-vs-empty case a set comparison alone gets
wrong), set equality ignores order and duplicates, and a fourth arm
feeds the predicate into the renderer so the two halves are joined.
Refs #350
QA S2: `init --tracker jira --recommended` run twice reported `Installed 24 generated skill reference(s)` and `Tracker assets: +24 reference(s), −0 reference(s), tracker agent installed` on EVERY run — reference mtimes advanced while sha256 stayed identical. Root cause: the full-install pre-clean `fs.rm`'d every directory in skillsMap before overlayGeneratedReferences ran, so stagedUnitIsAlreadyInstalled had no installed copy to compare against and every unit was force-promoted. `devflow tracker --set` never passes through the pre-clean, which is why its `(unchanged)` line was already correct. The pre-clean now empties the references-hosting skill directory around the paths the overlay owns — derived from installedReferenceManifest, never a hand-typed list — and empties everything else exactly as before, so a stale hand-authored skill file or a reference at the references ROOT (which the overlay may replace but never delete, D-OVERLAY-FLAT-UNIT) still does not survive. Converge-not-merge is unaffected: the overlay's own prune removes whatever under references/tracker/** the manifest no longer names. The sweep/pre-clean "deliberately opposite scoping" comment stays true; the new split is between the pre-clean and the overlay, stated at the pre-clean and in D-OVERLAY-OWNERSHIP. convergeTrackerArtifacts gets the same treatment: it byte-compares the source against the installed agent and reports `unchanged` when the copy would change nothing. Drift is still converged and still reported as written — a hand-edited agent is restored and reported `installed`. InstallReport carries `unchangedRefs` so "this run wrote nothing" is an assertable outcome rather than an absence indistinguishable from an install that never reached the overlay. Refs #350
…s it
Plan A3 and the Gate-1 decision: the Tracker agent stays declared in
`devflow-core-skills.agents` and is FILTERED AT INSTALL TIME, with
`convergeTrackerArtifacts` the one owner of whether
`{claudeDir}/agents/devflow/tracker.md` exists. The generic agent copy
loop in installer.ts wrote it too, so both owners acted on every install
and contradicted each other in both directions:
init --tracker github (fresh)
Tracker assets: +13 reference(s), −0 reference(s), tracker agent removed
init --tracker github (re-run)
Tracker assets: +0 reference(s), −0 reference(s), tracker agent removed
— a removal reported for a file only that same run had put there — while a
fresh `init --tracker jira` never said `tracker agent installed`, because
converge found the loop's byte-identical copy already in place.
The loop now skips the one agent it does not own, and the full-install
pre-clean empties `agents/devflow/` AROUND it instead of wiping the
directory whole. Both halves are needed and both read ONE authority, the
newly exported `TRACKER_AGENT_NAME` (D-TRACKER-AGENT-OWNER): the pre-clean
is the same split the reference tree needed in 772f052
(D-OVERLAY-OWNERSHIP), for the same reason — taking the file leaves
converge with nothing to byte-compare against, so a steady-state jira
re-init re-copies the agent and announces it every run.
Nothing about the registry changes: `tracker` stays declared, the roster
count stays 17, and the agent sweep still keys on getAllAgentNames(), so a
converged tracker.md is never an orphan. Uninstall is unaffected — it
removes `agents/devflow/` wholesale.
After the fix, live against a temp HOME:
github fresh Tracker assets: +13 reference(s), −0 reference(s)
github re-run (no Tracker assets line)
jira fresh Tracker assets: +24 reference(s), −0 reference(s), tracker agent installed
jira re-run (no Tracker assets line)
tracker --set github from jira ... tracker agent removed
The precedence guard now completes its tree through the agent's real
owner rather than dropping it from the population, so the one-authority
claim about `agentSourceDirs()` still covers all 17.
Refs #350
…55,750 base) Reviewer decision on PR #351: the phase-named ceiling pair is kept, so the two [Unreleased] lines that still cited the file measuring under 55,750 now state the gate as shipped.
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.
Summary
Half A — the selection-scoped install bundle. Devflow installed every skill from every plugin regardless of which plugins you picked, and overlaid all three tracker providers' generated mechanics onto every machine. A GitHub-only user carried 21 files and ~73 KB of Jira and Linear reference text they could never reach, and a Go project carried the React skill. Now each plugin hand-declares in a
requires:field the skills it uses but does not own, the install set is the closure ofskills ∪ requiresover the selected plugins (32 of 40 for the default plugin set), and the tracker bundle is{github} ∪ {selected provider}— 13 generated references forgithub, 24 forjira/linear, with the tool-call contractreferences/tracker/_mcp.mdand the Tracker agent file landing only for the MCP-backed providers. The build still compiles every provider, so the tarball is unchanged and switching providers needs no rebuild.Half B — tracker gap closure. The provider dimension had holes that only a second provider makes visible.
_mcp.mdis now a fixed per-spawn load under a non-githubprovider; the Git agent reads both the per-repo key and the manifest before deciding and splits the mismatch DEGRADED reason by cause; release evidence is gathered in the provider's own reference grammar with project-key equality instead of a bare#N; issue references render through{ISSUE_REF}; two connected tool servers are scoped per capability with ambiguity reported rather than guessed; Reference Rendering gets a rule, a default and an end to a permanent DEGRADED; the Tracker agent's claim is observable (CLAIMED/LOST) and its compose chain fails closed. Then the transition-era test scaffolding was deleted, the phase-named ceilings collapsed onto one number each, and the goldens re-captured once.What changed
Registry / installer / CLI
requires: readonly string[]on everyPluginDefinition;skillsOf,skillOwners,buildScopedSkillsMap,resolveSkillInstallPlan,PRESENCE_GATED_SKILLS,TEMPLATE_SKILL_REFSinsrc/core/plugins.ts.buildFullSkillsMapis now a thin wrapper.installedReferenceManifest({provider})insrc/core/mds-variants.ts— the INSTALL-side sibling ofgeneratedReferenceManifest(), with an injectablemodulesseam.src/targets/claude-code/tracker-install.ts:convergeTrackerArtifactsowns the scoped overlay and the Tracker agent file, in both directions.src/cli/commands/install-report.ts:SummaryLine,formatOverlaySummary,describeOverlayFailureState,formatTrackerAssetSummary,formatSkillScopeSummary— the first three moved out ofinit.ts.installViaFileCopyrequirestrackerProviderand takes optionaleffectivePlugins;InstallReportgainsremovedSkills,dormantShadowsandunchangedRefs.devflow tracker --setisrunTrackerSetover aTrackerSetIOseam with a fixed converge order;--statusgains aMechanics:line;devflow skills listshows each skill's owning plugin;devflow uninstall --pluginretains from the manifest, not the registry.Git agent + tracker references
_mcp.mdis a fixed per-spawn load under non-github, named on exactly one line.(repository override)and(conventions file).{ISSUE_REF}replaces the#-prefixed template slots; the template freeze is retired.Refs {KEY}-{n}default; the plan artifact posts as content, and over the cap posts none of it.src/assets/mds/tracker/_common.mdsholds the ref pre-flight heads and the shared provider lines.Tracker agent
agentPresent), not on whether this run happened to copy it — both write and removal go through one owner,applyTrackerSentinel.Command layer
implement.mdsandcode.mdtake a provider-neutral issue argument and PR-link line;github-api.mddefers to the D9 gate;/code-review's language focuses are presence-gated./implementroutes$ARGUMENTSby token count rather than by its first word, so a free-text task description is no longer mistaken for an issue reference.Build system
_jira.mds/_linear.mdsimport_mcp.mds/_common.mdsby alias rather than selectively, working around an@mdscript/mdsresolver cost that made those two modules' compile time scale with define count (npm run build:mds9,967 ms → 873 ms); a new compile-time budget guard pins the fix.Tests
requires-closure,install-shape,tracker-install,scoped-install-e2e,single-authority,reference-reachability,claude-md-tracker-section,build-mds-compile-time,compliance-gate.Docs
Behaviour changes users will notice
githubinstall carries 13 generated reference files;jira/linearcarry 24 plus_mcp.md. The Tracker agent file is installed only forjira/linear.proj-12now resolves where it previously degraded.devflow tracker --setexits 1 — leaving the manifest, sentinel and conventions untouched — whendevflow:gitis not installed or the reference overlay fails. It converges in both directions, so--set githubremoves whatjira/linearinstalled.devflow tracker --statusgains aMechanics:line with three outcomes:installed (N file(s)),MISSING — run devflow init,unreadable (<errno>).devflow skills listsays which plugin provides each skill and whether it is installed.devflow initre-run, ortracker --set <same provider>, now writes nothing. Previously every re-run re-promoted every reference file (mtimes advanced,Tracker assets: +N reference(s)printed every time) even though nothing had changed; each staged unit is now compared byte-for-byte against what's installed, unchanged units are skipped and reported inunchangedRefs, and theTracker assets:line — including the Tracker agent file itself — prints only when something actually moved./implementroutes its argument by token count, not by the first word. A single token (#42,42,PROJ-12, a.mdpath) is still an issue reference or plan artifact; two or more tokens are now the full task description — previously/implement fix the login bugforwarded onlyfixas the issue lookup and silently dropped the rest of the request.+,~,@,,,:) now suppresses only the tracker session-start directive, not Learning too. The two directives are gated on the paths each one actually interpolates rather than sharing one gate over both — the earlier shared gate silently disabled the entire learning pipeline on any such machine.Reviewer decisions (2026-09-22)
BUDGET_GIT_MD_P358,870 over the file,BUDGET_GIT_MD55,750 −PREAMBLE_CHARS_P2over the non-preamble remainder) stays as shipped. The twoCHANGELOG.mdlines now state that gate (0563a62). The downward collapse follows issue git.md: second size cut — move the 8 PR/review op bodies to references (SG-7) #326._mcp.mds, expanded into the four consuming operation files, absent from the emitted_mcp.md.Held for reviewer decision (resolved above)
1. The git.md ceiling collapse. The plan bounded the sign-off: "if measured git.md > 56,400 STOP and report."
dist/agents/git.mdBUDGET_GIT_MD_P3BUDGET_GIT_MDPREAMBLE_CHARS_P2Nothing is red. The conflict is with the collapse target, and a Code agent cannot pick between the three options:
BUDGET_GIT_MDat ≈58,160, plusBUDGET_GIT_MD_NON_PREAMBLEre-derived from the measured 51,076 (e.g. 51,140).BUDGET_GIT_MD_P3andPREAMBLE_CHARS_P2are then deleted. This raises the registeredbudget-git-mdceiling 55,750 → 58,160, a second authorised re-baseline beyond the one already granted for the loaded-set rows.*_P3/*_P2names in the end state the end-state sweep would otherwise have removed.git.mds, i.e. a new prompt-asset window.Consequence of holding, recorded:
PREAMBLE_CHARS_P2and the non-preamble arm were not deleted although the phase brief listed them, because they are the operands of the two-sided gate. Deleting them while the collapse is held would leavegit.mdgated only at the file level — strictly weaker with nothing replacing it.Also unresolved by the same hold:
CHANGELOG.mdlines ~35 and ~47 still citeBUDGET_GIT_MD55,750 / 55,664 characters. Whichever of the three options above is taken, those two lines need the matching correction.2. Reference Rendering rule placement. Gate 1 of the design review said the rule and its gate belong in
_mcp.md, with the default value in each provider file. What shipped authors the rule once —@define reference_rendering_gatein_mcp.mds— but expands it inline into the four consuming operation files; it appears zero times in the emitted_mcp.mditself. This kept_mcp.mdinside its own byte budget, and the define is registered as aSHARED_RULESsingle-author row, so there is exactly one place to edit the rule text and no drift is possible — but it is not the placement Gate 1 asked for. Needs an explicit accept, or a re-placement that would cost jira/linear loaded-set bytes (both already re-baselined once this branch).Deviations from the approved plan
converged, REMOVAL always attempted. What shipped (f405853) gates both write and removal on the same thing, whether a spawnable agent is actually on disk (agentPresent) — becauseconvergedanswers "did this run copy the file", not "is a working agent present", and reading it disabled a working jira install on a transient re-copy failure while leaving a stale sentinel behind on a jira→linear transition. Plan item M6 said thesession-start-contextpositive-shape control lands in P1 with its own tests; what shipped there (a1c993c) found and fixed a real regression in that control instead — it had gated the Learning directive on a value only the Tracker directive interpolates, silently disabling Learning on any home directory containing a space — and split it into one gate per directive-consumed value. Both fixes are proved against both transition directions (github↔jira↔linear for the sentinel; both directives for the hook gate), not only the one direction each original plan item named._common.mds, not_mcp.mds. Measured: compiling a provider module against_mcp.mdscosts 3.3 s at 9 defines, 8.3 s at 11, and does not finish at 12. The same defines in_common.mdscost 1.2 s. Design review L1's ownership rule is amended in both module headers rather than ignored.modeparameter. MDS@ifis block-structured and terminates the line; all fourteen call sites are fragments mid-list-item.TRACKER_GITHUB_OPSandtrackerConventionsBackupName/Pathwere kept, not deleted: the first already has a consumer, the second is pinned by tests and is a user-visible filename contract.requires-closure-token-floorwas registered although one phase row scopednumeric-floors.jsonto two entries — a floor living only in a test constant can be lowered silently. Value re-derived 37 → 44 from the widened corpus.tests/scoped-install-e2e.test.tslives intests/, nottests/integration/—vitest.config.tsexcludestests/integration/**from the default suite, so the planned path would have produced a proof nobody runs.--set's I/O was extracted intorunTrackerSet+TrackerSetIObecause the planned recording stub cannot be written against a Commander.action()body.GIT_AGENT_BYTESlives ingit-agent-golden.test.ts, not where the brief placed it, so that file joined the fixture-only commit.collectDegradedReasonswas fixed to balance parentheses while splitting the mismatch reason: the old alternation could never fire, returned a truncated reason and reported it as unregistered.Known follow-ups
§14.xphase-coordinate references remain in ~28 test files (previously stated as four — later commits on this branch added more test files carrying them than the original count captured) and 6src/JSDoc sites (src/core/feature-config.ts×4,src/core/mds-variants.ts,src/assets/scripts/redact-secrets.cjs). Argue per site, not per count.code.mdis a third authority for each provider's reference grammar. Accepted because the Code agent is outside the Git spawn surface and loads no mechanics file it could defer to; if a future change gives it a loadable reference, those arms move into it.@mdscript/mdsresolver still deep-copies a selective import's captured scope on every@definein the importing module. This branch worked around the one cliff it hit (_jira.mds/_linear.mdsimporting_mcp.mdsand_common.mds) with alias imports plus a compile-time budget guard, but the underlying resolver defect is unfixed — a future module with the same shape hits the same wall. Upstream fix belongs incollect_define, a per-moduleArc<CapturedScope>instead of a deep clone per define.X-RateLimit-Remainingis still restated, rather than pointed at its authority, at the skill/doctrine layer —devflow:git's ownSKILL.mdandreferences/github-api.md— outside the tracker-generated tree the single-authority guard covers. Pre-existing, not introduced by this branch;68f2ba3fixed only the one restatement insidegather-release-evidence.dedup unavailable — duplicate possibleis permanent on a stock workspace: devflow cannot ask which account it is, so a back-link posts with the warning rather than being withheld.git.mdcuts the bounded collapse needs.tests/tracker/schema-scope.test.tsis still unregistered innumeric-floors.json— it lives only in the test file itself, currently 21. A one-entry addition whenever someone wants it.Verification
npx vitest run --exclude tests/eager-memory-refresh.test.ts): 155 files / 5,518 tests, exit 0.npm run buildandnpx tsc --noEmit: clean.7bc70f1,463f4e0,80f33ab,8e8a0c4,772f052; the run for57a618cwas still in progress at time of writing.{ISSUE_REF}template sites (fixture 48/52/61/86/99), two release-evidence steps (165/166), twocode.mdPR-link lines (239/240). Line counts identical before and after (250 lines, 249 newlines), so there is no offset drift.npm run test:golden:updatewas never run: both fixtures are byte copies of an explicit--unfreeze --out-dir <scratch>output. The authorisation is spent; none is outstanding.BUDGET_LOADED_SET77,824 → 80,200 (measured 80,155);BUDGET_LOADED_SET_JIRA88,660 → 89,500 (89,459);BUDGET_LOADED_SET_LINEAR91,000 → 91,700 (91,642);PREAMBLE_MAX_LINES40 → 36 (35 lines);BUDGET_LOADED_SET_P3deleted;GIT_MD_CHARS58,818 → 58,100;GIT_AGENT_BYTES59,279 → 58,549;FIXTURE_BYTES17,527 → 18,270;GITHUB_API_MD_CHARS21,218 → 21,166.budget-git-mdandbudget-git-md-p3are untouched and green.claude-md-tracker-block-max-charsadded).Only a live
clauderun can answer these three — named, not scheduled: that ajiraspawn readsreferences/tracker/_mcp.mdexactly once and rendersRefs PROJ-1; that/code-reviewskips a language focus whose skill is absent; and that a real concurrent subagent claim race produces oneCLAIMEDand N−1LOST.Related Issues
Closes #350
Refs #321 #325 #343 #345 #346