(opts: HeadlessOptions)
| 22 | config: QodexConfig; |
| 23 | router: ModelRouter; |
| 24 | registry: ToolRegistry; |
| 25 | permissions: PermissionEngine; |
| 26 | prompt: string; |
| 27 | json: boolean; |
| 28 | autoApproveAll?: boolean; |
| 29 | explicitModel?: string; |
| 30 | resumeSessionId?: string; |
| 31 | /** Guardrailed autonomy contract (--budget-tokens/--budget-usd/--max-wall/--scope/ |
| 32 | * --verify/--rollback-on-fail). When set: budgets override config.budget, journaled |
| 33 | * writes are scope-gated, and the run ends with enforcement (verify → |
| 34 | * rollback-on-fail → RUN REPORT). */ |
| 35 | contract?: AutonomyContract; |
| 36 | /** `--receipt <path>`: write a signed, tamper-evident JSON receipt of the run there. |
| 37 | * Requires a contract (there is nothing to attest without one). */ |
| 38 | receiptPath?: string; |
| 39 | } |
| 40 | |
| 41 | export async function runHeadless(opts: HeadlessOptions): Promise<number> { |
| 42 | const startedAt = Date.now(); |
| 43 | const store = getSessionStore(); |
| 44 | |
| 45 | // ── Autonomy contract: apply budgets BEFORE the loop is built ── |
| 46 | // Budgets ride the existing BudgetTracker (fromConfig reads config.budget), so a |
| 47 | // shallow clone with overridden limits is the whole wiring. (The write-scope gate |
| 48 | // is armed further down, right before agent.run() — see the comment there.) |
| 49 | let config = opts.config; |
| 50 | if (opts.contract) { |
| 51 | const c = opts.contract; |
| 52 | if (c.budgetTokens !== undefined || c.budgetUsd !== undefined || c.maxWallSec !== undefined) { |
| 53 | config = { |
| 54 | ...config, |
| 55 | budget: { |
| 56 | ...config.budget, |
| 57 | ...(c.budgetTokens !== undefined ? { perTaskMaxTokens: c.budgetTokens } : {}), |
| 58 | ...(c.budgetUsd !== undefined ? { perTaskLimitUsd: c.budgetUsd } : {}), |
| 59 | ...(c.maxWallSec !== undefined ? { perTaskMaxWallSeconds: c.maxWallSec } : {}), |
| 60 | }, |
| 61 | }; |
| 62 | } |
| 63 | // A USD budget on a model we cannot price would never fire: computeCost multiplies by a |
| 64 | // placeholder 0, spend stays $0.00 forever, and the run is effectively unbounded. Say so |
| 65 | // loudly rather than letting the user believe a cap is protecting them. |
| 66 | if (c.budgetUsd !== undefined) { |
| 67 | try { |
| 68 | const routed = opts.router.route('main' as any, 0, {}); |
| 69 | if (routed?.modelInfo?.pricingSource === 'unknown') { |
| 70 | process.stderr.write( |
| 71 | `⚠ --budget-usd cannot be enforced for "${routed.modelInfo.id}": no pricing is known for this model, ` + |
| 72 | `so spend always computes as $0.00. Use --budget-tokens or --max-wall instead, or set the price under ` + |
| 73 | `providers.custom[].models[].inputCostPerMillion.\n`, |
| 74 | ); |
| 75 | } |
| 76 | } catch { /* routing probe is best-effort — never block the run on it */ } |
| 77 | } |
| 78 | } |
| 79 | |
| 80 | // Resolve a leading custom slash command into its template + one-shot overrides. |
| 81 | let effectivePrompt = opts.prompt; |
no test coverage detected