From ed6c89ff98fe2fc2006a993d94ed6b6ed8f731bd Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Sun, 18 Oct 2026 18:54:14 +0200 Subject: [PATCH] fix(story-mode): a failed check that Claude fixes in its next command says so MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flagged by four readers across rounds 10 to 12: after a push, "but a check after that fails" never said that Claude repaired what the check found (the new remote pointed at a branch with no commits) and checked again. When Claude's next command runs that same check again after a change (`git -C remote.git symbolic-ref HEAD refs/heads/main && git -C remote.git log …`) and passes, the caption adds "; Claude fixes it, and the check passes next." A check re-run with no change before it, or one that fails again, claims nothing. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/story-mode.ts | 15 ++++++++++- .../question-tests-first-push.expected.json | 2 +- test/story-corpus.test.ts | 2 +- test/story-mode.test.ts | 25 +++++++++++++++++++ 4 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/story-mode.ts b/src/story-mode.ts index e9224a60..81b43b8f 100644 --- a/src/story-mode.ts +++ b/src/story-mode.ts @@ -2953,13 +2953,26 @@ function decorate(draft: DraftPanel, events: StoryEvent[], turn: StoryTurn, inde ); // `git describe` with no tags: a lookup of a version name, not a check of the work. const noTags = /No names found, cannot describe anything|No tags can describe/.test(out); + // The failed check is run again in Claude's next command, after a change, and passes: it was fixed + // (`git -C remote.git symbolic-ref HEAD refs/heads/main && git -C remote.git log …`). + const lookSegs = after.filter((seg) => !/^(echo|printf)\b/.test(seg)); + const nextRun = events.slice(Math.max(...draft.events) + 1).find((e) => e.tool === 'Bash'); + let fixedNext = false; + if (looksOnly && !noTags && nextRun?.ok === true) { + const segs = nextRun.label + .split(/&&|\|\||;|\n/) + .map((seg) => seg.trim()) + .filter(Boolean); + const at = segs.findIndex((seg) => lookSegs.includes(seg)); + fixedNext = at > 0 && segs.slice(0, at).some((seg) => !GIT_LOOK.test(seg) && !/^(echo|printf|cd)\b/.test(seg)); + } const stepAfter = `${ noTags ? 'the version-tag lookup after that fails (there are no tags yet)' : looksOnly ? 'a check after that fails' : 'a step after that fails' - }${allowed ? ', as you said it might' : ''}`; + }${allowed ? ', as you said it might' : ''}${fixedNext ? '; Claude fixes it, and the check passes next' : ''}`; story = savedAnyway || hiddenFail ? uploadOnly diff --git a/test/fixtures/story-corpus/question-tests-first-push.expected.json b/test/fixtures/story-corpus/question-tests-first-push.expected.json index de05ea4c..baae4ef2 100644 --- a/test/fixtures/story-corpus/question-tests-first-push.expected.json +++ b/test/fixtures/story-corpus/question-tests-first-push.expected.json @@ -100,7 +100,7 @@ { "kind": "save", "ok": true, - "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.", + "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; Claude fixes it, and the check passes next.", "unaskedSave": false, "director": "git commit -q -F - · git push -u origin main · 1 error", "balloon": null, diff --git a/test/story-corpus.test.ts b/test/story-corpus.test.ts index e835c3e7..b8ecf08f 100644 --- a/test/story-corpus.test.ts +++ b/test/story-corpus.test.ts @@ -129,7 +129,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: "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.', + '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; Claude fixes it, and the check passes next.', }); }); diff --git a/test/story-mode.test.ts b/test/story-mode.test.ts index 482636eb..e0bb3401 100644 --- a/test/story-mode.test.ts +++ b/test/story-mode.test.ts @@ -1373,6 +1373,31 @@ describe('buildStoryPage', () => { expect(loop('Add mode() with tests.')).not.toMatch(/as you asked/); }); + it('a check that failed after a push and passes in the next command after a change reads as fixed', () => { + const save = (next: string, nextOk = true) => + pageOf([ + prompt('ship it'), + ...bash('b1', 'git commit -qm "add a"', 'Commit', ''), + ...bash( + 'b2', + 'git push -u origin main && git -C remote.git log --oneline', + 'Push', + 'Exit code 128\nTo ./remote.git\n * [new branch] main -> main\nfatal: your current branch does not have any commits yet', + true + ), + ...bash('b3', next, 'Next', nextOk ? 'f4c840d add a' : 'Exit code 1\nfatal: no', !nextOk), + end(1000), + ]).panels.find((p) => p.kind === 'save')!.caption.story; + expect(save('git -C remote.git symbolic-ref HEAD refs/heads/main && git -C remote.git log --oneline')).toMatch( + /, but a check after that fails; Claude fixes it, and the check passes next\.$/ + ); + // Run again with no change before it, or failing again: no claim of a fix. + expect(save('git status -sb && git -C remote.git log --oneline')).toMatch(/, but a check after that fails\.$/); + expect( + save('git -C remote.git symbolic-ref HEAD refs/heads/main && git -C remote.git log --oneline', false) + ).toMatch(/, but a check after that fails\.$/); + }); + it('a check after a push is named a check, and tests in a fresh copy say so', () => { const page = pageOf([ prompt('ship it'),