From 4b7b2d9c48d9ef270fde905f8cb7035e534c9b6d Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Thu, 16 Jul 2026 20:24:12 +0200 Subject: [PATCH] fix(acr): analyze persists agent-returned report (M-BUG-18) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Claude Code subagent harness instructs spawned agents NOT to write report/summary/findings/analysis .md files — the parent reads the final text message. Verified live: analyzer-agent skipped Write entirely and returned the report inline, so analysis-report.md never landed on disk and the plan/interview/status phases would find nothing to read. New contract (orchestrator-writes pattern): analyzer-agent returns the complete report as its final message; the analyze command saves it verbatim to the session directory before presenting the summary. Same class exists in plan/feature-gap/optimize/scanner agent pairs — deliberately left for their own dogfood chunks (plan is judged as-is first per the pipeline sequence). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01CTontYwY5JGS4nL2AuiASy --- agents/analyzer-agent.md | 11 +++- commands/analyze.md | 17 +++-- .../analyze-report-persistence.test.mjs | 62 +++++++++++++++++++ 3 files changed, 83 insertions(+), 7 deletions(-) create mode 100644 tests/commands/analyze-report-persistence.test.mjs diff --git a/agents/analyzer-agent.md b/agents/analyzer-agent.md index 7018314..e406563 100644 --- a/agents/analyzer-agent.md +++ b/agents/analyzer-agent.md @@ -51,11 +51,16 @@ In `--raw` mode, fall back to v5.0.0 severity prefiks and verbatim scanner title 5. **Identify optimizations**: Rules to globalize, missing configs, orphaned files 6. **Security scan**: Aggregate secret warnings, check for insecure patterns 7. **CLAUDE.md quality assessment**: Score each file against rubric, assign letter grades -8. **Generate report**: Write comprehensive markdown report — group findings by `userImpactCategory`, lead with `userActionLanguage` +8. **Generate report**: Compose the comprehensive markdown report — group findings by `userImpactCategory`, lead with `userActionLanguage` ## Output -Write to: `~/.claude/config-audit/sessions/{session-id}/analysis-report.md` +Return the complete report as your final message — do not write it to a file +yourself. The Claude Code subagent harness instructs agents not to write +report/analysis files; your text output IS the deliverable. The orchestrating +command saves your returned report verbatim to +`~/.claude/config-audit/sessions/{session-id}/analysis-report.md` for the +downstream plan/interview/status phases. **Output MUST NOT exceed 300 lines.** Prioritize findings by severity. Use tables, not prose. @@ -183,4 +188,4 @@ Verify report: all findings referenced, recommendations actionable, severity lev - Process findings in memory (typically < 1MB total) - Generate report in single pass -- No file modifications (read-only except report output) +- No file modifications (read-only; the report is returned as your final message) diff --git a/commands/analyze.md b/commands/analyze.md index 438e6f2..6387afd 100644 --- a/commands/analyze.md +++ b/commands/analyze.md @@ -60,12 +60,21 @@ Agent(subagent_type: "config-audit:analyzer-agent") raw severity. The humanizer already replaced jargon-heavy title/description/recommendation strings with plain-language equivalents — render them verbatim, do not paraphrase. - Output to: ~/.claude/config-audit/sessions/{session-id}/analysis-report.md + Return the complete report as your final message. Do not write it + to a file — the orchestrating command saves it to the session directory. ``` -### Step 4: Present summary +### Step 4: Save the report -After the agent completes, read the generated report and show a brief summary: +The agent returns the complete report as its final message — the Claude Code +subagent harness instructs agents not to write report/analysis files themselves, +so the command must persist it. Write the returned report verbatim (no edits, +no truncation) to `~/.claude/config-audit/sessions/{session-id}/analysis-report.md` +using the Write tool. Downstream phases (`plan`, `interview`, `status`) read this file. + +### Step 5: Present summary + +After saving the report, show a brief summary: ```markdown ### Analysis Complete @@ -84,6 +93,6 @@ Full report: `~/.claude/config-audit/sessions/{session-id}/analysis-report.md` - **`/config-audit fix`** — Auto-fix deterministic issues right away ``` -### Step 5: Update state +### Step 6: Update state Update `state.yaml` with `current_phase: "analyze"`, `next_phase: "plan"`. diff --git a/tests/commands/analyze-report-persistence.test.mjs b/tests/commands/analyze-report-persistence.test.mjs new file mode 100644 index 0000000..0d167f2 --- /dev/null +++ b/tests/commands/analyze-report-persistence.test.mjs @@ -0,0 +1,62 @@ +/** + * M-BUG-18 — analysis-report.md persistence contract. + * + * The Claude Code subagent harness instructs spawned agents NOT to write + * report/summary/findings/analysis .md files — the parent reads the agent's + * final text message, not files it creates. The analyzer-agent therefore + * cannot be the one that persists analysis-report.md (verified live: the + * agent skipped Write and returned the report inline). + * + * New contract (orchestrator-writes pattern): + * - analyzer-agent returns the complete report as its final message + * - the analyze command saves that returned report verbatim to + * ~/.claude/config-audit/sessions/{session-id}/analysis-report.md, + * which downstream phases (plan, interview, status) read. + */ + +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 AGENTS_DIR = resolve(__dirname, '..', '..', 'agents'); + +test('analyze.md: agent prompt does not tell the agent to write the report file', async () => { + const content = await readFile(resolve(COMMANDS_DIR, 'analyze.md'), 'utf-8'); + assert.doesNotMatch( + content, + /Output to:.*analysis-report\.md/, + 'the spawn prompt must not instruct the subagent to write analysis-report.md — the harness blocks agent-written report files' + ); +}); + +test('analyze.md: command saves the returned report to analysis-report.md', async () => { + const content = await readFile(resolve(COMMANDS_DIR, 'analyze.md'), 'utf-8'); + assert.match( + content, + /return[s]? the complete report as (its|your) final message/i, + 'analyze.md must state that the agent returns the report inline' + ); + assert.match( + content, + /Write tool[\s\S]{0,200}analysis-report\.md|analysis-report\.md[\s\S]{0,200}Write tool/, + 'analyze.md must instruct the command to persist the returned report to analysis-report.md with the Write tool' + ); +}); + +test('analyzer-agent.md: output contract is return-inline, not self-write', async () => { + const content = await readFile(resolve(AGENTS_DIR, 'analyzer-agent.md'), 'utf-8'); + assert.match( + content, + /return the complete report as your final message/i, + 'analyzer-agent must be told its final message IS the report' + ); + assert.doesNotMatch( + content, + /^Write to: .*analysis-report\.md/m, + 'analyzer-agent must not carry the old self-write output contract' + ); +});