refactor(build): compile the Git agent from an MDS generator host - #334
Merged
Merged
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
Owner
Author
Code Review — Cycle 1Full summary withheld (public repository).
Full report: /Users/dean/Sandbox/devflow/.devflow/docs/reviews/feat-323-tracker-phase-1-mds-plumbing-provably-by/2026-09-09_2237/review-summary.md (not committed; ask the author) |
resolveOutputDir knew which allowlist entry matched but returned a bare
string, so build-mds.ts re-derived the host kind by comparing the resolved
path against its own AGENTS_OUT_ABS constant — a second source of truth
outside the allowlist that mds-variants.ts declares the single extension
point. It now returns { variant, abs }; the strip strategy is dispatched by
an exhaustive switch over HostVariant with a never default, and
AGENTS_OUT_ABS is gone. The allowlist is a table of { dir, variant } whose
`satisfies` clause refuses an entry that names no variant, and a type-level
assertion refuses a variant that names no directory (both proven by
known-bad probes against tsc). (typescript-1)
Error rendering is now one message per error kind, for both OutputDirError
and OutputNameError, each with a never default. A non-canonical declaration
says so and names the correction the core module already computed instead
of claiming the directory is not the expected one — for `dist/commands/` it
is exactly the expected one, spelled wrong. (typescript-2)
Every refusal is thrown rather than exiting mid-loop, so main()'s existing
aggregation reports all of them and exits 1 once, after the loop. A bad
host no longer abandons the hosts that follow it and leaves dist/ a mix of
fresh and stale artifacts. (architecture-3)
path.posix.normalize() leaves a backslash untouched, so `dist\commands`
would have passed the canonical check on win32. Declarations are
POSIX-spelled by contract; a backslash is now its own refusal kind on every
platform. (typescript-10)
The docblock records that the -variants filename is a Phase-2 reservation
(DR-16), not a claim about today's contents. (consistency-6)
Also: the staging file is now scoped to the writing process. Two concurrent
builds shared one <dest>.tmp, and the first rename pulled it out from under
the second — a race the new aggregation tests made reproducible (1 in 3
runs) rather than merely latent.
applies ADR-003, ADR-024
The build read a frontmatter key it never stripped, wrote destinations it never compared, and deleted a delimited block it never checked the far side of. All three failed silently — the build exited 0 with a wrong artifact. - Rename `name-template:` -> `output-name:` everywhere (BuildKey, HostEntry, fixtures, KB). The key names one output file; it performs no templating, which is what the old spelling promised and what mds-variants.ts explicitly disclaims. `name-template:` stays free for Phase 2 (applies ADR-003). - Derive the command-host strip from BUILD_KEYS, the same list the build reads (stripOutputDirKey -> stripBuildKeys). A host declaring `output-name:` previously shipped that build directive inside the deployed command; tying strip to read means a newly-read key cannot leak. - Hard-fail a valueless build key during discovery. readFrontmatterKey returns '' (not null) for a bare `output-name:`, so `?? basename` never fired and the build failed with an opaque "(empty)". A silent basename fallback would hide the authoring mistake, so both keys now refuse with an explicit message. - Resolve every destination in a plan pass before the first write, and refuse a destination claimed by two or more hosts, naming all claimants and writing none of them. `output-name:` decouples emitted name from source filename; two same-basename hosts in different directories collide with no key at all. Previously the later host in walk order silently overwrote the earlier one. - Verify BOTH ends of the generator strip. It asserted only that a leading block existed, then sliced unconditionally: a single-block host - the shape every hand-authored agent has - lost its whole frontmatter and shipped headerless, build green (PF-061). A post-strip second-block assertion now fails the build with a message naming the two-block requirement. Tests: RED->GREEN on all five. Adds a frontmatter-shape guard over dist/agents/*.md whose collector emits one row per header FOUND rather than per file, so a lost header surfaces as a short array the caller compares against the file count instead of a flag someone forgot to assert (PF-018).
The whole-repo discovery walk recursed with no depth bound and no readdir error tolerance, against the project rule that every loop carries a fixed upper bound. It now stops at MAX_WALK_DEPTH (12, roughly double the shipped tree's depth) and THROWS there rather than truncating: a host silently skipped for being too deep compiles nothing while the build still prints its counts and exits 0, and no test can tell "not there" from "never looked" (avoids PF-018). ENOENT/ENOTDIR on readdir is tolerated; every other error rethrows. Alongside it, three messages that no longer described the code: - The IGNORE_DIRS comment (and its mirror in the test file) claimed tests/ and coverage/ are skipped because the suite plants .mds fixtures under tests/. It plants none — every fixture uses a hermetic mkdtemp root. Both now state the real invariant, including that the skip is by directory name and so applies under DEVFLOW_MDS_ROOT too (applies ADR-003: end-state, not a fictional transitional cause; PF-025: a comment is an agent's execution surface). - The no-hosts error named only src/assets/commands/*.mds though discovery covers two host kinds; it now names both directories. - The two leading-block regexes are one shared LEADING_BLOCK_RE. The failure print already carried the repo-relative path; a test assertion now pins it, since a bare basename no longer identifies a host uniquely across two source directories.
The leading `---…---` block regex was reimplemented in four test files
(six sites), each free to disagree about CRLF handling and about
whether a block below byte offset 0 counts. tests/helpers.ts now
exports splitFrontmatter(text) -> { block, inner, body } | null and
every one of those sites calls it.
Behaviour-preserving: build.test.ts's copy omitted the newline after
the closing delimiter, so the shared (stricter) shape could in
principle skip an agent — its collector already carries a non-vacuity
assertion that every registered agent was parsed, and it stays green.
scripts/build-mds.ts deliberately keeps its own LEADING_BLOCK_RE: the
build script must not import from tests/.
runRealBuild() ran the build against the real repo root, so two tests
rewrote dist/commands/ and dist/agents/ while vitest ran other files in
parallel workers reading those same paths. PID-scoping the staging file
closed the writer/writer clash; the writer/reader one outlived it — a
real-root build silently repairs a stale dist/ mid-suite, so the
staleness surfaces as a flake in whichever reader lost the race instead
of as itself (PF-055).
- buildCommittedTree() copies src/assets/{commands,agents} into a temp
DEVFLOW_MDS_ROOT and builds the copy, memoised so the census and
staleness assertions share ONE spawn instead of two real-root builds.
- Replace the byte-idempotence test, which proved the build agrees with
itself rather than anything about the artifacts (PF-057), with a
staleness check: dist/ must equal a fresh build of the committed src/,
in all three directions. dist/ is gitignored, so this pins "dist/ is in
sync with src/", not "dist/ holds reviewed bytes" — AC-1.5's pre-S1
hash list was hand-verified and lives in the PR #334 body (PF-019).
- Scenario 12 self-scans this file for a spawn lacking DEVFLOW_MDS_ROOT,
with a known-bad probe, so the no-real-writes claim is mechanical
(ADR-024).
- vi.setConfig testTimeout 120_000: every test here spawns tsx, and the
5s default sat under a cold start.
- Record the @mdscript/mds byte-offset-0 frontmatter assumption that
stripGeneratorFrontmatter depends on in platform-assumptions.md.
Fixes reliability-2, testing-1, testing-2, testing-5, architecture-9,
complexity-8, performance-3.
The dist-first agent-resolution policy was encoded three times — the installer's most-preferred-first array, loadShippedDefaults's reversed least-preferred-first array, and a hardcoded pair in tests/helpers.ts. Both production sites took the same type and the same two directories in OPPOSITE order, so passing one site's list to the other silently inverted precedence and still typechecked. - assets.ts owns the policy: agentSourceDirs(root?) returns the non-empty tuple AgentSourceDirs, most-preferred first. agentsDir/compiledAgentsDir gain an injectable root so the test harness reads the layout from here. - loadShippedDefaults takes the same most-preferred-first list and applies first-wins; the per-directory body is extracted as readDirDefaults(dir), restoring the single parallel pass the outer await had serialized. ENOENT tolerance, parse-error swallowing and onWarning are unchanged. - installViaFileCopy resolves through a module-level firstExisting(candidates), dropping the if/for/for/try nest to one resolve-or-throw line. - The not-found error leads with candidates[0], the compiled path: for a generator-host agent the source path does not and will never exist, so leading with it misdirected before naming `npm run build:mds`. - resolveAgentSource consumes agentSourceDirs(root), so the third encoding is gone and its error names both resolved paths. tests/guards/agent-source-precedence.test.ts pins that the installer and loadShippedDefaults, fed the same list, resolve every registry agent out of the same tree, with a reversed-list known-bad probe (ADR-024) and a real-tree byte-equality arm. Fixtures are derived from the real agent files (PF-043). KB sentences the change invalidated are corrected in the same commit (ADR-003).
loadShippedDefaults skipped an unreadable directory silently and said
nothing when a declared agent came back from neither. In a build:cli-only
tree dist/agents/ is absent and the generated agent has no .md source, so
resolveEffective returns model === undefined, reapplyAgentMapping buckets
it 'unchanged', and disabling the proxy leaves a GPT-pinned agent
unreverted with nothing said (PF-022). The installer throws on the same
invariant; this read path must keep rendering, so it warns instead — one
aggregate message naming every missing agent and `npm run build:mds`.
- loadShippedDefaults takes an onWarning channel and emits the gap once.
- reapplyAgentMapping/revertExternalAgents accept agentSourceDirs (same
most-preferred-first convention as the installer) and route the warning
into ReapplyResult.warnings; `devflow agents` renders it via p.log.warn.
- Correct reapplyAgentMapping's JSDoc: defaults come from agentSourceDirs(),
dist/agents/ preferred over src/assets/agents/.
- installer-new: byte-compare the installed agent against the compiled
artifact and prove the source tree has no file of that name — the old
`startsWith('---')` / `contains model:` pair was true of either tree.
- Delete the superseded Phase 0→1 conditional origin test; the
dist-agents guard asserts both arms unconditionally by name (ADR-003).
dist/agents/ is gitignored and outranks src/assets/agents/ in both the
installer's resolve and loadShippedDefaults's merge, but the build only
ever wrote into it — never removed. A renamed host's old output, or a
hand-dropped file, therefore superseded the audited source on every
`devflow init`, with nothing in the install path to notice. The CI parity
guard catches the same orphan a commit later; that is too late for the
machine that ran the build.
After a clean build, every .md in dist/agents/ that no host emitted is
deleted and reported. Scope is deliberate: dist/commands/ is untouched
(it also receives release.md, copied verbatim from a hand-authored source
that is not a host), non-.md entries survive (a concurrent build's
{dest}.{pid}.tmp staging file lives there), and a refused build prunes
nothing — dist/ is left exactly as the refusal found it.
The directory comes from AGENTS_OUTPUT_DIR in mds-variants.ts, the
allowlist table's own spelling, because a build with zero generator hosts
— where every file in the directory is an orphan — cannot derive it from
the plan.
The Phase-1 build change left seven prose sites in file-organization.md,
three lines in agent-design.md, and one sentence each in CLAUDE.md,
CHANGELOG.md and CONTRIBUTING.md asserting the retired contract:
src/assets/agents/ as the sole agent source, installed with no build step.
An .mds generator host now compiles to dist/agents/, which the installer
prefers, so every one of those statements was false.
All of them are rewritten to the end state rather than annotated with what
they used to say (ADR-003): an agent is either a hand-authored .md that
installs directly, or an .mds generator host compiled by `npm run build:mds`
to dist/agents/{name}.md.
CLAUDE.md and CHANGELOG.md additionally collapsed two different mechanisms
into one phrase ("the installer and loadShippedDefaults() resolve agents
dist-first with a src fallback"). They are now described separately and
accurately: both read their order from agentSourceDirs(), but the installer
resolves first-hit-wins and throws when neither directory has the agent,
while loadShippedDefaults() walks the same list first-wins and warns through
onWarning when a registry agent has no shipped default anywhere.
Two literals are registered in the retired-wording guard's RETIRED_LITERALS
so the sweep is enforced rather than merely performed: "The only intermediate
build step" and "No build step distributes agents". Both survived the earlier
CLAUDE.md sweep purely by being spelled differently, in files the corpus
already scanned — widening a literal-matching guard's corpus without widening
its vocabulary raises confidence without raising detection (PF-025). The
guard fails on the pre-fix docs and passes after.
update-golden.ts's origin label offered a 'src fallback' arm naming a file
this phase deleted; resolveAgentSource('git') can only return origin 'dist'
or throw. It now prints the path the resolver actually returned, so the label
cannot drift from the resolver when a second generator host appears.
vitest.integration.config.ts gated tests/integration/subagent-skill-preload.test.ts on the bare truthiness of DEVFLOW_INTEGRATION_ALL, so `=0`, `false`, `no` and `off` — every spelling a developer reaches for to say "no" — all ENABLED a suite that spawns live `claude` sessions against the developer's own ~/.claude with --dangerously-skip-permissions and has previously committed to this repo mid-run (avoids PF-060, PF-055). The gate is now the pure `isAffirmative`/`integrationExclude` pair exported from the config, asserted directly by tests/integration-config-gate.test.ts rather than left to the shape of the expression. Both branches spread `configDefaults.exclude`: setting `exclude` replaces vitest's defaults rather than merging with them, so the built-in node_modules and dist guards were being dropped.
…e manifest's guards Three fixes to the MDS name-manifest surface: - collectMdsNames() merged nested hosts and partials but discarded nested subdirs, so "commands/_partials/ is flat" only ever saw depth-1 directories. It now merges them path-qualified (`nested/deeper`), making the assertion mean what it says at every depth; the known-bad probe seeds a depth-2 directory so the guard cannot go vacuous again (avoids PF-018). - The local alias ALL_HOSTS (13 command hosts) read as the manifest's exported ALL_MDS_HOSTS (14, command + generator hosts). Renamed to COMMAND_HOSTS at every site, with the numeric-floor description and the three knowledge bases that name the constant following it (avoids PF-025). The manifest's own export names are untouched. - The manifest header named four enforcing sites; tests/mds-variants.test.ts imports ALL_MDS_HOSTS too. The header now lists every importer and what each enforces, and states the rule as an end-state — a count answers "how many?", these manifests answer "which?" — rather than as a changelog (applies ADR-003).
Each of these passed for a reason other than the property it claimed. tests/mds-variants.test.ts The union-completeness "known-bad probe" added a phantom kind to the REACHED set and asserted it differed from the declared set — true by construction, for any implementation, including a collector that reaches nothing. Replace it with the two directions that have teeth: a kind seeded into the DECLARED set must be reported missing by the same verdict the assertions read, and dropping every corpus input that produces a kind must turn the verdict red naming exactly that kind. The second is what proves the corpus, not the expectation list, is carrying the load (PF-018). The hostile corpora move to module scope so the probes can narrow them. "carries the offending name on every non-empty rejection" exercised one input; it now walks the whole rejection corpus with 'empty' excluded by name, so a kind that forgets err.name is caught. tests/guards/dist-agents.test.ts The resolver-origin test looped over the .mds hosts with no floor — removing git.mds made it iterate zero times and pass. Add the floor its sibling already had. FORBIDDEN_PHASE2_CONSTRUCTS matched bare substrings against a corpus that includes src/core/mds-variants.ts and scripts/build-mds.ts — the two files whose whole subject is this machinery — so `variants:`, `tracker-` and `expandVariants` fired on any docblock naming what Phase 2 will add, and a prior change had to word around it. Each construct is now an anchored regex (line-start YAML key, call, directive, generated filename) carrying the seeded instance that must trip it, plus a probe asserting Phase-2 prose is not a violation. No existing probe was weakened. tests/build.test.ts The agent-reference message read "should exist in src/assets/agents/", false for the compiled Git agent, and the fs.access behind it was dead — resolveAgentSource throws with a build hint before it. Assert existsSync on the resolved path with a message naming that path and its origin (ADR-003). Refs PR #334 review: testing-3, testing-6, testing-9 (folds consistency-9), testing-10, complexity-12.
Twenty beforeAll hooks in this file ran scripts/build-mds.ts against the real repo root purely to freshen dist/ before reading it. That made the unit suite the last writer into a tree other vitest files read from parallel workers: a stale dist/ was silently REPAIRED mid-suite, so the staleness surfaced as a flake in whichever reader lost the race rather than as itself (PF-055). Every build now runs against an isolated DEVFLOW_MDS_ROOT temp tree, and every compiled-command assertion reads from BUILT_COMMANDS — dist/commands/ inside a build of a COPY of the committed sources. The repo's own dist/ is only ever read: the deployed set is checked read-only via requireDistFiles (fail-loud when unbuilt, PF-018), and whether its bytes still match src/ stays the byte-level compare owned by build-mds-generator-hosts.test.ts. The copy-build-memoise machinery and the spawn-scoping collector move to tests/helpers.ts as runMdsBuild / copyCommittedSources / buildCommittedTree / cleanupCommittedTree / collectSpawnScoping, so the two build-spawning test files share one implementation rather than two that can drift. Also fixed, found on the way: the P3 ignored-dir test spawned with `cwd: tmpRoot` and no env var. build-mds.ts resolves its fallback root from the script's own location, not from cwd, so that build walked and rewrote the REAL repo while the test asserted about a tmpRoot it never opened — green for the wrong reason (PF-018). It now scopes the root and asserts the walk reached the planted tree. A new self-scan (scenario 22) reads this file's own source and fails it if a spawn is added without DEVFLOW_MDS_ROOT, with a known-bad probe and a non-vacuity floor, so the isolation claim is mechanical rather than prose (ADR-024, PF-018). numeric-floors.json: slow-test-timeout-ms drops from 21 sites to 3 — the floor VALUE is unchanged and the removed sites are accounted for by a new mds-build-spawn-timeout-ms entry pinning the same 60 000 ms at runMdsBuild, the one spawn they collapsed into. This is the manifest's sanctioned "a pinned site deliberately removed" path, not a lowered floor. Knowledge bases record the end state (applies ADR-003): the residual Batch D left open is closed, not annotated. Verified: 71 tests green across 3 solo runs; a concurrent vitest invocation over build-mds, build-mds-generator-hosts and goldens/git-agent-golden all green; dist/commands/implement.md mtime unchanged across all four runs (it moved on the pre-change run); dist/agents/git.md byte-identical to the golden fixture.
…-host file Batch K hoisted buildCommittedTree, copyCommittedSources and collectSpawnScoping into tests/helpers.ts with the same API this file's call sites already used, leaving the file's own Batch D copies as dead duplicates. Delete them and import the shared ones; afterAll now calls cleanupCommittedTree, which owns the same memo it tears down (applies ADR-003 — delete the duplicate, no tombstone). runBuild stays spelled out here on purpose: scenario 12 scans this file's own source for spawnSync( sites, so the one spawn it may make has to be visible to that scan. Isolation is unchanged — every build still runs against a temp DEVFLOW_MDS_ROOT and the real dist/ is only read (PF-055).
… owner The 14 commits on this branch moved the dist-first agent-resolution policy behind agentSourceDirs() and replaced loadShippedDefaults's merge with a first-wins walk plus an aggregate missing-default warning. Four KBs and one reference doc still described the prior shape. - feature-knowledge-system: agentSourceDirs() is the single owner of the most-preferred-first order; loadShippedDefaults tolerates a missing directory on EVERY entry, not "on the dist side only", and reports a registry agent no directory supplies through one aggregate onWarning rather than a throw. The dist-side-only tolerance belongs to the test resolver resolveAgentSource, now described separately as its own resolver. - external-model-routing: the two remaining sites naming src/assets/agents/ and agentsDir() as the sole origin of shipped defaults now name agentSourceDirs() first-wins and the missing-default warning (PF-025 — this KB is an execution surface for agents in this subsystem). - The 13/14/14 count rule gets one owner, dynamic-workflow-engine, holding the full rule with its durable reasons (release.md is a verbatim copy; git.mds is a generator host bound for dist/agents). feature-knowledge- system and test-harness reference it in one sentence and keep only what each owns (PF-053). build-mds.ts's presence in two KBs is justified by its being the compiler for both host kinds, not by a tracker phase. - numeric-floors.json: the d11-posting-ops floor reads from the resolved Git agent (dist/agents/git.md); floor values untouched. - file-organization.md: assets.ts also exports compiledAgentsDir and agentSourceDirs. applies ADR-003, PF-025, PF-053
This was referenced Sep 9, 2026
Open
- outputNameRefusal's 'empty' arm built its own message instead of reusing the shared `prefix` the other three arms use, producing a differently-shaped message for the same function. Reuse `prefix` for consistency across all four kinds. - tests/agent-models.test.ts aliased the shared writeAgentFile() helper to a locally-scoped `writeAgent` name inside one describe block, while a later describe block (added in this PR) calls writeAgentFile() directly. Drop the alias and call writeAgentFile() everywhere for one consistent name. No behavior change; verified against tsc --noEmit, the full targeted test set (build-mds*, mds-variants, agent-models, installer-new, guards/, goldens/, seams/), and a byte-identical dist/agents/git.md against the golden fixture.
Owner
Author
Resolution SummaryFull summary withheld (public repository).
Full report: /Users/dean/Sandbox/devflow/.devflow/docs/reviews/feat-323-tracker-phase-1-mds-plumbing-provably-by/2026-09-09_2237/resolution-summary.md (not committed; ask the author) |
The requireBuiltCli guard throws rather than skips, but the file's top-of-file docblock and inline comment still described the old skip-based behavior. Update both to match the current fail-loud contract.
…i and trim duplicate guard comments - init-review-publication.test.ts: spawn the compiled CLI via requireBuiltCli() instead of an unguarded path.resolve, so an unbuilt tree fails with the npm run build hint instead of an opaque assertion failure. - compliance-e2e.test.ts: drop the two-line comment restating the fail-loud contract already stated in the header docblock. - guards/literal-agent-paths.test.ts: note in the header that the requireBuiltCli GREEN contract test reads the real dist/cli.js, so the file is build-dependent.
dean0x
deleted the
feat/323-tracker-phase-1-mds-plumbing-provably-by
branch
September 13, 2026 21:55
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:367builds a copy of the committed tree under a tempDEVFLOW_MDS_ROOTand compares the result to the on-diskdist/, provingdist/commands/is byte-idempotent without re-running the real build.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:80/^[A-Za-z0-9][A-Za-z0-9._\/-]*$/→ 2 redb5272d1validateOutputNametraversal/separator kindstests/mds-variants.test.ts:101b5272d1resolveOutputDirtwo-entry allowlisttests/mds-variants.test.ts:160['dist/commands']→ 3 redb5272d1resolveOutputDircanonical-declaration ruletests/mds-variants.test.ts:190b5272d1Resulterror-union completenesstests/mds-variants.test.ts:292phantom-kindinjected into the expected setb5272d1(in-test probe)tests/build-mds-generator-hosts.test.ts:227---\n---\n…assertednot.toBea2ed82f(12 red)tests/build-mds-generator-hosts.test.ts:327implement.mdwith the whole-block strip applied fails the same collectora2ed82f(in-test probe)tests/build-mds-generator-hosts.test.ts:367a2ed82ftests/build-mds-generator-hosts.test.ts:415dist/wrong-dir,dist/commands/,dist/../..a2ed82ftests/build-mds-generator-hosts.test.ts:551output-name: ../x,output-name: a/ba2ed82fIGNORE_DIRScovers tests/ and coverage/tests/build-mds-generator-hosts.test.ts:606a2ed82fcompiledAgentsDir()shapetests/core-paths-assets.test.ts:91ef0b30ftests/installer-new.test.ts:718ef0b30ftests/installer-new.test.ts:443build:mds+ both paths in messageef0b30floadShippedDefaults⊇ registrytests/agent-models.test.ts:1072scanned > 0ef0b30floadShippedDefaultscompiled-over-sourcetests/agent-models.test.ts:1094git; 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:177,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:179nested/_buried.mdsand a subdirectory8b99dc1(in-test probe)tests/build-mds-generator-hosts.test.ts:679DEVFLOW_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:175cycle:\{CYCLE_NUMBER\}; clean{CYCLE_NUMBER}must NOT be flaggedcc95210(in-test probe).mdshadowing an.mdshosttests/guards/dist-agents.test.ts:293x.mdandx.mdscc95210(in-test probe)tests/guards/dist-agents.test.ts:322.mds→ throws with a build hintcc95210(in-test probe)tests/guards/dist-agents.test.ts:424@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:212CLAUDE.md→ red naming the file and the literal596233d(mechanic 1, recorded in the commit body)requireBuiltClifail-loud build gatetests/guards/literal-agent-paths.test.ts:19403ef52dPrefix-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:
Deviations
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 twooutput-namenegatives (../x,a/b) pinis not a valid output filename. A later phase should not re-derive/typo\?/ias the contract for all five.output-name: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. Commite054f04renamed the key from its earlier templating spelling; that earlier spelling is deliberately unclaimed and reserved for Phase 2 (scripts/build-mds.ts:5-13).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).cli-unknown-command,init-e2e-flags,integration/clause-ii-file-residue) pluscompliance-e2e, and on review alsoinit-review-publication(last hand-rolleddist/cli.jsconsumer) and one stale tombstone inregistry-integrity; an unbuilt local tree now reports a collection error namingnpm run buildinstead of SKIP marks (CI unaffected: build precedes test). Commits03ef52d,a98af63,7912ffb,dd7b53a.Related Issues
Closes #323
Tracking issue #321. Depends on #322 (merged via #327).