Skip to content

refactor(build): compile the Git agent from an MDS generator host - #334

Open
dean0x wants to merge 15 commits into
mainfrom
feat/323-tracker-phase-1-mds-plumbing-provably-by
Open

refactor(build): compile the Git agent from an MDS generator host#334
dean0x wants to merge 15 commits into
mainfrom
feat/323-tracker-phase-1-mds-plumbing-provably-by

Conversation

@dean0x

@dean0x dean0x commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Zero user-visible change. dist/agents/git.md is byte-identical to the file it replaces, the 13 compiled command outputs are byte-unchanged, and the frozen github-status-lines.txt is untouched. The only observable difference is that npm run build:cli alone no longer produces installable agents — npm run build:mds is now required.

Problem Being Solved

Phase 2 must split git.md into a contract layer plus generated per-provider references. The build could not own an agent file at all:

  • no generator-host convention — every .mds host wrote to dist/commands/, and the strip logic assumed a single frontmatter block
  • dest validation compared raw strings, so dist/skills/../commands resolved onto an allowlisted target and was accepted
  • the emitted output filename was never validated before being joined onto the destination
  • dist/agents was unknown to the installer and to the per-agent model pipeline, so a compiled agent had nowhere to land

This 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, no process.exit: validateOutputName and resolveOutputDir. Deliberately no expandVariants (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.ts is 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 single expected slot, so every /typo\?/i matcher still hits.
  • stripGeneratorFrontmatter (whole-block, for output-dir: dist/agents) lands alongside the retained key-only stripOutputDirKey (for dist/commands), so the 13 command outputs do not move a byte. Both strips run after compileFile: 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 declare output-dir: and would otherwise be discovered by the whole-repo walk and compiled into the real dist/ (EC-50).
  • compiledAgentsDir() in src/core/assets.ts; the installer and loadShippedDefaults() resolve agents dist-first with a src fallback and fail loudly naming both paths.

Conversion (S2)

  • src/assets/agents/git.mdgit.mds (recorded by git as R079; the 21% is escape backslashes). 171 { and 171 } outside column-0 fences escaped as \{ / \}; 141 inside fences left raw. cmp exited 0 on the first build.

Tests and packaging (S3, this subtask)

  • Three corpus scans that had silently lost git are repointed and now name the expected roster instead of counting it.
  • Count literals → tests/fixtures/mds-manifest.ts, one definition consumed by four assertion sites plus the printed-count test.
  • The build's printed counts are asserted for the first time (AC-1.8) — 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/mds devDependency pin guard (Guard 3 reads dependencies only).
  • Tarball decision D-A(a) accepted: the .mds sources 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:cli alone no longer produces installable agents. npm run build runs 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

  1. The golden equality test IS the review. dist/agents/git.md must equal tests/fixtures/golden/git-agent.md byte-for-byte, and bytes === 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.

  2. 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 old git.md). Start with fences 3 and 4 — post-review-summary FULL and STUB — which carry the D7 dedup marker:

    # open → close opener owning operation
    3 L394 → L402 5-space ``` post-review-summary (FULL)
    4 L404 → L414 5-space ``` post-review-summary (STUB)
    1 L222 → L224 3-space ```bash setup-task
    2 L320 → L326 3-space ``` fetch-issues-batch
    5 L600 → L617 3-space ```markdown learn-conventions
    6 L772 → L778 5-space ``` post-resolution-summary
    7 L781 → L791 5-space ``` post-resolution-summary
    8 L869 → L872 3-space ``` backlink-shipped-issues
    9 L904 → L908 5-space ```markdown ensure-traceable-issue
    10 L951 → L954 3-space ```markdown post-wave-report

    The exact string cycle:{CYCLE_NUMBER} ts:{REVIEW_TIMESTAMP} occurs 3× in dist/agents/git.md and 3× in the golden. The degenerate cycle: ts: that a lost interpolation would produce occurs 0×.

  3. grep -c -F '\{' dist/agents/git.md = 0 (grep exits 1, no match). -F is required: in BRE, \{ is an interval operator. Proven non-vacuous against a seeded file first, which returned 2.

  4. 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 after

    Before is the pre-S1 recording, taken from the tree at main before the first commit of this branch. After is the current build of this branch. diff of the two lists is empty — all 14 lines (13 compiled + the hand-authored release.md) are byte-identical, so no compiled command output moved.

    12769f62287032f62398f5255d9925df7bf77fbd41d49926153cf81d83a017b1  dist/commands/bug-analysis.md
    ba20df110bfc818ec43e738e92ad7317c21961b08c23f47bc267b4c8781f207b  dist/commands/code-review.md
    a1c108e214abeafe1af59090a93d8af2c7cab6c61a96cdeb8b780a6b715a2a89  dist/commands/debug.md
    e4c7cb75a44b1b314b5a654a2344d0faa975d2801b6ac6c88e5b43ab912a6ab7  dist/commands/dynamic-build.md
    26fd83241eab77221b0517e022fc25267979ea645f8504eafc25a491cfd037dd  dist/commands/dynamic-plan.md
    2d5f649b4d7d382937c65afc26b18ceff4bd2fa2439ba026f1320564bdbe7d50  dist/commands/dynamic-profile.md
    d0b533422614c0c130a5a721d6bffe766a5456952b8d8ccd8bc740bc8cf5f345  dist/commands/dynamic-tickets.md
    28c90a06468a008d9b7f8b0b3729f62dfaadb61c61ebcc2190c860eab3d1562a  dist/commands/explore.md
    3b9d4db64b6242a223ebd9afbc92cc3a344df85c36756d8447a05fd87692a0fb  dist/commands/implement.md
    cf85c1f4ce4405af75c1ca3eef232a4c04ccfd9755ce565120673714d7c32bac  dist/commands/plan.md
    1eefa92b7027205bd5b46eb3a5e9cda93af1465c01233ce906d9a77ee20c0bc3  dist/commands/release.md
    771792d722a1931cbc1aaff074ea134d800d5c95910848a4f3139107b96a14f4  dist/commands/research.md
    c6e3aa6de707d9ed0f1a24dacc4e7acf82bc168ebe487bda783763e8b4a89476  dist/commands/resolve.md
    36df553aa2c5fbd48b577888e518124a546f8fabf4fa3867b0698e485a9deeda  dist/commands/self-review.md
    

    release.md is 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:269 re-runs the real build and asserts dist/commands/ is byte-idempotent.

  5. Tarball decision and Guard 5's reason string. src/assets/ already shipped wholesale, so the 24 command-side .mds files were already inside every published tarball; git.mds joins them. No files[] change. The count is now pinned at 25 and Guard 5's reason string names the generator sources.

  6. The AC-1.8 printed-count test. discoverHosts() cannot be imported (build-mds.ts is a tsx script outside tsc), 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:line points at the owning describe/it at this branch's head — the section markers the table previously carried are superseded by the line numbers.

Guard File Known-bad sample Proven red at
validateOutputName charset/bounds tests/mds-variants.test.ts:56 charset widened to /^[A-Za-z0-9][A-Za-z0-9._\/-]*$/ → 2 red pre-b5272d1
validateOutputName traversal/separator kinds tests/mds-variants.test.ts:77 module absent → suite red pre-b5272d1
resolveOutputDir two-entry allowlist tests/mds-variants.test.ts:123 allowlist shrunk to ['dist/commands'] → 3 red pre-b5272d1
resolveOutputDir canonical-declaration rule tests/mds-variants.test.ts:153 canonical check deleted → 3 red pre-b5272d1
Result error-union completeness tests/mds-variants.test.ts:194 phantom-kind injected into the expected set b5272d1 (in-test probe)
generator whole-block strip tests/build-mds-generator-hosts.test.ts:150 key-only-strip result ---\n---\n… asserted not.toBe pre-a2ed82f (12 red)
13 command outputs keep key-only strip tests/build-mds-generator-hosts.test.ts:250 real implement.md with the whole-block strip applied fails the same collector a2ed82f (in-test probe)
dist/commands rebuild byte-idempotence tests/build-mds-generator-hosts.test.ts:269 paired with the recorded pre-S1 SHAs a2ed82f
dest allowlist negatives (3) tests/build-mds-generator-hosts.test.ts:286 dist/wrong-dir, dist/commands/, dist/../.. pre-a2ed82f
filename negatives (2) tests/build-mds-generator-hosts.test.ts:330 name-template: ../x, name-template: a/b pre-a2ed82f
IGNORE_DIRS covers tests/ and coverage/ tests/build-mds-generator-hosts.test.ts:382 same fixture outside an ignored dir IS compiled pre-a2ed82f
compiledAgentsDir() shape tests/core-paths-assets.test.ts:91 accessor absent → 3 red pre-ef0b30f
installer dist-preferred order tests/installer-new.test.ts:711 src-first order installs the src file pre-ef0b30f
installer loud failure on both-absent tests/installer-new.test.ts:442 both dirs empty; asserts build:mds + both paths in message pre-ef0b30f
loadShippedDefaults ⊇ registry tests/agent-models.test.ts:1070 registry-completeness, never scanned > 0 ef0b30f
loadShippedDefaults compiled-over-source tests/agent-models.test.ts:1092 dropping the dist dir loses git; reversed order flips the winner pre-ef0b30f
git.md ≡ golden, byte-for-byte tests/goldens/git-agent-golden.test.ts:34 cmp is the oracle; grep known-positive returned 2 on a seeded file e768675
agent-corpus completeness (3 sites) tests/build.test.ts:175, tests/registry-integrity.test.ts:134, tests/core-paths-assets.test.ts:75 the pre-repoint .md-only filter, run over the same input, loses git f218c00 (mechanic 1 recorded in the commit body + in-test probes)
recursive partial collector + no-subdirectories tests/build-mds.test.ts:126 temp dir with nested/_buried.mds and a subdirectory 8b99dc1 (in-test probe)
printed host/partial counts (AC-1.8) tests/build-mds-generator-hosts.test.ts:455 a DEVFLOW_MDS_ROOT copy of the real tree, seeded with one extra host → 15 ≠ 14 8b99dc1 (in-test probe)
dist/agents ↔ src parity, both directions tests/guards/dist-agents.test.ts:96 temp tree with dist/agents/orphan.md and an uncompiled uncompiled.mds; absent dist/agents/ throws cc95210 (in-test probes)
no escaped braces in dist/agents/*.md tests/guards/dist-agents.test.ts:167 seeded cycle:\{CYCLE_NUMBER\}; clean {CYCLE_NUMBER} must NOT be flagged cc95210 (in-test probe)
no .md shadowing an .mds host tests/guards/dist-agents.test.ts:210 temp dir holding both x.md and x.mds cc95210 (in-test probe)
resolver origins (dist arm, src arm, loud-failure arm) tests/guards/dist-agents.test.ts:239 unbuilt temp tree with only the .mds → throws with a build hint cc95210 (in-test probe)
AC-1.2 Phase-2 constructs absent tests/guards/dist-agents.test.ts:312 each of the 8 tokens seeded; plus a scoping probe showing @import in a .ts is not flagged cc95210 (in-test probes)
AC-1.1 byte baseline + origin === 'dist' tests/goldens/git-agent-golden.test.ts:71 equality baseline, moves only with the fixture cc95210
@mdscript/mds exact devDep pin tests/packaging.test.ts:140 (Guard 3b) ^0.2.0, ~0.2.0, undefined fed to the same collector; exact spelling must still pass cf09961 (in-test probe)
tarball carries dist/agents/git.md tests/packaging.test.ts:470 (Guard 6) dist/agents/git.md moved aside → expected [] to deeply equal [ 'dist/agents/git.md' ] cf09961 (mechanic 1, recorded in the commit body)
tarball ships 25 .mds sources (D-A(a)) tests/packaging.test.ts:504 (Guard 6) count derived from the manifest; measured 25 cf09961
installed agent came from dist/agents/ tests/integration/clause-ii-file-residue.test.ts:304 byte-compares the file devflow init wrote against the tarball's compiled artifact cf09961
retired wording: no generated copies anywhere tests/guards/retired-wording.test.ts:185 guard run against the pre-sweep CLAUDE.md → red naming the file and the literal 596233d (mechanic 1, recorded in the commit body)

Prefix-shippability

$ npm run build
EXIT=0

$ npm test
 Test Files  117 passed (117)
      Tests  4216 passed (4216)
EXIT=0

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

$ npm pack --dry-run
package size: 989.2 kB
unpacked size: 3.2 MB
total files: 377
EXIT=0

$ git status --porcelain
(empty)

$ git diff main -- tests/fixtures/golden/
(empty)

$ grep -c -F '\{' dist/agents/git.md
0                     (grep exit 1 — no match)

$ shasum -a 256 dist/agents/git.md tests/fixtures/golden/git-agent.md
84078f9c443ab036fc885670e15982c63d5fd0339de90cd1500e14a07f0e0a53  dist/agents/git.md
84078f9c443ab036fc885670e15982c63d5fd0339de90cd1500e14a07f0e0a53  tests/fixtures/golden/git-agent.md

$ stat -f %z dist/agents/git.md
66180

$ npm run build:mds | grep -E 'partial|host'
  11 partial(s) skipped (no output-dir:)
  14 host(s) to compile:

live: loadShippedDefaults keys: 16 | registry: 16
      superset (loadShippedDefaults ⊇ getAllAgentNames): true
      defaults['git'] = haiku

resolveAgentSource('git').origin === 'dist' is asserted in tests/goldens/git-agent-golden.test.ts and, for every generator host, in tests/guards/dist-agents.test.ts.

Process-only checklist:

  • test-inventory table present, with a red proof per guard
  • CHANGELOG enumerates the user-visible changes with before/after
  • prefix-shippability command sequence run and pasted
  • release-blocker items named — none for Phase 1
  • clause (ii) manual five-command walk-through (/plan → /implement → /code-review → /resolve → /release) — requires a live model and an authenticated GitHub project; not run

Deviations

  • S1dist/../.. is caught by the retained escape guard and reports escapes 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\?/i binds to the two allowlist rejections only (dist/wrong-dir, dist/commands/); the other three negatives pin their own messages — dist/../.. pins escapes the repo root, and the two name-template negatives (../x, a/b) pin is not a valid output filename. A later phase should not re-derive /typo\?/i as the contract for all five.
  • S1name-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.
  • S2 — five test sites were repointed through resolveAgentSource in the conversion commit itself, purely to keep that commit green (R6); all five are listed in its commit body.
  • S3numeric-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 assertions that carried them became set-equalities and the floor moved onto the manifest's length. No floor lowered, no entry removed. dist-files-count is untouched — toBe(14) still occurs 3× in build-mds.test.ts.
  • S3SKILL_GIT_CHARS moves 9_204 → 9_205. Fixing src/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.
  • S3 — the AC-1.9 "installed agent came from dist" proof lives in clause-ii-file-residue.test.ts, not pack-install.test.ts. pack-install never runs devflow init and has no scratch HOME, so it can only prove the tarball carries the artifact; the init-running test is where the installed file can be compared.
  • S3tests/goldens/git-agent-golden.test.ts was dropped from the literal-agent-paths exclusion 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).
  • S3 — the retired-wording corpus widened to dist/agents/, docs/ and the root prose so the new Phase-1 entry has something to scan. .devflow/features/*/KNOWLEDGE.md is 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).

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tracker Phase 1 — MDS plumbing, provably byte-identical

1 participant