mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 10:39:32 +01:00
fix(story-mode): read how current Claude Code builds deliver prompts and end turns
Found by replaying round-2 review transcripts while the reviews ran, then surveyed across 150 local transcripts: - A prompt typed while Claude works writes no user line in current builds, only a queued_command attachment where Claude took it in (602 of them locally, 485 of those background-task reports). It now joins the running turn; a report that lands mid-turn is a notice on the wire, not a new mission panel, and a background helper's answer joins back from it. - Some sessions never write turn_duration (13 of 135 locally): the Stop hook's stop_hook_summary (never preventedContinuation in 815 seen) now ends the turn, timed by its own span. Before, such a session's turns never ended. - A helper started as a teammate (teammate_spawned) is a background helper; its <teammate-message> reports (text or a JSON idle notification) join it by name, and the turn they start reads "readme-writer: README done". - A <cross-session-message> names its sender; pasted text loses Claude Code's <pasted_content> wrapper (it showed in balloons and the picker). - The mission panel, its balloon and its drawer follow who sent each prompt (you, a background job, another session), not who started the turn. - A caption names a script by its first line that says something, not `set -e`, a bare `cd` or `S=/tmp/...`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
03cb3a5961
commit
848566828f
5 files changed
+343
-26
No files matched your search
@@ -358,6 +358,140 @@ describe('buildStoryTurns', () => {
|
||||
expect(second.events[0].output).toBe('I wrote README.md.');
|
||||
});
|
||||
|
||||
it('a prompt typed while Claude works (a queued_command attachment, no user line) joins the turn', () => {
|
||||
const queued = (prompt: string, origin: Record<string, unknown>, mode = 'prompt') => ({
|
||||
type: 'attachment',
|
||||
uuid: 'att-1',
|
||||
timestamp: '2026-10-18T09:41:30.000Z',
|
||||
isSidechain: false,
|
||||
attachment: { type: 'queued_command', prompt, source_uuid: 'src-1', commandMode: mode, origin, humanTurn: true },
|
||||
});
|
||||
const turns = buildStoryTurns(
|
||||
entriesOf([
|
||||
prompt('Build it'),
|
||||
...bash('b1', 'ls', 'List', 'a'),
|
||||
queued('Also add a --csv option.', { kind: 'human' }),
|
||||
...bash('b2', 'pytest -q', 'Run tests', '3 passed in 0.1s'),
|
||||
end(1000),
|
||||
])
|
||||
);
|
||||
expect(turns).toHaveLength(1);
|
||||
const page = buildStoryPage(turns[0]);
|
||||
const added = page.panels.filter((p) => p.kind === 'mission')[1];
|
||||
expect(added.caption.story).toBe('You add something while Claude works.');
|
||||
expect(added.balloon?.text).toBe('Also add a --csv option.');
|
||||
expect(page.events.find((e) => e.id === 'src-1')).toMatchObject({ origin: 'human', actor: 'you' });
|
||||
});
|
||||
|
||||
it("a background helper's report that lands mid-turn is a notice on the wire and joins the helper", () => {
|
||||
const report = [
|
||||
'<task-notification>',
|
||||
'<tool-use-id>a1</tool-use-id>',
|
||||
'<status>completed</status>',
|
||||
'<summary>Agent "Docs" finished</summary>',
|
||||
'<result>README written.</result>',
|
||||
'</task-notification>',
|
||||
].join('\n');
|
||||
const page = pageOf([
|
||||
prompt('docs and tests'),
|
||||
tool('a1', 'Agent', { description: 'Docs', subagent_type: 'general-purpose', prompt: 'Write docs' }),
|
||||
result('a1', 'Async agent launched successfully.', {
|
||||
toolUseResult: { isAsync: true, status: 'async_launched' },
|
||||
}),
|
||||
...bash('b1', 'npm test', 'Run tests', ' Tests 2 passed (2)'),
|
||||
{
|
||||
type: 'attachment',
|
||||
timestamp: '2026-10-18T09:42:00.000Z',
|
||||
attachment: {
|
||||
type: 'queued_command',
|
||||
prompt: report,
|
||||
commandMode: 'task-notification',
|
||||
origin: { kind: 'task-notification', producer: 'session-task' },
|
||||
},
|
||||
},
|
||||
said('Both done.'),
|
||||
end(1000),
|
||||
]);
|
||||
expect(page.panels.filter((p) => p.kind === 'mission')).toHaveLength(1);
|
||||
const notice = page.events.find((e) => e.intent === 'notice')!;
|
||||
expect(notice).toMatchObject({ tool: 'Report', label: 'Agent "Docs" finished', output: 'README written.' });
|
||||
expect(page.toolCalls).toBe(2);
|
||||
expect(page.events[1].helper).toMatchObject({ result: 'README written.' });
|
||||
expect(page.helpersRunning).toBe(0);
|
||||
});
|
||||
|
||||
it('the Stop hook summary ends a turn that never gets turn_duration, timed by its own span', () => {
|
||||
const stop = (prevented = false) => ({
|
||||
type: 'system',
|
||||
subtype: 'stop_hook_summary',
|
||||
preventedContinuation: prevented,
|
||||
timestamp: '2026-10-18T09:45:00.000Z',
|
||||
});
|
||||
const [ended] = buildStoryTurns(entriesOf([prompt('one'), ...bash('b', 'ls', 'List', 'a'), stop()]));
|
||||
expect(ended.ended).toBe(true);
|
||||
expect(ended.durationMs).toBeGreaterThan(200_000); // 09:41:01 to 09:45:00
|
||||
const [kept] = buildStoryTurns(entriesOf([prompt('one'), ...bash('b', 'ls', 'List', 'a'), stop(true)]));
|
||||
expect(kept.ended).toBe(false);
|
||||
});
|
||||
|
||||
it('a teammate helper (teammate_spawned) runs until its teammate message reports back by name', () => {
|
||||
const spawned = [
|
||||
prompt('docs please'),
|
||||
tool('a1', 'Agent', {
|
||||
description: 'Write README',
|
||||
name: 'readme-writer',
|
||||
subagent_type: 'general-purpose',
|
||||
prompt: 'Write it',
|
||||
}),
|
||||
result('a1', 'Spawned successfully.', {
|
||||
toolUseResult: { status: 'teammate_spawned', agentId: 'areadme-writer-1', name: 'readme-writer' },
|
||||
}),
|
||||
end(1000),
|
||||
];
|
||||
expect(pageOf(spawned).events[1].helper).toMatchObject({
|
||||
background: true,
|
||||
running: true,
|
||||
agentId: 'areadme-writer-1',
|
||||
});
|
||||
const message = [
|
||||
'Another Claude session sent a message:',
|
||||
'<teammate-message teammate_id="readme-writer" color="blue" summary="README done">',
|
||||
'README written with examples.',
|
||||
'</teammate-message>',
|
||||
'<teammate-message teammate_id="readme-writer" color="blue">',
|
||||
'{"type":"idle_notification","from":"readme-writer","idleReason":"available","result":"I wrote README.md."}',
|
||||
'</teammate-message>',
|
||||
'',
|
||||
'This came from another Claude session.',
|
||||
].join('\n');
|
||||
const turns = buildStoryTurns(
|
||||
entriesOf([...spawned, prompt(message, { turnOrigin: undefined, promptSource: undefined })])
|
||||
);
|
||||
expect(turns).toHaveLength(2);
|
||||
expect(turns[1]).toMatchObject({ origin: 'task', prompt: 'readme-writer: README done' });
|
||||
const first = buildStoryPage(turns[0]);
|
||||
expect(first.events[1].helper).toMatchObject({ result: 'I wrote README.md.' });
|
||||
expect(first.helpersRunning).toBe(0);
|
||||
});
|
||||
|
||||
it('a message from another session names its sender; pasted text loses its wrapper', () => {
|
||||
const [peer] = buildStoryTurns(
|
||||
entriesOf([
|
||||
prompt(
|
||||
'<cross-session-message from="uds:/x.sock" from-name="planner-2">Rebase first, please.</cross-session-message>',
|
||||
{
|
||||
turnOrigin: undefined,
|
||||
}
|
||||
),
|
||||
])
|
||||
);
|
||||
expect(peer).toMatchObject({ origin: 'peer', prompt: 'planner-2: Rebase first, please.' });
|
||||
const [pasted] = buildStoryTurns(
|
||||
entriesOf([prompt('<pasted_content id="291c">\nDo these steps.\n</pasted_content>')])
|
||||
);
|
||||
expect(pasted.prompt).toBe('Do these steps.');
|
||||
});
|
||||
|
||||
it('an interrupt ends the turn', () => {
|
||||
const turns = buildStoryTurns(
|
||||
entriesOf([prompt('go'), ...bash('b', 'ls', 'List', 'a'), prompt('[Request interrupted by user]')])
|
||||
@@ -433,6 +567,15 @@ describe('buildStoryPage', () => {
|
||||
expect(check?.caption.story).toBe('3 tests fail.');
|
||||
});
|
||||
|
||||
it('a caption names a script by its first line that says something (not set -e, cd or S=...)', () => {
|
||||
const page = pageOf([
|
||||
prompt('commit'),
|
||||
...bash('b', 'set -e\nS=/tmp/x\ncd repo\ngit commit -qm "feat: a"', 'Commit', 'ok'),
|
||||
end(1000),
|
||||
]);
|
||||
expect(page.panels.find((p) => p.kind === 'save')!.caption.director).toBe('git commit -qm "feat: a"');
|
||||
});
|
||||
|
||||
it('a test run that reports failures failed, even when a pipe hid its exit code', () => {
|
||||
const page = pageOf([
|
||||
prompt('test it'),
|
||||
|
||||
Reference in new issue
Block a user