fix(story-mode): the badge counts the card it sits on, opinions mid-step, a deviation is a note (review round 10)

From the fact-checker:
- "+37" sat on mathx.py's card, which has 11 of those lines; with
  several files the badge now counts the card in front (the Director's
  cut keeps every file's count).
- A second opinion asked in the middle of a step's calls read "after
  asking"; it now reads "along the way, asks a stronger AI model…".
- "Two places where I didn't follow your wording exactly" under
  "Mission complete!": not following your words is a note ("Done, with a
  note"), and the sentence counts as a word on what you asked.
- A test run that patched a file written a panel before said "Claude
  writes" it: whole files are written, patched ones changed ("Claude
  writes pyproject.toml and changes test_csvstat.py; …").

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-18 17:22:11 +02:00
1 parent 713978eb81
commit 175017f604
8 files changed
+100 -22

No files matched your search

@@ -179,7 +179,7 @@
"kind": "plan",
"ok": null,
"story": "Claude writes a plan: 3 steps.",
"director": "EnterPlanMode · Write · plan file · 3 steps · after 1 second opinion",
"director": "EnterPlanMode · Write · plan file · 3 steps · 1 second opinion along the way",
"balloon": null,
"tests": null,
"roundsOk": null,
@@ -41,7 +41,7 @@
"kind": "run",
"ok": null,
"story": "Claude works in the terminal.",
"director": "Bash ×2 · python3 -c \"import calc_stats\" 2>&1 | tail -1; ls pytest.in… · after 1 second opinion",
"director": "Bash ×2 · python3 -c \"import calc_stats\" 2>&1 | tail -1; ls pytest.in… · 1 second opinion along the way",
"balloon": null,
"tests": null,
"roundsOk": null,
@@ -171,7 +171,7 @@
"helpers": 0,
"helpersRunning": 0,
"finishedLater": 0,
"openNote": false,
"openNote": "note",
"asks": false,
"panels": [
{
@@ -62,7 +62,7 @@
{
"kind": "check",
"ok": true,
"story": "Claude writes test_csvstat.py and pyproject.toml; the tests pass, then Claude starts a git repository and saves the work.",
"story": "Claude writes pyproject.toml and changes test_csvstat.py; the tests pass, then 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": {
+8 -1
View File
@@ -107,7 +107,8 @@ 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, then Claude starts a git repository and saves the work.'
// test_csvstat.py was written a panel before and only patched here.
'Claude writes pyproject.toml and changes test_csvstat.py; the tests pass, then 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 });
@@ -195,6 +196,12 @@ describe('Story Mode on real sessions', () => {
expect(panel).toBeTruthy();
});
it('an ending that says Claude did not follow your wording is a note, not a clean finish (round 10)', () => {
const [, exact] = pagesOf('long-turn-prompt-added');
expect(exact.openNote).toBe('note');
expect(exact.panels.at(-1)!.balloon).toMatch(/Steps 5, 10 and 15: I added a second -m/);
});
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');
+39
View File
@@ -36,10 +36,14 @@ function storyOf(lines: unknown[]) {
};
}
/** Every text a scene drew, in order (the fake context records `fillText`). */
const drawnTexts: string[] = [];
/** A 2D context that accepts every call; measureText answers a plausible width. */
function fakeContext() {
const target: Record<string, unknown> = {
measureText: (text: string) => ({ width: String(text).length * 6 }),
fillText: (text: unknown) => void drawnTexts.push(String(text)),
};
return new Proxy(target, {
get(t, key) {
@@ -886,6 +890,41 @@ describe('story-mode.js overlay', () => {
expect(note.textContent).toBe('after asking a stronger AI model for a second opinion');
});
it('a second opinion asked in the middle of a step says so, and the badge counts the card in front', async () => {
const advisor = {
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' } },
],
},
};
boot(
storyOf([
prompt('fix it'),
...edit('e1', '/work/demo-shop/a.py', ['+a = 1', '+b = 2']),
advisor,
...edit('e2', '/work/demo-shop/b.py', ['+c = 3', '+d = 4', '+e = 5']),
end(1000),
])
);
drawnTexts.length = 0;
app.openStoryMode(SID);
await settle();
expect($('.story-panel[data-kind="fix"] .story-cap-note')!.textContent).toBe(
'along the way, asks a stronger AI model for a second opinion'
);
// Two files: the badge counts a.py, the card in front (+2), not the panel's +5. (JSDOM lays out
// nothing, so the panel is given a size and drawn late in its animation, the badge showing.)
const rt = app._story.panels.find((p: { kind: string }) => p.kind === 'fix');
Object.assign(rt, { w: 600, h: 270, dpr: 1, bgKey: '', clock: 200 });
app._storyDrawPanel(rt);
expect(drawnTexts).toContain('+2');
expect(drawnTexts).not.toContain('+5');
});
it('a second opinion that came back with an error says no answer came', async () => {
const failed = {
type: 'assistant',