// purser v2.7 - pressure-tested by an orchestration red-team, an economics review, // and an SRE review; recast from measured per-stage costs across a batch of real // runs. Casting: haiku for mechanical steps + scouts, sonnet for // simple implementers/integration, opus for complex items/retries/review, fable // ONLY for planning (pinned - never inherit the session default; the old // undefined-model complex items silently ran on fable-5[1m] at 2x opus cost). // v2.5 (opt-in): --innovate adds a model-tier selector that escalates qualifying // items to fable within a dollar budget; normal /purser is unchanged. // v2.6: (a) preview->pick->execute actually works: planOnly returns {plan, // selector, recon, planHash}; pass them back with fableItems to execute the // EXACT previewed plan with zero Explore/Plan/Select spend; unknown item ids // now fail loudly instead of being silently ignored. (b) the selector default // is SONNET - a model outside the Opus/Fable candidate set, i.e. genuinely // disinterested; opus and fable remain as A/B arms (--selector=) so runs // generate first-party evidence for/against the self-interest hypothesis. // (c) dollar math is deterministic: the selector estimates tokens, the // harness prices them from a dated PRICING table; the roi float is replaced // by an ordinal benefit rank. (d) opus-first-cascade items reserve budget at // a discounted expected cost (--cascade-reserve) instead of full price. // (e) human picks outside the budget are honored AND logged with dollars. // (f) inline flags are stripped only from the edges of the task string. // (g) the nudge respects user-supplied verify commands and prints a // deterministic projected Fable spend. (h) token budget and Fable dollar // budget are labeled distinctly in every log line. (i) per-item ledger in // telemetry joins selector verdict -> strategy -> attempts -> outcome, the // raw material for selector-calibration evals. (j) Fable behavioral claims // in the selector prompt are pinned to a dated constant (FABLE_BEHAVIOR). // v2.6.1: the quote/execute release. The manual pick flow becomes two commands: // --quote (= innovate + planOnly + persistence) prints the offer (per-item // table, cost spectrum, exposure line, pick menu) and saves it under // /purser/previews/.json plus a "latest" pointer; // --execute [=hash] loads the saved quote and routes it through the existing // supplied-plan path (identical to passing plan/verdicts/recon back). // Persistence is agent-mediated (Design B): the workflow sandbox exposes no // fs API (verified by probe), so a haiku agent writes the quote (same // pattern as the preflight lock) and the preflight agent reads it back on // execute. Also: --fable-items=all (every non-hard-exclusion item) and // --fable-budget=unlimited (no cap, logged as "unlimited"). // v2.6.2: the quote menu takes reply words instead of numbers - "advisor" // (Fable on the advisor's picks, named, with the spectrum total), "all", // "none" (maps to --fable-budget=0: recommended items fall back to Opus, // priced honestly in the menu), or naming items directly (example printed // with the quote's real ids). The equivalent commands are demoted to one // parenthetical footnote. Exclusions ("all except X") are unsupported by // design; the assistant line says to name the wanted items instead. // Plain-language pass on the printed quote block: "quote ready" headline // (slug/hash demoted to the internals footnote), "model plan" and "what // Fable buys you" columns (selector rationale is consequence-first by // prompt), "cost: standard | recommended | everything-Fable", and "risk if // you skip Fable entirely". The words "escalation"/"exposure" never appear // in the printed quote block (internal logs, schema keys, and telemetry // keep the old terms). // v2.7: --explain (Learn Mode, opt-in, OFF by default). When set, an in-workflow // tutor agent reads the TASK + the PLAN (what's proposed to be built) and // explains the SYSTEM DESIGN OF THE FEATURE BEING BUILT - components, data // flow, key decisions + tradeoffs, and the CS/SWE/ML concepts it exercises - // so running real work doubles as interview-grade system-design practice. It // explains the THING being built, NOT purser itself. --explain=deep also maps // each design choice to the canonical interview topic; --explain= // emphasizes an angle (e.g. scaling, data-modeling). It reads the plan and // never alters it (plan hash unchanged) - orthogonal to what's built; it just // costs one Sonnet agent. Also: the unapplied-flag warning - a recognized --flag that // lands in the task INTERIOR (edge-only recognition treats it as literal text) // now logs a visible note instead of silently no-op'ing. // v2.7 also: --audit (innovate only, opt-in, OFF by default). A trust/ROI // audit of what Fable actually did and whether it beat plain Opus. A // DISINTERESTED, SKEPTICAL Sonnet auditor (never Fable - it must not grade its // own homework) rules each Fable item warranted | unclear | likely-unnecessary; // escalate-on-failure items are MEASURED from the ledger (Opus verifiably failed, // then Fable ran), fable-first items are ASSESSED from the diff (no // counterfactual). Agents fire only where Fable actually ran; reserved cascade // items are $0. Result carries telemetry.audit + a PR-body section; dollars // are estimates (the runtime exposes no per-Fable token counts). export const meta = { name: 'purser', description: 'Preflight -> explore (haiku) -> plan (fable high) -> parallel worktree implementers (sonnet med; opus high for complex; retries escalate to opus) -> merge (sonnet) -> adversarial review (opus high, read-only) -> PR', whenToUse: 'Implementation tasks worth splitting across parallel agents, or run in volume. args: { task, base?, verify?, pr?, explorers?, maxItems?, small?, planOnly?, force?, innovate?, quote?, execute?, fableBudget?, fableItems?, selector?, nudge?, cascadeReserve?, plan?, verdicts?, recon? } or a plain task string. QUOTE/EXECUTE (v2.6.1, the documented pick flow): --quote (= innovate + planOnly + persistence) prints the per-item recommendation table, a cost spectrum (normal | advisor\'s picks | all-Fable), an exposure line, and a reply-word pick menu (advisor | all | none | named items), and SAVES the quote to /purser/previews/.json plus a "latest" pointer; --execute builds the latest saved quote in this repo (--execute=, or a bare hash as the task text, addresses an older one); missing/mismatched/different-task quotes fail loudly with what was found and how to re-quote; combines with --fable-items and --fable-budget exactly as the structured form does. --fable-items=all expands to every plan item that is not a hard exclusion. --fable-budget=unlimited removes the dollar cap (logged as "unlimited", never as a number). small:true = 1 scout + 1 implementer + skip integrator (~1/4 cost). planOnly:true = preflight+explore+plan only, returns the plan with zero implementer spend (cost brake). force:true = steal a stale/crashed run\'s repo lock. --innovate (or innovate:true) adds a disinterested model-tier selector that escalates qualifying items to fable within --fable-budget=$8 (default). PREVIEW->PICK->EXECUTE: run planOnly:true with --innovate to get {plan, selector, recon, estUsdByItem, planHash}; hand-pick item ids; re-run passing plan/verdicts/recon back plus fableItems:[ids] (or --fable-items=a,b) - Explore, Plan, and Select are all skipped and your picks bind to the exact plan you previewed (unknown ids fail loudly with the valid id list). Human picks are honored OUTSIDE the dollar budget and logged. --selector=sonnet|opus|fable (sonnet default: disinterested, outside the Opus/Fable candidate set; opus and fable are self-interested A/B arms). --cascade-reserve=0.35 sets the budget-reservation discount for opus-first-cascade items (they only spend Fable dollars on Opus failure). --no-nudge (or nudge:false) silences the normal-run innovate hint. Inline flags are stripped only from the leading/trailing edges of the task string, never the middle. --explain (or explain:true) turns on Learn Mode: an in-workflow Sonnet tutor reads the task + plan and explains the SYSTEM DESIGN OF THE FEATURE BEING BUILT (components, data flow, decisions+tradeoffs, the CS/SWE/ML concepts it exercises) - the thing being built, NOT purser itself - so real work doubles as system-design-interview practice; --explain=deep also maps each choice to the canonical interview topic, --explain= emphasizes an angle; returned as designExplanation + a PR section; off by default, reads the plan without altering it (plan hash unchanged), costs one agent. A recognized --flag that lands in the MIDDLE of the task is reported as unapplied via a note (flags only bind at the task edges). --audit (innovate only, opt-in) adds a disinterested, SKEPTICAL Sonnet auditor (never Fable) that rules per Fable item whether the escalation was warranted vs likely-unnecessary: escalate-on-failure items judged from the ledger (MEASURED - Opus failed, Fable fixed), fable-first items assessed from the diff (no counterfactual), reserved cascade items $0; result carries telemetry.audit and a PR-body section; dollars are estimates.', phases: [ { title: 'Preflight', detail: 'haiku: repo lock + sanity + verify-command detection', model: 'haiku' }, { title: 'Explore', detail: 'haiku scouts map the task surface (read-only); skipped when a previewed plan is passed back in', model: 'haiku' }, { title: 'Plan', detail: 'one fable-high planner writes file-disjoint work items; skipped when a previewed plan is passed back in', model: 'fable' }, { title: 'Select', detail: 'innovate only: a disinterested selector (sonnet default; opus/fable as A/B arms) rules Opus-vs-Fable per item; skipped when previewed verdicts are passed back in', model: 'sonnet' }, { title: 'Quote', detail: 'quote only: haiku persists the quote (plan + verdicts) for --execute', model: 'haiku' }, { title: 'Explain', detail: '--explain only: a Sonnet tutor explains the system design of the FEATURE being built (from the plan); --explain=deep maps to interview topics (read-only)', model: 'sonnet' }, { title: 'Implement', detail: 'sonnet-medium agents (opus-high for complex items; retries escalate to opus), one worktree each', model: 'sonnet' }, { title: 'Integrate', detail: 'sonnet merges item branches, runs the FULL verify suite', model: 'sonnet' }, { title: 'Review', detail: 'opus-high read-only adversarial review (sonnet-medium fallback when budget is low; never skipped on a green build); opus fixes; sonnet re-verifies the fix', model: 'opus' }, { title: 'Audit', detail: '--audit only: a disinterested, skeptical Sonnet auditor rules whether each Fable item was warranted vs likely-unnecessary (read-only)', model: 'sonnet' }, { title: 'Finalize', detail: 'haiku: push (force-with-lease) + PR + cleanup', model: 'haiku' }, ], } // ---------------- pricing + pinned model-behavior facts ---------------- // ALL dollar figures in purser are computed here, deterministically, from token // estimates. The selector never invents dollars (models are unreliable on their // own pricing); it estimates tokens, this table prices them. EDIT on every // price change and bump asOf. const PRICING = { asOf: '2026-07-06', // blended $ per 1K tokens for implementer-shaped work (~3:1 output:input). // VERIFY against the current published Fable list price before each release. // 2026-07-06: corrected from 0.12/0.06 - those decoded to Opus 4.1's retired // $15/$75 list price (and 2x that for fable); repriced against the published // Fable 5 $10/$50 and Opus 4.8 $5/$25 list rates. fableUsdPerKTok: 0.04, opusUsdPerKTok: 0.02, // sonnet is priced here ONLY for the quote's cost-spectrum line (the // "normal" column runs simple items on sonnet). VERIFY like the others. // (Sonnet 5 list $3/$15; the $2/$10 intro price through 2026-08-31 is // deliberately not used - this table pins list prices.) sonnetUsdPerKTok: 0.012, } const estUsdFromTokensK = (k) => Math.round((Number(k) || 0) * PRICING.fableUsdPerKTok * 100) / 100 // Deterministic rough sizing used ONLY where no selector verdict exists // (the nudge, and human picks the selector never saw). Crude by design and // always labeled as such. const roughItemTokensK = (item) => 25 + 12 * (item.files?.length ?? 1) // Behavioral claims about Fable baked into the selector prompt are FACTS ABOUT // A MODEL VERSION, not timeless truths. They are pinned here with a date so a // model bump is a one-line audit, and so the prompt and the docs cannot drift // apart. If classifierNote stops being true, the security/crypto hard // exclusion changes from "saves money" to "leaves quality on the table". const FABLE_BEHAVIOR = { asOf: '2026-07-04', modelId: 'claude-fable-5', classifierNote: 'Fable\'s safety classifier reroutes security/cryptography/privacy/bio/chem work to an Opus-class path while still billing Fable rates, so escalating those items pays ~2x for Opus-grade output.', } // ---------------- args ---------------- const A = (typeof args === 'object' && args) || {} // ---- innovate-mode flags: from object-args AND inline tokens in the task string // (invoked as /purser ). v2.6: tokens are recognized ONLY at the leading and // trailing edges of the string - a task like "fix the parser so --innovate is // documented" keeps its middle intact. ---- let INNOVATE = A.innovate === true let EXPLAIN = A.explain === true ? true : (typeof A.explain === 'string' && A.explain.trim() ? A.explain.trim().toLowerCase() : false) // Learn Mode: true | 'deep' | '' | false let AUDIT = A.audit === true // opt-in Fable trust/ROI audit (innovate only); skeptical, disinterested Sonnet auditor let NUDGE = A.nudge !== false // deterministic innovate-eligibility hint; on by default if (A.selector != null && !['sonnet', 'opus', 'fable'].includes(A.selector)) throw new Error(`invalid selector '${A.selector}' - valid options: sonnet | opus | fable (sonnet is the disinterested default)`) let SELECTOR_MODEL = A.selector ?? 'sonnet' // ^ sonnet default: the selector chooses BETWEEN Opus and Fable, so only a model // outside that candidate set is disinterested. opus (self-retention risk) and // fable (self-recommendation risk) are kept as A/B arms; every run's item // ledger (telemetry.itemLedger) is the eval data for comparing them. let FABLE_BUDGET = A.fableBudget === 'unlimited' ? Infinity : typeof A.fableBudget === 'number' ? A.fableBudget : 8 // dollars - a budget, not a count; Infinity = unlimited const fmtFableBudget = () => (FABLE_BUDGET === Infinity ? 'unlimited' : '$' + FABLE_BUDGET) let CASCADE_RESERVE = typeof A.cascadeReserve === 'number' ? Math.min(Math.max(A.cascadeReserve, 0), 1) : 0.35 // ^ opus-first-cascade items only spend Fable dollars if Opus fails, so they // reserve budget at estUsd * CASCADE_RESERVE (an assumed Opus failure rate) // instead of crowding out fable-first items at full price. let FABLE_ITEMS = A.fableItems === 'all' || (Array.isArray(A.fableItems) && A.fableItems.length === 1 && A.fableItems[0] === 'all') ? 'all' : Array.isArray(A.fableItems) && A.fableItems.length ? A.fableItems.slice() : null // human tier-pick override; 'all' = every non-hard-exclusion item; empty list = no picks // v2.6.1 quote/execute. QUOTE = innovate + planOnly + persist the preview. // EXECUTE = load the persisted quote and run it through the supplied-plan // path; 'latest' means the repo's latest pointer, otherwise a plan hash. let QUOTE = A.quote === true // SECURITY (audit 2026-07-12): validate the --execute hash on ALL entry points, including the // object-arg path, before it can reach the preflight `HASH="${EXECUTE}"` shell sink. Fail closed. const parseExecute = (v) => { if (v === true) return 'latest' if (typeof v !== 'string' || !v.length) return null const t = v.trim().toLowerCase() if (!/^(latest|[0-9a-f]{4,16})$/.test(t)) { throw new Error('purser --execute: quote hash must be "latest" or 4-16 hex chars') } return t } let EXECUTE = parseExecute(A.execute) let taskText = (typeof args === 'string' ? args : A.task) || '' { const eatFlag = (tok) => { if (tok === '--innovate') { INNOVATE = true; return true } if (tok === '--explain') { EXPLAIN = true; return true } if (tok === '--audit') { AUDIT = true; return true } if (tok === '--no-audit') { AUDIT = false; return true } if (tok === '--no-nudge') { NUDGE = false; return true } if (tok === '--quote') { QUOTE = true; return true } if (tok === '--execute') { EXECUTE = EXECUTE ?? 'latest'; return true } const mx = tok.match(/^--execute=([0-9a-fA-F]{4,16})$/); if (mx) { EXECUTE = mx[1].toLowerCase(); return true } const me = tok.match(/^--explain=([A-Za-z0-9_-]+)$/); if (me) { EXPLAIN = me[1].toLowerCase(); return true } const mb = tok.match(/^--fable-budget=(?:\$?(\d+(?:\.\d+)?)|unlimited)$/); if (mb) { FABLE_BUDGET = mb[1] ? parseFloat(mb[1]) : Infinity; return true } const ms = tok.match(/^--selector=(sonnet|opus|fable)$/); if (ms) { SELECTOR_MODEL = ms[1]; return true } const mi = tok.match(/^--fable-items=([A-Za-z0-9,_-]+)$/); if (mi) { const ids = mi[1].split(',').filter(Boolean); FABLE_ITEMS = mi[1] === 'all' ? 'all' : ids.length ? ids : FABLE_ITEMS; return true } const mc = tok.match(/^--cascade-reserve=(0(?:\.\d+)?|1(?:\.0+)?)$/); if (mc) { CASCADE_RESERVE = parseFloat(mc[1]); return true } return false } // Consume flag tokens off the string's edges directly (never tokenize-and- // rejoin: that would collapse interior newlines in multi-line tasks). let s = taskText.trim() for (let m; (m = s.match(/^(\S+)\s*/)) && eatFlag(m[1]);) s = s.slice(m[0].length) for (let m; (m = s.match(/\s*(\S+)$/)) && eatFlag(m[1]);) s = s.slice(0, s.length - m[0].length) taskText = s } // "--execute 7f3a91c2" arrives as two tokens: the flag is eaten and the hash // lands in the task text. A task that is nothing but a short hex string is // unambiguously a hash reference, never a real task. if (EXECUTE === 'latest' && /^[0-9a-f]{4,16}$/i.test(taskText.trim())) { EXECUTE = taskText.trim().toLowerCase(); taskText = '' } let task = taskText // let: execute mode replaces it with the saved quote's task // ---- unapplied-flag warning: edge-only recognition means a recognized --flag // sitting in the task INTERIOR was treated as literal text. Surface it (a note, // logged after the mode banner) instead of a silent no-op. Never changes behavior. const looksLikeKnownFlag = (tok) => ['--innovate', '--explain', '--audit', '--no-audit', '--no-nudge', '--quote', '--execute'].includes(tok) || /^--(?:execute|explain|fable-budget|selector|fable-items|cascade-reserve)=\S+$/.test(tok) const unappliedFlags = [...new Set(task.split(/\s+/).filter(looksLikeKnownFlag))] if (QUOTE && EXECUTE) throw new Error('--quote and --execute are mutually exclusive: --quote saves a new quote, --execute builds a saved one') if (EXECUTE && (A.plan || A.verdicts || A.recon)) throw new Error('--execute loads the saved quote; do not also pass structured plan/verdicts/recon (pick one path)') if (!task && !EXECUTE) throw new Error('purser needs a task: Workflow({name:"purser", args:{task:"..."}})') if (QUOTE || EXECUTE) INNOVATE = true // both are innovate-mode flows by definition if (FABLE_ITEMS && !INNOVATE) { INNOVATE = true } // picking tiers IS innovate mode; auto-enable // --explain value is free-form: true = standard, 'deep' = + interview-topic map, // any other string = a focus angle (e.g. 'scaling') passed to the tutor. No fixed // catalog - the subject is the user's build, not a set of purser topics. // SECURITY (audit 2026-07-12): validate --base against a git-ref allow-list before it is // interpolated into git shell commands. Fail closed on shell metacharacters and path traversal. const BASE_ARG = (() => { if (A.base == null) return null const b = String(A.base) // Must START with a letter/digit (never '-'): a leading dash would be parsed by git as a // FLAG (argument injection), not a ref. Then letters/digits/. _ / - only; no '..'. if (b.includes('..') || !/^[A-Za-z0-9][A-Za-z0-9._\/-]{0,99}$/.test(b)) { throw new Error('purser --base: invalid git ref (must start with a letter/digit; then letters, digits, . _ / - ; no "..")') } return b })() // resolved by preflight (repos vary: main/master/dev) const WANT_PR = A.pr !== false const SMALL = !!A.small const EXPLORERS_ARG = A.explorers // N_EXPLORERS resolved after preflight (auto-raises on huge repos) const MAX_ITEMS = SMALL ? 1 : Math.min(A.maxItems || 4, 8) const USER_VERIFY = Array.isArray(A.verify) ? A.verify : A.verify ? [A.verify] : null const PLAN_ONLY = !!A.planOnly || QUOTE // a quote is a planOnly run that persists itself const FORCE = !!A.force const LOCK_STALE_MIN = Math.max(1, Math.min(A.lockStaleMinutes ?? 20, 240)) // ---- v2.6: preview->pick->execute inputs. planOnly returns these; pass them // back to execute the exact previewed plan with zero Explore/Plan/Select spend. // (let, not const: execute mode assigns all three from the loaded quote) let SUPPLIED_PLAN = A.plan && typeof A.plan === 'object' && Array.isArray(A.plan.items) && A.plan.items.length ? A.plan : null let SUPPLIED_VERDICTS = Array.isArray(A.verdicts) && A.verdicts.length ? A.verdicts : null let SUPPLIED_RECON = Array.isArray(A.recon) ? A.recon : null const budgetLow = () => budget.total && budget.remaining() < 40_000 const startPhase = (title) => { phase(title); if (budget.total) log(`token budget @ ${title}: ${budget.spent().toLocaleString()} spent / ${budget.remaining().toLocaleString()} remaining (tokens, not dollars)`) } // Stable hash of the plan's load-bearing content. Logged on preview and // execute so a human can confirm picks bound to the plan they actually read. const planHashOf = (p) => { const s = JSON.stringify({ slug: p.slug, items: (p.items ?? []).map((i) => ({ id: i.id, files: i.files, wave: i.wave, instructions: i.instructions })) }) let h = 5381 for (let i = 0; i < s.length; i++) h = ((h * 33) ^ s.charCodeAt(i)) >>> 0 return h.toString(16) } // ---------------- quote math: cost spectrum + exposure (pure, deterministic) ---------------- // Implementer-spend estimates only; pipeline overhead (scouts, integrate, // review, finalize) is identical across the three columns, so the deltas are // the signal. Token counts come from the selector's estTokensK where a verdict // exists and the deterministic rough sizing otherwise; every column is priced // from the pinned PRICING table at that casting's rate. const tierRate = { fable: PRICING.fableUsdPerKTok, opus: PRICING.opusUsdPerKTok, sonnet: PRICING.sonnetUsdPerKTok } const costSpectrum = (items, verdictById, cascadeReserve) => { const r2 = (n) => Math.round(n * 100) / 100 let normal = 0, advisor = 0, allFable = 0 for (const it of items) { const v = verdictById.get(it.id) const k = typeof v?.estTokensK === 'number' ? v.estTokensK : roughItemTokensK(it) const normalRate = it.complex ? tierRate.opus : tierRate.sonnet normal += k * normalRate // all-Fable respects hard exclusions (they are safety routing, not economy) allFable += k * (v?.forceOpus ? tierRate.opus : tierRate.fable) if (v && v.escalate && !v.forceOpus) { // cascade items pay Opus up front and Fable only on failure; price the // expected cost with the same reserve factor the budget greedy uses advisor += v.strategy === 'opus-first-cascade' ? k * (tierRate.opus + cascadeReserve * tierRate.fable) : k * tierRate.fable } else { advisor += k * (v?.forceOpus ? tierRate.opus : normalRate) } } return { normal: r2(normal), advisor: r2(advisor), allFable: r2(allFable) } } // Exposure: where a decision NOT to escalate is most likely to be silently // regretted. Deterministic mapping from verdict signals already computed: // HIGH - some recommended item has weak verification (a premium-tier miss // there is plausible and would NOT be caught by tests) // MEDIUM - recommendations exist but all have strong verify (a miss surfaces // as a red verify: a retry, not silent quality loss) // LOW - no recommendations const WEAK_VERIFY_RE = /(no|without|lacks?|missing)[ -]?test|weak[ -]?verif|no[ -]?coverage|untested/i const exposureOf = (verdicts) => { const recs = (verdicts ?? []).filter((v) => v.escalate && !v.forceOpus) const weak = recs.filter((v) => (v.signals ?? []).some((s) => WEAK_VERIFY_RE.test(s))) if (weak.length) return { level: 'HIGH', exposed: weak.map((v) => v.id), reason: `Fable is recommended where verification is weak (${[...new Set(weak.flatMap((v) => (v.signals ?? []).filter((s) => WEAK_VERIFY_RE.test(s))))].join('; ')}); a cheap-model miss there would ship silently - nothing in the verify suite would catch it` } if (recs.length) return { level: 'MEDIUM', exposed: recs.map((v) => v.id), reason: 'Fable is recommended but verification is strong; a cheap-model miss would surface as a failed verify (a retry, not a silent defect)' } return { level: 'LOW', exposed: [], reason: 'no items were recommended for Fable' } } // ---------------- innovate telemetry + mode banner ---------------- const fableStats = { calls: 0, byRole: {}, estUsd: 0, overrideUsd: 0, fableFirst: 0, escalateOnFailure: 0 } const logFable = (role, id, estUsd) => { fableStats.calls++; fableStats.byRole[role] = (fableStats.byRole[role] || 0) + 1 fableStats.estUsd += Number(estUsd) || 0 log(`Fable: ${role} on item ${id} (~$${(Number(estUsd) || 0).toFixed(2)} est)`) } const modeLine = EXECUTE ? `MODE: execute - building the saved quote (${EXECUTE === 'latest' ? 'latest in this repo' : 'hash ' + EXECUTE}); Fable $ budget: ${fmtFableBudget()}` : QUOTE ? `MODE: quote - plan + per-item recommendations only, nothing built; the quote is saved for --execute (Fable $ budget: ${fmtFableBudget()}); selector=${SELECTOR_MODEL}${SELECTOR_MODEL === 'sonnet' ? ' (disinterested: outside the Opus/Fable candidate set)' : ' (A/B arm: an interested party in the Opus-vs-Fable decision)'}` : INNOVATE ? `MODE: innovate - Fable: planning + selector-approved items (Fable $ budget: ${fmtFableBudget()}); selector=${SELECTOR_MODEL}${SELECTOR_MODEL === 'sonnet' ? ' (disinterested: outside the Opus/Fable candidate set)' : ' (A/B arm: an interested party in the Opus-vs-Fable decision)'}` : 'MODE: normal - Fable: planning only' log(EXPLAIN ? `${modeLine} | Learn Mode: ON (${EXPLAIN === true ? 'standard' : EXPLAIN === 'deep' ? 'deep + interview map' : 'focus=' + EXPLAIN}) - a tutor explains the system design of what this run BUILDS (not purser itself)` : modeLine) if (unappliedFlags.length) log(`note: ${unappliedFlags.map((f) => `'${f}'`).join(', ')} ${unappliedFlags.length === 1 ? 'appears' : 'appear'} inside your task text, so ${unappliedFlags.length === 1 ? 'it was' : 'they were'} read as part of the task rather than applied as ${unappliedFlags.length === 1 ? 'a flag' : 'flags'} (flags take effect only at the very start or end of the task). If you intended ${unappliedFlags.length === 1 ? 'it' : 'them'} as ${unappliedFlags.length === 1 ? 'a flag' : 'flags'}, move ${unappliedFlags.length === 1 ? 'it' : 'them'} to an edge.`) // Shared worktree rules. Red-team verified: fresh worktrees are born ON AN // AUTO-NAMED BRANCH (not detached) - committing before detaching locks that // branch to the worktree. Publishing via branch -f from detached HEAD never // locks anything. const GIT_RULES = (start) => ` You are in your OWN git worktree (isolated checkout; object store and branches are SHARED with the main repo). Non-negotiable rules: 1. FIRST: git checkout --detach ${start} Then confirm you are detached: 'git symbolic-ref -q HEAD' must print nothing. Never commit while on a named branch. 2. Never 'git checkout ' or 'git checkout -b'. Publish ONLY the exact branch name these instructions give you, via: git branch -f HEAD 3. Untracked env dirs (node_modules, .venv, target) are absent here. Node: symlink the main repo's: ln -sfn "$(dirname "$(git rev-parse --path-format=absolute --git-common-dir)")/node_modules" node_modules Python: same idea for .venv, or use the main repo's interpreter path directly. NEVER install or change dependencies through a symlink (it is the user's live environment). If your work changes package.json/pyproject: rm the symlink and create a real local env inside the worktree. 4. Before committing: run 'git status' and stage ONLY files you intentionally changed (never node_modules, scratch files, or logs). git add ; git commit. 5. No pushing. No destructive git. Verify commands must exit on their own - never start dev servers or watch mode (vitest run, not vitest).` // ---------------- Phase 0: Preflight (haiku) ---------------- startPhase('Preflight') const PREFLIGHT_SCHEMA = { type: 'object', required: ['base', 'baseExists', 'dirty'], properties: { base: { type: 'string' }, baseExists: { type: 'boolean' }, dirty: { type: 'boolean' }, currentBranch: { type: 'string' }, verifyCommands: { type: 'array', items: { type: 'string' } }, ecosystem: { type: 'string' }, notes: { type: 'string' }, lockAcquired: { type: 'boolean' }, lockPath: { type: 'string' }, fileCount: { type: 'integer' }, // execute mode only: the saved quote, read by this same (already running) agent quoteFound: { type: 'boolean' }, quoteJson: { type: 'string' }, quotePath: { type: 'string' }, quoteCandidates: { type: 'array', items: { type: 'string' } }, }, } const pre = await agent( `Repo preflight. Read-only EXCEPT step 0's single lock file. Run: ${PLAN_ONLY ? '0. planOnly mode - no lock needed (read-only run). Return lockAcquired=true.' : `0. Concurrency lock (one purser run per repo; stale after ${LOCK_STALE_MIN} min). Run this EXACTLY: LOCK="$(git rev-parse --path-format=absolute --git-common-dir)/purser.lock"; TMP="$LOCK.steal.$$"${FORCE ? '\n rm -f "$LOCK" # user passed force:true - steal any existing lock' : ''} acquire() { ( set -o noclobber; echo "purser started $(date -u +%FT%TZ)" > "$LOCK" ) 2>/dev/null; } if acquire; then echo ACQUIRED elif [ -n "$(find "$LOCK" -mmin +${LOCK_STALE_MIN} 2>/dev/null)" ] && mv "$LOCK" "$TMP" 2>/dev/null; then if [ -n "$(find "$TMP" -mmin +${LOCK_STALE_MIN} 2>/dev/null)" ]; then rm -f "$TMP"; acquire && echo ACQUIRED || echo LOCKED else mv -n "$TMP" "$LOCK" 2>/dev/null; rm -f "$TMP"; echo LOCKED fi else echo LOCKED fi Return lockAcquired=true ONLY if ACQUIRED printed; always return lockPath="$LOCK".`}${EXECUTE ? ` 0b. Saved quote (execute mode). Run: DIR="$(git rev-parse --path-format=absolute --git-common-dir)/purser/previews" ${EXECUTE === 'latest' ? 'HASH="$(cat "$DIR/latest" 2>/dev/null | tr -d "[:space:]")"' : `HASH="${EXECUTE}"`} FILE="$DIR/$HASH.json" If $HASH is non-empty and $FILE exists: return quoteFound=true, quotePath="$FILE", and quoteJson = the file's FULL contents VERBATIM (do not reformat, summarize, or trim). Else: return quoteFound=false and quotePath="$FILE" (or "$DIR" if $HASH is empty). Either way return quoteCandidates = the basenames without .json of every *.json in "$DIR" (empty array if the directory does not exist).` : ''} 1. Base branch: ${BASE_ARG ? `the user chose '${BASE_ARG}' - verify it: git rev-parse --verify ${BASE_ARG}` : `detect the default branch (git symbolic-ref refs/remotes/origin/HEAD, else 'gh repo view --json defaultBranchRef', else whichever of main/master exists) and verify it resolves`}. Return it as "base" + baseExists. 2. git status --porcelain -> dirty (true if any output) 3. git rev-parse --abbrev-ref HEAD -> currentBranch 4. verifyCommands: the commands that prove this repo healthy (typecheck, test, build). Read package.json scripts AND check for test-runner configs the scripts may omit (vitest/jest/pytest config present but no "test" script -> include e.g. 'npx vitest run' / 'pytest -x' directly). For Python repos use pyproject/requirements conventions. Every command must run to completion and exit on its own - never watch mode, never dev servers. 5. ecosystem: "node" | "python" | "rust" | "go" | "other". 6. fileCount: 'git ls-files | wc -l' as an integer. Return the facts.`, { label: 'preflight', phase: 'Preflight', model: 'haiku', effort: 'low', agentType: 'Explore', schema: PREFLIGHT_SCHEMA }, ) if (!pre) throw new Error('preflight agent failed') if (!PLAN_ONLY && !pre.lockAcquired) throw new Error(`another purser run appears active in this repo (lock: ${pre.lockPath ?? '/purser.lock'}; locks go stale after ${LOCK_STALE_MIN} min). Wait for it to finish - or if a previous run crashed, rm that file or pass force:true.`) const BASE = BASE_ARG || pre.base if (!pre.baseExists || !BASE) throw new Error(`base branch ${BASE_ARG ?? pre.base ?? '?'} does not resolve`) const N_EXPLORERS = SMALL ? 1 : Math.min(EXPLORERS_ARG || ((pre.fileCount ?? 0) > 5000 ? 4 : 2), 5) if (!SMALL && !EXPLORERS_ARG && (pre.fileCount ?? 0) > 5000) log(`large repo (${pre.fileCount} tracked files): scouts auto-raised to 4`) if (pre.dirty) log(`⚠ main checkout is dirty (on ${pre.currentBranch ?? '?'}); scouts will verify facts against ${BASE}, implementers build from ${BASE}`) // ---------------- execute mode: load and validate the saved quote ---------------- // Every failure here is LOUD and says what was found and how to re-quote; // silently building the wrong plan is the one unforgivable outcome. let executedQuote = null if (EXECUTE) { const where = pre.quotePath ?? '/purser/previews' const others = (pre.quoteCandidates ?? []).join(', ') || 'none' const reQuote = 'Get a fresh quote first: /purser --quote .' if (!pre.quoteFound || !pre.quoteJson) throw new Error(`--execute: no saved quote ${EXECUTE === 'latest' ? "found ('latest' pointer missing or dangling)" : `with hash ${EXECUTE}`} at ${where}. Saved quotes in this repo: ${others}. ${reQuote}`) let q try { q = JSON.parse(pre.quoteJson) } catch { throw new Error(`--execute: the saved quote at ${where} is not valid JSON (corrupted or hand-edited). Saved quotes in this repo: ${others}. ${reQuote}`) } if (!q || typeof q !== 'object' || !q.task || !q.plan || !Array.isArray(q.plan.items) || !q.planHash) throw new Error(`--execute: the saved quote at ${where} is missing task/plan/planHash. ${reQuote}`) const rehash = planHashOf(q.plan) if (rehash !== q.planHash) throw new Error(`--execute: quote integrity check failed at ${where}: the stored plan hashes to ${rehash} but the quote claims ${q.planHash} (edited or corrupted). ${reQuote}`) if (EXECUTE !== 'latest' && q.planHash !== EXECUTE) throw new Error(`--execute: asked for quote ${EXECUTE} but the file at ${where} contains quote ${q.planHash}. Saved quotes in this repo: ${others}. ${reQuote}`) if (task && task !== q.task) throw new Error(`--execute: you typed a task but the saved quote (hash ${q.planHash}) is for a different task.\n yours: ${JSON.stringify(task.length > 100 ? task.slice(0, 97) + '...' : task)}\n quote: ${JSON.stringify(q.task.length > 100 ? q.task.slice(0, 97) + '...' : q.task)}\nA quote executes only the task it priced. Run bare --execute to build the saved quote as-is, or re-quote: /purser --quote .`) task = q.task SUPPLIED_PLAN = q.plan SUPPLIED_VERDICTS = Array.isArray(q.verdicts) && q.verdicts.length ? q.verdicts : null SUPPLIED_RECON = Array.isArray(q.recon) ? q.recon : [] executedQuote = { hash: q.planHash, path: pre.quotePath ?? null, pricingAsOf: q.pricingAsOf ?? null, reconTrimmed: !!q.reconTrimmed } if (q.pricingAsOf && q.pricingAsOf !== PRICING.asOf) log(`⚠ quote ${q.planHash} was priced under rate table ${q.pricingAsOf}; the pinned table is now ${PRICING.asOf} - keeping the previewed dollars (your picks were made against them); re-quote if the drift matters`) log(`execute: loaded quote ${q.planHash} (${EXECUTE === 'latest' ? 'latest' : 'by hash'}) from ${where} - task: ${JSON.stringify(q.task.length > 80 ? q.task.slice(0, 77) + '...' : q.task)}`) } // ---------------- Phase 1: Explore (haiku, read-only) ---------------- // v2.6: skipped when a previewed plan is passed back in, UNLESS the selector // still has to run (innovate without supplied verdicts) and no recon was // supplied - the selector's novelty judgement needs scout facts. const EXPLORE_SCHEMA = { type: 'object', required: ['findings'], properties: { findings: { type: 'array', maxItems: 15, items: { type: 'object', required: ['fact', 'where'], properties: { fact: { type: 'string' }, where: { type: 'string' } } } }, risks: { type: 'array', items: { type: 'string' } }, }, } const NEED_SCOUTS = !SUPPLIED_PLAN || (INNOVATE && !SUPPLIED_VERDICTS && !SUPPLIED_RECON) let scouts = SUPPLIED_RECON ?? [] if (NEED_SCOUTS) { startPhase('Explore') const LENSES = [ 'TASK SURFACE: the exact files/functions/routes/schemas this task will touch - current behavior, callers, conventions to imitate (naming, error handling, test idioms)', 'BLAST RADIUS: consumers, integrations, migrations, flags - what breaks if the touched surface changes; similar prior implementations in this repo to copy', 'ARCHITECTURE: modules, layers, entry points relevant to the task', 'DEPENDENCIES: third-party libs, shared utilities, cross-cutting concerns (auth, i18n, telemetry) the task must respect', 'TESTS: existing test coverage of the touched surface and the patterns new tests must follow', ].slice(0, N_EXPLORERS) const dirtyNote = pre.dirty ? `\nThe working tree is DIRTY and may not match ${BASE}. Verify load-bearing facts against the base: 'git show ${BASE}:'.` : '' scouts = (await parallel(LENSES.map((lens) => () => agent( `Read-only reconnaissance for an implementation task. TASK: ${task}\n\nYour lens - ${lens}.${dirtyNote}\nReturn compact facts with file:line pointers - facts a planner can rely on without re-reading files.`, { label: `scout:${lens.split(':')[0].toLowerCase().replace(/ /g, '-')}`, phase: 'Explore', model: 'haiku', effort: 'low', agentType: 'Explore', schema: EXPLORE_SCHEMA }, )))).filter(Boolean) if (!scouts.length) throw new Error('all explore scouts failed') log(`explore: ${scouts.length}/${LENSES.length} scouts, ${scouts.reduce((s, r) => s + r.findings.length, 0)} facts`) } else { log(`explore: skipped (${SUPPLIED_RECON ? 'recon supplied from preview' : 'plan supplied; no phase needs scout facts'})`) } // ---------------- Phase 2: Plan (fable, high) ---------------- // v2.6: skipped entirely when a previewed plan is passed back in. Item ids are // minted by the planner, so re-planning would re-mint them and orphan any human // picks - reusing the previewed plan is what makes fableItems binding. const PLAN_SCHEMA = { type: 'object', required: ['slug', 'summary', 'verify', 'items'], properties: { slug: { type: 'string', pattern: '^[a-z0-9][a-z0-9-]{2,28}$' }, summary: { type: 'string' }, verify: { type: 'array', minItems: 1, items: { type: 'string' } }, items: { type: 'array', minItems: 1, maxItems: MAX_ITEMS, items: { type: 'object', required: ['id', 'title', 'instructions', 'files', 'wave', 'verify'], properties: { id: { type: 'string', pattern: '^[a-z0-9][a-z0-9-]{1,18}$' }, title: { type: 'string' }, instructions: { type: 'string' }, files: { type: 'array', items: { type: 'string' } }, wave: { type: 'integer', minimum: 1, maximum: 3 }, verify: { type: 'array', minItems: 1, items: { type: 'string' } }, complex: { type: 'boolean' }, }, }, }, }, } let plan // SECURITY (audit 2026-07-12): a supplied/quoted plan's slug and item ids flow into git branch/ // commit names (shell). Re-validate them against safe charsets at this trust boundary. // Security property is "only [a-z0-9-] — no shell metacharacters", not a minimum length: // short slugs/ids like 'p'/'a' are safe and legitimate. Char class matches purser's own plan // schema (lines ~458/466); length is bounded but permissive so safe values are never rejected. const SLUG_RE = /^[a-z0-9][a-z0-9-]{0,63}$/ // lowercase alnum + hyphen, no metachars const ITEM_ID_RE = /^[a-z0-9][a-z0-9-]{0,63}$/ // same class as the schema id pattern if (SUPPLIED_PLAN) { if (!SUPPLIED_PLAN.slug || !SUPPLIED_PLAN.summary || !Array.isArray(SUPPLIED_PLAN.verify) || !SUPPLIED_PLAN.verify.length) { throw new Error('supplied plan is missing slug/summary/verify - pass the exact .plan object a planOnly run returned') } if (!SLUG_RE.test(String(SUPPLIED_PLAN.slug))) { throw new Error('supplied plan slug must be lowercase alphanumeric/hyphen only (no shell metacharacters)') } for (const it of SUPPLIED_PLAN.items) { if (!it.id || !it.title || !it.instructions || !Array.isArray(it.files) || !Number.isInteger(it.wave) || !Array.isArray(it.verify) || !it.verify.length) { throw new Error(`supplied plan item '${it.id ?? '?'}' is malformed (needs id/title/instructions/files/wave/verify) - pass the exact .plan object a planOnly run returned`) } if (!ITEM_ID_RE.test(String(it.id))) { throw new Error(`supplied plan item id '${it.id}' must be lowercase alphanumeric/hyphen only (no shell metacharacters)`) } } plan = SUPPLIED_PLAN log(`plan: reusing supplied plan '${plan.slug}' (hash ${planHashOf(plan)}) - zero Explore/Plan spend`) } else { startPhase('Plan') plan = await agent( `You are the planning architect. Produce an implementation plan that parallel agents execute in isolated git worktrees, building from ${BASE}. TASK: ${task} ${USER_VERIFY ? `USER-SUPPLIED FULL VERIFY COMMANDS (authoritative): ${JSON.stringify(USER_VERIFY)}` : `PREFLIGHT-DETECTED VERIFY COMMANDS: ${JSON.stringify(pre.verifyCommands ?? [])}`} RECON (from read-only scouts; trust pointers, spot-check anything load-bearing): ${JSON.stringify(scouts)} Plan rules: - 1-${MAX_ITEMS} work items. Same-wave items run in parallel and MUST be file-disjoint. Waves 2/3 only for items that genuinely depend on earlier output. - Each item's "instructions" is self-contained: exact files, what to change, what NOT to touch, conventions to follow. - item "verify" = FAST subset proving that item alone (typecheck + targeted tests). NEVER the full suite - the integrator runs that once. All commands must exit on their own (no watch mode, no servers). - top-level "verify" = the full-suite commands the integrated result must pass. - Mark "complex": true only for items with subtle invariants, concurrency, or cross-cutting refactors (they get a stronger model). - "slug" = short kebab-case name for branches (wf//...) and the PR. - Prefer FEWER coherent items; parallelize only where it saves wall-clock without inviting conflicts. If the task is small, return ONE item.`, { label: 'planner', phase: 'Plan', model: 'fable', effort: 'high', agentType: 'Explore', schema: PLAN_SCHEMA }, ) if (!plan) throw new Error('planner failed') } const VERIFY = USER_VERIFY || plan.verify // Enforce same-wave file-disjointness in plain JS (schema can't); demote // overlapping items to the next wave instead of discovering it as conflicts. // Runs on supplied plans too (idempotent on already-clean plans). { const filesByWave = new Map() for (const it of [...plan.items].sort((a, b) => a.wave - b.wave)) { let w = it.wave for (;;) { const s = filesByWave.get(w) ?? new Set() if (it.files.some((f) => s.has(f))) { w++; continue } it.files.forEach((f) => s.add(f)); filesByWave.set(w, s) if (w !== it.wave) log(`plan: demoted item '${it.id}' to wave ${w} (file overlap)`) it.wave = w; break } } } const PLAN_HASH = planHashOf(plan) const FAST = plan.items.length === 1 // In quote mode the slug/hash are demoted to the quote block's internals // footnote - the plan line stays plain language. log(QUOTE ? `plan: ${plan.items.length} work item${plan.items.length === 1 ? '' : 's'}${FAST ? ' (single item: fast path, no integrator)' : ''}` : `plan: ${plan.items.length} item(s), slug=${plan.slug}, hash=${PLAN_HASH}${FAST ? ', FAST PATH (single item, no integrator)' : ''}`) // v2.6: fail LOUDLY on unknown fableItems ids. The old behavior (log + ignore) // silently dropped human picks whenever a fresh run re-minted item ids. if (INNOVATE && FABLE_ITEMS && FABLE_ITEMS !== 'all') { const ids = new Set(plan.items.map((i) => i.id)) const bad = FABLE_ITEMS.filter((id) => !ids.has(id)) if (bad.length) throw new Error(`fableItems: unknown item id(s): ${bad.join(', ')}. Valid ids in this plan (hash ${PLAN_HASH}): ${[...ids].join(', ')}. Item ids are minted by the planner, so a fresh run replans and re-mints them - to execute the exact plan you previewed, pass it back: plan:, verdicts:, recon:, then your fableItems picks bind correctly.`) } // ---------------- per-item ledger (telemetry; joins verdict -> outcome) ---------------- // This is the raw eval data for the selector A/B: for every item, what the // selector said, what actually ran, and whether it landed first try. const ledger = new Map(plan.items.map((i) => [i.id, { id: i.id, complex: !!i.complex, verdict: null, assignedMode: 'normal', attempts: [], finalModel: null, firstAttemptGreen: null, escalatedOnFailure: false, unresolvedFindings: 0, }])) // ---------------- innovate: disinterested selector + dollar budget ---------------- // The selector prompt depends ONLY on task/plan/recon - never on --fable-budget, // fableItems, or CASCADE_RESERVE - so a planOnly preview and its execute run // share the cache, and budget/picks can change without re-selecting. const SELECTOR_SCHEMA = { type: 'object', required: ['verdicts'], properties: { verdicts: { type: 'array', items: { type: 'object', required: ['id', 'escalate', 'forceOpus', 'strategy', 'signals', 'rationale', 'estTokensK', 'benefit'], properties: { id: { type: 'string' }, escalate: { type: 'boolean' }, forceOpus: { type: 'boolean' }, strategy: { type: 'string', enum: ['fable-first', 'opus-first-cascade'] }, signals: { type: 'array', items: { type: 'string' } }, rationale: { type: 'string' }, estTokensK: { type: 'number' }, benefit: { type: 'string', enum: ['high', 'medium', 'low'] }, }, }, }, }, } const assign = new Map() // id -> { mode: 'fable-first'|'opus-first-cascade'|'opus-only'|'normal', estUsd, why } let selector = null if (INNOVATE) { startPhase('Select') if (SUPPLIED_VERDICTS) { selector = { verdicts: SUPPLIED_VERDICTS } const ids = new Set(plan.items.map((i) => i.id)) const stray = SUPPLIED_VERDICTS.filter((v) => !ids.has(v.id)).map((v) => v.id) if (stray.length) log(`⚠ supplied verdicts reference ids not in this plan (ignored): ${stray.join(', ')} - did the plan and verdicts come from the same preview?`) log('selector: reusing supplied verdicts from preview - zero Select spend') } else { selector = await agent( `You are a DISINTERESTED model-tier selector. For each work item decide whether it warrants escalation to the FABLE tier (most capable, ~2x the per-token cost of OPUS) or should run on OPUS. You have NO stake in Fable being used - protect cost. TASK: ${task} PLAN: ${plan.summary} ITEMS: ${JSON.stringify(plan.items.map((i) => ({ id: i.id, title: i.title, files: i.files, wave: i.wave, complex: !!i.complex, verify: i.verify, instructions: i.instructions })))} RECON (read-only scouts - use for novelty / prior-art judgement): ${JSON.stringify(scouts)} ESCALATE to Fable when ANY fire (record which in "signals"): - estimated effort > ~1hr human-equivalent, or explicitly multi-stage - touches > 3 files, or must hold an invariant spanning modules - no test coverage exists for the affected code - production-critical / high blast radius / expensive to reverse - genuine novelty with no in-repo prior art (judge against RECON) HARD EXCLUSIONS - set forceOpus=true, escalate=false (NEVER Fable): - security / cryptography / privacy / bio / chem. Pinned fact (${FABLE_BEHAVIOR.asOf}, ${FABLE_BEHAVIOR.modelId}): ${FABLE_BEHAVIOR.classifierNote} - zero-data-retention-sensitive items (BEST-EFFORT: only flag when the item explicitly handles sensitive data; ZDR need is often invisible from a description) - interactive / trivial / single-function / boilerplate - underspecified items (Fable over-explores loose prompts) - keep on Opus STRATEGY for escalated items: - has tests AND normal blast radius -> "opus-first-cascade" (try Opus; only escalate to Fable, with the failure as context, if Opus fails verification) - weak-verification / high-blast / long-horizon / genuinely novel -> "fable-first" Per item return: id, escalate, forceOpus, strategy, signals (which fired), a one-line rationale, estTokensK (your honest estimate of TOTAL tokens, in THOUSANDS, input+output, this item would consume on its escalated tier), and benefit ("high"|"medium"|"low": how much escalation would actually improve the outcome). The rationale is printed to the user under the column "what Fable buys you": for escalated items it must state the CONSEQUENCE of a cheap-model miss in that item (what would break or ship silently wrong, and whether verification would catch it), not a description of the item; for non-escalated items, one line on why the standard tier suffices. Keep it under ~55 characters - the table truncates at 60. Estimate tokens honestly; never inflate or deflate to steer the decision. You do NOT estimate dollars - the harness prices your token estimate deterministically from a pinned rate table.`, { label: 'selector', phase: 'Select', model: SELECTOR_MODEL, effort: 'medium', agentType: 'Explore', schema: SELECTOR_SCHEMA }, ) } // Deterministic dollar math: price the token estimates. Supplied verdicts // from a preview already carry estUsd; keep them (the human's picks were // made against those numbers) - but if PRICING changed since the preview, // say so loudly instead of letting stale dollars win silently. for (const v of selector?.verdicts ?? []) if (typeof v.estUsd !== 'number') v.estUsd = estUsdFromTokensK(v.estTokensK) if (SUPPLIED_VERDICTS) { const stale = (selector?.verdicts ?? []).filter((v) => typeof v.estTokensK === 'number' && v.estUsd !== estUsdFromTokensK(v.estTokensK)) if (stale.length) log(`⚠ supplied verdicts were priced under a pricing table OLDER than the pinned one (asOf ${PRICING.asOf}): ${stale.map((v) => `${v.id} $${v.estUsd.toFixed(2)} (would be $${estUsdFromTokensK(v.estTokensK).toFixed(2)} today)`).join(', ')} - keeping the previewed dollars your picks were made against; re-run the preview if the drift matters`) } const V = new Map((selector?.verdicts ?? []).map((v) => [v.id, v])) // --fable-items=all expands to every plan item that is not a hard exclusion. // Expansion happens HERE (verdicts are needed to know the exclusions); the // expanded list then flows through the normal human-pick machinery. if (FABLE_ITEMS === 'all') { const excluded = plan.items.filter((i) => V.get(i.id)?.forceOpus).map((i) => i.id) FABLE_ITEMS = plan.items.filter((i) => !V.get(i.id)?.forceOpus).map((i) => i.id) log(`fable-items=all: expanded to every non-excluded plan item: ${FABLE_ITEMS.join(', ') || '(none)'}${excluded.length ? `; hard exclusions kept on Opus: ${excluded.join(', ')}` : ''}`) } const BENEFIT_RANK = { high: 3, medium: 2, low: 1 } const cands = plan.items.map((it) => ({ it, v: V.get(it.id) })) .filter((x) => x.v && x.v.escalate && !x.v.forceOpus) .sort((a, b) => ((BENEFIT_RANK[b.v.benefit] ?? 0) - (BENEFIT_RANK[a.v.benefit] ?? 0)) || (a.v.estUsd - b.v.estUsd)) // Budget reservation: fable-first items reserve full estUsd (they WILL spend // it); cascade items reserve estUsd * CASCADE_RESERVE (they only spend on an // Opus failure). Worst-case exposure is logged alongside. const reserveOf = (v) => v.strategy === 'opus-first-cascade' ? Math.round(v.estUsd * CASCADE_RESERVE * 100) / 100 : v.estUsd const chosen = new Set(); let reserved = 0; let worstCase = 0 for (const { it, v } of cands) { const r = reserveOf(v) if (reserved + r <= FABLE_BUDGET) { chosen.add(it.id); reserved += r; worstCase += v.estUsd } else log(`demoted to Opus (Fable $ budget ${fmtFableBudget()} exhausted at $${reserved.toFixed(2)} reserved): ${it.id} (~$${v.estUsd.toFixed(2)} est, benefit ${v.benefit})`) } // Human picks: honored OUTSIDE the budget (an explicit human decision beats // the cap) but every dollar of override is tallied and logged - silent // budget bypass is exactly what our own review phase would flag. // A pick always MEANS fable-first: the human asked for Fable, so a cascade // or escalate=false verdict is overridden (and logged). Hard exclusions // still beat picks. The ledger keeps the selector's original verdict; // only the assignment changes, so the eval join stays honest. const humanPicks = new Set() if (FABLE_ITEMS) for (const id of FABLE_ITEMS) { let v = V.get(id) if (!v) { const it = plan.items.find((i) => i.id === id) v = { id, escalate: true, forceOpus: false, strategy: 'fable-first', signals: ['human pick'], rationale: 'human pick (no selector verdict for this item)', estUsd: estUsdFromTokensK(roughItemTokensK(it)), benefit: 'high' } V.set(id, v) log(`fableItems: '${id}' had no selector verdict - treating as fable-first at a rough deterministic estimate (~$${v.estUsd.toFixed(2)})`) } if (v.forceOpus) { log(`fableItems: '${id}' is a HARD EXCLUSION (${(v.signals || []).join('; ') || 'forced Opus'}) - kept on Opus despite your pick`); continue } if (!v.escalate || v.strategy !== 'fable-first') { const overridden = [!v.escalate ? 'escalate=false' : null, v.strategy !== 'fable-first' ? `strategy=${v.strategy}` : null].filter(Boolean).join(', ') log(`fableItems: human pick '${id}' overrides the selector verdict (${overridden}) - a pick always means fable-first`) } humanPicks.add(id) if (!chosen.has(id)) { chosen.add(id) fableStats.overrideUsd += v.estUsd log(`fableItems: human pick '${id}' added OUTSIDE the ${fmtFableBudget()} Fable $ budget (~$${v.estUsd.toFixed(2)} est) - honoring the override; total override spend now $${fableStats.overrideUsd.toFixed(2)} (est)`) } } for (const it of plan.items) { const v = V.get(it.id) if (!v) assign.set(it.id, { mode: 'normal' }) else if (v.forceOpus) assign.set(it.id, { mode: 'opus-only', estUsd: 0, why: v.rationale }) else if (chosen.has(it.id)) assign.set(it.id, { mode: humanPicks.has(it.id) ? 'fable-first' : v.strategy, estUsd: v.estUsd || 0, why: humanPicks.has(it.id) ? 'human pick (fable-first)' : v.rationale }) else assign.set(it.id, { mode: v.escalate ? 'opus-only' : 'normal', estUsd: 0, why: v.escalate ? 'demoted (Fable $ budget)' : v.rationale }) const L = ledger.get(it.id) if (L) { L.verdict = v ? { escalate: v.escalate, forceOpus: v.forceOpus, strategy: v.strategy, benefit: v.benefit, estUsd: v.estUsd, estTokensK: v.estTokensK, signals: v.signals } : null L.assignedMode = (assign.get(it.id) || { mode: 'normal' }).mode } } const fableIds = [...chosen] // v2.6.2 plain-language rule: no "escalation"/"exposure" in user-facing // prints. Demotions below budget are itemized by their own log lines. log(`selector (${SELECTOR_MODEL}): ${cands.length} item${cands.length === 1 ? '' : 's'} recommended for Fable${fableIds.length < cands.length ? `, ${fableIds.length} within budget` : ''} - $${reserved.toFixed(2)} reserved of the ${fmtFableBudget()} budget (worst-case $${worstCase.toFixed(2)} if every backup fires${fableStats.overrideUsd ? `; +$${fableStats.overrideUsd.toFixed(2)} human override outside budget` : ''}): ${fableIds.join(', ') || 'none'}`) } // ---------------- deterministic innovate-eligibility nudge (normal runs only; NO model) ---------------- // v2.6: respects user-supplied verify commands, and prices the hint with the // same deterministic table so the upsell is a number, not an adjective. let nudge = null if (!INNOVATE && NUDGE) { const verifyCmds = USER_VERIFY || pre.verifyCommands || [] const hasTests = verifyCmds.some((c) => /\b(test|vitest|jest|pytest|mocha|rspec|go test|cargo test|phpunit)\b/i.test(c)) const hits = plan.items.filter((it) => it.complex || (it.files?.length ?? 0) > 3 || !hasTests) if (hits.length) { const reasons = [...new Set(hits.flatMap((it) => [ it.complex ? 'cross-cutting/complex item' : null, (it.files?.length ?? 0) > 3 ? 'item touches >3 files' : null, !hasTests ? 'no test suite detected (weak verification)' : null, ].filter(Boolean)))] const roughUsd = hits.reduce((s, it) => s + estUsdFromTokensK(roughItemTokensK(it)), 0) nudge = `innovate hint: ${hits.length} item(s) matched innovate criteria (${reasons.join('; ')}); rough Fable projection ~$${roughUsd.toFixed(2)} (deterministic estimate from file counts and the pinned rate table of ${PRICING.asOf}; no model consulted). Re-run with --quote to see per-item recommendations and pick yourself (nothing is built), or --innovate to let the selector decide, if quality matters more than cost on this task (suppress with --no-nudge).` log(nudge) } } // ---------------- quote: print the offer + persist it for --execute ---------------- // Everything printed here is deterministic and costs zero extra model calls; // the only agent is the haiku writer that persists the two files (Design B: // the workflow sandbox has no fs API, so persistence is agent-mediated, the // same pattern as the preflight lock write). let quoteSaved = null, spectrum = null, exposure = null if (QUOTE) { const V2 = new Map((selector?.verdicts ?? []).map((v) => [v.id, v])) spectrum = costSpectrum(plan.items, V2, CASCADE_RESERVE) exposure = exposureOf(selector?.verdicts ?? []) const modeLabel = (it) => { const m = (assign.get(it.id) || { mode: 'normal' }).mode if (m === 'fable-first') return 'Fable recommended' if (m === 'opus-first-cascade') return 'Opus first, Fable as backup on failure' if (m === 'opus-only') return V2.get(it.id)?.forceOpus ? 'Opus (locked: safety rule)' : 'Opus (over the Fable budget)' return `standard (${it.complex ? 'Opus' : 'Sonnet'})` } const rows = plan.items.map((it) => { const v = V2.get(it.id) const why = (v?.rationale ?? '-').replace(/\s+/g, ' ') return [it.id, modeLabel(it), why.length > 60 ? why.slice(0, 57) + '...' : why, v && v.escalate && !v.forceOpus ? '$' + (v.estUsd ?? 0).toFixed(2) : '-'] }) const wid = ['item'.length, 'model plan'.length, 'what Fable buys you'.length].map((h, c) => Math.max(h, ...rows.map((r) => r[c].length))) log([` ${'item'.padEnd(wid[0])} ${'model plan'.padEnd(wid[1])} ${'what Fable buys you'.padEnd(wid[2])} est`, ...rows.map((r) => ` ${r[0].padEnd(wid[0])} ${r[1].padEnd(wid[1])} ${r[2].padEnd(wid[2])} ${r[3]}`)].join('\n')) log(`cost: standard ~$${spectrum.normal.toFixed(2)} | recommended ~$${spectrum.advisor.toFixed(2)} | everything-Fable ~$${spectrum.allFable.toFixed(2)}`) log(`(implementer-spend estimates from the pinned ${PRICING.asOf} rate table; live runs so far have come in under estimates)`) const lowIds = plan.items.map((i) => i.id).filter((id) => !exposure.exposed.includes(id)) log(`risk if you skip Fable entirely: ${exposure.level} - ${exposure.exposed.length ? exposure.exposed.join(', ') + ': ' : ''}${exposure.reason}.${exposure.exposed.length && lowIds.length ? ` ${lowIds.join(', ')} ${lowIds.length === 1 ? 'is' : 'are'} low-risk.` : ''}`) // persist (agent-mediated; size-disciplined: only what execution needs) const payload = { savedBy: 'purser v2.6.2', task, planHash: PLAN_HASH, pricingAsOf: PRICING.asOf, plan, verdicts: selector?.verdicts ?? [], recon: scouts ?? [], reconTrimmed: false } const MAX_QUOTE_CHARS = 48000 if (JSON.stringify(payload).length > MAX_QUOTE_CHARS) { payload.reconTrimmed = true payload.recon = (scouts ?? []).map((s) => ({ findings: (s.findings ?? []).slice(0, 8) })) while (JSON.stringify(payload).length > MAX_QUOTE_CHARS && payload.recon.length) payload.recon.pop() log(`quote: recon trimmed to keep the saved payload under ${MAX_QUOTE_CHARS} chars (execution does not re-read it; the selector already consumed the full recon)`) } startPhase('Quote') const saver = await agent( `Persist a small JSON file. The ONLY writes allowed are the two files below. Run exactly: DIR="$(git rev-parse --path-format=absolute --git-common-dir)/purser/previews" mkdir -p "$DIR" Write the JSON between the markers VERBATIM (byte-for-byte; never reformat, summarize, or fix it) to "$DIR/${PLAN_HASH}.json" (overwrite if present), then write the single line ${PLAN_HASH} to "$DIR/latest" (overwrite). Read "$DIR/${PLAN_HASH}.json" back and confirm it parses as JSON. Return written=true ONLY if both files exist and the readback parses; return path="$DIR/${PLAN_HASH}.json" and notes on anything odd. =====BEGIN QUOTE JSON===== ${JSON.stringify(payload)} =====END QUOTE JSON=====`, { label: 'quote-save', phase: 'Quote', model: 'haiku', effort: 'low', schema: { type: 'object', required: ['written'], properties: { written: { type: 'boolean' }, path: { type: 'string' }, notes: { type: 'string' } } } }, ) if (saver?.written) { quoteSaved = { hash: PLAN_HASH, path: saver.path ?? null, reconTrimmed: payload.reconTrimmed } log(`quote ready - ${plan.items.length} work item${plan.items.length === 1 ? '' : 's'} (saved; reply below, or execute any time later)`) // v2.6.2: reply words, not menu numbers. "none" maps to --fable-budget=0, // which demotes recommended items to Opus (not sonnet) - price it honestly // here rather than reusing the spectrum's "normal" column. const advisorPicks = (selector?.verdicts ?? []).filter((v) => v.escalate && !v.forceOpus).map((v) => v.id) const nameable = plan.items.map((i) => i.id).filter((id) => !V2.get(id)?.forceOpus) const noneUsd = plan.items.reduce((s, it) => { const v = V2.get(it.id) const k = typeof v?.estTokensK === 'number' ? v.estTokensK : roughItemTokensK(it) return s + k * (v?.forceOpus || v?.escalate || it.complex ? tierRate.opus : tierRate.sonnet) }, 0) log(`what next? reply with a word: advisor - Fable on the advisor's picks: ${advisorPicks.length ? `${advisorPicks.join(', ')} (~$${spectrum.advisor.toFixed(2)} total)` : 'none recommended (same as "none")'} all - everything on Fable (~$${spectrum.allFable.toFixed(2)}) none - no Fable; recommended items fall back to Opus (~$${(Math.round(noneUsd * 100) / 100).toFixed(2)}) or name the items you want on Fable${nameable.length ? ` - e.g. "${nameable.slice(0, 2).join(', ')}"` : ''}`) log(`(internals, only needed for scripting: quote id ${PLAN_HASH}, branch slug ${plan.slug}; execute later with /purser --execute, an older quote with /purser --execute=, item picks with --fable-items=|all, no Fable with --fable-budget=0)`) log(`assistant: when the user replies to this menu, translate the reply into a command and run it - "advisor" means /purser --execute; "all" means /purser --execute --fable-items=all; "none" means /purser --execute --fable-budget=0; item names (bare, or with words like "just") mean /purser --execute --fable-items=. Exclusions ("all except docs") are NOT supported - translate by naming the wanted items instead. Add --fable-budget=... only if the user asks. An older quote is addressed as --execute=${PLAN_HASH}. Run it via Workflow({name:'purser', args:{task:''}}).`) } else { log(`⚠ quote save FAILED (${saver?.notes ?? 'writer agent returned nothing'}) - --execute will NOT find this quote. The preview below is still complete; execute it with the structured-args rerun instead.`) } } // ---------------- Learn Mode: explain the SYSTEM DESIGN of what's being built ---------------- // --explain teaches the developer the system design of the FEATURE this run builds // (NOT purser itself), so real work doubles as interview-grade practice. An // in-workflow Sonnet tutor reads the task + plan and explains it; the plan is INPUT // only - never altered - so this is orthogonal to what gets built. It runs here // (after planning/selection, before the planOnly return) so it appears in both // previews (planOnly/quote) and full runs. let designExplanation = null if (EXPLAIN) { startPhase('Explain') const deep = EXPLAIN === 'deep' const focus = (typeof EXPLAIN === 'string' && EXPLAIN !== 'deep') ? EXPLAIN : null // minItems:1 makes the tradeoffs non-optional: the runtime rejects an empty // keyDecisions and the model retries, so "explains the tradeoffs" is enforced, // not merely requested. (Depth/quality is still the model's; only presence is // guaranteed by schema.) const EXPLAIN_SCHEMA = { type: 'object', required: ['overview', 'keyDecisions', 'conceptsExercised'], properties: { overview: { type: 'string' }, keyDecisions: { type: 'array', minItems: 1, items: { type: 'object', required: ['decision', 'tradeoff'], properties: { decision: { type: 'string' }, tradeoff: { type: 'string' }, alternative: { type: 'string' } } } }, conceptsExercised: { type: 'array', minItems: 1, items: { type: 'string' } }, interviewTopics: { type: 'array', items: { type: 'string' } }, }, } const planForExplain = plan.items.map((i) => ({ id: i.id, title: i.title, instructions: i.instructions, files: i.files })) const explainPrompt = `You are a system-design tutor. A developer is building the change below (via an automated implementation pipeline). Explain the SYSTEM DESIGN OF THE FEATURE BEING BUILT so they genuinely learn - target: interview-grade system-design understanding. Explain the design of the THING being built; do NOT explain the pipeline or tooling. TASK: ${task} PLAN (work items to be implemented): ${JSON.stringify(planForExplain).slice(0, 6000)} Cover, concretely for THIS change: what it builds and its components + data flow; the key design decisions with their tradeoffs and the alternatives not taken (ALWAYS give at least one design decision with an explicit tradeoff - that is the point of the exercise); and the general CS/SWE concepts it exercises - data structures & algorithms, storage/data modeling, concurrency, caching, scaling & load, failure modes, security, and AI/ML system-design aspects IF the task involves ML/data. Be honest about what the plan does not determine. You may read referenced files read-only to ground it.${focus ? `\nEMPHASIZE the '${focus}' angle throughout.` : ''}${deep ? `\nDEEP MODE: for each major design choice, also name the canonical system-design INTERVIEW topic it exemplifies and what an interviewer would probe - fill interviewTopics.` : ''} Return overview, keyDecisions (decision/tradeoff/alternative), conceptsExercised${deep ? ', and interviewTopics' : ''}.` const explainOpts = { label: 'explain', phase: 'Explain', model: 'sonnet', effort: deep ? 'high' : 'medium', agentType: 'Explore', schema: EXPLAIN_SCHEMA } designExplanation = await agent(explainPrompt, explainOpts).catch(() => null) if (!designExplanation) { // one retry so a single hard failure doesn't silently drop the lesson log('Learn Mode: the tutor did not return - retrying once') designExplanation = await agent(explainPrompt, { ...explainOpts, label: 'explain:retry' }).catch(() => null) } if (designExplanation) log(`Learn Mode: explained the system design of this change - ${(designExplanation.conceptsExercised || []).length} concept(s)${deep ? `, ${(designExplanation.interviewTopics || []).length} interview topic(s)` : ''}.`) else log('Learn Mode: the tutor did not return after a retry; skipped (the run is unaffected).') } if (PLAN_ONLY) { const planWaves = [...new Set(plan.items.map((i) => i.wave))].sort((a, b) => a - b) const complexIds = plan.items.filter((i) => i.complex).map((i) => i.id) log(`planOnly: stopping before implementation - ${plan.items.length} item(s) in ${planWaves.length} wave(s); complex/opus items: ${complexIds.length ? complexIds.join(', ') : 'none'}`) return { planOnly: true, planHash: PLAN_HASH, plan, recon: scouts, ...(INNOVATE ? { selector: selector?.verdicts ?? [], fableBudget: FABLE_BUDGET === Infinity ? 'unlimited' : FABLE_BUDGET, assignments: Object.fromEntries([...assign].map(([k, v]) => [k, v.mode])), estUsdByItem: Object.fromEntries((selector?.verdicts ?? []).map((v) => [v.id, v.estUsd])), pricingAsOf: PRICING.asOf, rerun: quoteSaved ? `The quote is saved. Execute it: /purser --execute (advisor's picks) or /purser --execute --fable-items=; an older quote is --execute=${PLAN_HASH}. Structured-args alternative (identical result): Workflow({ name: 'purser', args: { task: , innovate: true, plan: , verdicts: , recon: , fableItems: [''] } }).` : `To execute EXACTLY this plan with your hand-picked Fable items (zero Explore/Plan/Select spend): Workflow({ name: 'purser', args: { task: , innovate: true, plan: , verdicts: , recon: , fableItems: [''] } }). Unknown ids fail loudly against plan hash ${PLAN_HASH}.`, } : {}), ...(QUOTE ? { quote: { saved: !!quoteSaved, hash: PLAN_HASH, path: quoteSaved?.path ?? null, reconTrimmed: !!quoteSaved?.reconTrimmed, spectrum, exposure } } : {}), ...(nudge ? { nudge } : {}), ...(EXPLAIN ? { explain: EXPLAIN } : {}), ...(designExplanation ? { designExplanation } : {}), ...(unappliedFlags.length ? { unappliedFlags } : {}), } } // ---------------- Phase 3+4: Implement waves, integrate ---------------- const IMPL_SCHEMA = { type: 'object', required: ['committed', 'verified', 'summary'], properties: { committed: { type: 'boolean' }, verified: { type: 'boolean' }, summary: { type: 'string' }, notes: { type: 'string' } }, } const MERGE_SCHEMA = { type: 'object', required: ['merged', 'verified'], properties: { merged: { type: 'array', items: { type: 'string' } }, dropped: { type: 'array', items: { type: 'object', required: ['id', 'why'], properties: { id: { type: 'string' }, why: { type: 'string' } } } }, verified: { type: 'boolean' }, notes: { type: 'string' }, }, } const branchOf = (item) => `wf/${plan.slug}/${item.id}` const INTEG = FAST ? branchOf(plan.items[0]) : `wf/${plan.slug}/integration` const implStats = { sonnet: { tried: 0, failed: 0, budgetAborted: 0 }, opus: { tried: 0, failed: 0, budgetAborted: 0 } } const implementItem = (item, tip, attempt, step = {}) => { const model = step.model || (item.complex || attempt > 1 ? 'opus' : 'sonnet') const effort = step.effort || (item.complex ? 'high' : 'medium') implStats[model] = implStats[model] || { tried: 0, failed: 0, budgetAborted: 0 } implStats[model].tried++ const guard = step.guard ? '\nSCOPE GUARD (cost control): work ONLY within the files listed below; do not investigate or refactor beyond this item. If you cannot finish within a focused effort, stop and return committed=false with notes - do not open-endedly explore.' : '' const ctx = step.contextNote ? `\n${step.contextNote}` : '' return agent( `Implement ONE work item from a reviewed plan. Stay strictly inside its scope.${attempt > 1 ? '\n(RETRY: a previous attempt failed to commit - be careful to finish: implement, verify, commit, publish.)' : ''}${guard}${ctx} ${GIT_RULES(tip)} ITEM ${item.id}: ${item.title} FILES YOU OWN (edit others only for trivially-necessary imports): ${JSON.stringify(item.files)} INSTRUCTIONS: ${item.instructions} VERIFY with (must all pass; if one cannot run here, say so in notes - never skip silently): ${JSON.stringify(FAST ? VERIFY : item.verify)} When green: commit (per rule 4) with message "purser(${plan.slug}): ${item.id} - ${item.title}" Then publish EXACTLY: git branch -f ${branchOf(item)} HEAD Return committed, verified, a one-line summary, and notes for the integrator.`, { label: `impl:${item.id}${step.label || (attempt > 1 ? ':retry' : '')}`, phase: 'Implement', model, effort, isolation: 'worktree', schema: IMPL_SCHEMA }, ).then((r) => { const L = ledger.get(item.id) if (L) { L.attempts.push({ attempt, model, committed: !!r?.committed, verified: !!r?.verified, summary: r?.summary ?? null, notes: r?.notes ?? null }) if (attempt === 1) L.firstAttemptGreen = !!(r?.committed && r?.verified) if (r?.committed) L.finalModel = model } if (!r?.committed) implStats[model].failed++ return { item, r, model } }).catch(() => { const L = ledger.get(item.id) if (L) { L.attempts.push({ attempt, model, committed: false, verified: false }); if (attempt === 1) L.firstAttemptGreen = false } implStats[model][budgetLow() ? 'budgetAborted' : 'failed']++ return { item, r: null, model } }) } // innovate: resolve the model/strategy for an item at a given attempt. // Normal mode returns {} so implementItem uses today's casting unchanged. const stepFor = (item, attempt, prev) => { if (!INNOVATE) return {} const a = assign.get(item.id) || { mode: 'normal' } if (a.mode === 'fable-first') { if (attempt === 1) { logFable('impl:fable-first', item.id, a.estUsd); fableStats.fableFirst++; return { model: 'fable', guard: true, effort: 'high', label: ':fable' } } const why = prev?.r ? `Notes from the Fable attempt: ${prev.r.notes || 'none'}.` : 'The Fable attempt did not return (Fable may be unavailable).' log(`Fable-first item '${item.id}' did not land - falling back to Opus (graceful degradation)`) return { model: 'opus', label: ':degrade', contextNote: `(A Fable-first attempt did not land. ${why} Complete this item on Opus.)` } } if (a.mode === 'opus-first-cascade') { if (attempt === 1) return { model: 'opus' } logFable('impl:escalate-on-failure', item.id, a.estUsd); fableStats.escalateOnFailure++ const L = ledger.get(item.id); if (L) L.escalatedOnFailure = true return { model: 'fable', guard: true, effort: 'high', label: ':escalate', contextNote: `(The Opus attempt failed verification. Failure notes: ${prev?.r?.notes || 'none provided'}. You are Fable - diagnose and resolve it, then verify, commit, and publish.)` } } if (a.mode === 'opus-only') return { model: 'opus' } return {} } let tip = BASE let integVerified = false let anyMerged = false const done = [], droppedItems = [] const waves = [...new Set(plan.items.map((i) => i.wave))].sort((a, b) => a - b) for (const w of waves) { if (budgetLow()) throw new Error(`TOKEN BUDGET EXHAUSTED before wave ${w} (${budget.remaining()} tokens remaining; distinct from the Fable $ budget) - stopping cleanly before implementer spend. Completed item branches: ${done.map((x) => branchOf(x.item)).join(', ') || 'none'}. Resume with resumeFromRunId when budget allows; the repo lock goes stale in <=${LOCK_STALE_MIN} min (or rm it / pass force:true).`) startPhase('Implement') const waveItems = plan.items.filter((i) => i.wave === w) let results = await parallel(waveItems.map((item) => () => implementItem(item, tip, 1, stepFor(item, 1)))) // one retry for failed implementers (cheap vs losing a planned item) const failed = results.filter((x) => !x?.r?.committed).map((x) => x?.item).filter(Boolean) if (failed.length) { log(`wave ${w}: retrying ${failed.length} failed item(s): ${failed.map((i) => i.id).join(', ')}`) const retried = await parallel(failed.map((item) => () => implementItem(item, tip, 2, stepFor(item, 2, results.find((x) => x?.item?.id === item.id))))) results = results.map((x) => (x?.r?.committed ? x : retried.find((y) => y?.item?.id === x?.item?.id) ?? x)) } for (const x of results) { if (x?.r?.committed) done.push(x) else droppedItems.push({ id: x?.item?.id ?? 'unknown', why: budgetLow() ? 'token budget exhausted' : 'implementer failed twice' }) } // Branch names are derived, never trusted from agent output (red-team #2). const waveBranches = results.filter((x) => x?.r?.committed).map((x) => branchOf(x.item)) if (!waveBranches.length) { log(`wave ${w}: nothing to merge`); continue } if (FAST) { tip = waveBranches[0]; integVerified = !!results[0]?.r?.verified; anyMerged = true; break } startPhase('Integrate') const merge = await agent( `You integrate parallel work branches into one. ${GIT_RULES(tip)} For each branch, in order: ${JSON.stringify(waveBranches)} - First 'git rev-parse --verify ' - if missing, record {id, why} in dropped (id = the wf/.../ suffix) and continue. - 'git merge --no-ff '. On conflict, resolve it YOURSELF using the plan below as intent; if unresolvable after two honest attempts or the branch is broken, 'git merge --abort', record it in dropped, continue. - After all merges, run the FULL verify suite: ${JSON.stringify(VERIFY)}. Fix small integration breakages (imports, types) yourself and commit the fixes. - Publish EXACTLY: git branch -f ${INTEG} HEAD - but ONLY if you merged at least one branch. Return verified=true only if the full suite passed. PLAN: ${plan.summary} WAVE ITEMS: ${JSON.stringify(waveItems.map((i) => ({ id: i.id, title: i.title, files: i.files })))} IMPLEMENTER NOTES: ${JSON.stringify(results.filter(Boolean).map((x) => ({ id: x.item.id, notes: x.r?.notes ?? null })))}`, { label: `merge:wave${w}`, phase: 'Integrate', model: 'sonnet', effort: 'medium', isolation: 'worktree', schema: MERGE_SCHEMA }, ) if (!merge) throw new Error(`wave ${w} integration agent failed (item branches are published: ${waveBranches.join(', ')})`) for (const d of merge.dropped ?? []) { droppedItems.push(d) const i = done.findIndex((x) => x.item.id === d.id || branchOf(x.item) === d.id) if (i >= 0) done.splice(i, 1) } if (merge.merged.length) anyMerged = true integVerified = !!merge.verified if (!merge.verified) log(`⚠ wave ${w} integrated but full verify NOT green: ${merge.notes ?? ''}`) tip = INTEG // Later waves depend on earlier output; don't build on a hole (red-team #4). if (droppedItems.length && waves.some((w2) => w2 > w)) { throw new Error(`items dropped in wave ${w} but later waves depend on earlier output - stopping. Published branches survive for resume/manual recovery: ${JSON.stringify(droppedItems)}`) } } if (!anyMerged) throw new Error(`no work item survived (dropped: ${JSON.stringify(droppedItems)})`) if (droppedItems.length) log(`dropped (NOT in the PR): ${JSON.stringify(droppedItems)}`) // If integration is red, one repair attempt - never send opus to review a // known-red build (economics #8), never ship silently-red work (SRE #6). if (!integVerified) { startPhase('Integrate') const repair = await agent( `The integrated branch fails its verify suite. Repair it. ${GIT_RULES(tip)} Run: ${JSON.stringify(VERIFY)} - diagnose failures, fix them, re-run until green. When green: commit (rule 4) and publish EXACTLY: git branch -f ${INTEG} HEAD If you cannot get it green, DO NOT move ${INTEG}: return committed=false with notes explaining what still fails.`, { label: 'repair', phase: 'Integrate', model: 'opus', effort: 'medium', isolation: 'worktree', schema: IMPL_SCHEMA }, ) if (repair?.committed && repair.verified) { integVerified = true; tip = INTEG } else log(`⚠ repair failed; PR will be flagged VERIFY-FAILED (${repair?.notes ?? 'no notes'})`) } // ---------------- Phase 5: Review (opus high, READ-ONLY) + fix + fix-verify ---------------- const REVIEW_SCHEMA = { type: 'object', required: ['findings'], properties: { findings: { type: 'array', items: { type: 'object', required: ['severity', 'file', 'summary', 'failure_scenario'], properties: { severity: { type: 'string', enum: ['high', 'medium', 'low'] }, file: { type: 'string' }, line: { type: 'integer' }, summary: { type: 'string' }, failure_scenario: { type: 'string' }, }, }, }, }, } let review = null let unresolved = [] let reviewTier = 'opus' if (!integVerified) log('skipping adversarial review: build is red (repair failed)') else { const lowBudget = budgetLow() if (lowBudget) { reviewTier = 'sonnet (token-budget fallback)' log('token budget nearly exhausted: review downgraded to sonnet-medium instead of skipped (green builds are always reviewed)') } startPhase('Review') review = await agent( `Adversarial code review of branch ${tip} against ${BASE}. You are read-only in the main repo. Run: git diff ${BASE}...${tip} - then read surrounding code for anything suspicious (git show ${tip}: for full files). TASK the diff claims to implement: ${task} PLAN: ${plan.summary} Hunt CORRECTNESS bugs (broken behavior, races, security/privacy leaks, unhandled errors, contract violations with existing callers) and plan divergence. Treat ANY modification to test files as a finding unless the plan explicitly required changing those tests - weakened or deleted assertions are how a bad implementer games the verify suite. Only report findings verified against the actual code, each with a concrete failure scenario. An empty findings array is a valid, good outcome.`, { label: 'review', phase: 'Review', model: lowBudget ? 'sonnet' : 'opus', effort: lowBudget ? 'medium' : 'high', agentType: 'Explore', schema: REVIEW_SCHEMA }, ) const serious = (review?.findings ?? []).filter((f) => f.severity !== 'low') log(`review: ${review?.findings?.length ?? 0} finding(s), ${serious.length} serious`) if (serious.length) { unresolved = serious const fix = await agent( `Fix confirmed review findings on the integration branch. ${GIT_RULES(tip)} FINDINGS (skip any you can PROVE wrong - say so in notes): ${JSON.stringify(serious)} Re-run the full verify suite: ${JSON.stringify(VERIFY)} When green: commit (rule 4, message "purser(${plan.slug}): review fixes") and publish EXACTLY: git branch -f ${INTEG} HEAD If not green, DO NOT move ${INTEG}: return committed=false with notes.`, { label: 'fix', phase: 'Review', model: 'opus', effort: 'high', isolation: 'worktree', schema: IMPL_SCHEMA }, ) if (fix?.committed) { tip = INTEG integVerified = !!fix.verified // Cheap diff-scoped confirmation instead of a second full opus round. const check = await agent( `Read-only: confirm review findings were resolved on ${tip}. For each finding below, check the current code (git show ${tip}:) and return ONLY the ones still present. ${JSON.stringify(serious)}`, { label: 'fix-verify', phase: 'Review', model: 'sonnet', effort: 'medium', agentType: 'Explore', schema: REVIEW_SCHEMA }, ) unresolved = check ? (check.findings ?? []) : serious log(`fix-verify: ${unresolved.length} finding(s) still unresolved`) } else { log(`⚠ fix agent did not land; findings carry into the PR (${fix?.notes ?? ''})`) } } } // Ledger: attribute still-unresolved findings to owning items (best-effort by // file path; suffix matches only at a path-segment boundary, else 'schema.ts' // would attribute to whoever owns 'a.ts'). for (const f of unresolved) { const owner = plan.items.find((i) => (i.files ?? []).some((p) => p === f.file || p.endsWith('/' + f.file) || (f.file && f.file.endsWith('/' + p)))) if (owner) { const L = ledger.get(owner.id); if (L) L.unresolvedFindings++ } } // ---------------- Fable audit (innovate + --audit): disinterested, skeptical ---------------- // A trust/ROI audit of what Fable actually did and whether it beat plain Opus. // The auditor is SONNET, deliberately NOT Fable - Fable must never grade its own // homework (same principle as the zero-model nudge). It is prompted as a SKEPTIC // that can return 'likely-unnecessary': an audit incapable of a negative verdict // is not a trust audit, and the negatives are exactly the selector-calibration // signal itemLedger exists to collect. Evidence is routed by case: escalate-on- // failure is MEASURED (Opus verifiably failed, then Fable ran) and judged from the // ledger; fable-first is ASSESSED from the actual diff (there is no counterfactual). // Dollars are ESTIMATES - the runtime exposes no per-Fable token counts. Agents // fire ONLY for items where Fable actually ran, so cost is proportional. let audit = null if (INNOVATE && AUDIT) { startPhase('Audit') const AUDIT_SCHEMA = { type: 'object', required: ['verdict', 'opusLikelySufficed', 'evidence'], properties: { verdict: { type: 'string', enum: ['warranted', 'unclear', 'likely-unnecessary'] }, opusLikelySufficed: { type: 'boolean' }, evidence: { type: 'string' }, }, } // Route by what ACTUALLY landed, not just "a Fable attempt exists": a fable-first // item that failed and fell back to Opus has a model:'fable' attempt on record // but its branch holds OPUS's work - sending that diff to the auditor as "what // Fable produced" is a false premise. Audit (agent) only where Fable owns the // outcome: fable-first that LANDED on Fable (diff), or escalate-on-failure (ledger). const ranFableItems = [], reservedItems = [], degradedItems = [] for (const it of plan.items) { const L = ledger.get(it.id); if (!L) continue if (L.escalatedOnFailure || (L.assignedMode === 'fable-first' && L.finalModel === 'fable')) ranFableItems.push(it) else if (L.assignedMode === 'fable-first') degradedItems.push(it) // Fable tried first, did not land else if (L.assignedMode === 'opus-first-cascade') reservedItems.push(it) // Opus succeeded, Fable never fired } const assessed = await parallel(ranFableItems.map((it) => async () => { const L = ledger.get(it.id) const sig = (L.verdict?.signals ?? []).join('; ') || 'none recorded' const why = (assign.get(it.id) || {}).why || 'none recorded' const escalate = L.escalatedOnFailure const opusA = L.attempts.find((a) => a.model === 'opus') const fableA = L.attempts.find((a) => a.model === 'fable') const prompt = escalate ? `You are a DISINTERESTED auditor - you are NOT Fable and have no stake in whether Fable is used. Judge SKEPTICALLY whether Fable earned its cost on this item. Item '${it.id}': ${it.title}. The selector chose opus-first, escalate-to-Fable-on-failure. Signals: ${sig}. Rationale: ${why}. What actually happened (MEASURED, from the run ledger): Opus attempted it and FAILED verification (Opus notes: ${opusA?.notes || opusA?.summary || 'none'}). Fable then ran and ${fableA?.verified ? 'PASSED verification' : 'did NOT pass'} (Fable notes: ${fableA?.notes || fableA?.summary || 'none'}). Confirm read-only from git if useful: git diff ${BASE}...${branchOf(it)}. Decide: is this a GENUINE case where Opus verifiably missed and Fable resolved it ('warranted' - a real measured win), or does the Opus failure look flaky/environmental so the Fable spend bought little ('likely-unnecessary')? Use 'unclear' only if you genuinely cannot tell. Returning a negative verdict when the evidence supports it is EXPECTED and valuable. Return verdict, opusLikelySufficed, and evidence citing specifics.` : `You are a DISINTERESTED auditor - you are NOT Fable and have no stake in whether Fable is used. Judge SKEPTICALLY whether Fable earned its cost on this item. Item '${it.id}': ${it.title}. The selector chose Fable FIRST (up front). Signals: ${sig}. Rationale: ${why}. There is NO Opus counterfactual - only Fable's output exists. Read what Fable produced (read-only: git diff ${BASE}...${branchOf(it)}) and judge whether the work actually shows characteristics that plausibly needed the stronger model - genuine novelty, a cross-module invariant, subtle correctness ('warranted') - or whether it looks like routine work a cheaper model would very likely have produced identically ('likely-unnecessary'). Use 'unclear' only if the diff genuinely does not tell you. Returning 'likely-unnecessary' when the work looks routine is EXPECTED and valuable - a trust audit must be able to say Fable was not needed. Return verdict, opusLikelySufficed, and evidence citing specifics from the diff.` const a = await agent(prompt, { label: `audit:${it.id}`, phase: 'Audit', model: 'sonnet', effort: 'low', agentType: 'Explore', schema: AUDIT_SCHEMA }).catch(() => null) return { it, L, escalate, assessment: a } })) const items = assessed.filter(Boolean).map(({ it, L, escalate, assessment }) => ({ id: it.id, strategy: L.assignedMode, ranAs: escalate ? 'escalate-on-failure (Opus failed, Fable ran)' : (L.finalModel === 'fable' ? 'fable-first (landed on Fable)' : 'fable-first (fell back to Opus)'), basis: escalate ? 'measured' : 'assessed', chosenBecause: L.verdict?.signals ?? [], estUsd: (assign.get(it.id) || {}).estUsd || 0, landedOn: L.finalModel, assessment: assessment ? { verdict: assessment.verdict, opusLikelySufficed: !!assessment.opusLikelySufficed, evidence: assessment.evidence } : null, })) const reserved = reservedItems.map((it) => ({ id: it.id, strategy: 'opus-first-cascade', ranAs: 'reserved - Opus succeeded, Fable never fired ($0 premium)', basis: 'reserved', estUsd: 0, assessment: null })) const degraded = degradedItems.map((it) => ({ id: it.id, strategy: 'fable-first', ranAs: 'fable-first attempted, did not land - fell back to Opus (graceful degradation); estimated Fable $ not spent as expected', basis: 'degraded', estUsd: 0, assessment: null })) const tally = (v) => items.filter((x) => x.assessment?.verdict === v).length audit = { fired: items.length, reserved: reserved.length, degraded: degraded.length, warranted: tally('warranted'), unclear: tally('unclear'), likelyUnnecessary: tally('likely-unnecessary'), estFableUsd: Math.round(fableStats.estUsd * 100) / 100, auditor: 'sonnet (disinterested; not Fable)', items: [...items, ...degraded, ...reserved], honestNote: 'Assessed by a disinterested Sonnet auditor (not Fable), able to return a negative verdict. Dollars are ESTIMATES (the runtime exposes no per-Fable token counts). No Opus counterfactual was run: escalate-on-failure items are MEASURED (Opus verifiably failed first); fable-first items are ASSESSED from the diff Fable actually landed; items that fell back to Opus are not judged as Fable work.', } log(`Fable audit: ${items.length} item(s) ran Fable - ${tally('warranted')} warranted, ${tally('unclear')} unclear, ${tally('likely-unnecessary')} likely unnecessary; ${degraded.length} fell back to Opus; ${reserved.length} reserved (never fired, $0). Est Fable ~$${audit.estFableUsd.toFixed(2)}. Auditor: disinterested Sonnet (not Fable).`) } // ---------------- Phase 6: Finalize (haiku, own worktree) ---------------- startPhase('Finalize') const FINAL_SCHEMA = { type: 'object', required: ['pushed'], properties: { pushed: { type: 'boolean' }, prUrl: { type: 'string' }, notes: { type: 'string' } }, } const mergedBranches = done.map((x) => branchOf(x.item)) const banner = integVerified ? '' : '## ⚠ VERIFY FAILED AT INTEGRATION - do not merge without fixing\n\n' const body = `${banner}## ${plan.summary} Task: ${task} ### Work items ${done.map((x) => `- ${x.item.id}: ${x.item.title} - ${x.r.summary}`).join('\n') || '- (none)'} ${droppedItems.length ? `\n### Dropped (NOT included; branches preserved for recovery)\n${droppedItems.map((d) => `- ${d.id}: ${d.why}`).join('\n')}` : ''} ### Adversarial review (${review ? reviewTier : 'SKIPPED - ' + (integVerified ? 'review agent failed' : 'build was red')}) ${(review?.findings ?? []).map((f) => `- [${f.severity}] ${f.file}: ${f.summary}${unresolved.some((u) => u.file === f.file && u.summary === f.summary) ? ' **(UNRESOLVED)**' : ' (fixed)'}`).join('\n') || 'no findings'} ${designExplanation ? `\n### System design of this change (Learn Mode)\n${designExplanation.overview}\n${(designExplanation.keyDecisions || []).map((d) => `- **${d.decision}** — tradeoff: ${d.tradeoff}${d.alternative ? `; alternative: ${d.alternative}` : ''}`).join('\n')}${designExplanation.conceptsExercised?.length ? `\nConcepts exercised: ${designExplanation.conceptsExercised.join(', ')}.` : ''}${designExplanation.interviewTopics?.length ? `\nInterview topics: ${designExplanation.interviewTopics.join(', ')}.` : ''}\n` : ''} ${INNOVATE ? `\n### Innovate mode\nFable fired ${fableStats.calls} time(s) (${fableStats.fableFirst} fable-first, ${fableStats.escalateOnFailure} escalate-on-failure); estimated Fable cost ~$${fableStats.estUsd.toFixed(2)}${fableStats.overrideUsd ? ` (of which $${fableStats.overrideUsd.toFixed(2)} human-pick override outside the budget)` : ''} (ESTIMATE; rates pinned ${PRICING.asOf}). Selector: ${SELECTOR_MODEL}${SELECTOR_MODEL === 'sonnet' ? ' (disinterested)' : ' (A/B arm)'}, Fable $ budget ${fmtFableBudget()}, plan hash ${PLAN_HASH}.${executedQuote ? ` Built from saved quote ${executedQuote.hash} (--execute).` : ''}\n` : ''}${audit ? `\n### Why Fable was used (disinterested Sonnet audit)\n${audit.fired} item(s) ran Fable: **${audit.warranted} warranted, ${audit.unclear} unclear, ${audit.likelyUnnecessary} likely unnecessary**. ${audit.reserved} reserved (Opus succeeded, $0 premium). Est ~$${audit.estFableUsd.toFixed(2)}.\n${audit.items.map((x) => `- \`${x.id}\`: ${x.ranAs}${x.assessment ? ` - **${x.assessment.verdict}** (${x.basis}): ${x.assessment.evidence}` : ''}`).join('\n')}\n\n_${audit.honestNote}_\n` : ''} 🤖 Generated with [Claude Code](https://claude.com/claude-code)` const finalizePrompt = `Publish the result from your own worktree. Steps, exactly: 1. git push -u --force-with-lease origin ${tip} (--force-with-lease only; NEVER push -f. If it is still rejected, stop and return pushed=false with notes.) ${WANT_PR ? `2. Write the PR body below to a file pr-body.md in your cwd (verbatim, do not commit it), then: gh pr create --base ${BASE} --head ${tip} --title ${JSON.stringify(`${integVerified ? '' : '⚠ '}purser: ${plan.summary.slice(0, 60)}`)} --body-file pr-body.md Then rm pr-body.md.` : '2. No PR requested - stop after the push.'} 3. Delete ONLY these fully-merged item branches (never others, never ${tip}): ${JSON.stringify(FAST ? [] : mergedBranches)} git branch -D each. Then 'git worktree prune' and note any leftover worktrees from 'git worktree list'. 4. Release the purser lock: rm -f "$(git rev-parse --path-format=absolute --git-common-dir)/purser.lock" Return pushed, prUrl if created, notes. PR BODY (between the markers, exclusive): =====BEGIN BODY===== ${body} =====END BODY=====` const runFinalizer = (label) => agent(finalizePrompt, { label, phase: 'Finalize', model: 'haiku', effort: 'low', isolation: 'worktree', schema: FINAL_SCHEMA }).catch(() => null) let finalizer = await runFinalizer('finalize') if (!finalizer) { log('⚠ finalize agent did not spawn - retrying once') finalizer = await runFinalizer('finalize:retry') } if (!finalizer) { log(`⚠⚠ finalize failed twice - run ends with the repo lock STILL HELD at ${pre.lockPath}; it goes stale after ${LOCK_STALE_MIN} min. rm that file or pass force:true to run again sooner. Branch ${tip} is intact; push/PR were NOT completed.`) } else if (finalizer.pushed === false) { log(`⚠ finalize push was rejected; the repo lock is likely still held at ${pre.lockPath} (stale after ${LOCK_STALE_MIN} min; rm that file or pass force:true to run again sooner).${finalizer.notes ? ` notes: ${finalizer.notes}` : ''}`) } return { branch: tip, pr: finalizer?.prUrl ?? null, pushed: !!finalizer?.pushed, verifyGreen: integVerified, planHash: PLAN_HASH, items: done.map((x) => ({ id: x.item.id, summary: x.r.summary })), dropped: droppedItems, reviewFindings: review?.findings ?? null, unresolvedFindings: unresolved, notes: finalizer?.notes ?? null, ...(EXPLAIN ? { explain: EXPLAIN } : {}), ...(designExplanation ? { designExplanation } : {}), ...(unappliedFlags.length ? { unappliedFlags } : {}), telemetry: { budget: { total: budget.total, spent: budget.spent(), remaining: budget.total ? budget.remaining() : null, unit: 'tokens' }, implementerStats: implStats, // Per-item join of selector verdict -> assignment -> attempts -> outcome. // Accumulated across runs, this table IS the selector-calibration eval: // escalation rates, first-attempt green rates, and estimate accuracy per // selector arm (sonnet vs opus vs fable). itemLedger: [...ledger.values()], ...(audit ? { audit } : {}), ...(INNOVATE ? { innovate: { selectorModel: SELECTOR_MODEL, fableBudget: FABLE_BUDGET === Infinity ? 'unlimited' : FABLE_BUDGET, cascadeReserve: CASCADE_RESERVE, pricingAsOf: PRICING.asOf, fable: fableStats, ...(executedQuote ? { executedQuote } : {}), tokenNote: 'per-Fable token counts are not exposed by the runtime; estUsd prices the selector token estimate against the pinned rate table' } } : {}), }, }