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
109 lines
4.8 KiB
JavaScript
109 lines
4.8 KiB
JavaScript
/**
|
|
* Wave 5 Step 15 — Action-command-template shape tests.
|
|
*
|
|
* Verifies that the 7 action command templates have the correct structural
|
|
* shape after the humanizer integration:
|
|
*
|
|
* - All 7 files: contain a Bash invocation block, reference the Read tool,
|
|
* and contain the `--raw` flag (or the literal `"$ARGUMENTS"` string) so
|
|
* `--raw` plumbing is uniform across the toolchain.
|
|
*
|
|
* - help.md additionally: removes the most obviously technical jargon
|
|
* ("PreToolUse" / "frontmatter" mentions in the user-facing prose) and
|
|
* introduces a plain-language vocabulary table referencing the
|
|
* humanized userImpactCategory and userActionLanguage labels.
|
|
*/
|
|
|
|
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 ACTION_FILES = [
|
|
'fix.md',
|
|
'rollback.md',
|
|
'plan.md',
|
|
'implement.md',
|
|
'cleanup.md',
|
|
'help.md',
|
|
'interview.md',
|
|
];
|
|
|
|
const RAW_OR_ARGUMENTS_REGEX = /--raw|"\$ARGUMENTS"/;
|
|
const BASH_BLOCK_REGEX = /```bash\b/;
|
|
const READ_TOOL_REGEX = /\bRead\s+tool\b|allowed-tools:.*\bRead\b/;
|
|
|
|
async function readCommand(name) {
|
|
return await readFile(resolve(COMMANDS_DIR, name), 'utf-8');
|
|
}
|
|
|
|
// plan.md invokes no scanner — it spawns the planner agent. Its 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` (confirmed at runtime by the planner agent, session #49).
|
|
// Removing that block is the fix, so this assertion skips it.
|
|
const AGENT_DRIVEN = new Set(['plan.md']);
|
|
|
|
test('Action: every scanner-invoking file contains a Bash invocation block', async () => {
|
|
for (const name of ACTION_FILES) {
|
|
if (AGENT_DRIVEN.has(name)) continue;
|
|
const content = await readCommand(name);
|
|
assert.match(content, BASH_BLOCK_REGEX, `${name} missing bash block`);
|
|
}
|
|
});
|
|
|
|
test('Action: 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('Action: every file references the Read tool', async () => {
|
|
for (const name of ACTION_FILES) {
|
|
const content = await readCommand(name);
|
|
assert.match(content, READ_TOOL_REGEX, `${name} missing Read tool reference`);
|
|
}
|
|
});
|
|
|
|
test('Action: every file contains --raw or "$ARGUMENTS" (pass-through plumbing)', async () => {
|
|
for (const name of ACTION_FILES) {
|
|
const content = await readCommand(name);
|
|
assert.match(content, RAW_OR_ARGUMENTS_REGEX, `${name} missing --raw / $ARGUMENTS plumbing`);
|
|
}
|
|
});
|
|
|
|
test('help.md: introduces plain-language vocabulary referencing humanized categories', async () => {
|
|
const content = await readCommand('help.md');
|
|
// At least three of the userImpactCategory labels should appear
|
|
const labels = ['Configuration mistake', 'Conflict', 'Wasted tokens', 'Missed opportunity', 'Dead config'];
|
|
const present = labels.filter(l => content.includes(l));
|
|
assert.ok(present.length >= 3, `help.md must surface ≥3 humanized impact labels; found ${present.length}: ${present.join(', ')}`);
|
|
// At least three of the userActionLanguage phrases should appear
|
|
const actions = ['Fix this now', 'Fix soon', 'Fix when convenient', 'Optional cleanup', 'FYI'];
|
|
const presentActions = actions.filter(a => content.includes(a));
|
|
assert.ok(presentActions.length >= 3, `help.md must surface ≥3 humanized action phrases; found ${presentActions.length}: ${presentActions.join(', ')}`);
|
|
});
|
|
|
|
test('help.md: no bare "PreToolUse" jargon in user-facing copy', async () => {
|
|
const content = await readCommand('help.md');
|
|
// Allow the word in code/quoted contexts but the body table descriptions should not lean on it.
|
|
// Heuristic: no occurrence of "PreToolUse" outside of code spans / quoted blocks.
|
|
// Simple check: no "PreToolUse" anywhere except in any backtick span — since this file is doc-only,
|
|
// require zero occurrences.
|
|
assert.doesNotMatch(content, /\bPreToolUse\b/, 'help.md user copy must not lean on "PreToolUse" jargon — use plain language');
|
|
});
|
|
|
|
test('help.md: no bare "frontmatter" jargon in user-facing copy', async () => {
|
|
const content = await readCommand('help.md');
|
|
assert.doesNotMatch(content, /\bfrontmatter\b/, 'help.md user copy must not lean on "frontmatter" jargon — use plain language ("metadata block at the top of each file")');
|
|
});
|