Dogfooding `plan` + `implement` against a throwaway config surfaced one root
defect with many arms: the command templates treat consecutive fenced blocks as
one shell. They are not. Every ```bash fence runs as its own Bash call in its own
process, so a variable set in one block is empty in the next, and `$$` is a
different PID (measured: 21710 vs 22109).
The planner agent confirmed the sharpest arm at runtime, reporting that
`Mode: $RAW_FLAG` "arrived literally unsubstituted" — `--raw` was documented in
three command files while being functionally dead. A machine sweep found the same
root in 20 places across 9 files, well past the two the written fasit predicted:
- `$RAW_FLAG` read from non-shell agent prompts (analyze, plan, implement)
- `$TMPFILE` read across blocks (tokens, manifest, whats-active,
plugin-health) — each command could not read the file it had just written
- `$GLOBAL_FLAG` across blocks (fix)
- `$TODAY` never assigned in any block (campaign), passing
`--reference-date ""` to a write CLI in six places
- three `$$` temp paths handed to the Read tool (fix), which expands neither
All now follow the hardened drift.md pattern: a fixed literal path, or a
re-derivation inside each block that needs it.
Also fixed, all confirmed against ground truth rather than inferred:
- `implement` printed a rollback ID it never captured (the timestamp lived only
inside a command substitution) — the one message a user reads after a bad run
- `plan` reported "No analysis results found" for valid sessions, because Read
was pointed at a glob it cannot expand; now uses Glob and verifies the
analysis report exists before spawning the agent
- five phase commands wrote state.yaml with two of four required fields; since
the agent writes all four, a follow-up write silently deleted the rest
- `implement` promised rollback deletes created files; rollback deliberately
leaves them (M-BUG-26 still open) — the doc, not the engine, was wrong
- `implement` claimed a score delta with no pre-change measurement
- `verifier-agent` was told to write a report it has no tool to write
- dead `Task` tool name in always-loaded rule context; planner-agent template
demonstrated the inline file content its own line 110 forbids
The sweeps land as tests/commands/command-shell-state-shape.test.mjs, verified
red before the fix and proven able to fail by reintroducing the defect. Two
existing tests asserted the old bash-block mechanism rather than the intent and
were updated. Suite 1449/0; frozen v5.0.0 snapshots and all scanner code
untouched.
Not fixed, deliberately: neither command scope-gates its actions to the audit
target. The generated plan included an edit to a real file under ~/.claude,
outside the throwaway target, because the skill/agent scanners are machine-wide.
That is a design change, not a side fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0195udHgCcFegzm7ecKku2Yc
180 lines
7.1 KiB
JavaScript
180 lines
7.1 KiB
JavaScript
/**
|
||
* Wave 5 Step 14 — Group B command-template shape tests.
|
||
*
|
||
* Verifies that the 6 audit/analysis command templates in Group B have the
|
||
* correct structural shape after the humanizer integration:
|
||
*
|
||
* - All 6 files: contain a Bash invocation block, reference the Read tool,
|
||
* and contain the `--raw` flag (or the literal `"$ARGUMENTS"` string).
|
||
*
|
||
* - Findings-rendering files (drift.md, plugin-health.md, config-audit.md,
|
||
* discover.md, analyze.md): reference at least one of
|
||
* `userImpactCategory|userActionLanguage|relevanceContext`, and do NOT
|
||
* contain hardcoded grade-prose tables of the form `[ABCDF]\s+grade\s+is`.
|
||
*
|
||
* - status.md: phase-label table is present, the machine field name
|
||
* `current_phase` is preserved (machine contract), and at least one
|
||
* humanized phase label appears ("Looking at your config files",
|
||
* "Working out what to recommend", "Putting together your action plan",
|
||
* "Making the changes", "Double-checking everything worked").
|
||
*
|
||
* - Anchor must-contains from plan line 575–579:
|
||
* - config-audit.md: contains userImpactCategory|userActionLanguage
|
||
* - drift.md: contains --raw OR humanized
|
||
*/
|
||
|
||
import { test } from 'node:test';
|
||
import { strict as assert } from 'node:assert';
|
||
import { readFile } from 'node:fs/promises';
|
||
import { resolve, dirname } from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||
const COMMANDS_DIR = resolve(__dirname, '..', '..', 'commands');
|
||
|
||
const GROUP_B_FILES = [
|
||
'drift.md',
|
||
'plugin-health.md',
|
||
'config-audit.md',
|
||
'discover.md',
|
||
'analyze.md',
|
||
'status.md',
|
||
];
|
||
|
||
const FINDINGS_RENDERING_FILES = [
|
||
'drift.md',
|
||
'plugin-health.md',
|
||
'config-audit.md',
|
||
'discover.md',
|
||
'analyze.md',
|
||
];
|
||
|
||
const HUMANIZED_FIELD_REGEX = /userImpactCategory|userActionLanguage|relevanceContext/;
|
||
const RAW_OR_ARGUMENTS_REGEX = /--raw|"\$ARGUMENTS"/;
|
||
const HARDCODED_GRADE_PROSE_REGEX = /[ABCDF]\s+grade\s+is/;
|
||
const BASH_BLOCK_REGEX = /```bash\b/;
|
||
const READ_TOOL_REGEX = /\bRead\s+tool\b|allowed-tools:.*\bRead\b/;
|
||
|
||
const HUMANIZED_PHASE_LABELS = [
|
||
'Looking at your config files',
|
||
'Working out what to recommend',
|
||
'Asking what you',
|
||
'Putting together your action plan',
|
||
'Making the changes',
|
||
'Double-checking everything worked',
|
||
];
|
||
|
||
async function readCommand(name) {
|
||
return await readFile(resolve(COMMANDS_DIR, name), 'utf-8');
|
||
}
|
||
|
||
// Agent-driven commands invoke no scanner, so they have no bash block to
|
||
// assert. analyze.md's only bash block used to be a `RAW_FLAG=` assignment
|
||
// referenced from the agent prompt below it — a prompt is not a shell, so the
|
||
// agent received the literal string `$RAW_FLAG` (session #49). Removing that
|
||
// block is the fix; requiring one here would re-assert the defect.
|
||
const AGENT_DRIVEN = new Set(['analyze.md']);
|
||
|
||
test('Group B: every scanner-invoking file contains a Bash invocation block', async () => {
|
||
for (const name of GROUP_B_FILES) {
|
||
if (AGENT_DRIVEN.has(name)) continue;
|
||
const content = await readCommand(name);
|
||
assert.match(content, BASH_BLOCK_REGEX, `${name} missing bash block`);
|
||
}
|
||
});
|
||
|
||
test('Group B: agent-driven files spawn an Agent instead of a scanner', async () => {
|
||
for (const name of AGENT_DRIVEN) {
|
||
const content = await readCommand(name);
|
||
assert.match(content, /Agent\(subagent_type:/, `${name} should spawn an Agent`);
|
||
assert.doesNotMatch(
|
||
content,
|
||
/RAW_FLAG=/,
|
||
`${name} must not assign a shell variable it then references from an agent prompt`,
|
||
);
|
||
}
|
||
});
|
||
|
||
test('Group B: every file references the Read tool', async () => {
|
||
for (const name of GROUP_B_FILES) {
|
||
const content = await readCommand(name);
|
||
assert.match(content, READ_TOOL_REGEX, `${name} missing Read tool reference`);
|
||
}
|
||
});
|
||
|
||
test('Group B: every file contains --raw or "$ARGUMENTS" (pass-through plumbing)', async () => {
|
||
for (const name of GROUP_B_FILES) {
|
||
const content = await readCommand(name);
|
||
assert.match(content, RAW_OR_ARGUMENTS_REGEX, `${name} missing --raw / $ARGUMENTS plumbing`);
|
||
}
|
||
});
|
||
|
||
test('Group B findings-renderers: reference at least one humanized field', async () => {
|
||
for (const name of FINDINGS_RENDERING_FILES) {
|
||
const content = await readCommand(name);
|
||
assert.match(
|
||
content,
|
||
HUMANIZED_FIELD_REGEX,
|
||
`${name} must reference userImpactCategory, userActionLanguage, or relevanceContext`,
|
||
);
|
||
}
|
||
});
|
||
|
||
test('Group B findings-renderers: no hardcoded grade-prose tables', async () => {
|
||
for (const name of FINDINGS_RENDERING_FILES) {
|
||
const content = await readCommand(name);
|
||
assert.doesNotMatch(
|
||
content,
|
||
HARDCODED_GRADE_PROSE_REGEX,
|
||
`${name} contains a hardcoded "[grade] grade is..." prose table — humanizer owns grade vocabulary now`,
|
||
);
|
||
}
|
||
});
|
||
|
||
test('Group B anchor: config-audit.md references userImpactCategory|userActionLanguage', async () => {
|
||
const content = await readCommand('config-audit.md');
|
||
assert.match(content, /userImpactCategory|userActionLanguage/);
|
||
});
|
||
|
||
test('Group B anchor: drift.md references --raw or humanized', async () => {
|
||
const content = await readCommand('drift.md');
|
||
assert.match(content, /--raw|humanized/);
|
||
});
|
||
|
||
test('status.md: preserves current_phase machine field and adds humanized phase labels', async () => {
|
||
const content = await readCommand('status.md');
|
||
// Machine contract preserved
|
||
assert.match(content, /\bcurrent_phase\b/, 'status.md must keep current_phase as machine field');
|
||
// At least 3 of the 6 humanized phase labels appear
|
||
const present = HUMANIZED_PHASE_LABELS.filter(label => content.includes(label));
|
||
assert.ok(
|
||
present.length >= 3,
|
||
`status.md must include at least 3 humanized phase labels; found ${present.length}: ${present.join(', ')}`,
|
||
);
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Økt #46 — ux-rules rule 2 for the plugin-health scanner.
|
||
//
|
||
// plugin-health.md passed the humanized-field assertion above while the data it
|
||
// names was unreachable: the scanner had no --output-file, and its default-mode
|
||
// report went to stderr, which the command discards with `2>/dev/null`. A .md
|
||
// contract test that only greps for prose cannot catch that — these assert the
|
||
// plumbing that makes the prose true.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
test('plugin-health.md invokes the scanner with --output-file (ux-rules rule 2)', async () => {
|
||
const content = await readCommand('plugin-health.md');
|
||
const call = content.split('\n').find(l => l.includes('plugin-health-scanner.mjs'));
|
||
assert.ok(call, 'plugin-health.md must invoke plugin-health-scanner.mjs');
|
||
assert.match(call, /--output-file/, 'scanner call must write to a file, not stdout/stderr');
|
||
});
|
||
|
||
test('posture.md invokes the plugin-health and drift scanners with --output-file', async () => {
|
||
const content = await readCommand('posture.md');
|
||
for (const scanner of ['plugin-health-scanner.mjs', 'drift-cli.mjs']) {
|
||
const call = content.split('\n').find(l => l.includes(`scanners/${scanner}`));
|
||
assert.ok(call, `posture.md must invoke ${scanner}`);
|
||
assert.match(call, /--output-file/, `${scanner} call in posture.md discards its output`);
|
||
}
|
||
});
|