From 2ec74273f0ab4d09f2da5e59e3bf6d192ea4c98c Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Sun, 18 Oct 2026 16:47:23 +0200 Subject: [PATCH] fix(story-mode): a helper reporting back is heard in its own words MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From review round 9: a background job's page showed only Claude Code's line 'Agent "Helper B: write extra pytest tests" finished', so what the helper said about its own work vanished, here that it had never run its tests against the real wordcount.py. The notice balloon now carries the helper's report like Claude's last words (its first sentence and the key sentences from the rest): '"Helper B: write extra pytest tests" reports: I wrote the file: tests/test_extra.py It has 18 test cases … I haven't run it against the real wordcount.py.' A report that is only a path is its file name, and absolute paths keep their last two parts, so a long path does not push the sentence that matters out of a phone's balloon. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/api-reference.md | 2 +- src/story-mode.ts | 20 +++++++++++++++++++ .../background-helpers-reports.expected.json | 6 +++--- ...ash-script-background-helper.expected.json | 2 +- .../text-plan-dry-run-helper.expected.json | 2 +- test/story-corpus.test.ts | 10 ++++++++++ 6 files changed, 36 insertions(+), 6 deletions(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index 0ab02f61..3dee5372 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -1083,7 +1083,7 @@ A Bash call sent to the background (its result is only `Command running in backg A `page` carries the turn's summary fields plus: - `events`: one per prompt, sentence Claude wrote and tool call. Each has `intent`, `tool`, `label` and, when the call returned, `ok` (`false` when the result was flagged as an error, or reported failed tests or lint problems a pipe kept from the exit code). Depending on the call: `edit` (an Edit/Write, hunks in Claude Code's own `structuredPatch` shape), `edits` (files a Bash call wrote through here-documents or `printf`/`echo` redirects, that a python or node script fed by a here-document opens for writing, or that Claude Code says it changed; the last two have no lines, since what changed is not in the command; an append's hunk has `atEnd: true` and no line numbers, since where it lands is not in the command), the tail of `output`, `tests` (`{ passed, failed, errors? }`, counted on the whole output before it is cut), `helper`, `files` (files a Bash call printed with `cat`/`head`/`tail`, resolved against the command's folder), `headline` (the part of a one-liner that decided its intent), `commits`, `subject` (the first commit's message, when the command spells it out with `-m` or a `git commit -F -` here-document), `ships` (`push` / `pr`), `running`, `background` and `finishedLater` (see Background work), `keyPoints` (for a sentence Claude wrote: up to two sentences from further into a long message that a reader must not lose, such as an open problem, something not yet done or checked, or a word on what you asked for; read on the whole message, past the `label`'s clip, and used by the final panel's balloon), `question` and, for prompts, `origin`. The advisor (a server tool) is an event with tool `Advisor` and intent `helper`; its answer is encrypted in the transcript, so it is never a panel of its own: the panel of the step after it (or the last one, when nothing follows) holds its events and counts them in `secondOpinion`. In plan mode the plan file under `~/.claude/plans/` is planning (intent `plan`, its numbered steps as `todos`), and `ExitPlanMode`'s result carries the plan (`output`) and your answer (`Approved` / `Sent back`). -- `panels`: at most 12, each `{ kind, events, caption: { story, director }, balloon?, files?, notes?, helpers?, todos?, tests?, ok?, rounds?, roundsOk?, question?, live? }`, with `events` as indices into `events`. A run panel's `printed` holds the first lines its last command printed (its last lines when it failed), for the terminal card: the card is drawn on the canvas and goes into a saved image, so lines that look like secrets are left out and addresses and long token-like strings are masked. `balloon.who` is `you`, `claude` or `note` (words that start a turn but are not yours: a background job's report, another session's message). A loop's `roundsOk` lists every test run in order, `true` where it passed and `null` for one still running, so a failure inside a round that passed in the end still shows, and a live loop shows the runs that finished. Kinds: `mission`, `investigate`, `research`, `plan`, `backup`, `fix`, `check`, `loop` (fix and check rounds folded), `run`, `save`, `ask`, `compact`, `montage` (quieter panels folded to fit) and `complete`. +- `panels`: at most 12, each `{ kind, events, caption: { story, director }, balloon?, files?, notes?, helpers?, todos?, tests?, ok?, rounds?, roundsOk?, question?, live? }`, with `events` as indices into `events`. A run panel's `printed` holds the first lines its last command printed (its last lines when it failed), for the terminal card: the card is drawn on the canvas and goes into a saved image, so lines that look like secrets are left out and addresses and long token-like strings are masked. `balloon.who` is `you`, `claude` or `note` (words that start a turn but are not yours: a background job's report, another session's message); a helper's report keeps its own words there (`"" reports: …`, its first sentence and key points, absolute paths shortened to their last two parts). A loop's `roundsOk` lists every test run in order, `true` where it passed and `null` for one still running, so a failure inside a round that passed in the end still shows, and a live loop shows the runs that finished. Kinds: `mission`, `investigate`, `research`, `plan`, `backup`, `fix`, `check`, `loop` (fix and check rounds folded), `run`, `save`, `ask`, `compact`, `montage` (quieter panels folded to fit) and `complete`. - `filesChanged`, `added`, `removed` (summed per edit), `helpers` (the turn's own helper calls; a report turn's prompt carries the helper it answers, which is not counted), `helpersRunning`, `noneWorked` (every tool call of the turn failed, so the last panel is no celebration; a command whose last check only found nothing, exit code 1 after its answer, does not count as failed), `durationMs`, `partial` (the turn began before the part that was read) and `truncated` (older outputs, hunks and helper texts were dropped to keep the page small: a page carries at most 120,000 characters of them, newest first; an edit that lost its lines keeps its counts and an empty `hunks`). ## Git worktrees of a case diff --git a/src/story-mode.ts b/src/story-mode.ts index 5dc6da7a..f43f26c2 100644 --- a/src/story-mode.ts +++ b/src/story-mode.ts @@ -2376,6 +2376,26 @@ function decorate(draft: DraftPanel, events: StoryEvent[], turn: StoryTurn, inde : lines[origin]; // Words that started the turn but are not yours are a note, not your balloon. if (panel.balloon && !command && origin !== 'human') panel.balloon = { ...panel.balloon, who: 'note' }; + // A helper reporting back: its own words, not only the line saying it finished, so what it + // did not do ("I haven't run them against the real file") is on the page. + const report = origin === 'task' && evs[0]?.helper?.result?.trim(); + if (panel.balloon && report) { + // A report that is only a path says which file it wrote: its name is enough. + const said = ( + /^\S+$/.test(report) && report.includes('/') ? basename(report) : closingText(report, keyPointsOf(report)) + ) + // An absolute path is mostly room: its last two parts ("tests/test_extra.py") say which file. + // Run on the clipped balloon (560 characters at most), so the scan stays short. + .replace(/(? { expect(panel).toBeTruthy(); }); + it('a helper reporting back is heard in its own words, not only "finished" (round 9)', () => { + const [, readme, extra] = pagesOf('background-helpers-reports'); + expect(readme.panels[0].balloon).toBe('"Helper A: write README.md" reports: README.md'); + // What the helper did not do is on the page, and a long path does not crowd it out. + expect(extra.panels[0].balloon).toMatch( + /^"Helper B: write extra pytest tests" reports: I wrote the file: tests\/test_extra\.py / + ); + expect(extra.panels[0].balloon).toMatch(/I haven't run it against the real wordcount\.py\.$/); + }); + it('a blocked command ends a turn where nothing worked, without a celebration', () => { const last = pagesOf('teammates-then-interrupt')[4]; expect(last.panels.map((p) => p.story)).toEqual([