fix(story-mode): a quoted report marks what it skips, a run that never loaded says so (review round 11)

From the fact-checker's last items:
- A helper's report quoted in a notice joined its first sentence to one
  from paragraphs later with nothing between: where text was left out,
  the balloon now says "…" (found in the report itself, so two
  sentences that follow each other are never marked).
- "The first run fails on purpose, as you asked" for a run that never
  loaded a test (a collection error) now reads "At first the tests don't
  even load, as you asked; the second run passes.", as a test panel
  already says it.
- A list intro without a count ("What failed and how I fixed it:") brings
  its items while they fit in 360 characters, not 260.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-18 18:05:48 +02:00
1 parent 1a66dbabfb
commit bcfef8af5b
4 files changed
+55 -10

No files matched your search

+29 -8
View File
@@ -411,7 +411,7 @@ function keyPointsOf(text: string): string[] {
)?.[1];
const lines = count ? (/^(three|3)$/i.test(count) ? 3 : 2) : 0;
wide = lines > 0;
const room = lines ? 480 : 260;
const room = lines ? 480 : 360;
for (
let k = i + 1;
k < sentences.length &&
@@ -458,13 +458,30 @@ function keyPointsOf(text: string): string[] {
* further in (an open problem, what was or was not done), so the final panel never reads as a clean
* finish when Claude said otherwise.
*/
export function closingText(text: string, keyPoints: string[] = []): string {
export function closingText(text: string, keyPoints: string[] = [], gap = ' '): string {
const [one, two] = firstTwoSentences(text);
// A very short first sentence ("Not done.") says too little alone.
const parts = [one];
if (two && (CAVEAT.test(two) || OUTCOME.test(two) || one.length < 24) && two.length <= 200) parts.push(two);
for (const point of keyPoints) if (!parts.some((p) => p.includes(point) || point.includes(p))) parts.push(point);
return clip(parts.join(' '), parts.length > 1 ? 560 : 300);
const head = [one];
if (two && (CAVEAT.test(two) || OUTCOME.test(two) || one.length < 24) && two.length <= 200) head.push(two);
const more: string[] = [];
for (const point of keyPoints)
if (![...head, ...more].some((p) => p.includes(point) || point.includes(p))) more.push(point);
// `gap` marks where the balloon skips ahead (" … " for a helper's report quoted in a notice): only
// where text was left out between two kept sentences, found in the message itself.
const flat = (t: string) =>
t
.replace(/[`*_#>|-]/g, '')
.replace(/\s+/g, ' ')
.trim();
const whole = gap === ' ' ? '' : flat(text.slice(0, 8000));
let out = head.join(' ');
let end = whole ? whole.indexOf(flat(out).slice(0, 60)) + flat(out).length : -1;
for (const point of more) {
const at = whole ? whole.indexOf(flat(point).slice(0, 60)) : -1;
out += at >= 0 && end >= 0 && at - end <= 2 ? ` ${point}` : `${gap}${point}`;
end = at >= 0 ? at + flat(point).length : -1;
}
return clip(out, head.length + more.length > 1 ? 560 : 300);
}
/** The first sentence of a message, without markdown, for a speech balloon. */
@@ -2510,7 +2527,7 @@ function decorate(draft: DraftPanel, events: StoryEvent[], turn: StoryTurn, inde
if (panel.balloon && report) {
// A report that is only a path says which file it wrote: its name is enough.
const pathOnly = /^\S+$/.test(report) && report.includes('/');
const said = (pathOnly ? basename(report) : closingText(report, keyPointsOf(report)))
const said = (pathOnly ? basename(report) : closingText(report, keyPointsOf(report), ' … '))
// An absolute path is mostly room: its last two parts ("tests/test_extra.py") say which file.
// Run on the clipped balloon (560 characters at most), so the scan stays short.
.replace(/(?<![\w.~-])\/(?:[^\s/]+\/)+([^\s/]+\/[^\s/]+)/g, '$1');
@@ -2685,6 +2702,8 @@ function decorate(draft: DraftPanel, events: StoryEvent[], turn: StoryTurn, inde
).length;
const n = panel.rounds;
const testFails = fails - lintFails;
// A run where not one test loaded (a collection error) "doesn't even load", as a check panel says it.
const loadsNot = (e: StoryEvent) => !!e.tests && !e.tests.passed && !e.tests.failed && !!e.tests.errors;
// A failure you asked for (test first), or that Claude said it expected, is the plan working.
const planned = passed(checks[0]) === false ? plannedFailure(turn, events) : false;
// The check that failed while the tests passed, by name: "a formatting check", not "another check".
@@ -2698,7 +2717,9 @@ function decorate(draft: DraftPanel, events: StoryEvent[], turn: StoryTurn, inde
? `Build and test, ${plural(n, 'round')}. The tests pass every time; ${otherCheck} fails ${fails === 1 ? 'once' : `${fails} times`}, then passes.`
: panel.ok === true && planned === 'asked'
? checks.length === 2
? 'The first run fails on purpose, as you asked; the second passes.'
? loadsNot(checks[0])
? "At first the tests don't even load, as you asked; the second run passes."
: 'The first run fails on purpose, as you asked; the second passes.'
: `${plural(n, 'round')}: the first run fails on purpose, as you asked, then it passes.`
: panel.ok === true && planned && checks.length === 2
? 'Test first: the first run fails as planned, the second passes.'
@@ -54,7 +54,7 @@
{
"kind": "loop",
"ok": true,
"story": "The first run fails on purpose, as you asked; the second passes.",
"story": "At first the tests don't even load, as you asked; the second run passes.",
"unaskedSave": false,
"director": "2 checks · 4 edits · +150 · 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.",
@@ -142,7 +142,7 @@
"story": "A background job reports back.",
"unaskedSave": false,
"director": "Prompt · 6 words · task",
"balloon": "\"Write README for csvstat\" reports: I wrote README.md and committed it on its own on feat/csvstat. The README has every section you asked for and no badges.",
"balloon": "\"Write README for csvstat\" reports: I wrote README.md and committed it on its own on feat/csvstat. … The README has every section you asked for and no badges.",
"tests": null,
"roundsOk": null,
"printed": null,
+24
View File
@@ -9,6 +9,7 @@ import {
buildStoryPage,
buildStoryTurns,
classifyBashCommand,
closingText,
createStoryReducer,
exactDuration,
firstSentence,
@@ -1103,6 +1104,29 @@ describe('buildStoryPage', () => {
expect(performance.now() - start).toBeLessThan(1500);
});
it('a quoted report marks only the text it skips with "…"', () => {
const report = [
'I wrote README.md and committed it.',
'It covers usage and the exit codes.',
'It reads the file and prints the counts for each column, one line per column. '.repeat(3),
'One known issue: big numbers are shown rounded.',
].join('\n\n');
// The second sentence follows the first: no mark. The issue is three paragraphs on: marked.
expect(
closingText(
report,
['It covers usage and the exit codes.', 'One known issue: big numbers are shown rounded.'],
' … '
)
).toBe(
'I wrote README.md and committed it. It covers usage and the exit codes. … One known issue: big numbers are shown rounded.'
);
// Claude's own closing words are never marked.
expect(closingText(report, ['One known issue: big numbers are shown rounded.'])).toBe(
'I wrote README.md and committed it. One known issue: big numbers are shown rounded.'
);
});
it('a short ending keeps the sentence that leaves something open', () => {
const page = pageOf([
prompt('run them'),