mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 02:29:33 +01:00
fix(story-mode): what a non-technical reader would believe, from the cold-reader review
Review round 5 had an agent read only the comic of the nine real sessions
(20 turns), write down what it believed happened, and only then compare
with the transcripts. Three panels told it something false, one hid the
failure the user asked to see:
- A push that went through, followed by a check that failed in the same
command, drew as a failed push; the output (`[new branch]`, `->`) now
decides: "Claude saves the work in 2 commits and uploads it, but a step
after that fails."
- A formatter's "would be reformatted" preview, printed before it fixed
the file, made a clean run a failure: the last verdict wins.
- A blocked command ended in confetti and "Mission complete!": a run panel
where everything failed says "Claude tries a command, and it is
blocked.", and a turn where nothing worked ends "Turn over" / "Nothing
Claude tried worked, so the job is not done." with a neutral robot.
- The loop stamped only the final verdict (POW ×2), so a first run that
failed on purpose never showed. It now marks every test run red or green,
and says it: "Fix and test: the first run fails, the second passes." /
"Build and test, 4 rounds. A check fails once, then everything passes."
Also:
- Files written inside a check or a save reach the plain caption ("Claude
writes 4 files; the tests pass.").
- Balloons keep the next sentence when the first reports a problem ("…left a
stray file. I'll remove it…") or is very short ("Not done. The harness
blocked sleep 65…"); Claude's last words keep a caveat that follows them
("Nothing is committed because this folder isn't a git repository.").
- The advisor never shares a panel with helpers that do work; ToolSearch
reads "Claude gets the tools it needs ready."; Codeman's own case file is
not drawn as a file Claude read; looking around shows what Claude meant
(the call's description), not raw commands; a montage at the start of a
turn reads "Claude gets started".
- Words: boards count tests ("19 tests"), "uploads it" replaces "sends it
up", and the prompt balloon keeps 400 characters.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
0effbb9ce4
commit
814bec739d
14 files changed
+307
-109
No files matched your search
+136
-36
@@ -235,6 +235,36 @@ const restoreSpans = (text: string, spans: string[]): string =>
|
||||
*/
|
||||
const SENTENCE_END = /^(.+?(?<!\.)(?<!\b(?:e\.g|i\.e|vs|cf))[.!?](?!\.))(?=\s|$)/;
|
||||
|
||||
/** The first two sentences of a message, without markdown (the second may be empty). */
|
||||
function firstTwoSentences(text: string): [string, string] {
|
||||
const spans: string[] = [];
|
||||
const plain = plainMarkdown(text, spans).replace(/\s+/g, ' ').trim();
|
||||
const one = SENTENCE_END.exec(plain);
|
||||
if (!one) return [restoreSpans(plain, spans), ''];
|
||||
const rest = plain.slice(one[1].length).trim();
|
||||
const two = SENTENCE_END.exec(rest);
|
||||
return [restoreSpans(one[1], spans), two ? restoreSpans(two[1], spans) : ''];
|
||||
}
|
||||
|
||||
/** A sentence that reports a problem: its next sentence (usually the fix) belongs with it. */
|
||||
const REPORTS_PROBLEM = /\b(left|stray|broke|broken|fail(s|ed)?|error|missing|wrong|bug|oops|mistake|forgot)\b/i;
|
||||
/** A sentence that carries a caveat a reader must not lose. */
|
||||
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;
|
||||
|
||||
/** 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);
|
||||
return clip(two && REPORTS_PROBLEM.test(one) && two.length <= 140 ? `${one} ${two}` : one, 260);
|
||||
}
|
||||
|
||||
/** Claude's last words: the first sentence, and the next when it carries a caveat. */
|
||||
export function closingText(text: string): string {
|
||||
const [one, two] = firstTwoSentences(text);
|
||||
// A very short first sentence ("Not done.") says too little alone.
|
||||
return clip(two && (CAVEAT.test(two) || one.length < 24) && two.length <= 200 ? `${one} ${two}` : one, 300);
|
||||
}
|
||||
|
||||
/** The first sentence of a message, without markdown, for a speech balloon. */
|
||||
export function firstSentence(text: string, max = 160): string {
|
||||
const spans: string[] = [];
|
||||
@@ -573,14 +603,26 @@ export function heredocWrites(command: string, cwd?: string): StoryEdit[] {
|
||||
* reformatted`, `✖ 5 problems`, `error TS2322:`). Read when a trailing command (`; ls`) hid the exit code.
|
||||
*/
|
||||
export function reportsProblems(output: string): boolean {
|
||||
return [
|
||||
// The last verdict wins: a formatter's "1 file would be reformatted" preview, then "1 file
|
||||
// reformatted" and a clean check, is clean.
|
||||
const last = (patterns: RegExp[]) =>
|
||||
Math.max(-1, ...patterns.map((re) => [...output.matchAll(new RegExp(re.source, 'gm'))].pop()?.index ?? -1));
|
||||
const problem = last([
|
||||
/\bFound [1-9]\d* errors?\b/,
|
||||
/\b[1-9]\d* files? would be reformatted\b/,
|
||||
/^would reformat /m,
|
||||
/^would reformat /,
|
||||
/✖ [1-9]\d* problems?\b/,
|
||||
/\[warn\] Code style issues found/,
|
||||
/\berror TS\d+:/,
|
||||
].some((re) => re.test(output));
|
||||
]);
|
||||
if (problem < 0) return false;
|
||||
const clean = last([
|
||||
/\bAll checks passed!/,
|
||||
/\bFound 0 errors\b/,
|
||||
/^(?!.*would be).*\b\d+ files? reformatted\b/,
|
||||
/^(?!.*would be).*\b\d+ files? (left unchanged|already formatted)\b/,
|
||||
]);
|
||||
return problem > clean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1655,21 +1697,32 @@ const TALKS_RESULTS = /\b(pass(es|ed|ing)?|fail(s|ed|ing|ures?)?|green|clean)\b/
|
||||
/** Claude reporting a save ("Commits are in.", "Pushed to origin."). */
|
||||
const TALKS_SAVES = /\b(commit(s|ted)?|pushed|merged|tagged)\b/i;
|
||||
/** The remark that reports on a panel of this kind, if any. */
|
||||
const REPORT_TALK: Partial<Record<StoryPanelKind, RegExp>> = { check: TALKS_RESULTS, save: TALKS_SAVES };
|
||||
const REPORT_TALK: Partial<Record<StoryPanelKind, RegExp>> = {
|
||||
check: TALKS_RESULTS,
|
||||
save: TALKS_SAVES,
|
||||
run: TALKS_RESULTS,
|
||||
};
|
||||
const ANNOUNCES = /\b(I'll|I will|let me|let's|I'm going to|next,? I|now I)\b/i;
|
||||
|
||||
function groupPanels(events: StoryEvent[]): { panels: DraftPanel[]; trailingSay?: string } {
|
||||
const panels: DraftPanel[] = [];
|
||||
let pendingSay: string | undefined;
|
||||
let lastSaid: string | undefined;
|
||||
events.forEach((ev, idx) => {
|
||||
if (ev.intent === 'say' && !ev.todos) {
|
||||
pendingSay ??= firstSentence(ev.label);
|
||||
pendingSay ??= balloonText(ev.label);
|
||||
lastSaid ??= ev.label;
|
||||
return;
|
||||
}
|
||||
lastSaid = undefined;
|
||||
const kind = ev.todos && ev.intent === 'say' ? 'plan' : PANEL_FOR[ev.intent];
|
||||
if (!kind) return;
|
||||
const last = panels[panels.length - 1];
|
||||
if (last && last.kind === kind && MERGEABLE.has(kind)) {
|
||||
const advisorMix =
|
||||
kind === 'backup' &&
|
||||
last?.kind === 'backup' &&
|
||||
(events[last.events[0]].tool === 'Advisor') !== (ev.tool === 'Advisor');
|
||||
if (last && last.kind === kind && MERGEABLE.has(kind) && !advisorMix) {
|
||||
last.events.push(idx);
|
||||
if (pendingSay && !last.balloon) last.balloon = { who: 'claude', text: pendingSay };
|
||||
} else {
|
||||
@@ -1686,17 +1739,18 @@ function groupPanels(events: StoryEvent[]): { panels: DraftPanel[]; trailingSay?
|
||||
pendingSay &&
|
||||
talk &&
|
||||
last &&
|
||||
(!last.balloon || !talk.test(last.balloon.text)) &&
|
||||
(!last.balloon || (last.kind === 'check' && !talk.test(last.balloon.text))) &&
|
||||
talk.test(pendingSay) &&
|
||||
!ANNOUNCES.test(pendingSay);
|
||||
if (kind === 'mission') panel.balloon = { who: 'you', text: clip(ev.label, 220) };
|
||||
if (kind === 'mission') panel.balloon = { who: 'you', text: clip(ev.label, 400) };
|
||||
else if (reportsBack && last && pendingSay) last.balloon = { who: 'claude', text: pendingSay };
|
||||
else if (pendingSay) panel.balloon = { who: 'claude', text: pendingSay };
|
||||
panels.push(panel);
|
||||
}
|
||||
pendingSay = undefined;
|
||||
});
|
||||
return { panels, trailingSay: pendingSay };
|
||||
// The closing words keep a caveat that follows them ("Nothing is committed: this is no git repo.").
|
||||
return { panels, trailingSay: lastSaid !== undefined ? closingText(lastSaid) : pendingSay };
|
||||
}
|
||||
|
||||
/** Fix → check → fix → check (4 panels or more) is one "loop" panel: try, test, repeat. */
|
||||
@@ -1863,14 +1917,18 @@ function decorate(draft: DraftPanel, events: StoryEvent[], turn: StoryTurn, inde
|
||||
evs.flatMap((e) => (e.intent === 'read' && e.file ? [e.file] : e.intent === 'look' ? (e.files ?? []) : []))
|
||||
);
|
||||
panel.files = files.slice(0, 12);
|
||||
// Codeman's own case file is plumbing, not part of the story.
|
||||
panel.files = panel.files.filter((f) => !/(^|\/)\.codeman[-.]/.test(f));
|
||||
panel.notes = evs
|
||||
.filter((e) => e.intent !== 'read')
|
||||
.map(shortCommand)
|
||||
.map((e) => e.description || shortCommand(e))
|
||||
.slice(0, 4);
|
||||
if (evs.every((e) => e.tool === 'ToolSearch')) story = 'Claude gets the tools it needs ready.';
|
||||
if (evs.every((e) => e.tool === 'Bash')) director = `${tools} · ${shortCommand(evs[0])}`;
|
||||
if (errors) director += ` · ${plural(errors, 'error')}`;
|
||||
story = files.length
|
||||
? `Claude reads ${plural(files.length, 'file')} to find its way around.`
|
||||
const shown = files.filter((f) => !/(^|\/)\.codeman[-.]/.test(f));
|
||||
story = shown.length
|
||||
? `Claude reads ${plural(shown.length, 'file')} to find its way around.`
|
||||
: evs.every((e) => e.intent === 'search')
|
||||
? 'Claude searches the code.'
|
||||
: 'Claude looks around the project.';
|
||||
@@ -1960,20 +2018,31 @@ function decorate(draft: DraftPanel, events: StoryEvent[], turn: StoryTurn, inde
|
||||
if (ok !== undefined) panel.ok = ok;
|
||||
const tests = lastCheck?.tests;
|
||||
if (tests) panel.tests = tests;
|
||||
// Every check green from the start: Claude was building piece by piece, not fixing.
|
||||
const neverFailed = checks.every((e) => passed(e) !== false);
|
||||
// Every test run's verdict, so a run that failed (often the one you asked for) is not lost,
|
||||
// even inside a round that passed in the end.
|
||||
const runs = checks.map(passed);
|
||||
if (runs.every((r) => r !== undefined)) panel.roundsOk = runs as boolean[];
|
||||
const failed = checks.slice(0, -1).filter((e) => passed(e) === false);
|
||||
const fails = failed.length;
|
||||
// A failure where the tests passed and only a linter or formatter complained.
|
||||
const lintFails = failed.filter(
|
||||
(e) => !!e.tests && e.tests.passed > 0 && !e.tests.failed && !e.tests.errors
|
||||
).length;
|
||||
const n = panel.rounds;
|
||||
story =
|
||||
neverFailed && panel.ok === true
|
||||
? `Build and test, ${plural(panel.rounds, 'round')}. Every check passes.`
|
||||
: `Fix and test, ${plural(panel.rounds, 'round')}.${
|
||||
panel.ok === true
|
||||
? ' It passes in the end.'
|
||||
panel.ok === true && !fails
|
||||
? `Build and test, ${plural(n, 'round')}. Every check passes.`
|
||||
: panel.ok === true && fails === lintFails
|
||||
? `Build and test, ${plural(n, 'round')}. A check fails ${fails === 1 ? 'once' : `${fails} times`}, then everything passes.`
|
||||
: panel.ok === true && n === 2
|
||||
? 'Fix and test: the first run fails, the second passes.'
|
||||
: panel.ok === true
|
||||
? `Fix and test, ${plural(n, 'round')}: ${fails === 1 ? 'one run fails' : `${fails} runs fail`}, then it passes.`
|
||||
: panel.ok === false && tests && tests.passed && !tests.failed && !tests.errors
|
||||
? ' The tests pass, but another check fails.'
|
||||
? `Fix and test, ${plural(n, 'round')}. The tests pass, but another check fails.`
|
||||
: panel.ok === false
|
||||
? ' Still failing.'
|
||||
: ''
|
||||
}`;
|
||||
? `Fix and test, ${plural(n, 'round')}. Still failing.`
|
||||
: `Fix and test, ${plural(n, 'round')}.`;
|
||||
director = `${plural(checks.length, 'check')} · ${plural(edits.length, 'edit')} · ${fmtCounts(panel.added, panel.removed)} · last: ${
|
||||
lastCheck ? outcome(lastCheck) : 'running'
|
||||
}`;
|
||||
@@ -2017,6 +2086,8 @@ 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')}`;
|
||||
const written = unique(edits.map((d) => d.file)).length;
|
||||
if (written) story = `Claude writes ${plural(written, 'file')}; ${story[0].toLowerCase()}${story.slice(1)}`;
|
||||
const saved = checks.reduce((n, e) => n + (e.commits ?? 0), 0);
|
||||
if (saved) {
|
||||
director += ` · ${plural(saved, 'commit')}`;
|
||||
@@ -2027,7 +2098,18 @@ function decorate(draft: DraftPanel, events: StoryEvent[], turn: StoryTurn, inde
|
||||
}
|
||||
case 'run':
|
||||
panel.notes = notesOf(evs);
|
||||
story = evs.every((e) => e.tool !== 'Bash') ? 'Claude uses a tool.' : 'Claude works in the terminal.';
|
||||
story =
|
||||
errors && errors === evs.length
|
||||
? `Claude tries ${evs.length === 1 ? 'a command' : `${evs.length} commands`}, and ${
|
||||
evs.some((e) => /\bblocked\b|PreToolUse|denied/i.test(e.output ?? ''))
|
||||
? 'it is blocked'
|
||||
: evs.length === 1
|
||||
? 'it fails'
|
||||
: 'they fail'
|
||||
}.`
|
||||
: evs.every((e) => e.tool !== 'Bash')
|
||||
? 'Claude uses a tool.'
|
||||
: 'Claude works in the terminal.';
|
||||
director = `${tools} · ${shortCommand(evs[0])}${errors ? ` · ${plural(errors, 'error')}` : ''}`;
|
||||
break;
|
||||
case 'save': {
|
||||
@@ -2035,20 +2117,34 @@ function decorate(draft: DraftPanel, events: StoryEvent[], turn: StoryTurn, inde
|
||||
const last = evs[evs.length - 1];
|
||||
if (last?.ok !== undefined) panel.ok = last.ok;
|
||||
const ships = evs.some((e) => e.ships === 'pr') ? 'pr' : evs.some((e) => e.ships === 'push') ? 'push' : '';
|
||||
// The command failed, but its own output shows the push (or the commit) went through: a step
|
||||
// chained after it failed, not the save.
|
||||
const out = last?.output ?? '';
|
||||
const savedAnyway =
|
||||
last?.ok === false &&
|
||||
(ships === 'push'
|
||||
? /\[new branch\]|\S+\.\.\S+\s+\S+\s+->|\bset up to track\b|Everything up-to-date/.test(out)
|
||||
: /^\[[^\]\n]+ [0-9a-f]{7,}\]/m.test(out));
|
||||
if (savedAnyway) panel.ok = true;
|
||||
const commits = evs.reduce((n, e) => n + (e.commits ?? (e.intent === 'save' ? 1 : 0)), 0);
|
||||
const inCommits = commits > 1 ? ` in ${commits} commits` : '';
|
||||
story =
|
||||
panel.ok === false
|
||||
story = savedAnyway
|
||||
? 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.`
|
||||
: panel.ok === false
|
||||
? ships === 'pr'
|
||||
? 'Claude tries to open a pull request, and the command fails.'
|
||||
: ships === 'push'
|
||||
? 'Claude tries to send the work up, and the command fails.'
|
||||
? 'Claude tries to upload the work, and the command fails.'
|
||||
: 'Claude tries to save the work, and the command fails.'
|
||||
: ships === 'pr'
|
||||
? 'Claude opens a pull request.'
|
||||
: ships === 'push'
|
||||
? `Claude saves the work${inCommits} and sends it up.`
|
||||
? `Claude saves the work${inCommits} and uploads it.`
|
||||
: `Claude saves the work${inCommits}.`;
|
||||
const wrote = unique(edits.map((d) => d.file)).length;
|
||||
if (wrote && story.startsWith('Claude ')) story = `Claude writes ${plural(wrote, 'file')} and ${story.slice(7)}`;
|
||||
director = `${evs.map(shortCommand).slice(0, 2).join(' · ')}${errors ? ` · ${plural(errors, 'error')}` : ''}${
|
||||
edits.length ? ` · writes ${plural(unique(edits.map((d) => d.file)).length, 'file')}` : ''
|
||||
}`;
|
||||
@@ -2079,7 +2175,7 @@ function decorate(draft: DraftPanel, events: StoryEvent[], turn: StoryTurn, inde
|
||||
case 'montage':
|
||||
panel.notes = notesOf(evs.filter((e) => e.tool === 'Bash'));
|
||||
const added = evs.filter((e) => e.intent === 'prompt').length;
|
||||
story = `Claude keeps going${added ? ` (you add ${plural(added, 'thing')})` : ''}: ${montageSummary(evs)}.`;
|
||||
story = `Claude ${index <= 1 ? 'gets started' : 'keeps going'}${added ? ` (you add ${plural(added, 'thing')})` : ''}: ${montageSummary(evs)}.`;
|
||||
break;
|
||||
case 'complete':
|
||||
break;
|
||||
@@ -2093,13 +2189,15 @@ function completePanel(turn: StoryTurn, page: Omit<StoryPage, 'panels'>, trailin
|
||||
const done = turn.durationMs !== undefined ? `Done in ${roughDuration(turn.durationMs)}.` : 'Done.';
|
||||
const story = turn.interrupted
|
||||
? 'You stop Claude here.'
|
||||
: turn.endedBy === 'next prompt'
|
||||
? 'The trail stops here: the next prompt starts a new turn.'
|
||||
: running
|
||||
? `Claude's part is done${turn.durationMs !== undefined ? ` in ${roughDuration(turn.durationMs)}` : ''}. ${
|
||||
running === 1 ? 'A helper is' : `${running} helpers are`
|
||||
} still working.`
|
||||
: done;
|
||||
: page.noneWorked
|
||||
? 'Nothing Claude tried worked, so the job is not done.'
|
||||
: turn.endedBy === 'next prompt'
|
||||
? 'The trail stops here: the next prompt starts a new turn.'
|
||||
: running
|
||||
? `Claude's part is done${turn.durationMs !== undefined ? ` in ${roughDuration(turn.durationMs)}` : ''}. ${
|
||||
running === 1 ? 'A helper is' : `${running} helpers are`
|
||||
} still working.`
|
||||
: done;
|
||||
const director = [
|
||||
turn.endedBy === 'next prompt' ? 'superseded by the next prompt' : (turn.endedBy ?? 'turn_duration'),
|
||||
turn.durationMs !== undefined ? exactDuration(turn.durationMs) : '',
|
||||
@@ -2165,6 +2263,8 @@ export function buildStoryPage(turn: StoryTurn): StoryPage {
|
||||
helpers: events.filter((e) => e.helper && e.tool !== 'Advisor').length,
|
||||
helpersRunning: events.filter((e) => e.helper?.running).length,
|
||||
};
|
||||
const calls = events.filter((e) => !NOT_TOOL_CALLS.has(e.intent));
|
||||
if (calls.length && calls.every((e) => e.ok === false)) base.noneWorked = true;
|
||||
if (turn.endedAt) base.endedAt = turn.endedAt;
|
||||
if (turn.durationMs !== undefined) base.durationMs = turn.durationMs;
|
||||
if (turn.ended) {
|
||||
|
||||
@@ -156,6 +156,8 @@ export interface StoryPanel {
|
||||
removed?: number;
|
||||
ok?: boolean;
|
||||
rounds?: number;
|
||||
/** A loop's test runs in order: whether each passed (absent while one is still running). */
|
||||
roundsOk?: boolean[];
|
||||
tests?: StoryTests;
|
||||
todos?: StoryTodo[];
|
||||
question?: { text: string; answer?: string };
|
||||
@@ -190,6 +192,8 @@ export interface StoryPage extends StoryTurnSummary {
|
||||
helpers: number;
|
||||
/** Background helpers that had not reported back when this page was drawn. */
|
||||
helpersRunning: number;
|
||||
/** Every tool call of the turn failed (a blocked command, nothing that worked): no celebration. */
|
||||
noneWorked?: boolean;
|
||||
}
|
||||
|
||||
/** `GET /api/sessions/:id/story/helpers/:agentId`: one helper's own steps, as one page. */
|
||||
|
||||
@@ -1080,7 +1080,7 @@ const StoryArt = (() => {
|
||||
const dots = Math.min(42, total);
|
||||
const fail = bad ? Math.max(1, Math.round((bad / total) * dots)) : 0;
|
||||
const fill = running ? (f * 0.6) % (dots + 6) : Math.min(dots, (f - 4) * 1.6);
|
||||
b = board(ctx, 16, 106, `${total} checks`, dots, fail, fill);
|
||||
b = board(ctx, 16, 106, `${total} tests`, dots, fail, fill);
|
||||
} else {
|
||||
b = verdict(ctx, 16, 106, 'checks', p.panel.ok, f);
|
||||
}
|
||||
@@ -1139,7 +1139,7 @@ const StoryArt = (() => {
|
||||
if (total) {
|
||||
const dots = Math.min(28, total);
|
||||
const fail = bad ? Math.max(1, Math.round((bad / total) * dots)) : 0;
|
||||
b = board(ctx, bx, 116, `${total} checks`, dots, fail, Math.min(dots, (f - 4) * 1.6));
|
||||
b = board(ctx, bx, 116, `${total} tests`, dots, fail, Math.min(dots, (f - 4) * 1.6));
|
||||
} else b = verdict(ctx, bx, 116, 'checks', p.panel.ok, f);
|
||||
hit('output', bx, 116, b.w, b.h, 'See the test output');
|
||||
}
|
||||
@@ -1169,17 +1169,50 @@ const StoryArt = (() => {
|
||||
ctx.closePath();
|
||||
inked(ctx, C.yellow, 2);
|
||||
ctx.restore();
|
||||
// Rounds and verdict stack under the top corner, which the balloon may take.
|
||||
sfx(
|
||||
ctx,
|
||||
`×${p.panel.rounds || 1}`,
|
||||
W - 60,
|
||||
214,
|
||||
30,
|
||||
C.yellow,
|
||||
-0.1,
|
||||
spring(f - 20, { damping: 9, stiffness: 160 })
|
||||
);
|
||||
// Every round's verdict, in order: a first run that failed (often the one you asked for) shows.
|
||||
const marks = p.panel.roundsOk || [];
|
||||
if (marks.length) {
|
||||
const shown = marks.slice(-6);
|
||||
const gap = 22;
|
||||
const x0 = W - 60 - ((shown.length - 1) * gap) / 2;
|
||||
shown.forEach((ok, k) => {
|
||||
const pr = spring(f - 20 - k * 6, { damping: 10, stiffness: 160 });
|
||||
if (pr <= 0.01) return;
|
||||
const cx = x0 + k * gap;
|
||||
ctx.save();
|
||||
ctx.translate(cx, 214);
|
||||
ctx.scale(pr, pr);
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, 9, 0, TAU);
|
||||
inked(ctx, ok ? C.go : C.magenta, 2.5);
|
||||
ctx.strokeStyle = C.balloon;
|
||||
ctx.lineWidth = 2.5;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.beginPath();
|
||||
if (ok) {
|
||||
ctx.moveTo(-4, 0);
|
||||
ctx.lineTo(-1, 3.5);
|
||||
ctx.lineTo(4.5, -3.5);
|
||||
} else {
|
||||
ctx.moveTo(-3.5, -3.5);
|
||||
ctx.lineTo(3.5, 3.5);
|
||||
ctx.moveTo(3.5, -3.5);
|
||||
ctx.lineTo(-3.5, 3.5);
|
||||
}
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
});
|
||||
} else
|
||||
sfx(
|
||||
ctx,
|
||||
`×${p.panel.rounds || 1}`,
|
||||
W - 60,
|
||||
214,
|
||||
30,
|
||||
C.yellow,
|
||||
-0.1,
|
||||
spring(f - 20, { damping: 9, stiffness: 160 })
|
||||
);
|
||||
// The verdict stamp only where it clears the board (a narrow panel shows the board alone).
|
||||
if (p.panel.ok !== undefined && W - 108 > bx + 131) {
|
||||
const word = p.panel.ok ? 'POW!' : 'KRAK!';
|
||||
@@ -1392,6 +1425,10 @@ const StoryArt = (() => {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (p.page.noneWorked) {
|
||||
agent(ctx, ax, 250, { frame: f, scale: sc, mood: 'neutral', l: [-34, -48], r: [34, -48] });
|
||||
return;
|
||||
}
|
||||
const colors = [C.cyan, C.magenta, C.yellow, C.go];
|
||||
for (let i = 0; i < 42; i++) {
|
||||
const x = rnd(i) * W;
|
||||
@@ -2272,7 +2309,9 @@ Object.assign(CodemanApp.prototype, {
|
||||
? 'Stopped!'
|
||||
: page.superseded
|
||||
? 'To be continued…'
|
||||
: 'Mission complete!';
|
||||
: page.noneWorked
|
||||
? 'Turn over'
|
||||
: 'Mission complete!';
|
||||
const chips = rt.splash.querySelector('.story-chips');
|
||||
const items = [];
|
||||
if (page.durationMs !== undefined) items.push({ text: this._storyDuration(page.durationMs) });
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"ok": null,
|
||||
"story": "You give Claude a job.",
|
||||
"director": "Prompt · 65 words",
|
||||
"balloon": "First write a short todo list for this job, then build a bash script rename_by_date.sh in this folder that renames *.jpg files to their modification time as YYYY-MM-DD_HHMMSS.jpg with a --dry-run flag, and while you do…",
|
||||
"balloon": "First write a short todo list for this job, then build a bash script rename_by_date.sh in this folder that renames *.jpg files to their modification time as YYYY-MM-DD_HHMMSS.jpg with a --dry-run flag, and while you do that use the Agent tool to have one helper agent write README.md for the script; finally test it on a few sample files you create in a samples folder.",
|
||||
"tests": null,
|
||||
"events": 1
|
||||
},
|
||||
@@ -33,7 +33,7 @@
|
||||
{
|
||||
"kind": "investigate",
|
||||
"ok": null,
|
||||
"story": "Claude reads 1 file to find its way around.",
|
||||
"story": "Claude looks around the project.",
|
||||
"director": "Bash ×2 · ls -la && git log --oneline | head; which shellcheck; bash…",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
@@ -42,11 +42,20 @@
|
||||
{
|
||||
"kind": "backup",
|
||||
"ok": null,
|
||||
"story": "Claude calls in 2 helpers.",
|
||||
"director": "Advisor · Agent · a second opinion · general-purpose · 6 calls inside",
|
||||
"story": "Claude asks the advisor for a second opinion.",
|
||||
"director": "Advisor · a second opinion",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
"events": 2
|
||||
"events": 1
|
||||
},
|
||||
{
|
||||
"kind": "backup",
|
||||
"ok": null,
|
||||
"story": "Claude sends 1 helper off to work in the background.",
|
||||
"director": "Agent · general-purpose · 6 calls inside",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
"events": 1
|
||||
},
|
||||
{
|
||||
"kind": "fix",
|
||||
@@ -62,7 +71,7 @@
|
||||
"ok": null,
|
||||
"story": "Claude works in the terminal.",
|
||||
"director": "Bash ×5 · rm -- ./-- && S=/tmp/claude/-work-cases-story-review-c/740e…",
|
||||
"balloon": "My mk -- ... helper call left a stray file named --.",
|
||||
"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,
|
||||
"events": 5
|
||||
},
|
||||
@@ -122,7 +131,7 @@
|
||||
"ok": null,
|
||||
"story": "Done in 21 seconds.",
|
||||
"director": "turn_duration · 21 s · 2 tool calls · 1 helper",
|
||||
"balloon": "I built rename_by_date.sh, the helper agent wrote README.md, and the script passed every test I ran on sample files in samples/.",
|
||||
"balloon": "I built rename_by_date.sh, the helper agent wrote README.md, and the script passed every test I ran on sample files in samples/. Nothing is committed because this folder isn't a git repository, even though the session context said it was.",
|
||||
"tests": null,
|
||||
"events": 0
|
||||
}
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
"ok": null,
|
||||
"story": "You give Claude a job.",
|
||||
"director": "Prompt · 41 words",
|
||||
"balloon": "Write mathx.py with a function clamp(value, low, high) that returns value limited to the range low..high and raises ValueError when low > high, then write test_mathx.py with exactly 5 pytest tests for it, and run them w…",
|
||||
"balloon": "Write mathx.py with a function clamp(value, low, high) that returns value limited to the range low..high and raises ValueError when low > high, then write test_mathx.py with exactly 5 pytest tests for it, and run them with python3 -m pytest -q.",
|
||||
"tests": null,
|
||||
"events": 1
|
||||
},
|
||||
{
|
||||
"kind": "investigate",
|
||||
"ok": null,
|
||||
"story": "Claude reads 1 file to find its way around.",
|
||||
"story": "Claude looks around the project.",
|
||||
"director": "Bash ×2 · ls -la && git status && git log --oneline -5 2>&1; python3… · 1 error",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
@@ -140,7 +140,7 @@
|
||||
"ok": null,
|
||||
"story": "You give Claude a job.",
|
||||
"director": "Prompt · 50 words",
|
||||
"balloon": "Call your EnterPlanMode tool to enter plan mode, then write a short 3-step plan for adding a wrap(value, low, high) function (modular wrap into the range) to mathx.py with 3 more pytest tests, and present that plan with…",
|
||||
"balloon": "Call your EnterPlanMode tool to enter plan mode, then write a short 3-step plan for adding a wrap(value, low, high) function (modular wrap into the range) to mathx.py with 3 more pytest tests, and present that plan with your ExitPlanMode tool. After I approve, implement it and run the tests.",
|
||||
"tests": null,
|
||||
"events": 1
|
||||
},
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
"ok": null,
|
||||
"story": "You give Claude a job.",
|
||||
"director": "Prompt · 67 words",
|
||||
"balloon": "Create a small JavaScript ES module slugify.mjs exporting slugify(str) (lowercase, trim, spaces and punctuation to single hyphens, no leading or trailing hyphens) plus slugify.test.mjs with 4 node:test tests; on purpose…",
|
||||
"balloon": "Create a small JavaScript ES module slugify.mjs exporting slugify(str) (lowercase, trim, spaces and punctuation to single hyphens, no leading or trailing hyphens) plus slugify.test.mjs with 4 node:test tests; on purpose make ONE test expectation wrong so the first run of node --test fails, run it, then fix that expectation, rerun until all pass, then git init if needed and git commit both files w…",
|
||||
"tests": null,
|
||||
"events": 1
|
||||
},
|
||||
{
|
||||
"kind": "investigate",
|
||||
"ok": null,
|
||||
"story": "Claude reads 1 file to find its way around.",
|
||||
"story": "Claude looks around the project.",
|
||||
"director": "Bash ×2 · ls -la && git status && git log --oneline -5 2>&1; node --v…",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
@@ -42,7 +42,7 @@
|
||||
{
|
||||
"kind": "loop",
|
||||
"ok": true,
|
||||
"story": "Fix and test, 2 rounds. It passes in the end.",
|
||||
"story": "Fix and test: the first run fails, the second passes.",
|
||||
"director": "2 checks · 3 edits · +35 −1 · last: ok",
|
||||
"balloon": "All 4 pass.",
|
||||
"tests": {
|
||||
@@ -98,14 +98,14 @@
|
||||
"ok": null,
|
||||
"story": "You give Claude a job.",
|
||||
"director": "Prompt · 38 words",
|
||||
"balloon": "Add a fifth node:test test for slugify('') to slugify.test.mjs that on purpose expects 'x' so the first run fails, run node --test, then fix that expectation to the empty string, rerun until all pass, and commit the cha…",
|
||||
"balloon": "Add a fifth node:test test for slugify('') to slugify.test.mjs that on purpose expects 'x' so the first run fails, run node --test, then fix that expectation to the empty string, rerun until all pass, and commit the change.",
|
||||
"tests": null,
|
||||
"events": 1
|
||||
},
|
||||
{
|
||||
"kind": "loop",
|
||||
"ok": true,
|
||||
"story": "Fix and test, 2 rounds. It passes in the end.",
|
||||
"story": "Fix and test: the first run fails, the second passes.",
|
||||
"director": "2 checks · 2 edits · +5 −1 · last: ok",
|
||||
"balloon": "All 5 pass.",
|
||||
"tests": {
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
"ok": null,
|
||||
"story": "You give Claude a job.",
|
||||
"director": "Prompt · 96 words",
|
||||
"balloon": "Build a small Python package inventory/ (a CLI to add, list and remove items kept in a JSON file) split into at least 4 modules (for example storage.py, models.py, commands.py, cli.py), with a pytest suite of at least 2…",
|
||||
"balloon": "Build a small Python package inventory/ (a CLI to add, list and remove items kept in a JSON file) split into at least 4 modules (for example storage.py, models.py, commands.py, cli.py), with a pytest suite of at least 25 tests in total; build it one module at a time, and after EACH module write its tests and run pytest, fixing any failures before moving on to the next module; then git init the fo…",
|
||||
"tests": null,
|
||||
"events": 1
|
||||
},
|
||||
{
|
||||
"kind": "montage",
|
||||
"ok": null,
|
||||
"story": "Claude keeps going: runs 4 commands.",
|
||||
"story": "Claude gets started: runs 4 commands.",
|
||||
"director": "Bash ×4",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
@@ -59,9 +59,9 @@
|
||||
},
|
||||
{
|
||||
"kind": "loop",
|
||||
"ok": false,
|
||||
"story": "Fix and test, 4 rounds. The tests pass, but another check fails.",
|
||||
"director": "5 checks · 10 edits · +694 · last: error",
|
||||
"ok": true,
|
||||
"story": "Build and test, 4 rounds. A check fails once, then everything passes.",
|
||||
"director": "5 checks · 10 edits · +694 · last: ok",
|
||||
"balloon": "All 4 modules are done: 63 tests pass and ruff is clean.",
|
||||
"tests": {
|
||||
"passed": 63,
|
||||
@@ -146,14 +146,14 @@
|
||||
"ok": null,
|
||||
"story": "You give Claude a job.",
|
||||
"director": "Prompt · 188 words",
|
||||
"balloon": "Now do these 15 small steps strictly one at a time, each as its own separate tool call, in exactly this order, never combining two steps into one call and without extra steps in between: 1) Read inventory/models.py with…",
|
||||
"balloon": "Now do these 15 small steps strictly one at a time, each as its own separate tool call, in exactly this order, never combining two steps into one call and without extra steps in between: 1) Read inventory/models.py with the Read tool; 2) with the Edit tool add the line 'Version 0.1.0.' at the end of README.md; 3) run 'python3 -m pytest -q' in Bash; 4) run 'uname -s' in Bash; 5) run 'git add -A &&…",
|
||||
"tests": null,
|
||||
"events": 1
|
||||
},
|
||||
{
|
||||
"kind": "montage",
|
||||
"ok": null,
|
||||
"story": "Claude keeps going: reads 1 file, runs 2 commands, changes files, runs checks, saves the work.",
|
||||
"story": "Claude gets started: reads 1 file, runs 2 commands, changes files, runs checks, saves the work.",
|
||||
"director": "Read · Edit · Bash ×4",
|
||||
"balloon": "Step 1:",
|
||||
"tests": null,
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
"ok": null,
|
||||
"story": "You give Claude a job.",
|
||||
"director": "Prompt · 52 words",
|
||||
"balloon": "Build a small Python command-line tool wcount.py in this folder that prints the line, word and character counts of the files given as arguments, like wc, with a --json flag that prints the counts as JSON, plus pytest te…",
|
||||
"balloon": "Build a small Python command-line tool wcount.py in this folder that prints the line, word and character counts of the files given as arguments, like wc, with a --json flag that prints the counts as JSON, plus pytest tests in test_wcount.py; run pytest and fix anything that fails until all tests pass.",
|
||||
"tests": null,
|
||||
"events": 1
|
||||
},
|
||||
{
|
||||
"kind": "investigate",
|
||||
"ok": null,
|
||||
"story": "Claude reads 1 file to find its way around.",
|
||||
"story": "Claude looks around the project.",
|
||||
"director": "Bash ×2 · ls -la && git log --oneline -5 2>&1; git branch -a; python3… · 1 error",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
@@ -42,7 +42,7 @@
|
||||
{
|
||||
"kind": "loop",
|
||||
"ok": true,
|
||||
"story": "Fix and test, 2 rounds. It passes in the end.",
|
||||
"story": "Fix and test: the first run fails, the second passes.",
|
||||
"director": "3 checks · 5 edits · +218 −1 · last: ok",
|
||||
"balloon": "All 19 tests pass, ruff check is clean, and the output matches real wc column for column.",
|
||||
"tests": {
|
||||
@@ -54,7 +54,7 @@
|
||||
{
|
||||
"kind": "save",
|
||||
"ok": true,
|
||||
"story": "Claude saves the work.",
|
||||
"story": "Claude writes 1 file and saves the work.",
|
||||
"director": "git commit -q -m \"chore: ignore Python and pytest caches · writes 1 file",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"ok": null,
|
||||
"story": "You give Claude a job.",
|
||||
"director": "Prompt · 121 words",
|
||||
"balloon": "Build a tiny Python command-line tool in this folder that turns text into URL slugs. Before doing anything else, ask me with your AskUserQuestion tool whether the tool should be named slugify or kebab (offer exactly tho…",
|
||||
"balloon": "Build a tiny Python command-line tool in this folder that turns text into URL slugs. Before doing anything else, ask me with your AskUserQuestion tool whether the tool should be named slugify or kebab (offer exactly those two options). After my answer, first write me your plan as a short numbered list, then write the pytest tests FIRST (at least 8 tests in tests/), run them with python3 -m pytest…",
|
||||
"tests": null,
|
||||
"events": 1
|
||||
},
|
||||
@@ -33,7 +33,7 @@
|
||||
{
|
||||
"kind": "investigate",
|
||||
"ok": null,
|
||||
"story": "Claude reads 1 file to find its way around.",
|
||||
"story": "Claude looks around the project.",
|
||||
"director": "Bash ×2 · ls -la && git status 2>&1 | head -5; git rev-parse --show-t…",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
@@ -51,7 +51,7 @@
|
||||
{
|
||||
"kind": "loop",
|
||||
"ok": true,
|
||||
"story": "Fix and test, 2 rounds. It passes in the end.",
|
||||
"story": "Fix and test: the first run fails, the second passes.",
|
||||
"director": "2 checks · 4 edits · +151 −1 · last: ok",
|
||||
"balloon": "All 19 test cases pass (13 test functions), and ruff check and ruff format both come back clean.",
|
||||
"tests": {
|
||||
@@ -80,8 +80,8 @@
|
||||
},
|
||||
{
|
||||
"kind": "save",
|
||||
"ok": false,
|
||||
"story": "Claude tries to send the work up, and the command fails.",
|
||||
"ok": true,
|
||||
"story": "Claude saves the work in 2 commits and uploads it, but a step after that fails.",
|
||||
"director": "git commit -q -F - · git push -u origin main · 1 error",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
"ok": null,
|
||||
"story": "You give Claude a job.",
|
||||
"director": "Prompt · 77 words",
|
||||
"balloon": "Create a tiny node project here: package.json (type module) and index.js exporting two functions add(a,b) and slugify(s). Then start TWO helpers with the Agent tool IN THE BACKGROUND (run_in_background: true): one write…",
|
||||
"balloon": "Create a tiny node project here: package.json (type module) and index.js exporting two functions add(a,b) and slugify(s). Then start TWO helpers with the Agent tool IN THE BACKGROUND (run_in_background: true): one writes README.md describing the project, the other writes CONTRIBUTING.md. While they work, write index.test.mjs using node:test with a few tests for both functions and run it with node…",
|
||||
"tests": null,
|
||||
"events": 1
|
||||
},
|
||||
{
|
||||
"kind": "investigate",
|
||||
"ok": null,
|
||||
"story": "Claude reads 1 file to find its way around.",
|
||||
"story": "Claude looks around the project.",
|
||||
"director": "Bash ×2 · ls -la && git log --oneline -3 2>&1; node --version · 1 error",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
@@ -42,7 +42,7 @@
|
||||
{
|
||||
"kind": "save",
|
||||
"ok": true,
|
||||
"story": "Claude saves the work.",
|
||||
"story": "Claude writes 2 files and saves the work.",
|
||||
"director": "git commit -q -m \"feat: add package.json and index.js with… · writes 2 files",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
@@ -60,7 +60,7 @@
|
||||
{
|
||||
"kind": "check",
|
||||
"ok": true,
|
||||
"story": "The tests pass.",
|
||||
"story": "Claude writes 1 file; the tests pass.",
|
||||
"director": "node --test index.test.mjs → ok · 8 passed · writes 1 file",
|
||||
"balloon": null,
|
||||
"tests": {
|
||||
@@ -212,7 +212,7 @@
|
||||
"ok": null,
|
||||
"story": "You give Claude a job.",
|
||||
"director": "Prompt · 56 words",
|
||||
"balloon": "Big refactor: split index.js into three modules under lib/ (lib/add.js, lib/slugify.js, lib/util.js with shared helpers), keep index.js as a re-export barrel, add full JSDoc to every function, add at least 15 more node:…",
|
||||
"balloon": "Big refactor: split index.js into three modules under lib/ (lib/add.js, lib/slugify.js, lib/util.js with shared helpers), keep index.js as a re-export barrel, add full JSDoc to every function, add at least 15 more node:test tests in separate files under test/, update README and CONTRIBUTING to match, run the tests after each step and commit each step separately.",
|
||||
"tests": null,
|
||||
"events": 1
|
||||
},
|
||||
@@ -228,7 +228,7 @@
|
||||
{
|
||||
"kind": "check",
|
||||
"ok": true,
|
||||
"story": "The tests pass.",
|
||||
"story": "Claude writes 4 files; the tests pass.",
|
||||
"director": "npm test 2>&1 → ok · 8 passed · writes 4 files",
|
||||
"balloon": "All 8 tests pass, the README examples give the same outputs, and the barrel exports only add and slugify.",
|
||||
"tests": {
|
||||
@@ -249,7 +249,7 @@
|
||||
{
|
||||
"kind": "check",
|
||||
"ok": true,
|
||||
"story": "The tests pass.",
|
||||
"story": "Claude writes 4 files; the tests pass.",
|
||||
"director": "npm test 2>&1 → ok · 8 passed · writes 4 files",
|
||||
"balloon": "Every @example output matches what the code actually returns, and 8/8 tests pass.",
|
||||
"tests": {
|
||||
@@ -303,7 +303,7 @@
|
||||
{
|
||||
"kind": "run",
|
||||
"ok": null,
|
||||
"story": "Claude works in the terminal.",
|
||||
"story": "Claude tries a command, and it is blocked.",
|
||||
"director": "Bash · sleep 65 · 1 error",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
@@ -312,9 +312,9 @@
|
||||
{
|
||||
"kind": "complete",
|
||||
"ok": null,
|
||||
"story": "Done in 6 seconds.",
|
||||
"story": "Nothing Claude tried worked, so the job is not done.",
|
||||
"director": "stop_hook_summary · 6 s · 1 tool call",
|
||||
"balloon": "Not done.",
|
||||
"balloon": "Not done. The harness blocked sleep 65: a sleep on its own in the foreground isn't allowed.",
|
||||
"tests": null,
|
||||
"events": 0
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"ok": null,
|
||||
"story": "You give Claude a job.",
|
||||
"director": "Prompt · 81 words",
|
||||
"balloon": "First reply with a numbered plan of exactly 4 steps (as text, not a tool). Then build csvstat.py, a Python CLI that prints row count and per-column min/max/mean of a CSV, with pytest tests. Do a dry run on a sample CSV…",
|
||||
"balloon": "First reply with a numbered plan of exactly 4 steps (as text, not a tool). Then build csvstat.py, a Python CLI that prints row count and per-column min/max/mean of a CSV, with pytest tests. Do a dry run on a sample CSV and save its log with `| tee /tmp/csvstat-dryrun.log`. Start ONE helper with the Agent tool, run_in_background: true and WITHOUT a name parameter, to write README.md; do not wait f…",
|
||||
"tests": null,
|
||||
"events": 1
|
||||
},
|
||||
@@ -33,7 +33,7 @@
|
||||
{
|
||||
"kind": "investigate",
|
||||
"ok": null,
|
||||
"story": "Claude reads 1 file to find its way around.",
|
||||
"story": "Claude looks around the project.",
|
||||
"director": "Bash ×2 · ls -la && git status && git branch -a && git log --oneline… · 1 error",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
@@ -60,7 +60,7 @@
|
||||
{
|
||||
"kind": "check",
|
||||
"ok": true,
|
||||
"story": "The tests pass, and Claude saves the work.",
|
||||
"story": "Claude writes 2 files; the tests pass, and Claude 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": {
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
"ok": null,
|
||||
"story": "You give Claude a job.",
|
||||
"director": "Prompt · 56 words",
|
||||
"balloon": "Build a tiny single-page pomodoro timer in this folder: index.html with inline CSS, timer.js exporting the timer logic as plain functions, and timer.test.mjs using node:test that covers start, pause and reset; run the t…",
|
||||
"balloon": "Build a tiny single-page pomodoro timer in this folder: index.html with inline CSS, timer.js exporting the timer logic as plain functions, and timer.test.mjs using node:test that covers start, pause and reset; run the tests with node --test until they pass, then git init and commit everything with a short message, with no remote and no push.",
|
||||
"tests": null,
|
||||
"events": 1
|
||||
},
|
||||
{
|
||||
"kind": "investigate",
|
||||
"ok": null,
|
||||
"story": "Claude reads 2 files to find its way around.",
|
||||
"story": "Claude reads 1 file to find its way around.",
|
||||
"director": "Bash ×2 · ls -la && git status 2>&1 | head; git log --oneline 2>&1 |…",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
@@ -33,7 +33,7 @@
|
||||
{
|
||||
"kind": "run",
|
||||
"ok": null,
|
||||
"story": "Claude works in the terminal.",
|
||||
"story": "Claude tries a command, and it fails.",
|
||||
"director": "Bash · grep -oE '\"(permissions|env|allow|deny)\"|[A-Za-z_]*(TOKEN|K… · 1 error",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
|
||||
@@ -68,7 +68,9 @@ describe('Story Mode on real sessions', () => {
|
||||
const panel = (name: string, turn: number, kind: string) => pagesOf(name)[turn].panels.find((p) => p.kind === kind);
|
||||
|
||||
it('a fix, a failing test (exit code hidden by a pipe), a fix and a pass fold into one loop', () => {
|
||||
expect(panel('python-cli-fix-loop', 0, 'loop')?.story).toBe('Fix and test, 2 rounds. It passes in the end.');
|
||||
expect(panel('python-cli-fix-loop', 0, 'loop')?.story).toBe(
|
||||
'Fix and test: the first run fails, the second passes.'
|
||||
);
|
||||
});
|
||||
|
||||
it('a prompt typed mid-turn (a queued_command attachment) is drawn where Claude took it in', () => {
|
||||
@@ -93,7 +95,9 @@ 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('The tests pass, and Claude saves the work.');
|
||||
expect(panel('text-plan-dry-run-helper', 0, 'check')?.story).toBe(
|
||||
'Claude writes 2 files; the tests pass, and Claude 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 });
|
||||
});
|
||||
@@ -106,10 +110,19 @@ describe('Story Mode on real sessions', () => {
|
||||
expect(pages[2].filesChanged).toBe(2);
|
||||
});
|
||||
|
||||
it('a push whose command failed says so', () => {
|
||||
it('a push that went through, with a step after it that failed, says both', () => {
|
||||
expect(panel('question-tests-first-push', 0, 'save')).toMatchObject({
|
||||
ok: false,
|
||||
story: 'Claude tries to send the work up, and the command fails.',
|
||||
ok: true,
|
||||
story: 'Claude saves the work in 2 commits and uploads it, but a step after that fails.',
|
||||
});
|
||||
});
|
||||
|
||||
it('a blocked command ends a turn where nothing worked, without a celebration', () => {
|
||||
const last = pagesOf('teammates-then-interrupt')[4];
|
||||
expect(last.panels.map((p) => p.story)).toEqual([
|
||||
'You give Claude a job.',
|
||||
'Claude tries a command, and it is blocked.',
|
||||
'Nothing Claude tried worked, so the job is not done.',
|
||||
]);
|
||||
});
|
||||
});
|
||||
+40
-7
@@ -129,6 +129,15 @@ describe('parseTestCounts beyond "N passed"', () => {
|
||||
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);
|
||||
// The last verdict wins: a formatter's preview, then the fix.
|
||||
expect(
|
||||
reportsProblems('1 file would be reformatted\n1 file reformatted\nAll checks passed!\n10 files already formatted')
|
||||
).toBe(false);
|
||||
expect(
|
||||
reportsProblems(
|
||||
'All checks passed!\nWould reformat: a.py\n1 file would be reformatted, 9 files already formatted'
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('a line that only mentions a failure is not a summary', () => {
|
||||
@@ -736,7 +745,7 @@ describe('buildStoryPage', () => {
|
||||
expect(byKind.backup.caption.story).toBe('Claude calls in 1 helper.');
|
||||
expect(byKind.backup.caption.director).toContain('3 calls inside');
|
||||
expect(byKind.loop).toMatchObject({ rounds: 2, ok: true, added: 2, removed: 1 });
|
||||
expect(byKind.loop.caption.story).toBe('Fix and test, 2 rounds. It passes in the end.');
|
||||
expect(byKind.loop.caption.story).toBe('Fix and test: the first run fails, the second passes.');
|
||||
expect(byKind.save.caption.story).toBe('Claude saves the work.');
|
||||
expect(byKind.complete.caption.story).toBe('Done in 7 minutes.');
|
||||
expect(byKind.complete.caption.director).toContain('7 min 12 s');
|
||||
@@ -749,7 +758,11 @@ describe('buildStoryPage', () => {
|
||||
text: 'The checkout button does nothing on phones. Can you fix it?',
|
||||
});
|
||||
expect(page.panels[1].balloon).toEqual({ who: 'claude', text: 'Let me see how checkout works.' });
|
||||
expect(page.panels[page.panels.length - 1].balloon).toEqual({ who: 'claude', text: 'Fixed.' });
|
||||
// A first sentence this short says too little alone: the next one comes along.
|
||||
expect(page.panels[page.panels.length - 1].balloon).toEqual({
|
||||
who: 'claude',
|
||||
text: 'Fixed. The banner sat on top of the button.',
|
||||
});
|
||||
});
|
||||
|
||||
it('lists files and keeps edits reachable from the panel', () => {
|
||||
@@ -812,7 +825,7 @@ describe('buildStoryPage', () => {
|
||||
end(1000),
|
||||
]);
|
||||
const loop = page.panels.find((p) => p.kind === 'loop')!;
|
||||
expect(loop.caption.story).toBe('Fix and test, 2 rounds. It passes in the end.');
|
||||
expect(loop.caption.story).toBe('Fix and test: the first run fails, the second passes.');
|
||||
expect(loop.balloon?.text).toBe('All 4 tests pass now.');
|
||||
expect(loop.tests).toEqual({ passed: 4, failed: 0 });
|
||||
});
|
||||
@@ -830,7 +843,7 @@ describe('buildStoryPage', () => {
|
||||
expect(check.caption.director).toContain('0 passed, 1 error');
|
||||
const save = page.panels.find((p) => p.kind === 'save')!;
|
||||
expect(save.ok).toBe(false);
|
||||
expect(save.caption.story).toBe('Claude tries to send the work up, and the command fails.');
|
||||
expect(save.caption.story).toBe('Claude tries to upload the work, and the command fails.');
|
||||
expect(save.caption.director).toContain('1 error');
|
||||
});
|
||||
|
||||
@@ -857,8 +870,8 @@ describe('buildStoryPage', () => {
|
||||
expect(page.filesChanged).toBe(1);
|
||||
const check = page.panels.find((p) => p.kind === 'check')!;
|
||||
expect(check.caption.director).toBe('node --test → ok · 3 passed · writes 1 file · 1 commit');
|
||||
// The commit at the end of the test one-liner is not lost: the check says it saved.
|
||||
expect(check.caption.story).toBe('The tests pass, and Claude saves the work.');
|
||||
// The commit at the end of the test one-liner is not lost, nor the file written: the check says both.
|
||||
expect(check.caption.story).toBe('Claude writes 1 file; the tests pass, and Claude saves the work.');
|
||||
});
|
||||
|
||||
it('one-line printf/echo writes and the files Claude Code says a command changed are edits', () => {
|
||||
@@ -914,6 +927,26 @@ describe('buildStoryPage', () => {
|
||||
expect(heredocWrites('tee -a notes.md <<EOF\nx\nEOF', '/w').map((d) => d.file)).toEqual(['/w/notes.md']);
|
||||
});
|
||||
|
||||
it('a loop keeps every run: a lint failure inside a round that passed in the end still shows', () => {
|
||||
const page = pageOf([
|
||||
prompt('build'),
|
||||
...edit('e1', '/w/a.py', ['+a = 1']),
|
||||
...bash('b1', 'pytest -q', 'Run tests', '3 passed in 0.1s'),
|
||||
...edit('e2', '/w/b.py', ['+b = 1']),
|
||||
...bash(
|
||||
'b2',
|
||||
'pytest -q; ruff format --check .',
|
||||
'Run tests and format check',
|
||||
'6 passed in 0.1s\n1 file would be reformatted'
|
||||
),
|
||||
...bash('b3', 'ruff format . && pytest -q', 'Format and retest', '1 file reformatted\n6 passed in 0.1s'),
|
||||
end(1000),
|
||||
]);
|
||||
const loop = page.panels.find((p) => p.kind === 'loop')!;
|
||||
expect(loop.roundsOk).toEqual([true, false, true]);
|
||||
expect(loop.caption.story).toBe('Build and test, 2 rounds. A check fails once, then everything passes.');
|
||||
});
|
||||
|
||||
it('checks that all pass from the start read as building, not fixing', () => {
|
||||
const page = pageOf([
|
||||
prompt('build it'),
|
||||
@@ -1026,7 +1059,7 @@ describe('buildStoryPage', () => {
|
||||
end(1000),
|
||||
]);
|
||||
expect(page.panels.find((p) => p.kind === 'save')!.caption.story).toBe(
|
||||
'Claude saves the work in 2 commits and sends it up.'
|
||||
'Claude saves the work in 2 commits and uploads it.'
|
||||
);
|
||||
const lines: unknown[] = [prompt('many steps')];
|
||||
for (let i = 0; i < 14; i++) {
|
||||
|
||||
Reference in new issue
Block a user