// tests/lib/claude-md-scope.test.mjs // Fix round 4.5b2 part B, Step 1 (tag CAT): the rule catalogue holds the two // CLAUDE.md keys at MINOR (decision 98), and every text that counts the keys // says 17. Each [CAT] case fails at 96fba75 on an assertion. Step 10 (tags // CMD and DPR) adds the instruction set and its filter, below. import { after, test } from 'node:test'; import { strict as assert } from 'node:assert'; import { spawnSync } from 'node:child_process'; import { appendFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { dirname, isAbsolute, join } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { RULE_CATALOGUE, RULE_KEYS, getRule } from '../../lib/review/rule-catalogue.mjs'; import { computeFindingIds } from '../../lib/parsers/finding-id.mjs'; import { childEnv } from '../helpers/child-env.mjs'; const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); const read = (rel) => { const p = join(ROOT, rel); assert.ok(existsSync(p), `${rel} exists`); return readFileSync(p, 'utf-8'); }; test('[CAT] the catalogue holds CLAUDE_MD_DEVIATION and CLAUDE_MD_STALE at MINOR with memory_route null', () => { for (const key of ['CLAUDE_MD_DEVIATION', 'CLAUDE_MD_STALE']) { assert.ok(RULE_KEYS.has(key), `RULE_KEYS holds ${key}`); const rule = getRule(key); assert.ok(rule !== null, `getRule(${key}) finds the entry`); assert.equal(rule.severity, 'MINOR', `${key} is MINOR`); assert.equal(rule.category, 'conformance', `${key} is conformance`); assert.equal(rule.memory_route, null, `${key} is never proposed as project memory`); assert.ok(typeof rule.description === 'string' && rule.description.length > 0, `${key} has a description`); } assert.equal( getRule('CLAUDE_MD_DEVIATION').description, "A change that breaks a line of the target repo's tracked CLAUDE.md, AGENTS.md or .claude/rules file.", ); assert.equal( getRule('CLAUDE_MD_STALE').description, 'A change that makes a line of such an instruction file untrue.', ); }); test('[CAT] the catalogue has 17 keys and docs/architecture.md and both reviewer agents say 17-key', () => { assert.equal(RULE_CATALOGUE.length, 17); assert.equal(RULE_KEYS.size, 17); const header = read('lib/review/rule-catalogue.mjs').split('\n').slice(0, 10).join('\n'); assert.match(header, /\b17 rule keys\b/, 'the catalogue header counts 17 rule keys'); const arch = read('docs/architecture.md'); assert.match(arch, /rule catalogue \(17 keys\)/); assert.match(arch, /the 17-key rule catalogue/); assert.doesNotMatch(arch, /\b(12|15)-key rule catalogue|rule catalogue \((12|15) keys\)/); for (const agent of ['agents/code-correctness-reviewer.md', 'agents/brief-conformance-reviewer.md']) { const text = read(agent); assert.match(text, /the 17-key catalogue/, `${agent} says 17-key`); assert.doesNotMatch(text, /\b(12|15)-key catalogue/, `${agent} no longer says 12-key`); } }); test('[CAT] the armB RULE_TIERS table holds both keys at MINOR', () => { const src = read('scripts/trekreview-armB.workflow.mjs'); const start = src.indexOf('const RULE_TIERS = {'); assert.ok(start !== -1, 'the script carries a RULE_TIERS literal block'); const block = src.slice(start, src.indexOf('};', start)); const tiers = Object.fromEntries([...block.matchAll(/([A-Z_]+):\s*'([A-Z]+)'/g)].map((m) => [m[1], m[2]])); assert.equal(tiers.CLAUDE_MD_DEVIATION, 'MINOR'); assert.equal(tiers.CLAUDE_MD_STALE, 'MINOR'); assert.equal(Object.keys(tiers).length, 17); }); // --- CMD / DPR: the instruction set and its filter (Step 10) --------------------------- // [CMD] lib/review/instruction-scope.mjs builds the set of the repo's tracked // instruction files at the reviewed commit (root and on-path CLAUDE.md and // AGENTS.md, .claude/rules/*.md; a symlink inside the repo keeps its own name // next to its tracked target, one out of it is dropped), and review-finalize.mjs sets aside // under Declined to Judge a CLAUDE.md-key finding that cites a file outside it. // [DPR] a changed instruction file at any depth, docs/** included, gets deep // review (`isInstructionFile`). Each case runs in a throwaway git repo with no // hooks; each fails on an assertion while instruction-scope.mjs is absent. const SCOPE = join(ROOT, 'lib', 'review', 'instruction-scope.mjs'); const FINALIZE = join(ROOT, 'lib', 'review', 'review-finalize.mjs'); const GATE = join(ROOT, 'lib', 'review', 'verdict-gate.mjs'); const VALIDATOR = join(ROOT, 'lib', 'validators', 'review-validator.mjs'); const REVIEW_REL = '.claude/projects/2026-11-02-cmd/review.md'; // Loaded so that each new assertion fails on its own while the module is absent. let scopeMod = {}; try { scopeMod = await import(pathToFileURL(SCOPE).href); } catch (e) { if (existsSync(SCOPE)) throw e; } const made = []; after(() => { for (const b of made) rmSync(b, { recursive: true, force: true }); }); // Copied from tests/lib/review-policy.test.mjs (g). function g(cwd, ...args) { const r = spawnSync('git', args, { cwd, encoding: 'utf8', env: childEnv() }); if (r.status !== 0) throw new Error(`git ${args.join(' ')}: ${r.stderr}`); return r.stdout.trim(); } // Copied from tests/lib/review-policy.test.mjs (putIn). function putIn(dir, rel, text) { mkdirSync(dirname(join(dir, rel)), { recursive: true }); writeFileSync(join(dir, rel), text); } const parse = (r) => { try { return JSON.parse(r.stdout); } catch { return {}; } }; const three = (tag) => `export const ${tag}1 = 1;\nexport const ${tag}2 = 2;\nexport const ${tag}3 = 3;\n`; /** * A throwaway repo beside an `outside` folder and a `home` folder that holds a * global `.claude/CLAUDE.md`. The start commit holds the instruction files of * the cases (two of them symlinks: inner/CLAUDE.md -> ../docs/rules.md inside * the repo, lnk/CLAUDE.md -> ../../outside/CLAUDE.md and lnk/AGENTS.md -> an * absolute path out of it); the end commit (the reviewed one) changes * sub/deep/x.mjs, inner/y.mjs and lnk/z.mjs. After it, sub/deep/CLAUDE.md is * committed later and sub/AGENTS.md is left untracked. */ function scopeRepo() { const base = mkdtempSync(join(tmpdir(), 'claude-md-scope-cmd-')); made.push(base); const repo = join(base, 'repo'); const outside = join(base, 'outside'); const home = join(base, 'home'); mkdirSync(repo, { recursive: true }); mkdirSync(join(base, 'hooks-none')); putIn(outside, 'CLAUDE.md', '# outside the repo\n'); putIn(outside, 'AGENTS.md', '# outside the repo\n'); putIn(home, '.claude/CLAUDE.md', '# the global CLAUDE.md\n'); g(repo, 'init', '-q', '-b', 'main'); appendFileSync(join(repo, '.git', 'config'), '[user]\n\tname = Test\n\temail = test@example.invalid\n' + `[commit]\n\tgpgsign = false\n[core]\n\thooksPath = "${join(base, 'hooks-none')}"\n`); appendFileSync(join(repo, '.git', 'info', 'exclude'), '.claude/projects/\n'); for (const rel of ['CLAUDE.md', 'AGENTS.md', 'sub/CLAUDE.md', 'sub/deep/AGENTS.md', 'other/CLAUDE.md', '.claude/rules/a.md', '.claude/rules/notes.txt', 'docs/rules.md']) putIn(repo, rel, `# ${rel}\n`); for (const [rel, tag] of [['sub/deep/x.mjs', 'x'], ['other/y.mjs', 'o'], ['inner/y.mjs', 'y'], ['lnk/z.mjs', 'z'], ['src/a.mjs', 'a']]) { putIn(repo, rel, three(tag)); } symlinkSync('../docs/rules.md', join(repo, 'inner', 'CLAUDE.md')); symlinkSync('../../outside/CLAUDE.md', join(repo, 'lnk', 'CLAUDE.md')); symlinkSync(join(outside, 'AGENTS.md'), join(repo, 'lnk', 'AGENTS.md')); g(repo, 'add', '-A'); g(repo, 'commit', '-q', '-m', 'chore: start'); const start = g(repo, 'rev-parse', 'HEAD'); for (const [rel, tag] of [['sub/deep/x.mjs', 'x'], ['inner/y.mjs', 'y'], ['lnk/z.mjs', 'z']]) { putIn(repo, rel, three(tag).replace(`${tag}1 = 1;`, `${tag}1 = 11;`)); } g(repo, 'add', '-A'); g(repo, 'commit', '-q', '-m', 'feat: the work'); const end = g(repo, 'rev-parse', 'HEAD'); putIn(repo, 'sub/deep/CLAUDE.md', '# committed after the reviewed commit\n'); g(repo, 'add', '-A'); g(repo, 'commit', '-q', '-m', 'docs: later'); putIn(repo, 'sub/AGENTS.md', '# never committed\n'); const reviewPath = join(repo, ...REVIEW_REL.split('/')); return { base, repo, outside, home, start, end, reviewPath, set: (changedFile, env = {}) => spawnSync(process.execPath, [SCOPE, '--set', '--repo', repo, '--sha', end, '--changed', changedFile], { encoding: 'utf8', env: childEnv(env) }), write: (text) => putIn(repo, REVIEW_REL, text), read: () => readFileSync(reviewPath, 'utf8'), finalize: () => spawnSync(process.execPath, [FINALIZE, '--review', reviewPath, '--repo', repo, '--project', dirname(reviewPath), '--round', '1'], { encoding: 'utf8', env: childEnv() }), gate: () => spawnSync(process.execPath, [GATE, '--json', reviewPath, '--repo', repo], { encoding: 'utf8', env: childEnv() }), validate: () => spawnSync(process.execPath, [VALIDATOR, '--strict', reviewPath], { encoding: 'utf8', env: childEnv() }), }; } /** The set of the end commit for the three changed paths of the end commit. */ const CHANGED = ['sub/deep/x.mjs', 'inner/y.mjs', 'lnk/z.mjs']; const EXPECTED_SET = ['.claude/rules/a.md', 'AGENTS.md', 'CLAUDE.md', 'docs/rules.md', 'inner/CLAUDE.md', 'sub/CLAUDE.md', 'sub/deep/AGENTS.md']; test('[CMD] the set holds the repo\'s tracked root and on-path CLAUDE.md/AGENTS.md and tracked .claude/rules/*.md', () => { assert.equal(typeof scopeMod.instructionSet, 'function', 'instruction-scope.mjs exports instructionSet'); const fx = scopeRepo(); const r = scopeMod.instructionSet(fx.repo, fx.end, CHANGED); assert.equal(r.ok, true, JSON.stringify(r)); assert.deepEqual([...r.files].sort(), EXPECTED_SET, 'root CLAUDE.md and AGENTS.md, sub/CLAUDE.md and sub/deep/AGENTS.md on the changed path, .claude/rules/a.md, and inner/CLAUDE.md with its target docs/rules.md'); assert.ok(r.files.every((p) => !isAbsolute(p) && !p.startsWith('..')), `every path is repo-relative: ${r.files.join(', ')}`); // Control: with no changed path the set still holds the root files and the rules. const bare = scopeMod.instructionSet(fx.repo, fx.end, []); assert.equal(bare.ok, true, JSON.stringify(bare)); assert.deepEqual([...bare.files].sort(), ['.claude/rules/a.md', 'AGENTS.md', 'CLAUDE.md']); }); test('[CMD] the set leaves out an untracked file, a file outside the repo and a tracked symlink resolving outside the repo', () => { assert.equal(typeof scopeMod.instructionSet, 'function', 'instruction-scope.mjs exports instructionSet'); const fx = scopeRepo(); const r = scopeMod.instructionSet(fx.repo, fx.end, [...CHANGED, 'sub/z.mjs', join(fx.outside, 'CLAUDE.md'), '../outside/CLAUDE.md']); assert.equal(r.ok, true, JSON.stringify(r)); for (const left of ['sub/AGENTS.md', 'sub/deep/CLAUDE.md', 'other/CLAUDE.md', 'lnk/CLAUDE.md', 'lnk/AGENTS.md', '.claude/rules/notes.txt']) { assert.ok(!r.files.includes(left), `${left} is left out: ${r.files.join(', ')}`); } assert.ok(r.files.every((p) => !isAbsolute(p) && !p.split('/').includes('..') && !p.includes(fx.outside)), `no path outside the repo enters the set: ${r.files.join(', ')}`); assert.deepEqual([...r.files].sort(), EXPECTED_SET); // A git failure gives {ok:false}: no repo, or a sha that is not a full commit id. const noRepo = scopeMod.instructionSet(fx.outside, fx.end, CHANGED); assert.equal(noRepo.ok, false, `outside a repo: ${JSON.stringify(noRepo)}`); assert.equal(scopeMod.instructionSet(fx.repo, fx.end.slice(0, 12), CHANGED).ok, false, 'a short sha'); }); test('[CMD] a global CLAUDE.md outside the repo is left out', () => { assert.ok(existsSync(SCOPE), 'lib/review/instruction-scope.mjs exists'); const fx = scopeRepo(); const globalFile = join(fx.home, '.claude', 'CLAUDE.md'); const list = join(fx.base, 'changed.txt'); writeFileSync(list, `${globalFile}\n~/.claude/CLAUDE.md\nsub/deep/x.mjs\n`); const r = fx.set(list, { HOME: fx.home }); assert.equal(r.status, 0, r.stdout + r.stderr); const out = parse(r); assert.equal(out.ok, true, r.stdout); assert.ok(Array.isArray(out.files), r.stdout); assert.ok(!out.files.some((p) => p.includes(fx.home) || p.startsWith('~') || isAbsolute(p)), `the global CLAUDE.md never enters the set: ${out.files.join(', ')}`); assert.deepEqual([...out.files].sort(), ['.claude/rules/a.md', 'AGENTS.md', 'CLAUDE.md', 'sub/CLAUDE.md', 'sub/deep/AGENTS.md']); // A git failure is exit 2, never a set. const bad = spawnSync(process.execPath, [SCOPE, '--set', '--repo', fx.outside, '--sha', fx.end, '--changed', list], { encoding: 'utf8', env: childEnv() }); assert.equal(bad.status, 2, bad.stdout + bad.stderr); assert.equal(parse(bad).ok, false, bad.stdout); }); /** * A review_version 1.2 review of `start..end` with MINOR findings * [{rule_key, file, line, title, instruction_ref?}]; ids over (file, line, rule_key). */ // Shape copied from tests/lib/review-policy.test.mjs (polReview), MINOR findings only. function cmdReview({ start, end, findings }) { const list = findings.map((f, i) => ({ severity: 'MINOR', rule_key: f.rule_key, file: f.file, line: f.line, title: f.title, detail: `d${i + 1}`, recommended_action: 'Fix the line.', ...(f.instruction_ref !== undefined ? { instruction_ref: f.instruction_ref } : {}), })); const ids = computeFindingIds(list); const ordered = list.map((f, i) => ({ id: ids[i], ...f })); const counts = { BLOCKER: 0, MAJOR: 0, MINOR: ordered.length, SUGGESTION: 0 }; const json = JSON.stringify({ verdict: 'ALLOW', counts, allow_blocked_by: [], declined_to_judge: [], findings: ordered }, null, 2); const entries = ordered.map((f) => `### ${f.id}\n\n- **Rule:** \`${f.rule_key}\`\n- **Location:** \`${f.file}:${f.line}\`\n- **Title:** ${f.title}\n`).join('\n'); return `--- type: trekreview review_version: "1.2" task: "t" slug: cmd project_dir: .claude/projects/2026-11-02-cmd/ brief_path: .claude/projects/2026-11-02-cmd/brief.md scope_sha_start: ${start} scope_sha_end: ${end} reviewed_files_count: 3 verdict: ALLOW findings: ${ordered.map((f) => ` - ${f.id}`).join('\n')} --- # Review: t ## Executive Summary x ## Coverage | File | Treatment | Reason | |------|-----------|--------| | sub/deep/x.mjs | deep-review | default | | inner/y.mjs | deep-review | default | | lnk/z.mjs | deep-review | default | ## Findings (MINOR) ${entries} ## Declined to Judge None. Trimmed: 0 ## Remediation Summary x ## Remediation Steps None. \`\`\`json ${json} \`\`\` `; } const jsonOf = (text) => JSON.parse(text.slice(text.lastIndexOf('```json\n') + 8, text.lastIndexOf('```'))); /** The text from `heading` up to the next `## ` heading ('' when absent). */ function sectionOf(text, heading) { const start = text.indexOf(`\n${heading}`); if (start === -1) return ''; const next = text.indexOf('\n## ', start + heading.length + 1); return text.slice(start + 1, next === -1 ? undefined : next); } test('[CMD] the filter: a finding under the CLAUDE.md keys that cites a file outside the set is set aside under Declined to Judge', () => { const fx = scopeRepo(); const findings = [ { rule_key: 'CLAUDE_MD_DEVIATION', file: 'sub/deep/x.mjs', line: 1, title: 'cites an off-path file', instruction_ref: 'other/CLAUDE.md' }, { rule_key: 'CLAUDE_MD_STALE', file: 'inner/y.mjs', line: 1, title: 'cites no file' }, { rule_key: 'CLAUDE_MD_DEVIATION', file: 'lnk/z.mjs', line: 1, title: 'cites a link out of the repo', instruction_ref: 'lnk/CLAUDE.md' }, { rule_key: 'CLAUDE_MD_STALE', file: 'sub/deep/x.mjs', line: 2, title: 'cites the global file', instruction_ref: join(fx.home, '.claude', 'CLAUDE.md') }, { rule_key: 'CLAUDE_MD_DEVIATION', file: 'sub/deep/x.mjs', line: 3, title: 'cites an on-path file', instruction_ref: 'sub/CLAUDE.md' }, { rule_key: 'MISSING_ERROR_HANDLING', file: 'inner/y.mjs', line: 2, title: 'another key' }, ]; fx.write(cmdReview({ start: fx.start, end: fx.end, findings })); const r = fx.finalize(); assert.equal(r.status, 0, r.stdout + r.stderr); const text = fx.read(); const json = jsonOf(text); const declined = (json.declined_to_judge ?? []).filter((d) => d.reason === 'instruction-out-of-set'); assert.deepEqual(declined.map((d) => [d.title, d.rule_key, d.severity, d.instruction_ref ?? null]).sort(), [ ['cites a link out of the repo', 'CLAUDE_MD_DEVIATION', 'MINOR', 'lnk/CLAUDE.md'], ['cites an off-path file', 'CLAUDE_MD_DEVIATION', 'MINOR', 'other/CLAUDE.md'], ['cites no file', 'CLAUDE_MD_STALE', 'MINOR', null], ['cites the global file', 'CLAUDE_MD_STALE', 'MINOR', join(fx.home, '.claude', 'CLAUDE.md')], ], 'each CLAUDE.md-key finding citing a file outside the set is declined with instruction-out-of-set and the cited file'); assert.deepEqual(json.findings.map((f) => f.title).sort(), ['another key', 'cites an on-path file'], 'the finding that cites a file in the set and the finding under another key stay reported'); assert.deepEqual(json.counts, { BLOCKER: 0, MAJOR: 0, MINOR: 2, SUGGESTION: 0 }, 'counts lose the declined findings'); const section = sectionOf(text, '## Declined to Judge'); for (const d of declined) { assert.ok(section.includes(d.title), `## Declined to Judge lists ${d.title}:\n${section}`); assert.ok(!sectionOf(text, '## Findings (MINOR)').includes(d.title), `${d.title} has no entry under ## Findings (MINOR) any more`); } assert.ok(section.includes('instruction-out-of-set') && section.includes('other/CLAUDE.md'), `the section names the reason and the cited file:\n${section}`); assert.doesNotMatch(section, /^None\.$/m, 'the section no longer says None.'); // Control: the finalized review validates and the gate reads it. const v = fx.validate(); assert.equal(v.status, 0, v.stdout + v.stderr); g(fx.repo, 'checkout', '-q', '--detach', fx.end); const gate = fx.gate(); assert.equal(gate.status, 0, gate.stdout + gate.stderr); }); test('[CMD] a range that deletes a file under a directory holding a tracked CLAUDE.md keeps a finding citing that CLAUDE.md', () => { const base = mkdtempSync(join(tmpdir(), 'claude-md-scope-del-')); made.push(base); const repo = join(base, 'repo'); mkdirSync(repo, { recursive: true }); mkdirSync(join(base, 'hooks-none')); g(repo, 'init', '-q', '-b', 'main'); appendFileSync(join(repo, '.git', 'config'), '[user]\n\tname = Test\n\temail = test@example.invalid\n' + `[commit]\n\tgpgsign = false\n[core]\n\thooksPath = "${join(base, 'hooks-none')}"\n`); appendFileSync(join(repo, '.git', 'info', 'exclude'), '.claude/projects/\n'); putIn(repo, 'CLAUDE.md', '# root\n'); putIn(repo, 'gone/CLAUDE.md', '# gone/old.mjs exports o1\n'); putIn(repo, 'gone/old.mjs', three('o')); putIn(repo, 'src/a.mjs', three('a')); g(repo, 'add', '-A'); g(repo, 'commit', '-q', '-m', 'chore: start'); const start = g(repo, 'rev-parse', 'HEAD'); g(repo, 'rm', '-q', 'gone/old.mjs'); g(repo, 'commit', '-q', '-m', 'refactor: drop gone/old.mjs'); const end = g(repo, 'rev-parse', 'HEAD'); // Control: the Phase 5 command lists the deleted path, so the prompt set holds gone/CLAUDE.md. const list = join(base, 'changed.txt'); writeFileSync(list, g(repo, '-c', 'core.quotePath=false', 'diff', '--name-only', `${start}..${end}`) + '\n'); const set = spawnSync(process.execPath, [SCOPE, '--set', '--repo', repo, '--sha', end, '--changed', list], { encoding: 'utf8', env: childEnv() }); assert.equal(set.status, 0, set.stdout + set.stderr); assert.ok((parse(set).files ?? []).includes('gone/CLAUDE.md'), `Control: the reviewer is told to check gone/CLAUDE.md: ${set.stdout}`); const reviewPath = join(repo, ...REVIEW_REL.split('/')); putIn(repo, REVIEW_REL, cmdReview({ start, end, findings: [ { rule_key: 'CLAUDE_MD_STALE', file: 'gone/CLAUDE.md', line: 1, title: 'names a deleted file', instruction_ref: 'gone/CLAUDE.md' }, ] })); const r = spawnSync(process.execPath, [FINALIZE, '--review', reviewPath, '--repo', repo, '--project', dirname(reviewPath), '--round', '1'], { encoding: 'utf8', env: childEnv() }); assert.equal(r.status, 0, r.stdout + r.stderr); const json = jsonOf(readFileSync(reviewPath, 'utf8')); assert.deepEqual((json.declined_to_judge ?? []).filter((d) => d.reason === 'instruction-out-of-set').map((d) => d.title), [], 'a finding citing gone/CLAUDE.md, which the prompt set holds, is not declined'); assert.deepEqual(json.findings.map((f) => f.title), ['names a deleted file'], 'the finding stays reported'); }); test('[DPR] a changed instruction file at any depth, docs/** included, gets deep review', () => { assert.equal(typeof scopeMod.isInstructionFile, 'function', 'instruction-scope.mjs exports isInstructionFile'); for (const p of ['CLAUDE.md', 'AGENTS.md', 'docs/CLAUDE.md', 'docs/guide/AGENTS.md', 'a/b/c/CLAUDE.md', '.claude/rules/x.md', '.claude/CLAUDE.md']) { assert.equal(scopeMod.isInstructionFile(p), true, `${p} is an instruction file`); } for (const p of ['README.md', 'docs/guide.md', 'claude.md', 'CLAUDE.md.bak', 'xCLAUDE.md', 'docs/CLAUDE.mdx', '.claude/rules/x.txt', '.claude/settings.json', 'tests/fixtures/AGENTS.json']) { assert.equal(scopeMod.isInstructionFile(p), false, `${p} is not an instruction file`); } assert.ok(existsSync(SCOPE), 'lib/review/instruction-scope.mjs exists'); const r = spawnSync(process.execPath, [SCOPE, '--triage', 'docs/CLAUDE.md', 'src/a.mjs', 'docs/guide.md', '.claude/rules/r.md', 'pkg/AGENTS.md'], { encoding: 'utf8', env: childEnv() }); assert.equal(r.status, 0, r.stdout + r.stderr); assert.deepEqual(parse(r).files, ['docs/CLAUDE.md', '.claude/rules/r.md', 'pkg/AGENTS.md'], `--triage lists the instruction files: ${r.stdout}`); });