fix(story-mode): whose idea a failure was, new repositories everywhere, honest counts (review round 10)

From the person who asked and a fact-checker, reading fresh pages:
- "as planned" for failing first runs the prompt asked for ("it should
  fail because mode is missing", "run them to watch them fail"): those
  read "The first run fails on purpose, as you asked".
- A repository Claude started was told only on a save panel; the first
  save inside a loop or a test run says it too ("Claude starts a git
  repository and saves the work in 4 commits along the way.").
- `git push && git describe --tags; echo "exit=$?"` exits 0, so a
  failure after the push went unsaid; what it printed after the push
  (`fatal:`, `exit=128`) now counts. The commit subject stays on that
  caption too.
- Tool-call counts no longer include second opinions.
- "a layout check" is "a formatting check"; "Turn over" (a game word)
  is "Not done".
- "the last check finds nothing" told nothing: it now says what came
  back empty ("the last one finds a git setting that is not set, which is
  no error").

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-18 17:16:03 +02:00
1 parent 05dc6fbd95
commit 30b907860d
15 files changed
+161 -78

No files matched your search

@@ -5,7 +5,7 @@
"complete": true,
"interrupted": false,
"durationMs": 121460,
"toolCalls": 14,
"toolCalls": 13,
"filesChanged": 2,
"added": 96,
"removed": 3,
@@ -89,7 +89,7 @@
"kind": "complete",
"ok": null,
"story": "Claude's part is done in 2 minutes. Its helper finished later and reported back.",
"director": "turn_duration · 2 min 1 s · 14 tool calls · 2 files changed · 1 helper",
"director": "turn_duration · 2 min 1 s · 13 tool calls · 2 files changed · 1 helper",
"balloon": "All the script tests pass.",
"tests": null,
"roundsOk": null,
@@ -5,7 +5,7 @@
"complete": true,
"interrupted": false,
"durationMs": 72474,
"toolCalls": 7,
"toolCalls": 6,
"filesChanged": 2,
"added": 37,
"removed": 0,
@@ -77,7 +77,7 @@
"kind": "complete",
"ok": null,
"story": "Done in 1 minute.",
"director": "turn_duration · 1 min 12 s · 7 tool calls · 2 files changed",
"director": "turn_duration · 1 min 12 s · 6 tool calls · 2 files changed",
"balloon": "I wrote clamp and its five tests, and all five pass. I didn't commit CLAUDE.md, .claude/ or .codeman-agent-case.json.",
"tests": null,
"roundsOk": null,
@@ -143,7 +143,7 @@
"complete": true,
"interrupted": false,
"durationMs": 66973,
"toolCalls": 10,
"toolCalls": 9,
"filesChanged": 2,
"added": 31,
"removed": 1,
@@ -237,7 +237,7 @@
"kind": "complete",
"ok": null,
"story": "Done in 1 minute.",
"director": "turn_duration · 1 min 7 s · 10 tool calls · 2 files changed",
"director": "turn_duration · 1 min 7 s · 9 tool calls · 2 files changed",
"balloon": "I added wrap() as planned, with 3 new tests. All 8 tests pass and ruff reports no problems.",
"tests": null,
"roundsOk": null,
@@ -5,7 +5,7 @@
"complete": true,
"interrupted": false,
"durationMs": 130001,
"toolCalls": 12,
"toolCalls": 10,
"filesChanged": 3,
"added": 81,
"removed": 0,
@@ -125,7 +125,7 @@
"kind": "complete",
"ok": null,
"story": "Done in 2 minutes.",
"director": "turn_duration · 2 min 10 s · 12 tool calls · 3 files changed",
"director": "turn_duration · 2 min 10 s · 10 tool calls · 3 files changed",
"balloon": "I built it test-first: the suite failed at collection before calc_stats.py existed, then passed once the module was written. I didn't push, didn't touch origin.git/ and didn't create a README, since none existed to keep in sync.",
"tests": null,
"roundsOk": null,
@@ -164,7 +164,7 @@
{
"kind": "loop",
"ok": true,
"story": "Test first: the first run fails as planned, the second passes.",
"story": "The first run fails on purpose, as you asked; the second passes.",
"director": "2 checks · 3 edits · +40 −1 · last: ok",
"balloon": "Green: 18 passed and ruff is clean.",
"tests": {
@@ -181,7 +181,7 @@
{
"kind": "save",
"ok": true,
"story": "Claude saves the work: \"add calc_stats.mode with smallest-value tie-breaking\", then copies it to origin.git, a folder on this computer.",
"story": "Claude saves the work: \"add calc_stats.mode with smallest-value tie-breaking\" and copies it to origin.git, a folder on this computer, but a check after that fails.",
"director": "git commit -q -F - · git push -u origin HEAD",
"balloon": null,
"tests": null,
@@ -268,7 +268,7 @@
{
"kind": "save",
"ok": true,
"story": "Claude saves the work and copies it to origin.git, a folder on this computer, but a check after that fails.",
"story": "Claude saves the work: \"add NOTES.md describing mode behavior\" and copies it to origin.git, a folder on this computer, but a check after that fails.",
"director": "git commit -q -F - · git push · 1 error",
"balloon": null,
"tests": null,
@@ -5,7 +5,7 @@
"complete": true,
"interrupted": false,
"durationMs": 83905,
"toolCalls": 10,
"toolCalls": 9,
"filesChanged": 2,
"added": 35,
"removed": 1,
@@ -83,7 +83,7 @@
"kind": "complete",
"ok": null,
"story": "Done in 1 minute.",
"director": "turn_duration · 1 min 24 s · 10 tool calls · 2 files changed",
"director": "turn_duration · 1 min 24 s · 9 tool calls · 2 files changed",
"balloon": "slugify.mjs and its test file are written, all 4 tests pass, and both files are committed. Set git config user.name if you want a different name on future commits.",
"tests": null,
"roundsOk": null,
@@ -5,7 +5,7 @@
"complete": true,
"interrupted": false,
"durationMs": 385054,
"toolCalls": 26,
"toolCalls": 24,
"filesChanged": 13,
"added": 798,
"removed": 0,
@@ -77,7 +77,7 @@
{
"kind": "loop",
"ok": true,
"story": "Build and test, 4 rounds. The tests pass every time; a layout check fails once, then passes.",
"story": "Build and test, 4 rounds. The tests pass every time; a formatting check fails once, then passes.",
"director": "5 checks · 10 edits · +694 · last: ok",
"balloon": "All 4 modules are done: 63 tests pass and ruff is clean.",
"tests": {
@@ -149,7 +149,7 @@
"kind": "complete",
"ok": null,
"story": "Done in 6 minutes.",
"director": "turn_duration · 6 min 25 s · 26 tool calls · 13 files changed",
"director": "turn_duration · 6 min 25 s · 24 tool calls · 13 files changed",
"balloon": "The inventory/ package is built and committed. Decisions you may want to change: I committed CLAUDE.md. .claude/settings.local.json, .codeman-agent-case.json and the default inventory.json are gitignored. --csv is part of the CLI commit rather than its own commit, because you asked for it before that module existed. Known quirks: remove pear prints the name as you typed it, not as it was stored (\"Pear\"). Because of how the temporary file is created, the saved JSON file is readable only by its owner (permissions 0600).",
"tests": null,
"roundsOk": null,
@@ -5,7 +5,7 @@
"complete": true,
"interrupted": false,
"durationMs": 192853,
"toolCalls": 13,
"toolCalls": 11,
"filesChanged": 3,
"added": 220,
"removed": 1,
@@ -40,7 +40,7 @@
{
"kind": "loop",
"ok": true,
"story": "Fix and test, 2 rounds: a test fails and Claude fixes it, then a layout check fails and Claude fixes that; everything passes. Claude saves the work in 4 commits along the way.",
"story": "Fix and test, 2 rounds: a test fails and Claude fixes it, then a formatting check fails and Claude fixes that; everything passes. Claude starts a git repository and saves the work in 4 commits along the way.",
"director": "3 checks · 5 edits · +218 −1 · last: ok · after 1 second opinion",
"balloon": "All 19 tests pass, ruff check is clean, and the output matches real wc column for column.",
"tests": {
@@ -85,7 +85,7 @@
"kind": "complete",
"ok": null,
"story": "Done in 3 minutes.",
"director": "turn_duration · 3 min 13 s · 13 tool calls · 3 files changed",
"director": "turn_duration · 3 min 13 s · 11 tool calls · 3 files changed",
"balloon": "I built wcount.py and test_wcount.py. All 19 tests pass (19 passed in 0.46s) and ruff check . is clean. What failed and how I fixed it: one test failed on the first run.",
"tests": null,
"roundsOk": null,
@@ -5,7 +5,7 @@
"complete": true,
"interrupted": false,
"durationMs": 297209,
"toolCalls": 20,
"toolCalls": 18,
"filesChanged": 6,
"added": 221,
"removed": 2,
@@ -51,7 +51,7 @@
{
"kind": "loop",
"ok": true,
"story": "Test first: the first run fails as planned, the second passes.",
"story": "The first run fails on purpose, as you asked; the second passes.",
"director": "2 checks · 4 edits · +151 −1 · last: ok · after 1 second opinion",
"balloon": "All 19 test cases pass (13 test functions), and ruff check and ruff format both come back clean.",
"tests": {
@@ -94,7 +94,7 @@
{
"kind": "save",
"ok": true,
"story": "Claude starts a git repository, saves the work and copies it to remote.git, a folder on this computer, but a check after that fails.",
"story": "Claude starts a git repository, saves the work: \"add kebab CLI that turns text into URL slugs\" and copies it to remote.git, a folder on this computer, but a check after that fails.",
"director": "git commit -q -F - · git push -u origin main · 1 error",
"balloon": null,
"tests": null,
@@ -120,7 +120,7 @@
"kind": "complete",
"ok": null,
"story": "Done in 5 minutes.",
"director": "turn_duration · 4 min 57 s · 20 tool calls · 6 files changed",
"director": "turn_duration · 4 min 57 s · 18 tool calls · 6 files changed",
"balloon": "I've built the kebab slug tool in this folder, committed it, and pushed it to the new bare repo remote.git. If you want a different author, run git commit --amend --reset-author and push again. Not tested: the README says pip install . gives you a kebab command.",
"tests": null,
"roundsOk": null,
@@ -5,7 +5,7 @@
"complete": true,
"interrupted": false,
"durationMs": 103688,
"toolCalls": 8,
"toolCalls": 7,
"filesChanged": 3,
"added": 73,
"removed": 0,
@@ -88,7 +88,7 @@
"kind": "complete",
"ok": null,
"story": "Claude's part is done in 2 minutes. Its 2 helpers finished later and reported back.",
"director": "stop_hook_summary · 1 min 44 s · 8 tool calls · 3 files changed · 2 helpers",
"director": "stop_hook_summary · 1 min 44 s · 7 tool calls · 3 files changed · 2 helpers",
"balloon": "I've set up the project, started both helpers, and the tests pass: node --test index.test.mjs ran 8 tests, 8 passed, 0 failed. No linter is set up in this project, so I didn't run one.",
"tests": null,
"roundsOk": null,
@@ -230,7 +230,7 @@
"complete": false,
"interrupted": true,
"durationMs": 161073,
"toolCalls": 5,
"toolCalls": 4,
"filesChanged": 4,
"added": 148,
"removed": 0,
@@ -305,7 +305,7 @@
"kind": "complete",
"ok": null,
"story": "You stop Claude here, during its step 3 (new tests under test/). Saved before the stop: 2 commits.",
"director": "interrupted · 2 min 41 s · 5 tool calls · 4 files changed",
"director": "interrupted · 2 min 41 s · 4 tool calls · 4 files changed",
"balloon": "Step 3: new tests under test/.",
"tests": null,
"roundsOk": null,
@@ -5,7 +5,7 @@
"complete": true,
"interrupted": false,
"durationMs": 163647,
"toolCalls": 11,
"toolCalls": 10,
"filesChanged": 5,
"added": 255,
"removed": 0,
@@ -62,7 +62,7 @@
{
"kind": "check",
"ok": true,
"story": "Claude writes test_csvstat.py and pyproject.toml; the tests pass, and Claude saves the work.",
"story": "Claude writes test_csvstat.py and pyproject.toml; the tests pass, and Claude starts a git repository and saves the work.",
"director": "python3 -m pytest -q 2>&1 → ok ×2 · 17 passed · writes 2 files · 1 commit",
"balloon": "Code and tests are committed.",
"tests": {
@@ -103,7 +103,7 @@
"kind": "complete",
"ok": null,
"story": "Claude's part is done in 3 minutes. Its helper finished later and reported back.",
"director": "turn_duration · 2 min 44 s · 11 tool calls · 5 files changed · 1 helper",
"director": "turn_duration · 2 min 44 s · 10 tool calls · 5 files changed · 1 helper",
"balloon": "I've built csvstat.py, a command-line tool that prints a CSV's row count and the min, max and mean of each column. Its 17 pytest tests pass, the linter (ruff) is clean, and the dry run on a sample CSV worked. A background helper is writing README.md; I didn't wait for it, so it isn't confirmed yet. Problem: numbers are shown to only 6 significant digits. Once it has finished, the fix is a one-line formatting change and an update to the README example; say if you want it.",
"tests": null,
"roundsOk": null,
@@ -5,7 +5,7 @@
"complete": true,
"interrupted": false,
"durationMs": 159207,
"toolCalls": 10,
"toolCalls": 8,
"filesChanged": 3,
"added": 236,
"removed": 0,
@@ -40,7 +40,7 @@
{
"kind": "run",
"ok": null,
"story": "Claude checks a few things; the last check finds nothing.",
"story": "Claude looks a few things up; the last one finds a git setting that is not set, which is no error.",
"director": "Bash · grep -oE '\"(permissions|env|allow|deny)\"|[A-Za-z_]*(TOKEN|K… · 1 error",
"balloon": null,
"tests": null,
@@ -91,7 +91,7 @@
"kind": "complete",
"ok": null,
"story": "Done in 3 minutes.",
"director": "turn_duration · 2 min 39 s · 10 tool calls · 3 files changed",
"director": "turn_duration · 2 min 39 s · 8 tool calls · 3 files changed",
"balloon": "I built the pomodoro timer. All 7 tests pass under node --test, and everything is committed on main as c4e7b0b feat: add pomodoro timer with tests. Opening the page: it won't work if you double-click index.html. Serve the folder instead, for example with python3 -m http.server in the folder, then open http://localhost:8000. I didn't open it in a browser; I only checked that its script is valid.",
"tests": null,
"roundsOk": null,
+8 -6
View File
@@ -77,7 +77,7 @@ describe('Story Mode on real sessions', () => {
expect(panel('python-cli-fix-loop', 0, 'loop')).toMatchObject({
// The failing test was a real bug, the second failure only ruff: said apart, with the 4 commits made inside.
story:
'Fix and test, 2 rounds: a test fails and Claude fixes it, then a layout check fails and Claude fixes that; everything passes. Claude saves the work in 4 commits along the way.',
'Fix and test, 2 rounds: a test fails and Claude fixes it, then a formatting check fails and Claude fixes that; everything passes. Claude starts a git repository and saves the work in 4 commits along the way.',
roundsOk: [false, false, true],
});
});
@@ -107,7 +107,7 @@ describe('Story Mode on real sessions', () => {
it('a commit at the end of a test one-liner is kept, and a background helper reports back in 9 s', () => {
expect(panel('text-plan-dry-run-helper', 0, 'check')?.story).toBe(
'Claude writes test_csvstat.py and pyproject.toml; the tests pass, and Claude saves the work.'
'Claude writes test_csvstat.py and pyproject.toml; the tests pass, and Claude starts a git repository and saves the work.'
);
expect(panel('text-plan-dry-run-helper', 0, 'plan')?.story).toBe('Claude writes a plan: 4 steps.');
expect(pagesOf('text-plan-dry-run-helper')[1]).toMatchObject({ origin: 'task', durationMs: 9008 });
@@ -127,7 +127,7 @@ describe('Story Mode on real sessions', () => {
// One commit, then a push in a command of its own: one commit, not two.
// The folder had no repository: the first save starts one, and the reader is told.
story:
'Claude starts a git repository, saves the work and copies it to remote.git, a folder on this computer, but a check after that fails.',
'Claude starts a git repository, saves the work: "add kebab CLI that turns text into URL slugs" and copies it to remote.git, a folder on this computer, but a check after that fails.',
});
});
@@ -135,14 +135,16 @@ describe('Story Mode on real sessions', () => {
const [first, second, third] = pagesOf('errors-push-append');
expect(first.panels.find((p) => p.kind === 'check')?.story).toBe("The tests don't even load, as you asked.");
expect(second.panels.find((p) => p.kind === 'loop')).toMatchObject({
story: 'Test first: the first run fails as planned, the second passes.',
// The prompt asked for it ("it should fail because mode is missing"): the user's idea, not Claude's.
story: 'The first run fails on purpose, as you asked; the second passes.',
roundsOk: [false, true],
});
expect(second.panels.find((p) => p.kind === 'save')?.story).toBe(
'Claude saves the work: "add calc_stats.mode with smallest-value tie-breaking", then copies it to origin.git, a folder on this computer.'
// `git describe --tags` failed after the push; `; echo "exit=$?"` hid it from the exit code, not from the output.
'Claude saves the work: "add calc_stats.mode with smallest-value tie-breaking" and copies it to origin.git, a folder on this computer, but a check after that fails.'
);
expect(third.panels.find((p) => p.kind === 'save')?.story).toBe(
'Claude saves the work and copies it to origin.git, a folder on this computer, but a check after that fails.'
'Claude saves the work: "add NOTES.md describing mode behavior" and copies it to origin.git, a folder on this computer, but a check after that fails.'
);
});
+52 -3
View File
@@ -994,10 +994,13 @@ describe('buildStoryPage', () => {
expect(pageFor('cat .env.example | wc -l; git config user.name', 'Exit code 1\n4').noneWorked).toBeUndefined();
expect(pageFor('./deploy.sh; which deploy', 'Exit code 2\nno target').noneWorked).toBe(true);
expect(run('cat .env.example | wc -l; git config user.name', 'Exit code 1\n4')).toBe(
'Claude checks a few things; the last check finds nothing.'
'Claude looks a few things up; the last one finds a git setting that is not set, which is no error.'
);
expect(run('node scripts/count.js && grep -c TODO src/a.js', 'Exit code 1\n12 files\n0')).toBe(
'Claude checks a few things; the last check finds nothing.'
'Claude looks a few things up; the last search finds no match, which is no error.'
);
expect(run('node a.js; which ffmpeg', 'Exit code 1\nok')).toBe(
'Claude looks a few things up; the last one finds a program that is not installed, which is no error.'
);
// A real failure, or one with nothing printed before it, still fails.
expect(run('./deploy.sh; which deploy', 'Exit code 2\nno target')).toBe('Claude tries a command, and it fails.');
@@ -1207,6 +1210,52 @@ describe('buildStoryPage', () => {
expect(keyKindOf('Decisions you may want to change: I committed CLAUDE.md.')).toBe('offer');
});
it('a failure after the push that `; echo` hid from the exit code still shows; a second opinion is no tool call', () => {
const page = pageOf([
prompt('ship it'),
...bash('b1', 'git commit -qm "add a"', 'Commit', ''),
...bash(
'b2',
'git push -u origin HEAD && git describe --tags; echo "exit=$?"',
'Push',
'To ./origin.git\n * [new branch] HEAD -> master\nfatal: No names found, cannot describe anything.\nexit=128'
),
{
type: 'assistant',
timestamp: '2026-10-18T09:41:20.000Z',
message: {
content: [
{ type: 'server_tool_use', id: 'srv1', name: 'advisor', input: {} },
{ type: 'advisor_tool_result', tool_use_id: 'srv1', content: { type: 'advisor_redacted_result' } },
],
},
},
end(1000),
]);
expect(page.panels.find((p) => p.kind === 'save')!.caption.story).toBe(
'Claude saves the work: "add a" and copies it to origin.git, a folder on this computer, but a check after that fails.'
);
expect(page.toolCalls).toBe(2);
});
it('a failing first run the prompt asked for, in its own words, is "as you asked"', () => {
const loop = (ask: string) =>
pageOf([
prompt(ask),
...edit('e1', '/w/test_a.py', ['+def test_a(): assert f()']),
...bash('b1', 'pytest -q', 'Test', '1 failed in 0.1s', true),
...edit('e2', '/w/a.py', ['+def f(): return True']),
...bash('b2', 'pytest -q', 'Test', '1 passed in 0.1s'),
end(1000),
]).panels.find((p) => p.kind === 'loop')!.caption.story;
for (const ask of [
'Add mode(); run the tests, it should fail because mode is missing.',
'Write the tests and run them to watch them fail.',
])
expect(loop(ask), ask).toBe('The first run fails on purpose, as you asked; the second passes.');
expect(loop('Add mode() with tests.')).not.toMatch(/as you asked/);
});
it('a check after a push is named a check, and tests in a fresh copy say so', () => {
const page = pageOf([
prompt('ship it'),
@@ -1390,7 +1439,7 @@ describe('buildStoryPage', () => {
const loop = page.panels.find((p) => p.kind === 'loop')!;
expect(loop.roundsOk).toEqual([true, false, true]);
expect(loop.caption.story).toBe(
'Build and test, 2 rounds. The tests pass every time; a layout check fails once, then passes.'
'Build and test, 2 rounds. The tests pass every time; a formatting check fails once, then passes.'
);
});