fix(story-mode): commits in montages, lint verdicts, and a page that holds still

From review round 2 (a long turn, and a prompt added mid-turn):

- Commits fold last and a montage names them ("saves the work in 3
  commits"); a save counts its commits ("saves the work in 6 commits").
  Before, all three commits of a 16-step turn vanished into montages that
  read "runs 2 commands".
- Folding a full page starts at the oldest pair and grows a montage already
  there, and never takes the newest panel while another choice exists: a
  live page no longer reshuffles panels the reader is watching.
- A check whose output reports problems ("1 file would be reformatted",
  "Found 3 errors", "✖ 5 problems", `error TS…`) failed, though a trailing
  `; ls` hid the exit code: "The tests pass, but another check fails."
- Shell structure is not a command (`for x in`, `do`, `done`, `if`), so a
  `--version` probe in a loop reads as looking; `command -v` and
  `git config --get` too.
- The montage draws only the kinds of steps it folded, with a box for
  saves; the loop's arrow has a head and rests on a finished page; the
  "MISSION COMPLETE!" title has room for its tilt; drawer commands keep
  their line breaks.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-18 11:10:14 +02:00
1 parent ca035dd41b
commit a2db03cda5
5 files changed
+162 -30

No files matched your search

+52
View File
@@ -13,6 +13,7 @@ import {
firstSentence,
heredocWrites,
parseTestCounts,
reportsProblems,
planFromText,
printedFiles,
roughDuration,
@@ -71,6 +72,10 @@ describe('classifyBashCommand', () => {
// A here-document's body is data: `pytest.main()` in it runs nothing.
["cat > app.py <<'EOF'\nimport pytest\npytest.main()\nEOF", 'edit'],
['git commit -m "ignore pytest caches"', 'save'],
// Shell structure is not a command; read-only probes are looking.
['for t in ruff flake8; do $t --version; done', 'look'],
['command -v ruff; git config --get user.name', 'look'],
['if pytest -q; then echo ok; fi', 'test'],
['node --test timer.test.mjs 2>&1 | tail -20', 'test'],
['timeout 60 npm test', 'test'],
['FOO=1 CI=true pytest -q', 'test'],
@@ -118,6 +123,14 @@ describe('parseTestCounts beyond "N passed"', () => {
expect(parseTestCounts(output)).toEqual(counts);
});
it('reportsProblems reads linters, formatters and type checkers', () => {
expect(reportsProblems('Found 3 errors.\n[*] 2 fixable')).toBe(true);
expect(reportsProblems('1 file would be reformatted, 4 files already formatted')).toBe(true);
expect(reportsProblems('✖ 5 problems (3 errors, 2 warnings)')).toBe(true);
expect(reportsProblems("src/a.ts(3,1): error TS2322: Type 'x' is not assignable")).toBe(true);
expect(reportsProblems('All checks passed!\n5 files already formatted\nFound 0 errors.')).toBe(false);
});
it('a line that only mentions a failure is not a summary', () => {
expect(parseTestCounts('The request failed twice: 2 failed attempts')).toBeNull();
});
@@ -726,6 +739,45 @@ describe('buildStoryPage', () => {
expect(page.panels.find((p) => p.kind === 'save')!.balloon).toBeUndefined();
});
it('a check whose output reports lint problems failed, though a trailing ls hid the exit code', () => {
const page = pageOf([
prompt('check'),
...bash(
'b1',
'pytest -q | tail -1; ruff format --check .; ls',
'Tests and format',
'6 passed in 0.1s\n1 file would be reformatted\nREADME.md'
),
end(1000),
]);
const check = page.panels.find((p) => p.kind === 'check')!;
expect(check.ok).toBe(false);
expect(check.caption.story).toBe('The tests pass, but another check fails.');
});
it('a save counts its commits, and a montage names what it folded, commits included', () => {
const page = pageOf([
prompt('commit'),
...bash('b1', 'git add a && git commit -qm a && git add b && git commit -qm b && git push', 'Commit twice', 'ok'),
end(1000),
]);
expect(page.panels.find((p) => p.kind === 'save')!.caption.story).toBe(
'Claude saves the work in 2 commits and sends it up.'
);
const lines: unknown[] = [prompt('many steps')];
for (let i = 0; i < 14; i++) {
lines.push(...read(`r${i}`, `/w/f${i}.ts`));
lines.push(...bash(`c${i}`, `git commit -qm step${i}`, `Commit ${i}`, 'ok'));
}
lines.push(end(60_000));
const long = pageOf(lines);
const montages = long.panels.filter((p) => p.kind === 'montage');
expect(montages.length).toBeGreaterThan(0);
expect(montages.some((p) => /saves the work/.test(p.caption.story))).toBe(true);
// The newest panels keep their place: folding starts at the oldest.
expect(long.panels[long.panels.length - 2].kind).not.toBe('montage');
});
it('a loop ends at its last check; writes after it are a panel of their own', () => {
const page = pageOf([
prompt('tdd'),