mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 02:29:33 +01:00
fix(story-mode): last words in context, the check after a push, a fresh copy (review round 10)
From the person who asked for the work, reading fresh pages:
- "The result was 4 passed, 1 failed" under "Mission complete!" read as
a failing end. A test count is no failed step, and Claude's second
sentence is kept when it is the outcome ("All 5 tests now pass, and the
change is committed.").
- "Two places where I didn't follow your wording:" showed one. A list
intro that announces how many brings that many lines.
- "…would fix it for up to 10 files. Want me to add that?" never said
what "it" was. An offer that points back brings the start of its
paragraph ("Suffix swap: the two files with the same time would trade
their _1 suffix. …").
- "Decisions you may want to change" is something for you, like an offer.
- A second opinion whose call failed read as given; it now reads "after
trying to ask … (no answer came)".
- "but a step after that fails" after a push sounded as if the push
broke: when everything after it only looks (git log, status), it is "a
check after that fails".
- Tests run in a fresh clone read "The tests pass in a fresh copy made
from remote.git."
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
2859b3a207
commit
f46369a5af
16 files changed
+195
-34
No files matched your search
@@ -1082,7 +1082,7 @@ A Bash call sent to the background (its result is only `Command running in backg
|
||||
|
||||
A `page` carries the turn's summary fields plus:
|
||||
|
||||
- `events`: one per prompt, sentence Claude wrote and tool call. Each has `intent`, `tool`, `label` and, when the call returned, `ok` (`false` when the result was flagged as an error, or reported failed tests or lint problems a pipe kept from the exit code). Depending on the call: `edit` (an Edit/Write, hunks in Claude Code's own `structuredPatch` shape), `edits` (files a Bash call wrote through here-documents or `printf`/`echo` redirects, that a python or node script fed by a here-document opens for writing, or that Claude Code says it changed; the last two have no lines, since what changed is not in the command; an append's hunk has `atEnd: true` and no line numbers, since where it lands is not in the command), the tail of `output`, `tests` (`{ passed, failed, errors? }`, counted on the whole output before it is cut), `helper`, `files` (files a Bash call printed with `cat`/`head`/`tail`, resolved against the command's folder), `headline` (the part of a one-liner that decided its intent), `commits`, `subject` (the first commit's message, when the command spells it out with `-m` or a `git commit -F -` here-document), `ships` (`push` / `pr`), `running`, `background` and `finishedLater` (see Background work), `keyPoints` (for a sentence Claude wrote: up to two sentences from further into a long message that a reader must not lose, such as an open problem, something not yet done or checked, or a word on what you asked for; read on the whole message, past the `label`'s clip, and used by the final panel's balloon), `question` and, for prompts, `origin`. The advisor (a server tool) is an event with tool `Advisor` and intent `helper`; its answer is encrypted in the transcript, so it is never a panel of its own: the panel of the step after it (or the last one, when nothing follows) holds its events and counts them in `secondOpinion`. In plan mode the plan file under `~/.claude/plans/` is planning (intent `plan`, its numbered steps as `todos`), and `ExitPlanMode`'s result carries the plan (`output`) and your answer (`Approved` / `Sent back`).
|
||||
- `events`: one per prompt, sentence Claude wrote and tool call. Each has `intent`, `tool`, `label` and, when the call returned, `ok` (`false` when the result was flagged as an error, or reported failed tests or lint problems a pipe kept from the exit code). Depending on the call: `edit` (an Edit/Write, hunks in Claude Code's own `structuredPatch` shape), `edits` (files a Bash call wrote through here-documents or `printf`/`echo` redirects, that a python or node script fed by a here-document opens for writing, or that Claude Code says it changed; the last two have no lines, since what changed is not in the command; an append's hunk has `atEnd: true` and no line numbers, since where it lands is not in the command), the tail of `output`, `tests` (`{ passed, failed, errors? }`, counted on the whole output before it is cut), `helper`, `files` (files a Bash call printed with `cat`/`head`/`tail`, resolved against the command's folder), `headline` (the part of a one-liner that decided its intent), `commits`, `subject` (the first commit's message, when the command spells it out with `-m` or a `git commit -F -` here-document), `ships` (`push` / `pr`), `running`, `background` and `finishedLater` (see Background work), `keyPoints` (for a sentence Claude wrote: up to two sentences from further into a long message that a reader must not lose, such as an open problem, something not yet done or checked, or a word on what you asked for; read on the whole message, past the `label`'s clip, and used by the final panel's balloon), `question` and, for prompts, `origin`. The advisor (a server tool) is an event with tool `Advisor` and intent `helper`; its answer is encrypted in the transcript, so it is never a panel of its own: the panel of the step after it (or the last one, when nothing follows) holds its events and counts them in `secondOpinion` (an advisor call that returned an error is drawn as a second opinion that got no answer). In plan mode the plan file under `~/.claude/plans/` is planning (intent `plan`, its numbered steps as `todos`), and `ExitPlanMode`'s result carries the plan (`output`) and your answer (`Approved` / `Sent back`).
|
||||
- `panels`: at most 12, each `{ kind, events, caption: { story, director }, balloon?, files?, notes?, helpers?, todos?, tests?, ok?, rounds?, roundsOk?, question?, live? }`, with `events` as indices into `events`. A run panel's `printed` holds the first lines its last command printed (its last lines when it failed), for the terminal card: the card is drawn on the canvas and goes into a saved image, so lines that look like secrets are left out and addresses and long token-like strings are masked. `balloon.who` is `you`, `claude` or `note` (words that start a turn but are not yours: a background job's report, another session's message); a helper's report keeps its own words there (`"<helper>" reports: …`, its first sentence and key points, absolute paths shortened to their last two parts). A loop's `roundsOk` lists every test run in order, `true` where it passed and `null` for one still running, so a failure inside a round that passed in the end still shows, and a live loop shows the runs that finished. Kinds: `mission`, `investigate`, `research`, `plan`, `backup`, `fix`, `check`, `loop` (fix and check rounds folded), `run`, `save`, `ask`, `compact`, `montage` (quieter panels folded to fit) and `complete`.
|
||||
- `filesChanged`, `added`, `removed` (summed per edit), `helpers` (the turn's own helper calls; a report turn's prompt carries the helper it answers, which is not counted), `helpersRunning`, `noneWorked` (every tool call of the turn failed, so the last panel is no celebration; a command whose last check only found nothing, exit code 1 after its answer, does not count as failed), `durationMs`, `partial` (the turn began before the part that was read) and `truncated` (older outputs, hunks and helper texts were dropped to keep the page small: a page carries at most 120,000 characters of them, newest first; an edit that lost its lines keeps its counts and an empty `hunks`).
|
||||
|
||||
|
||||
+77
-13
@@ -263,6 +263,10 @@ const REPORTS_PROBLEM = /\b(left|stray|broke|broken|fail(s|ed)?|error|missing|wr
|
||||
const CAVEAT =
|
||||
/^(but|however|note|nothing|no\b|none|one (caveat|gap|thing)|heads up|caveat|warning|i (didn't|did not|couldn't|could not|haven't|have not|wasn't|left)|it (isn't|is not|wasn't|was not)|this (isn't|is not)|the (only|one) )/i;
|
||||
|
||||
/** A sentence that says how it ended: the tests, the save. Kept in Claude's last words after the first. */
|
||||
const OUTCOME =
|
||||
/\b(all \d+ (tests? )?(now )?pass|tests? (now )?pass|(is|are|was) (now )?committed|committed (it|them|the)|pushed|nothing is (lost|committed))\b/i;
|
||||
|
||||
/** What a speech balloon says: the first sentence, and the next when the first reports a problem. */
|
||||
export function balloonText(text: string): string {
|
||||
const [one, two] = firstTwoSentences(text);
|
||||
@@ -295,7 +299,7 @@ const KEY_KINDS: Array<[StoryKeyKind, RegExp]> = [
|
||||
],
|
||||
[
|
||||
'offer',
|
||||
/\b(want me to|should i|shall i|do you want|if you want|whenever you want|let me know|say if you want|i can (add|fix|change|do|update|make|run|commit))\b/i,
|
||||
/\b(want me to|should i|shall i|do you want|if you want|whenever you want|let me know|say if you want|i can (add|fix|change|do|update|make|run|commit)|you (may|might) want to (change|revisit|undo|review)|decisions? (you|for you)|worth a second look)\b/i,
|
||||
],
|
||||
[
|
||||
'howto',
|
||||
@@ -307,7 +311,7 @@ const KEY_KINDS: Array<[StoryKeyKind, RegExp]> = [
|
||||
/(?<!(deliberately|intentionally|purposely) )\bleft (out|open)\b(?! (on purpose|deliberately|intentionally|rather))|\b(not yet|haven't|have not|(hasn't|has not) (answered|replied|finished|started|run|been (run|tested|checked|merged|pushed))|can't tell|isn't (confirmed|committed|pushed|tested|done|checked)|not (checked|tested|verified|confirmed|committed|pushed|opened|run)|still (running|working|open(?!\s+(the|a|an|in|on|it|them)\b)|missing)|didn't (open|try|check|test|run|verify) it)\b/i,
|
||||
],
|
||||
// A step that failed, not a description of failure ("1 for a failed operation").
|
||||
['failed', /(?<!\b(a|the|any|on) )\b(failed|fails)\b/i],
|
||||
['failed', /(?<!\b(a|the|any|on) )(?<!\d )\b(failed|fails)\b/i],
|
||||
[
|
||||
'asked',
|
||||
/\b(as you (asked|expected|said|wanted|suggested)|you asked|your (--?[\w-]+|request|option|flag|change))\b/i,
|
||||
@@ -338,6 +342,8 @@ const sentenceKind = (s: string): StoryKeyKind | undefined =>
|
||||
(kind !== 'problem' || openProblem(s)) &&
|
||||
((kind !== 'problem' && kind !== 'pending') || !HISTORY.test(s))
|
||||
)?.[0];
|
||||
/** A sentence whose "it" or "that" is further back: "would fix it", "That's the cause". */
|
||||
const POINTS_BACK = /^(that|this|it)\b|\b(fix|solve|change|handle|avoid|prevent|add) (it|that|this|them)\b/i;
|
||||
/** A sentence that only points elsewhere ("described below"): nothing a balloon can show. */
|
||||
const POINTS_ELSEWHERE = /\b(described|listed|shown|details?) below\b|\bsee below\b|\bas follows\b/i;
|
||||
/** The fix that follows a problem sentence: then the problem is not open. */
|
||||
@@ -371,13 +377,18 @@ function keyPointsOf(text: string): string[] {
|
||||
const plain = plainMarkdown(text.slice(0, 8000), spans);
|
||||
// Every sentence in order, with the line it came from (list items are lines).
|
||||
const sentences: string[] = [];
|
||||
const lineOf: number[] = [];
|
||||
let lineNo = 0;
|
||||
for (const line of plain.split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
lineNo++;
|
||||
if (trimmed.startsWith('|') || (trimmed.match(/\|/g)?.length ?? 0) >= 2) continue; // a table row
|
||||
let rest = trimmed.replace(/^[-*•]\s+/, '').replace(/\s+/g, ' ');
|
||||
while (rest) {
|
||||
const m = SENTENCE_END.exec(rest);
|
||||
sentences.push((m ? m[1] : rest).trim());
|
||||
lineOf.push(lineNo);
|
||||
rest = m ? rest.slice(m[1].length).trim() : '';
|
||||
}
|
||||
}
|
||||
@@ -390,20 +401,46 @@ function keyPointsOf(text: string): string[] {
|
||||
// Judged on its own words (a list intro too: "Known quirks:", "Two places where I didn't…:").
|
||||
const kind = sentenceKind(sentence);
|
||||
if (!kind && (weak || !KEY_WEAK.test(sentence))) continue;
|
||||
// A list intro brings its first items, while short, never one that only points elsewhere.
|
||||
if (/:$/.test(sentence))
|
||||
for (let k = i + 1; k <= i + 2 && k < sentences.length && sentence.length + sentences[k].length < 260; k++)
|
||||
// A list intro brings its first items, while short, never one that only points elsewhere. One that
|
||||
// announces how many ("Two places where I didn't follow your wording:") brings that many lines.
|
||||
let wide = false;
|
||||
if (/:$/.test(sentence)) {
|
||||
const count =
|
||||
/\b(two|three|2|3)\s+(places|things|points|changes|issues|differences|deviations|items|notes|decisions|quirks|caveats)\b/i.exec(
|
||||
sentence
|
||||
)?.[1];
|
||||
const lines = count ? (/^(three|3)$/i.test(count) ? 3 : 2) : 0;
|
||||
wide = lines > 0;
|
||||
const room = lines ? 480 : 260;
|
||||
for (
|
||||
let k = i + 1;
|
||||
k < sentences.length &&
|
||||
(lines ? lineOf[k] <= lineOf[i] + lines : k <= i + 2) &&
|
||||
sentence.length + sentences[k].length < room;
|
||||
k++
|
||||
)
|
||||
if (!POINTS_ELSEWHERE.test(sentences[k])) sentence = `${sentence} ${sentences[k]}`;
|
||||
}
|
||||
// "That's out of date" needs what "that" was, and so does "Want me to add that?".
|
||||
// ("It doesn't mention…" is a sentence of its own: only "that"/"this" point back.)
|
||||
const refersBack =
|
||||
/^(that|this)\b/i.test(sentence) ||
|
||||
(kind === 'offer' && sentence.length < 80 && /\b(that|this|it|them)\b/i.test(sentence));
|
||||
const before = refersBack && i > 1 ? `${sentences[i - 1]} ` : '';
|
||||
let before = refersBack && i > 1 ? `${sentences[i - 1]} ` : '';
|
||||
// "…would fix it for up to 10 files. Want me to add that?": what "it" is sits further back, at the
|
||||
// start of that paragraph ("Suffix swap: the two files … would trade their _1 suffix.").
|
||||
if (before && kind === 'offer' && i > 2 && POINTS_BACK.test(sentences[i - 1])) {
|
||||
let start = i - 1;
|
||||
while (start > 1 && lineOf[start - 1] === lineOf[i - 1]) start--;
|
||||
if (start < i - 1) before = `${sentences[start]}${start < i - 2 ? ' …' : ''} ${before}`;
|
||||
}
|
||||
// A problem reported with its fix right after it: both, so it never reads as still open.
|
||||
const fix =
|
||||
kind === 'problem' && sentences[i + 1] && FIX_FOLLOWS.test(sentences[i + 1]) ? ` ${sentences[i + 1]}` : '';
|
||||
const shown = clip(restoreSpans(before + sentence + fix, spans), before || fix ? 360 : 300);
|
||||
const shown = clip(
|
||||
restoreSpans(before + sentence + fix, spans),
|
||||
before.includes(' …') || wide ? 480 : before || fix ? 360 : 300
|
||||
);
|
||||
if (kind) candidates.push({ at: i, text: shown, kind });
|
||||
else weak = { at: i, text: shown };
|
||||
}
|
||||
@@ -425,9 +462,9 @@ export function closingText(text: string, keyPoints: string[] = []): 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) || one.length < 24) && two.length <= 200) parts.push(two);
|
||||
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 > 2 ? 560 : 300);
|
||||
return clip(parts.join(' '), parts.length > 1 ? 560 : 300);
|
||||
}
|
||||
|
||||
/** The first sentence of a message, without markdown, for a speech balloon. */
|
||||
@@ -2634,6 +2671,13 @@ function decorate(draft: DraftPanel, events: StoryEvent[], turn: StoryTurn, inde
|
||||
tests.errors ? `, ${plural(tests.errors, 'error')}` : ''
|
||||
}`;
|
||||
if (edits.length) director += ` · writes ${plural(unique(edits.map((d) => d.file)).length, 'file')}`;
|
||||
// Tests run in a fresh copy (`git clone remote.git "$C" && (cd "$C" && pytest)`): the copy is what passed.
|
||||
const cloned = /\bgit clone\b(?:\s+-{1,2}[\w-]+)*\s+["']?([^\s"';&|]+)/.exec(lastCheck?.label ?? '')?.[1];
|
||||
if (cloned && /^The (tests|checks) (pass|fail)\w*/.test(story))
|
||||
story = story.replace(
|
||||
/^The (tests|checks) (pass|fail)(\w*)/,
|
||||
`The $1 $2$3 in a fresh copy made from ${basename(cloned.replace(/\/+$/, ''))}`
|
||||
);
|
||||
const written = unique(edits.map((d) => d.file));
|
||||
if (written.length) story = `Claude writes ${fileWords(written)}; ${story[0].toLowerCase()}${story.slice(1)}`;
|
||||
const saved = checks.reduce((n, e) => n + (e.commits ?? 0), 0);
|
||||
@@ -2704,12 +2748,31 @@ function decorate(draft: DraftPanel, events: StoryEvent[], turn: StoryTurn, inde
|
||||
// Its conventional prefix (`feat:`, `chore(deps):`) is for git, not for the reader.
|
||||
const plainSubject = subject?.replace(/^[a-z]+(\([^)]*\))?!?:\s*/i, '');
|
||||
const said = plainSubject ? `: "${clip(plainSubject, 60).replace(/[.]$/, '')}"` : '';
|
||||
// What came after the save in that command: only looking ("git log", "git status") makes it a check.
|
||||
const ship = (last?.label ?? '').search(ships === 'push' ? /\bgit push\b/ : /\bgit commit\b/);
|
||||
const after =
|
||||
ship < 0
|
||||
? []
|
||||
: (last?.label ?? '')
|
||||
.slice(ship)
|
||||
.split(/&&|\|\||;|\n/)
|
||||
.slice(1)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
const looksOnly =
|
||||
after.length > 0 &&
|
||||
after.every((s) =>
|
||||
/^(echo|printf|ls|cat|head|tail|wc|git (-C \S+ )?(log|status|remote|ls-remote|show|diff|branch|rev-parse|describe))\b/.test(
|
||||
s
|
||||
)
|
||||
);
|
||||
const stepAfter = looksOnly ? 'a check after that fails' : 'a step after that fails';
|
||||
story = savedAnyway
|
||||
? uploadOnly
|
||||
? `Claude ${uploads('the work')}, but a step after that fails.`
|
||||
? `Claude ${uploads('the work')}, but ${stepAfter}.`
|
||||
: ships === 'push'
|
||||
? `Claude saves the work${inCommits} and ${uploads('it')}, but a step after that fails.`
|
||||
: `Claude saves the work${inCommits}, but a step after that fails.`
|
||||
? `Claude saves the work${inCommits} and ${uploads('it')}, but ${stepAfter}.`
|
||||
: `Claude saves the work${inCommits}, but ${stepAfter}.`
|
||||
: panel.ok === false
|
||||
? ships === 'pr'
|
||||
? 'Claude tries to open a pull request, and the command fails.'
|
||||
@@ -2783,7 +2846,8 @@ function decorate(draft: DraftPanel, events: StoryEvent[], turn: StoryTurn, inde
|
||||
// Asked before the step, or (nothing following) after the last one.
|
||||
const lastWork = Math.max(...draft.events.filter((i) => events[i].tool !== 'Advisor'));
|
||||
const trailing = draft.events.some((i) => events[i].tool === 'Advisor' && i > lastWork);
|
||||
director += ` · ${trailing ? 'then' : 'after'} ${plural(panel.secondOpinion, 'second opinion')}`;
|
||||
const unanswered = draft.events.every((i) => events[i].tool !== 'Advisor' || events[i].ok === false);
|
||||
director += ` · ${trailing ? 'then' : 'after'} ${plural(panel.secondOpinion, 'second opinion')}${unanswered ? ' (no answer)' : ''}`;
|
||||
}
|
||||
panel.caption = { story, director };
|
||||
return panel;
|
||||
|
||||
@@ -2492,11 +2492,16 @@ Object.assign(CodemanApp.prototype, {
|
||||
if (rt.capNote) {
|
||||
// Asked before this step, or after it when nothing followed.
|
||||
const work = p.events.filter((i) => page.events[i]?.tool !== 'Advisor');
|
||||
const trailing = p.events.some((i) => page.events[i]?.tool === 'Advisor' && i > Math.max(...work));
|
||||
const asked = p.events.filter((i) => page.events[i]?.tool === 'Advisor');
|
||||
const trailing = asked.some((i) => i > Math.max(...work));
|
||||
// A call that came back with an error brought no opinion: say so, never that one was given.
|
||||
const unanswered = asked.length > 0 && asked.every((i) => page.events[i].ok === false);
|
||||
rt.capNote.hidden = !p.secondOpinion;
|
||||
rt.capNote.textContent = p.secondOpinion
|
||||
? `${trailing ? 'then asks' : 'after asking'} a stronger AI model for a second opinion`
|
||||
: '';
|
||||
rt.capNote.textContent = !p.secondOpinion
|
||||
? ''
|
||||
: unanswered
|
||||
? `${trailing ? 'then tries to ask' : 'after trying to ask'} a stronger AI model for a second opinion (no answer came)`
|
||||
: `${trailing ? 'then asks' : 'after asking'} a stronger AI model for a second opinion`;
|
||||
}
|
||||
rt.el.classList.toggle('is-live', !!p.live);
|
||||
// Named by its place only: the caption inside is read as the panel's content, once.
|
||||
|
||||
@@ -172,7 +172,7 @@
|
||||
"ok": null,
|
||||
"story": "Done in 11 seconds.",
|
||||
"director": "turn_duration · 11 s · 1 tool call",
|
||||
"balloon": "Both helpers have finished.",
|
||||
"balloon": "Both helpers have finished. Against the real wordcount.py, all 22 tests pass: my 4 in tests/test_basic.py and helper B's 18 in tests/test_extra.py.",
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
"kind": "backup",
|
||||
"ok": null,
|
||||
"story": "Claude sends 1 helper off to work in the background.",
|
||||
"director": "Agent · general-purpose · 6 calls inside · after 1 second opinion",
|
||||
"director": "Agent · general-purpose · 6 calls inside · after 1 second opinion (no answer)",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
@@ -244,7 +244,7 @@
|
||||
"ok": null,
|
||||
"story": "Done in 1 minute.",
|
||||
"director": "turn_duration · 1 min 14 s · 9 tool calls · 2 files changed",
|
||||
"balloon": "I added a --prefix option to rename_by_date.sh. Making the script sort file names the plain byte-by-byte way would fix it for up to 10 files with the same time. Want me to add that?",
|
||||
"balloon": "I added a --prefix option to rename_by_date.sh. Suffix swap: the two files with the same time (2024-05-01_093000*) would trade their _1 suffix. … Making the script sort file names the plain byte-by-byte way would fix it for up to 10 files with the same time. Want me to add that?",
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
|
||||
@@ -238,7 +238,7 @@
|
||||
"ok": null,
|
||||
"story": "Done in 1 minute.",
|
||||
"director": "turn_duration · 1 min 7 s · 10 tool calls · 2 files changed",
|
||||
"balloon": "I added wrap() as planned, with 3 new tests.",
|
||||
"balloon": "I added wrap() as planned, with 3 new tests. All 8 tests pass and ruff reports no problems.",
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
|
||||
@@ -268,7 +268,7 @@
|
||||
{
|
||||
"kind": "save",
|
||||
"ok": true,
|
||||
"story": "Claude saves the work and copies it to origin.git, a folder on this computer, but a step after that fails.",
|
||||
"story": "Claude saves the work and copies it to origin.git, a folder on this computer, but a check after that fails.",
|
||||
"director": "git commit -q -F - · git push · 1 error",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
"ok": null,
|
||||
"story": "Done in 1 minute.",
|
||||
"director": "turn_duration · 1 min 24 s · 10 tool calls · 2 files changed",
|
||||
"balloon": "slugify.mjs and its test file are written, all 4 tests pass, and both files are committed. First run (deliberately failing): the \"strips leading and trailing hyphens\" test expected '-hello-world'. That's wrong against the spec, and the run ended with 3 passed, 1 failed, exit code 1. Set git config user.name if you want a different name on future commits.",
|
||||
"balloon": "slugify.mjs and its test file are written, all 4 tests pass, and both files are committed. Set git config user.name if you want a different name on future commits.",
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
@@ -152,7 +152,7 @@
|
||||
"ok": null,
|
||||
"story": "Done in 21 seconds.",
|
||||
"director": "turn_duration · 21 s · 5 tool calls · 1 file changed",
|
||||
"balloon": "I added a fifth test for slugify(''). The result was 4 passed, 1 failed, exit code 1.",
|
||||
"balloon": "I added a fifth test for slugify(''). All 5 tests now pass, and the change is committed.",
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
|
||||
@@ -150,7 +150,7 @@
|
||||
"ok": null,
|
||||
"story": "Done in 6 minutes.",
|
||||
"director": "turn_duration · 6 min 25 s · 26 tool calls · 13 files changed",
|
||||
"balloon": "The inventory/ package is built and committed. --csv is part of the CLI commit rather than its own commit, because you asked for it before that module existed. Known quirks: remove pear prints the name as you typed it, not as it was stored (\"Pear\"). Because of how the temporary file is created, the saved JSON file is readable only by its owner (permissions 0600).",
|
||||
"balloon": "The inventory/ package is built and committed. Decisions you may want to change: I committed CLAUDE.md. .claude/settings.local.json, .codeman-agent-case.json and the default inventory.json are gitignored. --csv is part of the CLI commit rather than its own commit, because you asked for it before that module existed. Known quirks: remove pear prints the name as you typed it, not as it was stored (\"Pear\"). Because of how the temporary file is created, the saved JSON file is readable only by its owner (permissions 0600).",
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
@@ -305,7 +305,7 @@
|
||||
"ok": null,
|
||||
"story": "Done in 1 minute.",
|
||||
"director": "turn_duration · 1 min 16 s · 16 tool calls · 2 files changed",
|
||||
"balloon": "I ran all 16 steps in order, one tool call each. Two places where I didn't follow your wording exactly: Step 6: this session has no Grep tool, so I ran grep -rn 'def ' inventory/ through Bash instead. It found the same matches.",
|
||||
"balloon": "I ran all 16 steps in order, one tool call each. Two places where I didn't follow your wording exactly: Step 6: this session has no Grep tool, so I ran grep -rn 'def ' inventory/ through Bash instead. It found the same matches. Steps 5, 10 and 15: I added a second -m with the Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> line to each commit, as the earlier commits have. The commit subjects are exactly as you wrote them.",
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
"ok": null,
|
||||
"story": "Done in 3 minutes.",
|
||||
"director": "turn_duration · 3 min 13 s · 13 tool calls · 3 files changed",
|
||||
"balloon": "I built wcount.py and test_wcount.py. What failed and how I fixed it: one test failed on the first run.",
|
||||
"balloon": "I built wcount.py and test_wcount.py. All 19 tests pass (19 passed in 0.46s) and ruff check . is clean. What failed and how I fixed it: one test failed on the first run.",
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
{
|
||||
"kind": "save",
|
||||
"ok": true,
|
||||
"story": "Claude starts a git repository, saves the work and copies it to remote.git, a folder on this computer, but a step after that fails.",
|
||||
"story": "Claude starts a git repository, saves the work and copies it to remote.git, a folder on this computer, but a check after that fails.",
|
||||
"director": "git commit -q -F - · git push -u origin main · 1 error",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
@@ -105,7 +105,7 @@
|
||||
{
|
||||
"kind": "check",
|
||||
"ok": true,
|
||||
"story": "The tests pass.",
|
||||
"story": "The tests pass in a fresh copy made from remote.git.",
|
||||
"director": "python3 -m pytest -q -p no:cacheprovider) → ok · 19 passed · then 1 second opinion",
|
||||
"balloon": null,
|
||||
"tests": {
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
"ok": null,
|
||||
"story": "Claude's part is done in 3 minutes. Its helper finished later and reported back.",
|
||||
"director": "turn_duration · 2 min 44 s · 11 tool calls · 5 files changed · 1 helper",
|
||||
"balloon": "I've built csvstat.py, a command-line tool that prints a CSV's row count and the min, max and mean of each column. A background helper is writing README.md; I didn't wait for it, so it isn't confirmed yet. Problem: numbers are shown to only 6 significant digits. Once it has finished, the fix is a one-line formatting change and an update to the README example; say if you want it.",
|
||||
"balloon": "I've built csvstat.py, a command-line tool that prints a CSV's row count and the min, max and mean of each column. Its 17 pytest tests pass, the linter (ruff) is clean, and the dry run on a sample CSV worked. A background helper is writing README.md; I didn't wait for it, so it isn't confirmed yet. Problem: numbers are shown to only 6 significant digits. Once it has finished, the fix is a one-line formatting change and an update to the README example; say if you want it.",
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
"ok": null,
|
||||
"story": "Done in 3 minutes.",
|
||||
"director": "turn_duration · 2 min 39 s · 10 tool calls · 3 files changed",
|
||||
"balloon": "I built the pomodoro timer. Opening the page: it won't work if you double-click index.html. Serve the folder instead, for example with python3 -m http.server in the folder, then open http://localhost:8000. I didn't open it in a browser; I only checked that its script is valid.",
|
||||
"balloon": "I built the pomodoro timer. All 7 tests pass under node --test, and everything is committed on main as c4e7b0b feat: add pomodoro timer with tests. Opening the page: it won't work if you double-click index.html. Serve the folder instead, for example with python3 -m http.server in the folder, then open http://localhost:8000. I didn't open it in a browser; I only checked that its script is valid.",
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
|
||||
@@ -127,7 +127,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 and copies it to remote.git, a folder on this computer, but a step after that fails.',
|
||||
'Claude starts a git repository, saves the work and copies it to remote.git, a folder on this computer, but a check after that fails.',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -142,7 +142,7 @@ describe('Story Mode on real sessions', () => {
|
||||
'Claude saves the work: "add calc_stats.mode with smallest-value tie-breaking", then copies it to origin.git, a folder on this computer.'
|
||||
);
|
||||
expect(third.panels.find((p) => p.kind === 'save')?.story).toBe(
|
||||
'Claude saves the work and copies it to origin.git, a folder on this computer, but a step after that fails.'
|
||||
'Claude saves the work and copies it to origin.git, a folder on this computer, but a check after that fails.'
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -886,6 +886,29 @@ describe('story-mode.js overlay', () => {
|
||||
expect(note.textContent).toBe('after asking a stronger AI model for a second opinion');
|
||||
});
|
||||
|
||||
it('a second opinion that came back with an error says no answer came', async () => {
|
||||
const failed = {
|
||||
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_tool_result_error', error_code: 'unavailable' },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
boot(storyOf([prompt('fix it'), failed, ...edit('e1', '/work/demo-shop/a.py', ['+a = 1']), end(1000)]));
|
||||
app.openStoryMode(SID);
|
||||
await settle();
|
||||
expect($('.story-panel[data-kind="fix"] .story-cap-note')!.textContent).toBe(
|
||||
'after trying to ask a stronger AI model for a second opinion (no answer came)'
|
||||
);
|
||||
});
|
||||
|
||||
it('the final panel counts the commits of the whole turn, and says when the work was uploaded', async () => {
|
||||
const chipsFor = async (to: string) => {
|
||||
boot(
|
||||
|
||||
@@ -1163,6 +1163,75 @@ describe('buildStoryPage', () => {
|
||||
expect(page).toMatchObject({ openNote: 'catch', asks: true });
|
||||
});
|
||||
|
||||
it('the closing balloon keeps the outcome, the items a list announces and what an offer points back to (round 10)', () => {
|
||||
const last = (text: string) => pageOf([prompt('go'), said(text), end(1000)]).panels.at(-1)!.balloon!.text;
|
||||
const filler = 'It reads the file and prints the counts for each column, one line per column. '.repeat(3);
|
||||
// The outcome after the first sentence, and a test count ("1 failed") is no failed step.
|
||||
expect(
|
||||
last(
|
||||
[
|
||||
'I added a fifth test.',
|
||||
'All 5 tests now pass, and the change is committed.',
|
||||
filler,
|
||||
'- First run: the result was 4 passed, 1 failed.',
|
||||
].join('\n')
|
||||
)
|
||||
).toBe('I added a fifth test. All 5 tests now pass, and the change is committed.');
|
||||
// "Two places…:" brings both list lines, not only the first.
|
||||
expect(
|
||||
last(
|
||||
[
|
||||
'I ran all 16 steps in order, one tool call each.',
|
||||
filler,
|
||||
"Two places where I didn't follow your wording exactly:",
|
||||
'- Step 6: there is no Grep tool, so I ran grep through Bash. It found the same matches.',
|
||||
'- Steps 5 and 10: I added a co-author line to each commit.',
|
||||
].join('\n')
|
||||
)
|
||||
).toBe(
|
||||
"I ran all 16 steps in order, one tool call each. Two places where I didn't follow your wording exactly: Step 6: there is no Grep tool, so I ran grep through Bash. It found the same matches. Steps 5 and 10: I added a co-author line to each commit."
|
||||
);
|
||||
// An offer whose "it" is further back brings the start of its paragraph.
|
||||
expect(
|
||||
last(
|
||||
[
|
||||
'I added a --prefix option.',
|
||||
filler,
|
||||
'Suffix swap: the two files with the same time would trade their _1 suffix. This was already in the script before. Sorting byte by byte would fix it. Want me to add that?',
|
||||
].join('\n')
|
||||
)
|
||||
).toBe(
|
||||
'I added a --prefix option. Suffix swap: the two files with the same time would trade their _1 suffix. … Sorting byte by byte would fix it. Want me to add that?'
|
||||
);
|
||||
// A decision left to you is something for you.
|
||||
expect(keyKindOf('Decisions you may want to change: I committed CLAUDE.md.')).toBe('offer');
|
||||
});
|
||||
|
||||
it('a check after a push is named a check, and tests in a fresh copy say so', () => {
|
||||
const page = pageOf([
|
||||
prompt('ship it'),
|
||||
...bash('b1', 'git commit -qm "add a"', 'Commit', ''),
|
||||
...bash(
|
||||
'b2',
|
||||
'git push -u origin main && git remote -v && 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',
|
||||
'C=/tmp/x && git clone -q remote.git "$C" && (cd "$C" && python3 -m pytest -q)',
|
||||
'Fresh copy',
|
||||
'19 passed in 0.11s'
|
||||
),
|
||||
end(1000),
|
||||
]);
|
||||
expect(page.panels.find((p) => p.kind === 'save')!.caption.story).toMatch(/, but a check after that fails\.$/);
|
||||
expect(page.panels.find((p) => p.kind === 'check')!.caption.story).toBe(
|
||||
'The tests pass in a fresh copy made from remote.git.'
|
||||
);
|
||||
});
|
||||
|
||||
it("a run panel's terminal card shows what the command printed, never what looks like a secret", () => {
|
||||
const page = pageOf([
|
||||
prompt('dry run please'),
|
||||
|
||||
Reference in new issue
Block a user