Files
Garry Tanandcapy-ai[bot] 90a170c941 v1.92.50.0 wave(autoplan): #3781 #3743 #3751 #3762 (#3788)
* fix(eval-plan): per-arm pilot projection and --seat-metered arms (#3653)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(autoplan): guard tolerates Claude Code's cwd-reset footer and recovers after an unreadable init (#3651)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes (journal reader strips the cwd-reset footer; an init with no text block no longer locks the session)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: journal reader skips a failed or unreadable init, as invocation() does

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(autoplan): --ceo-only runs the CEO review alone with zero questions (GST-537)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(eval-plan): all-seat pilot, single-attempt arm and --seat-metered CLI wiring

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(eval-plan): summed deviation adds arm deviations, matching the summed band (arms share rows)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes (test pins an unpriced seat-metered arm)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes (seat-metered declaration fixed per --out, price row still required, a real charge settles as spent)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: classify autoplan-ceo-only in the Codex host metadata

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: a seat-metered arm that settles a charge is projected as a priced arm

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: select autoplan-ceo-only on the skills it runs; say it needs Claude Code

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(skills): /eval-plan --seat-metered and per-arm projection

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(eval-plan): skill prose stays under its context budget and carries no $N token

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: run autoplan-ceo-only in the marathon lane

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(skills): a seat-metered arm leaves the seat set once it settles a charge

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: changelog entry for /autoplan --ceo-only

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(autoplan): init_failed covers unreadable init output and the cwd-reset line (#3651)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: count the autoplan-ceo-only paid file in the shard inventory

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(autoplan): #3651 tests pass the test-code type ratchet

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(dx): CAP finding titles carry a one-line description (GBRA-223 claim 2)

The DX error contract told both autoplan DX voices to title capped findings
exactly CAP4:<path>:<item>, so gates, tasks and reconcile saw a bare code.
The title is now CAP4:<path>:<item> <what fails>; the grader already parses
that shape.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(autoplan): reconcile pairs same-file findings that carry no line (GBRA-223 claim 1)

Plan reviews name files, rarely lines, and reconcile associated by location
only when both findings had a line, so findings both voices raised on one
file were filed new and native-only. A same-file pair with a missing line is
now associated as unverified (basis file), never confirmed; a CAP code no
longer counts in title comparison. DRIVER.md names submit --reconcile.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(autoplan): open Critical and High findings reach the gate as apply items (GBRA-223 claim 3)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(autoplan): a spend cap binds on every unattended host; --estimate-usd persists; DRIVER names --usage-usd (GBRA-223 claim 4)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(autoplan): run binds complete api results, names a truncated response, --max-tokens; DRIVER documents run (GBRA-223 claim 6)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(autoplan): carry an item-by-item answer when the later gate asks nothing new (GBRA-223 claim 7)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(dx): trim the compact error contract so plan-devex-review stays under its size pin

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes (p1b applies finding fixes, api attempts reserve no host default, cross-file title pairing, paired severity counted)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: review fixes (priced api reservations, no file-only twins, two-pass pairing, partial replies keep p1-dependent defaults open, outside refusal degrades)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* P7: record whose words answered a gate (--words-by agent:<id>), never carry an agent's own words, import each row under its own answerer (GEVA-2)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: review pass 2 (p1b needs --rec-hash after recommend; api calls send the output limit they reserve for)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* changelog: fragment for the /autoplan field-report wave (GST-672)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: captain record notes --words-by agent attribution

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: follow finding apply items in approval-class and Captain flow tests; bind run-api limit to DEFAULT_MAX_TOKENS

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* wave autoplan: lower the gemini plan-devex-review pin to its exact size after #3781's DX trim

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(autoplan): CAP titles on different paths never match by title in reconcile or confirm rounds (wave review)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(autoplan): a truncated api call settles its cost and a retry is admitted afresh against the cap

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(autoplan): an applied finding stays open until the revised plan is reviewed; a shared location alone never twins two findings

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(approval): a plan gate answered in an agent's own words is not owner approval

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(gate): a new answer replaces the earlier answer's authorship and words

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(autoplan): a failed api call with no usage keeps its attempt open; reservations price the bound prompt (wave fix-check)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(autoplan): a spend cap fails closed: only a call refused before sending keeps its attempt; a sent call with unknown usage settles at its reservation

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(autoplan): the 16384 output default applies to both providers; changelog for the wave's integrator fixes

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(autoplan): how run settles a failed api call; changelog scopes it to the native voice

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(autoplan): a spend cap fails closed on every exit of run; an unpriced model reserves the default once a cap is set

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(autoplan): every sent call settles; an unpriced model under a cap reserves the default

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(autoplan): DRIVER and help say a cap reserves in any session kind and how run settles a sent call

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-11-02 01:35:47 +00:00

122 lines
7.6 KiB
TypeScript

/**
* What the publication guard reads of its parent journal (CEO-2, ENG-4, ENG-5).
* The bounded reader's index keeps every owned record's events without content.
* This policy reloads in full only what the evaluation reads: every literal
* init and its result, the current call, and inside the invocation window
* (from the latest human turn at or before the bound invocation chain's first
* init) the results of guarded and close-packet Reads, structured Bash results,
* error results, assistant text after the first close-packet Read and
* mutations of the active plan; plus the earlier Read a dedup reply cites,
* even before the window.
*/
import { readOwnedClaudePublicTranscript, type IndexedRecord, type JournalPrefix, type OwnedRead,
type OwnedReadMeasure } from '../../lib/claude-owned-journal';
import { object, sameNativePath, type ClaudeParentPublicEvent, type NativePublicToolEvent } from '../../lib/claude-journal-records';
type Event = ClaudeParentPublicEvent;
type Use = NativePublicToolEvent & { order: number };
/** Claude Code's reply to a Read of an unchanged file it already returned in full. */
export const DEDUP_REPLY = 'Wasted call — file unchanged since your last Read. Refer to that earlier tool_result instead.';
/** Claude Code appends exactly this one line to a Bash result when the command's `cd` left the session cwd (#3651). */
const SHELL_CWD_RESET = /\nShell cwd was reset to [^\n]+$/;
/** A Bash result's JSON, without that one host footer; anything else must still be strict JSON. */
export function bashJson(text: string): any { return JSON.parse(text.replace(SHELL_CWD_RESET, '')); }
/** The longest Read input or Bash command the index keeps; a literal init is far shorter. */
const INDEX_INPUT_MAX = 4096;
export function textResult(event: Event): string | undefined {
if (event.kind !== 'result' || event.isError !== false) return;
if (typeof event.content === 'string') return event.content;
if (Array.isArray(event.content) && event.content.length === 1 && event.content[0]?.type === 'text' &&
typeof event.content[0].text === 'string') return event.content[0].text;
}
/** The hook's own classifiers, passed in so this policy and the evaluation cannot disagree. */
export interface GuardJournalTests {
/** A literal snapshot init's [source, active, restore], or undefined. */
init(command: unknown): string[] | undefined;
/** A phase driver or artifact Read ('phase'), a close-packet Read ('close'), or neither. */
read(file: unknown): 'phase' | 'close' | undefined;
reviewer(input: Record<string, unknown>): boolean;
}
function indexInput(tests: GuardJournalTests) {
return (name: string, input: Record<string, unknown>): Record<string, unknown> | undefined => {
if (name === 'Read') return JSON.stringify(input).length <= INDEX_INPUT_MAX ? input : undefined;
if (name === 'Bash') return typeof input.command === 'string' && input.command.length <= INDEX_INPUT_MAX ? { command: input.command } : undefined;
if (name === 'Agent') return tests.reviewer(input) ? input : undefined;
if (name === 'Write' || name === 'Edit')
return typeof input.file_path === 'string' && input.file_path.length <= INDEX_INPUT_MAX ? { file_path: input.file_path } : undefined;
};
}
function select(tests: GuardJournalTests, currentToolUseId: string) {
return (records: readonly IndexedRecord[], load: (records: readonly IndexedRecord[]) => void) => {
const useOf = new Map<string, { use: Use; record: IndexedRecord }>(), resultsOf = new Map<string, IndexedRecord[]>();
const reads: Array<{ use: Use; record: IndexedRecord }> = [], inits: Array<{ use: Use; record: number; args: string[] }> = [];
records.forEach((record, at) => {
for (const e of record.events) {
if (e.kind === 'use') {
const use = e as Use;
if (!useOf.has(use.toolUseId)) useOf.set(use.toolUseId, { use, record });
if (use.name === 'Read') reads.push({ use, record });
const args = use.name === 'Bash' ? tests.init(use.input?.command) : undefined;
if (args) inits.push({ use, record: at, args });
} else if (e.kind === 'result') { const list = resultsOf.get(e.toolUseId) ?? []; list.push(record); resultsOf.set(e.toolUseId, list); }
}
});
const withResults = (id: string) => [...(useOf.has(id) ? [useOf.get(id)!.record] : []), ...resultsOf.get(id) ?? []];
// invocation() binds and checks every literal init, so all of them load, window or not.
load(inits.flatMap(init => resultsOf.get(init.use.toolUseId) ?? []));
let bound: Record<string, any> | undefined, chainStart: number | undefined;
for (const init of inits) {
const replies = (resultsOf.get(init.use.toolUseId) ?? []).flatMap(r => r.events)
.filter(e => e.kind === 'result' && e.toolUseId === init.use.toolUseId && e.order > init.use.order);
let reply: unknown;
try { reply = replies.length === 1 ? bashJson(textResult(replies[0]!) ?? 'null') : undefined; } catch { reply = undefined; }
// A failed or unreadable init binds nothing and is skipped, as invocation() does.
if (!object(reply)) continue;
// A reused:true init keeps the earlier binding, as invocation() does.
if (object(reply) && reply.reused === true && bound && bound.activePlan === reply.activePlan &&
bound.restorePath === reply.restorePath) continue;
chainStart = init.record; bound = reply;
}
let start = 0;
for (let i = chainStart ?? records.length - 1; i >= 0; i--)
if (records[i]!.events.some(e => e.kind === 'user_turn')) { start = i; break; }
const activePlans = inits.filter(init => init.record >= start).map(init => init.args[1]);
const wanted = new Set<IndexedRecord>(withResults(currentToolUseId));
let afterClose = false;
for (const record of records.slice(start)) for (const e of record.events) {
if (e.kind === 'use' && e.name === 'Read' && tests.read(e.input?.file_path) === 'close') afterClose = true;
if (e.kind === 'message' && afterClose) wanted.add(record);
if (e.kind === 'use' && (e.name === 'Write' || e.name === 'Edit') && activePlans.some(plan => sameNativePath(plan, e.input?.file_path)))
wanted.add(record);
if (e.kind !== 'result') continue;
const use = useOf.get(e.toolUseId)?.use;
if (e.isError || (use?.name === 'Read' && tests.read(use.input?.file_path)) ||
(use?.name === 'Bash' && record.jsonResults?.includes(e.toolUseId))) wanted.add(record);
}
load([...wanted]);
// ENG-4: a dedup reply's cited earlier Read (and its own reply) loads even before the window.
for (let pending = [...wanted]; pending.length;) {
const next: IndexedRecord[] = [];
for (const record of pending) for (const e of record.events) {
const use = e.kind === 'result' ? useOf.get(e.toolUseId)?.use : undefined;
if (!use || use.name !== 'Read' || textResult(e) !== DEDUP_REPLY) continue;
const prior = reads.findLast(r => r.use.order < use.order && sameNativePath(r.use.input?.file_path, use.input?.file_path));
if (prior) next.push(...withResults(prior.use.toolUseId).filter(r => !r.full));
}
load(next);
pending = next;
}
};
}
/** The guard's owned read: the bounded reader under this policy, keeping the prefix witness across reads. */
export function readGuardJournal(file: string, owners: readonly string[], sessionId: string, currentToolUseId: string,
tests: GuardJournalTests, prior?: JournalPrefix, measure?: OwnedReadMeasure): OwnedRead {
return readOwnedClaudePublicTranscript(file, owners, sessionId,
{ prior, measure, policy: { indexInput: indexInput(tests), select: select(tests, currentToolUseId) } });
}