fix(story-mode): plans you approved, project rules, tag lookups and dry runs (review round 12)

From a reader who has never written code:
- "a save your prompt here did not ask for" on a save that was step 3 of
  the plan you approved ("Verify and commit"): a commit step in an
  approved plan asks for the save. When Claude says a project rule asks
  for its commits (read on its whole message), the line says "…; your
  project's rules do".
- "a check after that fails" read as a failed test where it was `git
  describe` finding no tags: "the version-tag lookup after that fails
  (there are no tags yet)".
- The dry run you asked for never showed: the terminal card took the
  last command's edge-case errors. A dry run in the panel is now what the
  card shows, its "a -> b" lines, under "Claude does the dry run you
  asked for: it shows what would happen, changing nothing."

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-18 18:19:01 +02:00
1 parent bcfef8af5b
commit f6c540276c
12 files changed
+120 -31

No files matched your search

@@ -78,16 +78,16 @@
{
"kind": "run",
"ok": null,
"story": "Claude works in the terminal.",
"story": "Claude does the dry run you asked for: it shows what would happen, changing nothing.",
"unaskedSave": false,
"director": "Bash ×5 · rm -- ./-- && S=/tmp/claude/-work-cases-story-review-c/740e…",
"balloon": "My mk -- ... helper call left a stray file named --. I'll remove it, then run the dry run and save a listing to confirm nothing changed.",
"tests": null,
"roundsOk": null,
"printed": [
"--bogus exit=2",
"two dirs exit=2",
"-- two dirs exit=2"
"[dry-run] 2022-07-04_120000.jpg -> 2020-01-01_000000.jpg",
"[dry-run] a.jpg -> 2024-05-01_093000.jpg",
"[dry-run] b.jpg -> 2024-05-01_093000_1.jpg"
],
"events": 5
},
@@ -230,16 +230,16 @@
{
"kind": "run",
"ok": null,
"story": "Claude works in the terminal.",
"story": "Claude does the dry run you asked for: it shows what would happen, changing nothing.",
"unaskedSave": false,
"director": "Bash ×2 · bash -n \"$P/rename_by_date.sh\" && echo \"syntax ok\"",
"balloon": "Here's the dry run you asked for on the samples, plus a check that nothing on disk changed:",
"tests": null,
"roundsOk": null,
"printed": [
"--- missing value",
"exit=2 error: --prefix needs a value",
"--- slash"
"[dry-run] 2020-01-01_000000.jpg -> holiday_2020-01-01_000000.jpg",
"[dry-run] 2021-02-03_040506.jpg -> holiday_2021-02-03_040506.jpg",
"[dry-run] 2022-07-04_120000.jpg -> holiday_2022-07-04_120000.jpg"
],
"events": 2
},
@@ -70,7 +70,7 @@
"kind": "save",
"ok": true,
"story": "Claude starts a git repository and saves the work: \"add clamp() with pytest coverage\".",
"unaskedSave": true,
"unaskedSave": "rule",
"director": "git commit -q -F -",
"balloon": null,
"tests": null,
@@ -241,7 +241,7 @@
"kind": "save",
"ok": true,
"story": "Claude saves the work: \"add wrap() for modular wrapping into a range\".",
"unaskedSave": true,
"unaskedSave": false,
"director": "git commit -q -F -",
"balloon": null,
"tests": null,
@@ -193,7 +193,7 @@
{
"kind": "save",
"ok": true,
"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, as you said it might.",
"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 the version-tag lookup after that fails (there are no tags yet), as you said it might.",
"unaskedSave": false,
"director": "git commit -q -F - · git push -u origin HEAD",
"balloon": null,
@@ -286,7 +286,7 @@
{
"kind": "save",
"ok": true,
"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, as you said it might.",
"story": "Claude saves the work: \"add NOTES.md describing mode behavior\" and copies it to origin.git, a folder on this computer, but the version-tag lookup after that fails (there are no tags yet), as you said it might.",
"unaskedSave": false,
"director": "git commit -q -F - · git push · 1 error",
"balloon": null,
@@ -43,7 +43,7 @@
"kind": "save",
"ok": true,
"story": "Claude starts a git repository, writes package.json and index.js, and saves the work: \"add package.json and index.js with add and slugify\".",
"unaskedSave": true,
"unaskedSave": "rule",
"director": "git commit -q -m \"feat: add package.json and index.js with… · writes 2 files · after 1 second opinion",
"balloon": null,
"tests": null,
@@ -93,7 +93,7 @@
{
"kind": "run",
"ok": null,
"story": "Claude works in the terminal.",
"story": "Claude does the dry run you asked for: it shows what would happen, changing nothing.",
"unaskedSave": false,
"director": "Bash · set -o pipefail && python3 csvstat.py sample.csv | tee /tmp…",
"balloon": null,
+6 -3
View File
@@ -143,10 +143,10 @@ describe('Story Mode on real sessions', () => {
});
expect(second.panels.find((p) => p.kind === 'save')?.story).toBe(
// `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, as you said it might.'
'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 the version-tag lookup after that fails (there are no tags yet), as you said it might.'
);
expect(third.panels.find((p) => p.kind === 'save')?.story).toBe(
'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, as you said it might.'
'Claude saves the work: "add NOTES.md describing mode behavior" and copies it to origin.git, a folder on this computer, but the version-tag lookup after that fails (there are no tags yet), as you said it might.'
);
});
@@ -206,7 +206,10 @@ describe('Story Mode on real sessions', () => {
it('the saves no prompt asked for are marked on their first panel (round 11)', () => {
const marked = (name: string) =>
pagesOf(name).flatMap((p, t) => p.panels.filter((x) => x.unaskedSave).map((x) => `T${t + 1} ${x.kind}`));
expect(marked('compact-then-plan-mode')).toEqual(['T1 save', 'T3 save']);
// T3's save was a step of the plan you approved ("Verify and commit"): asked for.
expect(marked('compact-then-plan-mode')).toEqual(['T1 save']);
// Claude said why: the project's rules ask for a commit after each change.
expect(pagesOf('teammates-then-interrupt')[0].panels.find((x) => x.unaskedSave)?.unaskedSave).toBe('rule');
expect(marked('python-cli-fix-loop')).toEqual(['T1 loop']);
expect(marked('text-plan-dry-run-helper')).toEqual(['T1 check']);
// The prompt asked to commit and push: nothing to mark.
+16
View File
@@ -948,6 +948,22 @@ describe('story-mode.js overlay', () => {
expect($('.story-panel[data-kind="complete"] .story-splash-title')!.textContent).toBe('Done, with a catch');
});
it("an unasked save that Claude says the project's rules ask for says so", async () => {
boot(
storyOf([
prompt('Add a.'),
...bash('b1', 'git commit -qm "add a"', 'Commit', ''),
said("I added a. I committed it to follow CLAUDE.md's commit-after-each-change rule."),
end(1000),
])
);
app.openStoryMode(SID);
await settle();
expect($('.story-panel[data-kind="save"] .story-cap-note')!.textContent).toBe(
"a save your prompt here did not ask for; your project's rules do"
);
});
it('a second opinion that came back with an error says no answer came', async () => {
const failed = {
type: 'assistant',
+22 -3
View File
@@ -1127,6 +1127,25 @@ describe('buildStoryPage', () => {
);
});
it('the dry run you asked for is what the terminal card shows, whatever ran after it', () => {
const page = pageOf([
prompt('add --prefix and show me a dry run on the samples'),
...bash(
'b1',
'./rename.sh --dry-run --prefix x_',
'Dry run',
'syntax ok\n[dry-run] a.jpg -> x_a.jpg\n[dry-run] b.jpg -> x_b.jpg'
),
...bash('b2', './rename.sh --prefix', 'Edge cases', '--- missing value\nexit=2 error: --prefix needs a value'),
end(1000),
]);
const run = page.panels.find((p) => p.kind === 'run')!;
expect(run.printed).toEqual(['[dry-run] a.jpg -> x_a.jpg', '[dry-run] b.jpg -> x_b.jpg']);
expect(run.caption.story).toBe(
'Claude does the dry run you asked for: it shows what would happen, changing nothing.'
);
});
it('a short ending keeps the sentence that leaves something open', () => {
const page = pageOf([
prompt('run them'),
@@ -1331,7 +1350,7 @@ describe('buildStoryPage', () => {
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.'
'Claude saves the work: "add a" and copies it to origin.git, a folder on this computer, but the version-tag lookup after that fails (there are no tags yet).'
);
expect(page.toolCalls).toBe(2);
});
@@ -1384,8 +1403,8 @@ describe('buildStoryPage', () => {
prompt('dry run please'),
...bash(
'b1',
'./rename.sh --dry-run samples/',
'Dry run',
'./rename.sh --list samples/',
'List the renames',
'a.jpg -> holiday_2024-05-01.jpg\nAPI_TOKEN=abc123\nauthor: me@example.com\nb.jpg -> holiday_2024-05-02.jpg'
),
end(1000),