@claudexor/orchestrator 1.0.1 → 2.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/dist/attemptTelemetry.d.ts +47 -2
  2. package/dist/attemptTelemetry.d.ts.map +1 -1
  3. package/dist/attemptTelemetry.js +100 -2
  4. package/dist/attemptTelemetry.js.map +1 -1
  5. package/dist/attemptUsage.d.ts +19 -0
  6. package/dist/attemptUsage.d.ts.map +1 -0
  7. package/dist/attemptUsage.js +29 -0
  8. package/dist/attemptUsage.js.map +1 -0
  9. package/dist/candidateEvidence.d.ts +22 -0
  10. package/dist/candidateEvidence.d.ts.map +1 -0
  11. package/dist/candidateEvidence.js +42 -0
  12. package/dist/candidateEvidence.js.map +1 -0
  13. package/dist/candidateOutputs.d.ts +43 -0
  14. package/dist/candidateOutputs.d.ts.map +1 -0
  15. package/dist/candidateOutputs.js +146 -0
  16. package/dist/candidateOutputs.js.map +1 -0
  17. package/dist/contract-gates.d.ts +21 -0
  18. package/dist/contract-gates.d.ts.map +1 -0
  19. package/dist/contract-gates.js +93 -0
  20. package/dist/contract-gates.js.map +1 -0
  21. package/dist/credential-profiles.d.ts +162 -0
  22. package/dist/credential-profiles.d.ts.map +1 -0
  23. package/dist/credential-profiles.js +357 -0
  24. package/dist/credential-profiles.js.map +1 -0
  25. package/dist/diffReview.d.ts +12 -1
  26. package/dist/diffReview.d.ts.map +1 -1
  27. package/dist/diffReview.js +117 -18
  28. package/dist/diffReview.js.map +1 -1
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +1 -0
  32. package/dist/index.js.map +1 -1
  33. package/dist/modelGovernance.d.ts +6 -1
  34. package/dist/modelGovernance.d.ts.map +1 -1
  35. package/dist/modelGovernance.js +8 -2
  36. package/dist/modelGovernance.js.map +1 -1
  37. package/dist/orchestrateExecutor.d.ts +43 -0
  38. package/dist/orchestrateExecutor.d.ts.map +1 -0
  39. package/dist/orchestrateExecutor.js +207 -0
  40. package/dist/orchestrateExecutor.js.map +1 -0
  41. package/dist/orchestratePlanner.d.ts.map +1 -1
  42. package/dist/orchestratePlanner.js.map +1 -1
  43. package/dist/orchestrator.d.ts +97 -85
  44. package/dist/orchestrator.d.ts.map +1 -1
  45. package/dist/orchestrator.js +1056 -1090
  46. package/dist/orchestrator.js.map +1 -1
  47. package/dist/outcomeReducer.d.ts +25 -0
  48. package/dist/outcomeReducer.d.ts.map +1 -0
  49. package/dist/outcomeReducer.js +121 -0
  50. package/dist/outcomeReducer.js.map +1 -0
  51. package/dist/policyFindings.d.ts +18 -0
  52. package/dist/policyFindings.d.ts.map +1 -0
  53. package/dist/policyFindings.js +166 -0
  54. package/dist/policyFindings.js.map +1 -0
  55. package/dist/requestRequirements.d.ts +38 -0
  56. package/dist/requestRequirements.d.ts.map +1 -0
  57. package/dist/requestRequirements.js +127 -0
  58. package/dist/requestRequirements.js.map +1 -0
  59. package/dist/reviewerPanel.d.ts +15 -1
  60. package/dist/reviewerPanel.d.ts.map +1 -1
  61. package/dist/reviewerPanel.js +85 -2
  62. package/dist/reviewerPanel.js.map +1 -1
  63. package/dist/routeContext.d.ts +37 -0
  64. package/dist/routeContext.d.ts.map +1 -0
  65. package/dist/routeContext.js +38 -0
  66. package/dist/routeContext.js.map +1 -0
  67. package/dist/runSupport.d.ts +37 -21
  68. package/dist/runSupport.d.ts.map +1 -1
  69. package/dist/runSupport.js +102 -78
  70. package/dist/runSupport.js.map +1 -1
  71. package/dist/runTelemetryWriter.d.ts +20 -0
  72. package/dist/runTelemetryWriter.d.ts.map +1 -0
  73. package/dist/runTelemetryWriter.js +68 -0
  74. package/dist/runTelemetryWriter.js.map +1 -0
  75. package/dist/runTerminals.d.ts +7 -1
  76. package/dist/runTerminals.d.ts.map +1 -1
  77. package/dist/runTerminals.js +42 -4
  78. package/dist/runTerminals.js.map +1 -1
  79. package/dist/structuredOutput.d.ts +33 -0
  80. package/dist/structuredOutput.d.ts.map +1 -0
  81. package/dist/structuredOutput.js +106 -0
  82. package/dist/structuredOutput.js.map +1 -0
  83. package/package.json +19 -18
  84. package/dist/finalVerifier.d.ts +0 -25
  85. package/dist/finalVerifier.d.ts.map +0 -1
  86. package/dist/finalVerifier.js +0 -110
  87. package/dist/finalVerifier.js.map +0 -1
@@ -1,33 +1,43 @@
1
- import { cpSync, existsSync, readFileSync, rmSync } from "node:fs";
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { observeNativeSessionEvent, preflightCredentialProfile, preflightDefaultSubject, resolveCredentialProfile, resumeSessionForProfile, rotateSpecOnTypedLimit, selectedProfileAvailability, } from "./credential-profiles.js";
3
+ import { writeRunTelemetryArtifact } from "./runTelemetryWriter.js";
4
+ import { buildFileBackedSynthesisInput, materializeWinnerOutputs, stageFileBackedContext, writeCandidateAttemptArtifacts, } from "./candidateOutputs.js";
5
+ import { processAttemptUsage } from "./attemptUsage.js";
6
+ import { toCandidateEvidence } from "./candidateEvidence.js";
7
+ import { capabilityIntents } from "@claudexor/gateway";
8
+ import { policyFindings } from "./policyFindings.js";
2
9
  import { join } from "node:path";
3
- import { HarnessRunSpec, OrchestrateContract as OrchestrateContractSchema, toolRisk, DecisionRecord as DecisionRecordSchema, WorkProduct as WorkProductSchema, SessionReboundLineage as SessionReboundLineageSchema, SpecPack as SpecPackZ, ModeKind as ModeKindSchema, ReviewFinding as ReviewFindingSchema, RunTelemetry as RunTelemetrySchema, SCHEMA_VERSION, TRUST_FULL_ACCESS_CODE, TaskContract as TaskContractSchema, isBlocking, orchestratePlanJsonSchema, } from "@claudexor/schema";
10
+ import { HarnessRunSpec, OrchestrateContract as OrchestrateContractSchema, DecisionRecord as DecisionRecordSchema, WorkProduct as WorkProductSchema, SessionReboundLineage as SessionReboundLineageSchema, SpecPack as SpecPackZ, ModeKind as ModeKindSchema, SCHEMA_VERSION, TRUST_FULL_ACCESS_CODE, FrozenTaskContractArtifact as TaskContractSchema, isBlocking, orchestratePlanJsonSchema, normalizeUserOutputSchema, strictifyOutputSchema, estimateEffectiveAuthRoute, } from "@claudexor/schema";
4
11
  import { globalConfigDir, loadConfig, trustConfigPath } from "@claudexor/config";
5
12
  import { specPackToTaskContract } from "@claudexor/interview";
6
- import { HarnessUnavailableError, summarizeDiffPaths as diffStats, validateModel, withInactivityWatchdog } from "@claudexor/core";
13
+ import { AnswerAssembly, HarnessUnavailableError, summarizeDiffPaths as diffStats, withInactivityWatchdog, } from "@claudexor/core";
7
14
  import { assertRouteModelsAllowed } from "./modelGovernance.js";
15
+ import { RequestRequirementsResolver } from "./requestRequirements.js";
8
16
  import { cancelledResult, failTerminally, guardAnnouncedRun, writeFailure, } from "./runTerminals.js";
9
- import { transientRetryDelayMs, gateProtectedPaths, promptWithProtectedPathConstraint, sleep, redactHarnessEvent, harnessEventPayload, pushUniqueText, formatFindings, renderSummary, readRunStatus, readRunPatch, observeBudgetSignals, rotateOnStall, recordCleanAttemptMetrics, buildEnvelopeSubInput, deliverPlanAnswer, envInheritance, transientRetryPolicy, reviewerTimeoutMs, harnessInactivityTimeoutMs, observeAuthSwitch, relayPriorPlansSection, } from "./runSupport.js";
10
- import { resolveExplicitReviewerPanel } from "./reviewerPanel.js";
17
+ import { assertOutputSchemaCompiles, finalizeStructuredOutput } from "./structuredOutput.js";
18
+ import { transientRetryDelayMs, promptWithProtectedPathConstraint, sleep, redactHarnessEvent, harnessEventPayload, formatFindings, renderSummary, readRunPatch, observeBudgetSignals, rotateOnStall, recordCleanAttemptMetrics, buildEnvelopeSubInput, deliverPlanAnswer, envInheritance, transientRetryPolicy, reviewerTimeoutMs, harnessInactivityTimeoutMs, observeAuthSwitch, relayPriorPlansSection, deliveryRefusalFailure, writeRaceDeliveryDecision, } from "./runSupport.js";
19
+ import { candidateStatusInRouteContext, resolveReadOnlyRouteContext, } from "./routeContext.js";
20
+ import { resolveAutoReviewerPanel, resolveExplicitReviewerPanel } from "./reviewerPanel.js";
11
21
  import { buildOrchestratePlannerPrompt, extractOrchestratePlan } from "./orchestratePlanner.js";
12
- import { blockedDecisionOverride, finalVerifyBlocks, finalVerifyPatch } from "./finalVerifier.js";
22
+ import { orchestrateFailureFor, readRunStatus } from "./outcomeReducer.js";
23
+ import { executeOrchestratePlan, } from "./orchestrateExecutor.js";
13
24
  import { runDiffReview } from "./diffReview.js";
14
- import { aggregateRunWebEvidence, attemptTelemetryRecord, createAttemptTelemetry, observeAttemptTelemetry, setAttemptOutcome, telemetrySummary, toolWarnings, unrecoveredToolErrors, webUnsatisfied, } from "./attemptTelemetry.js";
25
+ import { createAttemptTelemetry, observeAttemptTelemetry, setAttemptOutcome, telemetrySummary, toolWarnings, unrecoveredToolErrors, webUnsatisfied, } from "./attemptTelemetry.js";
15
26
  import { interactionChannelFor } from "./interaction.js";
27
+ import { gateSpecsFromContract, renderTestsEvidence, resolveContractGates, } from "./contract-gates.js";
16
28
  import { ArtifactStore } from "@claudexor/artifact-store";
17
29
  import { EventLog } from "@claudexor/event-log";
18
- import { assertMandatoryContext, buildContextPack, matchAny, preflightEvidence, writeEvidencePacket, } from "@claudexor/context";
19
- import { WorkspaceManager, applyPatchProtected, ensureGitRepository, snapshotTree, } from "@claudexor/workspace";
20
- import { deliver, validateApplyGate } from "@claudexor/delivery";
30
+ import { assertMandatoryContext, buildContextPack, rawContextForEnvelope, preflightEvidence, writeEvidencePacket, } from "@claudexor/context";
31
+ import { WorkspaceManager, captureRawPatchEnvelope, createRevertAnchorFromPatchOrNull, createRevertAnchorOrNull, ensureGitRepository, consumeRawPatchEnvelope, snapshotTree, } from "@claudexor/workspace";
32
+ import { blockedDecisionOverride, finalVerifyBlocks, finalVerifyPatch, validateApplyGate, verifyAndDeliver, } from "@claudexor/delivery";
21
33
  import { HarnessGateway } from "@claudexor/gateway";
22
34
  import { ReadinessLedger, evaluateConvergence, failureSignature, gatesPassed, reviewCandidate, revalidateFindings, runGates, } from "@claudexor/review";
23
35
  import { arbitrate } from "@claudexor/arbitration";
24
36
  import { buildSynthesisPlan, decideSynthesis } from "@claudexor/synthesis";
25
- import { BudgetLedger, loadHarnessMetrics, promptFingerprint, selectHarness, } from "@claudexor/budget";
26
- import { classifyRisk, DEFAULT_REQUIRE_HUMAN_PATHS, requireHuman, reviewDepthForRisk, } from "@claudexor/policy";
37
+ import { attemptCostEvidence, attemptUsageCostSettlement, BudgetLedger, isBudgetTerminal, loadHarnessMetrics, promptFingerprint, unknownCostSettlement, rankHarnesses, reviewUsageCostSettlement, } from "@claudexor/budget";
27
38
  import { appendLine, assertNoInlineSecretValues, containsSecretLikeToken, hashJson, newId, noProjectRepoRoot, nowIso, redactSecrets, safeInvoke, sha256, userConfigDir, writeText, } from "@claudexor/util";
28
39
  const LABELS = "ABCDEFGHIJ".split("");
29
40
  const NO_PROJECT_ROOT = noProjectRepoRoot();
30
- const REVIEW_EVIDENCE_DIRNAME = ".claudexor-review-evidence";
31
41
  /** Concurrency cap for parallel candidates/explorers (locked decision: min(n, 4)). */
32
42
  const MAX_PARALLEL_CANDIDATES = 4;
33
43
  /** Default wait for one interactive answer before a benign decline. */
@@ -66,6 +76,11 @@ async function runBounded(items, limit, work) {
66
76
  export class Orchestrator {
67
77
  deps;
68
78
  gateway;
79
+ requestRequirements = new RequestRequirementsResolver();
80
+ /** Per-attempt cap on forwarded live delta chunks (W-C4 flood guard, sol
81
+ * #10): past this the deltas are dropped and the cutoff is disclosed once;
82
+ * the complete message always still lands. */
83
+ static MAX_DELTAS_PER_ATTEMPT = 4000;
69
84
  constructor(deps) {
70
85
  this.deps = deps;
71
86
  this.gateway = new HarnessGateway(deps.registry);
@@ -83,9 +98,17 @@ export class Orchestrator {
83
98
  const resolved = this.resolveRunInput(input);
84
99
  // INV-062 at the ENGINE boundary: every surface fences prompts already,
85
100
  // but a direct embedder (or the daemon-less local REPL fallback) reaches
86
- // this entry without one. Prompts are durable artifacts — the hard block
87
- // applies here too, so no in-process path can ever bypass it.
88
- assertNoInlineSecretValues({ prompt: resolved.prompt }, "$", "run input");
101
+ // this entry without one. Prompts, per-run instructions, AND outputSchema
102
+ // are durable artifacts (all land in the TaskContract) the hard block
103
+ // applies here too, so no in-process path can ever bypass it. outputSchema
104
+ // rides the schema-aware branch: its property NAMES are field names (a
105
+ // `token` field is legitimate), but string VALUES (const/default/enum) are
106
+ // scanned for real secrets, matching the HTTP boundary exactly.
107
+ assertNoInlineSecretValues({
108
+ prompt: resolved.prompt,
109
+ instructions: resolved.instructions,
110
+ outputSchema: resolved.outputSchema ?? undefined,
111
+ }, "$", "run input");
89
112
  const parsedMode = ModeKindSchema.safeParse(resolved.mode ?? "agent");
90
113
  if (!parsedMode.success) {
91
114
  throw new Error(`unknown mode: ${String(resolved.mode)}`);
@@ -95,9 +118,39 @@ export class Orchestrator {
95
118
  // executor's plan steps — on any other mode it would be a silent no-op
96
119
  // knob. The CLI and control API validate this already; a direct embedder
97
120
  // must get the same loud refusal, not quiet acceptance.
98
- if (resolved.maxToolCalls !== undefined && resolved.maxToolCalls !== null && mode !== "orchestrate") {
121
+ if (resolved.maxToolCalls !== undefined &&
122
+ resolved.maxToolCalls !== null &&
123
+ mode !== "orchestrate") {
99
124
  throw new Error(`maxToolCalls caps the orchestrate EXECUTOR's plan steps and only applies to mode=orchestrate (got mode=${mode}); drop the knob or switch modes`);
100
125
  }
126
+ // denyPaths is enforced by the post-diff policy gate BEFORE delivery, which
127
+ // only exists on envelope/isolated runs — an in-place run mutates the live
128
+ // tree directly, so the gate could not contain a violation. Refuse loudly
129
+ // rather than accept a knob the engine cannot honor (INV-023).
130
+ if ((resolved.denyPaths?.length ?? 0) > 0 && resolved.inPlace === true) {
131
+ throw new Error("denyPaths requires an isolated/envelope run: the post-diff policy gate blocks a violating patch before delivery, which an in-place run cannot guarantee; drop --deny-path or run isolated");
132
+ }
133
+ // outputSchema constrains the run's final ANSWER. It is honored exactly
134
+ // where a final answer is delivered (agent race incl. synthesis, and ask);
135
+ // every other strategy refuses loudly rather than carrying a contract the
136
+ // engine would not validate (INV-023). The schema itself is normalized for
137
+ // the native structured-output routes here at the boundary — unsupported
138
+ // shapes ($ref, non-object root) are a typed refusal, not a mid-run 400.
139
+ if (resolved.outputSchema !== undefined && resolved.outputSchema !== null) {
140
+ if (mode !== "agent" && mode !== "ask") {
141
+ throw new Error(`outputSchema constrains the final answer and applies to agent/ask runs (got mode=${mode}); drop the schema or switch modes`);
142
+ }
143
+ if (resolved.untilClean || (resolved.attempts !== undefined && resolved.attempts !== null)) {
144
+ throw new Error("outputSchema is not supported with convergence flags (--until-clean/--attempts): convergence delivers a gated patch, not a structured answer; drop the schema or the convergence flags");
145
+ }
146
+ // Shape-refuse unsupported schemas, then PROVE it compiles under the same
147
+ // ajv the engine validator uses — a malformed schema is a preflight
148
+ // refusal here (before any run dir), never a mid-run validator crash. The
149
+ // contract keeps the ORIGINAL (conformance authority); strictify is a
150
+ // transport-only transform applied per-lane in harnessSpecKnobs.
151
+ resolved.outputSchema = normalizeUserOutputSchema(resolved.outputSchema);
152
+ assertOutputSchemaCompiles(resolved.outputSchema);
153
+ }
101
154
  // P1: a versioned `mandatory_files` contract is enforced UNIFORMLY here, for
102
155
  // every mode, so the same repo state can't pass `run`/`ask` while failing
103
156
  // `audit`. No-op when the list is empty (the default) or for no-project runs.
@@ -119,7 +172,9 @@ export class Orchestrator {
119
172
  return this.runAsk(resolved, announce);
120
173
  case "audit":
121
174
  // `--swarm` selects the bounded read-only research swarm (old `explore`).
122
- return resolved.swarm ? this.runExplore(resolved, announce) : this.runAudit(resolved, announce);
175
+ return resolved.swarm
176
+ ? this.runExplore(resolved, announce)
177
+ : this.runAudit(resolved, announce);
123
178
  case "agent":
124
179
  // Engine strategies are FLAGS on agent (v0.9 collapse): `--until-clean`
125
180
  // and `--attempts` select the convergence loop; `--n` selects the race
@@ -149,73 +204,15 @@ export class Orchestrator {
149
204
  if (this.deps.reviewerPanel && this.deps.reviewerPanel.length > 0) {
150
205
  return this.resolveExplicitReviewerPanel(cwd, this.deps.reviewerPanel, runAuthPreference);
151
206
  }
152
- const specs = [];
153
- const seen = new Set();
154
- const harnessSettings = this.config(cwd)?.global.harnesses ?? {};
155
- const reviewHome = new WorkspaceManager(cwd).readOnlyHomeEnv();
156
- try {
157
- for (const adapter of this.deps.registry.values()) {
158
- let m = null;
159
- try {
160
- m = await adapter.discover();
161
- }
162
- catch {
163
- continue;
164
- }
165
- if (!m || m.kind === "fake" || seen.has(m.provider_family))
166
- continue;
167
- // Per-harness settings gate reviewers before doctor/model probes: a disabled
168
- // harness must not spend auth/API-key readiness checks.
169
- if (harnessSettings[adapter.id]?.enabled === false)
170
- continue;
171
- const authPreference = this.authPreferenceForHarness(cwd, adapter.id, runAuthPreference);
172
- let report = null;
173
- try {
174
- report = await adapter.doctor({ cwd, env: reviewHome.env, authPreference });
175
- }
176
- catch {
177
- continue;
178
- }
179
- if (report.status !== "ok")
180
- continue; // reviewer eligibility needs scoped doctor-OK.
181
- if (!report.enabled_intents.includes("review"))
182
- continue;
183
- if (!m.capabilities.review || !m.access_profiles_supported.includes("readonly"))
184
- continue;
185
- // Explicit per-family override first, then the user's per-harness
186
- // default model: an explicit model request makes the route provable
187
- // (accepted_model_arg) on CLIs that never echo their model.
188
- const requestedModel = this.deps.reviewerModels?.[m.provider_family] ??
189
- harnessSettings[adapter.id]?.default_model ??
190
- null;
191
- // STRICT: the auto panel applies the SAME model truth gate as the
192
- // explicit panel — a doomed reviewer model is refused here, never
193
- // forwarded to die as an opaque native error mid-review.
194
- if (requestedModel) {
195
- const check = validateModel(requestedModel, typeof adapter.models === "function"
196
- ? (await adapter.models({ cwd, env: reviewHome.env, authPreference })).map((x) => x.id)
197
- : m.capabilities.known_models, typeof adapter.models === "function" ? "api" : "manifest");
198
- if (check.status !== "ok") {
199
- throw new HarnessUnavailableError(`auto-selected reviewer harness '${adapter.id}' refused model '${requestedModel}': ${check.message}; ` +
200
- `fix the reviewer model override or harnesses.${adapter.id}.default_model, or run \`claudexor models --harness ${adapter.id}\``);
201
- }
202
- }
203
- seen.add(m.provider_family);
204
- specs.push({
205
- adapter,
206
- providerFamily: m.provider_family,
207
- requestedModel,
208
- requestedEffort: this.deps.reviewerEfforts?.[m.provider_family] ?? null,
209
- authPreference,
210
- });
211
- if (specs.length >= 2)
212
- break;
213
- }
214
- }
215
- finally {
216
- reviewHome.dispose();
217
- }
218
- return specs;
207
+ return resolveAutoReviewerPanel({
208
+ cwd,
209
+ registry: this.deps.registry,
210
+ harnessSettings: this.config(cwd)?.global.harnesses ?? {},
211
+ authPreferenceFor: (id) => this.authPreferenceForHarness(cwd, id, runAuthPreference),
212
+ }, {
213
+ reviewerModels: this.deps.reviewerModels,
214
+ reviewerEfforts: this.deps.reviewerEfforts,
215
+ });
219
216
  }
220
217
  /**
221
218
  * Resolve reviewers INSIDE a strategy, after run-dir creation: an explicit
@@ -285,31 +282,6 @@ export class Orchestrator {
285
282
  candidateIntent(input) {
286
283
  return input.create === true ? "create_from_scratch" : "implement";
287
284
  }
288
- /**
289
- * Resolve the per-harness browser-tool wiring for a run spec. Returns null
290
- * (no browser) unless: the run opted in (`input.browser`), the harness has the
291
- * `browser_tool` capability, AND web policy is not `off` (the browser is live
292
- * egress and must ride `external_context_policy`). Screenshots/PDFs are written
293
- * into the run's artifact tree so they surface in the Canvas gallery. Headed by
294
- * default so the user can watch; `cdp_endpoint` is filled by the headed-Chromium
295
- * launcher (7B) for the shared, mirrored window.
296
- */
297
- browserSpecFor(input, routed, webPolicy, access, paths) {
298
- if (!input?.browser || !routed.supportsBrowser || webPolicy === "off")
299
- return null;
300
- // The browser MCP drives a real Chromium (subprocess + live network). Codex's
301
- // workspace-write sandbox cancels the navigation (live-verified across
302
- // network_access / approval_policy / external-CDP variants) — only full access
303
- // lets it through. Require full access rather than silently inject a browser
304
- // whose first navigation will fail. The composer discloses this when the user
305
- // arms the tool; a non-full run drops the browser honestly (no broken tool).
306
- // headless:false -> a real headed window is the live view (locked design:
307
- // the user watches the browser itself, not a mirrored feed); output_dir
308
- // captures navigation snapshots into the run tree.
309
- if (access !== "full" && access !== "external_sandbox_full")
310
- return null;
311
- return { output_dir: join(paths.root, "browser"), headless: false };
312
- }
313
285
  /**
314
286
  * Session fields for a route's run spec: auth route preference + native
315
287
  * resume id. Preference precedence: explicit per-run > per-harness
@@ -324,8 +296,9 @@ export class Orchestrator {
324
296
  execRootOf(input) {
325
297
  return input.executionRoot ?? input.repoRoot;
326
298
  }
327
- sessionSpecFields(input, harnessId) {
299
+ sessionSpecFields(input, harnessId, log) {
328
300
  const cfg = this.config(input.repoRoot)?.global;
301
+ const profile = this.preflightProfile(input, harnessId, log);
329
302
  const explicit = (v) => (v && v !== "auto" ? v : undefined);
330
303
  return {
331
304
  // "auto" at ANY level falls through (thread turns send the thread default
@@ -334,22 +307,49 @@ export class Orchestrator {
334
307
  explicit(cfg?.harnesses?.[harnessId]?.auth_preference) ??
335
308
  explicit(cfg?.routing?.auth_preference) ??
336
309
  "auto",
337
- resume_session_id: input.resumeSessions?.[harnessId] ?? null,
310
+ resume_session_id: resumeSessionForProfile(input.resumeSessions?.[harnessId], profile),
311
+ credential_profile: profile,
338
312
  };
339
313
  }
340
- /** Record a harness-emitted native session id for future thread resume (observer never fails the run). */
341
- observeNativeSession(input, harnessId, ev) {
342
- if (!input?.onSessionObserved || ev.type !== "started")
343
- return;
344
- const nid = ev.payload?.["native_session_id"];
345
- if (typeof nid === "string" && nid.length > 0) {
346
- try {
347
- input.onSessionObserved(harnessId, nid, ev.observed_model ?? null);
348
- }
349
- catch {
350
- /* observer errors must never fail the run */
351
- }
314
+ resolveCredentialProfile(input, harnessId) {
315
+ if (!input.credentialProfileId)
316
+ return null;
317
+ const registry = this.config(input.repoRoot)?.global.credential_profiles ?? [];
318
+ return resolveCredentialProfile(registry, input.credentialProfileId, harnessId);
319
+ }
320
+ /** The typed effective auth route for a SELECTED credential profile
321
+ * (round-18 #2): adapters execute strictly by credential_kind, so routing,
322
+ * billing classification, model truth, and quota lookup must share this
323
+ * one fact — never the default store's sources or a previous default-route
324
+ * metric. null = no profile selected or it does not resolve here. */
325
+ profileAuthRoute(input, harnessId) {
326
+ try {
327
+ const profile = this.resolveCredentialProfile(input, harnessId);
328
+ if (!profile)
329
+ return null;
330
+ return profile.credential_kind === "api_key" ? "api_key" : "local_session";
331
+ }
332
+ catch {
333
+ return null;
334
+ }
335
+ }
336
+ profilePolicy(repoRoot, harnessId) {
337
+ const policy = this.config(repoRoot)?.global.harnesses?.[harnessId]?.profile_policy;
338
+ return policy ?? { limit_action: "fail", rotation_eligible: [], headroom_threshold: 0.9 };
339
+ }
340
+ preflightProfile(input, harnessId, log) {
341
+ const profile = this.resolveCredentialProfile(input, harnessId);
342
+ const policy = this.profilePolicy(input.repoRoot, harnessId);
343
+ const registry = this.config(input.repoRoot)?.global.credential_profiles ?? [];
344
+ const snapshots = this.deps.quotaSnapshots?.() ?? [];
345
+ const emit = (type, payload) => log?.emit(type, payload);
346
+ if (!profile) {
347
+ // Unpinned runs (INV-135 auto-balance): under `rotate`, a fresh
348
+ // default-subject headroom breach starts on the next eligible
349
+ // subscription profile instead; `fail`/`ask` change nothing.
350
+ return preflightDefaultSubject({ harnessId, policy, registry, snapshots, emit });
352
351
  }
352
+ return preflightCredentialProfile({ profile, harnessId, policy, registry, snapshots, emit });
353
353
  }
354
354
  /**
355
355
  * Lift an adapter's auth-route override marker into the typed
@@ -361,7 +361,7 @@ export class Orchestrator {
361
361
  /**
362
362
  * Resolve candidate adapters: explicit `--harness`, else available real harnesses, then
363
363
  * **capability-gate** to those that can actually produce work for `intent` (e.g. a
364
- * raw-API reviewer with `implement: false` is dropped from an implement race), and
364
+ * a planner-only adapter with `implement: false` is dropped from an implement race), and
365
365
  * expand to n. Fails loudly if nothing can perform the intent.
366
366
  */
367
367
  resolveRunInput(input) {
@@ -420,16 +420,16 @@ export class Orchestrator {
420
420
  primaryHarness,
421
421
  model: undefined,
422
422
  models,
423
- portfolio: input.portfolio ??
424
- this.deps.portfolio ??
425
- cfg?.project.budget?.portfolio ??
426
- cfg?.global.default_portfolio ??
427
- "subscription-first",
423
+ routingGoal: input.routingGoal ??
424
+ this.deps.routingGoal ??
425
+ cfg?.project.budget?.routing_goal ??
426
+ cfg?.global.routing.goal ??
427
+ "auto",
428
428
  web,
429
429
  externalContextPolicy: web,
430
430
  };
431
431
  }
432
- async resolveCandidateAdapters(input, intent, ledger) {
432
+ async resolveCandidateAdapters(input, intent, ledger, routeContext) {
433
433
  let ids = input.harnesses;
434
434
  const explicitPool = Boolean(ids && ids.length > 0);
435
435
  const harnessSettings = this.config(input.repoRoot)?.global.harnesses ?? {};
@@ -442,22 +442,47 @@ export class Orchestrator {
442
442
  const statuses = probeIds.length > 0 ? await this.gateway.statusAll({ cwd: input.repoRoot }, probeIds) : [];
443
443
  const statusById = new Map(statuses.map((s) => [s.id, s]));
444
444
  if (!ids || ids.length === 0) {
445
- // Auto-pools take only doctor-OK harnesses (BIBLE §2: doctor decides
446
- // readiness; a key string or degraded route is visible but not routable).
447
- ids = statuses
448
- .filter((s) => s.manifest?.kind !== "fake" && s.status === "ok" && s.enabledIntents.includes(intent))
449
- .map((s) => s.id);
450
- if (ids.length === 0) {
451
- throw new HarnessUnavailableError("no doctor-ok harness for this mode; install/login codex/claude/cursor/opencode (see `claudexor doctor`), or pass --harness explicitly");
445
+ // INV-135 (round-18 BLOCK): an explicit credential profile NAMES its
446
+ // harness the implicit pool is exactly the profile's enabled
447
+ // harness(es) from the registry, and the profile probe (below) is the
448
+ // auth verdict. Deriving the pool from default doctor-OK status would
449
+ // exclude a valid profile whose default store is logged out while
450
+ // keeping unrelated harnesses that later fail profile resolution.
451
+ const profilePool = input.credentialProfileId
452
+ ? [
453
+ ...new Set((this.config(input.repoRoot)?.global.credential_profiles ?? [])
454
+ .filter((p) => p.enabled && p.profile_id === input.credentialProfileId)
455
+ .map((p) => p.harness_id)
456
+ .filter((hid) => !disabledHarnessIds.has(hid) && this.deps.registry.has(hid))),
457
+ ]
458
+ : [];
459
+ if (profilePool.length > 0) {
460
+ ids = profilePool;
461
+ }
462
+ else if (input.credentialProfileId) {
463
+ // Fable-checkpoint NIT: an unknown/disabled profile id must refuse
464
+ // HERE, not fall through to the default auto-pool — that would run on
465
+ // the DEFAULT credentials while the caller explicitly named an
466
+ // account, surfacing later as a per-harness "not registered" error.
467
+ const registered = (this.config(input.repoRoot)?.global.credential_profiles ?? []).filter((p) => p.profile_id === input.credentialProfileId);
468
+ throw new HarnessUnavailableError(registered.length === 0
469
+ ? `credential profile "${input.credentialProfileId}" is not registered (see \`claudexor profiles list\`)`
470
+ : registered.every((p) => !p.enabled)
471
+ ? `credential profile "${input.credentialProfileId}" is disabled`
472
+ : `credential profile "${input.credentialProfileId}" belongs to unavailable harness(es): ${registered.map((p) => p.harness_id).join(", ")}`);
473
+ }
474
+ else {
475
+ // Auto-pools take only doctor-OK harnesses (BIBLE §2: doctor decides
476
+ // readiness; a key string or degraded route is visible but not routable).
477
+ ids = statuses
478
+ .filter((s) => s.manifest?.kind !== "fake" && s.status === "ok" && s.enabledIntents.includes(intent))
479
+ .map((s) => s.id);
480
+ if (ids.length === 0) {
481
+ throw new HarnessUnavailableError("no doctor-ok harness for this mode; install/login codex/claude/cursor/opencode (see `claudexor doctor`), or pass --harness explicitly");
482
+ }
452
483
  }
453
484
  }
454
485
  const policy = input.web ?? input.externalContextPolicy ?? "auto";
455
- // Vision is a capability: a run carrying an image attachment must route to a
456
- // harness that can actually deliver it (image_input != "none"). Routing an
457
- // image to cursor/opencode (image_input="none") silently drops it and the
458
- // model honestly reports it saw nothing — the schema's attachment contract
459
- // (attachment.ts) promises the opposite. Gate the pool below, mirroring web.
460
- const needsVision = (input.attachments ?? []).some((a) => a.kind === "image");
461
486
  const pool = [];
462
487
  const dropped = [];
463
488
  for (const id of ids) {
@@ -484,26 +509,61 @@ export class Orchestrator {
484
509
  dropped.push(why);
485
510
  continue;
486
511
  }
487
- const status = statusById.get(id);
512
+ // W3.3 (TZ-1 §B): a route is admitted on readiness truth from the SAME
513
+ // resolved env/cwd its run will spawn with (see routeContext.ts).
514
+ let status = await candidateStatusInRouteContext(this.gateway, routeContext, id, this.authPreferenceForHarness(input.repoRoot, id, input.authPreference), statusById);
488
515
  const manifest = status?.manifest ?? null;
489
516
  if (!status || !manifest) {
490
517
  dropped.push(`${id} (unavailable)`);
491
518
  continue;
492
519
  }
493
- // Doctor status is the readiness truth: auto-pools take only doctor-OK
494
- // routes, and explicitly selecting an UNAVAILABLE harness fails loudly
495
- // with the doctor's reasons. A DEGRADED harness (e.g. key present but
520
+ // Doctor status is the readiness truth. A DEGRADED harness (e.g. key present but
496
521
  // unproven by isolated smoke) is admitted only by explicit user
497
522
  // selection — degraded means usable-with-caveats, and the caveats are
498
523
  // visible in doctor output and run events.
499
- if (status.status === "unavailable") {
524
+ // INV-135 (round-13, extended by the round-18 BLOCK): an EXPLICIT
525
+ // profile is authenticated by ITS store — the profile probe overrides
526
+ // the default auth verdict for ANY non-ok default status, and a
527
+ // profile-admitted route joins even an AUTO pool (the run spawns with
528
+ // the profile's transport, so the default store's state is not the
529
+ // routing truth). Capability/manifest gating above still applies.
530
+ let profileAdmitted = false;
531
+ const profileAdapter = this.deps.registry.get(id);
532
+ const profileVerdict = await selectedProfileAvailability({
533
+ registry: this.config(input.repoRoot)?.global.credential_profiles ?? [],
534
+ profileId: input.credentialProfileId,
535
+ harnessId: id,
536
+ probe: profileAdapter?.probeCredentialProfile?.bind(profileAdapter),
537
+ });
538
+ if (profileVerdict !== null) {
539
+ if (profileVerdict === "available") {
540
+ profileAdmitted = true;
541
+ // A valid profile restores manifest intent truth when the default store failed.
542
+ if (status.status !== "ok") {
543
+ status = {
544
+ ...status,
545
+ status: "degraded",
546
+ enabledIntents: capabilityIntents(manifest.capabilities),
547
+ };
548
+ statusById.set(id, status);
549
+ }
550
+ }
551
+ else {
552
+ const why = `${id} credential profile is not ready: ${profileVerdict}`;
553
+ if (explicitPool)
554
+ throw new HarnessUnavailableError(why);
555
+ dropped.push(why);
556
+ continue;
557
+ }
558
+ }
559
+ if (status.status === "unavailable" && !profileAdmitted) {
500
560
  const why = `${id} is unavailable${status.reasons.length ? `: ${status.reasons.join("; ")}` : ""}`;
501
561
  if (explicitPool)
502
562
  throw new HarnessUnavailableError(why);
503
563
  dropped.push(why);
504
564
  continue;
505
565
  }
506
- if (status.status !== "ok" && !explicitPool) {
566
+ if (status.status !== "ok" && !explicitPool && !profileAdmitted) {
507
567
  dropped.push(`${id} is ${status.status}${status.reasons.length ? `: ${status.reasons.join("; ")}` : ""}`);
508
568
  continue;
509
569
  }
@@ -512,18 +572,13 @@ export class Orchestrator {
512
572
  intent === "explain" ||
513
573
  intent === "audit" ||
514
574
  intent === "orchestrate";
515
- // Mirror buildContract: the trust-config default decides write-mode access
516
- // when the run does not request a profile explicitly.
517
- const requiredAccess = readOnlyIntent
575
+ const requiredAccess = this.requestRequirements.adapterAccess(intent, manifest.capabilities.implementation_transport, readOnlyIntent
518
576
  ? "readonly"
519
- : (input.access ?? this.config(input.repoRoot).trust.access_default);
577
+ : (input.access ?? this.config(input.repoRoot).trust.access_default));
520
578
  const accessSupported = !requiredAccess || manifest.access_profiles_supported.includes(requiredAccess);
521
579
  const webSupport = manifest.capabilities.web_policy;
522
- // The PER-ROUTE policy is what this harness will actually execute: a
523
- // per-harness `web` default upgrades a run-level `auto` (routeSpecKnobs
524
- // applies the same rule when building the spec), so the capability gate
525
- // must judge that effective policy — not admit a route whose configured
526
- // default it could never honor.
580
+ // Match routeSpecKnobs: a per-harness web default upgrades run-level auto,
581
+ // so judge the effective per-route policy.
527
582
  const routePolicy = policy === "auto" && cfgEntry?.web && cfgEntry.web !== "auto" ? cfgEntry.web : policy;
528
583
  const routeWebRequired = routePolicy === "cached" || routePolicy === "live";
529
584
  // Web policy is a capability: `off` needs an enforceable off state and a
@@ -541,34 +596,44 @@ export class Orchestrator {
541
596
  dropped.push(why);
542
597
  continue;
543
598
  }
544
- // Vision gate: an image-bearing run only routes to vision-capable harnesses.
545
- // Exclude blind ones from auto-pools; fail loud if the user explicitly chose one.
546
- if (needsVision && manifest.capability_profile.image_input === "none") {
547
- const why = `${id} cannot accept image attachments (manifest image_input=none); choose a vision-capable harness (see \`claudexor doctor\` — capability image_input) or remove the image attachment`;
599
+ const attachmentRefusal = this.requestRequirements.attachmentRefusal(id, input.attachments ?? [], manifest.capability_profile.attachment_inputs);
600
+ if (attachmentRefusal) {
548
601
  if (explicitPool)
549
- throw new HarnessUnavailableError(why);
550
- dropped.push(why);
602
+ throw new HarnessUnavailableError(attachmentRefusal);
603
+ dropped.push(attachmentRefusal);
551
604
  continue;
552
605
  }
553
606
  const reason = status.reasons.length > 0 ? `: ${status.reasons.join("; ")}` : "";
554
607
  if (status.enabledIntents.includes(intent) && accessSupported) {
555
608
  pool.push({
556
609
  adapter,
610
+ adapterAccess: requiredAccess,
557
611
  webSupport,
558
612
  providerFamily: manifest.provider_family,
559
613
  supportsMaxTurns: manifest.capabilities.max_turns,
560
614
  supportsToolLists: manifest.capabilities.tool_lists,
561
- supportsBrowser: manifest.capabilities.browser_tool,
615
+ browserRequirement: this.requestRequirements.resolveBrowser({
616
+ harnessId: id,
617
+ requested: input.browser === true,
618
+ manifestCapable: manifest.capabilities.browser_tool,
619
+ webPolicy: routePolicy,
620
+ access: requiredAccess,
621
+ }),
622
+ denyRequirement: this.requestRequirements.resolveDenyPaths(id, (input.denyPaths?.length ?? 0) > 0),
562
623
  effortLevels: manifest.capabilities.effort_levels,
563
624
  knownModels: manifest.capabilities.known_models,
625
+ // A selected profile's credential_kind IS the route (round-18 #2);
626
+ // the default store's sources apply only to profile-less runs.
627
+ authRouteEstimate: this.profileAuthRoute(input, id) ??
628
+ estimateEffectiveAuthRoute(this.authPreferenceForHarness(input.repoRoot, id, input.authPreference), status.authSources),
564
629
  supportsInteractive: manifest.capabilities.interactive,
565
630
  supportsJsonSchemaOutput: manifest.capabilities.json_schema_output,
631
+ implementationTransport: manifest.capabilities.implementation_transport,
566
632
  settings: cfgEntry
567
633
  ? {
568
634
  defaultModel: cfgEntry.default_model,
569
635
  effort: cfgEntry.effort,
570
636
  web: cfgEntry.web === "auto" ? null : cfgEntry.web,
571
- maxUsd: cfgEntry.max_usd,
572
637
  maxTurns: cfgEntry.max_turns,
573
638
  maxRounds: cfgEntry.max_rounds,
574
639
  toolsAllow: cfgEntry.tools_allow,
@@ -584,69 +649,116 @@ export class Orchestrator {
584
649
  if (pool.length === 0) {
585
650
  throw new HarnessUnavailableError(`no harness can perform '${intent}' for this mode${dropped.length ? ` (skipped: ${dropped.join(", ")})` : ""}`);
586
651
  }
587
- const ordered = this.orderPool(pool, input, statusById, ledger);
652
+ const ordered = this.orderPool(pool, input, intent, statusById, ledger);
653
+ if (ordered.length === 0) {
654
+ throw new HarnessUnavailableError(`no harness remains eligible for '${intent}' after budget and quota routing`);
655
+ }
588
656
  const n = input.n ?? ordered.length;
589
657
  const out = [];
590
658
  for (let i = 0; i < n; i++)
591
659
  out.push(ordered[i % ordered.length]);
660
+ this.requestRequirements.requireEffectiveBrowser(input.browser === true, out.map((lane) => lane.browserRequirement));
661
+ // outputSchema is MANDATORY (Quiz-6a): a selected lane that cannot
662
+ // natively constrain its final message would deliver best-effort text —
663
+ // that is a typed preflight refusal, never silent degradation. The
664
+ // interactive stream-json transport x --json-schema is an unverified
665
+ // vendor combination, so lanes that would ride it refuse too.
666
+ if (input.outputSchema !== undefined && input.outputSchema !== null) {
667
+ const incapable = out.filter((lane) => !lane.supportsJsonSchemaOutput);
668
+ if (incapable.length > 0) {
669
+ throw new HarnessUnavailableError(`outputSchema is mandatory but selected lane(s) cannot constrain output natively: ${[...new Set(incapable.map((lane) => lane.adapter.id))].join(", ")} (manifest capabilities.json_schema_output=false); choose schema-capable harnesses or drop the schema`);
670
+ }
671
+ // NOTE (DT2.1-16): the daemon ALWAYS arms an interaction channel, so an
672
+ // interactive-capable lane (claude) is refused for outputSchema on every
673
+ // daemon/CLI run today — the --json-schema x stream-json interactive combo
674
+ // is not yet live-verified. Structured-output runs therefore route through
675
+ // a non-interactive lane (codex). The message names that reality instead
676
+ // of pointing at a channel a daemon caller cannot turn off.
677
+ const interactive = Boolean(input.onInteraction)
678
+ ? out.filter((lane) => lane.supportsInteractive)
679
+ : [];
680
+ if (interactive.length > 0) {
681
+ throw new HarnessUnavailableError(`outputSchema is not yet available on interactive-transport lane(s): ${[...new Set(interactive.map((lane) => lane.adapter.id))].join(", ")} (the --json-schema x stream-json combination is unverified). Route structured-output runs through a non-interactive schema-capable harness (e.g. codex), or drop the schema`);
682
+ }
683
+ }
592
684
  // Strict pre-run model gate (INV-104) — see modelGovernance.ts.
593
685
  await assertRouteModelsAllowed(out, input.models, this.execRootOf(input));
594
686
  return out;
595
687
  }
596
688
  /**
597
- * Order the eligible pool by portfolio routing utility (budget router): an
689
+ * Order the eligible pool by the selected routing goal (budget router): an
598
690
  * explicit user pool keeps the user's order; an explicit primary harness is
599
691
  * always pinned first. Cross-family diversity is encouraged for later slots.
600
692
  */
601
- orderPool(pool, input, statusById, ledger) {
693
+ orderPool(pool, input, intent, statusById, ledger) {
602
694
  let ordered = pool;
603
- const explicitPool = Boolean(input.harnesses && input.harnesses.length > 0);
604
- if (!explicitPool && pool.length > 1) {
695
+ if (pool.length > 0) {
605
696
  const routeLedger = ledger ?? new BudgetLedger();
606
- const portfolio = input.portfolio ?? this.deps.portfolio ?? "subscription-first";
697
+ const config = this.config(input.repoRoot).global;
698
+ const goal = input.routingGoal ?? this.deps.routingGoal ?? config.routing.goal;
607
699
  const byId = new Map(pool.map((r) => [r.adapter.id, r]));
608
- // real routing metrics observed per-harness cost/latency averages
609
- // (single producer: attempt settlement) and operator-declared per-family
610
- // quality priors. Absent data rides the router's neutral defaults.
700
+ // Settled cost is evidence for economy routing, never a provider quality prior.
611
701
  const metrics = loadHarnessMetrics(globalConfigDir());
612
- const priors = this.config(input.repoRoot).global.routing.quality_priors;
613
702
  const remaining = pool.map((r) => {
614
703
  const authModes = statusById.get(r.adapter.id)?.manifest?.auth_modes ?? [];
615
704
  const metric = metrics[r.adapter.id];
705
+ // Auth mode for routing: prefer the ROUTE EVIDENCE from the
706
+ // last settled attempt (adapter-disclosed, persisted in metrics) over
707
+ // the manifest capability guess — auth_modes lists what a harness CAN
708
+ // use, not what it actually runs under.
709
+ const guessedAuthMode = authModes.includes("local_session")
710
+ ? "local_session"
711
+ : authModes.includes("api_key")
712
+ ? "api_key"
713
+ : "unknown";
714
+ // A selected profile's credential_kind decides the route outright
715
+ // (round-18 #2): an api_key profile must never inherit a
716
+ // subscription classification from the default store's metric.
717
+ const authMode = this.profileAuthRoute(input, r.adapter.id) ??
718
+ (input.authPreference === "api_key"
719
+ ? "api_key"
720
+ : input.authPreference === "subscription"
721
+ ? "local_session"
722
+ : (metric?.last_auth_mode ?? guessedAuthMode));
723
+ // The quota subject this candidate would actually run as (release
724
+ // wave round-16 #2): the resolved profile id, or null for the engine
725
+ // default — so profile A's cooldown never excludes profile B or the
726
+ // default. A profile that does not resolve for this harness routes
727
+ // as unknown (undefined) and stays conservatively any-subject.
728
+ let credentialSubjectId;
729
+ try {
730
+ credentialSubjectId =
731
+ this.resolveCredentialProfile(input, r.adapter.id)?.profile_id ?? null;
732
+ }
733
+ catch {
734
+ credentialSubjectId = undefined;
735
+ }
616
736
  return {
617
737
  harnessId: r.adapter.id,
618
- providerFamily: r.providerFamily,
619
738
  available: true,
620
- authMode: authModes.includes("local_session")
621
- ? "local_session"
622
- : authModes.includes("api_key")
623
- ? "api_key"
624
- : "unknown",
625
- qualityForIntent: priors[r.providerFamily],
626
- costPerCall: metric?.avg_cost_usd ?? undefined,
627
- latencyMs: metric?.avg_duration_ms ?? undefined,
739
+ model: input.models?.[r.adapter.id] ??
740
+ config.harnesses[r.adapter.id]?.default_model ??
741
+ undefined,
742
+ effort: input.effort ?? config.harnesses[r.adapter.id]?.effort ?? undefined,
743
+ billingKnowledge: authMode === "api_key" ? "metered" : "unknown",
744
+ incrementalCostUsd: authMode === "api_key" ? (metric?.avg_cost_usd ?? null) : null,
745
+ credentialRoute: authMode === "api_key"
746
+ ? "managed_api_key"
747
+ : authMode === "local_session"
748
+ ? "vendor_native"
749
+ : undefined,
750
+ credentialSubjectId,
628
751
  };
629
752
  });
630
- const ranked = [];
631
- while (remaining.length > 0) {
632
- const best = selectHarness(remaining, {
633
- portfolio,
634
- ledger: routeLedger,
635
- diversityAgainst: ranked.map((r) => r.providerFamily),
636
- });
637
- if (!best)
638
- break; // cooldowns/zero-utility: keep residual pool order
639
- const idx = remaining.findIndex((c) => c.harnessId === best.harnessId);
640
- remaining.splice(idx, 1);
641
- const routed = byId.get(best.harnessId);
642
- if (routed)
643
- ranked.push(routed);
644
- }
645
- for (const c of remaining) {
646
- const routed = byId.get(c.harnessId);
647
- if (routed)
648
- ranked.push(routed);
649
- }
753
+ const ranked = rankHarnesses(remaining, {
754
+ goal,
755
+ paidFallback: config.routing.paid_fallback,
756
+ intent,
757
+ qualityTiers: config.routing.quality_tiers,
758
+ ledger: routeLedger,
759
+ })
760
+ .map((candidate) => byId.get(candidate.harnessId))
761
+ .filter((candidate) => Boolean(candidate));
650
762
  ordered = ranked;
651
763
  }
652
764
  if (input.primaryHarness) {
@@ -695,22 +807,6 @@ export class Orchestrator {
695
807
  "",
696
808
  ].join("\n");
697
809
  }
698
- /**
699
- * Ledger a routed harness reserves from: harnesses with a configured
700
- * `max_usd` get a child sub-ledger (spend rolls up to the run cap), so one
701
- * harness exhausting its own budget cannot drain the whole run.
702
- */
703
- harnessLedger(map, parent, routed) {
704
- const cap = routed.settings?.maxUsd;
705
- if (!cap || cap <= 0)
706
- return parent;
707
- let child = map.get(routed.adapter.id);
708
- if (!child) {
709
- child = parent.child({ maxUsd: cap });
710
- map.set(routed.adapter.id, child);
711
- }
712
- return child;
713
- }
714
810
  /**
715
811
  * The web mode a routed harness actually executes for a requested policy.
716
812
  * Tools-permissioned web (e.g. claude) has no cached index: `cached` upgrades
@@ -762,7 +858,10 @@ export class Orchestrator {
762
858
  // turn (TurnEnqueueError.code), so surfaces key remedies on the CODE —
763
859
  // never on substring-matching this human message.
764
860
  throw Object.assign(new Error(`access profile 'full' requires allow_full_access: true in the user-level trust file for this repo ` +
765
- `(${trustConfigPath(input.repoRoot)}); enable it with \`claudexor trust --allow-full-access\` — refusing to run unsandboxed`), { code: TRUST_FULL_ACCESS_CODE });
861
+ `(${trustConfigPath(input.repoRoot)}); enable it with \`claudexor trust --allow-full-access\` — refusing to run unsandboxed`),
862
+ // Refusal semantics are born at the throw (W24): the one-time grant is
863
+ // a 403, and the daemon persists this status onto the job record.
864
+ { code: TRUST_FULL_ACCESS_CODE, status: 403 });
766
865
  }
767
866
  const externalContextPolicy = input.web ?? input.externalContextPolicy ?? "auto";
768
867
  // A frozen SpecPack's CONTENT reaches the contract (success criteria,
@@ -785,7 +884,7 @@ export class Orchestrator {
785
884
  repoRoot: input.repoRoot,
786
885
  mode,
787
886
  baseRef: input.baseRef,
788
- maxUsd: input.maxUsd,
887
+ paidBudget: input.paidBudget,
789
888
  });
790
889
  specFields = {
791
890
  success_criteria: fromSpec.success_criteria,
@@ -795,7 +894,12 @@ export class Orchestrator {
795
894
  task_graph: fromSpec.task_graph,
796
895
  constraints: fromSpec.constraints,
797
896
  };
798
- specTestCommands = fromSpec.tests.commands.map((test) => test.command);
897
+ specTestCommands = fromSpec.tests.commands.map(({ program, args, cwd, envAllowlist }) => ({
898
+ program,
899
+ args,
900
+ ...(cwd === undefined ? {} : { cwd }),
901
+ envAllowlist,
902
+ }));
799
903
  }
800
904
  catch (err) {
801
905
  // An unreadable/unfrozen spec must fail the run loudly, never silently
@@ -806,22 +910,18 @@ export class Orchestrator {
806
910
  // Deterministic gate commands come from the frozen SpecPack, explicit run
807
911
  // input, then versioned project config. Without these, gateSpecs is empty
808
912
  // and convergence is review-only; with them, convergence is test-driven.
809
- const seenCommands = new Set();
810
- const commands = [...specTestCommands, ...(input.tests ?? []), ...(cfg?.tests?.commands ?? [])]
811
- .map((c) => c.trim())
812
- .filter(Boolean)
813
- .filter((command) => {
814
- if (seenCommands.has(command))
815
- return false;
816
- seenCommands.add(command);
817
- return true;
818
- })
819
- .map((command, i) => {
820
- assertNoSecretLikeTokens(`gate command ${i + 1}`, command);
821
- return { id: `gate-${i + 1}`, command, required: true };
913
+ const resolvedGates = resolveContractGates({
914
+ repoRoot: input.repoRoot,
915
+ effectiveAccess,
916
+ config: cfg,
917
+ trustGrants: resolvedCfg.trust.test_command_grants,
918
+ specCommands: specTestCommands,
919
+ operatorCommands: input.tests ?? [],
920
+ projectCommands: cfg.tests?.commands ?? [],
822
921
  });
922
+ const commands = resolvedGates.commands;
823
923
  const protectedPaths = [...new Set(specFields.constraints?.protected_paths ?? [])];
824
- const autoProtectedPaths = [...new Set(gateProtectedPaths(commands.map((c) => c.command)))];
924
+ const autoProtectedPaths = resolvedGates.autoProtectedPaths;
825
925
  const protectedPathApprovals = [
826
926
  ...new Map([...(input.protectedPathApprovals ?? [])].map((approval) => [approval.path, approval])).values(),
827
927
  ];
@@ -832,6 +932,17 @@ export class Orchestrator {
832
932
  repo: { root: input.repoRoot, base_ref: input.baseRef ?? "HEAD", dirty_policy: "snapshot" },
833
933
  mode: { kind: mode },
834
934
  user_intent: { raw: redactSecrets(input.prompt) },
935
+ // Redacted for symmetry with user_intent.raw — a no-op on fenced input
936
+ // (the inline-secret fence already blocked any secret-like value at every
937
+ // ingress incl. this engine boundary), so task-producing lanes read back
938
+ // the real instructions via harnessSpecKnobs().
939
+ instructions: input.instructions === undefined ? undefined : redactSecrets(input.instructions),
940
+ // Already normalized/strictified at the engine boundary (run() refuses
941
+ // unsupported shapes before any run dir exists).
942
+ output_schema: input.outputSchema ?? null,
943
+ auth_preference: input.authPreference ?? "auto",
944
+ credential_profile_id: input.credentialProfileId ?? null,
945
+ max_turns: input.maxTurns ?? null,
835
946
  spec: input.specId || input.specHash || input.specPath
836
947
  ? {
837
948
  id: input.specId,
@@ -842,6 +953,7 @@ export class Orchestrator {
842
953
  ...specFields,
843
954
  constraints: {
844
955
  protected_paths: protectedPaths,
956
+ deny_paths: [...new Set(input.denyPaths ?? [])],
845
957
  auto_protected_paths: autoProtectedPaths,
846
958
  protected_path_approvals: protectedPathApprovals,
847
959
  },
@@ -866,11 +978,8 @@ export class Orchestrator {
866
978
  deny: [],
867
979
  },
868
980
  budget: {
869
- portfolio: input.portfolio ?? this.deps.portfolio ?? cfg?.budget?.portfolio ?? "subscription-first",
870
- // Run cap precedence: explicit run input > surface deps > the user's
871
- // configured global per-run default. ($/day caps were removed; the budget
872
- // priority is respecting harness-reported subscription/OAuth quota.)
873
- max_usd: this.resolveMaxUsdCap(input.maxUsd, resolvedCfg),
981
+ routing_goal: input.routingGoal ?? this.deps.routingGoal ?? cfg?.budget?.routing_goal ?? "auto",
982
+ paid_budget: this.resolvePaidBudget(input.paidBudget, resolvedCfg),
874
983
  },
875
984
  // The resolved harness-scoped model map (scalar already expanded to the
876
985
  // primary by resolveRunInput). The contract is what route spec building
@@ -878,61 +987,43 @@ export class Orchestrator {
878
987
  routing_models: input.models ?? {},
879
988
  });
880
989
  }
881
- gateSpecs(contract) {
882
- return contract.tests.commands.map((c) => ({
883
- id: c.id,
884
- command: c.command,
885
- required: c.required,
886
- }));
887
- }
888
- testsEvidence(contract, gates) {
889
- const specs = this.gateSpecs(contract);
890
- if (gates === undefined) {
891
- if (specs.length === 0)
892
- return "(no test commands configured)";
893
- return [
894
- "Configured test commands (not run yet):",
895
- ...specs.map((spec) => `- ${spec.id}${spec.required === false ? " (optional)" : ""}: ${spec.command}`),
896
- ].join("\n");
897
- }
898
- if (gates.length === 0) {
899
- if (specs.length === 0)
900
- return "(no test commands configured)";
901
- return [
902
- "Configured test commands did not produce gate results before this review:",
903
- ...specs.map((spec) => `- ${spec.id}${spec.required === false ? " (optional)" : ""}: ${spec.command}`),
904
- ].join("\n");
905
- }
906
- const required = gates.filter((gate) => gate.required);
907
- const requiredPassed = required.filter((gate) => gate.status === "passed").length;
908
- const lines = [
909
- `Gate results: required ${requiredPassed}/${required.length} passed; total ${gates.length}.`,
910
- ];
911
- const appendTail = (label, text) => {
912
- if (!text)
913
- return;
914
- lines.push(` ${label}: |`);
915
- for (const line of text.split(/\r?\n/))
916
- lines.push(` ${line}`);
917
- };
918
- for (const gate of gates) {
919
- lines.push(`- ${gate.id}${gate.required === false ? " (optional)" : ""}: ${gate.status}; exit=${gate.exit_code ?? "null"}; duration_ms=${gate.duration_ms}`);
920
- lines.push(` command: ${gate.command}`);
921
- if (gate.output_truncated)
922
- lines.push(" output_truncated: true");
923
- appendTail("stdout_tail", gate.stdout_tail);
924
- appendTail("stderr_tail", gate.stderr_tail);
925
- }
926
- return lines.join("\n");
927
- }
928
- writeTestsEvidence(evidenceDir, contract, gates) {
929
- writeText(join(evidenceDir, "TESTS.txt"), this.testsEvidence(contract, gates).trim() + "\n");
930
- }
931
990
  /**
932
991
  * Per-harness settings applied to one route's run spec (model/effort/web
933
992
  * defaults, max_turns, tool lists). Knobs the manifest does not support are
934
993
  * RETURNED as ignored reasons (disclosed by the caller), never silently sent.
935
994
  */
995
+ /**
996
+ * The HarnessRunSpec fields every TASK-PRODUCING lane shares (primary,
997
+ * candidate, planner, explorer, orchestrate-planner). Extracting the identical
998
+ * block into ONE owner means a new task-producing field lands here once —
999
+ * never forgotten in one of the HarnessRunSpec.parse sites (the multi-path
1000
+ * trap). Per-run `instructions` ride every task-producing lane but are withheld
1001
+ * from `synthesize` (a merge of existing candidates, not a fresh task
1002
+ * execution — owner Quiz-5a); reviewers and the auth smoke build their own
1003
+ * specs and never call this.
1004
+ */
1005
+ harnessSpecKnobs(contract, knobs, intent) {
1006
+ return {
1007
+ external_context_policy: knobs.webPolicy,
1008
+ tool_permission_policy: {
1009
+ web: knobs.webPolicy,
1010
+ allow: [...new Set([...contract.tool_permission_policy.allow, ...knobs.toolsAllow])],
1011
+ deny: [...new Set([...contract.tool_permission_policy.deny, ...knobs.toolsDeny])],
1012
+ },
1013
+ model_hint: knobs.model,
1014
+ effort_hint: knobs.effort,
1015
+ max_turns: knobs.maxTurns,
1016
+ ...(intent === "synthesize" ? {} : { instructions: contract.instructions }),
1017
+ // The user's answer contract rides every answer-producing lane INCLUDING
1018
+ // synthesis (its answer can become the final one); the orchestrate
1019
+ // planner owns its own plan schema instead (set at its spec site). The
1020
+ // adapter gets the vendor-STRICT transport form; the engine validator
1021
+ // keeps the ORIGINAL contract as the conformance authority.
1022
+ ...(intent !== "orchestrate" && contract.output_schema
1023
+ ? { output_schema: strictifyOutputSchema(contract.output_schema) }
1024
+ : {}),
1025
+ };
1026
+ }
936
1027
  routeSpecKnobs(routed, contract, overrideModel, effortHint) {
937
1028
  const s = routed.settings;
938
1029
  const contractPolicy = contract.external_context.policy;
@@ -940,11 +1031,13 @@ export class Orchestrator {
940
1031
  let maxTurns = null;
941
1032
  let toolsAllow = [];
942
1033
  let toolsDeny = [];
943
- if (s?.maxTurns) {
1034
+ // Run-level cap beats per-harness settings (specific beats general).
1035
+ const requestedMaxTurns = contract.max_turns ?? s?.maxTurns ?? null;
1036
+ if (requestedMaxTurns) {
944
1037
  if (routed.supportsMaxTurns)
945
- maxTurns = s.maxTurns;
1038
+ maxTurns = requestedMaxTurns;
946
1039
  else
947
- ignored.push(`max_turns=${s.maxTurns} (manifest capabilities.max_turns=false for ${routed.adapter.id})`);
1040
+ ignored.push(`max_turns=${requestedMaxTurns} (manifest capabilities.max_turns=false for ${routed.adapter.id})`);
948
1041
  }
949
1042
  if ((s?.toolsAllow.length ?? 0) > 0 || (s?.toolsDeny.length ?? 0) > 0) {
950
1043
  if (routed.supportsToolLists) {
@@ -980,44 +1073,37 @@ export class Orchestrator {
980
1073
  ignored,
981
1074
  };
982
1075
  }
983
- /** Run one candidate inside an already-created envelope. Never creates/disposes the envelope. */
984
- async runCandidateInEnvelope(routed, envelope, attemptId, label, contract, prompt, store, paths, wsm, ledger, access = "workspace_write", onHarnessEvent, signal, modelHint, effortHint, intent = "implement", log, effectiveWebMode, interaction, budgetGuard, runInput) {
1076
+ async runCandidateInEnvelope(routed, envelope, attemptId, label, contract, prompt, store, paths, wsm, ledger, access = "workspace_write", onHarnessEvent, signal, modelHint, effortHint, intent = "implement", log, effectiveWebMode, interaction, budgetGuard, runInput, streamDeltas = false, fileBackedContext) {
985
1077
  const adapter = routed.adapter;
986
1078
  const knobs = this.routeSpecKnobs(routed, contract, modelHint, effortHint);
987
- // In-place envelopes mutate the live tree under the user's native environment
988
- // (no scoped HOME), so the vendor's own session store is reachable: the turn
989
- // RESUMES the native CLI session (real continuity, like the read-only paths).
990
- // Isolated envelopes (race candidates) get a fresh scoped home where that
991
- // session id cannot exist — they run fresh, with a typed session.rebound
992
- // disclosure, never a deterministic session-not-found failure.
1079
+ // Isolated scoped-home sessions are never retained after disposal.
993
1080
  const inPlaceEnvelope = envelope.worktree_path === envelope.repo_root;
994
- const sessionFields = runInput ? this.sessionSpecFields(runInput, adapter.id) : undefined;
995
- const spec = HarnessRunSpec.parse({
1081
+ const rawContextPacket = await rawContextForEnvelope(routed.implementationTransport, envelope);
1082
+ const sessionFields = runInput ? this.sessionSpecFields(runInput, adapter.id, log) : undefined;
1083
+ let spec = HarnessRunSpec.parse({
996
1084
  session_id: newId("ses"),
997
1085
  intent,
998
1086
  prompt: promptWithProtectedPathConstraint(prompt, contract.constraints.protected_paths, contract.constraints.auto_protected_paths, contract.constraints.protected_path_approvals),
999
1087
  attachments: runInput?.attachments ?? [],
1000
- browser: this.browserSpecFor(runInput, routed, knobs.webPolicy, access, paths),
1088
+ browser: this.requestRequirements.browserSpec(routed.browserRequirement, join(paths.root, "browser")),
1001
1089
  cwd: envelope.worktree_path,
1002
- access,
1003
- external_context_policy: knobs.webPolicy,
1004
- tool_permission_policy: {
1005
- web: knobs.webPolicy,
1006
- allow: [...new Set([...contract.tool_permission_policy.allow, ...knobs.toolsAllow])],
1007
- deny: [...new Set([...contract.tool_permission_policy.deny, ...knobs.toolsDeny])],
1008
- },
1009
- model_hint: knobs.model,
1010
- effort_hint: knobs.effort,
1011
- max_turns: knobs.maxTurns,
1012
- max_usd: routed.settings?.maxUsd ?? null,
1090
+ access: routed.adapterAccess,
1091
+ ...this.harnessSpecKnobs(contract, knobs, intent),
1013
1092
  env_inheritance: envInheritance(this.config(contract.repo.root)),
1014
- ...(sessionFields ? { auth_preference: sessionFields.auth_preference } : {}),
1093
+ ...(sessionFields
1094
+ ? {
1095
+ auth_preference: sessionFields.auth_preference,
1096
+ credential_profile: sessionFields.credential_profile,
1097
+ }
1098
+ : {}),
1015
1099
  ...(inPlaceEnvelope && sessionFields?.resume_session_id
1016
1100
  ? { resume_session_id: sessionFields.resume_session_id }
1017
1101
  : {}),
1018
1102
  // Scoped harness home only for isolated envelopes; in-place runs use the
1019
1103
  // native environment so the resumed vendor session is actually reachable.
1020
1104
  ...(inPlaceEnvelope ? {} : { env: wsm.envFor(envelope) }),
1105
+ raw_context_packet: rawContextPacket,
1106
+ stream_deltas: streamDeltas,
1021
1107
  });
1022
1108
  if (!inPlaceEnvelope && runInput?.threadId && sessionFields?.resume_session_id) {
1023
1109
  log?.emit("session.rebound", SessionReboundLineageSchema.parse({
@@ -1034,15 +1120,19 @@ export class Orchestrator {
1034
1120
  const inactivityMs = harnessInactivityTimeoutMs(this.config(contract.repo.root));
1035
1121
  const attemptStartedMs = Date.now();
1036
1122
  const budgetSignalState = { quotaPressureDisclosed: false };
1123
+ const triedProfiles = new Set(); // W5.4 failover: each profile at most once
1037
1124
  let cost = 0;
1038
1125
  let costEstimated = false;
1039
1126
  let harnessErrored = false;
1127
+ // W-C4 delta flood budget (per attempt): counts forwarded delta chunks.
1128
+ let deltaCount = 0;
1129
+ let deltaCutoffDisclosed = false;
1040
1130
  const errors = [];
1041
- const messageParts = [];
1131
+ const answer = new AnswerAssembly();
1042
1132
  const retryPolicy = transientRetryPolicy(this.config(contract.repo.root));
1043
1133
  const telemetry = createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required ||
1044
1134
  knobs.webPolicy === "cached" ||
1045
- knobs.webPolicy === "live", effectiveWebMode ?? knobs.webPolicy);
1135
+ knobs.webPolicy === "live", effectiveWebMode ?? knobs.webPolicy, [routed.browserRequirement, routed.denyRequirement], knobs.model);
1046
1136
  let activeSessionId = spec.session_id;
1047
1137
  const onAbort = () => {
1048
1138
  void adapter.cancel?.(activeSessionId)?.catch(() => { });
@@ -1055,6 +1145,7 @@ export class Orchestrator {
1055
1145
  }
1056
1146
  try {
1057
1147
  for (let nativeTry = 0; !signal?.aborted; nativeTry += 1) {
1148
+ const clearFileBackedContext = stageFileBackedContext(envelope.worktree_path, fileBackedContext);
1058
1149
  const runSpec = nativeTry === 0
1059
1150
  ? spec
1060
1151
  : HarnessRunSpec.parse({ ...spec, session_id: newId("ses"), extra: { ...spec.extra } });
@@ -1070,6 +1161,8 @@ export class Orchestrator {
1070
1161
  : attemptAbort.signal;
1071
1162
  activeSessionId = runSpec.session_id;
1072
1163
  const transientStart = telemetry.transientFailures.length;
1164
+ const rateLimitStart = telemetry.rateLimits.length;
1165
+ let rawPatch = null;
1073
1166
  try {
1074
1167
  const watched = withInactivityWatchdog(adapter.run(runSpec), {
1075
1168
  timeoutMs: inactivityMs,
@@ -1084,7 +1177,31 @@ export class Orchestrator {
1084
1177
  for await (const ev of watched) {
1085
1178
  if (signal?.aborted)
1086
1179
  break;
1180
+ rawPatch = captureRawPatchEnvelope(rawContextPacket !== null, rawPatch, ev);
1181
+ if (ev.type === "patch_produced")
1182
+ continue;
1087
1183
  const safeEv = redactHarnessEvent(ev);
1184
+ // W-C4 flood guard (review sol #10): a per-character delta stream
1185
+ // would otherwise persist/SSE one journal event PER CHUNK without
1186
+ // bound. Delta messages are DISPLAY-only (the complete message
1187
+ // still follows and carries the authoritative text), so past a
1188
+ // per-attempt budget we DROP further deltas and disclose the
1189
+ // cutoff ONCE — the final answer is unaffected.
1190
+ if (safeEv.type === "message" && safeEv.payload?.["delta"] === true) {
1191
+ deltaCount += 1;
1192
+ if (deltaCount > Orchestrator.MAX_DELTAS_PER_ATTEMPT) {
1193
+ if (!deltaCutoffDisclosed) {
1194
+ deltaCutoffDisclosed = true;
1195
+ log?.emit("harness.event", {
1196
+ harness_id: adapter.id,
1197
+ attempt_id: attemptId,
1198
+ type: "status",
1199
+ title: `live delta stream capped at ${Orchestrator.MAX_DELTAS_PER_ATTEMPT} chunks; the complete message still lands`,
1200
+ });
1201
+ }
1202
+ continue; // drop this delta; never journal past the budget
1203
+ }
1204
+ }
1088
1205
  safeInvoke(onHarnessEvent, safeEv);
1089
1206
  // In-place turns run in the live tree under the native environment, so
1090
1207
  // the session they emit IS reachable for the next turn: record it. An
@@ -1092,38 +1209,35 @@ export class Orchestrator {
1092
1209
  // deletes, so observing it would poison the thread resume map with
1093
1210
  // unreachable ids — skip it there.
1094
1211
  if (inPlaceEnvelope)
1095
- this.observeNativeSession(runInput, adapter.id, safeEv);
1212
+ observeNativeSessionEvent(runInput, adapter.id, safeEv);
1096
1213
  observeAuthSwitch(log, adapter.id, attemptId, safeEv);
1097
1214
  observeAttemptTelemetry(telemetry, safeEv);
1098
1215
  // Live plan checklist: forward the adapter's typed plan
1099
1216
  // progress as a run event (LAST WINS; the UI renders the latest).
1100
1217
  if (safeEv.plan_progress) {
1101
- log?.emit("plan.progress", { attempt_id: attemptId, harness_id: adapter.id, items: safeEv.plan_progress.items });
1102
- }
1103
- if (safeEv.type === "usage" && safeEv.usage?.cost_usd) {
1104
- cost += safeEv.usage.cost_usd;
1105
- if (safeEv.usage.estimated)
1106
- costEstimated = true;
1107
- log?.emit("budget.observation", {
1108
- harness_id: adapter.id,
1218
+ log?.emit("plan.progress", {
1109
1219
  attempt_id: attemptId,
1110
- kind: "spend",
1111
- usd: safeEv.usage.cost_usd,
1112
- estimated: safeEv.usage.estimated === true,
1220
+ harness_id: adapter.id,
1221
+ items: safeEv.plan_progress.items,
1222
+ });
1223
+ }
1224
+ if (safeEv.type === "usage") {
1225
+ const usage = processAttemptUsage({
1226
+ event: safeEv,
1227
+ telemetry,
1228
+ harnessId: adapter.id,
1229
+ attemptId,
1230
+ cost,
1231
+ costEstimated,
1232
+ emit: (type, payload) => log?.emit(type, payload),
1233
+ budgetGuard,
1234
+ cancel: () => void adapter.cancel?.(runSpec.session_id)?.catch(() => { }),
1113
1235
  });
1114
- // Mid-flight cap enforcement: the guard raises this attempt's hold
1115
- // to the streamed cost; a hard tier aborts NOW instead of letting a
1116
- // streaming candidate overshoot max_usd until settlement.
1117
- if (budgetGuard?.(cost)) {
1236
+ cost = usage.cost;
1237
+ costEstimated = usage.costEstimated;
1238
+ if (usage.hardCapReached) {
1118
1239
  harnessErrored = true;
1119
1240
  errors.push("budget hard cap reached mid-attempt; stream aborted");
1120
- log?.emit("budget.observation", {
1121
- harness_id: adapter.id,
1122
- attempt_id: attemptId,
1123
- kind: "cooldown",
1124
- detail: "hard cap mid-flight abort",
1125
- });
1126
- void adapter.cancel?.(runSpec.session_id)?.catch(() => { });
1127
1241
  break;
1128
1242
  }
1129
1243
  }
@@ -1132,16 +1246,21 @@ export class Orchestrator {
1132
1246
  errors.push(redactSecrets(safeEv.error ?? safeEv.text ?? "harness emitted error"));
1133
1247
  }
1134
1248
  // Capture assistant prose so an answer-only turn (no file changes) still
1135
- // has an honest output artifact instead of an empty "succeeded".
1136
- if (safeEv.type === "message" &&
1137
- safeEv.text &&
1138
- safeEv.payload?.["auth_switched"] !== true) {
1139
- pushUniqueText(messageParts, safeEv.text);
1140
- }
1249
+ // has an honest output artifact; a TYPED final message wins verbatim.
1250
+ answer.observe(safeEv);
1141
1251
  // Observe ALL budget/quota signals (one codex usage event carries
1142
1252
  // BOTH spend and quota); pressure disclosed once per attempt.
1143
1253
  observeBudgetSignals(ledger, log, adapter.id, attemptId, safeEv, budgetSignalState);
1254
+ this.deps.quotaEventSink?.(adapter.id, safeEv);
1144
1255
  }
1256
+ if (rawContextPacket && !harnessErrored)
1257
+ await consumeRawPatchEnvelope({
1258
+ repoRoot: envelope.repo_root,
1259
+ worktreePath: envelope.worktree_path,
1260
+ baseCommitSha: envelope.base_sha ?? "HEAD",
1261
+ context: rawContextPacket,
1262
+ envelope: rawPatch,
1263
+ });
1145
1264
  }
1146
1265
  catch (err) {
1147
1266
  // A throwing adapter must not lose the cost already streamed: record the
@@ -1149,11 +1268,40 @@ export class Orchestrator {
1149
1268
  harnessErrored = true;
1150
1269
  errors.push(safeErrorMessage(err));
1151
1270
  }
1271
+ finally {
1272
+ clearFileBackedContext();
1273
+ }
1152
1274
  const transient = telemetry.transientFailures.at(-1) ?? null;
1153
1275
  const sawTransient = telemetry.transientFailures.length > transientStart;
1276
+ const sawTypedLimit = telemetry.rateLimits.length > rateLimitStart;
1154
1277
  const currentDiff = await wsm.diff(envelope);
1155
- const currentAnswer = messageParts.join("\n").trim();
1278
+ const currentAnswer = answer.text();
1156
1279
  const deliverableEmpty = currentDiff.trim().length === 0 && currentAnswer.length === 0;
1280
+ // W5.4 failover: a typed-limit hit rebuilds the spec on a NEW vendor
1281
+ // session under the next profile with provenance (vendor_limit_rejected).
1282
+ if (harnessErrored && runInput && !signal?.aborted) {
1283
+ const rotated = rotateSpecOnTypedLimit({
1284
+ spec,
1285
+ harnessId: adapter.id,
1286
+ attemptId,
1287
+ policy: this.profilePolicy(contract.repo.root, adapter.id),
1288
+ registry: this.config(contract.repo.root)?.global.credential_profiles ?? [],
1289
+ snapshots: this.deps.quotaSnapshots?.() ?? [],
1290
+ triedProfiles,
1291
+ sawTypedLimit,
1292
+ deliverableEmpty,
1293
+ lastLimit: telemetry.rateLimits.at(-1) ?? null,
1294
+ emit: (type, payload) => log?.emit(type, payload),
1295
+ newSessionId: () => newId("ses"),
1296
+ defaultRouteWasVendorNative: routed.authRouteEstimate === "local_session",
1297
+ });
1298
+ if (rotated) {
1299
+ spec = rotated;
1300
+ errors.length = 0;
1301
+ harnessErrored = false;
1302
+ continue;
1303
+ }
1304
+ }
1157
1305
  if (!harnessErrored ||
1158
1306
  !sawTransient ||
1159
1307
  !deliverableEmpty ||
@@ -1194,7 +1342,7 @@ export class Orchestrator {
1194
1342
  errors.push(`web evidence unsatisfied: ${telemetry.web.errorSummary ?? (telemetry.web.attempted ? "web tool failed without verified recovery" : "web evidence required but never attempted")}`);
1195
1343
  }
1196
1344
  const diff = await wsm.diff(envelope);
1197
- const answerText = messageParts.join("\n").trim() || undefined;
1345
+ const answerText = answer.text() || undefined;
1198
1346
  const deliverablePresent = diff.trim().length > 0 || Boolean(answerText);
1199
1347
  // Cancelled attempts skip gates entirely: the operator asked to
1200
1348
  // stop NOW; running a 600s-per-gate suite after the abort delays the ack
@@ -1202,11 +1350,14 @@ export class Orchestrator {
1202
1350
  // still land, so partial work stays inspectable.
1203
1351
  const gateSignalAborted = signal?.aborted === true;
1204
1352
  if (!gateSignalAborted) {
1205
- log?.emit("gate.started", { attempt_id: attemptId, gates: this.gateSpecs(contract).length });
1353
+ log?.emit("gate.started", {
1354
+ attempt_id: attemptId,
1355
+ gates: gateSpecsFromContract(contract).length,
1356
+ });
1206
1357
  }
1207
1358
  const gates = gateSignalAborted
1208
1359
  ? []
1209
- : await runGates(this.gateSpecs(contract), {
1360
+ : await runGates(gateSpecsFromContract(contract), {
1210
1361
  cwd: envelope.worktree_path,
1211
1362
  env: wsm.envFor(envelope),
1212
1363
  signal,
@@ -1243,34 +1394,32 @@ export class Orchestrator {
1243
1394
  // must carry it so the slot catch settles the TRUE cost, not 0.
1244
1395
  throw Object.assign(err instanceof Error ? err : new Error(String(err)), { costUsd: cost });
1245
1396
  }
1246
- store.writeText(join(attemptDir, "patch.diff"), diff);
1247
- // Routing metrics (one owner in runSupport; clean attempts only).
1248
1397
  recordCleanAttemptMetrics(globalConfigDir(), adapter.id, {
1249
1398
  costUsd: cost,
1250
1399
  streamMs: attemptStreamEndedMs - attemptStartedMs,
1251
1400
  errored,
1252
1401
  aborted: signal?.aborted === true,
1402
+ authMode: telemetry.authMode,
1253
1403
  });
1254
- const attemptDiffstat = diffStats(diff);
1255
- store.writeYaml(join(attemptDir, "attempt.yaml"), {
1256
- attempt_id: attemptId,
1257
- harness_id: adapter.id,
1258
- label,
1259
- cost_usd: cost,
1260
- cost_estimated: costEstimated,
1261
- errored,
1262
- errors: errors.slice(0, 5),
1263
- ...telemetrySummary(telemetry),
1264
- outcome: telemetry.outcome,
1265
- gates: gates.map((g) => ({ id: g.id, status: g.status })),
1266
- // Candidate-card evidence: the Candidates tab renders per-attempt
1267
- // diffstat without re-parsing patch bytes client-side.
1268
- diffstat: {
1269
- files: attemptDiffstat.paths.length,
1270
- additions: attemptDiffstat.additions,
1271
- deletions: attemptDiffstat.deletions,
1404
+ const producedFiles = writeCandidateAttemptArtifacts({
1405
+ store,
1406
+ attemptDir,
1407
+ worktreePath: envelope.worktree_path,
1408
+ diff,
1409
+ answerText,
1410
+ record: {
1411
+ attempt_id: attemptId,
1412
+ harness_id: adapter.id,
1413
+ label,
1414
+ cost_usd: cost,
1415
+ cost_estimated: costEstimated,
1416
+ errored,
1417
+ errors: errors.slice(0, 5),
1418
+ ...telemetrySummary(telemetry),
1419
+ outcome: telemetry.outcome,
1420
+ gates: gates.map((g) => ({ id: g.id, status: g.status })),
1421
+ branch: envelope.branch_name,
1272
1422
  },
1273
- branch: envelope.branch_name,
1274
1423
  });
1275
1424
  return {
1276
1425
  attemptId,
@@ -1280,6 +1429,7 @@ export class Orchestrator {
1280
1429
  answerText,
1281
1430
  reviewCwd: envelope.worktree_path,
1282
1431
  baseSha: envelope.base_sha ?? undefined,
1432
+ producedFiles,
1283
1433
  gates,
1284
1434
  cost,
1285
1435
  errored,
@@ -1288,51 +1438,6 @@ export class Orchestrator {
1288
1438
  telemetry,
1289
1439
  };
1290
1440
  }
1291
- toEvidence(run, contract, findings, finalReviewClean, reviewVerified = false) {
1292
- const passed = gatesPassed(run.gates) && !run.errored;
1293
- // Honest acceptance evidence: 0/0 when the contract has no success criteria
1294
- // (no spec). The old code fabricated a 1/1 ("AC-implicit") cover, which made
1295
- // arbitration report a vacuous "acceptance=100%" that just restated gates.
1296
- const acTotal = contract.success_criteria.length;
1297
- const acCovered = passed && contract.success_criteria.length > 0
1298
- ? contract.success_criteria.map((c) => c.id)
1299
- : [];
1300
- // Treat a harness error as a failed required gate so it cannot win arbitration.
1301
- const gates = run.errored
1302
- ? [
1303
- ...run.gates,
1304
- {
1305
- id: "harness",
1306
- command: "harness",
1307
- exit_code: 1,
1308
- status: "failed",
1309
- duration_ms: 0,
1310
- required: true,
1311
- stdout_tail: null,
1312
- stderr_tail: null,
1313
- output_truncated: false,
1314
- },
1315
- ]
1316
- : run.gates;
1317
- return {
1318
- attemptId: run.attemptId,
1319
- label: run.label,
1320
- gates,
1321
- acceptanceCovered: acCovered,
1322
- acceptanceTotal: acTotal,
1323
- findings,
1324
- // Counted from the EVIDENCE gates (including the injected harness-failure
1325
- // gate), so an errored candidate scores 0/1 — never a vacuous 0/0.
1326
- testsPassed: gates.filter((g) => g.status === "passed").length,
1327
- testsTotal: gates.length,
1328
- finalReviewClean,
1329
- reviewVerified,
1330
- toolWarningsCount: run.telemetry.outcome?.toolWarningsCount ?? toolWarnings(run.telemetry).length,
1331
- diffSize: run.diff.split("\n").length,
1332
- diffBytes: Buffer.byteLength(run.diff, "utf8"),
1333
- costUsd: run.cost,
1334
- };
1335
- }
1336
1441
  interactionChannelFor(input, log, runId, taskId, attemptId, harnessId,
1337
1442
  // REQUIRED (no default): every call site must state the routed manifest's
1338
1443
  // `interactive` capability, or a future site would silently bypass the gate.
@@ -1342,8 +1447,8 @@ export class Orchestrator {
1342
1447
  }
1343
1448
  /**
1344
1449
  * Guarantee a git boundary for write-mode runs. Non-git project folders are
1345
- * initialized in place (`.gitignore` seeded with `.claudexor/`, `git init`,
1346
- * deterministic baseline commit) and the action is announced via a
1450
+ * initialized in place (`git init`, deterministic baseline commit) without
1451
+ * creating or editing `.gitignore`, and the action is announced via a
1347
1452
  * `project.git.initialized` event. Returns the failure message when the
1348
1453
  * boundary cannot be established (the terminal failure events are already
1349
1454
  * emitted); null on success.
@@ -1406,8 +1511,17 @@ export class Orchestrator {
1406
1511
  const wsm = new WorkspaceManager(execRoot);
1407
1512
  safeInvoke(input.onRunStart, { runId, taskId, runDir: paths.root });
1408
1513
  log.emit("run.created", { mode, prompt: redactSecrets(input.prompt) });
1409
- const ledger = new BudgetLedger({ maxUsd: contract.budget.max_usd ?? null });
1410
- announce?.({ log, store, paths, runId, taskId, mode, phase: "race", spend: () => ledger.spend() });
1514
+ const ledger = this.rootLedger(input, contract, log);
1515
+ announce?.({
1516
+ log,
1517
+ store,
1518
+ paths,
1519
+ runId,
1520
+ taskId,
1521
+ mode,
1522
+ phase: "race",
1523
+ spend: () => ledger.spend(),
1524
+ });
1411
1525
  store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
1412
1526
  log.emit("task.contract.created", { task_contract_hash: hashJson(contract) });
1413
1527
  // Write modes need a git boundary for worktree isolation and honest diffs.
@@ -1448,7 +1562,7 @@ export class Orchestrator {
1448
1562
  writeEvidencePacket(reviewDir, {
1449
1563
  userIntent: redactSecrets(input.prompt),
1450
1564
  diff: "(per-candidate diffs are supplied to reviewers individually)\n",
1451
- tests: this.testsEvidence(contract),
1565
+ tests: renderTestsEvidence(contract),
1452
1566
  });
1453
1567
  let adapters;
1454
1568
  try {
@@ -1487,7 +1601,6 @@ export class Orchestrator {
1487
1601
  return reviewersOutcome.failed;
1488
1602
  const reviewers = reviewersOutcome.reviewers;
1489
1603
  const reviewVerified = this.routeVerified(reviewers);
1490
- const harnessLedgers = new Map();
1491
1604
  const reviewEnvelopes = [];
1492
1605
  const disposeReviewEnvelopes = async () => {
1493
1606
  const envelopes = reviewEnvelopes.splice(0);
@@ -1497,25 +1610,17 @@ export class Orchestrator {
1497
1610
  const candidateAccess = contract.access.effective_profile;
1498
1611
  let budgetStopped = false;
1499
1612
  let softWarned = false;
1500
- // The USER-requested race width, before any budget trimming.
1501
1613
  const requestedSingleCandidate = adapters.length === 1;
1502
1614
  const slots = [];
1503
1615
  for (let i = 0; i < adapters.length; i++) {
1504
1616
  const routed = adapters[i];
1505
1617
  const attemptId = `a${String(i + 1).padStart(2, "0")}`;
1506
- // Per-harness max_usd runs through a child ledger that rolls up to the run cap.
1507
- const lease = this.harnessLedger(harnessLedgers, ledger, routed).reserve({
1618
+ const lease = ledger.reserve({
1508
1619
  taskId,
1509
1620
  attemptId,
1510
1621
  intent: this.candidateIntent(input),
1511
1622
  harnessId: routed.adapter.id,
1512
- // wave guard: every slot AFTER the first holds the estimate
1513
- // floor at reservation, so concurrent candidates are visible to the
1514
- // breaker BEFORE any usage streams and a parallel wave cannot blow
1515
- // past the cap between settlements. The first slot holds nothing —
1516
- // a cap smaller than the floor must still run ONE candidate and stop
1517
- // on real usage, never zero.
1518
- ...(i > 0 ? { estimateUsd: this.estimateUsdFloor(input.repoRoot) } : {}),
1623
+ cost: attemptCostEvidence(routed.adapter.id, attemptId, i > 0 ? this.estimateUsdFloor(input.repoRoot) : undefined, this.routeBillingKnowledge(input, routed.adapter.id)),
1519
1624
  });
1520
1625
  log.emit("budget.lease.created", {
1521
1626
  granted: lease.granted,
@@ -1538,17 +1643,16 @@ export class Orchestrator {
1538
1643
  });
1539
1644
  }
1540
1645
  const runsBySlot = new Array(slots.length);
1541
- const slotLedger = (slot) => this.harnessLedger(harnessLedgers, ledger, slot.routed);
1542
1646
  const runSlot = async (slot, slotIdx) => {
1543
1647
  if (input.signal?.aborted) {
1544
- slotLedger(slot).cancel(slot.leaseId);
1648
+ ledger.cancel(slot.leaseId);
1545
1649
  return;
1546
1650
  }
1547
1651
  // Leases are granted upfront (before spend exists); a worker still
1548
1652
  // re-checks the circuit breaker so queued slots beyond the parallel wave
1549
1653
  // do not start after earlier candidates already blew the hard cap.
1550
- if (budgetStopped || slotLedger(slot).tier() === "hard") {
1551
- slotLedger(slot).cancel(slot.leaseId);
1654
+ if (budgetStopped || ledger.tier() === "hard") {
1655
+ ledger.cancel(slot.leaseId);
1552
1656
  log.emit("budget.lease.created", {
1553
1657
  granted: false,
1554
1658
  reason: "budget exhausted (hard cap reached)",
@@ -1563,7 +1667,7 @@ export class Orchestrator {
1563
1667
  // Soft + downgrade breaker (before the hard cap): soft = a one-time
1564
1668
  // warning; downgrade = run this attempt on the per-harness fallback_model
1565
1669
  // (cheaper) instead of hard-killing — gives fallback_model a real job.
1566
- const breakerTier = slotLedger(slot).tier();
1670
+ const breakerTier = ledger.tier();
1567
1671
  if (breakerTier === "soft" && !softWarned) {
1568
1672
  softWarned = true;
1569
1673
  log.emit("budget.observation", {
@@ -1598,28 +1702,24 @@ export class Orchestrator {
1598
1702
  baseRef: contract.repo.base_ref,
1599
1703
  dirtyPolicy: "snapshot",
1600
1704
  accessProfile: candidateAccess,
1601
- // A single-candidate turn (agent n=1) on an in-place/isolated thread
1602
- // runs directly in the execution tree so the next turn sees its work
1603
- // and the native session resumes. Race candidates (n>1) always stay in
1604
- // isolated envelopes; the winner is auto-adopted into the tree after.
1605
- // REQUESTED width decides: a budget-degraded race whose wave
1606
- // guard trimmed it to one slot still runs enveloped + adoption —
1607
- // never a silent switch to direct live-tree mutation.
1608
- inPlace: input.inPlace === true && requestedSingleCandidate,
1705
+ // Direct-workspace singletons run in place. Races and patch-envelope
1706
+ // transports stay isolated and adopt through the delivery service.
1707
+ inPlace: input.inPlace === true &&
1708
+ requestedSingleCandidate &&
1709
+ slot.routed.implementationTransport !== "git_patch_envelope",
1609
1710
  });
1610
1711
  const run = await this.runCandidateInEnvelope(slot.routed, envelope, slot.attemptId, slot.label, contract, input.prompt, store, paths, wsm, ledger, candidateAccess, (ev) => {
1611
1712
  const safeEv = redactHarnessEvent(ev);
1612
1713
  safeInvoke(input.onHarnessEvent, safeEv);
1613
1714
  log.emit("harness.event", harnessEventPayload(adapter.id, slot.attemptId, safeEv));
1614
1715
  }, input.signal, downgradeModel ?? undefined, input.effort, this.candidateIntent(input), log, effectiveWeb, this.interactionChannelFor(input, log, runId, taskId, slot.attemptId, adapter.id, slot.routed.supportsInteractive), (streamedUsd) => {
1615
- const lg = slotLedger(slot);
1616
- lg.updateHold(slot.leaseId, streamedUsd);
1617
- if (lg.tier() !== "hard")
1716
+ ledger.updateHold(slot.leaseId, streamedUsd);
1717
+ if (ledger.tier() !== "hard")
1618
1718
  return false;
1619
1719
  budgetStopped = true;
1620
1720
  return true;
1621
- }, input);
1622
- slotLedger(slot).settle(slot.leaseId, run.cost);
1721
+ }, input, requestedSingleCandidate);
1722
+ ledger.settle(slot.leaseId, attemptUsageCostSettlement(run.cost, run.costEstimated, run.attemptId, run.harnessId, run.telemetry.authMode, run.telemetry.usageCost));
1623
1723
  log.emit("harness.completed", {
1624
1724
  harness_id: adapter.id,
1625
1725
  attempt_id: slot.attemptId,
@@ -1637,8 +1737,10 @@ export class Orchestrator {
1637
1737
  // post-stream throw (e.g. the secret-token assertion) carries its
1638
1738
  // streamed spend on the error — settle the TRUE cost, never launder
1639
1739
  // real spend down to 0.
1640
- const carriedCost = typeof err?.costUsd === "number" ? err.costUsd : 0;
1641
- slotLedger(slot).settle(slot.leaseId, carriedCost);
1740
+ const carriedCost = typeof err?.costUsd === "number"
1741
+ ? err.costUsd
1742
+ : 0;
1743
+ ledger.settle(slot.leaseId, unknownCostSettlement("post-stream-error", carriedCost));
1642
1744
  const message = safeErrorMessage(err);
1643
1745
  // envelope is still undefined when wsm.create() itself threw — that is
1644
1746
  // a workspace-phase infrastructure failure, not a harness error.
@@ -1669,7 +1771,7 @@ export class Orchestrator {
1669
1771
  errored: true,
1670
1772
  costEstimated: false,
1671
1773
  errors: [message],
1672
- telemetry: createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required, effectiveWeb),
1774
+ telemetry: createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required, effectiveWeb, [slot.routed.browserRequirement, slot.routed.denyRequirement], knobs.model),
1673
1775
  infraPhase,
1674
1776
  };
1675
1777
  }
@@ -1702,10 +1804,14 @@ export class Orchestrator {
1702
1804
  }
1703
1805
  if (input.signal?.aborted) {
1704
1806
  await disposeReviewEnvelopes();
1705
- return cancelledResult(log, runId, taskId, mode, paths.root, cancelledCandidates(), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, runs.map((r) => ({ attemptId: r.attemptId, harnessId: r.harnessId, telemetry: r.telemetry })), null), ledger.spend());
1807
+ return cancelledResult(log, runId, taskId, mode, paths.root, cancelledCandidates(), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, runs.map((r) => ({
1808
+ attemptId: r.attemptId,
1809
+ harnessId: r.harnessId,
1810
+ telemetry: r.telemetry,
1811
+ })), null), ledger.spend(), input.signal, store);
1706
1812
  }
1707
1813
  if (runs.length === 0) {
1708
- const status = budgetStopped ? "exhausted" : "failed";
1814
+ const status = ledger.terminal() ?? (budgetStopped ? "exhausted" : "failed");
1709
1815
  const why = budgetStopped
1710
1816
  ? "budget exhausted before any candidate run"
1711
1817
  : "no candidates produced";
@@ -1721,7 +1827,7 @@ export class Orchestrator {
1721
1827
  store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${mode})\n\n- Status: ${status}\n- Phase: budget\n\n${why}\n`);
1722
1828
  writeFailure(store, paths, {
1723
1829
  phase: "budget",
1724
- category: status === "exhausted" ? "budget" : "internal",
1830
+ category: isBudgetTerminal(status) ? "budget" : "internal",
1725
1831
  safeMessage: why,
1726
1832
  runDir: paths.root,
1727
1833
  });
@@ -1831,7 +1937,11 @@ export class Orchestrator {
1831
1937
  await disposeReviewEnvelopes();
1832
1938
  }
1833
1939
  if (input.signal?.aborted) {
1834
- return cancelledResult(log, runId, taskId, mode, paths.root, cancelledCandidates(), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, runs.map((r) => ({ attemptId: r.attemptId, harnessId: r.harnessId, telemetry: r.telemetry })), null), ledger.spend());
1940
+ return cancelledResult(log, runId, taskId, mode, paths.root, cancelledCandidates(), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, runs.map((r) => ({
1941
+ attemptId: r.attemptId,
1942
+ harnessId: r.harnessId,
1943
+ telemetry: r.telemetry,
1944
+ })), null), ledger.spend(), input.signal, store);
1835
1945
  }
1836
1946
  // Synthesis: if worthwhile, run a synthesizer as a NEW, re-checked candidate.
1837
1947
  const synth = decideSynthesis(evidences, input.synthesis ?? "auto");
@@ -1839,22 +1949,22 @@ export class Orchestrator {
1839
1949
  log.emit("synthesis.started", { synthesize: synth.synthesize, reason: synth.reason });
1840
1950
  if (synth.synthesize && !budgetStopped) {
1841
1951
  const synthRouted = adapters[0];
1842
- // Per-harness child ledger: synthesis spend counts against the
1843
- // synthesizer harness's own cap, not only the run cap.
1844
- const synthLedger = this.harnessLedger(harnessLedgers, ledger, synthRouted);
1845
- const lease = synthLedger.reserve({
1952
+ const lease = ledger.reserve({
1846
1953
  taskId,
1847
1954
  attemptId: "synth",
1848
1955
  intent: "synthesize",
1849
1956
  harnessId: synthRouted.adapter.id,
1957
+ cost: attemptCostEvidence(synthRouted.adapter.id, "synth", undefined, this.routeBillingKnowledge(input, synthRouted.adapter.id)),
1850
1958
  });
1851
1959
  if (lease.granted) {
1852
1960
  let envelope;
1853
1961
  try {
1854
1962
  const plan = buildSynthesisPlan(evidences);
1855
- const sourceDiffs = workingRuns
1856
- .map((r) => `### ${r.label} (${r.attemptId})\n${r.diff}`)
1857
- .join("\n\n");
1963
+ const synthesisInput = buildFileBackedSynthesisInput({
1964
+ instructions: plan.instructions,
1965
+ findings: plan.fixFindings,
1966
+ candidates: workingRuns,
1967
+ });
1858
1968
  const synthAdapter = synthRouted.adapter;
1859
1969
  // Disclose against the PER-ROUTE policy (per-harness web defaults
1860
1970
  // included), exactly like the candidate slots do.
@@ -1867,20 +1977,23 @@ export class Orchestrator {
1867
1977
  dirtyPolicy: "snapshot",
1868
1978
  accessProfile: candidateAccess,
1869
1979
  });
1870
- const synthPrompt = `${plan.instructions}\n\nFindings to fix:\n${plan.fixFindings.map((f) => `- ${f}`).join("\n") || "(none)"}\n\nCandidate diffs:\n${sourceDiffs}`;
1871
- const run = await this.runCandidateInEnvelope(synthRouted, envelope, "synth", "Synthesis", contract, synthPrompt, store, paths, wsm, synthLedger, candidateAccess, (ev) => {
1980
+ const run = await this.runCandidateInEnvelope(synthRouted, envelope, "synth", "Synthesis", contract, synthesisInput.prompt, store, paths, wsm, ledger, candidateAccess, (ev) => {
1872
1981
  const safeEv = redactHarnessEvent(ev);
1873
1982
  safeInvoke(input.onHarnessEvent, safeEv);
1874
1983
  log.emit("harness.event", harnessEventPayload(synthAdapter.id, "synth", safeEv));
1875
- }, input.signal, undefined, input.effort, "synthesize", log, effectiveWeb, this.interactionChannelFor(input, log, runId, taskId, "synth", synthAdapter.id, synthRouted.supportsInteractive), undefined, input);
1876
- synthLedger.settle(lease.lease?.lease_id ?? "", run.cost);
1984
+ }, input.signal, undefined, input.effort, "synthesize", log, effectiveWeb, this.interactionChannelFor(input, log, runId, taskId, "synth", synthAdapter.id, synthRouted.supportsInteractive), undefined, input, false, synthesisInput.content);
1985
+ ledger.settle(lease.lease?.lease_id ?? "", attemptUsageCostSettlement(run.cost, run.costEstimated, run.attemptId, run.harnessId, run.telemetry.authMode, run.telemetry.usageCost));
1877
1986
  reviewEnvelopes.push(envelope);
1878
1987
  envelope = undefined;
1879
1988
  try {
1880
1989
  const synthEvidence = await this.reviewRuns([run], reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal);
1881
1990
  evidences.push(...synthEvidence);
1882
1991
  if (input.signal?.aborted) {
1883
- return cancelledResult(log, runId, taskId, mode, paths.root, cancelledCandidates(), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, runs.map((r) => ({ attemptId: r.attemptId, harnessId: r.harnessId, telemetry: r.telemetry })), null), ledger.spend());
1992
+ return cancelledResult(log, runId, taskId, mode, paths.root, cancelledCandidates(), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, runs.map((r) => ({
1993
+ attemptId: r.attemptId,
1994
+ harnessId: r.harnessId,
1995
+ telemetry: r.telemetry,
1996
+ })), null), ledger.spend(), input.signal, store);
1884
1997
  }
1885
1998
  }
1886
1999
  finally {
@@ -1890,7 +2003,7 @@ export class Orchestrator {
1890
2003
  workingRuns.push(run);
1891
2004
  }
1892
2005
  catch (err) {
1893
- synthLedger.settle(lease.lease?.lease_id ?? "", 0);
2006
+ ledger.settle(lease.lease?.lease_id ?? "", unknownCostSettlement("synthesis-error"));
1894
2007
  log.emit("harness.completed", {
1895
2008
  attempt_id: "synth",
1896
2009
  status: "failed",
@@ -1904,7 +2017,11 @@ export class Orchestrator {
1904
2017
  }
1905
2018
  }
1906
2019
  if (input.signal?.aborted) {
1907
- return cancelledResult(log, runId, taskId, mode, paths.root, cancelledCandidates(), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, runs.map((r) => ({ attemptId: r.attemptId, harnessId: r.harnessId, telemetry: r.telemetry })), null), ledger.spend());
2020
+ return cancelledResult(log, runId, taskId, mode, paths.root, cancelledCandidates(), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, runs.map((r) => ({
2021
+ attemptId: r.attemptId,
2022
+ harnessId: r.harnessId,
2023
+ telemetry: r.telemetry,
2024
+ })), null), ledger.spend(), input.signal, store);
1908
2025
  }
1909
2026
  let result;
1910
2027
  try {
@@ -1933,41 +2050,48 @@ export class Orchestrator {
1933
2050
  ? (evidences.find((e) => e.attemptId === winnerRun.attemptId)?.reviewVerified ?? false)
1934
2051
  : evidences.length > 0 && evidences.every((e) => e.reviewVerified);
1935
2052
  let status = needsHuman && result.decision.status !== "success" ? "blocked" : result.decision.status;
1936
- // FinalVerifier (INV-115): an otherwise-adoptable winner with a patch
1937
- // must ALSO apply cleanly onto a fresh tree at its own base and pass the
1938
- // deterministic gates there, BEFORE adoption/apply eligibility. A failure
1939
- // BLOCKS the run with a typed reason instead of shipping it.
2053
+ if (status === "success" && ledger.terminal() !== null)
2054
+ status = ledger.terminal();
2055
+ // FinalVerifier blocks adoption until the patch and gates pass on a fresh base.
1940
2056
  let finalVerify = null;
1941
2057
  let finalVerifyFailed = false;
1942
- // IN-PLACE turns are explicitly EXEMPT (not merely base-less): a thread
1943
- // turn's snapshot base_sha IS recorded, but its diff was produced against
1944
- // the LIVE tree a fresh snapshot worktree lacks gitignored deps
1945
- // (node_modules etc.), so gates there would false-block green turns. The
1946
- // verifier's contract is isolated-envelope patches only.
1947
- const inPlaceWinner = input.inPlace === true && requestedSingleCandidate;
2058
+ let deliveryFailureReason = null;
2059
+ let raceDeliveryReceipt = null;
2060
+ // A single in-place turn already mutated its execution tree; race adoption
2061
+ // instead defers verification until immediately before delivery.
2062
+ const inPlaceWinner = winnerRun?.reviewCwd === execRoot;
2063
+ const deferredRaceVerify = input.inPlace === true && !inPlaceWinner;
1948
2064
  if (winnerRun &&
1949
2065
  !inPlaceWinner &&
2066
+ !deferredRaceVerify &&
1950
2067
  winnerRun.diff.trim().length > 0 &&
1951
2068
  (status === "success" || status === "ungated") &&
1952
2069
  !input.signal?.aborted) {
1953
- finalVerify = await finalVerifyPatch(execRoot, winnerRun, this.gateSpecs(contract), log);
1954
- // FAIL CLOSED (INV-115): verify errors block like proven failures
1955
- // shared verdict owner (finalVerifyBlocks). accept_risk stays available.
2070
+ finalVerify = await finalVerifyPatch(execRoot, winnerRun, gateSpecsFromContract(contract), log);
2071
+ // Verify errors block like proven failures; accept_risk stays available.
1956
2072
  finalVerifyFailed = finalVerifyBlocks(finalVerify);
1957
2073
  if (finalVerifyFailed)
1958
2074
  status = "blocked";
1959
2075
  }
1960
2076
  store.writeYaml(join(paths.arbitrationDir, "decision.yaml"), {
1961
2077
  ...result.decision,
1962
- // Shared honesty owner: a blocked terminal overrides the persisted
1963
- // decision (status/outcome/human_review + evidence fact).
1964
- ...(status === "blocked" ? blockedDecisionOverride(result.decision.evidence_facts, finalVerify) : {}),
2078
+ // A blocked terminal overrides the persisted green arbitration fields.
2079
+ ...(status === "blocked"
2080
+ ? blockedDecisionOverride(result.decision.evidence_facts, finalVerify)
2081
+ : {}),
1965
2082
  review_verified: actualReviewVerified,
1966
2083
  final_verify: finalVerify,
1967
2084
  });
1968
2085
  store.writeYaml(join(paths.arbitrationDir, "pairwise.yaml"), result.pairwise);
1969
2086
  const decisionPath = join(paths.arbitrationDir, "decision.yaml");
1970
2087
  if (winnerRun) {
2088
+ for (const path of materializeWinnerOutputs({
2089
+ attemptDir: join(paths.attemptsDir, winnerRun.attemptId),
2090
+ runRoot: paths.root,
2091
+ paths: winnerRun.producedFiles ?? [],
2092
+ })) {
2093
+ log.emit("output.ready", { kind: "artifact", path });
2094
+ }
1971
2095
  assertNoSecretLikeTokens("final patch diff", winnerRun.diff);
1972
2096
  const patchSha256 = sha256(winnerRun.diff);
1973
2097
  store.writeText(join(paths.finalDir, "patch.diff"), winnerRun.diff);
@@ -1977,45 +2101,55 @@ export class Orchestrator {
1977
2101
  const blockers = winnerEvidence
1978
2102
  ? winnerEvidence.findings.filter((f) => isBlocking(f)).length
1979
2103
  : 0;
1980
- // An empty-diff winner that produced prose is an ANSWER (the chat shows it
1981
- // and the honest result_kind is "answer", not a misleading "patch").
2104
+ // Prose from an empty-diff winner is an answer, never a patch.
1982
2105
  const winnerAnswer = winnerRun.answerText?.trim() ?? "";
1983
2106
  const resultKind = hasDiff ? "patch" : winnerAnswer.length > 0 ? "answer" : "none";
1984
- if (!hasDiff && winnerAnswer.length > 0) {
2107
+ // The winner's final MESSAGE is the human-facing answer and materializes
2108
+ // for diff-ful runs too: the chat renders final/answer.md (the projection
2109
+ // prefers it), never the arbitration summary — "Run … Winner: a01 …" is
2110
+ // machine telemetry, not what the agent said. The diff stays in the
2111
+ // Diff tab; summary.md remains a diagnostics artifact.
2112
+ if (winnerAnswer.length > 0) {
1985
2113
  store.writeText(join(paths.finalDir, "answer.md"), winnerAnswer + "\n");
1986
2114
  }
1987
- // a single-candidate in-place turn already mutated the live tree (its
1988
- // diff IS the live change). A race (n>1) ran candidates in isolated
1989
- // envelopes, so the winner's patch must be ADOPTED into the live tree for
1990
- // the next turn to see it. Blockers / non-success stop adoption; a failed
1991
- // apply (the user edited the tree mid-race) is disclosed, never lost.
1992
- // A clean terminal to adopt is success OR ungated (review passed but no
1993
- // test gates were configured to certify it) — never blocked/failed/no_op.
1994
- // Adoption is HONEST: `adopted` reflects whether the live in-place tree was
1995
- // actually mutated, DECOUPLED from a clean review. A single-candidate
1996
- // in-place turn edits the live tree directly — so it is "applied" even when
1997
- // review is blocked (applyState = applied_review_blocked + Revert offered).
1998
- // A race (n>1) ran candidates in isolated envelopes; its winner mutates the
1999
- // live tree only when we apply it, which we gate on a clean terminal.
2000
- const adoptable = status === "success" || status === "ungated";
2115
+ // The run's structured-output contract: ONE engine validator, called on
2116
+ // the winner's answer regardless of diff presence (a non-conformant
2117
+ // answer stays success-with-warnings; the receipt is the truth).
2118
+ if (contract.output_schema) {
2119
+ finalizeStructuredOutput({
2120
+ store,
2121
+ finalDir: paths.finalDir,
2122
+ log,
2123
+ schema: contract.output_schema,
2124
+ answerText: winnerAnswer,
2125
+ });
2126
+ }
2127
+ // Only a fully verified success may auto-adopt; ungated remains an artifact.
2128
+ const adoptable = status === "success";
2001
2129
  let adopted = null;
2002
2130
  let applyState = "not_applied";
2003
2131
  let postTurnSha = null;
2132
+ let revertAnchorId = null;
2004
2133
  if (input.inPlace === true && hasDiff) {
2005
- if (requestedSingleCandidate) {
2134
+ if (inPlaceWinner) {
2006
2135
  // Already live: the candidate ran in-place and wrote the tree itself.
2007
2136
  adopted = true;
2008
2137
  applyState = adoptable ? "applied" : "applied_review_blocked";
2009
- // Fence taken right after the candidate finished (pre-review), so user
2010
- // edits made during review/arbitration are not folded into the target.
2138
+ // The pre-review fence excludes later user edits from the target.
2011
2139
  postTurnSha = earlyPostTurnSha;
2012
2140
  }
2013
2141
  else if (adoptable) {
2014
- // Protected path: --check first, restore on 3way failure
2015
- // adopted:false MUST mean the live tree is byte-identical (INV-114);
2016
- // a failed restore is disclosed as tree_mutated, never hidden.
2017
- const applied = await applyPatchProtected(execRoot, winnerRun.diff);
2018
- if (applied.ok) {
2142
+ // Protected apply preserves the live tree or reports tree_mutated.
2143
+ const applied = await verifyAndDeliver(execRoot, winnerRun.diff, { mode: "apply", protectedApply: true }, gateSpecsFromContract(contract), (freshVerify) => {
2144
+ finalVerify = freshVerify;
2145
+ return finalVerifyBlocks(freshVerify)
2146
+ ? (freshVerify.reason ?? "final verify failed before race adoption")
2147
+ : null;
2148
+ }, log);
2149
+ raceDeliveryReceipt = applied;
2150
+ store.writeYaml(join(paths.finalDir, "delivery_receipt.yaml"), applied);
2151
+ finalVerify = applied.finalVerify;
2152
+ if (applied.applied) {
2019
2153
  adopted = true;
2020
2154
  applyState = "applied";
2021
2155
  log.emit("work_product.adopted", {
@@ -2023,17 +2157,21 @@ export class Orchestrator {
2023
2157
  patch_sha256: patchSha256,
2024
2158
  winner: winnerRun.attemptId,
2025
2159
  });
2026
- // Race winner: snapshot immediately after applying (minimal window).
2027
2160
  try {
2028
2161
  postTurnSha = await snapshotTree(execRoot);
2029
2162
  }
2030
2163
  catch {
2031
2164
  postTurnSha = null;
2032
2165
  }
2166
+ revertAnchorId = createRevertAnchorFromPatchOrNull(execRoot, winnerRun.diff);
2033
2167
  }
2034
2168
  else {
2035
2169
  adopted = false;
2036
2170
  applyState = "not_applied";
2171
+ deliveryFailureReason = applied.detail ?? "race adoption delivery was refused";
2172
+ status = "blocked";
2173
+ if (finalVerifyBlocks(finalVerify))
2174
+ finalVerifyFailed = true;
2037
2175
  log.emit("work_product.adopted", {
2038
2176
  applied: false,
2039
2177
  patch_sha256: patchSha256,
@@ -2043,18 +2181,30 @@ export class Orchestrator {
2043
2181
  }
2044
2182
  }
2045
2183
  }
2184
+ writeRaceDeliveryDecision(store, decisionPath, {
2185
+ decision: result.decision,
2186
+ status,
2187
+ reviewVerified: actualReviewVerified,
2188
+ finalVerify,
2189
+ deliveryFailureReason,
2190
+ deliveryReceiptPath: raceDeliveryReceipt ? "final/delivery_receipt.yaml" : null,
2191
+ });
2192
+ if (inPlaceWinner && requestedSingleCandidate && adopted === true) {
2193
+ revertAnchorId = await createRevertAnchorOrNull(execRoot, preTurnSha, postTurnSha);
2194
+ }
2046
2195
  store.writeYaml(join(paths.finalDir, "work_product.yaml"), {
2047
2196
  id: newId("wp"),
2048
2197
  kind: input.create === true ? "new_repo" : "patch",
2049
2198
  source_task_id: taskId,
2050
2199
  producer_attempt_id: winnerRun.attemptId,
2200
+ ...(raceDeliveryReceipt
2201
+ ? { files: { delivery_receipt: "final/delivery_receipt.yaml" } }
2202
+ : {}),
2051
2203
  meta: {
2052
2204
  harness_id: winnerRun.harnessId,
2053
2205
  synthesis: synth,
2054
2206
  mode,
2055
- // Terminal run status rides the artifact so the artifact-only CLI
2056
- // apply path enforces the same state bar as the daemon gate (a
2057
- // blocked race must read as blocked from the run dir alone).
2207
+ // Artifact-only apply reads the same terminal status as the daemon.
2058
2208
  status,
2059
2209
  review_verified: actualReviewVerified,
2060
2210
  budget_stopped: budgetStopped,
@@ -2070,6 +2220,7 @@ export class Orchestrator {
2070
2220
  apply_state: applyState,
2071
2221
  pre_turn_sha: preTurnSha,
2072
2222
  post_turn_sha: postTurnSha,
2223
+ revert_anchor_id: revertAnchorId,
2073
2224
  },
2074
2225
  });
2075
2226
  store.writeText(join(paths.finalDir, "summary.md"), renderSummary(runId, mode, { ...result.decision, status }, evidences, synth.reason, actualReviewVerified));
@@ -2077,14 +2228,17 @@ export class Orchestrator {
2077
2228
  log.emit("output.ready", {
2078
2229
  kind: "summary",
2079
2230
  path: "final/summary.md",
2080
- ...(status === "success" ? {} : { state: "diagnostic" }),
2231
+ state: status === "success" || winnerAnswer.length > 0 ? "ready" : "diagnostic",
2081
2232
  });
2082
2233
  }
2083
2234
  this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, runs.map((r) => ({ attemptId: r.attemptId, harnessId: r.harnessId, telemetry: r.telemetry })), result.decision.status === "success"
2084
2235
  ? result.decision.winner
2085
2236
  : (winnerRun?.attemptId ?? null));
2086
2237
  const honestTerminal = status === "no_op" || status === "ungated" || status === "review_not_run";
2087
- if (finalVerifyFailed) {
2238
+ if (deliveryFailureReason && !finalVerifyFailed) {
2239
+ writeFailure(store, paths, deliveryRefusalFailure(deliveryFailureReason, paths.root));
2240
+ }
2241
+ else if (finalVerifyFailed) {
2088
2242
  writeFailure(store, paths, {
2089
2243
  phase: "verification",
2090
2244
  // RunFailure.category is a closed enum; "validation" is the honest
@@ -2112,7 +2266,7 @@ export class Orchestrator {
2112
2266
  ? "policy"
2113
2267
  : winnerRun?.errored
2114
2268
  ? "harness_error"
2115
- : status === "exhausted"
2269
+ : isBudgetTerminal(status)
2116
2270
  ? "budget"
2117
2271
  : "internal",
2118
2272
  harnessId: winnerRun?.errored ? winnerRun.harnessId : undefined,
@@ -2154,7 +2308,11 @@ export class Orchestrator {
2154
2308
  // phase "verification", not "review").
2155
2309
  log.emit("run.blocked", {
2156
2310
  status,
2157
- phase: finalVerifyFailed ? "verification" : "review",
2311
+ phase: deliveryFailureReason && !finalVerifyFailed
2312
+ ? "delivery"
2313
+ : finalVerifyFailed
2314
+ ? "verification"
2315
+ : "review",
2158
2316
  failure_ref: "final/failure.yaml",
2159
2317
  });
2160
2318
  }
@@ -2181,156 +2339,27 @@ export class Orchestrator {
2181
2339
  }
2182
2340
  /** Single-owner telemetry artifact (final/telemetry.yaml); surfaces project it, never recompute. */
2183
2341
  writeRunTelemetry(store, paths, contract, runId, taskId, mode, attempts, finalAttemptId) {
2184
- const records = attempts.map((a) => attemptTelemetryRecord(a.attemptId, a.harnessId, a.telemetry));
2185
- const finalRecord = finalAttemptId
2186
- ? records.find((r) => r.attempt_id === finalAttemptId)
2187
- : undefined;
2188
- const runWeb = finalRecord?.web ?? aggregateRunWebEvidence(records, contract);
2189
- const telemetry = RunTelemetrySchema.parse({
2190
- schema_version: SCHEMA_VERSION,
2191
- run_id: runId,
2192
- task_id: taskId,
2342
+ writeRunTelemetryArtifact({
2343
+ store,
2344
+ finalDir: paths.finalDir,
2345
+ contract,
2346
+ runId,
2347
+ taskId,
2193
2348
  mode,
2194
- requested_access: contract.access.requested_profile,
2195
- effective_access: contract.access.effective_profile,
2196
- external_context_policy: contract.external_context.policy,
2197
- effective_web_mode: finalRecord?.web.effective_mode ?? contract.external_context.effective_mode,
2198
- web_required: contract.external_context.web_required,
2199
- final_attempt_id: finalAttemptId,
2200
- web: runWeb,
2201
- attempts: records,
2202
- tool_warnings_total: records.reduce((sum, r) => sum + r.outcome.tool_warnings_count, 0),
2203
- generated_at: nowIso(),
2349
+ attempts,
2350
+ finalAttemptId,
2351
+ resolveAuthPreference: (harnessId) => this.authPreferenceForHarness(contract.repo.root, harnessId, contract.auth_preference),
2204
2352
  });
2205
- store.writeYaml(join(paths.finalDir, "telemetry.yaml"), telemetry);
2206
2353
  }
2207
2354
  /** Review a set of runs and return their evidence (with finalReviewClean + review_verified caveat). */
2208
- /**
2209
- * Deterministic policy findings from the typed diff (no LLM, no regex over
2210
- * prose): protected-path changes and critical-risk diffs escalate NEEDS_HUMAN;
2211
- * a high-risk diff without a cross-family panel escalates as well. Each
2212
- * finding cites the matched files as evidence (BIBLE: evidence beats summaries).
2213
- */
2214
- policyFindings(run, reviewVerified, protectedPaths = [], autoProtectedPaths = [], protectedPathApprovals = []) {
2215
- const stats = diffStats(run.diff);
2216
- const approvalPatterns = protectedPathApprovals.map((approval) => approval.path);
2217
- const unapprovedExistingAutoProtectedPaths = stats.existingPaths.filter((path) => !matchAny(path, approvalPatterns));
2218
- const specProtectedOnly = requireHuman(stats.existingPaths, protectedPaths);
2219
- const autoProtectedOnly = requireHuman(unapprovedExistingAutoProtectedPaths, autoProtectedPaths);
2220
- const protectedOnly = {
2221
- required: specProtectedOnly.required || autoProtectedOnly.required,
2222
- reasons: [...new Set([...specProtectedOnly.reasons, ...autoProtectedOnly.reasons])],
2223
- matchedPaths: [
2224
- ...new Set([...specProtectedOnly.matchedPaths, ...autoProtectedOnly.matchedPaths]),
2225
- ],
2226
- };
2227
- const risk = classifyRisk({
2228
- changedPaths: stats.paths,
2229
- additions: stats.additions,
2230
- deletions: stats.deletions,
2231
- protectedPaths: protectedOnly.matchedPaths,
2232
- });
2233
- const findings = [];
2234
- const reviewer = {
2235
- harness_id: "policy",
2236
- requested_model: null,
2237
- requested_effort: null,
2238
- observed_model: null,
2239
- route_proof_status: "verified",
2240
- };
2241
- const evidenceFor = (reasons) => ({
2242
- files: stats.paths
2243
- .filter((p) => reasons.some((r) => r.includes(p)))
2244
- .map((path) => ({ path, lines: null })),
2245
- });
2246
- // Structured matched-path evidence (never reconstructed from prose).
2247
- const evidenceFromPaths = (paths) => ({
2248
- files: paths.map((path) => ({ path, lines: null })),
2249
- });
2250
- const reportedRisk = protectedOnly.required
2251
- ? {
2252
- level: "critical",
2253
- reasons: [...new Set([...risk.reasons, ...protectedOnly.reasons])],
2254
- matchedPaths: [...new Set([...risk.matchedPaths, ...protectedOnly.matchedPaths])],
2255
- }
2256
- : risk;
2257
- if (protectedOnly.required) {
2258
- findings.push(ReviewFindingSchema.parse({
2259
- id: newId("find"),
2260
- severity: "BLOCK",
2261
- category: "test_gap",
2262
- claim: `candidate changed protected path(s): ${protectedOnly.matchedPaths.join(", ")}`,
2263
- evidence: evidenceFromPaths(protectedOnly.matchedPaths),
2264
- reviewer,
2265
- status: "accepted",
2266
- }));
2267
- }
2268
- // Contract protected_paths escalate the human gate only for tampering with
2269
- // existing protected files. Creating a new test/package file for create or
2270
- // test-authoring flows is not tamper by itself; built-in critical paths still
2271
- // apply to all changed paths.
2272
- const builtInHumanPaths = [...new Set([...stats.paths, ...stats.existingPaths])];
2273
- const builtInHuman = requireHuman(builtInHumanPaths, DEFAULT_REQUIRE_HUMAN_PATHS);
2274
- const human = {
2275
- required: builtInHuman.required || protectedOnly.required,
2276
- reasons: [...new Set([...builtInHuman.reasons, ...protectedOnly.reasons])],
2277
- matchedPaths: [...new Set([...builtInHuman.matchedPaths, ...protectedOnly.matchedPaths])],
2278
- };
2279
- if (human.required) {
2280
- findings.push(ReviewFindingSchema.parse({
2281
- id: newId("find"),
2282
- severity: "NEEDS_HUMAN",
2283
- category: "security",
2284
- claim: `protected-path change requires human approval: ${human.reasons.join("; ")}`,
2285
- evidence: evidenceFromPaths(human.matchedPaths),
2286
- reviewer,
2287
- status: "accepted",
2288
- }));
2289
- }
2290
- const depth = reviewDepthForRisk(reportedRisk.level);
2291
- if (depth.humanApproval) {
2292
- findings.push(ReviewFindingSchema.parse({
2293
- id: newId("find"),
2294
- severity: "NEEDS_HUMAN",
2295
- category: "security",
2296
- claim: `critical-risk diff requires human approval: ${reportedRisk.reasons.join("; ")}`,
2297
- evidence: reportedRisk.matchedPaths.length > 0
2298
- ? evidenceFromPaths(reportedRisk.matchedPaths)
2299
- : evidenceFor(reportedRisk.reasons),
2300
- reviewer,
2301
- status: "accepted",
2302
- }));
2303
- }
2304
- else if (depth.crossFamily && !reviewVerified) {
2305
- findings.push(ReviewFindingSchema.parse({
2306
- id: newId("find"),
2307
- severity: "NEEDS_HUMAN",
2308
- category: "architecture",
2309
- claim: `high-risk diff requires a cross-family review panel (>=2 provider families), which is not available: ${reportedRisk.reasons.join("; ")}`,
2310
- evidence: reportedRisk.matchedPaths.length > 0
2311
- ? evidenceFromPaths(reportedRisk.matchedPaths)
2312
- : evidenceFor(reportedRisk.reasons),
2313
- reviewer,
2314
- status: "accepted",
2315
- }));
2316
- }
2317
- return {
2318
- findings,
2319
- risk: {
2320
- level: reportedRisk.level,
2321
- reasons: reportedRisk.reasons,
2322
- changedFiles: stats.paths.length,
2323
- },
2324
- };
2325
- }
2326
2355
  /**
2327
2356
  * SINGLE funnel for every reviewer-panel invocation: run it inside a per-review
2328
- * scoped harness HOME (Bible §6) so reviewer children (codex session rollouts,
2329
- * claude config) never write native state into the operator's real ~/.codex /
2330
- * ~/.claude. The codex route-proof transcript is read from this same scoped
2331
- * CODEX_HOME, so cross-family verification is unaffected. Every call site
2332
- * MUST go through here so the scoping cannot drift. Disposed once the panel
2333
- * settles (resolve OR reject).
2357
+ * scoped harness HOME (Bible §6) so reviewer scratch state and injected auth
2358
+ * routes do not enter the project or ordinary operator HOME. Native
2359
+ * Codex/Claude routes deliberately keep their vendor-owned host-user stores;
2360
+ * no credential file is copied into the scoped home. Every call site MUST go
2361
+ * through here so the non-native scoping cannot drift. Disposed once the
2362
+ * panel settles (resolve OR reject).
2334
2363
  */
2335
2364
  reviewScoped(input) {
2336
2365
  const reviewHome = new WorkspaceManager(input.cwd).readOnlyHomeEnv();
@@ -2347,7 +2376,7 @@ export class Orchestrator {
2347
2376
  const candidateCwd = run.reviewCwd ?? cwd;
2348
2377
  const candidateEvidenceDir = this.prepareReviewEvidenceDir(reviewDir, candidateCwd);
2349
2378
  try {
2350
- this.writeTestsEvidence(candidateEvidenceDir, contract, run.gates);
2379
+ writeText(join(candidateEvidenceDir, "TESTS.txt"), renderTestsEvidence(contract, run.gates).trim() + "\n");
2351
2380
  // a candidate that changed NO files has nothing to review — never
2352
2381
  // spend a reviewer panel on "(empty diff)" (a trivial greeting in agent mode used to
2353
2382
  // cost two reviewers). It still flows through policy gates and arbitration
@@ -2360,6 +2389,7 @@ export class Orchestrator {
2360
2389
  attemptId: run.attemptId,
2361
2390
  intent: "review",
2362
2391
  harnessId: "review-panel",
2392
+ cost: attemptCostEvidence("review-panel", run.attemptId),
2363
2393
  })
2364
2394
  : undefined;
2365
2395
  const result = hasDiff && reviewers.length > 0 && (reviewLease?.granted ?? true)
@@ -2385,15 +2415,21 @@ export class Orchestrator {
2385
2415
  distinctProviders: [],
2386
2416
  reviewSpendUsd: 0,
2387
2417
  reviewSpendEstimated: false,
2418
+ reviewCashUsd: 0,
2419
+ reviewValuationUsd: 0,
2420
+ reviewUnknownUsd: 0,
2388
2421
  };
2389
2422
  if (reviewLease?.granted) {
2390
- ledger?.settle(reviewLease.lease?.lease_id ?? "", result.reviewSpendUsd ?? 0);
2423
+ ledger?.settle(reviewLease.lease?.lease_id ?? "", reviewUsageCostSettlement(result.reviewCashUsd, result.reviewValuationUsd, result.reviewSpendEstimated, [`attempt:${run.attemptId}`, "review:panel"], result.reviewUnknownUsd));
2391
2424
  if ((result.reviewSpendUsd ?? 0) > 0) {
2392
2425
  log.emit("budget.observation", {
2393
2426
  harness_id: "review-panel",
2394
2427
  attempt_id: run.attemptId,
2395
2428
  kind: "spend",
2396
2429
  usd: result.reviewSpendUsd,
2430
+ cash_usd: result.reviewCashUsd,
2431
+ valuation_usd: result.reviewValuationUsd,
2432
+ unknown_usd: result.reviewUnknownUsd,
2397
2433
  estimated: result.reviewSpendEstimated === true,
2398
2434
  });
2399
2435
  }
@@ -2417,7 +2453,7 @@ export class Orchestrator {
2417
2453
  // Mirrors the convergence path (actualReviewVerified).
2418
2454
  const candidateReviewVerified = reviewVerified && result.crossFamilyHealthy && result.crossFamilyVerified;
2419
2455
  // Typed policy gate (risk + protected paths) merges with reviewer findings.
2420
- const policy = this.policyFindings(run, candidateReviewVerified, contract.constraints.protected_paths, contract.constraints.auto_protected_paths, contract.constraints.protected_path_approvals);
2456
+ const policy = policyFindings(run, candidateReviewVerified, contract.constraints.protected_paths, contract.constraints.auto_protected_paths, contract.constraints.protected_path_approvals, contract.constraints.deny_paths);
2421
2457
  const allFindings = [...policy.findings, ...revalidated];
2422
2458
  const inconclusive = allFindings.some((f) => f.severity === "INSUFFICIENT_EVIDENCE" || f.status === "insufficient_evidence");
2423
2459
  const noBlockers = !allFindings.some((f) => isBlocking(f));
@@ -2425,6 +2461,7 @@ export class Orchestrator {
2425
2461
  store.writeYaml(join(paths.reviewsDir, `${run.attemptId}.yaml`), {
2426
2462
  attempt_id: run.attemptId,
2427
2463
  review_verified: candidateReviewVerified,
2464
+ final_review_clean: reviewClean,
2428
2465
  cross_family_healthy: result.crossFamilyHealthy,
2429
2466
  cross_family_verified: result.crossFamilyVerified,
2430
2467
  healthy_providers: result.healthyProviders,
@@ -2440,7 +2477,7 @@ export class Orchestrator {
2440
2477
  severity: f.severity,
2441
2478
  status: f.status,
2442
2479
  });
2443
- evidences.push(this.toEvidence(run, contract, allFindings, reviewClean, candidateReviewVerified));
2480
+ evidences.push(toCandidateEvidence(run, contract, allFindings, reviewClean, candidateReviewVerified));
2444
2481
  }
2445
2482
  finally {
2446
2483
  this.recordReviewEvidenceCleanup(store, join(paths.reviewsDir, `${run.attemptId}-evidence-cleanup.yaml`), run.attemptId, candidateEvidenceDir, candidateCwd);
@@ -2448,23 +2485,15 @@ export class Orchestrator {
2448
2485
  }
2449
2486
  return evidences;
2450
2487
  }
2451
- prepareReviewEvidenceDir(sourceDir, candidateCwd) {
2452
- const targetDir = join(candidateCwd, REVIEW_EVIDENCE_DIRNAME);
2453
- if (sourceDir === targetDir) {
2454
- return this.requireReviewEvidence(targetDir);
2455
- }
2488
+ prepareReviewEvidenceDir(sourceDir, _candidateCwd) {
2489
+ // Evidence is an external runtime artifact. ReviewEngine builds a separate
2490
+ // reviewer workspace and copies the packet there; writing/copying it into
2491
+ // the candidate tree would contaminate the Git diff and, worse, overwrite a
2492
+ // user-owned path with the same name.
2456
2493
  if (!existsSync(sourceDir)) {
2457
2494
  throw new Error(`review evidence preflight failed for ${sourceDir}: source packet missing`);
2458
2495
  }
2459
- try {
2460
- rmSync(targetDir, { recursive: true, force: true });
2461
- cpSync(sourceDir, targetDir, { recursive: true });
2462
- }
2463
- catch (err) {
2464
- const message = err instanceof Error ? err.message : String(err);
2465
- throw new Error(`review evidence copy into candidate tree failed: ${message}`);
2466
- }
2467
- return this.requireReviewEvidence(targetDir);
2496
+ return this.requireReviewEvidence(sourceDir);
2468
2497
  }
2469
2498
  requireReviewEvidence(dir) {
2470
2499
  const result = preflightEvidence(dir);
@@ -2474,20 +2503,9 @@ export class Orchestrator {
2474
2503
  const empty = result.empty.length ? `empty=${result.empty.join(",")}` : "";
2475
2504
  throw new Error(`review evidence preflight failed for ${dir}: ${[missing, empty].filter(Boolean).join(" ")}`);
2476
2505
  }
2477
- cleanupReviewEvidenceDir(candidateEvidenceDir, candidateCwd) {
2478
- if (candidateEvidenceDir === join(candidateCwd, REVIEW_EVIDENCE_DIRNAME)) {
2479
- try {
2480
- rmSync(candidateEvidenceDir, { recursive: true, force: true });
2481
- }
2482
- catch (error) {
2483
- const detail = error instanceof Error ? error.message : String(error);
2484
- return {
2485
- review_evidence_cleanup: "failed",
2486
- candidate_evidence_dir: candidateEvidenceDir,
2487
- error: redactSecrets(detail),
2488
- };
2489
- }
2490
- }
2506
+ cleanupReviewEvidenceDir(_candidateEvidenceDir, _candidateCwd) {
2507
+ // No candidate-tree packet exists in v2; external runtime retention is
2508
+ // governed by the artifact/journal lifecycle rather than best-effort rm.
2491
2509
  return null;
2492
2510
  }
2493
2511
  recordReviewEvidenceCleanup(store, metadataPath, attemptId, candidateEvidenceDir, candidateCwd) {
@@ -2521,11 +2539,20 @@ export class Orchestrator {
2521
2539
  const execRoot = this.execRootOf(input);
2522
2540
  const wsm = new WorkspaceManager(execRoot);
2523
2541
  const readiness = new ReadinessLedger();
2524
- const ledger = new BudgetLedger({ maxUsd: contract.budget.max_usd ?? null });
2542
+ const ledger = this.rootLedger(input, contract, log);
2525
2543
  store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
2526
2544
  safeInvoke(input.onRunStart, { runId, taskId, runDir: paths.root });
2527
2545
  log.emit("run.created", { mode, prompt: redactSecrets(input.prompt) });
2528
- announce?.({ log, store, paths, runId, taskId, mode, phase: "convergence", spend: () => ledger.spend() });
2546
+ announce?.({
2547
+ log,
2548
+ store,
2549
+ paths,
2550
+ runId,
2551
+ taskId,
2552
+ mode,
2553
+ phase: "convergence",
2554
+ spend: () => ledger.spend(),
2555
+ });
2529
2556
  // Live (in-place) isolation deliberately tolerates non-git stateful
2530
2557
  // environments; only envelope isolation needs the git boundary.
2531
2558
  if (!input.inPlace) {
@@ -2548,7 +2575,7 @@ export class Orchestrator {
2548
2575
  writeEvidencePacket(reviewDir, {
2549
2576
  userIntent: redactSecrets(input.prompt),
2550
2577
  diff: "(per-attempt)\n",
2551
- tests: this.testsEvidence(contract),
2578
+ tests: renderTestsEvidence(contract),
2552
2579
  });
2553
2580
  const reviewersOutcome = await this.resolveReviewersWithArtifacts(input, log, store, paths, runId, taskId, mode);
2554
2581
  if ("failed" in reviewersOutcome)
@@ -2558,7 +2585,8 @@ export class Orchestrator {
2558
2585
  // One envelope carried forward across attempts so the harness can repair its own work.
2559
2586
  let adapterPool;
2560
2587
  try {
2561
- adapterPool = await this.resolveCandidateAdapters({ ...input, n: undefined }, this.candidateIntent(input));
2588
+ adapterPool = await this.resolveCandidateAdapters({ ...input, n: undefined }, this.candidateIntent(input), ledger);
2589
+ this.requestRequirements.assertConvergenceWorkspace(input.inPlace === true, adapterPool);
2562
2590
  }
2563
2591
  catch (err) {
2564
2592
  const message = safeErrorMessage(err);
@@ -2655,7 +2683,6 @@ export class Orchestrator {
2655
2683
  const stallThreshold = input.untilClean === true ? 4 : 2;
2656
2684
  const allCooledDown = () => adapterPool.every((a) => ledger.cooldownActive(a.adapter.id));
2657
2685
  const attemptTelemetries = [];
2658
- const harnessLedgers = new Map();
2659
2686
  let lastDiffStable = true;
2660
2687
  let reviewSpendEstimated = false;
2661
2688
  try {
@@ -2707,12 +2734,12 @@ export class Orchestrator {
2707
2734
  exhausted = true;
2708
2735
  break;
2709
2736
  }
2710
- // Per-harness max_usd runs through a child ledger that rolls up to the run cap.
2711
- const lease = this.harnessLedger(harnessLedgers, ledger, routed).reserve({
2737
+ const lease = ledger.reserve({
2712
2738
  taskId,
2713
2739
  attemptId,
2714
2740
  intent: "repair",
2715
2741
  harnessId: adapter.id,
2742
+ cost: attemptCostEvidence(adapter.id, attemptId, undefined, this.routeBillingKnowledge(input, adapter.id)),
2716
2743
  });
2717
2744
  if (!lease.granted) {
2718
2745
  exhausted = true;
@@ -2733,11 +2760,10 @@ export class Orchestrator {
2733
2760
  safeInvoke(input.onHarnessEvent, safeEv);
2734
2761
  log.emit("harness.event", harnessEventPayload(adapter.id, attemptId, safeEv));
2735
2762
  }, input.signal, undefined, input.effort, "repair", log, effectiveWeb, this.interactionChannelFor(input, log, runId, taskId, attemptId, adapter.id, routed.supportsInteractive), (streamedUsd) => {
2736
- const lg = this.harnessLedger(harnessLedgers, ledger, routed);
2737
- lg.updateHold(lease.lease?.lease_id ?? "", streamedUsd);
2738
- return lg.tier() === "hard";
2739
- }, input);
2740
- this.harnessLedger(harnessLedgers, ledger, routed).settle(lease.lease?.lease_id ?? "", run.cost);
2763
+ ledger.updateHold(lease.lease?.lease_id ?? "", streamedUsd);
2764
+ return ledger.tier() === "hard";
2765
+ }, input, true);
2766
+ ledger.settle(lease.lease?.lease_id ?? "", attemptUsageCostSettlement(run.cost, run.costEstimated, run.attemptId, run.harnessId, run.telemetry.authMode, run.telemetry.usageCost));
2741
2767
  log.emit("harness.completed", {
2742
2768
  harness_id: adapter.id,
2743
2769
  attempt_id: attemptId,
@@ -2749,7 +2775,7 @@ export class Orchestrator {
2749
2775
  catch (err) {
2750
2776
  // Envelope/setup failure before the stream; stream errors are absorbed
2751
2777
  // inside runCandidateInEnvelope with their real accumulated cost.
2752
- this.harnessLedger(harnessLedgers, ledger, routed).settle(lease.lease?.lease_id ?? "", 0);
2778
+ ledger.settle(lease.lease?.lease_id ?? "", unknownCostSettlement("attempt-error"));
2753
2779
  log.emit("harness.completed", {
2754
2780
  harness_id: adapter.id,
2755
2781
  attempt_id: attemptId,
@@ -2766,7 +2792,7 @@ export class Orchestrator {
2766
2792
  errored: true,
2767
2793
  costEstimated: false,
2768
2794
  errors: [safeErrorMessage(err)],
2769
- telemetry: createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required, effectiveWeb),
2795
+ telemetry: createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required, effectiveWeb, [routed.browserRequirement, routed.denyRequirement], knobs.model),
2770
2796
  };
2771
2797
  }
2772
2798
  lastRun = run;
@@ -2790,7 +2816,7 @@ export class Orchestrator {
2790
2816
  const candidateReviewCwd = run.reviewCwd ?? input.repoRoot;
2791
2817
  const candidateReviewEvidenceDir = this.prepareReviewEvidenceDir(reviewDir, candidateReviewCwd);
2792
2818
  try {
2793
- this.writeTestsEvidence(candidateReviewEvidenceDir, contract, run.gates);
2819
+ writeText(join(candidateReviewEvidenceDir, "TESTS.txt"), renderTestsEvidence(contract, run.gates).trim() + "\n");
2794
2820
  // Reviewer panels spend real money in convergence too: reserve before,
2795
2821
  // settle the observed cost, and surface it as a budget observation
2796
2822
  // (parity with the race path's reviewRuns metering).
@@ -2800,6 +2826,7 @@ export class Orchestrator {
2800
2826
  attemptId,
2801
2827
  intent: "review",
2802
2828
  harnessId: "review-panel",
2829
+ cost: attemptCostEvidence("review-panel", attemptId),
2803
2830
  })
2804
2831
  : null;
2805
2832
  const reviewResult = reviewers.length > 0 && (reviewLease?.granted ?? false)
@@ -2824,15 +2851,21 @@ export class Orchestrator {
2824
2851
  distinctProviders: [],
2825
2852
  reviewSpendUsd: 0,
2826
2853
  reviewSpendEstimated: false,
2854
+ reviewCashUsd: 0,
2855
+ reviewValuationUsd: 0,
2856
+ reviewUnknownUsd: 0,
2827
2857
  };
2828
2858
  if (reviewLease?.granted) {
2829
- ledger.settle(reviewLease.lease?.lease_id ?? "", reviewResult.reviewSpendUsd ?? 0);
2859
+ ledger.settle(reviewLease.lease?.lease_id ?? "", reviewUsageCostSettlement(reviewResult.reviewCashUsd, reviewResult.reviewValuationUsd, reviewResult.reviewSpendEstimated, [`attempt:${attemptId}`, "review:panel"], reviewResult.reviewUnknownUsd));
2830
2860
  if ((reviewResult.reviewSpendUsd ?? 0) > 0) {
2831
2861
  log.emit("budget.observation", {
2832
2862
  harness_id: "review-panel",
2833
2863
  attempt_id: attemptId,
2834
2864
  kind: "spend",
2835
2865
  usd: reviewResult.reviewSpendUsd,
2866
+ cash_usd: reviewResult.reviewCashUsd,
2867
+ valuation_usd: reviewResult.reviewValuationUsd,
2868
+ unknown_usd: reviewResult.reviewUnknownUsd,
2836
2869
  estimated: reviewResult.reviewSpendEstimated === true,
2837
2870
  });
2838
2871
  if (reviewResult.reviewSpendEstimated === true)
@@ -2856,12 +2889,18 @@ export class Orchestrator {
2856
2889
  evidenceDir: candidateReviewEvidenceDir,
2857
2890
  });
2858
2891
  // Typed policy gate (risk + protected paths) merges with reviewer findings.
2859
- const policy = this.policyFindings(run, actualReviewVerified, contract.constraints.protected_paths, contract.constraints.auto_protected_paths, contract.constraints.protected_path_approvals);
2892
+ const policy = policyFindings(run, actualReviewVerified, contract.constraints.protected_paths, contract.constraints.auto_protected_paths, contract.constraints.protected_path_approvals, contract.constraints.deny_paths);
2860
2893
  const allFindings = [...policy.findings, ...revalidated];
2861
2894
  lastFindings = allFindings;
2895
+ const inconclusive = allFindings.some((f) => f.severity === "INSUFFICIENT_EVIDENCE" || f.status === "insufficient_evidence");
2896
+ const finalReviewClean = reviewResult.crossFamilyHealthy &&
2897
+ reviewResult.crossFamilyVerified &&
2898
+ !inconclusive &&
2899
+ !allFindings.some((f) => isBlocking(f));
2862
2900
  store.writeYaml(join(paths.reviewsDir, `${attemptId}.yaml`), {
2863
2901
  attempt_id: attemptId,
2864
2902
  review_verified: actualReviewVerified,
2903
+ final_review_clean: finalReviewClean,
2865
2904
  cross_family_healthy: reviewResult.crossFamilyHealthy,
2866
2905
  cross_family_verified: reviewResult.crossFamilyVerified,
2867
2906
  healthy_providers: reviewResult.healthyProviders,
@@ -2871,11 +2910,6 @@ export class Orchestrator {
2871
2910
  findings: allFindings,
2872
2911
  route_proofs: reviewResult.routeProofs,
2873
2912
  });
2874
- const inconclusive = allFindings.some((f) => f.severity === "INSUFFICIENT_EVIDENCE" || f.status === "insufficient_evidence");
2875
- const finalReviewClean = reviewResult.crossFamilyHealthy &&
2876
- reviewResult.crossFamilyVerified &&
2877
- !inconclusive &&
2878
- !allFindings.some((f) => isBlocking(f));
2879
2913
  lastFinalReviewClean = finalReviewClean;
2880
2914
  // Measure diff stability instead of asserting it: the tree must not have
2881
2915
  // changed between the candidate diff capture and the end of review.
@@ -2994,10 +3028,12 @@ export class Orchestrator {
2994
3028
  : exhausted
2995
3029
  ? "exhausted"
2996
3030
  : "not_converged";
3031
+ if (status === "success" && ledger.terminal() !== null)
3032
+ status = ledger.terminal();
2997
3033
  let decision = null;
2998
3034
  if (lastRun) {
2999
3035
  const arb = arbitrate([
3000
- this.toEvidence(lastRun, contract, lastFindings, lastFinalReviewClean, actualReviewVerified),
3036
+ toCandidateEvidence(lastRun, contract, lastFindings, lastFinalReviewClean, actualReviewVerified),
3001
3037
  ], {
3002
3038
  spendUsd: ledger.spend(),
3003
3039
  estimatedSpend: lastRun.costEstimated || reviewSpendEstimated,
@@ -3027,7 +3063,7 @@ export class Orchestrator {
3027
3063
  lastRun.diff.trim().length > 0 &&
3028
3064
  (status === "success" || status === "ungated") &&
3029
3065
  !input.signal?.aborted) {
3030
- convFinalVerify = await finalVerifyPatch(execRoot, lastRun, this.gateSpecs(contract), log);
3066
+ convFinalVerify = await finalVerifyPatch(execRoot, lastRun, gateSpecsFromContract(contract), log);
3031
3067
  if (finalVerifyBlocks(convFinalVerify))
3032
3068
  status = "blocked";
3033
3069
  }
@@ -3036,7 +3072,9 @@ export class Orchestrator {
3036
3072
  // overrides the persisted decision; final_verify is recorded either way.
3037
3073
  decision = {
3038
3074
  ...decision,
3039
- ...(status === "blocked" ? blockedDecisionOverride(decision.evidence_facts, convFinalVerify) : {}),
3075
+ ...(status === "blocked"
3076
+ ? blockedDecisionOverride(decision.evidence_facts, convFinalVerify)
3077
+ : {}),
3040
3078
  final_verify: convFinalVerify,
3041
3079
  };
3042
3080
  store.writeYaml(join(paths.arbitrationDir, "decision.yaml"), decision);
@@ -3058,6 +3096,9 @@ export class Orchestrator {
3058
3096
  ? "applied"
3059
3097
  : "applied_review_blocked"
3060
3098
  : "not_applied";
3099
+ const revertAnchorId = convAdopted === true
3100
+ ? await createRevertAnchorOrNull(execRoot, preTurnSha, lastPostTurnSha)
3101
+ : null;
3061
3102
  store.writeYaml(join(paths.finalDir, "work_product.yaml"), {
3062
3103
  id: newId("wp"),
3063
3104
  kind: "patch",
@@ -3065,6 +3106,7 @@ export class Orchestrator {
3065
3106
  producer_attempt_id: lastRun.attemptId,
3066
3107
  meta: {
3067
3108
  harness_id: lastRun.harnessId,
3109
+ result_kind: "patch",
3068
3110
  mode,
3069
3111
  attempts: attempt,
3070
3112
  status,
@@ -3074,6 +3116,7 @@ export class Orchestrator {
3074
3116
  apply_state: convApplyState,
3075
3117
  pre_turn_sha: convAdopted === true ? preTurnSha : null,
3076
3118
  post_turn_sha: convAdopted === true ? lastPostTurnSha : null,
3119
+ revert_anchor_id: revertAnchorId,
3077
3120
  },
3078
3121
  });
3079
3122
  store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${mode})\n\n- Status: ${status}\n- Attempts: ${attempt}\n- Winner: ${lastRun.attemptId}\n- Review verified (cross-family): ${actualReviewVerified}\n- Apply recommendation: ${decision?.apply_recommendation ?? "inspect"}${stuckNoProgressReason ? `\n- No-progress reason: ${stuckNoProgressReason}` : ""}\n`);
@@ -3088,7 +3131,7 @@ export class Orchestrator {
3088
3131
  if (!converged) {
3089
3132
  writeFailure(store, paths, {
3090
3133
  phase: "convergence",
3091
- category: status === "exhausted"
3134
+ category: isBudgetTerminal(status)
3092
3135
  ? "budget"
3093
3136
  : status === "cancelled"
3094
3137
  ? "cancelled"
@@ -3204,19 +3247,32 @@ export class Orchestrator {
3204
3247
  const log = new EventLog(paths.eventsPath, runId, taskId, input.onEvent, input.threadId);
3205
3248
  safeInvoke(input.onRunStart, { runId, taskId, runDir: paths.root });
3206
3249
  log.emit("run.created", { mode: "plan", prompt: redactSecrets(input.prompt) });
3207
- const ledger = new BudgetLedger({ maxUsd: contract.budget.max_usd ?? null });
3208
- announce?.({ log, store, paths, runId, taskId, mode: "plan", phase: "plan", spend: () => ledger.spend() });
3250
+ const ledger = this.rootLedger(input, contract, log);
3251
+ announce?.({
3252
+ log,
3253
+ store,
3254
+ paths,
3255
+ runId,
3256
+ taskId,
3257
+ mode: "plan",
3258
+ phase: "plan",
3259
+ spend: () => ledger.spend(),
3260
+ });
3209
3261
  store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
3210
3262
  log.emit("task.contract.created", { task_contract_hash: hashJson(contract) });
3211
3263
  const reviewersOutcome = await this.resolveReviewersWithArtifacts(input, log, store, paths, runId, taskId, "plan");
3212
3264
  if ("failed" in reviewersOutcome)
3213
3265
  return reviewersOutcome.failed;
3214
3266
  const reviewers = reviewersOutcome.reviewers;
3267
+ // W3.3: ONE resolved read-only context — the routing point-probe and every
3268
+ // planner spawn consume the SAME scoped env (see routeContext.ts).
3269
+ const roHome = resolveReadOnlyRouteContext(this.execRootOf(input));
3215
3270
  let adapters;
3216
3271
  try {
3217
- adapters = await this.resolveCandidateAdapters({ ...input, n: undefined }, "plan");
3272
+ adapters = await this.resolveCandidateAdapters({ ...input, n: undefined }, "plan", ledger, roHome);
3218
3273
  }
3219
3274
  catch (err) {
3275
+ roHome.dispose();
3220
3276
  const message = safeErrorMessage(err);
3221
3277
  store.writeText(join(paths.contextDir, "context_error.md"), `# Routing Error\n\n${message}\n`);
3222
3278
  writeFailure(store, paths, {
@@ -3251,6 +3307,7 @@ export class Orchestrator {
3251
3307
  contextSection = await this.lazyContextSection(input, contract, store, paths, log);
3252
3308
  }
3253
3309
  catch (err) {
3310
+ roHome.dispose();
3254
3311
  const message = safeErrorMessage(err);
3255
3312
  store.writeText(join(paths.contextDir, "context_error.md"), `# Context Error\n\n${message}\n`);
3256
3313
  writeFailure(store, paths, {
@@ -3282,17 +3339,19 @@ export class Orchestrator {
3282
3339
  const plans = [];
3283
3340
  const planAttempts = [];
3284
3341
  const attemptTelemetries = [];
3285
- // scope the planners' HOME/config dirs so claude-code plan files (and any
3286
- // native session state) stay inside the run's scoped home, never the
3287
- // operator's real ~/.claude/plans. Disposed after the planners finish.
3288
- const roHome = new WorkspaceManager(this.execRootOf(input)).readOnlyHomeEnv();
3289
3342
  try {
3290
3343
  for (const [idx, routed] of adapters.entries()) {
3291
3344
  if (input.signal?.aborted)
3292
3345
  break;
3293
3346
  const adapter = routed.adapter;
3294
3347
  const attemptId = `p${String(idx + 1).padStart(2, "0")}`;
3295
- const lease = ledger.reserve({ taskId, attemptId, intent: "plan", harnessId: adapter.id });
3348
+ const lease = ledger.reserve({
3349
+ taskId,
3350
+ attemptId,
3351
+ intent: "plan",
3352
+ harnessId: adapter.id,
3353
+ cost: attemptCostEvidence(adapter.id, attemptId, undefined, this.routeBillingKnowledge(input, adapter.id)),
3354
+ });
3296
3355
  if (!lease.granted) {
3297
3356
  log.emit("budget.lease.created", {
3298
3357
  granted: false,
@@ -3313,16 +3372,8 @@ export class Orchestrator {
3313
3372
  // Planners must SEE any image/file the user attached (e.g. "plan a fix for
3314
3373
  // what's in this screenshot"), not just agent/race runs.
3315
3374
  attachments: input.attachments ?? [],
3316
- ...this.sessionSpecFields(input, adapter.id),
3317
- external_context_policy: knobs.webPolicy,
3318
- tool_permission_policy: {
3319
- web: knobs.webPolicy,
3320
- allow: [...new Set([...contract.tool_permission_policy.allow, ...knobs.toolsAllow])],
3321
- deny: [...new Set([...contract.tool_permission_policy.deny, ...knobs.toolsDeny])],
3322
- },
3323
- model_hint: knobs.model,
3324
- effort_hint: knobs.effort,
3325
- max_turns: knobs.maxTurns,
3375
+ ...this.sessionSpecFields(input, adapter.id, log),
3376
+ ...this.harnessSpecKnobs(contract, knobs, "plan"),
3326
3377
  env_inheritance: envInheritance(this.config(input.repoRoot)),
3327
3378
  env: roHome.env,
3328
3379
  });
@@ -3334,10 +3385,13 @@ export class Orchestrator {
3334
3385
  if (planInteraction)
3335
3386
  spec.extra["interactionChannel"] = planInteraction;
3336
3387
  const attemptEventsPath = join(paths.attemptsDir, attemptId, "events.jsonl");
3337
- const parts = [];
3388
+ const answer = new AnswerAssembly();
3338
3389
  const telemetry = createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required ||
3339
3390
  knobs.webPolicy === "cached" ||
3340
- knobs.webPolicy === "live", effectiveWeb);
3391
+ knobs.webPolicy === "live", effectiveWeb, [],
3392
+ // Requested-model capture: a plan lane silently downgraded to another
3393
+ // model surfaces the mismatch in its route receipt, just like agent.
3394
+ knobs.model);
3341
3395
  const onAbort = () => {
3342
3396
  void adapter.cancel?.(spec.session_id)?.catch(() => { });
3343
3397
  };
@@ -3348,6 +3402,7 @@ export class Orchestrator {
3348
3402
  input.signal.addEventListener("abort", onAbort, { once: true });
3349
3403
  }
3350
3404
  let cost = 0;
3405
+ let costEstimated = false;
3351
3406
  let harnessError = null;
3352
3407
  const budgetSignalState = { quotaPressureDisclosed: false };
3353
3408
  try {
@@ -3371,23 +3426,29 @@ export class Orchestrator {
3371
3426
  break;
3372
3427
  const safeEv = redactHarnessEvent(ev);
3373
3428
  safeInvoke(input.onHarnessEvent, safeEv);
3374
- // NOT observed for resume: this read-only/plan attempt runs in a
3375
- // DISPOSABLE roHome (disposed below), so its native session id is
3376
- // unreachable afterwards. Recording it would poison the thread resume
3377
- // map with dead ids the read-side mirror of the agent path's
3378
- // `if (inPlaceEnvelope)` guard. Codex-review-confirmed.
3429
+ // NOT observed for resume: a read-only planner is not a chat turn,
3430
+ // and attaching its session id would poison thread continuity (and
3431
+ // race parallel planner/reviewer sessions), regardless of whether
3432
+ // the vendor stored that session in the scoped or native store.
3379
3433
  observeAuthSwitch(log, adapter.id, attemptId, safeEv);
3380
3434
  log.emit("harness.event", harnessEventPayload(adapter.id, attemptId, safeEv));
3381
3435
  appendLine(attemptEventsPath, JSON.stringify(safeEv));
3382
3436
  observeAttemptTelemetry(telemetry, safeEv);
3383
3437
  if (safeEv.plan_progress) {
3384
- log.emit("plan.progress", { attempt_id: attemptId, harness_id: adapter.id, items: safeEv.plan_progress.items });
3438
+ log.emit("plan.progress", {
3439
+ attempt_id: attemptId,
3440
+ harness_id: adapter.id,
3441
+ items: safeEv.plan_progress.items,
3442
+ });
3385
3443
  }
3386
3444
  // read-only routes burn quota too (the orchestrate PLANNER is
3387
3445
  // the loudest) — same single owner as the agent loop.
3388
3446
  observeBudgetSignals(ledger, log, adapter.id, attemptId, safeEv, budgetSignalState);
3447
+ this.deps.quotaEventSink?.(adapter.id, safeEv);
3389
3448
  if (safeEv.type === "usage" && safeEv.usage?.cost_usd) {
3390
3449
  cost += safeEv.usage.cost_usd;
3450
+ if (safeEv.usage.estimated)
3451
+ costEstimated = true;
3391
3452
  log.emit("budget.observation", {
3392
3453
  harness_id: adapter.id,
3393
3454
  attempt_id: attemptId,
@@ -3396,11 +3457,8 @@ export class Orchestrator {
3396
3457
  estimated: safeEv.usage.estimated === true,
3397
3458
  });
3398
3459
  }
3399
- if (safeEv.type === "message" &&
3400
- safeEv.text &&
3401
- safeEv.payload?.["auth_switched"] !== true) {
3402
- pushUniqueText(parts, safeEv.text);
3403
- }
3460
+ // A TYPED final message wins verbatim over joined narration.
3461
+ answer.observe(safeEv);
3404
3462
  if (safeEv.type === "error")
3405
3463
  harnessError = safeEv.error
3406
3464
  ? redactSecrets(safeEv.error)
@@ -3413,7 +3471,7 @@ export class Orchestrator {
3413
3471
  }
3414
3472
  finally {
3415
3473
  input.signal?.removeEventListener("abort", onAbort);
3416
- ledger.settle(lease.lease?.lease_id ?? "", cost);
3474
+ ledger.settle(lease.lease?.lease_id ?? "", attemptUsageCostSettlement(cost, costEstimated, attemptId, adapter.id, telemetry.authMode, telemetry.usageCost));
3417
3475
  }
3418
3476
  attemptTelemetries.push({ attemptId, harnessId: adapter.id, telemetry });
3419
3477
  const unrecovered = unrecoveredToolErrors(telemetry);
@@ -3443,7 +3501,7 @@ export class Orchestrator {
3443
3501
  });
3444
3502
  continue;
3445
3503
  }
3446
- const text = parts.join("\n").trim() || "(no output)";
3504
+ const text = answer.text() || "(no output)";
3447
3505
  log.emit("harness.completed", {
3448
3506
  harness_id: adapter.id,
3449
3507
  attempt_id: attemptId,
@@ -3456,7 +3514,7 @@ export class Orchestrator {
3456
3514
  }
3457
3515
  }
3458
3516
  finally {
3459
- // Planners done (or threw) — always reclaim the scoped home (it may hold seeded creds).
3517
+ // Planners done (or threw) — reclaim scoped scratch/API-route state.
3460
3518
  roHome.dispose();
3461
3519
  }
3462
3520
  if (input.signal?.aborted) {
@@ -3464,7 +3522,7 @@ export class Orchestrator {
3464
3522
  attemptId: p.attemptId,
3465
3523
  harnessId: p.harnessId,
3466
3524
  status: p.status,
3467
- })), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, "plan", attemptTelemetries, null), ledger.spend());
3525
+ })), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, "plan", attemptTelemetries, null), ledger.spend(), input.signal, store);
3468
3526
  }
3469
3527
  if (plans.length === 0) {
3470
3528
  const blocked = planAttempts.some((p) => p.status === "blocked");
@@ -3523,7 +3581,7 @@ export class Orchestrator {
3523
3581
  userIntent: redactSecrets(input.prompt),
3524
3582
  planAccepted: planEvidence,
3525
3583
  diff: planReviewDiff,
3526
- tests: this.testsEvidence(contract),
3584
+ tests: renderTestsEvidence(contract),
3527
3585
  });
3528
3586
  // Reserve BEFORE spending: a hard budget tier must stop the paid plan
3529
3587
  // review from starting, not account for it after the fact.
@@ -3532,10 +3590,12 @@ export class Orchestrator {
3532
3590
  attemptId: "plan-review",
3533
3591
  intent: "review",
3534
3592
  harnessId: "review-panel",
3593
+ cost: attemptCostEvidence("review-panel", "plan-review"),
3535
3594
  });
3536
3595
  if (lease.granted) {
3537
3596
  const res = await this.reviewScoped({
3538
3597
  candidateLabel: "Plan",
3598
+ reviewSubject: "plan",
3539
3599
  diff: planReviewDiff,
3540
3600
  evidenceDir: reviewDir,
3541
3601
  artifactsDir: join(paths.reviewsDir, "plan-reviewers"),
@@ -3555,12 +3615,15 @@ export class Orchestrator {
3555
3615
  route_proofs: res.routeProofs,
3556
3616
  reviewer_requests: res.reviewerRequests,
3557
3617
  });
3558
- ledger.settle(lease.lease?.lease_id ?? "", res.reviewSpendUsd ?? 0);
3618
+ ledger.settle(lease.lease?.lease_id ?? "", reviewUsageCostSettlement(res.reviewCashUsd, res.reviewValuationUsd, res.reviewSpendEstimated, ["attempt:plan-review", "review:panel"], res.reviewUnknownUsd));
3559
3619
  if ((res.reviewSpendUsd ?? 0) > 0) {
3560
3620
  log.emit("budget.observation", {
3561
3621
  harness_id: "review-panel",
3562
3622
  kind: "spend",
3563
3623
  usd: res.reviewSpendUsd,
3624
+ cash_usd: res.reviewCashUsd,
3625
+ valuation_usd: res.reviewValuationUsd,
3626
+ unknown_usd: res.reviewUnknownUsd,
3564
3627
  estimated: res.reviewSpendEstimated,
3565
3628
  });
3566
3629
  }
@@ -3579,7 +3642,7 @@ export class Orchestrator {
3579
3642
  attemptId: p.attemptId,
3580
3643
  harnessId: p.harnessId,
3581
3644
  status: p.status,
3582
- })), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, "plan", attemptTelemetries, null), ledger.spend());
3645
+ })), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, "plan", attemptTelemetries, null), ledger.spend(), input.signal, store);
3583
3646
  }
3584
3647
  const failedPlanners = planAttempts.filter((p) => p.status !== "success");
3585
3648
  // ALL review findings are shown (severity-marked), so a BLOCK like "the
@@ -3690,18 +3753,40 @@ export class Orchestrator {
3690
3753
  defaultPrompt: "audit this repository",
3691
3754
  }, announce);
3692
3755
  }
3693
- /**
3694
- * orchestrate: the autonomous planner. NOT a privileged harness — the planner
3695
- * is routed like reviewers (doctor-ok + `orchestrate` capability + headroom)
3696
- * and runs READ-ONLY. With default `suggest` autonomy its work product is a
3697
- * typed orchestration plan over the 6-tool belt (start_run / race / status /
3698
- * answer_question / apply / review); execution happens as subsequent thread
3699
- * turns. Degradation contract: any 1 harness works (single-route plan); 2+
3700
- * harnesses unlock cross-family race/review in the plan space.
3701
- */
3702
- /** ONE owner of the per-run USD cap precedence: explicit input -> embedder deps -> operator config. */
3703
- resolveMaxUsdCap(inputMaxUsd, cfg) {
3704
- return inputMaxUsd ?? this.deps.maxUsd ?? cfg.global.budget.max_usd_per_run ?? null;
3756
+ resolvePaidBudget(inputBudget, cfg) {
3757
+ return inputBudget ?? this.deps.paidBudget ?? cfg.global.budget.paid_budget_per_run;
3758
+ }
3759
+ rootLedger(input, contract, log) {
3760
+ // A passed-in ledger (orchestrate sub-runs) keeps its OWNER's cash
3761
+ // disclosure the parent run owns the budget, so its event log gets the
3762
+ // budget.cash events. A fresh root ledger discloses into THIS run's log:
3763
+ // the ledger is the one owner of the cash fact (subscription-entitled
3764
+ // work settles to 0 there), and the UI renders `budget.cash` verbatim —
3765
+ // never inferring money from route labels (W4.3 sol #15).
3766
+ const ledger = input.budgetLedger ??
3767
+ new BudgetLedger(contract.budget.paid_budget, undefined, {
3768
+ onCashSettled: (cashSpendUsd, valuationUsd) => log.emit("budget.cash", {
3769
+ cash_spend_usd: cashSpendUsd,
3770
+ valuation_usd: valuationUsd,
3771
+ }),
3772
+ });
3773
+ for (const snapshot of this.deps.quotaSnapshots?.() ?? []) {
3774
+ ledger.observeQuotaSnapshot(snapshot);
3775
+ }
3776
+ return ledger;
3777
+ }
3778
+ routeBillingKnowledge(input, harnessId) {
3779
+ // A selected profile's credential_kind decides billing (round-18 #2).
3780
+ const profileRoute = this.profileAuthRoute(input, harnessId);
3781
+ if (profileRoute)
3782
+ return profileRoute === "api_key" ? "metered" : "unknown";
3783
+ if (input.authPreference === "api_key")
3784
+ return "metered";
3785
+ if (input.authPreference === "subscription")
3786
+ return "unknown";
3787
+ return loadHarnessMetrics(globalConfigDir())[harnessId]?.last_auth_mode === "api_key"
3788
+ ? "metered"
3789
+ : "unknown";
3705
3790
  }
3706
3791
  async runOrchestrate(input, announce) {
3707
3792
  // "Doctor-verified" must mean status ok — degraded key-present routes are
@@ -3715,15 +3800,11 @@ export class Orchestrator {
3715
3800
  // the executor below is its consumer. Default `suggest` (plan-only) preserves
3716
3801
  // the read-only contract when no autonomy is requested.
3717
3802
  const autonomy = input.autonomy ?? "suggest";
3718
- // The aggregate cap resolves the SAME chain as run(): explicit input ->
3719
- // orchestrator deps -> operator config default. Without the fallback an
3720
- // operator's max_usd_per_run capped every sub-run individually but never
3721
- // the aggregate (R33 finding).
3722
- const aggregateMaxUsd = this.resolveMaxUsdCap(input.maxUsd, this.config(input.repoRoot));
3803
+ const paidBudget = this.resolvePaidBudget(input.paidBudget, this.config(input.repoRoot));
3723
3804
  const orchestrateContract = OrchestrateContractSchema.parse({
3724
3805
  thread_id: input.threadId ?? newId("th"),
3725
3806
  goal,
3726
- budget: { max_usd: aggregateMaxUsd, max_tool_calls: input.maxToolCalls ?? null },
3807
+ budget: { paid_budget: paidBudget, max_tool_calls: input.maxToolCalls ?? null },
3727
3808
  autonomy,
3728
3809
  });
3729
3810
  const plannerPrompt = buildOrchestratePlannerPrompt(goal, pool, crossFamily, orchestrateContract);
@@ -3762,8 +3843,17 @@ export class Orchestrator {
3762
3843
  const log = new EventLog(paths.eventsPath, runId, taskId, input.onEvent, input.threadId);
3763
3844
  safeInvoke(input.onRunStart, { runId, taskId, runDir: paths.root });
3764
3845
  log.emit("run.created", { mode: opts.mode, prompt: redactSecrets(prompt) });
3765
- const ledger = new BudgetLedger({ maxUsd: contract.budget.max_usd ?? null });
3766
- announce?.({ log, store, paths, runId, taskId, mode: opts.mode, phase: "report", spend: () => ledger.spend() });
3846
+ const ledger = this.rootLedger(input, contract, log);
3847
+ announce?.({
3848
+ log,
3849
+ store,
3850
+ paths,
3851
+ runId,
3852
+ taskId,
3853
+ mode: opts.mode,
3854
+ phase: "report",
3855
+ spend: () => ledger.spend(),
3856
+ });
3767
3857
  store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
3768
3858
  log.emit("task.contract.created", { task_contract_hash: hashJson(contract) });
3769
3859
  if (opts.orchestrateContract) {
@@ -3814,9 +3904,12 @@ export class Orchestrator {
3814
3904
  : externalContextPolicy === "off"
3815
3905
  ? 1
3816
3906
  : Math.min(Math.max(input.n ?? 2, 1), 3);
3907
+ // W3.3: ONE resolved read-only context — the routing point-probe and every
3908
+ // read-only attempt spawn consume the SAME scoped env (see routeContext.ts).
3909
+ const roHome = resolveReadOnlyRouteContext(this.execRootOf(input));
3817
3910
  let adapters;
3818
3911
  try {
3819
- adapters = await this.resolveCandidateAdapters({ ...input, prompt, n: width }, opts.intent);
3912
+ adapters = await this.resolveCandidateAdapters({ ...input, prompt, n: width }, opts.intent, ledger, roHome);
3820
3913
  if (!opts.swarm) {
3821
3914
  const seen = new Set();
3822
3915
  adapters = adapters.filter((routed) => {
@@ -3828,6 +3921,7 @@ export class Orchestrator {
3828
3921
  }
3829
3922
  }
3830
3923
  catch (err) {
3924
+ roHome.dispose();
3831
3925
  const message = safeErrorMessage(err);
3832
3926
  store.writeText(join(paths.contextDir, "context_error.md"), `# Routing Error\n\n${message}\n`);
3833
3927
  writeFailure(store, paths, {
@@ -3855,11 +3949,6 @@ export class Orchestrator {
3855
3949
  candidates: [],
3856
3950
  };
3857
3951
  }
3858
- // read-only routes still spawn harness processes that write native
3859
- // state (plan files, session rollouts). Scope their HOME/config dirs so they
3860
- // cannot escape into the operator's real ~/.claude, ~/.codex, etc. — the
3861
- // adapters seed auth into these scoped dirs (§6). Disposed at run end.
3862
- const roHome = new WorkspaceManager(this.execRootOf(input)).readOnlyHomeEnv();
3863
3952
  const attempts = [];
3864
3953
  const attemptTelemetries = [];
3865
3954
  let fallbackOpen = false;
@@ -3880,6 +3969,7 @@ export class Orchestrator {
3880
3969
  attemptId,
3881
3970
  intent: opts.intent,
3882
3971
  harnessId: adapter.id,
3972
+ cost: attemptCostEvidence(adapter.id, attemptId, undefined, this.routeBillingKnowledge(input, adapter.id)),
3883
3973
  });
3884
3974
  if (!lease.granted) {
3885
3975
  log.emit("budget.lease.created", {
@@ -3896,11 +3986,11 @@ export class Orchestrator {
3896
3986
  const explorerPrompt = (opts.swarm
3897
3987
  ? `${prompt}\n\nExplorer ${idx + 1}/${adapters.length}: focus on a distinct slice. Emit evidence-cited findings, explicit unknowns/omissions, and follow-up questions. Do not edit files.`
3898
3988
  : prompt) + contextSection;
3899
- const sessionFields = this.sessionSpecFields(input, adapter.id);
3989
+ const sessionFields = this.sessionSpecFields(input, adapter.id, log);
3900
3990
  const grantResume = sessionFields.resume_session_id !== null && !resumeGranted.has(adapter.id);
3901
3991
  if (grantResume)
3902
3992
  resumeGranted.add(adapter.id);
3903
- const spec = HarnessRunSpec.parse({
3993
+ let spec = HarnessRunSpec.parse({
3904
3994
  session_id: newId("ses"),
3905
3995
  intent: opts.intent,
3906
3996
  prompt: explorerPrompt,
@@ -3911,16 +4001,9 @@ export class Orchestrator {
3911
4001
  // the model honestly reported it saw nothing (the v0.13 attachment bug).
3912
4002
  attachments: input.attachments ?? [],
3913
4003
  auth_preference: sessionFields.auth_preference,
4004
+ credential_profile: sessionFields.credential_profile,
3914
4005
  resume_session_id: grantResume ? sessionFields.resume_session_id : null,
3915
- external_context_policy: knobs.webPolicy,
3916
- tool_permission_policy: {
3917
- web: knobs.webPolicy,
3918
- allow: [...new Set([...contract.tool_permission_policy.allow, ...knobs.toolsAllow])],
3919
- deny: [...new Set([...contract.tool_permission_policy.deny, ...knobs.toolsDeny])],
3920
- },
3921
- model_hint: knobs.model,
3922
- effort_hint: knobs.effort,
3923
- max_turns: knobs.maxTurns,
4006
+ ...this.harnessSpecKnobs(contract, knobs, opts.intent),
3924
4007
  env_inheritance: envInheritance(this.config(input.repoRoot)),
3925
4008
  env: roHome.env,
3926
4009
  // Structured output: the orchestrate PLANNER's deliverable IS the
@@ -3944,10 +4027,13 @@ export class Orchestrator {
3944
4027
  if (reportInteraction)
3945
4028
  spec.extra["interactionChannel"] = reportInteraction;
3946
4029
  const attemptEventsPath = join(paths.attemptsDir, attemptId, "events.jsonl");
3947
- const parts = [];
4030
+ const answer = new AnswerAssembly();
3948
4031
  const telemetry = createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required ||
3949
4032
  knobs.webPolicy === "cached" ||
3950
- knobs.webPolicy === "live", effectiveWeb);
4033
+ knobs.webPolicy === "live", effectiveWeb, [],
4034
+ // Requested-model capture so ask/audit route receipts detect a silent
4035
+ // model downgrade (typed model_mismatch), not just agent runs.
4036
+ knobs.model);
3951
4037
  const retryPolicy = transientRetryPolicy(this.config(input.repoRoot));
3952
4038
  let activeSessionId = spec.session_id;
3953
4039
  const onAbort = () => {
@@ -3960,8 +4046,10 @@ export class Orchestrator {
3960
4046
  input.signal.addEventListener("abort", onAbort, { once: true });
3961
4047
  }
3962
4048
  let cost = 0;
4049
+ let costEstimated = false;
3963
4050
  let harnessError = null;
3964
4051
  try {
4052
+ const triedProfiles = new Set(); // W5.4 failover: each profile at most once
3965
4053
  for (let nativeTry = 0; !input.signal?.aborted; nativeTry += 1) {
3966
4054
  const runSpec = nativeTry === 0
3967
4055
  ? spec
@@ -3973,6 +4061,7 @@ export class Orchestrator {
3973
4061
  });
3974
4062
  activeSessionId = runSpec.session_id;
3975
4063
  const transientStart = telemetry.transientFailures.length;
4064
+ const rateLimitStart = telemetry.rateLimits.length;
3976
4065
  log.emit("harness.started", {
3977
4066
  harness_id: adapter.id,
3978
4067
  attempt_id: attemptId,
@@ -3995,23 +4084,28 @@ export class Orchestrator {
3995
4084
  break;
3996
4085
  const safeEv = redactHarnessEvent(ev);
3997
4086
  safeInvoke(input.onHarnessEvent, safeEv);
3998
- // NOT observed for resume: this read-only/plan attempt runs in a
3999
- // DISPOSABLE roHome (disposed below), so its native session id is
4000
- // unreachable afterwards. Recording it would poison the thread resume
4001
- // map with dead ids — the read-side mirror of the agent path's
4002
- // `if (inPlaceEnvelope)` guard. Codex-review-confirmed.
4087
+ // NOT observed for resume: this read-only attempt is not a chat
4088
+ // turn. Recording its id would poison thread continuity and can
4089
+ // race parallel read-only sessions, regardless of storage route.
4003
4090
  observeAuthSwitch(log, adapter.id, attemptId, safeEv);
4004
4091
  log.emit("harness.event", harnessEventPayload(adapter.id, attemptId, safeEv));
4005
4092
  appendLine(attemptEventsPath, JSON.stringify(safeEv));
4006
4093
  observeAttemptTelemetry(telemetry, safeEv);
4007
4094
  if (safeEv.plan_progress) {
4008
- log.emit("plan.progress", { attempt_id: attemptId, harness_id: adapter.id, items: safeEv.plan_progress.items });
4095
+ log.emit("plan.progress", {
4096
+ attempt_id: attemptId,
4097
+ harness_id: adapter.id,
4098
+ items: safeEv.plan_progress.items,
4099
+ });
4009
4100
  }
4010
4101
  // read-only routes burn quota too (the orchestrate PLANNER is
4011
4102
  // the loudest) — same single owner as the agent loop.
4012
4103
  observeBudgetSignals(ledger, log, adapter.id, attemptId, safeEv, budgetSignalState);
4104
+ this.deps.quotaEventSink?.(adapter.id, safeEv);
4013
4105
  if (safeEv.type === "usage" && safeEv.usage?.cost_usd) {
4014
4106
  cost += safeEv.usage.cost_usd;
4107
+ if (safeEv.usage.estimated)
4108
+ costEstimated = true;
4015
4109
  log.emit("budget.observation", {
4016
4110
  harness_id: adapter.id,
4017
4111
  attempt_id: attemptId,
@@ -4020,11 +4114,8 @@ export class Orchestrator {
4020
4114
  estimated: safeEv.usage.estimated === true,
4021
4115
  });
4022
4116
  }
4023
- if (safeEv.type === "message" &&
4024
- safeEv.text &&
4025
- safeEv.payload?.["auth_switched"] !== true) {
4026
- pushUniqueText(parts, safeEv.text);
4027
- }
4117
+ // A TYPED final message wins verbatim over joined narration.
4118
+ answer.observe(safeEv);
4028
4119
  if (safeEv.type === "error")
4029
4120
  harnessError = safeEv.error
4030
4121
  ? redactSecrets(safeEv.error)
@@ -4036,7 +4127,32 @@ export class Orchestrator {
4036
4127
  }
4037
4128
  const transient = telemetry.transientFailures.at(-1) ?? null;
4038
4129
  const sawTransient = telemetry.transientFailures.length > transientStart;
4039
- const reportSoFar = parts.join("\n").trim();
4130
+ const sawTypedLimit = telemetry.rateLimits.length > rateLimitStart;
4131
+ const reportSoFar = answer.text();
4132
+ // W5.4 reactive failover, READ-ONLY lane (same contract as the
4133
+ // candidate lane; typed limits only, never plain transients).
4134
+ if (harnessError && !input.signal?.aborted) {
4135
+ const rotated = rotateSpecOnTypedLimit({
4136
+ spec,
4137
+ harnessId: adapter.id,
4138
+ attemptId,
4139
+ policy: this.profilePolicy(input.repoRoot, adapter.id),
4140
+ registry: this.config(input.repoRoot)?.global.credential_profiles ?? [],
4141
+ snapshots: this.deps.quotaSnapshots?.() ?? [],
4142
+ triedProfiles,
4143
+ sawTypedLimit,
4144
+ deliverableEmpty: reportSoFar.length === 0,
4145
+ lastLimit: telemetry.rateLimits.at(-1) ?? null,
4146
+ emit: (type, payload) => log.emit(type, payload),
4147
+ newSessionId: () => newId("ses"),
4148
+ defaultRouteWasVendorNative: routed.authRouteEstimate === "local_session",
4149
+ });
4150
+ if (rotated) {
4151
+ spec = rotated;
4152
+ harnessError = null;
4153
+ continue;
4154
+ }
4155
+ }
4040
4156
  if (!harnessError ||
4041
4157
  !sawTransient ||
4042
4158
  reportSoFar.length > 0 ||
@@ -4063,7 +4179,7 @@ export class Orchestrator {
4063
4179
  }
4064
4180
  finally {
4065
4181
  input.signal?.removeEventListener("abort", onAbort);
4066
- ledger.settle(lease.lease?.lease_id ?? "", cost);
4182
+ ledger.settle(lease.lease?.lease_id ?? "", attemptUsageCostSettlement(cost, costEstimated, attemptId, adapter.id, telemetry.authMode, telemetry.usageCost));
4067
4183
  }
4068
4184
  if (harnessError && telemetry.transientFailures.length > 0) {
4069
4185
  log.emit("route.transient.exhausted", {
@@ -4073,7 +4189,7 @@ export class Orchestrator {
4073
4189
  });
4074
4190
  }
4075
4191
  attemptTelemetries.push({ attemptId, harnessId: adapter.id, telemetry });
4076
- const report = redactSecrets(parts.join("\n").trim());
4192
+ const report = redactSecrets(answer.text());
4077
4193
  const unrecovered = unrecoveredToolErrors(telemetry);
4078
4194
  const webBlocked = webUnsatisfied(telemetry);
4079
4195
  const deliverablePresent = report.length > 0;
@@ -4213,8 +4329,8 @@ export class Orchestrator {
4213
4329
  }
4214
4330
  }
4215
4331
  finally {
4216
- // All read-only attempts done (or threw) — reclaim the scoped harness home
4217
- // (it contained every native write for this run and may hold seeded creds).
4332
+ // All read-only attempts done (or threw) — reclaim scoped scratch and
4333
+ // injected API-route state. Vendor-owned native credentials were not copied.
4218
4334
  roHome.dispose();
4219
4335
  }
4220
4336
  if (input.signal?.aborted) {
@@ -4222,7 +4338,7 @@ export class Orchestrator {
4222
4338
  attemptId: a.attemptId,
4223
4339
  harnessId: a.harnessId,
4224
4340
  status: a.status,
4225
- })), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, opts.mode, attemptTelemetries, null), ledger.spend());
4341
+ })), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, opts.mode, attemptTelemetries, null), ledger.spend(), input.signal, store);
4226
4342
  }
4227
4343
  const succeededReadonly = attempts.filter((a) => a.status === "success");
4228
4344
  if (!opts.swarm && succeededReadonly.length === 0) {
@@ -4377,6 +4493,18 @@ export class Orchestrator {
4377
4493
  ].join("\n")
4378
4494
  : (succeeded[0]?.report ?? "(no output)");
4379
4495
  store.writeText(join(paths.finalDir, opts.artifactName), `# ${opts.title}\n\n${report}\n`);
4496
+ // ask is the only read-only strategy that can carry a structured-output
4497
+ // contract (the boundary refuses the rest); validate the RAW answer text,
4498
+ // not the titled artifact wrapper.
4499
+ if (opts.mode === "ask" && contract.output_schema) {
4500
+ finalizeStructuredOutput({
4501
+ store,
4502
+ finalDir: paths.finalDir,
4503
+ log,
4504
+ schema: contract.output_schema,
4505
+ answerText: succeeded[0]?.report ?? "",
4506
+ });
4507
+ }
4380
4508
  // orchestrate: the planner's plan is a TYPED artifact, not just prose. Extract
4381
4509
  // the required fenced JSON block, validate it against the tool belt, and
4382
4510
  // persist final/orchestration.yaml; a missing/invalid block is disclosed in
@@ -4436,6 +4564,10 @@ export class Orchestrator {
4436
4564
  // terminal outcome (success / blocked / failed) becomes the run's terminal.
4437
4565
  const autonomy = opts.orchestrateContract?.autonomy ?? input.autonomy ?? "suggest";
4438
4566
  let terminal = "success";
4567
+ let orchestrateReadOnly = true;
4568
+ let orchestrateReceiptRefs = [];
4569
+ if (ledger.terminal() !== null)
4570
+ terminal = ledger.terminal();
4439
4571
  // orchestrate's contract output IS the typed plan. If the planner failed to
4440
4572
  // produce a valid one, the run is NOT a clean success — disclose it honestly
4441
4573
  // (the markdown plan stays as a diagnostic artifact) rather than reporting
@@ -4446,11 +4578,22 @@ export class Orchestrator {
4446
4578
  // Thread the GENERATED runId onto input so the executor's answer_question
4447
4579
  // step keys the interaction registry by this orchestrate run's id (callers
4448
4580
  // often invoke run() without a preassigned runId).
4449
- const exec = await this.executeOrchestratePlan({ ...input, runId }, orchestratePlan, autonomy, opts.orchestrateContract ?? null, store, paths, log,
4450
- // The PLANNER's own settled spend counts against the same cap: the
4451
- // aggregate must start from it, not from zero (completeness).
4452
- ledger.spend());
4581
+ const executionInput = { ...input, runId };
4582
+ const exec = await executeOrchestratePlan({
4583
+ plan: orchestratePlan,
4584
+ autonomy,
4585
+ maxToolCalls: opts.orchestrateContract?.budget.max_tool_calls ?? null,
4586
+ signal: input.signal,
4587
+ store,
4588
+ paths,
4589
+ log,
4590
+ ledger,
4591
+ executeSafeStep: (call) => this.executeSafeStep(executionInput, call, log, store, paths, ledger),
4592
+ executeApplyStep: (call) => this.executeApplyStep(executionInput, call, log),
4593
+ });
4453
4594
  terminal = exec.terminal;
4595
+ orchestrateReadOnly = exec.readOnly;
4596
+ orchestrateReceiptRefs = exec.receiptRefs;
4454
4597
  typedPlanNote += `\n- Executor (${autonomy}): ${exec.note}`;
4455
4598
  }
4456
4599
  const harnessLabel = attempts
@@ -4462,69 +4605,32 @@ export class Orchestrator {
4462
4605
  kind: "report",
4463
4606
  source_task_id: taskId,
4464
4607
  producer_attempt_id: succeeded[0]?.attemptId ?? "a01",
4465
- files: { [opts.artifactName]: join(paths.finalDir, opts.artifactName) },
4608
+ files: Object.fromEntries([
4609
+ [opts.artifactName, join(paths.finalDir, opts.artifactName)],
4610
+ ...orchestrateReceiptRefs.map((ref, index) => [`delivery_receipt_${index + 1}`, ref]),
4611
+ ]),
4466
4612
  meta: {
4467
4613
  harnesses: attempts.map((a) => a.harnessId),
4468
4614
  mode: opts.mode,
4469
4615
  intent: opts.intent,
4470
- read_only: true,
4616
+ read_only: orchestrateReadOnly,
4471
4617
  },
4472
4618
  });
4473
4619
  log.emit("work_product.emitted", { kind: "report", winner: succeeded[0]?.attemptId ?? null });
4474
- if (terminal === "blocked") {
4475
- writeFailure(store, paths, {
4476
- phase: "executor",
4477
- category: "policy",
4478
- safeMessage: "orchestrate executor stopped at a risky step (apply) under auto_safe; awaiting a human decision",
4479
- runDir: paths.root,
4480
- nextActions: [
4481
- "Review the proposed apply",
4482
- "Approve via the run decision endpoint",
4483
- "Re-run with auto_full to apply automatically",
4484
- ],
4485
- });
4620
+ const orchestrateFailure = orchestrateFailureFor(terminal);
4621
+ if (terminal === "blocked" && orchestrateFailure) {
4622
+ writeFailure(store, paths, { ...orchestrateFailure, runDir: paths.root });
4486
4623
  log.emit("run.blocked", {
4487
4624
  status: terminal,
4488
- phase: "executor",
4625
+ phase: orchestrateFailure.phase,
4489
4626
  failure_ref: "final/failure.yaml",
4490
4627
  });
4491
4628
  }
4492
- else if (terminal === "failed") {
4493
- writeFailure(store, paths, {
4494
- phase: "executor",
4495
- category: "internal",
4496
- safeMessage: "orchestrate executor failed: a safe step errored fatally (see final/orchestration_progress.yaml)",
4497
- runDir: paths.root,
4498
- nextActions: [
4499
- "Inspect final/orchestration_progress.yaml",
4500
- "Open the failed sub-run",
4501
- "Re-run after the cause is fixed",
4502
- ],
4503
- });
4629
+ else if (orchestrateFailure) {
4630
+ writeFailure(store, paths, { ...orchestrateFailure, runDir: paths.root });
4504
4631
  log.emit("run.failed", {
4505
4632
  status: terminal,
4506
- phase: "executor",
4507
- failure_ref: "final/failure.yaml",
4508
- });
4509
- }
4510
- else if (terminal === "not_converged") {
4511
- // Orchestrate's typed-plan contract failed (the planner produced no valid
4512
- // plan): a failure-shaped terminal with artifacts, never run.completed —
4513
- // jobs.json and events.jsonl must agree the run did not converge.
4514
- writeFailure(store, paths, {
4515
- phase: "plan",
4516
- category: "harness_error",
4517
- safeMessage: "orchestrate planner produced no valid typed plan (see final/orchestration_parse_error.md); the markdown report is diagnostic only",
4518
- runDir: paths.root,
4519
- nextActions: [
4520
- "Inspect final/orchestration_parse_error.md",
4521
- "Re-run orchestrate",
4522
- "Check the planner harness doctor status",
4523
- ],
4524
- });
4525
- log.emit("run.failed", {
4526
- status: terminal,
4527
- phase: "plan",
4633
+ phase: orchestrateFailure.phase,
4528
4634
  failure_ref: "final/failure.yaml",
4529
4635
  });
4530
4636
  }
@@ -4534,26 +4640,6 @@ export class Orchestrator {
4534
4640
  // mistake an operator abort for a clean report.
4535
4641
  log.emit("run.failed", { status: terminal });
4536
4642
  }
4537
- else if (terminal === "exhausted") {
4538
- // A budget-truncated plan is failure-shaped: skipped steps mean the
4539
- // goal was NOT met — `claudexor follow` and jobs.json must not read a
4540
- // cut-short auto_full run as a clean success (exit 0).
4541
- writeFailure(store, paths, {
4542
- phase: "executor",
4543
- category: "budget",
4544
- safeMessage: "orchestrate executor exhausted its budget before completing the plan (see final/orchestration_progress.yaml for the skipped steps)",
4545
- runDir: paths.root,
4546
- nextActions: [
4547
- "Inspect final/orchestration_progress.yaml",
4548
- "Re-run with a higher --max-usd / --max-tool-calls",
4549
- ],
4550
- });
4551
- log.emit("run.failed", {
4552
- status: terminal,
4553
- phase: "executor",
4554
- failure_ref: "final/failure.yaml",
4555
- });
4556
- }
4557
4643
  else {
4558
4644
  log.emit("run.completed", { status: terminal });
4559
4645
  }
@@ -4573,190 +4659,20 @@ export class Orchestrator {
4573
4659
  })),
4574
4660
  };
4575
4661
  }
4576
- /**
4577
- * Execute a typed orchestration plan under auto_safe / auto_full. Runs the
4578
- * tool_calls IN ORDER, classifying each via toolRisk (FAIL-CLOSED). Persists
4579
- * final/orchestration_progress.yaml and emits progress events. Returns the
4580
- * executor's terminal status (success / blocked / failed) and a short note.
4581
- *
4582
- * SAFETY INVARIANTS (see CLAUDEXOR doctrine):
4583
- * 1. A SAFE step NEVER mutates the live tree: start_run/race run as isolated
4584
- * ENVELOPE sub-runs (inPlace=false, asserted), review/status/answer are reads.
4585
- * 2. Risk is fail-closed (toolRisk): any unknown/undeclared tool is risky.
4586
- * 3. auto_safe STOPS at the first risky step (apply) without executing it; the
4587
- * run ends `blocked` awaiting a human decision.
4588
- * 4. answer_question / status / review are read-only w.r.t. the tree.
4589
- */
4590
- async executeOrchestratePlan(input, plan, autonomy, contract, store, paths, log, brainSpentUsd = 0) {
4591
- const maxToolCalls = contract?.budget.max_tool_calls ?? null;
4592
- // The RESOLVED aggregate cap (input -> deps -> operator config) rides the
4593
- // contract; raw input.maxUsd alone would ignore the config default.
4594
- const aggregateMaxUsd = contract?.budget.max_usd ?? input.maxUsd ?? null;
4595
- const steps = plan.tool_calls.map((call, index) => ({
4596
- index,
4597
- tool: call.tool,
4598
- risk: toolRisk(call.tool),
4599
- status: "pending",
4600
- run_id: null,
4601
- detail: null,
4602
- }));
4603
- let stoppedReason = null;
4604
- let terminal = "success";
4605
- const persist = () => {
4606
- const progress = { steps, autonomy, stopped_reason: stoppedReason };
4607
- store.writeYaml(join(paths.finalDir, "orchestration_progress.yaml"), progress);
4608
- };
4609
- persist();
4610
- log.emit("output.ready", { kind: "report", path: "final/orchestration_progress.yaml" });
4611
- let executed = 0;
4612
- // Aggregate budget: the planner's own spend plus every sub-run share
4613
- // ONE cap. Each sequential sub-run gets the REMAINING headroom (cap minus
4614
- // settled spend so far) — never the full cap again per step.
4615
- let aggregateSpentUsd = brainSpentUsd;
4616
- for (let i = 0; i < plan.tool_calls.length; i++) {
4617
- const call = plan.tool_calls[i];
4618
- const step = steps[i];
4619
- // Honor input.signal abort: stop, mark remaining steps skipped.
4620
- if (input.signal?.aborted) {
4621
- step.status = "skipped";
4622
- step.detail = "run cancelled before this step";
4623
- stoppedReason = "cancelled";
4624
- terminal = "cancelled";
4625
- persist();
4626
- break;
4627
- }
4628
- // Aggregate USD cap: stop before a step that has no headroom left.
4629
- if (aggregateMaxUsd !== null && aggregateSpentUsd >= aggregateMaxUsd) {
4630
- step.status = "skipped";
4631
- step.detail = `aggregate budget exhausted (${aggregateSpentUsd.toFixed(2)} of ${aggregateMaxUsd} USD spent)`;
4632
- stoppedReason = `aggregate budget exhausted after ${executed} step(s)`;
4633
- terminal = "exhausted";
4634
- persist();
4635
- break;
4636
- }
4637
- // Honor the budget cap on tool calls (count attempted executions).
4638
- // Same honesty as the USD cap: a plan cut short by a budget knob ends
4639
- // `exhausted`, never a quiet "success" whose note miscounts the steps.
4640
- if (maxToolCalls !== null && executed >= maxToolCalls) {
4641
- step.status = "skipped";
4642
- step.detail = `budget max_tool_calls=${maxToolCalls} reached`;
4643
- stoppedReason = `budget max_tool_calls=${maxToolCalls} reached after ${executed} step(s)`;
4644
- terminal = "exhausted";
4645
- persist();
4646
- break;
4647
- }
4648
- const risk = toolRisk(call.tool);
4649
- // RISKY step (apply, or any fail-closed-risky tool).
4650
- if (risk === "risky") {
4651
- if (autonomy === "auto_safe") {
4652
- // STOP: do not execute the risky step; block awaiting a human decision.
4653
- step.status = "blocked";
4654
- step.detail = "risky step requires human approval (auto_safe)";
4655
- stoppedReason = `blocked at risky step #${i} (${call.tool}) under auto_safe`;
4656
- terminal = "blocked";
4657
- log.emit("orchestrate.step.blocked", { index: i, tool: call.tool, autonomy });
4658
- persist();
4659
- break;
4660
- }
4661
- // auto_full: execute the risky step (apply) via the single gate.
4662
- step.status = "running";
4663
- persist();
4664
- executed++;
4665
- try {
4666
- const r = await this.executeApplyStep(input, call);
4667
- step.status = r.ok ? "done" : "failed";
4668
- step.run_id = r.runId;
4669
- step.detail = r.detail;
4670
- log.emit("orchestrate.step.done", {
4671
- index: i,
4672
- tool: call.tool,
4673
- ok: r.ok,
4674
- run_id: r.runId,
4675
- });
4676
- if (!r.ok) {
4677
- terminal = "failed";
4678
- stoppedReason = `apply step #${i} failed: ${r.detail}`;
4679
- persist();
4680
- break;
4681
- }
4682
- }
4683
- catch (err) {
4684
- step.status = "failed";
4685
- step.detail = safeErrorMessage(err);
4686
- terminal = "failed";
4687
- stoppedReason = `apply step #${i} threw: ${safeErrorMessage(err)}`;
4688
- persist();
4689
- break;
4690
- }
4691
- persist();
4692
- continue;
4693
- }
4694
- // SAFE step: execute as an isolated sub-run / pure read.
4695
- step.status = "running";
4696
- persist();
4697
- executed++;
4698
- try {
4699
- const remainingUsd = aggregateMaxUsd === null ? null : Math.max(0, aggregateMaxUsd - aggregateSpentUsd);
4700
- const r = await this.executeSafeStep(input, call, log, store, paths, remainingUsd);
4701
- aggregateSpentUsd += r.spendUsd ?? 0;
4702
- step.status = r.status;
4703
- step.run_id = r.runId;
4704
- step.detail = r.detail;
4705
- log.emit("orchestrate.step.done", {
4706
- index: i,
4707
- tool: call.tool,
4708
- status: r.status,
4709
- run_id: r.runId,
4710
- });
4711
- if (r.status === "failed") {
4712
- terminal = "failed";
4713
- stoppedReason = `safe step #${i} (${call.tool}) errored: ${r.detail}`;
4714
- persist();
4715
- break;
4716
- }
4717
- }
4718
- catch (err) {
4719
- step.status = "failed";
4720
- step.detail = safeErrorMessage(err);
4721
- terminal = "failed";
4722
- stoppedReason = `safe step #${i} (${call.tool}) threw: ${safeErrorMessage(err)}`;
4723
- persist();
4724
- break;
4725
- }
4726
- persist();
4727
- }
4728
- // A FINAL step can overspend its remaining headroom (spend is charged
4729
- // after the step; no later pre-step check exists to trip). An overshot
4730
- // cap must not read "success — all steps done".
4731
- if (terminal === "success" && aggregateMaxUsd !== null && aggregateSpentUsd > aggregateMaxUsd) {
4732
- terminal = "exhausted";
4733
- stoppedReason = `aggregate budget overshot on the final step (${aggregateSpentUsd.toFixed(2)} of ${aggregateMaxUsd} USD)`;
4734
- }
4735
- persist();
4736
- const done = steps.filter((s) => s.status === "done").length;
4737
- const note = terminal === "blocked"
4738
- ? `blocked at a risky step (${done}/${steps.length} safe steps done)`
4739
- : terminal === "failed"
4740
- ? `failed (${done}/${steps.length} steps done; ${stoppedReason ?? "see progress"})`
4741
- : terminal === "cancelled"
4742
- ? `cancelled (${done}/${steps.length} steps done)`
4743
- : terminal === "exhausted"
4744
- ? `budget exhausted (${done}/${steps.length} steps done; ${stoppedReason ?? "see progress"})`
4745
- : `all ${done}/${steps.length} steps done`;
4746
- return { terminal, note };
4747
- }
4748
4662
  /**
4749
4663
  * Run one SAFE plan step. start_run/race spawn ISOLATED ENVELOPE sub-runs
4750
4664
  * (inPlace=false, ASSERTED); review/status/answer_question are pure reads /
4751
4665
  * answer delivery that never mutate the live tree.
4752
4666
  */
4753
- async executeSafeStep(input, call, log, store, paths, remainingUsd) {
4667
+ async executeSafeStep(input, call, log, store, paths, ledger) {
4754
4668
  switch (call.tool) {
4755
4669
  case "start_run":
4756
4670
  case "race": {
4757
4671
  // Isolated envelope sub-run (construction owned by runSupport);
4758
4672
  // recursion guard via orchestrateDepth+1, may NOT orchestrate.
4759
- const subInput = { ...buildEnvelopeSubInput(input, call, remainingUsd) };
4673
+ const subInput = {
4674
+ ...buildEnvelopeSubInput(input, call, ledger),
4675
+ };
4760
4676
  // SAFETY INVARIANT 1 (asserted, not convention): a safe sub-run is an
4761
4677
  // isolated envelope — never a live in-place turn.
4762
4678
  assertEnvelopeSubRun(subInput);
@@ -4768,6 +4684,9 @@ export class Orchestrator {
4768
4684
  const res = await this.run(subInput);
4769
4685
  return {
4770
4686
  status: res.status === "failed" || res.status === "cancelled" ? "failed" : "done",
4687
+ terminalStatus: res.status,
4688
+ terminalSource: "subrun",
4689
+ evidenceRefs: [`run:${res.runId}`],
4771
4690
  runId: res.runId,
4772
4691
  spendUsd: res.spendUsd ?? null,
4773
4692
  detail: `${call.tool} sub-run ${res.runId} -> ${res.status}`,
@@ -4778,8 +4697,11 @@ export class Orchestrator {
4778
4697
  const read = readRunStatus(input.repoRoot, call.run_id);
4779
4698
  return {
4780
4699
  status: read ? "done" : "skipped",
4700
+ terminalStatus: read?.status ?? null,
4701
+ terminalSource: "subrun",
4702
+ evidenceRefs: read?.evidenceRefs ?? [],
4781
4703
  runId: call.run_id,
4782
- detail: read ?? `run ${call.run_id} has no readable status artifacts`,
4704
+ detail: read?.detail ?? `run ${call.run_id} has no readable status artifacts`,
4783
4705
  };
4784
4706
  }
4785
4707
  case "review": {
@@ -4791,15 +4713,21 @@ export class Orchestrator {
4791
4713
  if (diff === null)
4792
4714
  return {
4793
4715
  status: "skipped",
4716
+ terminalStatus: null,
4717
+ terminalSource: "review",
4718
+ evidenceRefs: [],
4794
4719
  runId: call.run_id,
4795
4720
  detail: `run ${call.run_id} has no patch.diff to review`,
4796
4721
  };
4797
4722
  // Aggregate honesty: reviewer panels spend real money on
4798
4723
  // API-keyed routes and the spend is charged AFTER the fact — with no
4799
4724
  // remaining headroom the review must not start at all.
4800
- if (remainingUsd !== null && remainingUsd <= 0) {
4725
+ if (ledger.terminal() !== null) {
4801
4726
  return {
4802
4727
  status: "skipped",
4728
+ terminalStatus: null,
4729
+ terminalSource: "review",
4730
+ evidenceRefs: [],
4803
4731
  runId: call.run_id,
4804
4732
  detail: "aggregate budget exhausted before the review step",
4805
4733
  };
@@ -4808,9 +4736,28 @@ export class Orchestrator {
4808
4736
  if (reviewers.length === 0)
4809
4737
  return {
4810
4738
  status: "skipped",
4739
+ terminalStatus: null,
4740
+ terminalSource: "review",
4741
+ evidenceRefs: [],
4811
4742
  runId: call.run_id,
4812
4743
  detail: "no doctor-OK reviewers available",
4813
4744
  };
4745
+ const reviewLease = ledger.reserve({
4746
+ taskId: input.taskId ?? "orchestrate",
4747
+ attemptId: `review-${call.run_id}`,
4748
+ intent: "review",
4749
+ harnessId: "review-panel",
4750
+ cost: attemptCostEvidence("review-panel", `review-${call.run_id}`),
4751
+ });
4752
+ if (!reviewLease.granted)
4753
+ return {
4754
+ status: "skipped",
4755
+ terminalStatus: ledger.terminal(),
4756
+ terminalSource: "review",
4757
+ evidenceRefs: [],
4758
+ runId: call.run_id,
4759
+ detail: reviewLease.reason ?? "root paid budget refused the review step",
4760
+ };
4814
4761
  const evidenceDir = join(paths.reviewsDir, `orchestrate-${call.run_id}`, "evidence");
4815
4762
  writeEvidencePacket(evidenceDir, {
4816
4763
  userIntent: redactSecrets(input.prompt),
@@ -4829,7 +4776,11 @@ export class Orchestrator {
4829
4776
  envInheritance: envInheritance(this.config(input.repoRoot)),
4830
4777
  signal: input.signal,
4831
4778
  onReviewerEvent: (event) => log.emit(event.type, { ...event }),
4779
+ }).catch((error) => {
4780
+ ledger.settle(reviewLease.lease?.lease_id ?? "", unknownCostSettlement("review-error"));
4781
+ throw error;
4832
4782
  });
4783
+ ledger.settle(reviewLease.lease?.lease_id ?? "", reviewUsageCostSettlement(result.reviewCashUsd, result.reviewValuationUsd, result.reviewSpendEstimated, [`orchestrate:review:${call.run_id}`], result.reviewUnknownUsd));
4833
4784
  const revalidated = await revalidateFindings(result.findings, {
4834
4785
  candidateRoot: input.repoRoot,
4835
4786
  evidenceDir,
@@ -4844,16 +4795,25 @@ export class Orchestrator {
4844
4795
  const blockers = revalidated.filter((f) => isBlocking(f)).length;
4845
4796
  return {
4846
4797
  status: "done",
4798
+ terminalStatus: result.crossFamilyVerified && blockers === 0 ? "success" : "blocked",
4799
+ terminalSource: "review",
4800
+ evidenceRefs: [`reviews/orchestrate-${call.run_id}.yaml`],
4847
4801
  runId: call.run_id,
4848
4802
  detail: `reviewed ${call.run_id}: ${result.distinctProviders.length} family(ies), ${revalidated.length} finding(s), ${blockers} blocker(s)`,
4849
4803
  // Reviewer panels can spend real money on API-keyed routes; the
4850
4804
  // aggregate cap must charge it like any other step.
4851
- spendUsd: result.reviewSpendUsd ?? null,
4805
+ spendUsd: result.reviewCashUsd,
4852
4806
  };
4853
4807
  }
4854
4808
  case "answer_question": {
4855
4809
  // Delivery + registry-keying rationale owned by runSupport.
4856
- return deliverPlanAnswer(input, call);
4810
+ const answer = await deliverPlanAnswer(input, call);
4811
+ return {
4812
+ ...answer,
4813
+ terminalStatus: answer.status === "done" ? "success" : null,
4814
+ terminalSource: "executor",
4815
+ evidenceRefs: [],
4816
+ };
4857
4817
  }
4858
4818
  default: {
4859
4819
  // FAIL-CLOSED: a risky tool (apply) must never reach the safe executor;
@@ -4862,13 +4822,8 @@ export class Orchestrator {
4862
4822
  }
4863
4823
  }
4864
4824
  }
4865
- /**
4866
- * Execute a RISKY `apply` step (auto_full only) through the SINGLE existing
4867
- * apply gate (`validateApplyGate`) + `deliver` — the same path
4868
- * accept_clean_patch uses. Reads the referenced run's patch + work_product +
4869
- * decision artifacts; refuses unless the gate passes.
4870
- */
4871
- async executeApplyStep(input, call) {
4825
+ /** Execute an auto_full apply through the shared fresh-verification gate. */
4826
+ async executeApplyStep(input, call, log) {
4872
4827
  const store = new ArtifactStore(input.repoRoot);
4873
4828
  const sub = store.runPaths(call.run_id);
4874
4829
  const patchPath = join(sub.finalDir, "patch.diff");
@@ -4877,25 +4832,29 @@ export class Orchestrator {
4877
4832
  return {
4878
4833
  ok: false,
4879
4834
  runId: call.run_id,
4880
- detail: `run ${call.run_id} has no patch.diff to apply`,
4835
+ detail: `run ${call.run_id} has no patch.diff`,
4836
+ receipt: null,
4881
4837
  };
4882
4838
  if (containsSecretLikeToken(patchText))
4883
4839
  return {
4884
4840
  ok: false,
4885
4841
  runId: call.run_id,
4886
4842
  detail: "patch contains a secret-like token; refusing apply",
4843
+ receipt: null,
4887
4844
  };
4888
4845
  const decision = store.readYaml(join(sub.arbitrationDir, "decision.yaml"));
4889
4846
  const workProduct = store.readYaml(join(sub.finalDir, "work_product.yaml"));
4847
+ const taskContract = TaskContractSchema.safeParse(store.readYaml(join(sub.contextDir, "task.yaml")));
4890
4848
  const parsedDecision = decision ? DecisionRecordSchema.safeParse(decision) : null;
4891
4849
  const parsedWp = workProduct ? WorkProductSchema.safeParse(workProduct) : null;
4892
- // The referenced run's recorded original project IS this orchestrate run's
4893
- // repoRoot (sub-runs were spawned against it); the gate re-verifies identity.
4894
- const gateError = validateApplyGate({
4895
- // Artifact-only path (we read the referenced run's decision/work_product
4896
- // from disk, not a live daemon job): pass state=null and let the gate's
4897
- // decision.status check be the terminal-state guard. Hardcoding "succeeded"
4898
- // would silently bypass that check if it were ever relaxed.
4850
+ if (!taskContract.success)
4851
+ return {
4852
+ ok: false,
4853
+ runId: call.run_id,
4854
+ detail: "fresh verification contract is missing",
4855
+ receipt: null,
4856
+ };
4857
+ const applyGateInput = {
4899
4858
  state: null,
4900
4859
  decision: parsedDecision?.success ? parsedDecision.data : null,
4901
4860
  workProduct: parsedWp?.success ? parsedWp.data : null,
@@ -4903,16 +4862,23 @@ export class Orchestrator {
4903
4862
  originalRepoRoot: input.repoRoot,
4904
4863
  targetRepoRoot: input.repoRoot,
4905
4864
  operatorDecision: null,
4906
- });
4865
+ };
4866
+ const gateError = validateApplyGate(applyGateInput);
4907
4867
  if (gateError)
4908
- return { ok: false, runId: call.run_id, detail: `apply gate refused: ${gateError}` };
4909
- const delivered = await deliver(input.repoRoot, patchText, { mode: call.mode });
4868
+ return {
4869
+ ok: false,
4870
+ runId: call.run_id,
4871
+ detail: `apply gate refused: ${gateError}`,
4872
+ receipt: null,
4873
+ };
4874
+ const delivered = await verifyAndDeliver(input.repoRoot, patchText, { mode: call.mode }, gateSpecsFromContract(taskContract.data), (finalVerify) => validateApplyGate({ ...applyGateInput, finalVerify }), log);
4910
4875
  return {
4911
4876
  ok: delivered.applied,
4912
4877
  runId: call.run_id,
4913
4878
  detail: delivered.applied
4914
4879
  ? `applied (${call.mode})`
4915
4880
  : `deliver failed: ${delivered.detail ?? "unknown"}`,
4881
+ receipt: delivered,
4916
4882
  };
4917
4883
  }
4918
4884
  }