refactor(build): compile the Git agent from an MDS generator host - #334
Open
dean0x wants to merge 15 commits into
Open
refactor(build): compile the Git agent from an MDS generator host#334dean0x wants to merge 15 commits into
dean0x wants to merge 15 commits into
Conversation
Pure, Result-returning validation core for the MDS generator-host convention (applies ADR-013; avoids PF-014 - no process.exit, every fallible path returns Result). - validateOutputName: anchored, bounded charset (same shape as MODEL_NAME_RE), refusing traversal, separators, and metacharacters. - resolveOutputDir: containment via isContainedIn plus a resolved-path allowlist of dist/commands and dist/agents; a non-canonical spelling of an allowlisted target is refused so one target has one spelling. Both error unions are discriminated and complete: tests/mds-variants.test.ts proves every declared kind is reachable from a concrete hostile input. Refs #323
scripts/build-mds.ts becomes a shell over src/core/mds-variants.ts: the pure module decides, the script renders the message and owns every exit. - Dest safety is now a resolved-path allowlist (dist/commands, dist/agents) instead of raw equality against a single value. The pre-existing message template is preserved verbatim, rendering both entries. - The emitted filename is validated before it is joined onto the destination. name-template: supplies the name when present, so a traversal or nested value is refused rather than escaping the dest. - stripGeneratorFrontmatter removes a generator host's whole steering block after compilation, promoting its second block into place. Command hosts keep the key-only strip, so their bytes do not move. - IGNORE_DIRS gains tests and coverage: this change lands .mds fixtures under tests/, which the whole-repo walk would otherwise compile into the real dist/. The 13 dist/commands outputs are byte-identical before and after. Refs #323
Adds compiledAgentsDir() (dist/agents/) and makes the two readers of the agent corpus prefer it, so an agent compiled from an .mds generator host supersedes a hand-authored file of the same name. - installViaFileCopy resolves each declared agent through an ordered dir list (default [compiled, source]). Absent from BOTH still throws; the message keeps its existing text and adds the build:mds hint plus every location searched. Never a silent skip. - loadShippedDefaults merges the compiled dir over the source dir, so the live shipped default for a generated agent comes from its compiled frontmatter. An absent compiled dir contributes nothing. Both dir lists are injectable (default = the real accessors), so the preference order is proved against temp trees rather than build state. Behaviour is unchanged until dist/agents/ exists. Refs #323
…entical
Rename src/assets/agents/git.md -> git.mds and prepend a two-line generator
frontmatter block (output-dir: dist/agents). npm run build:mds now emits
dist/agents/git.md byte-identical to the frozen golden
tests/fixtures/golden/git-agent.md (cmp exit 0, 66180 bytes, sha256
84078f9c443ab036fc885670e15982c63d5fd0339de90cd1500e14a07f0e0a53).
The body is otherwise unchanged. The only edit is a scripted fence-state-machine
escape pass: MDS interpolates {...} everywhere except inside column-0
triple-backtick fences, so 171 opening and 171 closing braces outside such
fences are escaped as \{ / \} (which compile back to literal braces), while the
141 + 141 braces inside the 19 column-0 fences are left untouched. The 10
indented fences (20 delimiter lines) are treated as prose, since de-indenting
them would not be byte-preserving; that includes the two post-review-summary
FULL/STUB templates whose D7 dedup marker
cycle:{CYCLE_NUMBER} ts:{REVIEW_TIMESTAMP} survives interpolation intact
(3 occurrences in dist, 3 in the golden). No literal backslash-brace leaks into
dist.
Zero MDS directives are used: no @if, @import, @define, variants, name-template
or partials. The emitted filename derives from the host basename.
All 14 dist/commands/*.md SHA-256s are unchanged from the S1-recorded list.
Four tests repointed off the now-absent literal src/assets/agents/git.md,
through the existing resolveAgentSource helper only (no new helpers, no literal
dist paths, no assertion or threshold changes):
tests/guards/agent-source-resolver.test.ts copyFileSync from a literal src
path -> writeFileSync(resolveAgentSource(name).content)
tests/build-mds-generator-hosts.test.ts realAgentShape() reads via
resolveAgentSource('git')
tests/installer-new.test.ts writeAgentFixture() reads via
resolveAgentSource(AGENT)
tests/build.test.ts agent-exists check resolves the
path via resolveAgentSource(agent); orphan check strips /\.mds?$/ so a
generator host cannot slip past it
Refs #323
Three guards filtered src/assets/agents/ to *.md. After git.md became the
generator host git.mds, each kept scanning 15 of 16 agents and stayed green:
the corpus shrank, no assertion noticed. This is GAP-07 — the `scanned > 0`
family of vacuity, where the count is never checked against the roster.
Each site now names the expected set instead of counting, and carries a
known-bad probe that runs the pre-repoint filter over the same input and
shows it losing `git`:
tests/build.test.ts compliance-frontmatter guard now reads
every agent through resolveAllAgents() (dist-preferred, so a compiled
agent is scanned in its shipping form). Parsing moved into the named
collector collectFrontmatterSkills(), called by the guard and both probes.
tests/registry-integrity.test.ts orphan check gains the named collector
collectAgentSourceNames(), accepting .md and .mds; asserts the collected
names cover getAllAgentNames() before looking for orphans.
tests/core-paths-assets.test.ts agentsDir() coverage assertion replaces
"at least one .md file" (green at 15/16) with set-containment over the
registered agent roster.
RED proof for the completeness assertion (mechanic 1): with
collectAgentSourceNames reverted to the .md-only filter,
registry-integrity.test.ts fails 2 tests --
"expected [ 'code', 'design', 'diagnose', ...(12) ] to deeply equal
ArrayContaining{...}"
Restored, the file is green again. The inline probes carry the same proof
so it re-runs on every suite execution.
npm test: 116 files / 4191 tests passed.
Refs #323
Four sites asserted "how many?" — toHaveLength(13), toHaveLength(11), and
toBe(14) twice. A count stays green through a rename plus an addition in the
same commit, which is the drift these sites exist to catch. They now assert
"which?", in both directions, against one definition.
New: tests/fixtures/mds-manifest.ts — the 13 command hosts, the 11 partials,
the 1 generator host (git), release.md, and the derived 14-file dist set.
Bidirectional-registry model after src/core/compliance-compose.ts:20/:36/:50;
its enforcing tests are named in the manifest's own JSDoc.
Consumers:
tests/build-mds.test.ts host + partial + dist-output sets;
KNOWLEDGE_HOSTS / DYNAMIC_HOSTS / ALL_HOSTS / DIST_FILES are now aliases of
the manifest, so the ~40 existing usages are untouched
tests/packaging.test.ts Guard 6 tarball dist/commands set
tests/build-mds-generator-hosts.test.ts the build's printed counts (below)
Also in build-mds.test.ts: the flat readdir over _partials/ becomes the named
recursive collector collectMdsNames(), plus an explicit "no subdirectories"
assertion — a flat reader could not distinguish "none" from "present but
unread". Known-bad probe seeds a temp dir with nested/_buried.mds and a
subdirectory and shows both being detected.
AC-1.8 — the printed counts are asserted for the first time. `grep 'partial(s)
skipped' tests/` returned zero hits before this commit: the build printed
"11 partial(s) skipped" and "14 host(s) to compile:" into a log nothing read.
discoverHosts() cannot be imported (build-mds.ts is a tsx script outside tsc),
so the printed output is the seam. parsePrintedCounts() throws on a missing
line rather than parsing it as 0. Known-bad probe copies src/assets/{commands,
agents} into a DEVFLOW_MDS_ROOT temp tree, asserts the copy reproduces 14/11,
then seeds one extra host and asserts the printed count moves to 15.
numeric-floors.json: dist-host-count and partial-count are re-registered at the
SAME floors (13, 11) with the new spelling toBeGreaterThanOrEqual(N), because
the assertion that used to carry them is now a set-equality and the floor moved
onto the manifest's length. No floor lowered, no entry removed. dist-files-count
is unchanged: toBe(14) still occurs 3x in build-mds.test.ts.
Measured: build prints "11 partial(s) skipped (no output-dir:)" and
"14 host(s) to compile:" (13 command hosts + git).
npm test: 116 files / 4195 tests passed.
Refs #323
dist/agents/ became a shipping directory the moment git.mds started compiling
into it, and it had none of the properties dist/commands/ has had since Guard 4.
New: tests/guards/dist-agents.test.ts
(a) source <-> output parity, both directions, FAIL-LOUD. Guard 4's
`catch { return }` + `if (distFiles.length === 0) return` shape is
deliberately not copied: that guard verifies nothing on an unbuilt tree,
which is the tree where it would matter. requireCompiledAgents() throws
with a `npm run build:mds` hint instead. Both counts asserted > 0.
Known-bad: a temp tree with dist/agents/orphan.md and an uncompiled
uncompiled.mds; both directions fire. Second probe: an absent
dist/agents/ throws rather than skipping.
(b) no escaped braces in dist/agents/*.md. A MISSED escape is a compile
error; a DOUBLED escape is silent — `\{` reaches the artifact and every
{PLACEHOLDER} contract at that site becomes dead text (PF-024, seventh
instance). Known-bad seeds `cycle:\{CYCLE_NUMBER\}` and also asserts a
clean `{CYCLE_NUMBER}` is NOT flagged, so the collector is not a blanket
fail.
(c) no hand-authored .md shadowing an .mds host — two sources for one agent
means the dist-preferred resolver picks a winner and the loser rots.
Known-bad: a temp dir holding both x.md and x.mds.
AC-1.6 / AC-1.3 origins against the REAL tree: every generator host resolves
with origin 'dist' (the dist-preferred branch had no live consumer before
this phase); every agent WITHOUT a generator host still resolves with
origin 'src', which is the fallback arm `git` can no longer prove since
its .md source is gone; and a generated agent in an unbuilt temp tree
throws with a build hint (the loud-failure arm).
AC-1.2 pins what Phase 1 did NOT build, over the union of the .mds host(s),
src/core/mds-variants.ts and scripts/build-mds.ts: no @if, no `variants:`,
no expandVariants, no `(module, op)` iteration, no `tracker-`, no
`{provider}.md`, and no @import/@define in the .mds. Two probes: one
seeds each forbidden token and confirms detection; one confirms the
mds-only scoping is real by showing @import in a .ts file is not flagged.
tests/goldens/git-agent-golden.test.ts gains the AC-1.1 equality baseline
GIT_AGENT_BYTES = 66_180 (derived once from `stat -f %z` on the fixture, cited
at the constant; an equality baseline like GIT_MD_LINES/GIT_MD_CHARS, therefore
NOT registered in numeric-floors.json where a floor would let the artifact
grow) and an explicit origin === 'dist' assertion, so the byte-equality above
is known to be measuring the compiled artifact.
Its stale description naming a src path is rewritten to name what the resolver
actually reads, so the file no longer needs its literal-agent-paths exclusion;
the entry and its justification paragraph are removed rather than left as
residue (ADR-003).
Verified: dist/agents/git.md and the golden are both 66180 bytes;
`grep -c -F '\{' dist/agents/git.md` = 0 (exit 1, no match).
npm test: 117 files / 4210 tests passed.
Refs #323
Guard 3b (new) — @mdscript/mds pin. Guard 3 reads `dependencies` only, so it
could say nothing about the compiler that turns src/assets/agents/git.mds into
dist/agents/git.md. A caret range would let an npm install change interpolation,
escaping or blank-line handling, and the golden fixture would go red with
nothing in the diff to explain it. Three assertions plus a probe:
- devDependencies["@mdscript/mds"] === "0.2.0", no ^ or ~
- the package is ABSENT from dependencies (moving it there would ship a
compiler to every install and step outside the pin above)
- package-lock resolves 0.2.0, carries a sha512 integrity field, and agrees
it is dev-only
- known-bad probe feeds ^0.2.0, ~0.2.0 and undefined to the same named
collector, and confirms the exact spelling still passes (not a blanket fail)
Tarball decision D-A(a), accepted at Gate 2: the .mds generator sources ship.
No files[] change was needed — src/assets/ already ships wholesale, so the 13
command hosts and 11 partials were already inside every tarball and git.mds
simply joins them. That was an accident of a broad glob; it is now a pinned
count of 25 (13 + 11 + 1), derived from the manifest, with the generator host
named explicitly. Guard 5's src/assets/ reason string names the MDS generator
sources instead of stopping at "hook scripts".
AC-1.9 — the compiled agent is pinned end to end for the first time:
tests/packaging.test.ts the tarball carries dist/agents/git.md for
every generator host, with frontmatter intact, no leaked output-dir:, and
no leaked \{ (PF-024)
tests/integration/pack-install.test.ts the installed package holds a source
for every registered agent (the old spot-check of code.md/review.md stayed
green while git stopped shipping), and carries the compiled artifact
tests/integration/clause-ii-file-residue.test.ts the file `devflow init`
writes to ~/.claude/agents/devflow/git.md is byte-identical to the tarball's
dist/agents/git.md — the dist-preferred installer path observed end to end,
which no test had done before
vitest.integration.config.ts excludes subagent-skill-preload.test.ts. It was
never actually excluded: the config's only filter was `include`, and the file
was skipped by naming the other five on the command line. It spawns real
`claude` against the developer's own ~/.claude with --dangerously-skip-
permissions and has historically committed to this repo mid-run. Still runnable
by explicit path.
RED proof (mechanic 1) for the tarball compiled-agent guard: with
dist/agents/git.md moved aside, packaging.test.ts fails 1 test --
"expected [] to deeply equal [ 'dist/agents/git.md' ]"
Restored; dist/agents/git.md is back at sha256 84078f9c443ab036…
Measured: 25 src/assets/**/*.mds entries in npm pack --dry-run.
npm test: 117 files / 4216 tests passed.
npm run test:integration: 5 files / 50 tests passed (subagent-skill-preload
now excluded rather than omitted by hand).
Refs #323
Swept the branch's final tree by grep for the artifact names rather than by
memory (PF-025), and wrote the end state rather than a note about the change
(ADR-003).
CLAUDE.md
- Build System: "no generated copies anywhere in the repo" was falsified the
moment dist/agents/git.md existed. Restated as the rule that is actually
true and actually load-bearing: **generated files never live in `src/`**
(GAP-53). The agents bullet now distinguishes hand-authored .md from a .mds
generator host and names the dist-first resolution.
- Build commands: `npm run build:cli` is marked as NOT producing installable
agents; build:mds is described by both destinations. The "13 hosts + 11
partials" wording is replaced by a pointer to the name manifest, since the
counts are no longer what the tests assert.
- Architecture overview, install paths, development loop, and the agent
authoring rule all name the generator-host form.
tests/guards/retired-wording.test.ts
- Denylist gains "no generated copies anywhere" (phase 1, removed from
CLAUDE.md). The denylist grows; it is never emptied and no new grep was
added (GAP-32).
- The corpus widens to reach it: dist/agents/, docs/, and the root prose
(CLAUDE.md, README.md, CONTRIBUTING.md). Widening the corpus is the correct
response to text that moved; loosening the denylist is not (R2). A new
assertion pins CLAUDE.md's presence in the corpus, so the doc half cannot go
unchecked while src/assets keeps the corpus non-empty.
- .devflow/features/*/KNOWLEDGE.md is deliberately NOT in the corpus: those
files record what each literal was and why it was retired, and a residue
grep must not demand that provenance be deleted (PF-040).
RED proof (mechanic 1): with the denylist entry and the widened corpus in
place but CLAUDE.md not yet edited, the guard failed --
'CLAUDE.md: contains retired literal "no generated copies anywhere"
(phase 1; removed from CLAUDE.md)'
Green after the CLAUDE.md restatement.
docs/reference/platform-assumptions.md — records the Node-22-only CI assumption
(GAP-57): ci.yml runs `node-version: [22]` while engines.node admits >=22.0.0,
so anything that behaves differently on Node 23+ passes CI and fails on a
user's machine. Follows the file's date-stamp + drift-symptom format.
docs/reference/file-organization.md, CONTRIBUTING.md — the agents tree, the
install-path table, and the build-command list name the generator host and
its compiled destination.
Four knowledge bases and .devflow/features/index.md repoint the literal
src/assets/agents/git.md at src/assets/agents/git.mds (source) or
dist/agents/git.md (artifact), as each site means. test-harness/KNOWLEDGE.md
additionally corrects three claims Phase 1 falsified: the Phase-0 "all agents
resolve from src" note, the DIST_FILES/ALL_HOSTS table (now aliases of the
name manifest, and the build discovers 14 hosts in total), and the
subagent-skill-preload exclusion, which is now real config rather than a
command-line convention.
src/assets/skills/git/SKILL.md — its cross-reference pointed at a file that no
longer exists. Fixing it moves the file by one character, so SKILL_GIT_CHARS
goes 9_204 -> 9_205 in tests/goldens/github-status-lines.test.ts. That is an
equality baseline, not a floor, and it moves in the SAME commit as the file it
measures, with the reason recorded at the constant. SKILL_GIT_LINES is
unchanged at 283.
CHANGELOG.md [Unreleased] gains a Changed section: the compiled Git agent
(byte-identical, zero user-visible change), the build:cli caveat, and the
integration-config exclusion. The "before" claims were verified against
`git show main:...` -- main has src/assets/agents/git.md and no git.mds, and
main's build:cli is a bare `tsc`.
Verified: `git diff main -- tests/fixtures/golden/` is empty (AC-1.11).
npm run build EXIT=0; npm test: 117 files / 4216 tests passed.
Refs #323
AC-1.10 sweep tail. Four sites named a path that no longer exists, in prose and
in assertion messages — a reader following any of them lands nowhere:
tests/git-agent.test.ts file docblock said the guards "read the source
file directly"; they read through resolveAgentSource, which is dist-preferred.
The non-vacuity message now interpolates the resolved path instead of naming
a deleted one.
tests/registry-integrity.test.ts Guard 6's failure message told the reader to
fix a file that is gone; it now names the generator host.
tests/goldens/github-status-lines.test.ts the measurement docblock labelled
the golden as equal to the src path (it equals dist/agents/git.md) and still
carried the pre-Phase-1 SKILL.md figures.
Every remaining occurrence of the literal is deliberate: the CHANGELOG "before"
clause, retired-wording's removedFrom metadata, literal-agent-paths' own
known-bad probe corpus, agent-source-resolver's temp-root comment (accurate for
the tree it builds), update-golden's src-fallback branch label, and the
gitignored design artifacts under .devflow/docs/. Provenance is not residue
(PF-040).
npm test: 117 files / 4216 tests passed.
Refs #323
- tests/mds-variants.test.ts: replace the hand-maintained REAL_BASENAMES literal (the 13 command hosts + git, duplicating tests/fixtures/mds-manifest.ts) with an import of ALL_MDS_HOSTS from the shared manifest, so there is one definition of "every basename the build owns" instead of two that could silently drift apart. Reviewed src/core/mds-variants.ts, scripts/build-mds.ts, src/core/assets.ts, src/core/agent-models.ts (loadShippedDefaults), the installer's agent install loop, vitest.integration.config.ts, and the full test/guard suite for Phase 1 (tests/mds-variants.test.ts, tests/build-mds-generator-hosts.test.ts, tests/guards/dist-agents.test.ts, tests/fixtures/mds-manifest.ts, and related golden/registry/packaging tests). No other slop found: no debug remnants, no redundant type checks, no over-defensive handling, no unused imports, no tombstone comments, and every guard already follows the named-collector + known-bad-probe shape. Left the guard structures, mds-variants.ts's exact export surface, and all pinned error text/numeric floors untouched per the Phase 1 constraints.
… CHANGELOG claim
The Phase-1 exclusion of subagent-skill-preload.test.ts shipped with a
documented recovery command that does not work:
$ npx vitest run --config vitest.integration.config.ts \
tests/integration/subagent-skill-preload.test.ts
No test files found, exiting with code 1
`exclude` is applied at glob time and a CLI positional only filters the
already-globbed set, so naming the file cannot bring it back. `--include` is
not a vitest option (CACError: Unknown option `--include`) and `--exclude`
appends rather than replaces. vitest.config.ts excludes tests/integration/**
as well, so after the exclusion the file was reachable through no invocation
at all -- an artifact with no consumer (ADR-003 clause iii), behind a comment
asserting the opposite (PF-025, docs are an execution surface).
The exclusion itself is correct and stays: the test drives live `claude`
sessions against the developer's own ~/.claude with
--dangerously-skip-permissions and has committed to this repo mid-run. It is
now gated on DEVFLOW_INTEGRATION_ALL so the documented opt-in is a real one.
The default sweep is byte-for-byte unchanged, so CI is unaffected.
The CHANGELOG entry carried a second, falsifiable "before" claim: that the
file "was kept out of it by naming the other files on the command line". No
such command line exists on main -- `test:integration` is `vitest run
--config vitest.integration.config.ts` with no file arguments, and ci.yml
passes none. The real pre-branch mechanism was the test's own
describe.skipIf(!isClaudeAvailable()) guard, identical on main and HEAD.
Restated to what `git show main:` actually shows.
Verification:
npx vitest list --filesOnly --config vitest.integration.config.ts
-> 5 files, subagent-skill-preload absent (unchanged)
DEVFLOW_INTEGRATION_ALL=1 npx vitest list --filesOnly \
--config vitest.integration.config.ts \
tests/integration/subagent-skill-preload.test.ts
-> tests/integration/subagent-skill-preload.test.ts (was: exit 1)
npm run build exit 0
cmp dist/agents/git.md tests/fixtures/golden/git-agent.md exit 0 (66180 B)
npx tsc --noEmit exit 0
npm test exit 0 (117 files / 4216 tests)
npm run test:integration exit 0 (5 files / 50 tests)
Three feature knowledge bases and two prose files still described the pre-branch tree, where every agent was a hand-authored .md and the build owned command files only. - installer-shadowing: accessor table gains a compiledAgentsDir() row and an amended agentsDir() description; the Hard-Error Policy row for agents states the real dist-first-then-src resolution and the throw that names both candidates plus the `npm run build:mds` hint. - dynamic-workflow-engine: the build compiles 14 hosts — 13 command hosts (ALL_HOSTS = 13, the test constant) plus the git.mds generator host — and DIST_FILES = 14 counts dist/commands/ only; names the shared tests/fixtures/mds-manifest.ts. - feature-knowledge-system: discovery is by output-dir: key over the src/assets/ walk (IGNORE_DIRS now skips tests and coverage), yielding the 13 command hosts plus the generator host; records the two-entry destination allowlist, the name-template override, and the atomic write. - CHANGELOG: 13 compiled command outputs are byte-unchanged; release.md is the hand-authored 14th deployed file. - file-organization.md: agents line annotated for MDS generator hosts. - packaging.test.ts: Guard 5 docstring realigned with its reason string. - build-mds-generator-hosts.test.ts: document why runRealBuild writes into the real dist/ and why that is safe under parallel workers. Refs #323
… in feature-knowledge-system KB
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.
Zero user-visible change.
dist/agents/git.mdis byte-identical to the file it replaces, the 13 compiled command outputs are byte-unchanged, and the frozengithub-status-lines.txtis untouched. The only observable difference is thatnpm run build:clialone no longer produces installable agents —npm run build:mdsis now required.Problem Being Solved
Phase 2 must split
git.mdinto a contract layer plus generated per-provider references. The build could not own an agent file at all:.mdshost wrote todist/commands/, and the strip logic assumed a single frontmatter blockdist/skills/../commandsresolved onto an allowlisted target and was accepteddist/agentswas unknown to the installer and to the per-agent model pipeline, so a compiled agent had nowhere to landThis PR is the plumbing for that split, and nothing else. Phase 2 constructs are pinned absent (AC-1.2) so their arrival is a reviewed change rather than something that accreted here.
Key Changes
Core (S1)
src/core/mds-variants.ts— new, pure, zero I/O, noprocess.exit:validateOutputNameandresolveOutputDir. Deliberately noexpandVariants(DR-16). Three refusal kinds, evaluated in order:escapes-root(resolved-path containment),non-canonical(dist/commands/,./dist/agents,dist/skills/../commands),not-allowlisted(compared on resolved absolute paths, not raw strings).scripts/build-mds.tsis now a shell over it — it owns every message and every exit. The pre-existing— typo?template is byte-preserved with the allowlist rendered into its singleexpectedslot, so every/typo\?/imatcher still hits.stripGeneratorFrontmatter(whole-block, foroutput-dir: dist/agents) lands alongside the retained key-onlystripOutputDirKey(fordist/commands), so the 13 command outputs do not move a byte. Both strips run aftercompileFile: only a block at byte offset 0 is emitted verbatim, so block 2 survives as body text and its trailing blank line is preserved.IGNORE_DIRS += tests, coverage— the build's own test fixtures declareoutput-dir:and would otherwise be discovered by the whole-repo walk and compiled into the realdist/(EC-50).compiledAgentsDir()insrc/core/assets.ts; the installer andloadShippedDefaults()resolve agents dist-first with a src fallback and fail loudly naming both paths.Conversion (S2)
src/assets/agents/git.md→git.mds(recorded by git asR079; the 21% is escape backslashes). 171{and 171}outside column-0 fences escaped as\{/\}; 141 inside fences left raw.cmpexited 0 on the first build.Tests and packaging (S3, this subtask)
gitare repointed and now name the expected roster instead of counting it.tests/fixtures/mds-manifest.ts, one definition consumed by four assertion sites plus the printed-count test.grep 'partial(s) skipped' tests/returned zero hits before this PR.tests/guards/dist-agents.test.ts— parity, escaped braces, no-.md-shadowing-an-.mds, resolver origins, and the AC-1.2 absence guard.@mdscript/mdsdevDependency pin guard (Guard 3 readsdependenciesonly)..mdssources ship, and the count is now pinned deliberately at 25 rather than being an accident of a broad glob.Breaking Changes
None user-visible. One contributor-visible change:
npm run build:clialone no longer produces installable agents.npm run buildruns both steps and is unchanged. The packaging and pack-install guards fail loudly if the compiled agent is missing from the tarball.Reviewer Focus Areas
The golden equality test IS the review.
dist/agents/git.mdmust equaltests/fixtures/golden/git-agent.mdbyte-for-byte, andbytes === 66180. A mismatch means the source is wrong, never the fixture (H2). The golden was never regenerated:git diff main -- tests/fixtures/golden/is empty.The 10 indented fences. All are inside numbered lists; de-indenting would not be byte-preserving, so every one is treated as prose and has its braces escaped. Line numbers are in
git.mds(+3 vs the oldgit.md). Start with fences 3 and 4 —post-review-summaryFULL and STUB — which carry the D7 dedup marker:post-review-summary(FULL)post-review-summary(STUB)setup-taskfetch-issues-batchlearn-conventionspost-resolution-summarypost-resolution-summarybacklink-shipped-issuesensure-traceable-issuepost-wave-reportThe exact string
cycle:{CYCLE_NUMBER} ts:{REVIEW_TIMESTAMP}occurs 3× indist/agents/git.mdand 3× in the golden. The degeneratecycle: ts:that a lost interpolation would produce occurs 0×.grep -c -F '\{' dist/agents/git.md= 0 (grepexits 1, no match).-Fis required: in BRE,\{is an interval operator. Proven non-vacuous against a seeded file first, which returned 2.The 13 command SHA-256s are unchanged from the pre-S1 recording, verified after a full
rm -rf dist && npm run build.AC-1.5 evidence —
shasum -a 256 dist/commands/*.md, before vs afterBefore is the pre-S1 recording, taken from the tree at
mainbefore the first commit of this branch. After is the current build of this branch.diffof the two lists is empty — all 14 lines (13 compiled + the hand-authoredrelease.md) are byte-identical, so no compiled command output moved.release.mdis in the list because it is a deployed command file, but it is hand-authored and copied verbatim — only the other 13 are MDS-compiled outputs. The equality is also pinned in CI, not only here:tests/build-mds-generator-hosts.test.ts:269re-runs the real build and assertsdist/commands/is byte-idempotent.Tarball decision and Guard 5's reason string.
src/assets/already shipped wholesale, so the 24 command-side.mdsfiles were already inside every published tarball;git.mdsjoins them. Nofiles[]change. The count is now pinned at 25 and Guard 5's reason string names the generator sources.The AC-1.8 printed-count test.
discoverHosts()cannot be imported (build-mds.tsis a tsx script outsidetsc), so the printed output is the seam — which is also the seam a human reads.parsePrintedCounts()throws on a missing line rather than parsing it as 0.Test inventory
Every guard, its known-bad sample, and the commit at which it was proven red. Each
file:linepoints at the owningdescribe/itat this branch's head — the section markers the table previously carried are superseded by the line numbers.validateOutputNamecharset/boundstests/mds-variants.test.ts:56/^[A-Za-z0-9][A-Za-z0-9._\/-]*$/→ 2 redb5272d1validateOutputNametraversal/separator kindstests/mds-variants.test.ts:77b5272d1resolveOutputDirtwo-entry allowlisttests/mds-variants.test.ts:123['dist/commands']→ 3 redb5272d1resolveOutputDircanonical-declaration ruletests/mds-variants.test.ts:153b5272d1Resulterror-union completenesstests/mds-variants.test.ts:194phantom-kindinjected into the expected setb5272d1(in-test probe)tests/build-mds-generator-hosts.test.ts:150---\n---\n…assertednot.toBea2ed82f(12 red)tests/build-mds-generator-hosts.test.ts:250implement.mdwith the whole-block strip applied fails the same collectora2ed82f(in-test probe)tests/build-mds-generator-hosts.test.ts:269a2ed82ftests/build-mds-generator-hosts.test.ts:286dist/wrong-dir,dist/commands/,dist/../..a2ed82ftests/build-mds-generator-hosts.test.ts:330name-template: ../x,name-template: a/ba2ed82fIGNORE_DIRScovers tests/ and coverage/tests/build-mds-generator-hosts.test.ts:382a2ed82fcompiledAgentsDir()shapetests/core-paths-assets.test.ts:91ef0b30ftests/installer-new.test.ts:711ef0b30ftests/installer-new.test.ts:442build:mds+ both paths in messageef0b30floadShippedDefaults⊇ registrytests/agent-models.test.ts:1070scanned > 0ef0b30floadShippedDefaultscompiled-over-sourcetests/agent-models.test.ts:1092git; reversed order flips the winneref0b30ftests/goldens/git-agent-golden.test.ts:34cmpis the oracle; grep known-positive returned 2 on a seeded filee768675tests/build.test.ts:175,tests/registry-integrity.test.ts:134,tests/core-paths-assets.test.ts:75.md-only filter, run over the same input, losesgitf218c00(mechanic 1 recorded in the commit body + in-test probes)tests/build-mds.test.ts:126nested/_buried.mdsand a subdirectory8b99dc1(in-test probe)tests/build-mds-generator-hosts.test.ts:455DEVFLOW_MDS_ROOTcopy of the real tree, seeded with one extra host → 15 ≠ 148b99dc1(in-test probe)tests/guards/dist-agents.test.ts:96dist/agents/orphan.mdand an uncompileduncompiled.mds; absentdist/agents/throwscc95210(in-test probes)dist/agents/*.mdtests/guards/dist-agents.test.ts:167cycle:\{CYCLE_NUMBER\}; clean{CYCLE_NUMBER}must NOT be flaggedcc95210(in-test probe).mdshadowing an.mdshosttests/guards/dist-agents.test.ts:210x.mdandx.mdscc95210(in-test probe)tests/guards/dist-agents.test.ts:239.mds→ throws with a build hintcc95210(in-test probe)tests/guards/dist-agents.test.ts:312@importin a.tsis not flaggedcc95210(in-test probes)origin === 'dist'tests/goldens/git-agent-golden.test.ts:71cc95210@mdscript/mdsexact devDep pintests/packaging.test.ts:140(Guard 3b)^0.2.0,~0.2.0,undefinedfed to the same collector; exact spelling must still passcf09961(in-test probe)dist/agents/git.mdtests/packaging.test.ts:470(Guard 6)dist/agents/git.mdmoved aside →expected [] to deeply equal [ 'dist/agents/git.md' ]cf09961(mechanic 1, recorded in the commit body).mdssources (D-A(a))tests/packaging.test.ts:504(Guard 6)cf09961dist/agents/tests/integration/clause-ii-file-residue.test.ts:304devflow initwrote against the tarball's compiled artifactcf09961no generated copies anywheretests/guards/retired-wording.test.ts:185CLAUDE.md→ red naming the file and the literal596233d(mechanic 1, recorded in the commit body)Prefix-shippability
resolveAgentSource('git').origin === 'dist'is asserted intests/goldens/git-agent-golden.test.tsand, for every generator host, intests/guards/dist-agents.test.ts.Process-only checklist:
/plan → /implement → /code-review → /resolve → /release) — requires a live model and an authenticated GitHub project; not runDeviations
dist/../..is caught by the retained escape guard and reportsescapes the repo root, not— typo?. The two messages are distinct by design: an escaping path throws and fails the build, a mis-spelled one exits 1 with the typo hint. The AC-1.7 pinning test is amended accordingly:/typo\?/ibinds to the two allowlist rejections only (dist/wrong-dir,dist/commands/); the other three negatives pin their own messages —dist/../..pinsescapes the repo root, and the twoname-templatenegatives (../x,a/b) pinis not a valid output filename. A later phase should not re-derive/typo\?/ias the contract for all five.name-template:is read and validated but no shipped host declares it. Its consumer is real (it determines the emitted filename when present) and the AC-1.7 negatives require the key to be read, but in Phase 1's end state it is exercised only by tests. This is the one place the phase sits closest to the clause-(iii) line, flagged rather than buried.resolveAgentSourcein the conversion commit itself, purely to keep that commit green (R6); all five are listed in its commit body.numeric-floors.json:dist-host-countandpartial-countare re-registered at the same floors (13, 11) with the new spellingtoBeGreaterThanOrEqual(N), because the assertions that carried them became set-equalities and the floor moved onto the manifest's length. No floor lowered, no entry removed.dist-files-countis untouched —toBe(14)still occurs 3× inbuild-mds.test.ts.SKILL_GIT_CHARSmoves 9_204 → 9_205. Fixingsrc/assets/skills/git/SKILL.md's cross-reference to a deleted file costs exactly one character. It is an equality baseline, not a floor, and it moves in the same commit as the file it measures.clause-ii-file-residue.test.ts, notpack-install.test.ts.pack-installnever runsdevflow initand has no scratchHOME, so it can only prove the tarball carries the artifact; the init-running test is where the installed file can be compared.tests/goldens/git-agent-golden.test.tswas dropped from theliteral-agent-pathsexclusion list. Its description named a deleted path; rewriting it removed the last literal, so the exclusion entry and its justification paragraph went with it rather than being left as residue (ADR-003).dist/agents/,docs/and the root prose so the new Phase-1 entry has something to scan..devflow/features/*/KNOWLEDGE.mdis deliberately excluded: those files record what each literal was and why it was retired, and a residue grep must not demand deleting provenance (PF-040).Related Issues
Closes #323
Tracking issue #321. Depends on #322 (merged via #327).