mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 02:29:33 +01:00
fix(story-mode): a second opinion is a line in the next panel, not a panel of its own
From review round 9: "Claude asks a stronger AI model for a second
opinion" stood as a whole panel on almost every page, and the reader
never learns what it said (the answer is encrypted). An advisor call now
rides on the panel of the step after it (or the last one, when nothing
follows), counted in `secondOpinion`: a small line under the caption,
"after asking a stronger AI model for a second opinion" ("then asks…"
when it came last), and "· after 1 second opinion" in the Director's
cut. Pages lose a panel of noise and keep room for the work.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
555efef53d
commit
a80638308b
17 files changed
+137
-240
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. 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`. 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 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), `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`).
|
||||
|
||||
|
||||
+30
-1
@@ -2082,7 +2082,19 @@ function groupPanels(events: StoryEvent[]): {
|
||||
const panels: DraftPanel[] = [];
|
||||
let pendingSay: string | undefined;
|
||||
let lastSaid: StoryEvent | undefined;
|
||||
// A second opinion (the advisor) is not a step of its own (its answer is not readable anyway): it
|
||||
// rides on the panel of the step after it, or the last one when nothing follows.
|
||||
let opinions: number[] = [];
|
||||
const takeOpinions = (panel: DraftPanel) => {
|
||||
if (!opinions.length) return;
|
||||
panel.events = [...panel.events, ...opinions].sort((a, b) => a - b);
|
||||
opinions = [];
|
||||
};
|
||||
events.forEach((ev, idx) => {
|
||||
if (ev.tool === 'Advisor') {
|
||||
opinions.push(idx);
|
||||
return;
|
||||
}
|
||||
if (ev.intent === 'say' && !ev.todos) {
|
||||
pendingSay ??= balloonText(ev.label);
|
||||
lastSaid ??= ev;
|
||||
@@ -2098,6 +2110,7 @@ function groupPanels(events: StoryEvent[]): {
|
||||
(events[last.events[0]].tool === 'Advisor') !== (ev.tool === 'Advisor');
|
||||
if (last && last.kind === kind && MERGEABLE.has(kind) && !advisorMix) {
|
||||
last.events.push(idx);
|
||||
takeOpinions(last);
|
||||
if (pendingSay && !last.balloon) last.balloon = { who: 'claude', text: pendingSay };
|
||||
} else {
|
||||
const panel: DraftPanel = { kind, events: [idx] };
|
||||
@@ -2119,10 +2132,16 @@ function groupPanels(events: StoryEvent[]): {
|
||||
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 };
|
||||
takeOpinions(panel);
|
||||
panels.push(panel);
|
||||
}
|
||||
pendingSay = undefined;
|
||||
});
|
||||
if (opinions.length) {
|
||||
const last = panels[panels.length - 1];
|
||||
if (last) takeOpinions(last);
|
||||
else panels.push({ kind: 'backup', events: opinions });
|
||||
}
|
||||
// The closing words keep a caveat that follows them ("Nothing is committed: this is no git repo.").
|
||||
return {
|
||||
panels,
|
||||
@@ -2282,8 +2301,12 @@ function plannedFailure(turn: StoryTurn, events: StoryEvent[]): false | 'asked'
|
||||
}
|
||||
|
||||
function decorate(draft: DraftPanel, events: StoryEvent[], turn: StoryTurn, index: number): StoryPanel {
|
||||
const evs = draft.events.map((i) => events[i]);
|
||||
const all = draft.events.map((i) => events[i]);
|
||||
// Second opinions riding on this step are counted, never part of its story.
|
||||
const opinions = all.filter((e) => e.tool === 'Advisor').length;
|
||||
const evs = opinions && opinions < all.length ? all.filter((e) => e.tool !== 'Advisor') : all;
|
||||
const panel: StoryPanel = { kind: draft.kind, events: draft.events, caption: { story: '', director: '' } };
|
||||
if (evs !== all) panel.secondOpinion = opinions;
|
||||
if (draft.balloon) panel.balloon = draft.balloon;
|
||||
const edits = evs.flatMap(editsOf);
|
||||
const checks = evs.filter((e) => e.intent === 'test' || e.intent === 'build');
|
||||
@@ -2666,6 +2689,12 @@ function decorate(draft: DraftPanel, events: StoryEvent[], turn: StoryTurn, inde
|
||||
case 'complete':
|
||||
break;
|
||||
}
|
||||
if (panel.secondOpinion) {
|
||||
// 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')}`;
|
||||
}
|
||||
panel.caption = { story, director };
|
||||
return panel;
|
||||
}
|
||||
|
||||
@@ -174,6 +174,8 @@ export interface StoryPanel {
|
||||
removed?: number;
|
||||
ok?: boolean;
|
||||
rounds?: number;
|
||||
/** Second opinions (the advisor) Claude asked for right before this step: not a step of their own. */
|
||||
secondOpinion?: number;
|
||||
/** A loop's test runs in order: whether each passed, `null` for one still running. */
|
||||
roundsOk?: (boolean | null)[];
|
||||
tests?: StoryTests;
|
||||
|
||||
@@ -340,6 +340,19 @@ body.story-open {
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
}
|
||||
.story-cap-note {
|
||||
display: block;
|
||||
margin-top: 3px;
|
||||
font: 600 10.5px/1.25 var(--story-mono);
|
||||
letter-spacing: 0.01em;
|
||||
text-transform: none;
|
||||
color: var(--story-cap-ink);
|
||||
opacity: 0.78;
|
||||
}
|
||||
.story-cap-note[hidden],
|
||||
.story-overlay.is-director .story-cap-note {
|
||||
display: none;
|
||||
}
|
||||
.story-hit:hover,
|
||||
.story-hit:focus-visible {
|
||||
outline: 3px dashed var(--story-magenta) !important;
|
||||
|
||||
@@ -2375,7 +2375,11 @@ Object.assign(CodemanApp.prototype, {
|
||||
capStory.className = 'story-cap-story';
|
||||
const capDir = document.createElement('span');
|
||||
capDir.className = 'story-cap-dir';
|
||||
cap.append(capStory, capDir);
|
||||
// A second opinion Claude asked for before this step: a line, not a panel of its own.
|
||||
const capNote = document.createElement('span');
|
||||
capNote.className = 'story-cap-note';
|
||||
capNote.hidden = true;
|
||||
cap.append(capStory, capDir, capNote);
|
||||
const balloon = document.createElement('p');
|
||||
balloon.className = 'story-balloon';
|
||||
balloon.hidden = true;
|
||||
@@ -2415,6 +2419,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
panel: null,
|
||||
capStory,
|
||||
capDir,
|
||||
capNote,
|
||||
balloon,
|
||||
liveTag,
|
||||
splash,
|
||||
@@ -2484,6 +2489,15 @@ Object.assign(CodemanApp.prototype, {
|
||||
: 'story-balloon--claude'
|
||||
}`;
|
||||
rt.liveTag.hidden = !p.live;
|
||||
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));
|
||||
rt.capNote.hidden = !p.secondOpinion;
|
||||
rt.capNote.textContent = p.secondOpinion
|
||||
? `${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.
|
||||
rt.el.setAttribute('aria-label', `Panel ${index + 1} of ${page.panels.length}`);
|
||||
|
||||
@@ -51,24 +51,13 @@
|
||||
{
|
||||
"kind": "backup",
|
||||
"ok": null,
|
||||
"story": "Claude asks a stronger AI model for a second opinion, but gets no answer.",
|
||||
"director": "Advisor · a second opinion",
|
||||
"story": "Claude calls in 2 helpers.",
|
||||
"director": "Agent · general-purpose · 6 calls inside · after 1 second opinion",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
"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,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
"events": 1
|
||||
"events": 2
|
||||
},
|
||||
{
|
||||
"kind": "fix",
|
||||
|
||||
@@ -37,27 +37,16 @@
|
||||
"printed": null,
|
||||
"events": 2
|
||||
},
|
||||
{
|
||||
"kind": "backup",
|
||||
"ok": null,
|
||||
"story": "Claude asks a stronger AI model for a second opinion.",
|
||||
"director": "Advisor · a second opinion",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
"events": 1
|
||||
},
|
||||
{
|
||||
"kind": "fix",
|
||||
"ok": null,
|
||||
"story": "Claude writes 2 new files.",
|
||||
"director": "Write ×2 · mathx.py +11 · test_mathx.py +26",
|
||||
"director": "Write ×2 · mathx.py +11 · test_mathx.py +26 · after 1 second opinion",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
"events": 2
|
||||
"events": 3
|
||||
},
|
||||
{
|
||||
"kind": "check",
|
||||
@@ -186,38 +175,16 @@
|
||||
"printed": null,
|
||||
"events": 1
|
||||
},
|
||||
{
|
||||
"kind": "plan",
|
||||
"ok": null,
|
||||
"story": "Claude switches to plan mode to think it through.",
|
||||
"director": "EnterPlanMode",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
"events": 1
|
||||
},
|
||||
{
|
||||
"kind": "backup",
|
||||
"ok": null,
|
||||
"story": "Claude asks a stronger AI model for a second opinion.",
|
||||
"director": "Advisor · a second opinion",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
"events": 1
|
||||
},
|
||||
{
|
||||
"kind": "plan",
|
||||
"ok": null,
|
||||
"story": "Claude writes a plan: 3 steps.",
|
||||
"director": "Write · plan file · 3 steps",
|
||||
"director": "EnterPlanMode · Write · plan file · 3 steps · after 1 second opinion",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
"events": 1
|
||||
"events": 3
|
||||
},
|
||||
{
|
||||
"kind": "ask",
|
||||
|
||||
+8
-30
@@ -27,37 +27,26 @@
|
||||
"events": 1
|
||||
},
|
||||
{
|
||||
"kind": "montage",
|
||||
"kind": "investigate",
|
||||
"ok": null,
|
||||
"story": "Claude gets started: runs 3 commands.",
|
||||
"director": "Bash ×3",
|
||||
"story": "Claude looks around the project.",
|
||||
"director": "Bash ×2 · ls -la && git log --oneline -5 2>&1; git status; python3 --…",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
"events": 3
|
||||
},
|
||||
{
|
||||
"kind": "backup",
|
||||
"ok": null,
|
||||
"story": "Claude asks a stronger AI model for a second opinion.",
|
||||
"director": "Advisor · a second opinion",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
"events": 1
|
||||
"events": 2
|
||||
},
|
||||
{
|
||||
"kind": "run",
|
||||
"ok": null,
|
||||
"story": "Claude works in the terminal.",
|
||||
"director": "Bash · mkdir -p /work/cases/story-r6-a/tests",
|
||||
"director": "Bash ×2 · python3 -c \"import calc_stats\" 2>&1 | tail -1; ls pytest.in… · after 1 second opinion",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
"events": 1
|
||||
"events": 3
|
||||
},
|
||||
{
|
||||
"kind": "fix",
|
||||
@@ -125,23 +114,12 @@
|
||||
"kind": "save",
|
||||
"ok": true,
|
||||
"story": "Claude saves the work: \"implement calc_stats mean and median\".",
|
||||
"director": "git commit -q -F -",
|
||||
"director": "git commit -q -F - · then 1 second opinion",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
"events": 1
|
||||
},
|
||||
{
|
||||
"kind": "backup",
|
||||
"ok": null,
|
||||
"story": "Claude asks a stronger AI model for a second opinion.",
|
||||
"director": "Advisor · a second opinion",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
"events": 1
|
||||
"events": 2
|
||||
},
|
||||
{
|
||||
"kind": "complete",
|
||||
|
||||
@@ -37,22 +37,11 @@
|
||||
"printed": null,
|
||||
"events": 2
|
||||
},
|
||||
{
|
||||
"kind": "backup",
|
||||
"ok": null,
|
||||
"story": "Claude asks a stronger AI model for a second opinion.",
|
||||
"director": "Advisor · a second opinion",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
"events": 1
|
||||
},
|
||||
{
|
||||
"kind": "loop",
|
||||
"ok": true,
|
||||
"story": "The first run fails on purpose, as you asked; the second passes.",
|
||||
"director": "2 checks · 3 edits · +35 −1 · last: ok",
|
||||
"director": "2 checks · 3 edits · +35 −1 · last: ok · after 1 second opinion",
|
||||
"balloon": "All 4 pass.",
|
||||
"tests": {
|
||||
"passed": 4,
|
||||
@@ -63,7 +52,7 @@
|
||||
true
|
||||
],
|
||||
"printed": null,
|
||||
"events": 5
|
||||
"events": 6
|
||||
},
|
||||
{
|
||||
"kind": "run",
|
||||
|
||||
+17
-24
@@ -27,37 +27,41 @@
|
||||
"events": 1
|
||||
},
|
||||
{
|
||||
"kind": "montage",
|
||||
"kind": "investigate",
|
||||
"ok": null,
|
||||
"story": "Claude gets started: runs 4 commands.",
|
||||
"director": "Bash ×4",
|
||||
"story": "Claude looks around the project.",
|
||||
"director": "Bash ×2 · ls -la && git status 2>&1 | head; git log --oneline 2>&1 |… · 1 error",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
"events": 4
|
||||
"events": 2
|
||||
},
|
||||
{
|
||||
"kind": "backup",
|
||||
"kind": "run",
|
||||
"ok": null,
|
||||
"story": "Claude asks a stronger AI model for a second opinion.",
|
||||
"director": "Advisor · a second opinion",
|
||||
"story": "Claude works in the terminal.",
|
||||
"director": "Bash ×2 · for t in ruff flake8 pyflakes pycodestyle pylint mypy black…",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
"events": 1
|
||||
"printed": [
|
||||
"name rc=1",
|
||||
"…@…",
|
||||
"email rc=0"
|
||||
],
|
||||
"events": 2
|
||||
},
|
||||
{
|
||||
"kind": "fix",
|
||||
"ok": null,
|
||||
"story": "Claude writes 3 files.",
|
||||
"director": "Bash · pyproject.toml +11 · .gitignore +11 · __init__.py +3",
|
||||
"director": "Bash · pyproject.toml +11 · .gitignore +11 · __init__.py +3 · after 1 second opinion",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
"events": 1
|
||||
"events": 2
|
||||
},
|
||||
{
|
||||
"kind": "mission",
|
||||
@@ -131,7 +135,7 @@
|
||||
"kind": "check",
|
||||
"ok": true,
|
||||
"story": "The tests pass.",
|
||||
"director": "python3 -m pytest -q -p no:cacheprovider 2>&1 → ok · 63 passed",
|
||||
"director": "python3 -m pytest -q -p no:cacheprovider 2>&1 → ok · 63 passed · then 1 second opinion",
|
||||
"balloon": null,
|
||||
"tests": {
|
||||
"passed": 63,
|
||||
@@ -139,18 +143,7 @@
|
||||
},
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
"events": 1
|
||||
},
|
||||
{
|
||||
"kind": "backup",
|
||||
"ok": null,
|
||||
"story": "Claude asks a stronger AI model for a second opinion.",
|
||||
"director": "Advisor · a second opinion",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
"events": 1
|
||||
"events": 2
|
||||
},
|
||||
{
|
||||
"kind": "complete",
|
||||
|
||||
@@ -37,22 +37,11 @@
|
||||
"printed": null,
|
||||
"events": 2
|
||||
},
|
||||
{
|
||||
"kind": "backup",
|
||||
"ok": null,
|
||||
"story": "Claude asks a stronger AI model for a second opinion.",
|
||||
"director": "Advisor · a second opinion",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
"events": 1
|
||||
},
|
||||
{
|
||||
"kind": "loop",
|
||||
"ok": true,
|
||||
"story": "Fix and test, 2 rounds: a test fails and Claude fixes it, then a layout check fails and Claude fixes that; everything passes. Claude saves the work in 4 commits along the way.",
|
||||
"director": "3 checks · 5 edits · +218 −1 · last: ok",
|
||||
"director": "3 checks · 5 edits · +218 −1 · last: ok · after 1 second opinion",
|
||||
"balloon": "All 19 tests pass, ruff check is clean, and the output matches real wc column for column.",
|
||||
"tests": {
|
||||
"passed": 19,
|
||||
@@ -64,7 +53,7 @@
|
||||
true
|
||||
],
|
||||
"printed": null,
|
||||
"events": 7
|
||||
"events": 8
|
||||
},
|
||||
{
|
||||
"kind": "save",
|
||||
@@ -77,22 +66,11 @@
|
||||
"printed": null,
|
||||
"events": 1
|
||||
},
|
||||
{
|
||||
"kind": "backup",
|
||||
"ok": null,
|
||||
"story": "Claude asks a stronger AI model for a second opinion.",
|
||||
"director": "Advisor · a second opinion",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
"events": 1
|
||||
},
|
||||
{
|
||||
"kind": "run",
|
||||
"ok": null,
|
||||
"story": "Claude works in the terminal.",
|
||||
"director": "Bash · python3 wcount.py nope nope2; echo \"exit=$?\"; wc nope nope2…",
|
||||
"director": "Bash · python3 wcount.py nope nope2; echo \"exit=$?\"; wc nope nope2… · after 1 second opinion",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
@@ -101,7 +79,7 @@
|
||||
"wcount: nope2: No such file or directory",
|
||||
"0 0 0 total"
|
||||
],
|
||||
"events": 1
|
||||
"events": 2
|
||||
},
|
||||
{
|
||||
"kind": "complete",
|
||||
|
||||
@@ -48,22 +48,11 @@
|
||||
"printed": null,
|
||||
"events": 2
|
||||
},
|
||||
{
|
||||
"kind": "backup",
|
||||
"ok": null,
|
||||
"story": "Claude asks a stronger AI model for a second opinion.",
|
||||
"director": "Advisor · a second opinion",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
"events": 1
|
||||
},
|
||||
{
|
||||
"kind": "loop",
|
||||
"ok": true,
|
||||
"story": "Test first: the first run fails as planned, the second passes.",
|
||||
"director": "2 checks · 4 edits · +151 −1 · last: ok",
|
||||
"director": "2 checks · 4 edits · +151 −1 · last: ok · after 1 second opinion",
|
||||
"balloon": "All 19 test cases pass (13 test functions), and ruff check and ruff format both come back clean.",
|
||||
"tests": {
|
||||
"passed": 19,
|
||||
@@ -74,7 +63,7 @@
|
||||
true
|
||||
],
|
||||
"printed": null,
|
||||
"events": 6
|
||||
"events": 7
|
||||
},
|
||||
{
|
||||
"kind": "fix",
|
||||
@@ -117,7 +106,7 @@
|
||||
"kind": "check",
|
||||
"ok": true,
|
||||
"story": "The tests pass.",
|
||||
"director": "python3 -m pytest -q -p no:cacheprovider) → ok · 19 passed",
|
||||
"director": "python3 -m pytest -q -p no:cacheprovider) → ok · 19 passed · then 1 second opinion",
|
||||
"balloon": null,
|
||||
"tests": {
|
||||
"passed": 19,
|
||||
@@ -125,18 +114,7 @@
|
||||
},
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
"events": 1
|
||||
},
|
||||
{
|
||||
"kind": "backup",
|
||||
"ok": null,
|
||||
"story": "Claude asks a stronger AI model for a second opinion.",
|
||||
"director": "Advisor · a second opinion",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
"events": 1
|
||||
"events": 2
|
||||
},
|
||||
{
|
||||
"kind": "complete",
|
||||
|
||||
@@ -37,27 +37,16 @@
|
||||
"printed": null,
|
||||
"events": 2
|
||||
},
|
||||
{
|
||||
"kind": "backup",
|
||||
"ok": null,
|
||||
"story": "Claude asks a stronger AI model for a second opinion.",
|
||||
"director": "Advisor · a second opinion",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
"events": 1
|
||||
},
|
||||
{
|
||||
"kind": "save",
|
||||
"ok": true,
|
||||
"story": "Claude writes 2 files and saves the work: \"add package.json and index.js with add and slugify\".",
|
||||
"director": "git commit -q -m \"feat: add package.json and index.js with… · writes 2 files",
|
||||
"director": "git commit -q -m \"feat: add package.json and index.js with… · writes 2 files · after 1 second opinion",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
"events": 1
|
||||
"events": 2
|
||||
},
|
||||
{
|
||||
"kind": "backup",
|
||||
@@ -262,22 +251,11 @@
|
||||
"printed": null,
|
||||
"events": 1
|
||||
},
|
||||
{
|
||||
"kind": "backup",
|
||||
"ok": null,
|
||||
"story": "Claude asks a stronger AI model for a second opinion.",
|
||||
"director": "Advisor · a second opinion",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
"events": 1
|
||||
},
|
||||
{
|
||||
"kind": "check",
|
||||
"ok": true,
|
||||
"story": "Claude writes 4 files; the tests pass.",
|
||||
"director": "npm test 2>&1 → ok · 8 passed · writes 4 files",
|
||||
"director": "npm test 2>&1 → ok · 8 passed · writes 4 files · after 1 second opinion",
|
||||
"balloon": "All 8 tests pass, the README examples give the same outputs, and the barrel exports only add and slugify.",
|
||||
"tests": {
|
||||
"passed": 8,
|
||||
@@ -285,7 +263,7 @@
|
||||
},
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
"events": 1
|
||||
"events": 2
|
||||
},
|
||||
{
|
||||
"kind": "save",
|
||||
|
||||
@@ -48,27 +48,16 @@
|
||||
"printed": null,
|
||||
"events": 2
|
||||
},
|
||||
{
|
||||
"kind": "backup",
|
||||
"ok": null,
|
||||
"story": "Claude asks a stronger AI model for a second opinion.",
|
||||
"director": "Advisor · a second opinion",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
"events": 1
|
||||
},
|
||||
{
|
||||
"kind": "fix",
|
||||
"ok": null,
|
||||
"story": "Claude writes 4 new files.",
|
||||
"director": "Write ×4 · csvstat.py +115 · sample.csv +6 · test_csvstat.py +129",
|
||||
"director": "Write ×4 · csvstat.py +115 · sample.csv +6 · test_csvstat.py +129 · after 1 second opinion",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
"events": 4
|
||||
"events": 5
|
||||
},
|
||||
{
|
||||
"kind": "check",
|
||||
|
||||
+4
-26
@@ -51,27 +51,16 @@
|
||||
],
|
||||
"events": 1
|
||||
},
|
||||
{
|
||||
"kind": "backup",
|
||||
"ok": null,
|
||||
"story": "Claude asks a stronger AI model for a second opinion.",
|
||||
"director": "Advisor · a second opinion",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
"events": 1
|
||||
},
|
||||
{
|
||||
"kind": "fix",
|
||||
"ok": null,
|
||||
"story": "Claude writes 3 new files.",
|
||||
"director": "Write ×3 · timer.js +42 · timer.test.mjs +74 · index.html +120",
|
||||
"director": "Write ×3 · timer.js +42 · timer.test.mjs +74 · index.html +120 · after 1 second opinion",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
"events": 3
|
||||
"events": 4
|
||||
},
|
||||
{
|
||||
"kind": "check",
|
||||
@@ -91,23 +80,12 @@
|
||||
"kind": "save",
|
||||
"ok": true,
|
||||
"story": "Claude saves the work: \"add pomodoro timer with tests\".",
|
||||
"director": "git commit -q -m \"feat: add pomodoro timer with tests\" -m \"…",
|
||||
"director": "git commit -q -m \"feat: add pomodoro timer with tests\" -m \"… · then 1 second opinion",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
"events": 1
|
||||
},
|
||||
{
|
||||
"kind": "backup",
|
||||
"ok": null,
|
||||
"story": "Claude asks a stronger AI model for a second opinion.",
|
||||
"director": "Advisor · a second opinion",
|
||||
"balloon": null,
|
||||
"tests": null,
|
||||
"roundsOk": null,
|
||||
"printed": null,
|
||||
"events": 1
|
||||
"events": 2
|
||||
},
|
||||
{
|
||||
"kind": "complete",
|
||||
|
||||
@@ -866,6 +866,26 @@ describe('story-mode.js overlay', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("a second opinion is a line in the next panel's caption, not a panel of its own", async () => {
|
||||
const advisor = {
|
||||
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_redacted_result' } },
|
||||
],
|
||||
},
|
||||
};
|
||||
boot(storyOf([prompt('fix it'), advisor, ...edit('e1', '/work/demo-shop/a.py', ['+a = 1']), end(1000)]));
|
||||
app.openStoryMode(SID);
|
||||
await settle();
|
||||
expect($$('.story-panel').map((p) => p.dataset.kind)).toEqual(['mission', 'fix', 'complete']);
|
||||
const note = $('.story-panel[data-kind="fix"] .story-cap-note')!;
|
||||
expect(note.hidden).toBe(false);
|
||||
expect(note.textContent).toBe('after asking a stronger AI model for a second opinion');
|
||||
});
|
||||
|
||||
it('the final panel counts the commits of the whole turn, and says when the work was uploaded', async () => {
|
||||
boot(
|
||||
storyOf([
|
||||
|
||||
@@ -1256,8 +1256,10 @@ describe('buildStoryPage', () => {
|
||||
},
|
||||
end(1000),
|
||||
]);
|
||||
const backup = page.panels.find((p) => p.kind === 'backup')!;
|
||||
expect(backup.caption.story).toBe('Claude asks a stronger AI model for a second opinion.');
|
||||
// Not a step of its own: it rides on the panel before it, here the mission (nothing follows).
|
||||
expect(page.panels.map((p) => p.kind)).toEqual(['mission', 'complete']);
|
||||
expect(page.panels[0]).toMatchObject({ secondOpinion: 1 });
|
||||
expect(page.panels[0].caption.director).toMatch(/then 1 second opinion$/);
|
||||
expect(page.events[1]).toMatchObject({ tool: 'Advisor', ok: true, helper: { name: 'advisor' } });
|
||||
expect(page.events[1].helper!.result).toMatch(/encrypted/);
|
||||
expect(page.helpers).toBe(0);
|
||||
|
||||
Reference in new issue
Block a user