mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 02:29:33 +01:00
fix(story-mode): one verdict per call, failed saves, and loops that end at a check
From review round 2 (a question, failing tests first, and a push):
- A run that reported failed tests is now failed on the event itself, so
its drawer and wire tags agree with the panel (the TDD red run was a red
panel with a green "ok" tag; `; echo "exit=$?"` had hidden the exit code).
- A save whose command failed says so ("Claude tries to send the work up,
and the command fails."), stamps ERROR instead of SAVED, and counts the
error in the Director's cut. A save still running stamps nothing.
- A loop ends at its last check: README, .gitignore and the like written
after the tests passed are their own fix panel instead of disappearing
into the loop's diff.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
848566828f
commit
d7a6644c2f
4 files changed
+69
-13
No files matched your search
+23
-7
@@ -1008,7 +1008,11 @@ function applyResult(ev: StoryEvent, r: Extract<StoryEntry, { k: 'result' }>): v
|
||||
ev.edit = r.edit;
|
||||
ev.file = r.edit.file;
|
||||
}
|
||||
if (r.tests) ev.tests = r.tests;
|
||||
if (r.tests) {
|
||||
ev.tests = r.tests;
|
||||
// A run that reports failed tests failed, whatever its exit code (`pytest | tail` exits 0).
|
||||
if (r.tests.failed > 0) ev.ok = false;
|
||||
}
|
||||
if (r.helper && ev.helper) {
|
||||
const helper: StoryHelper = { ...ev.helper };
|
||||
if (r.helper.background && !r.isError) {
|
||||
@@ -1330,6 +1334,8 @@ function collapseLoops(panels: DraftPanel[]): DraftPanel[] {
|
||||
) {
|
||||
j++;
|
||||
}
|
||||
// A loop ends at a check: writes after the last check (docs, config) are their own panel.
|
||||
while (j > i && panels[j - 1].kind === 'fix') j--;
|
||||
const run = panels.slice(i, j);
|
||||
if (run.length >= 4) {
|
||||
out.push({
|
||||
@@ -1573,12 +1579,22 @@ function decorate(draft: DraftPanel, events: StoryEvent[], turn: StoryTurn, inde
|
||||
break;
|
||||
case 'save': {
|
||||
panel.notes = notesOf(evs);
|
||||
story = evs.some((e) => e.ships === 'pr')
|
||||
? 'Claude opens a pull request.'
|
||||
: evs.some((e) => e.ships === 'push')
|
||||
? 'Claude saves the work and sends it up.'
|
||||
: 'Claude saves the work.';
|
||||
director = evs.map(shortCommand).slice(0, 2).join(' · ');
|
||||
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' : '';
|
||||
story =
|
||||
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 save the work, and the command fails.'
|
||||
: ships === 'pr'
|
||||
? 'Claude opens a pull request.'
|
||||
: ships === 'push'
|
||||
? 'Claude saves the work and sends it up.'
|
||||
: 'Claude saves the work.';
|
||||
director = `${evs.map(shortCommand).slice(0, 2).join(' · ')}${errors ? ` · ${plural(errors, 'error')}` : ''}`;
|
||||
break;
|
||||
}
|
||||
case 'ask': {
|
||||
|
||||
+4
-1
@@ -109,7 +109,10 @@ export interface StoryEvent {
|
||||
tool: string;
|
||||
label: string;
|
||||
intent: StoryIntent;
|
||||
/** False when the tool result was flagged as an error; absent while the call is still running. */
|
||||
/**
|
||||
* False when the call failed: its result was flagged as an error, or it reported failed tests (an
|
||||
* exit code a pipe hid). Absent while the call is still running.
|
||||
*/
|
||||
ok?: boolean;
|
||||
pending?: boolean;
|
||||
/** The one file this event concerns (Read, Edit, Write), as the transcript gave it. */
|
||||
|
||||
@@ -1180,25 +1180,34 @@ const StoryArt = (() => {
|
||||
ctx.fillStyle = C.line;
|
||||
ctx.fillRect(bx + 44, by + 18, 32, 8);
|
||||
hit('output', bx - 6, by - 14, 132, 104, 'See what was saved');
|
||||
const sp = spring(f - 28, { damping: 8, stiffness: 160 });
|
||||
// The stamp says how it went: SAVED, or ERROR when the command failed; none while it runs.
|
||||
const failed = p.panel.ok === false;
|
||||
const stamp = failed ? 'ERROR' : 'SAVED';
|
||||
const sp = p.panel.ok === undefined ? 0 : spring(f - 28, { damping: 8, stiffness: 160 });
|
||||
if (sp > 0.01) {
|
||||
ctx.save();
|
||||
ctx.translate(bx + 60, by + 56);
|
||||
ctx.rotate(-0.18);
|
||||
ctx.rotate(failed ? 0.14 : -0.18);
|
||||
ctx.scale(sp, sp);
|
||||
ctx.font = `20px ${DISPLAY}`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
const w = ctx.measureText('SAVED').width + 18;
|
||||
const w = ctx.measureText(stamp).width + 18;
|
||||
rr(ctx, -w / 2, -15, w, 30, 4);
|
||||
ctx.strokeStyle = C.magenta;
|
||||
ctx.lineWidth = 3;
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = C.magenta;
|
||||
ctx.fillText('SAVED', 0, 1);
|
||||
ctx.fillText(stamp, 0, 1);
|
||||
ctx.restore();
|
||||
}
|
||||
agent(ctx, W - 50, 250, { frame: f, mood: 'happy', scale: 0.8, l: [-30, -52], r: [26, -128] });
|
||||
agent(ctx, W - 50, 250, {
|
||||
frame: f,
|
||||
mood: failed ? 'shock' : p.panel.ok === undefined ? 'focus' : 'happy',
|
||||
scale: 0.8,
|
||||
l: [-30, -52],
|
||||
r: failed ? [30, -60] : [26, -128],
|
||||
});
|
||||
},
|
||||
},
|
||||
ask: {
|
||||
|
||||
@@ -607,6 +607,34 @@ describe('buildStoryPage', () => {
|
||||
expect(loop.tests).toEqual({ passed: 4, failed: 0 });
|
||||
});
|
||||
|
||||
it('a failed save says so; the failing run is marked failed on the event itself', () => {
|
||||
const page = pageOf([
|
||||
prompt('ship'),
|
||||
...bash('b1', 'pytest -q; echo "exit=$?"', 'Run tests', '1 error in 0.22s\nexit=2'),
|
||||
...bash('b2', 'git push -u origin main && git -C remote.git log', 'Push', 'fatal: bad object', true),
|
||||
end(1000),
|
||||
]);
|
||||
expect(page.events[1]).toMatchObject({ ok: false, tests: { passed: 0, failed: 1 } });
|
||||
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.director).toContain('1 error');
|
||||
});
|
||||
|
||||
it('a loop ends at its last check; writes after it are a panel of their own', () => {
|
||||
const page = pageOf([
|
||||
prompt('tdd'),
|
||||
...edit('e1', '/w/test_a.py', ['+def test_a(): ...']),
|
||||
...bash('b1', 'pytest -q', 'Run tests', '1 failed in 0.1s'),
|
||||
...edit('e2', '/w/a.py', ['+a = 1']),
|
||||
...bash('b2', 'pytest -q', 'Run tests', '1 passed in 0.1s'),
|
||||
...edit('e3', '/w/README.md', ['+# a']),
|
||||
end(1000),
|
||||
]);
|
||||
expect(page.panels.map((p) => p.kind)).toEqual(['mission', 'loop', 'fix', 'complete']);
|
||||
expect(page.panels[2].files).toEqual(['/w/README.md']);
|
||||
});
|
||||
|
||||
it('what Claude says about a check that just ran stays on that check', () => {
|
||||
const page = pageOf([
|
||||
prompt('test then commit'),
|
||||
|
||||
Reference in new issue
Block a user