fix(traceability): repair issue-seam defects and land the tracker test harness - #327
Merged
Conversation
Five command→agent spawn-key seams (PF-024 class) are fixed: 1. /debug #N: ISSUE_INPUT replaces the undefined ISSUE key; issue path now works for the first time. 2. /plan #N: Gate 0 now fetches issue before discovery (fetch-issue for single ref; fetch-issues-batch for multi-ref) and captures ISSUE_CONTENT, ACCEPTANCE_CRITERIA, ISSUE_REF, ISSUE_ID, ISSUE_URL. 3. git.md fetch-issues-batch: ISSUE_NUMBERS renamed to ISSUE_REFS (plan.mds and operations table aligned); Process rewritten as a single GraphQL alias query (not N+1 gh issue view calls), bounded at 50 with TRUNCATED report. 4. Issue body untrusted containment: fetch-issue and fetch-issues-batch Output blocks now wrap external body content in <untrusted-issue-body> markers; Principle 8 and the no-echo rule extended to all remote-originated bodies. 5. manage-debt D4 block added: degrades named with reason; caller records Tracked = (pending -- TRACEABILITY: DEGRADED) instead of silently losing the backfill. 6. resolve.mds D9 caller matches git.md verbatim (FIXED + commit_sha only; removes the FALSE_POSITIVE/BY_DESIGN divergence). 7. release.md: close-milestone tombstone deleted (ADR-003 end-state). 8. ensure-devflow-init: dead v2 fast-path marker replaced with live v3. Applies PF-024 (spawn-key seam integrity), ADR-003 (end-state not transition). TASK_ID: feat/322-tracker-phase-0
Captures two immutable golden fixtures from the post-A1 tree per P0-S14/S15. tests/fixtures/golden/git-agent.md: byte-equal to src/assets/agents/git.md (951L). tests/fixtures/golden/github-status-lines.txt: 215 lines extracted from the cited line ranges in git.md, code.md, dynamic-build.mds, and resolve.mds. G0.2: cmp passes, all 27 literals grep-verified at their cited source lines. These fixtures are immutable for the remainder of Phase 0-3; a mismatch means the source is wrong, never the fixture.
…n, op corrections
P0-S16: tests/seams/command-agent-input.test.ts — two-sided seam test (3 directions);
forward key check (mode 'sole', exact-match), reverse Input coverage, issue_capture_contract
producer check; per-type non-vacuity (Git+Code); op→section map built once [DR-24]; ≥14
DIST_FILES; known-bad inline `ISSUE: 42` sample proves RED on wrong key (H10, AC-0.1).
P0-S17: tests/helpers.ts — resolveAgentSource, resolveAllAgents (dist-preferred, src-fallback,
ENOENT-tolerant on dist); extractOpSectionFromCorpus with explicit 'sole'/'union' modes, no
default [DR-18]; gitAgentSinkCorpus (git.md ∪ dist/skills/git/references/*.md); parseFences,
isAgentBlock (verbatim from registry-integrity.test.ts:449-456); loadGolden (throw-on-absent);
extractStatusLines (pure function over P0-S15 line ranges, produces 215-line/16245-byte fixture).
tests/guards/agent-source-resolver.test.ts — resolver guard + both DR-18 mode unit cases with
RED proofs.
P0-S18: tests/git-agent.test.ts:359-425 — D11 forward/reverse/bypass guards repointed to
gitAgentSinkCorpus() + extractOpSectionFromCorpus { mode: 'union' }; match count available;
GIT_AGENT_PATH via resolveAgentSource('git'); extractOpSection wrapper for readability (AC-0.8).
P0-S19: tests/git-agent.test.ts — manage-debt: 60000 pin inserted after post-wave-report cap
it, before backlink-shipped-issues (AC-0.12, GAP-21).
P0-S20: tests/git-agent.test.ts REQUIRED_OPS 15→17 (+fetch-issue, +fetch-issues-batch);
tests/registry-integrity.test.ts INTERNAL_OPS removes fetch-issues-batch with SG-11 rationale
comment; Guard 1 agent check routed through resolveAgentSource (AC-0.7, AC-0.11, GAP-49).
P0-S23: tests/goldens/git-agent-golden.test.ts + tests/goldens/github-status-lines.test.ts;
scripts/update-golden.js (named target required; github-status-lines refuses without --unfreeze
per DR-03); package.json test:golden:update script; Phase-0 byte baselines as named exports
in github-status-lines.test.ts (C6).
Tests: 4051 passed / 110 files — zero failures.
…esolver (P0-S21–S25)
New guards (P0-S22):
- tests/guards/retired-wording.test.ts: per-phase allowlist for 5 Phase-0 retired
literals (ISSUE_NUMBERS, ISSUE: {issue, close milestone, may pre-fetch,
issue-first gate); mechanic-2 inline RED proof; corpus = src/assets/ + dist/commands/
- tests/fixtures/numeric-floors.json + tests/guards/numeric-floor-manifest.test.ts:
DR-27a mechanization — 8 pinned numeric floors; pattern-exists check + decrement RED proof
- tests/guards/extended-references.test.ts: P0-b Extended References resolver guard;
references/tracker/ generated-path exception asserted non-empty
Modifications:
- tests/build-mds.test.ts: DIST_FILES constant (requireDistFiles(), 14 entries);
bug-analysis added to SKILL_CHECK_HOSTS; §20 DIST_FILES non-vacuity + compliance_gate
adoption guard (6 hosts); §21 gh-issue scope guard; fix title/body mismatch (P0-S21)
- tests/agent-name-guards.test.ts:~749: resolveAgentSource(agentSlug).path replaces
literal AGENTS_DIR join — AC-0.7 (Phase 1 renames must not break guards)
- tests/shell-hooks.test.ts: non-contiguous v3 ensure-root-gitignore fixture (P0-S24)
- tests/init-logic.test.ts: non-contiguous v3 computeDevflowGitignore fixture (P0-S24)
- .github/workflows/ci.yml: npm run test:integration step added (P0-S25)
Gate G0.3: npm run build EXIT=0; npm test EXIT=0 (4065 tests / 113 files, 14 new);
npm run test:integration EXIT=1 (pre-existing: Simplify agent apply-decisions injection,
commit 9db3d85, not caused by A3b). All 43 other integration tests pass.
Refs #322
…LOG, KB citations (A4) - Add docs/reference/platform-assumptions.md: 5 rows (4 confirmed Claude Code facts + 1 UNMEASURED Bash truncation limit with observable symptom column) - CHANGELOG ### Fixed ×5: debug#42 key, plan fetch-issue, gh-issue scope guard, D9 authority consolidation, release milestone step removal (ACs 0.1/0.3/0.4/0.5/0.14) - dynamic-workflow-engine KB: document DIST_FILES=14 vs ALL_HOSTS=13 permanent divergence (SG-13) at two citation sites; add Deliberate Exceptions section for AC-0.4 guard (gh pr view ×3 and release.md:85 local-file-read exception) - resolve-pipeline KB: update 4 stale citations — test list expanded with 7 Phase-0 guard files, REQUIRED_OPS updated to 17, manage-debt/fetch-issues-batch bounds updated, INTERNAL_OPS corrected (fetch-issues-batch removed, now live from plan.mds) - feature-knowledge-system KB: fix 1 stale citation — ALL_HOSTS=13 MDS-compiled, DIST_FILES=14 with hand-authored release.md (SG-13 cross-reference) Gate G0.4: 4065 tests PASS; integration PASS (hud-git/pack-install/ambient-activation); subagent-skill-preload 1 pre-existing failure (helpers.ts:309 isolation defect, not this branch); golden SHAs verified; negative-grep 0 results; EXIT=0.
Pre-existing isolation defect: getAllSubagentPreloadedSkills scanned ALL
recent sessions for the cwd, so a concurrent pipeline agent (Code/Validate)
whose preload superset contained apply-decisions could satisfy the Simplify
find() predicate and fail the not.toContain('apply-decisions') assertion.
Fix (D33): runClaudeAndWait snapshots UUID session dirs before spawn and
diffs after close/timeout to recover the exact sessionId.
getSessionSubagentPreloadedSkills reads only that session's subagents/
directory — concurrent agents in the same cwd cannot contaminate the result.
A 3 s post-SIGTERM wait is added to the timeout path so the independently
running subagent has time to flush its initialization transcript (skill
preloads appear in the first JSONL lines) before the caller reads them.
Test prompts are hardened to explicit read-only constraints so agents
spawned with --dangerously-skip-permissions cannot write files or commit
(the Git agent ran git commit --allow-empty in an earlier run).
Unit tests for selectTranscriptsBySession are in helpers.test.ts.
… in the preload suite
The previous implementation used a directory-diff snapshot to infer the
session id after spawning: it enumerated UUID-named directories under
~/.claude/projects/<encoded-cwd>/ before the spawn and diffed after.
This was fragile in two ways:
1. A background Claude process (devflow memory worker, or a concurrent
pipeline agent) running in the same cwd could create a new UUID
directory at the same moment, causing the diff to pick the wrong
directory or return null from the tie-break.
2. When the haiku parent answered the prompt directly — without calling
the Agent tool — no subagents/ directory was created at all, and null
was indistinguishable from 'concurrent session won the diff'. The
assertion message said "No session_id in claude output" even though
nothing in the code reads claude's output.
Fix:
- runClaudeAndWait now generates a UUID with randomUUID() and passes
--session-id <uuid> to claude -p. The session identity is known before
the process starts; no diff or directory scan is needed. The directory-
diff snapshot code (existingDirs, findSessionId) is deleted (ADR-003:
end-state, no residue).
- A bounded OUTPUT_TAIL_BYTES rolling buffer captures combined
stdout+stderr. The tail is included in failure messages.
- getSubagentPreloadResult(sessionId) replaces the raw session dir read.
It returns a typed discriminated union: no-session-dir (parent spawned
no subagent), no-transcripts (dir exists but no agent-*.jsonl files),
or ok (transcripts found). This makes the assertion message say what
actually happened.
- buildSubagentsPath(homeDir, cwd, sessionId) is extracted as a pure,
exportable function and unit-tested (PF-043, PF-018).
- The parent prompt is tightened to an explicit imperative ("You MUST
call the Agent tool exactly once…") so haiku cannot answer directly.
- A MAX_SPAWN_ATTEMPTS = 2 bounded retry is added for the no-session-dir
case: LLM non-determinism occasionally causes the parent to answer
directly even with an imperative prompt; one retry is a legitimate
mitigation; the retry count is a named constant and the attempt is
logged.
- The 3 s post-SIGTERM wait is retained: it addresses a separate race
where the spawned subagent is still writing its initialization
transcript after the parent is killed.
Verification: two consecutive npx vitest runs of the preload suite both
returned 7/7; npm run test:integration returned 52/52; npm test returned
4065/4065; npx tsc --noEmit clean.
Five defects found reviewing the Phase-0 harness against its own contract.
No file under src/assets/ or tests/fixtures/golden/ is touched.
P0 — the seam test asserted nothing (AC-0.1).
Compiled fences carry the agent prompt as a quoted, sometimes indented prose
block ('"OPERATION: fetch-issue'), so the bare-line-start anchors matched 0 of
the 18 Git fences in dist/commands/. keysPassedByOp stayed empty and Directions
1 and 2 iterated nothing while every assertion stayed green; the RED proof used
a synthetic shape that occurs nowhere in the corpus (PF-043). Anchors now
tolerate leading whitespace and the opening quote; the known-bad sample is the
verbatim pre-A1 debug.mds text and runs through the same parser as the live
scan, with a GREEN post-A1 counterpart. Verified RED against a simulated pre-A1
dist and GREEN after. Added the assertion that would have caught this: every
prose Git fence mentioning OPERATION: must actually be parsed, plus a floor on
the number of ops with callers. Multi-agent ```js recipe fences are excluded
explicitly (one dist-build fence holds 24 calls across 9 agent types, so
fence-level key attribution is meaningless there) with a live-arm assertion.
D9: is excluded as a decision-ledger annotation, as a literal set so a future
D12: that is a field fails loudly.
P0 — npm test regenerated the frozen golden.
The --unfreeze acceptance test ran update-golden.js against the live fixture,
rewriting tests/fixtures/golden/github-status-lines.txt on every run including
CI. A drifted source would fail once and pass forever after. The script now
takes --out-dir; the test writes to a temp dir and asserts the output equals
both extractStatusLines() and the frozen fixture, with a second test pinning
the fixture's mtime. Fixture mtime is now unchanged across full suite runs.
P1 — the numeric-floor guard missed partial decreases and unenforceable pins.
toBe(14) appears at 3 sites and 60_000 at 21; a presence-only check could not
see one site lowered, and the decrement probe silently no-opped on grouped
numerals (60_000 does not contain 60000) while only ever probing floors[0].
Entries now record occurrences and the guard requires that many matches; the
probe runs over every entry, handles both numeral spellings, and a new
invariant rejects any entry whose pattern does not encode its floor. Added the
six exit-gate floors that were unpinned, including the D11 >= 8 in
git-agent.test.ts (the existing >= 8 entry pins a different floor in
plugins.test.ts). Verified RED by lowering one of the three toBe(14) sites.
P1 — AC-0.3's fetch-issues-batch guards did not exist.
The <=50 bound, TRUNCATED ({n} not processed), the ## Issues Batch header and
the single-GraphQL-query mechanic appeared under tests/ only inside golden
fixtures, which are data. Added four named assertions in Guard 2. The header is
asserted whole-file because extractOpSectionFromCorpus ends a section at the
next '## ' and the header is itself a '## ' line inside the op's Output
template; that scope caveat is now recorded in the KB, which had also claimed a
fetch-issues-batch bound guard that was never written.
P1 — dangling export left by the simplify commit.
getSessionSubagentPreloadedSkills had no consumer after fac739e removed its
sibling; deleted per ADR-003.
Also tightened the seam producer direction to word-boundary matching so
ISSUE_REFS can no longer satisfy ISSUE_REF.
Verification: npm run build clean, npx tsc --noEmit clean, npm test
4076/4076 (was 4065), npm pack --dry-run OK, goldens and src/assets byte-identical.
selectTranscriptsBySession and the TranscriptRecord interface it alone used are dead code: getSubagentPreloadResult (the production path) reads the session directory by exact path and never calls the selector. Remove both, their five unit tests, and their import lines. Closes #322
…e-golden into tsx
… (F2) Implements all misalignments from the alignment-fix pass: - M1 (D9 caller guard): cross-file pin that resolve.mds and dist/commands/resolve.md carry the exact D9 rule fragment from git.md's resolve-review-threads section - M2a (manage-debt D4): explicit it() asserting **Degradation (D4):** clause and (pending — TRACEABILITY: DEGRADED site in manage-debt op section - M2b (remote-I/O D4 loop): derives posting/mutation ops from corpus text (not a hand list, PF-049); accepts **Degradation (D4):** or TRACEABILITY: DEGRADED as equivalent evidence; excludes GraphQL (read-only ops like fetch-issues-batch) - M2c (pending sites): pins 4 (pending sites in resolve.mds and dist/resolve.md, each naming DEGRADED on the same line - M3 (containment guard): file-scoped per-op slices count ops with <untrusted-issue-body> (>= 2; fetch-review-threads uses <external-thread>); negative arm checks summary ops for remote field interpolation - M4 (dist fail-loud): removes silent `if (!distExists) return` skip; replaces with fail-loud expect(distExists).toBe(true) in registry-integrity.test.ts - M8 (DIST_FILES adoption x2): both COMPLIANCE sweep guards iterate DIST_FILES (14 files) instead of ALL_HOSTS (13) so release.md is covered; adds explanatory comments on the two ALL_HOSTS guards that are correctly scoped (compiler-only) - M9 (matchCount + floor comment): calls extractOpSectionFromCorpus directly to surface matchCount; seam test comment notes spec 16 vs corpus 13 floor - M11 (synthetic dist fixture): agent-source-resolver.test.ts gains beforeAll/ afterAll that create ROOT/dist/agents/git.md; removes dead scaffolding (unused imports, vars); switches src-fallback test to 'code' agent - M12a/b/c (non-vacuity fixes): retired-wording and extended-references probes now call the same collector as the main guard; build-mds extracts a named collectGhIssueProseViolations() used by both guard loop and probe - M12d (numeric floors): adds manage-debt-archive-cap and d10-dedup-marker-floor (occurrences=3 after M3 guard contributes a third >= 2 site) entries - M13 (extensionless hooks): retired-wording corpus adds '' to exts so src/assets/scripts/hooks/* extensionless files are scanned; installer-new.test.ts gains a P1 repoint comment on the literal src/assets/agents path - Guard 6 (OPERATION: regex): fixes /^OPERATION: (\S+)/gm → /^[ \t]*"?OPERATION: (\S+)/gm to match compiled MDS fences where lines start with a quoted string All 4086 tests pass; golden shasums unchanged; src/assets/** and tests/fixtures/golden/** were not touched. TASK_ID: feat/322-tracker-phase-0
Replace the beforeAll/afterAll writes to ROOT/dist/agents/git.md with a
hermetic mkdtempSync root. Vitest parallelism meant the sentinel could race
with every other test calling resolveAgentSource('git'), causing nondeterministic
failures and leaving a stale file on crash (ADR-003, PF-043).
Changes:
- resolveAgentSource(name, root = ROOT) and resolveAllAgents(root = ROOT):
injectable root param; all existing call sites unchanged (default = ROOT)
- agent-source-resolver fixture: temp dir with copies of all 16 real agent
files in src/assets/agents/ and a sentinel in dist/agents/git.md only
- New assertions: resolveAllAgents(tmpRoot) covers declared.length agents;
real-tree origin check made conditional on dist/agents/<name>.md presence
(Phase 1 safe); resolveAllAgents(tmpRoot) size uses declared.length (not
literal 16) to keep the numeric-floor-manifest pin at occurrences:1
…body summarisation
Add **Degradation (D4):** clause to both fetch-issue and fetch-issues-batch
ops (MIS-2): gh unauthenticated/absent/rate-limited at fetch time returns
TRACEABILITY: DEGRADED ({reason}), no issue content; /plan proceeds from
task description alone.
Restore {body summary} inside <untrusted-issue-body> for both ops (MIS-4):
the containment fix (4a00484) switched the placeholder from {body summary}
to {body}, adding an undocumented sixth user-visible change; restoring
summarisation keeps the enumeration at five.
Closes #322
…fter D4 fix Re-anchor extractStatusLines() in tests/helpers.ts after D4 degradation additions to fetch-issue (line 268) and fetch-issues-batch (line 314): - Lines < 268: unchanged - 268 <= N < 312: shift +2 - N >= 312: shift +4 Extended fetch-issue range to getLines(git, 268, 290) and fetch-issues-batch to getLines(git, 314, 339) so both D4 clauses appear in the captured corpus. Updated github-status-lines.test.ts: - Added PRE_PHASE0_GIT_MD_CHARS = 59_376 / PRE_PHASE0_GIT_MD_LINES = 938 baseline constants - GIT_MD_CHARS 60_440 -> 61_018, GIT_MD_LINES 959 -> 963 (post-D4) - SKILL_GIT_CHARS 9_236 -> 9_204, SKILL_WORKTREE_CHARS 2_950 -> 2_942 (was untied) - TOTAL_CHARS 72_626 -> 73_164, TOTAL_LINES 1_334 -> 1_338 - FIXTURE_BYTES 16_749 -> 17_379, FIXTURE_NEWLINES 225 -> 233 - Added live-file assertions for SKILL_GIT_* and SKILL_WORKTREE_* constants Updated numeric-floors.json: - git-md-lines floor 959 -> 963 - git-md-chars floor 60_440 -> 61_018
Re-captured with: npx tsx scripts/update-golden.ts git-agent npx tsx scripts/update-golden.ts github-status-lines --unfreeze git-agent.md: 61018 chars (was 60440); cmp with src/assets/agents/git.md exits 0 github-status-lines.txt: 17379 bytes, 233 newlines (was 16749 / 225)
The change-3 'before' clause was false: the pre-Phase-0 Git agent had
zero <untrusted-issue-body> occurrences — it did not wrap even {body}.
Rewrote to match §13 row-3: 'issue title, body, labels, acceptance
criteria, and dependencies reached Design agents unwrapped, with no
<untrusted-issue-body> containment tag of any kind.' (MIS-3)
…oor (MIS-1/MIS-2)
…collector probes, seam input scoping (MIS-5–9)
Captures the agent-source resolver API, goldens lifecycle, guard conventions (non-vacuity, DIST_FILES vs ALL_HOSTS, OPERATION: anchor), seam test three-direction contract, numeric floor manifest, and integration test hazards for the Phase 0 test harness (PR #327).
Removes the two internal seam corrections (gh issue routing and D9 divergence) from the [Unreleased] Fixed section — they stay documented in the PR body. The section now enumerates exactly the five user-visible fixes (AC-0.1, AC-0.3, AC-0.10, AC-0.14, and the pending-state fix). Closes #322
…x-round labels - Item 1 (baselines): retitle github-status-lines constants to "char baselines (JS .length, not bytes)"; rename PRE_PHASE0_GIT_MD_CHARS (wc-c) to PRE_PHASE0_GIT_MD_BYTES=59_376 and add verified PRE_PHASE0_GIT_MD_CHARS=58_903 (.length); delete the units-artifact 472-ch arithmetic sentence; annotate FIXTURE_BYTES/FIXTURE_NEWLINES as Buffer.byteLength bytes, not JS .length; rename describe blocks and error messages from "post-M3" to "Phase-0". - Item 2 (P0-S13 verify): add test in ensure-devflow-init behavioral asserting the fast-path gates on .root-gitignore-configured-v3 with no -v2 reference. RED: e726874 hook has -v2, no -v3. GREEN: current. - Item 3 (fix-round labels): strip MIS-1..9, MIS-5a, MIS-6a, post-M3, M12b, M13 across 8 test files and numeric-floors.json (22 sites). grep -rn 'MIS-|post-M3|M12b|M13\b' tests/ returns 0. - Item 4 (AC-0.10 record): add 4-line comment above containment guard recording the accepted mechanisation (>= 3 ops, one class, negative arm, <external-thread> pre-exists on main). - Item 5 (inaccurate comment): reword ~:562 to say the op set derives from REQUIRED_OPS but the 12 indicators are an explicit list. Closes #322
…t markers - Wrap setup-task's remote-sourced issue fields (title, description, criteria) in <untrusted-issue-body> tags, keeping the locally-derived issue number outside, so Principle 8's claim that all remote bodies are wrapped is now true. - Expand fetch-issues-batch Output template to show issue #2 with its full wrapper (not an elision), and add an explicit per-issue wrapping sentence. - Add Principle 8 marker-neutralisation sub-rule: before wrapping, scan for the literal closing marker and insert a backslash before the slash so an attacker cannot close containment early. - Add one-line pointer to the neutralisation rule in each affected operation: fetch-issue, fetch-issues-batch, setup-task, and fetch-review-threads.
… capture names - Add explicit carve-out to the MANDATORY Gate 0 spawn ban: Step 0's issue fetch precedes and informs Gate 0 and is the sole exception, so a model reading both lines no longer sees a contradiction that could cause it to silently skip the fetch. - Remove ISSUE_ID and ISSUE_URL from the capture list — neither name is emitted by fetch-issue or fetch-issues-batch; keeping them violated ADR-003 (no artifact without a reachable producer). ISSUE_CONTENT, ACCEPTANCE_CRITERIA, and ISSUE_REF are all derivable from the output templates and are retained.
…lean tree The learn-conventions operation writes .devflow/conventions.md, which is a git-tracked carve-out (ensure-root-gitignore re-includes it). Without a commit step, every fresh project leaves "?? .devflow/conventions.md" in git status. Add a non-blocking commit step after the Output block, mirroring the Knowledge agent pattern (knowledge.md:64-68): guard for detached HEAD, check for changes, stage and commit only .devflow/conventions.md via scoped pathspec, never push, never force, never amend. Errors are reported as CONVENTIONS_COMMIT: failed and never abort the caller's workflow.
…tall into a scratch HOME
Adds tests/integration/clause-ii-file-residue.test.ts — the composition step
neither pack-install.test.ts nor init-e2e-flags.test.ts performs:
pack real tarball → npm install into scratch node_modules → create throwaway
git repo (git init + commit .gitignore + index.js) → devflow init --recommended
with HOME pointed at a fresh mkdtemp scratch home → assert git status --porcelain
has no untracked (??) entries.
FINDING — clause (ii) is currently violated:
Observed git status --porcelain after devflow init --recommended:
M .gitignore
?? .claudeignore
devflow init --recommended calls installClaudeignore() whenever the CWD is a git
repo (claudeignoreEnabled = !!earlyGitRoot, set before the --recommended path runs;
the interactive claudeignore prompt in the advanced path is never reached). The
resulting .claudeignore file is untracked — a genuine clause-(ii) leak.
The test's clause-(ii) assertion is marked .fails() to document this finding
without papering it over with an allowlist. Remove .fails() once the residue is
fixed (e.g., add .claudeignore to the committed .gitignore, gitignore it via the
devflow carve-out, or add a --no-claudeignore flag).
Positive (non-vacuity) assertions also present:
- .gitignore shows as modified (proves init ran and wrote the carve-out).
- scratch HOME received ~/.devflow/manifest.json (proves init wrote to the
isolated scratch HOME, not the developer's real HOME).
- All other tests green: pack, install, git repo setup, init exit 0.
…hadowing feature knowledge bases
This was referenced Sep 6, 2026
Open
Updated fixture documentation from 11 to 14 shapes to reflect the current test setup where Shape L is split into L1 (fully-pushed develop) and L2 (develop +1 unpushed), and Shape M (dirty-tree asymmetry) was added during PR #287 integration. Shapes defined: A–K (11), L1, L2 (L split into 2 variants), M = 14 IIFEs.
…and walk skill references recursively
…-0.7) Replace every readdirSync(AGENTS_DIR) corpus enumeration in the test suite with resolveAllAgents() + completeness assertion so no test silently loses coverage when Phase 1 moves git.md into dist/agents/. Six files changed: - agent-frontmatter.test.ts: full rewrite — sync resolver, all async removed, AGENT_NAMES loop, completeness assertion - agent-name-guards.test.ts: GAP-1 and GAP-2 sites repointed; path.relative used in violation messages - skill-references.test.ts: four sites repointed (skill exists, ≥1 skill, install paths canonical, no self-invoke) - git-agent.test.ts: manage-debt AC-0.12 uses gitAgentSinkCorpus + union mode so the pin survives Phase 2 - installer-new.test.ts: pin filename + fix-hint literals instead of the src/assets/agents directory path - guards/literal-agent-paths.test.ts: remove installer-new bullet from block comment (it was outside SCAN_DIRS — never in exclusions array) Avoids PF-018 (scanned > 0 non-vacuity anti-pattern replaced by arrayContaining(getAllAgentNames()) completeness assertion).
…d-wording list a denylist
… respect user .claudeignore entries
…efixed refs, drop unresolved batch refs
…nd ref-handling fixes
Replace the two toBeGreaterThanOrEqual(963/61_018) floor assertions in
tests/goldens/github-status-lines.test.ts with toBe equality checks against
loadGolden('git-agent.md'), and delete the four git-md-* entries from the
numeric-floor manifest (D1: a floor on the quantity Phase 2 exists to shrink
is worse than an equality baseline pinned to the golden).
GIT_MD_LINES: 963 → 992, GIT_MD_CHARS: 61_018 → 65_677 (post-commit-7
measurements). TOTAL_* redefined as computed sums, not literals. The unused
resolveAgentSource import is dropped. The test describe block is renamed to
'git.md golden-dimension baselines' and uses loadGolden, not the live file.
Manifest comment clause added: equality baselines are not floors and are not
registered in numeric-floors.json. Remaining entries: 17.
…tinel semantics, and the new degradation handling
…oops Four for-of loops over resolveAllAgents() destructure [name, source] but only use source; replace with [, source] to make the skip explicit.
…ly do The Upgrade paragraph claimed a repo already ignoring .claudeignore "via a .gitignore entry or tracked status" still receives "the full carve-out block". Neither half held: computeDevflowGitignore and its shell twin read .gitignore lines only — tracked status is never inspected — and when a .claudeignore or !.claudeignore line is present they emit the block MINUS its .claudeignore line, not the full block. The #-prefixed-reference entry claimed the old path fetched "no issue in normal cases". The pre-branch fetch-issue step 1 sent #42 down the text-search path, which selects the first open match — an unrelated issue, or none — and fetch-issues-batch had no # handling at all. State the two shapes separately and quote the steps verbatim rather than asserting an outcome the doc never specified (PF-025).
…ract The parity paragraph still said the two twins "write the identical DEVFLOW_GITIGNORE_BLOCK" — read literally, the .claudeignore line always lands, which is the behaviour this branch removed. Name both shapes and pin the guarantee to the bytes rather than to one constant (PF-025, ADR-003).
… and refresh three stale comments
…ge bases for the Phase-0 gap closure
5 tasks
dean0x
added a commit
that referenced
this pull request
Sep 13, 2026
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.md` → `git.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`.
<details>
<summary><strong>AC-1.5 evidence — <code>shasum -a 256
dist/commands/*.md</code>, before vs after</strong></summary>
**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:367` builds a copy of the
committed tree under a temp `DEVFLOW_MDS_ROOT` and compares the result
to the on-disk `dist/`, proving `dist/commands/` is byte-idempotent
without re-running the real build.
</details>
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:80`
| charset widened to `/^[A-Za-z0-9][A-Za-z0-9._\/-]*$/` → 2 red |
pre-`b5272d1` |
| `validateOutputName` traversal/separator kinds |
`tests/mds-variants.test.ts:101` | module absent → suite red |
pre-`b5272d1` |
| `resolveOutputDir` two-entry allowlist |
`tests/mds-variants.test.ts:160` | allowlist shrunk to
`['dist/commands']` → 3 red | pre-`b5272d1` |
| `resolveOutputDir` canonical-declaration rule |
`tests/mds-variants.test.ts:190` | canonical check deleted → 3 red |
pre-`b5272d1` |
| `Result` error-union completeness | `tests/mds-variants.test.ts:292` |
`phantom-kind` injected into the expected set | `b5272d1` (in-test
probe) |
| generator whole-block strip |
`tests/build-mds-generator-hosts.test.ts:227` | 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:327` | 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:367` | paired with the recorded
pre-S1 SHAs | `a2ed82f` |
| dest allowlist negatives (3) |
`tests/build-mds-generator-hosts.test.ts:415` | `dist/wrong-dir`,
`dist/commands/`, `dist/../..` | pre-`a2ed82f` |
| filename negatives (2) | `tests/build-mds-generator-hosts.test.ts:551`
| `output-name: ../x`, `output-name: a/b` | pre-`a2ed82f` |
| `IGNORE_DIRS` covers tests/ and coverage/ |
`tests/build-mds-generator-hosts.test.ts:606` | 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:718` |
src-first order installs the src file | pre-`ef0b30f` |
| installer loud failure on both-absent |
`tests/installer-new.test.ts:443` | both dirs empty; asserts `build:mds`
+ both paths in message | pre-`ef0b30f` |
| `loadShippedDefaults` ⊇ registry | `tests/agent-models.test.ts:1072` |
registry-completeness, never `scanned > 0` | `ef0b30f` |
| `loadShippedDefaults` compiled-over-source |
`tests/agent-models.test.ts:1094` | 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:177`,
`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:179` | 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:679` | 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:175` | 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:293` | 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:322` | 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:424` | 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:212` | guard run against the
pre-sweep `CLAUDE.md` → red naming the file and the literal | `596233d`
(mechanic 1, recorded in the commit body) |
| `requireBuiltCli` fail-loud build gate |
`tests/guards/literal-agent-paths.test.ts:194` | temp root with no dist/
| `03ef52d` |
## 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:
- [x] test-inventory table present, with a red proof per guard
- [x] CHANGELOG enumerates the user-visible changes with before/after
- [x] prefix-shippability command sequence run and pasted
- [x] release-blocker items named — none for Phase 1
- [x] clause (ii) — waived per #321 "Clause (ii) waiver" section: one
live walk-through runs after Phase 3 merges
## Deviations
- **S1** — `dist/../..` 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 `output-name` 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.
- **S1** — `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. Commit `e054f04` renamed the key from its earlier
templating spelling; that earlier spelling is deliberately unclaimed and
reserved for Phase 2 (`scripts/build-mds.ts:5-13`).
- **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.
- **S3** — `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 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`.
- **S3** — `SKILL_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.
- **S3** — `tests/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).
- **S3** — the skipIf→fail-loud migration touched three Phase-0-era
files (`cli-unknown-command`, `init-e2e-flags`,
`integration/clause-ii-file-residue`) plus `compliance-e2e`, and on
review also `init-review-publication` (last hand-rolled `dist/cli.js`
consumer) and one stale tombstone in `registry-integrity`; an unbuilt
local tree now reports a collection error naming `npm run build` instead
of SKIP marks (CI unaffected: build precedes test). Commits `03ef52d`,
`a98af63`, `7912ffb`, `dd7b53a`.
## Related Issues
Closes #323
Tracking issue #321. Depends on #322 (merged via #327).
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.
Problem Being Solved
The command→agent issue seam is untyped and five defects are live:
/plannever fetches issues,/debugpasses an undeclared key, three sites bypass the Git agent for issue operations,resolve.mdscontradictsgit.md's D9 authority, andrelease.mdpromises a step that does not exist. The harness Phases 1–3 depend on does not exist in a form that survives an agent rename.Key Changes to Highlight
A1 (4aa15c5) — Prompt-text fixes (19 edits) incl. the
fetch-issues-batchsingle-query rewrite [DR-07]A2 (b9d36ed) — Two golden fixtures (git-agent, github-status-lines) captured from the post-A1 tree
A3 (6d595b6, 83594b9) — Resolver, seam test, D11 union, guard-gap closures, numeric-floor manifest [DR-27a], CI integration
A4 (e8c4055) — Docs sweep (platform-assumptions.md, CHANGELOG, KB citations)
Pipeline gate commits — integration-test isolation, Simplify, Scrutinize, alignment fixes, cleanup, knowledge write-back
Post-review fixes (75f13e7 – a2207ac) — containment gaps in
setup-taskand batch template, Gate 0 / Step 0 contradiction inplan.mds,learn-conventionscommit step,.claudeignoregitignore (marker v4), seam and guard repairs, golden re-capture, CHANGELOG expansion, KB updateInternal seam corrections (not user-visible):
gh issuerouting through the Git agent (AC-0.4)Thirteen User-Visible Behaviour Changes
/plan #42parsed the reference and consumed a body it never fetched/plan #42spawnsOPERATION: fetch-issue;/plan #12 #15 #18spawnsOPERATION: fetch-issues-batch(one query,≤50,TRUNCATED ({n} not processed))/debug #42passedISSUE: {issue number}— a keyfetch-issuedoes not declare; the issue path never worked/debug #42passesISSUE_INPUT: {issue reference}and the issue path worksfetch-issue/fetch-issues-batch: issue title, body, labels, ACs, and dependencies reached Design agents unwrapped<untrusted-issue-body>with a data-only note appended;### Suggested Branch(locally-derived) stays outside the blockresolution-summary.mdemitted a bareTracked = (pending)Tracked = (pending)names the DEGRADED reason, at all four sitesrelease.mdpromised "close milestone" — a step that does not existresolve.mdsauthorised the Git agent to auto-resolve threads on any of three verdicts:FIXED,FALSE_POSITIVE, orBY_DESIGNFIXEDandcommit_shais non-empty — matching the D9 contractgit.mdhad always enforced, closing a live divergence (closes PF-024)setup-taskemitted issue title, description, and ACs as bare bullets;fetch-issues-batchtemplate showed the wrapper on the first issue only, leaving up to 49 uncontained; no op defended against remote content containing the literal</untrusted-issue-body>closing markersetup-taskwraps all remote-sourced fields; batch template explicitly shows the wrapper on every entry; Principle 8 mandates neutralising any closing marker found in remote content before wrapping, with pointers from all four affected operationsplan.mdsdeclared "Do not spawn any agents until Gate 0 is confirmed" with no exception, directly contradicting the Step 0 issue fetch — a session honouring the ban could silently skip the fetchlearn-conventionswrote.devflow/conventions.md— a git-tracked carve-out path — but included no commit step, leaving?? .devflow/conventions.mdingit statuson every fresh projectsetup-taskstep 4b commits.devflow/conventions.mdaftergit checkout -b, on the feature branch and never on the base branch, viagit -C … commit --only -- .devflow/conventions.md(nevergit add -A, never push, never force, never amend), reportingCONVENTIONS_COMMIT: …non-blockingly;learn-conventionsno longer commitsdevflow initwrote.claudeignoreinto any git repo but never ignored the file, leaving?? .claudeignoreingit statuson every fresh install.claudeignoreis a line of the devflow-managed.gitignoreblock (markerv4); block presence is detected only by the devflow-unique!.devflow/conventions.mdsentinel — a user-authored.claudeignoreor!.claudeignoreline is respected (the block is appended without its own.claudeignoreline; an un-ignore is never overridden); the TypeScript function and theensure-root-gitignoreshell twin are byte-identical and idempotent across 15 parity rows| 11 |
/plan,/debugand the dynamic-build wave reader had no branch for aTRACEABILITY: DEGRADEDreturn from the Git agent — the status line could be consumed as issue content |/planwarns, carries the line verbatim into the report and runs Gate 0 with the bare issue reference;/debugreports it verbatim and asks for the bug description before generating hypotheses; the wave reader returns empty ready/blocked sets with the DEGRADED rationale and the wave stops (AC-0.6) || 12 |
fetch-issuesent#42down the text-search path (first open match for the literal#42— the wrong issue, or none);fetch-issues-batchleft#-prefixed tokens unspecified | Both strip a leading#(#42≡42) before the numeric/text branch (AC-0.3) || 13 | A null GraphQL alias (unresolvable reference) in a batch could abort the whole fetch | The alias is dropped, never aborts the batch, and is reported as
NOT_FOUND ({refs})alongside anyTRUNCATEDnote; comments are not fetched in batch mode (AC-0.3) |Honest label: "no change to the GitHub rendering of any existing traceability artifact." Not "no behaviour change" — see the thirteen above.
Breaking Changes
none.
Reviewer Focus Areas
Commit order — A1 (4aa15c5) edits
git.mdonly, no test file touched. A2 (b9d36ed) contains only the two fixtures, captured from the post-A1 tree. A3a/A3b split is pre-authorised. A4 (e8c4055) is the docs sweep.RED proofs — Verify seam known-bad at
tests/seams/command-agent-input.test.tsagainst the pre-A1 parser. D9 caller attests/git-agent.test.tsassertsresolve.mdsanddist/commands/resolve.mdcarry the D9 rule literal fromgit.md.manage-debtD4 atgit-agent.test.ts(AC-0.6a).gh issuescope viacollectGhIssueProseViolationsattests/build-mds.test.ts. Batch single-query [DR-07] atgit-agent.test.ts.Numeric floor manifest [DR-27a] —
tests/guards/numeric-floor-manifest.test.tsmechanises AC-0.17 againsttests/fixtures/numeric-floors.json(18 entries, occurrence-aware). GREEN: no numeric literal intests/decreases — with one argued exception (see Deviations from the Plan §1).Recorded exceptions:
src/assets/commands/release.md:85— conventions read (allowlisted)src/assets/commands/code-review.mds:77—gh pr view(allowlisted)src/assets/commands/bug-analysis.mds:44—gh pr view(allowlisted)[DR-03] —
npm run test:golden:updaterequires a named target, refusesgithub-status-lineswithout--unfreeze, has--out-dirso tests never rewrite the frozen fixture.Deviations from the Plan
Four deliberate departures from stated acceptance criteria, each argued below.
1. A numeric floor was decreased (AC-0.17 exception).
issue-capture-contract-sizeintests/fixtures/numeric-floors.jsonwent from 5 → 3. AC-0.17 states no threshold may be lowered. This is a deliberate, argued exception: the old value of 5 countedISSUE_IDandISSUE_URL, neither of which had a producer anywhere ingit.mdor a consumer inplan.mds— the floor was inflated by two erroneous entries. Lowering from 5 to 3 corrects the manifest to match the actual seam, not the imagined one. This is the only floor decrease in the manifest.2. The golden regeneration commit (
3a95c92) is not strictly fixture-only.That commit carries
tests/goldens/github-status-lines.test.tsalongside the.txtfixture, because that file holds the fixture's own byte/newline freeze baselines (17_379→17_914,233→246). Splitting them would leave the tree red at the commit boundary, which this branch's own rules forbid (all CI gates must be green at every commit). The AC's intent — no behaviour change in the golden commit — is preserved: the test file change is the baseline constant update, not a logic change.3. The frozen
github-status-lines.txtwas unfrozen and re-captured (AC-0.9 / DR-03).Third re-capture within Phase 0 (after
a5dd078and38db29e), on explicit user authorisation. The diff is exactly three hunks, all genuine status-line vocabulary introduced by75f13e7:setup-taskcontainment, the per-issue batch wrapper, and the external-thread neutralisation note. Context that matters for Phase 1: the fixture was previously extracted by hard-coded line offsets, so it broke on any insertion anywhere above a sampled range — it went red on a 26-line insertion that changed no status line at all, and would have failed outright at Phase 1 whengit.mdbecomes MDS-generated.e276175re-anchors extraction on content, proven faithful by reproducing the pre-change fixture byte-for-byte fromb6928e5before being run against HEAD.4. Phase 0 ships thirteen user-visible changes, not five.
Scope grew from review findings. Six of the thirteen were always on the branch (
/planfetch,/debugkey, containment,Trackedreason,close milestone, D9 gate — the last was present but undeclared, explaining why the CHANGELOG previously understated the count). Four are new: thesetup-task/batch/marker containment gaps, the Gate 0 / Step 0 contradiction,learn-conventionscommit step, and.claudeignoregitignore.97f421aand7074733are isolated commits and can be lifted out if the reviewer prefers a tighter phase.5. Size floors → equality baselines (user decision D1).
GIT_MD_LINES/GIT_MD_CHARS/TOTAL_*intests/goldens/github-status-lines.test.tsare equality baselines againsttests/fixtures/golden/git-agent.md(992 / 65,677 / TOTAL_CHARS 77,823), re-set in every golden-regeneration commit and deliberately not registered intests/fixtures/numeric-floors.json; the fourgit-md-*manifest entries were deleted (17 remain). Red proof with the old constants:expected 992 to be 963andexpected 65677 to be 61018.6. Seam caller floor measured 13, not the planned 14.
18
## Operation:sections, five without a live caller fence (learn-conventions,create-release,gather-release-evidence,backlink-shipped-issues,check-ci-status— the last is prose-only in implement/resolve); floor raised 10 → 13.7. Gitignore append forms.
Rules "continue an existing block" (v3→+
.claudeignore, v2→+conventions) use a no-blank-separator form so five pre-existing byte-identity assertions stay valid; only "start a new block" inserts the blank separator. A 15th parity row (a.gitignorewhose only content is the legacy.devflow/line) pins agrep -vexit-status hazard found while aligning the twins.8. Batch step-5 wording.
The plan's "
{n}counts fetched issues only" was self-contradictory against the literalTRUNCATED ({n} not processed); git.md states the invariant as two disjoint counts instead.Test Inventory (AC-0.13)
tests/seams/command-agent-input.test.tsKEY: passed is declared/plan,/debugundeclared keystests/seams/command-agent-input.test.tsrequired INPUT is passedtests/seams/command-agent-input.test.tsissue_capture_contract()searches agent source, consumer excluded; RED proof viaISSUE_URLre-addtests/seams/command-agent-input.test.tstests/git-agent.test.tsgit.mdresolve-review-threadssection literal matchesresolve.mdstests/git-agent.test.ts60000char cap +DEGRADEDreason presencetests/git-agent.test.ts≤50issues,TRUNCATED ({n} not processed),## Issues Batch ({n} issues)gh issuescopetests/build-mds.test.tsviacollectGhIssueProseViolationscompliance_gateadoptiontests/git-agent.test.tsgh api graphqlnotgh issue viewlooptests/git-agent.test.tsfetch-issue,fetch-issues-batch,setup-task}; floor 3 on issue-body opsmain— recorded RED proof (previously the broad>= 3predicate passed on unmodifiedmainvia pre-existing<external-thread>ops, validating nothing for issue-body; now split and named)tests/git-agent.test.tsfetch-review-threads,post-resolution-summary,post-wave-report}; stabilisation guardmainsince those ops pre-existed; labelled as such rather than dressed up as validationtests/git-agent.test.ts>=8posting ops + known-bad synthetic corpustests/git-agent.test.tsINTERNAL_OPStests/registry-integrity.test.ts:397-405fetch-issues-batchremoved with SG-11 rationaletests/goldens/git-agent-golden.test.ts<<</>>>marker device on mismatchtests/goldens/github-status-lines.test.tstest:golden:update -- github-status-linesrefuses without--unfreezetests/goldens/github-status-lines.test.tstests/guards/numeric-floor-manifest.test.ts| Agent-source resolver |
tests/harness/agent-source-resolver.test.ts|gitAgentSinkCorpusrecursive walk finds nestedreferences/tracker/github/fetch-issue.md;walkFilesmissing-dir →[]and depth cap;resolveAllAgents() ⊇ getAllAgentNames()in skill-references / agent-name-guards / agent-frontmatter | RED: nested reference missing on flatreaddir; resolver missing-gitfixture || Seam caller floor |
tests/seams/command-agent-input.test.ts| Floor 13 on live caller fences | RED: temporary floor 99 →expected 13 to be greater than or equal to 99|| Init logic exact-output |
tests/init-logic.test.ts| Cases (a)–(f) exact output + idempotency | RED: (a)–(c) returnednull, (e) returned a block, (f)trimEndcollapsed newlines || Shell-hooks parity |
tests/shell-hooks.test.ts| 15-row parity (bytes + booleans + TS/shell idempotency) | RED: 5 rows byte-diverged includinggrep -qFmatching.claudeignoreas substring of!.claudeignore|| Golden-dimension baselines |
tests/goldens/github-status-lines.test.ts| Equality baselines:GIT_MD_LINES992,GIT_MD_CHARS65,677 | RED: old constants — see Deviations §5 || Conventions-commit guard |
tests/git-agent.test.ts|collectConventionsCommitPlacementViolationslive guard + H10 probe | RED: 4 violations on synthetic corpus with old learn-conventions commit block and no step 4b |Byte/Char Baselines (P0-S1)
Post-Phase-0 (HEAD
b04e8f7):src/assets/agents/git.md=tests/fixtures/golden/git-agent.md: 992 lines / 65,677 chars (JS.length) / 66,180 bytes (18## Operation:sections)src/assets/skills/git/SKILL.md: 9,204 chars / 283 linessrc/assets/skills/worktree-support/SKILL.md: 2,942 chars / 92 linestests/fixtures/golden/github-status-lines.txt: 17,914 bytes / 246 newlines (byte-identical at every commit since08fbdd4)Pre-Phase-0 (
main@e726874):git.md: 938 lines / 59,376 bytes / 58,903 charsConstants live in
tests/goldens/github-status-lines.test.tsand are asserted against the live files.Prefix-Shippability Evidence (§14 clause i–iv)
Run on HEAD
a2207ac:npm run buildexit 0 (13 MDS hosts compiled,dist/commands/14 files)npx tsc --noEmitexit 0npm testexit 0 — 4,127 tests / 114 filesnpm run test:integrationexit 0 — 48 tests / 5 files (preload suite 7/7: Simplify 18.0s, Scrutinize 13.3s, Review 11.5s, Code 17.2s, Design 13.1s, Git 13.0s, Research 12.6s)npm pack --dry-runexit 0 — 375 filesf0e29b82106082b937f1e8804b998ca03e1454c08cb3ccbc1965390a95edd46f(status-lines) /6382d7698d80cdfdfe248ffe96db734a5836fb615ecd7e2634fbb7306b02384f(git-agent),cmpexit 0, fixture mtime unchanged across suitesPR link line|Branch tokenin git.md → 0 ·docs/reference/platform-assumptions.mdfirst inbound reference added (CLAUDE.md## Reference Documents, commit90fd060) — clause (iii) miss resolvedgit status --porcelainemptyClause (ii) — file-residue half MECHANISED; prompt half still manual (see Test Suite Status).
Test Suite Status
Full suite: 4,127 tests / 114 files — everything this PR modifies is green.
A full-suite run produced 12 failures across 7 files:
redact-secrets,decisions/ledger-ops,shell-hooksjson-helper describe,eager-memory-refresh,decisions/decisions-usage-scan,build-mds,compliance-e2e. All 7 files pass 3/3 in isolation, 21 clean runs, zero isolated failures. Root cause is load-induced subprocess-spawn contention under concurrent test workers, not regressions — none of the 7 files is in code this PR modifies.Integration: 5 files / 48 tests green.
tests/integration/subagent-skill-preload.test.tsis excluded — it spawns liveclaudeagainst the developer's real~/.claudeand has historically committed to this repo mid-run.Clause (ii) mechanisation status:
tests/integration/clause-ii-file-residue.test.tspacks a real tarball, installs into a scratch$HOME, runsdevflow init --recommendedin a throwaway git repo, and asserts no??untracked entries — with a positive assertion that.gitignorewas modified, so it cannot pass by doing nothing. It found a real violation on first run (?? .claudeignore), now fixed by7074733./plan → /implement → /code-review → /resolve → /releasewalk-through remain unverified. Remaining manual scope is narrowed to four model-generalisation questions: (a) does the<untrusted-issue-body>wrapper repeat across all 50 batched issues; (b) is the literalTRUNCATED ({n} not processed)emitted; (c) does the agent build one GraphQL query or fall back to 50 sequentialgh issue viewcalls; (d) doesplan.mdsStep 0 actually fire.CI: build-and-test PASS (51s) · security/snyk PASS — all checks green.
PR-Template Checklist (D-B)
Known Deviations
gh pr viewexceptions, not two (code-review.mds:77, bug-analysis.mds:44, resolve.mds:63 pre-exists on main)git.md:resolve-review-threadssection, not a line numberstep 1cretained; retired literal isissue-first gate>=3floor derivation; see Deviations from the Plan §1 and Test Inventory for detailtests/integration/**scope addition forced by wiringtest:integrationinto CIdocs/reference/platform-assumptions.mdnow has its first inbound reference (CLAUDE.md## Reference Documents, commit90fd060) — clause-(iii) miss resolvedCommit Subjects
Related Issues
Closes #322 · Tracking: #321