mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 10:39:32 +01:00
fix(story-mode): plans you approved, project rules, tag lookups and dry runs (review round 12)
From a reader who has never written code:
- "a save your prompt here did not ask for" on a save that was step 3 of
the plan you approved ("Verify and commit"): a commit step in an
approved plan asks for the save. When Claude says a project rule asks
for its commits (read on its whole message), the line says "…; your
project's rules do".
- "a check after that fails" read as a failed test where it was `git
describe` finding no tags: "the version-tag lookup after that fails
(there are no tags yet)".
- The dry run you asked for never showed: the terminal card took the
last command's edge-case errors. A dry run in the panel is now what the
card shows, its "a -> b" lines, under "Claude does the dry run you
asked for: it shows what would happen, changing nothing."
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
bcfef8af5b
commit
f6c540276c
12 files changed
+120
-31
No files matched your search
+49
-7
@@ -127,7 +127,7 @@ export type StoryEntry =
|
||||
/** Teammates (helpers started as a team) reporting back, matched to their helper by name. */
|
||||
teammates?: StoryTeammateReport[];
|
||||
}
|
||||
| { k: 'text'; at: string; text: string; keyPoints?: string[] }
|
||||
| { k: 'text'; at: string; text: string; keyPoints?: string[]; citesSaveRule?: boolean }
|
||||
| { k: 'tool'; at: string; id: string; name: string; input: ReducedInput }
|
||||
| {
|
||||
k: 'result';
|
||||
@@ -1493,7 +1493,15 @@ export function createStoryReducer({ sidechain = false }: { sidechain?: boolean
|
||||
const content = message.content;
|
||||
const textEntry = (text: string): StoryEntry => {
|
||||
const keyPoints = keyPointsOf(text);
|
||||
return { k: 'text', at, text: clip(text, STORY_LIMITS.textChars), ...(keyPoints.length ? { keyPoints } : {}) };
|
||||
// Read on the whole message: the label keeps only its start.
|
||||
const rule = RULE_SAVE.test(text.slice(0, 8000));
|
||||
return {
|
||||
k: 'text',
|
||||
at,
|
||||
text: clip(text, STORY_LIMITS.textChars),
|
||||
...(keyPoints.length ? { keyPoints } : {}),
|
||||
...(rule ? { citesSaveRule: true } : {}),
|
||||
};
|
||||
};
|
||||
if (typeof content === 'string') return content.trim() ? [textEntry(content)] : [];
|
||||
if (!Array.isArray(content)) return [];
|
||||
@@ -1933,6 +1941,7 @@ export function buildStoryTurns(entries: StoryEntry[]): StoryTurn[] {
|
||||
label: e.text,
|
||||
intent: 'say',
|
||||
...(e.keyPoints ? { keyPoints: e.keyPoints } : {}),
|
||||
...(e.citesSaveRule ? { citesSaveRule: true } : {}),
|
||||
});
|
||||
break;
|
||||
case 'tool': {
|
||||
@@ -2411,6 +2420,10 @@ function emptyAnswerWhat(e: StoryEvent): string {
|
||||
const ASKS_TO_SAVE =
|
||||
/\b(commit|commits|committed|git|push|pushed|pull request|check(ed)? in|ship|release|merge|version control)\b|\bpr\b|\b(save|saving|saved) (the |your |all |this |my |our )?(work|changes|progress|code|project)\b/i;
|
||||
|
||||
/** Claude saying a project rule asks for the commits ("CLAUDE.md says to commit after every change"). */
|
||||
const RULE_SAVE =
|
||||
/\b(CLAUDE\.md|project('s)? (rules|notes|instructions)|house rules)\b[^.\n]{0,120}\bcommit|\bcommit[^.\n]{0,120}\b(CLAUDE\.md|project('s)? (rules|notes|instructions)|house rules)\b/i;
|
||||
|
||||
/** The branch Claude created for this save (`git checkout -b x`, `git switch -c x`), when it is the first save on it. */
|
||||
function newBranchAt(indices: number[], events: StoryEvent[]): string | undefined {
|
||||
const firstAt = Math.min(...indices);
|
||||
@@ -2455,6 +2468,9 @@ function touchedBefore(file: string, before: StoryEvent[]): boolean {
|
||||
const GIT_LOOK =
|
||||
/^(?:(?:git (?:-C \S+ |--git-dir=\S+ )?(?:log|status|show|ls-remote|branch|remote|describe|rev-parse|diff)\b[^;&|\n]*|(?:echo|head|tail|wc|sort)\b[^;&|\n]*)\s*(?:&&|\|\||;|\||\n|$)\s*)+$/;
|
||||
|
||||
/** A dry run: a command that shows what it would do without doing it. */
|
||||
const DRY_RUN = /\bdry[- ]?run\b|--dry-run\b|--what-if\b/i;
|
||||
|
||||
/** One or two files by name ("calc.py and test_calc.py"), more by count ("3 files"). */
|
||||
const fileWords = (files: string[], noun = 'file'): string =>
|
||||
files.length && files.length <= 2 ? files.map((f) => basename(f)).join(' and ') : plural(files.length, noun);
|
||||
@@ -2831,13 +2847,19 @@ function decorate(draft: DraftPanel, events: StoryEvent[], turn: StoryTurn, inde
|
||||
case 'run': {
|
||||
panel.notes = notesOf(evs);
|
||||
// What it printed, so a dry run or a listing you asked to see is on the page, not only in the drawer.
|
||||
const lastOut = [...evs].reverse().find((e) => e.output?.trim() && !e.running);
|
||||
// A dry run in the panel is what the card shows, whatever ran after it (edge cases, a clean-up).
|
||||
const dryRun = evs.find(
|
||||
(e) => e.output?.trim() && !e.running && e.ok !== false && DRY_RUN.test(`${e.description ?? ''} ${e.label}`)
|
||||
);
|
||||
const lastOut = dryRun ?? [...evs].reverse().find((e) => e.output?.trim() && !e.running);
|
||||
if (lastOut?.output) {
|
||||
const lines = lastOut.output
|
||||
.split('\n')
|
||||
.map(printableLine)
|
||||
.filter((l): l is string => !!l);
|
||||
const picked = lastOut.ok === false ? lines.slice(-2) : lines.slice(0, 3);
|
||||
// Of a dry run, the lines that show what it would do ("a.jpg -> holiday_a.jpg").
|
||||
const shows = dryRun ? lines.filter((l) => /->|→|\bwould\b|dry[- ]?run\]/i.test(l)) : [];
|
||||
const picked = shows.length ? shows.slice(0, 3) : lastOut.ok === false ? lines.slice(-2) : lines.slice(0, 3);
|
||||
if (picked.length) panel.printed = picked;
|
||||
}
|
||||
story = evs.every((e) => e.running)
|
||||
@@ -2854,7 +2876,11 @@ function decorate(draft: DraftPanel, events: StoryEvent[], turn: StoryTurn, inde
|
||||
}.`
|
||||
: evs.every((e) => e.tool !== 'Bash')
|
||||
? 'Claude uses a tool.'
|
||||
: 'Claude works in the terminal.';
|
||||
: dryRun
|
||||
? events.some((e) => e.intent === 'prompt' && DRY_RUN.test(e.label))
|
||||
? 'Claude does the dry run you asked for: it shows what would happen, changing nothing.'
|
||||
: 'Claude does a dry run: it shows what would happen, changing nothing.'
|
||||
: 'Claude works in the terminal.';
|
||||
director = `${tools} · ${shortCommand(evs[0])}${errors ? ` · ${plural(errors, 'error')}` : ''}`;
|
||||
break;
|
||||
}
|
||||
@@ -2923,7 +2949,15 @@ function decorate(draft: DraftPanel, events: StoryEvent[], turn: StoryTurn, inde
|
||||
/\b(may|might|can|could|will) (fail|error)\b|\bexpected to fail\b|\b(fine|ok|okay) if it fails\b|\b(that is|that's) fine\b/i.test(
|
||||
asked
|
||||
);
|
||||
const stepAfter = `${looksOnly ? 'a check after that fails' : 'a step after that fails'}${allowed ? ', as you said it might' : ''}`;
|
||||
// `git describe` with no tags: a lookup of a version name, not a check of the work.
|
||||
const noTags = /No names found, cannot describe anything|No tags can describe/.test(out);
|
||||
const stepAfter = `${
|
||||
noTags
|
||||
? 'the version-tag lookup after that fails (there are no tags yet)'
|
||||
: looksOnly
|
||||
? 'a check after that fails'
|
||||
: 'a step after that fails'
|
||||
}${allowed ? ', as you said it might' : ''}`;
|
||||
story =
|
||||
savedAnyway || hiddenFail
|
||||
? uploadOnly
|
||||
@@ -3009,13 +3043,21 @@ function decorate(draft: DraftPanel, events: StoryEvent[], turn: StoryTurn, inde
|
||||
break;
|
||||
}
|
||||
const savesHere = draft.events.some((i) => (events[i].commits ?? 0) > 0);
|
||||
// A plan you approved that has a commit step asked for the save too ("Verify and commit").
|
||||
const planAsked = events.some(
|
||||
(e) =>
|
||||
e.tool === 'ExitPlanMode' &&
|
||||
e.question?.answer === 'Approved' &&
|
||||
(ASKS_TO_SAVE.test(e.label) || !!e.todos?.some((t) => ASKS_TO_SAVE.test(t.text)))
|
||||
);
|
||||
if (
|
||||
savesHere &&
|
||||
turn.origin === 'human' &&
|
||||
!planAsked &&
|
||||
!events.slice(0, Math.min(...draft.events)).some((e) => (e.commits ?? 0) > 0) &&
|
||||
!events.some((e) => e.intent === 'prompt' && ASKS_TO_SAVE.test(e.label))
|
||||
)
|
||||
panel.unaskedSave = true;
|
||||
panel.unaskedSave = events.some((e) => e.intent === 'say' && e.citesSaveRule) ? 'rule' : true;
|
||||
if (panel.secondOpinion) {
|
||||
// Asked before the step, after it (nothing following), or in the middle of a step's calls.
|
||||
const work = draft.events.filter((i) => events[i].tool !== 'Advisor');
|
||||
|
||||
+7
-2
@@ -137,6 +137,8 @@ export interface StoryEvent {
|
||||
* not taken, what you asked for. From the whole message, past the clip of `label`.
|
||||
*/
|
||||
keyPoints?: string[];
|
||||
/** Claude says a project rule asks for its commits ("CLAUDE.md's commit-after-each-change rule"). */
|
||||
citesSaveRule?: boolean;
|
||||
/** A save that ships the work: `gh pr create` (`pr`) or `git push` (`push`). */
|
||||
ships?: 'pr' | 'push';
|
||||
/** How many commits a Bash call makes (`git commit` segments). */
|
||||
@@ -174,8 +176,11 @@ export interface StoryPanel {
|
||||
removed?: number;
|
||||
ok?: boolean;
|
||||
rounds?: number;
|
||||
/** The turn's first save, when no prompt of the turn asked for one (a project rule, or Claude's own idea). */
|
||||
unaskedSave?: boolean;
|
||||
/**
|
||||
* The turn's first save, when no prompt of the turn (nor a plan you approved) asked for one: `rule`
|
||||
* when Claude said a project rule asks for it, `true` otherwise.
|
||||
*/
|
||||
unaskedSave?: true | 'rule';
|
||||
/** 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. */
|
||||
|
||||
@@ -2514,7 +2514,11 @@ Object.assign(CodemanApp.prototype, {
|
||||
const unanswered = asked.length > 0 && asked.every((i) => page.events[i].ok === false);
|
||||
const ask = unanswered ? 'tries to ask' : 'asks';
|
||||
const notes = [
|
||||
p.unaskedSave ? 'a save your prompt here did not ask for' : '',
|
||||
p.unaskedSave === 'rule'
|
||||
? "a save your prompt here did not ask for; your project's rules do"
|
||||
: p.unaskedSave
|
||||
? 'a save your prompt here did not ask for'
|
||||
: '',
|
||||
p.secondOpinion
|
||||
? `${
|
||||
before
|
||||
|
||||
Reference in new issue
Block a user