@claudexor/orchestrator 1.0.1 → 2.1.2

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 (75) hide show
  1. package/dist/attemptTelemetry.d.ts +38 -2
  2. package/dist/attemptTelemetry.d.ts.map +1 -1
  3. package/dist/attemptTelemetry.js +75 -2
  4. package/dist/attemptTelemetry.js.map +1 -1
  5. package/dist/contract-gates.d.ts +21 -0
  6. package/dist/contract-gates.d.ts.map +1 -0
  7. package/dist/contract-gates.js +93 -0
  8. package/dist/contract-gates.js.map +1 -0
  9. package/dist/credential-profiles.d.ts +152 -0
  10. package/dist/credential-profiles.d.ts.map +1 -0
  11. package/dist/credential-profiles.js +270 -0
  12. package/dist/credential-profiles.js.map +1 -0
  13. package/dist/diffReview.d.ts +9 -1
  14. package/dist/diffReview.d.ts.map +1 -1
  15. package/dist/diffReview.js +114 -18
  16. package/dist/diffReview.js.map +1 -1
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/modelGovernance.d.ts +6 -1
  22. package/dist/modelGovernance.d.ts.map +1 -1
  23. package/dist/modelGovernance.js +8 -2
  24. package/dist/modelGovernance.js.map +1 -1
  25. package/dist/orchestrateExecutor.d.ts +43 -0
  26. package/dist/orchestrateExecutor.d.ts.map +1 -0
  27. package/dist/orchestrateExecutor.js +207 -0
  28. package/dist/orchestrateExecutor.js.map +1 -0
  29. package/dist/orchestratePlanner.d.ts.map +1 -1
  30. package/dist/orchestratePlanner.js.map +1 -1
  31. package/dist/orchestrator.d.ts +100 -83
  32. package/dist/orchestrator.d.ts.map +1 -1
  33. package/dist/orchestrator.js +1008 -993
  34. package/dist/orchestrator.js.map +1 -1
  35. package/dist/outcomeReducer.d.ts +25 -0
  36. package/dist/outcomeReducer.d.ts.map +1 -0
  37. package/dist/outcomeReducer.js +121 -0
  38. package/dist/outcomeReducer.js.map +1 -0
  39. package/dist/policyFindings.d.ts +18 -0
  40. package/dist/policyFindings.d.ts.map +1 -0
  41. package/dist/policyFindings.js +166 -0
  42. package/dist/policyFindings.js.map +1 -0
  43. package/dist/requestRequirements.d.ts +38 -0
  44. package/dist/requestRequirements.d.ts.map +1 -0
  45. package/dist/requestRequirements.js +127 -0
  46. package/dist/requestRequirements.js.map +1 -0
  47. package/dist/reviewerPanel.d.ts +15 -1
  48. package/dist/reviewerPanel.d.ts.map +1 -1
  49. package/dist/reviewerPanel.js +85 -2
  50. package/dist/reviewerPanel.js.map +1 -1
  51. package/dist/routeContext.d.ts +37 -0
  52. package/dist/routeContext.d.ts.map +1 -0
  53. package/dist/routeContext.js +38 -0
  54. package/dist/routeContext.js.map +1 -0
  55. package/dist/runSupport.d.ts +37 -21
  56. package/dist/runSupport.d.ts.map +1 -1
  57. package/dist/runSupport.js +102 -78
  58. package/dist/runSupport.js.map +1 -1
  59. package/dist/runTelemetryWriter.d.ts +20 -0
  60. package/dist/runTelemetryWriter.d.ts.map +1 -0
  61. package/dist/runTelemetryWriter.js +68 -0
  62. package/dist/runTelemetryWriter.js.map +1 -0
  63. package/dist/runTerminals.d.ts +7 -1
  64. package/dist/runTerminals.d.ts.map +1 -1
  65. package/dist/runTerminals.js +42 -4
  66. package/dist/runTerminals.js.map +1 -1
  67. package/dist/structuredOutput.d.ts +33 -0
  68. package/dist/structuredOutput.d.ts.map +1 -0
  69. package/dist/structuredOutput.js +106 -0
  70. package/dist/structuredOutput.js.map +1 -0
  71. package/package.json +19 -18
  72. package/dist/finalVerifier.d.ts +0 -25
  73. package/dist/finalVerifier.d.ts.map +0 -1
  74. package/dist/finalVerifier.js +0 -110
  75. package/dist/finalVerifier.js.map +0 -1
@@ -1,33 +1,40 @@
1
- import { cpSync, existsSync, readFileSync, rmSync } from "node:fs";
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { observeNativeSessionEvent, preflightCredentialProfile, preflightDefaultSubject, resolveCredentialProfile, resumeSessionForProfile, rotateSpecOnTypedLimit, } from "./credential-profiles.js";
3
+ import { writeRunTelemetryArtifact } from "./runTelemetryWriter.js";
4
+ import { capabilityIntents } from "@claudexor/gateway";
5
+ import { policyFindings } from "./policyFindings.js";
2
6
  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";
7
+ 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
8
  import { globalConfigDir, loadConfig, trustConfigPath } from "@claudexor/config";
5
9
  import { specPackToTaskContract } from "@claudexor/interview";
6
- import { HarnessUnavailableError, summarizeDiffPaths as diffStats, validateModel, withInactivityWatchdog } from "@claudexor/core";
10
+ import { AnswerAssembly, HarnessUnavailableError, summarizeDiffPaths as diffStats, withInactivityWatchdog, } from "@claudexor/core";
7
11
  import { assertRouteModelsAllowed } from "./modelGovernance.js";
12
+ import { RequestRequirementsResolver } from "./requestRequirements.js";
8
13
  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";
14
+ import { assertOutputSchemaCompiles, finalizeStructuredOutput } from "./structuredOutput.js";
15
+ 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";
16
+ import { candidateStatusInRouteContext, resolveReadOnlyRouteContext, } from "./routeContext.js";
17
+ import { resolveAutoReviewerPanel, resolveExplicitReviewerPanel } from "./reviewerPanel.js";
11
18
  import { buildOrchestratePlannerPrompt, extractOrchestratePlan } from "./orchestratePlanner.js";
12
- import { blockedDecisionOverride, finalVerifyBlocks, finalVerifyPatch } from "./finalVerifier.js";
19
+ import { orchestrateFailureFor, readRunStatus } from "./outcomeReducer.js";
20
+ import { executeOrchestratePlan, } from "./orchestrateExecutor.js";
13
21
  import { runDiffReview } from "./diffReview.js";
14
- import { aggregateRunWebEvidence, attemptTelemetryRecord, createAttemptTelemetry, observeAttemptTelemetry, setAttemptOutcome, telemetrySummary, toolWarnings, unrecoveredToolErrors, webUnsatisfied, } from "./attemptTelemetry.js";
22
+ import { createAttemptTelemetry, observeAttemptTelemetry, setAttemptOutcome, telemetrySummary, toolWarnings, unrecoveredToolErrors, webUnsatisfied, } from "./attemptTelemetry.js";
15
23
  import { interactionChannelFor } from "./interaction.js";
24
+ import { gateSpecsFromContract, renderTestsEvidence, resolveContractGates, } from "./contract-gates.js";
16
25
  import { ArtifactStore } from "@claudexor/artifact-store";
17
26
  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";
27
+ import { assertMandatoryContext, buildContextPack, rawContextForEnvelope, preflightEvidence, writeEvidencePacket, } from "@claudexor/context";
28
+ import { WorkspaceManager, captureRawPatchEnvelope, createRevertAnchorFromPatchOrNull, createRevertAnchorOrNull, ensureGitRepository, consumeRawPatchEnvelope, snapshotTree, } from "@claudexor/workspace";
29
+ import { blockedDecisionOverride, finalVerifyBlocks, finalVerifyPatch, validateApplyGate, verifyAndDeliver, } from "@claudexor/delivery";
21
30
  import { HarnessGateway } from "@claudexor/gateway";
22
31
  import { ReadinessLedger, evaluateConvergence, failureSignature, gatesPassed, reviewCandidate, revalidateFindings, runGates, } from "@claudexor/review";
23
32
  import { arbitrate } from "@claudexor/arbitration";
24
33
  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";
34
+ import { attemptCostEvidence, attemptUsageCostSettlement, BudgetLedger, isBudgetTerminal, loadHarnessMetrics, promptFingerprint, unknownCostSettlement, usageCostSettlement, rankHarnesses, } from "@claudexor/budget";
27
35
  import { appendLine, assertNoInlineSecretValues, containsSecretLikeToken, hashJson, newId, noProjectRepoRoot, nowIso, redactSecrets, safeInvoke, sha256, userConfigDir, writeText, } from "@claudexor/util";
28
36
  const LABELS = "ABCDEFGHIJ".split("");
29
37
  const NO_PROJECT_ROOT = noProjectRepoRoot();
30
- const REVIEW_EVIDENCE_DIRNAME = ".claudexor-review-evidence";
31
38
  /** Concurrency cap for parallel candidates/explorers (locked decision: min(n, 4)). */
32
39
  const MAX_PARALLEL_CANDIDATES = 4;
33
40
  /** Default wait for one interactive answer before a benign decline. */
@@ -66,6 +73,11 @@ async function runBounded(items, limit, work) {
66
73
  export class Orchestrator {
67
74
  deps;
68
75
  gateway;
76
+ requestRequirements = new RequestRequirementsResolver();
77
+ /** Per-attempt cap on forwarded live delta chunks (W-C4 flood guard, sol
78
+ * #10): past this the deltas are dropped and the cutoff is disclosed once;
79
+ * the complete message always still lands. */
80
+ static MAX_DELTAS_PER_ATTEMPT = 4000;
69
81
  constructor(deps) {
70
82
  this.deps = deps;
71
83
  this.gateway = new HarnessGateway(deps.registry);
@@ -83,9 +95,17 @@ export class Orchestrator {
83
95
  const resolved = this.resolveRunInput(input);
84
96
  // INV-062 at the ENGINE boundary: every surface fences prompts already,
85
97
  // 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");
98
+ // this entry without one. Prompts, per-run instructions, AND outputSchema
99
+ // are durable artifacts (all land in the TaskContract) the hard block
100
+ // applies here too, so no in-process path can ever bypass it. outputSchema
101
+ // rides the schema-aware branch: its property NAMES are field names (a
102
+ // `token` field is legitimate), but string VALUES (const/default/enum) are
103
+ // scanned for real secrets, matching the HTTP boundary exactly.
104
+ assertNoInlineSecretValues({
105
+ prompt: resolved.prompt,
106
+ instructions: resolved.instructions,
107
+ outputSchema: resolved.outputSchema ?? undefined,
108
+ }, "$", "run input");
89
109
  const parsedMode = ModeKindSchema.safeParse(resolved.mode ?? "agent");
90
110
  if (!parsedMode.success) {
91
111
  throw new Error(`unknown mode: ${String(resolved.mode)}`);
@@ -95,9 +115,39 @@ export class Orchestrator {
95
115
  // executor's plan steps — on any other mode it would be a silent no-op
96
116
  // knob. The CLI and control API validate this already; a direct embedder
97
117
  // must get the same loud refusal, not quiet acceptance.
98
- if (resolved.maxToolCalls !== undefined && resolved.maxToolCalls !== null && mode !== "orchestrate") {
118
+ if (resolved.maxToolCalls !== undefined &&
119
+ resolved.maxToolCalls !== null &&
120
+ mode !== "orchestrate") {
99
121
  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
122
  }
123
+ // denyPaths is enforced by the post-diff policy gate BEFORE delivery, which
124
+ // only exists on envelope/isolated runs — an in-place run mutates the live
125
+ // tree directly, so the gate could not contain a violation. Refuse loudly
126
+ // rather than accept a knob the engine cannot honor (INV-023).
127
+ if ((resolved.denyPaths?.length ?? 0) > 0 && resolved.inPlace === true) {
128
+ 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");
129
+ }
130
+ // outputSchema constrains the run's final ANSWER. It is honored exactly
131
+ // where a final answer is delivered (agent race incl. synthesis, and ask);
132
+ // every other strategy refuses loudly rather than carrying a contract the
133
+ // engine would not validate (INV-023). The schema itself is normalized for
134
+ // the native structured-output routes here at the boundary — unsupported
135
+ // shapes ($ref, non-object root) are a typed refusal, not a mid-run 400.
136
+ if (resolved.outputSchema !== undefined && resolved.outputSchema !== null) {
137
+ if (mode !== "agent" && mode !== "ask") {
138
+ throw new Error(`outputSchema constrains the final answer and applies to agent/ask runs (got mode=${mode}); drop the schema or switch modes`);
139
+ }
140
+ if (resolved.untilClean || (resolved.attempts !== undefined && resolved.attempts !== null)) {
141
+ 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");
142
+ }
143
+ // Shape-refuse unsupported schemas, then PROVE it compiles under the same
144
+ // ajv the engine validator uses — a malformed schema is a preflight
145
+ // refusal here (before any run dir), never a mid-run validator crash. The
146
+ // contract keeps the ORIGINAL (conformance authority); strictify is a
147
+ // transport-only transform applied per-lane in harnessSpecKnobs.
148
+ resolved.outputSchema = normalizeUserOutputSchema(resolved.outputSchema);
149
+ assertOutputSchemaCompiles(resolved.outputSchema);
150
+ }
101
151
  // P1: a versioned `mandatory_files` contract is enforced UNIFORMLY here, for
102
152
  // every mode, so the same repo state can't pass `run`/`ask` while failing
103
153
  // `audit`. No-op when the list is empty (the default) or for no-project runs.
@@ -119,7 +169,9 @@ export class Orchestrator {
119
169
  return this.runAsk(resolved, announce);
120
170
  case "audit":
121
171
  // `--swarm` selects the bounded read-only research swarm (old `explore`).
122
- return resolved.swarm ? this.runExplore(resolved, announce) : this.runAudit(resolved, announce);
172
+ return resolved.swarm
173
+ ? this.runExplore(resolved, announce)
174
+ : this.runAudit(resolved, announce);
123
175
  case "agent":
124
176
  // Engine strategies are FLAGS on agent (v0.9 collapse): `--until-clean`
125
177
  // and `--attempts` select the convergence loop; `--n` selects the race
@@ -149,73 +201,15 @@ export class Orchestrator {
149
201
  if (this.deps.reviewerPanel && this.deps.reviewerPanel.length > 0) {
150
202
  return this.resolveExplicitReviewerPanel(cwd, this.deps.reviewerPanel, runAuthPreference);
151
203
  }
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;
204
+ return resolveAutoReviewerPanel({
205
+ cwd,
206
+ registry: this.deps.registry,
207
+ harnessSettings: this.config(cwd)?.global.harnesses ?? {},
208
+ authPreferenceFor: (id) => this.authPreferenceForHarness(cwd, id, runAuthPreference),
209
+ }, {
210
+ reviewerModels: this.deps.reviewerModels,
211
+ reviewerEfforts: this.deps.reviewerEfforts,
212
+ });
219
213
  }
220
214
  /**
221
215
  * Resolve reviewers INSIDE a strategy, after run-dir creation: an explicit
@@ -285,31 +279,6 @@ export class Orchestrator {
285
279
  candidateIntent(input) {
286
280
  return input.create === true ? "create_from_scratch" : "implement";
287
281
  }
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
282
  /**
314
283
  * Session fields for a route's run spec: auth route preference + native
315
284
  * resume id. Preference precedence: explicit per-run > per-harness
@@ -324,8 +293,9 @@ export class Orchestrator {
324
293
  execRootOf(input) {
325
294
  return input.executionRoot ?? input.repoRoot;
326
295
  }
327
- sessionSpecFields(input, harnessId) {
296
+ sessionSpecFields(input, harnessId, log) {
328
297
  const cfg = this.config(input.repoRoot)?.global;
298
+ const profile = this.preflightProfile(input, harnessId, log);
329
299
  const explicit = (v) => (v && v !== "auto" ? v : undefined);
330
300
  return {
331
301
  // "auto" at ANY level falls through (thread turns send the thread default
@@ -334,22 +304,72 @@ export class Orchestrator {
334
304
  explicit(cfg?.harnesses?.[harnessId]?.auth_preference) ??
335
305
  explicit(cfg?.routing?.auth_preference) ??
336
306
  "auto",
337
- resume_session_id: input.resumeSessions?.[harnessId] ?? null,
307
+ resume_session_id: resumeSessionForProfile(input.resumeSessions?.[harnessId], profile),
308
+ credential_profile: profile,
338
309
  };
339
310
  }
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
- }
311
+ resolveCredentialProfile(input, harnessId) {
312
+ if (!input.credentialProfileId)
313
+ return null;
314
+ const registry = this.config(input.repoRoot)?.global.credential_profiles ?? [];
315
+ return resolveCredentialProfile(registry, input.credentialProfileId, harnessId);
316
+ }
317
+ /** The typed effective auth route for a SELECTED credential profile
318
+ * (round-18 #2): adapters execute strictly by credential_kind, so routing,
319
+ * billing classification, model truth, and quota lookup must share this
320
+ * one fact — never the default store's sources or a previous default-route
321
+ * metric. null = no profile selected or it does not resolve here. */
322
+ profileAuthRoute(input, harnessId) {
323
+ try {
324
+ const profile = this.resolveCredentialProfile(input, harnessId);
325
+ if (!profile)
326
+ return null;
327
+ return profile.credential_kind === "api_key" ? "api_key" : "local_session";
352
328
  }
329
+ catch {
330
+ return null;
331
+ }
332
+ }
333
+ profilePolicy(repoRoot, harnessId) {
334
+ const policy = this.config(repoRoot)?.global.harnesses?.[harnessId]?.profile_policy;
335
+ return policy ?? { limit_action: "fail", rotation_eligible: [], headroom_threshold: 0.9 };
336
+ }
337
+ /** null = no profile selected (default verdict stands); "available" = the
338
+ * profile's own probe admits the route; any other string = typed refusal. */
339
+ async profileAvailabilityOverride(input, harnessId) {
340
+ if (!input.credentialProfileId)
341
+ return null;
342
+ let profile;
343
+ try {
344
+ profile = this.resolveCredentialProfile(input, harnessId);
345
+ }
346
+ catch (err) {
347
+ return err instanceof Error ? err.message : String(err);
348
+ }
349
+ if (!profile)
350
+ return null;
351
+ const adapter = this.deps.registry.get(harnessId);
352
+ if (!adapter?.probeCredentialProfile) {
353
+ return `harness "${harnessId}" has no profile probe`;
354
+ }
355
+ const probe = await adapter.probeCredentialProfile(profile);
356
+ if (probe.availability === "available")
357
+ return "available";
358
+ return probe.detail ?? `${probe.availability}/${probe.verification}`;
359
+ }
360
+ preflightProfile(input, harnessId, log) {
361
+ const profile = this.resolveCredentialProfile(input, harnessId);
362
+ const policy = this.profilePolicy(input.repoRoot, harnessId);
363
+ const registry = this.config(input.repoRoot)?.global.credential_profiles ?? [];
364
+ const snapshots = this.deps.quotaSnapshots?.() ?? [];
365
+ const emit = (type, payload) => log?.emit(type, payload);
366
+ if (!profile) {
367
+ // Unpinned runs (INV-135 auto-balance): under `rotate`, a fresh
368
+ // default-subject headroom breach starts on the next eligible
369
+ // subscription profile instead; `fail`/`ask` change nothing.
370
+ return preflightDefaultSubject({ harnessId, policy, registry, snapshots, emit });
371
+ }
372
+ return preflightCredentialProfile({ profile, harnessId, policy, registry, snapshots, emit });
353
373
  }
354
374
  /**
355
375
  * Lift an adapter's auth-route override marker into the typed
@@ -361,7 +381,7 @@ export class Orchestrator {
361
381
  /**
362
382
  * Resolve candidate adapters: explicit `--harness`, else available real harnesses, then
363
383
  * **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
384
+ * a planner-only adapter with `implement: false` is dropped from an implement race), and
365
385
  * expand to n. Fails loudly if nothing can perform the intent.
366
386
  */
367
387
  resolveRunInput(input) {
@@ -420,16 +440,16 @@ export class Orchestrator {
420
440
  primaryHarness,
421
441
  model: undefined,
422
442
  models,
423
- portfolio: input.portfolio ??
424
- this.deps.portfolio ??
425
- cfg?.project.budget?.portfolio ??
426
- cfg?.global.default_portfolio ??
427
- "subscription-first",
443
+ routingGoal: input.routingGoal ??
444
+ this.deps.routingGoal ??
445
+ cfg?.project.budget?.routing_goal ??
446
+ cfg?.global.routing.goal ??
447
+ "auto",
428
448
  web,
429
449
  externalContextPolicy: web,
430
450
  };
431
451
  }
432
- async resolveCandidateAdapters(input, intent, ledger) {
452
+ async resolveCandidateAdapters(input, intent, ledger, routeContext) {
433
453
  let ids = input.harnesses;
434
454
  const explicitPool = Boolean(ids && ids.length > 0);
435
455
  const harnessSettings = this.config(input.repoRoot)?.global.harnesses ?? {};
@@ -442,22 +462,47 @@ export class Orchestrator {
442
462
  const statuses = probeIds.length > 0 ? await this.gateway.statusAll({ cwd: input.repoRoot }, probeIds) : [];
443
463
  const statusById = new Map(statuses.map((s) => [s.id, s]));
444
464
  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");
465
+ // INV-135 (round-18 BLOCK): an explicit credential profile NAMES its
466
+ // harness the implicit pool is exactly the profile's enabled
467
+ // harness(es) from the registry, and the profile probe (below) is the
468
+ // auth verdict. Deriving the pool from default doctor-OK status would
469
+ // exclude a valid profile whose default store is logged out while
470
+ // keeping unrelated harnesses that later fail profile resolution.
471
+ const profilePool = input.credentialProfileId
472
+ ? [
473
+ ...new Set((this.config(input.repoRoot)?.global.credential_profiles ?? [])
474
+ .filter((p) => p.enabled && p.profile_id === input.credentialProfileId)
475
+ .map((p) => p.harness_id)
476
+ .filter((hid) => !disabledHarnessIds.has(hid) && this.deps.registry.has(hid))),
477
+ ]
478
+ : [];
479
+ if (profilePool.length > 0) {
480
+ ids = profilePool;
481
+ }
482
+ else if (input.credentialProfileId) {
483
+ // Fable-checkpoint NIT: an unknown/disabled profile id must refuse
484
+ // HERE, not fall through to the default auto-pool — that would run on
485
+ // the DEFAULT credentials while the caller explicitly named an
486
+ // account, surfacing later as a per-harness "not registered" error.
487
+ const registered = (this.config(input.repoRoot)?.global.credential_profiles ?? []).filter((p) => p.profile_id === input.credentialProfileId);
488
+ throw new HarnessUnavailableError(registered.length === 0
489
+ ? `credential profile "${input.credentialProfileId}" is not registered (see \`claudexor profiles list\`)`
490
+ : registered.every((p) => !p.enabled)
491
+ ? `credential profile "${input.credentialProfileId}" is disabled`
492
+ : `credential profile "${input.credentialProfileId}" belongs to unavailable harness(es): ${registered.map((p) => p.harness_id).join(", ")}`);
493
+ }
494
+ else {
495
+ // Auto-pools take only doctor-OK harnesses (BIBLE §2: doctor decides
496
+ // readiness; a key string or degraded route is visible but not routable).
497
+ ids = statuses
498
+ .filter((s) => s.manifest?.kind !== "fake" && s.status === "ok" && s.enabledIntents.includes(intent))
499
+ .map((s) => s.id);
500
+ if (ids.length === 0) {
501
+ throw new HarnessUnavailableError("no doctor-ok harness for this mode; install/login codex/claude/cursor/opencode (see `claudexor doctor`), or pass --harness explicitly");
502
+ }
452
503
  }
453
504
  }
454
505
  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
506
  const pool = [];
462
507
  const dropped = [];
463
508
  for (const id of ids) {
@@ -484,7 +529,9 @@ export class Orchestrator {
484
529
  dropped.push(why);
485
530
  continue;
486
531
  }
487
- const status = statusById.get(id);
532
+ // W3.3 (TZ-1 §B): a route is admitted on readiness truth from the SAME
533
+ // resolved env/cwd its run will spawn with (see routeContext.ts).
534
+ let status = await candidateStatusInRouteContext(this.gateway, routeContext, id, this.authPreferenceForHarness(input.repoRoot, id, input.authPreference), statusById);
488
535
  const manifest = status?.manifest ?? null;
489
536
  if (!status || !manifest) {
490
537
  dropped.push(`${id} (unavailable)`);
@@ -496,14 +543,43 @@ export class Orchestrator {
496
543
  // unproven by isolated smoke) is admitted only by explicit user
497
544
  // selection — degraded means usable-with-caveats, and the caveats are
498
545
  // visible in doctor output and run events.
499
- if (status.status === "unavailable") {
500
- const why = `${id} is unavailable${status.reasons.length ? `: ${status.reasons.join("; ")}` : ""}`;
501
- if (explicitPool)
502
- throw new HarnessUnavailableError(why);
503
- dropped.push(why);
504
- continue;
546
+ // INV-135 (round-13, extended by the round-18 BLOCK): an EXPLICIT
547
+ // profile is authenticated by ITS store the profile probe overrides
548
+ // the default auth verdict for ANY non-ok default status, and a
549
+ // profile-admitted route joins even an AUTO pool (the run spawns with
550
+ // the profile's transport, so the default store's state is not the
551
+ // routing truth). Capability/manifest gating above still applies.
552
+ let profileAdmitted = false;
553
+ if (status.status !== "ok") {
554
+ const profileVerdict = await this.profileAvailabilityOverride(input, id);
555
+ if (profileVerdict === "available") {
556
+ // The default-store failure may have zeroed enabled_intents; with
557
+ // the profile probe as the auth verdict, MANIFEST capabilities are
558
+ // the honest intent truth (degraded = usable-with-caveats).
559
+ status = {
560
+ ...status,
561
+ status: "degraded",
562
+ enabledIntents: capabilityIntents(manifest.capabilities),
563
+ };
564
+ statusById.set(id, status);
565
+ profileAdmitted = true;
566
+ }
567
+ else if (profileVerdict !== null) {
568
+ const why = `${id} credential profile is not ready: ${profileVerdict}`;
569
+ if (explicitPool)
570
+ throw new HarnessUnavailableError(why);
571
+ dropped.push(why);
572
+ continue;
573
+ }
574
+ else if (status.status === "unavailable") {
575
+ const why = `${id} is unavailable${status.reasons.length ? `: ${status.reasons.join("; ")}` : ""}`;
576
+ if (explicitPool)
577
+ throw new HarnessUnavailableError(why);
578
+ dropped.push(why);
579
+ continue;
580
+ }
505
581
  }
506
- if (status.status !== "ok" && !explicitPool) {
582
+ if (status.status !== "ok" && !explicitPool && !profileAdmitted) {
507
583
  dropped.push(`${id} is ${status.status}${status.reasons.length ? `: ${status.reasons.join("; ")}` : ""}`);
508
584
  continue;
509
585
  }
@@ -512,18 +588,13 @@ export class Orchestrator {
512
588
  intent === "explain" ||
513
589
  intent === "audit" ||
514
590
  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
591
+ const requiredAccess = this.requestRequirements.adapterAccess(intent, manifest.capabilities.implementation_transport, readOnlyIntent
518
592
  ? "readonly"
519
- : (input.access ?? this.config(input.repoRoot).trust.access_default);
593
+ : (input.access ?? this.config(input.repoRoot).trust.access_default));
520
594
  const accessSupported = !requiredAccess || manifest.access_profiles_supported.includes(requiredAccess);
521
595
  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.
596
+ // Match routeSpecKnobs: a per-harness web default upgrades run-level auto,
597
+ // so judge the effective per-route policy.
527
598
  const routePolicy = policy === "auto" && cfgEntry?.web && cfgEntry.web !== "auto" ? cfgEntry.web : policy;
528
599
  const routeWebRequired = routePolicy === "cached" || routePolicy === "live";
529
600
  // Web policy is a capability: `off` needs an enforceable off state and a
@@ -541,34 +612,44 @@ export class Orchestrator {
541
612
  dropped.push(why);
542
613
  continue;
543
614
  }
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`;
615
+ const attachmentRefusal = this.requestRequirements.attachmentRefusal(id, input.attachments ?? [], manifest.capability_profile.attachment_inputs);
616
+ if (attachmentRefusal) {
548
617
  if (explicitPool)
549
- throw new HarnessUnavailableError(why);
550
- dropped.push(why);
618
+ throw new HarnessUnavailableError(attachmentRefusal);
619
+ dropped.push(attachmentRefusal);
551
620
  continue;
552
621
  }
553
622
  const reason = status.reasons.length > 0 ? `: ${status.reasons.join("; ")}` : "";
554
623
  if (status.enabledIntents.includes(intent) && accessSupported) {
555
624
  pool.push({
556
625
  adapter,
626
+ adapterAccess: requiredAccess,
557
627
  webSupport,
558
628
  providerFamily: manifest.provider_family,
559
629
  supportsMaxTurns: manifest.capabilities.max_turns,
560
630
  supportsToolLists: manifest.capabilities.tool_lists,
561
- supportsBrowser: manifest.capabilities.browser_tool,
631
+ browserRequirement: this.requestRequirements.resolveBrowser({
632
+ harnessId: id,
633
+ requested: input.browser === true,
634
+ manifestCapable: manifest.capabilities.browser_tool,
635
+ webPolicy: routePolicy,
636
+ access: requiredAccess,
637
+ }),
638
+ denyRequirement: this.requestRequirements.resolveDenyPaths(id, (input.denyPaths?.length ?? 0) > 0),
562
639
  effortLevels: manifest.capabilities.effort_levels,
563
640
  knownModels: manifest.capabilities.known_models,
641
+ // A selected profile's credential_kind IS the route (round-18 #2);
642
+ // the default store's sources apply only to profile-less runs.
643
+ authRouteEstimate: this.profileAuthRoute(input, id) ??
644
+ estimateEffectiveAuthRoute(this.authPreferenceForHarness(input.repoRoot, id, input.authPreference), status.authSources),
564
645
  supportsInteractive: manifest.capabilities.interactive,
565
646
  supportsJsonSchemaOutput: manifest.capabilities.json_schema_output,
647
+ implementationTransport: manifest.capabilities.implementation_transport,
566
648
  settings: cfgEntry
567
649
  ? {
568
650
  defaultModel: cfgEntry.default_model,
569
651
  effort: cfgEntry.effort,
570
652
  web: cfgEntry.web === "auto" ? null : cfgEntry.web,
571
- maxUsd: cfgEntry.max_usd,
572
653
  maxTurns: cfgEntry.max_turns,
573
654
  maxRounds: cfgEntry.max_rounds,
574
655
  toolsAllow: cfgEntry.tools_allow,
@@ -584,69 +665,116 @@ export class Orchestrator {
584
665
  if (pool.length === 0) {
585
666
  throw new HarnessUnavailableError(`no harness can perform '${intent}' for this mode${dropped.length ? ` (skipped: ${dropped.join(", ")})` : ""}`);
586
667
  }
587
- const ordered = this.orderPool(pool, input, statusById, ledger);
668
+ const ordered = this.orderPool(pool, input, intent, statusById, ledger);
669
+ if (ordered.length === 0) {
670
+ throw new HarnessUnavailableError(`no harness remains eligible for '${intent}' after budget and quota routing`);
671
+ }
588
672
  const n = input.n ?? ordered.length;
589
673
  const out = [];
590
674
  for (let i = 0; i < n; i++)
591
675
  out.push(ordered[i % ordered.length]);
676
+ this.requestRequirements.requireEffectiveBrowser(input.browser === true, out.map((lane) => lane.browserRequirement));
677
+ // outputSchema is MANDATORY (Quiz-6a): a selected lane that cannot
678
+ // natively constrain its final message would deliver best-effort text —
679
+ // that is a typed preflight refusal, never silent degradation. The
680
+ // interactive stream-json transport x --json-schema is an unverified
681
+ // vendor combination, so lanes that would ride it refuse too.
682
+ if (input.outputSchema !== undefined && input.outputSchema !== null) {
683
+ const incapable = out.filter((lane) => !lane.supportsJsonSchemaOutput);
684
+ if (incapable.length > 0) {
685
+ 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`);
686
+ }
687
+ // NOTE (DT2.1-16): the daemon ALWAYS arms an interaction channel, so an
688
+ // interactive-capable lane (claude) is refused for outputSchema on every
689
+ // daemon/CLI run today — the --json-schema x stream-json interactive combo
690
+ // is not yet live-verified. Structured-output runs therefore route through
691
+ // a non-interactive lane (codex). The message names that reality instead
692
+ // of pointing at a channel a daemon caller cannot turn off.
693
+ const interactive = Boolean(input.onInteraction)
694
+ ? out.filter((lane) => lane.supportsInteractive)
695
+ : [];
696
+ if (interactive.length > 0) {
697
+ 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`);
698
+ }
699
+ }
592
700
  // Strict pre-run model gate (INV-104) — see modelGovernance.ts.
593
701
  await assertRouteModelsAllowed(out, input.models, this.execRootOf(input));
594
702
  return out;
595
703
  }
596
704
  /**
597
- * Order the eligible pool by portfolio routing utility (budget router): an
705
+ * Order the eligible pool by the selected routing goal (budget router): an
598
706
  * explicit user pool keeps the user's order; an explicit primary harness is
599
707
  * always pinned first. Cross-family diversity is encouraged for later slots.
600
708
  */
601
- orderPool(pool, input, statusById, ledger) {
709
+ orderPool(pool, input, intent, statusById, ledger) {
602
710
  let ordered = pool;
603
- const explicitPool = Boolean(input.harnesses && input.harnesses.length > 0);
604
- if (!explicitPool && pool.length > 1) {
711
+ if (pool.length > 0) {
605
712
  const routeLedger = ledger ?? new BudgetLedger();
606
- const portfolio = input.portfolio ?? this.deps.portfolio ?? "subscription-first";
713
+ const config = this.config(input.repoRoot).global;
714
+ const goal = input.routingGoal ?? this.deps.routingGoal ?? config.routing.goal;
607
715
  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.
716
+ // Settled cost is evidence for economy routing, never a provider quality prior.
611
717
  const metrics = loadHarnessMetrics(globalConfigDir());
612
- const priors = this.config(input.repoRoot).global.routing.quality_priors;
613
718
  const remaining = pool.map((r) => {
614
719
  const authModes = statusById.get(r.adapter.id)?.manifest?.auth_modes ?? [];
615
720
  const metric = metrics[r.adapter.id];
721
+ // Auth mode for routing: prefer the ROUTE EVIDENCE from the
722
+ // last settled attempt (adapter-disclosed, persisted in metrics) over
723
+ // the manifest capability guess — auth_modes lists what a harness CAN
724
+ // use, not what it actually runs under.
725
+ const guessedAuthMode = authModes.includes("local_session")
726
+ ? "local_session"
727
+ : authModes.includes("api_key")
728
+ ? "api_key"
729
+ : "unknown";
730
+ // A selected profile's credential_kind decides the route outright
731
+ // (round-18 #2): an api_key profile must never inherit a
732
+ // subscription classification from the default store's metric.
733
+ const authMode = this.profileAuthRoute(input, r.adapter.id) ??
734
+ (input.authPreference === "api_key"
735
+ ? "api_key"
736
+ : input.authPreference === "subscription"
737
+ ? "local_session"
738
+ : (metric?.last_auth_mode ?? guessedAuthMode));
739
+ // The quota subject this candidate would actually run as (release
740
+ // wave round-16 #2): the resolved profile id, or null for the engine
741
+ // default — so profile A's cooldown never excludes profile B or the
742
+ // default. A profile that does not resolve for this harness routes
743
+ // as unknown (undefined) and stays conservatively any-subject.
744
+ let credentialSubjectId;
745
+ try {
746
+ credentialSubjectId =
747
+ this.resolveCredentialProfile(input, r.adapter.id)?.profile_id ?? null;
748
+ }
749
+ catch {
750
+ credentialSubjectId = undefined;
751
+ }
616
752
  return {
617
753
  harnessId: r.adapter.id,
618
- providerFamily: r.providerFamily,
619
754
  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,
755
+ model: input.models?.[r.adapter.id] ??
756
+ config.harnesses[r.adapter.id]?.default_model ??
757
+ undefined,
758
+ effort: input.effort ?? config.harnesses[r.adapter.id]?.effort ?? undefined,
759
+ billingKnowledge: authMode === "api_key" ? "metered" : "unknown",
760
+ incrementalCostUsd: authMode === "api_key" ? (metric?.avg_cost_usd ?? null) : null,
761
+ credentialRoute: authMode === "api_key"
762
+ ? "managed_api_key"
763
+ : authMode === "local_session"
764
+ ? "vendor_native"
765
+ : undefined,
766
+ credentialSubjectId,
628
767
  };
629
768
  });
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
- }
769
+ const ranked = rankHarnesses(remaining, {
770
+ goal,
771
+ paidFallback: config.routing.paid_fallback,
772
+ intent,
773
+ qualityTiers: config.routing.quality_tiers,
774
+ ledger: routeLedger,
775
+ })
776
+ .map((candidate) => byId.get(candidate.harnessId))
777
+ .filter((candidate) => Boolean(candidate));
650
778
  ordered = ranked;
651
779
  }
652
780
  if (input.primaryHarness) {
@@ -695,22 +823,6 @@ export class Orchestrator {
695
823
  "",
696
824
  ].join("\n");
697
825
  }
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
826
  /**
715
827
  * The web mode a routed harness actually executes for a requested policy.
716
828
  * Tools-permissioned web (e.g. claude) has no cached index: `cached` upgrades
@@ -762,7 +874,10 @@ export class Orchestrator {
762
874
  // turn (TurnEnqueueError.code), so surfaces key remedies on the CODE —
763
875
  // never on substring-matching this human message.
764
876
  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 });
877
+ `(${trustConfigPath(input.repoRoot)}); enable it with \`claudexor trust --allow-full-access\` — refusing to run unsandboxed`),
878
+ // Refusal semantics are born at the throw (W24): the one-time grant is
879
+ // a 403, and the daemon persists this status onto the job record.
880
+ { code: TRUST_FULL_ACCESS_CODE, status: 403 });
766
881
  }
767
882
  const externalContextPolicy = input.web ?? input.externalContextPolicy ?? "auto";
768
883
  // A frozen SpecPack's CONTENT reaches the contract (success criteria,
@@ -785,7 +900,7 @@ export class Orchestrator {
785
900
  repoRoot: input.repoRoot,
786
901
  mode,
787
902
  baseRef: input.baseRef,
788
- maxUsd: input.maxUsd,
903
+ paidBudget: input.paidBudget,
789
904
  });
790
905
  specFields = {
791
906
  success_criteria: fromSpec.success_criteria,
@@ -795,7 +910,12 @@ export class Orchestrator {
795
910
  task_graph: fromSpec.task_graph,
796
911
  constraints: fromSpec.constraints,
797
912
  };
798
- specTestCommands = fromSpec.tests.commands.map((test) => test.command);
913
+ specTestCommands = fromSpec.tests.commands.map(({ program, args, cwd, envAllowlist }) => ({
914
+ program,
915
+ args,
916
+ ...(cwd === undefined ? {} : { cwd }),
917
+ envAllowlist,
918
+ }));
799
919
  }
800
920
  catch (err) {
801
921
  // An unreadable/unfrozen spec must fail the run loudly, never silently
@@ -806,22 +926,18 @@ export class Orchestrator {
806
926
  // Deterministic gate commands come from the frozen SpecPack, explicit run
807
927
  // input, then versioned project config. Without these, gateSpecs is empty
808
928
  // 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 };
929
+ const resolvedGates = resolveContractGates({
930
+ repoRoot: input.repoRoot,
931
+ effectiveAccess,
932
+ config: cfg,
933
+ trustGrants: resolvedCfg.trust.test_command_grants,
934
+ specCommands: specTestCommands,
935
+ operatorCommands: input.tests ?? [],
936
+ projectCommands: cfg.tests?.commands ?? [],
822
937
  });
938
+ const commands = resolvedGates.commands;
823
939
  const protectedPaths = [...new Set(specFields.constraints?.protected_paths ?? [])];
824
- const autoProtectedPaths = [...new Set(gateProtectedPaths(commands.map((c) => c.command)))];
940
+ const autoProtectedPaths = resolvedGates.autoProtectedPaths;
825
941
  const protectedPathApprovals = [
826
942
  ...new Map([...(input.protectedPathApprovals ?? [])].map((approval) => [approval.path, approval])).values(),
827
943
  ];
@@ -832,6 +948,17 @@ export class Orchestrator {
832
948
  repo: { root: input.repoRoot, base_ref: input.baseRef ?? "HEAD", dirty_policy: "snapshot" },
833
949
  mode: { kind: mode },
834
950
  user_intent: { raw: redactSecrets(input.prompt) },
951
+ // Redacted for symmetry with user_intent.raw — a no-op on fenced input
952
+ // (the inline-secret fence already blocked any secret-like value at every
953
+ // ingress incl. this engine boundary), so task-producing lanes read back
954
+ // the real instructions via harnessSpecKnobs().
955
+ instructions: input.instructions === undefined ? undefined : redactSecrets(input.instructions),
956
+ // Already normalized/strictified at the engine boundary (run() refuses
957
+ // unsupported shapes before any run dir exists).
958
+ output_schema: input.outputSchema ?? null,
959
+ auth_preference: input.authPreference ?? "auto",
960
+ credential_profile_id: input.credentialProfileId ?? null,
961
+ max_turns: input.maxTurns ?? null,
835
962
  spec: input.specId || input.specHash || input.specPath
836
963
  ? {
837
964
  id: input.specId,
@@ -842,6 +969,7 @@ export class Orchestrator {
842
969
  ...specFields,
843
970
  constraints: {
844
971
  protected_paths: protectedPaths,
972
+ deny_paths: [...new Set(input.denyPaths ?? [])],
845
973
  auto_protected_paths: autoProtectedPaths,
846
974
  protected_path_approvals: protectedPathApprovals,
847
975
  },
@@ -866,11 +994,8 @@ export class Orchestrator {
866
994
  deny: [],
867
995
  },
868
996
  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),
997
+ routing_goal: input.routingGoal ?? this.deps.routingGoal ?? cfg?.budget?.routing_goal ?? "auto",
998
+ paid_budget: this.resolvePaidBudget(input.paidBudget, resolvedCfg),
874
999
  },
875
1000
  // The resolved harness-scoped model map (scalar already expanded to the
876
1001
  // primary by resolveRunInput). The contract is what route spec building
@@ -878,61 +1003,43 @@ export class Orchestrator {
878
1003
  routing_models: input.models ?? {},
879
1004
  });
880
1005
  }
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
1006
  /**
932
1007
  * Per-harness settings applied to one route's run spec (model/effort/web
933
1008
  * defaults, max_turns, tool lists). Knobs the manifest does not support are
934
1009
  * RETURNED as ignored reasons (disclosed by the caller), never silently sent.
935
1010
  */
1011
+ /**
1012
+ * The HarnessRunSpec fields every TASK-PRODUCING lane shares (primary,
1013
+ * candidate, planner, explorer, orchestrate-planner). Extracting the identical
1014
+ * block into ONE owner means a new task-producing field lands here once —
1015
+ * never forgotten in one of the HarnessRunSpec.parse sites (the multi-path
1016
+ * trap). Per-run `instructions` ride every task-producing lane but are withheld
1017
+ * from `synthesize` (a merge of existing candidates, not a fresh task
1018
+ * execution — owner Quiz-5a); reviewers and the auth smoke build their own
1019
+ * specs and never call this.
1020
+ */
1021
+ harnessSpecKnobs(contract, knobs, intent) {
1022
+ return {
1023
+ external_context_policy: knobs.webPolicy,
1024
+ tool_permission_policy: {
1025
+ web: knobs.webPolicy,
1026
+ allow: [...new Set([...contract.tool_permission_policy.allow, ...knobs.toolsAllow])],
1027
+ deny: [...new Set([...contract.tool_permission_policy.deny, ...knobs.toolsDeny])],
1028
+ },
1029
+ model_hint: knobs.model,
1030
+ effort_hint: knobs.effort,
1031
+ max_turns: knobs.maxTurns,
1032
+ ...(intent === "synthesize" ? {} : { instructions: contract.instructions }),
1033
+ // The user's answer contract rides every answer-producing lane INCLUDING
1034
+ // synthesis (its answer can become the final one); the orchestrate
1035
+ // planner owns its own plan schema instead (set at its spec site). The
1036
+ // adapter gets the vendor-STRICT transport form; the engine validator
1037
+ // keeps the ORIGINAL contract as the conformance authority.
1038
+ ...(intent !== "orchestrate" && contract.output_schema
1039
+ ? { output_schema: strictifyOutputSchema(contract.output_schema) }
1040
+ : {}),
1041
+ };
1042
+ }
936
1043
  routeSpecKnobs(routed, contract, overrideModel, effortHint) {
937
1044
  const s = routed.settings;
938
1045
  const contractPolicy = contract.external_context.policy;
@@ -940,11 +1047,13 @@ export class Orchestrator {
940
1047
  let maxTurns = null;
941
1048
  let toolsAllow = [];
942
1049
  let toolsDeny = [];
943
- if (s?.maxTurns) {
1050
+ // Run-level cap beats per-harness settings (specific beats general).
1051
+ const requestedMaxTurns = contract.max_turns ?? s?.maxTurns ?? null;
1052
+ if (requestedMaxTurns) {
944
1053
  if (routed.supportsMaxTurns)
945
- maxTurns = s.maxTurns;
1054
+ maxTurns = requestedMaxTurns;
946
1055
  else
947
- ignored.push(`max_turns=${s.maxTurns} (manifest capabilities.max_turns=false for ${routed.adapter.id})`);
1056
+ ignored.push(`max_turns=${requestedMaxTurns} (manifest capabilities.max_turns=false for ${routed.adapter.id})`);
948
1057
  }
949
1058
  if ((s?.toolsAllow.length ?? 0) > 0 || (s?.toolsDeny.length ?? 0) > 0) {
950
1059
  if (routed.supportsToolLists) {
@@ -981,43 +1090,38 @@ export class Orchestrator {
981
1090
  };
982
1091
  }
983
1092
  /** 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) {
1093
+ 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) {
985
1094
  const adapter = routed.adapter;
986
1095
  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.
1096
+ // In-place envelopes can resume native sessions; isolated scoped homes cannot,
1097
+ // so their session ids are never retained after disposal.
993
1098
  const inPlaceEnvelope = envelope.worktree_path === envelope.repo_root;
994
- const sessionFields = runInput ? this.sessionSpecFields(runInput, adapter.id) : undefined;
995
- const spec = HarnessRunSpec.parse({
1099
+ const rawContextPacket = await rawContextForEnvelope(routed.implementationTransport, envelope);
1100
+ const sessionFields = runInput ? this.sessionSpecFields(runInput, adapter.id, log) : undefined;
1101
+ let spec = HarnessRunSpec.parse({
996
1102
  session_id: newId("ses"),
997
1103
  intent,
998
1104
  prompt: promptWithProtectedPathConstraint(prompt, contract.constraints.protected_paths, contract.constraints.auto_protected_paths, contract.constraints.protected_path_approvals),
999
1105
  attachments: runInput?.attachments ?? [],
1000
- browser: this.browserSpecFor(runInput, routed, knobs.webPolicy, access, paths),
1106
+ browser: this.requestRequirements.browserSpec(routed.browserRequirement, join(paths.root, "browser")),
1001
1107
  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,
1108
+ access: routed.adapterAccess,
1109
+ ...this.harnessSpecKnobs(contract, knobs, intent),
1013
1110
  env_inheritance: envInheritance(this.config(contract.repo.root)),
1014
- ...(sessionFields ? { auth_preference: sessionFields.auth_preference } : {}),
1111
+ ...(sessionFields
1112
+ ? {
1113
+ auth_preference: sessionFields.auth_preference,
1114
+ credential_profile: sessionFields.credential_profile,
1115
+ }
1116
+ : {}),
1015
1117
  ...(inPlaceEnvelope && sessionFields?.resume_session_id
1016
1118
  ? { resume_session_id: sessionFields.resume_session_id }
1017
1119
  : {}),
1018
1120
  // Scoped harness home only for isolated envelopes; in-place runs use the
1019
1121
  // native environment so the resumed vendor session is actually reachable.
1020
1122
  ...(inPlaceEnvelope ? {} : { env: wsm.envFor(envelope) }),
1123
+ raw_context_packet: rawContextPacket,
1124
+ stream_deltas: streamDeltas,
1021
1125
  });
1022
1126
  if (!inPlaceEnvelope && runInput?.threadId && sessionFields?.resume_session_id) {
1023
1127
  log?.emit("session.rebound", SessionReboundLineageSchema.parse({
@@ -1034,15 +1138,19 @@ export class Orchestrator {
1034
1138
  const inactivityMs = harnessInactivityTimeoutMs(this.config(contract.repo.root));
1035
1139
  const attemptStartedMs = Date.now();
1036
1140
  const budgetSignalState = { quotaPressureDisclosed: false };
1141
+ const triedProfiles = new Set(); // W5.4 failover: each profile at most once
1037
1142
  let cost = 0;
1038
1143
  let costEstimated = false;
1039
1144
  let harnessErrored = false;
1145
+ // W-C4 delta flood budget (per attempt): counts forwarded delta chunks.
1146
+ let deltaCount = 0;
1147
+ let deltaCutoffDisclosed = false;
1040
1148
  const errors = [];
1041
- const messageParts = [];
1149
+ const answer = new AnswerAssembly();
1042
1150
  const retryPolicy = transientRetryPolicy(this.config(contract.repo.root));
1043
1151
  const telemetry = createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required ||
1044
1152
  knobs.webPolicy === "cached" ||
1045
- knobs.webPolicy === "live", effectiveWebMode ?? knobs.webPolicy);
1153
+ knobs.webPolicy === "live", effectiveWebMode ?? knobs.webPolicy, [routed.browserRequirement, routed.denyRequirement], knobs.model);
1046
1154
  let activeSessionId = spec.session_id;
1047
1155
  const onAbort = () => {
1048
1156
  void adapter.cancel?.(activeSessionId)?.catch(() => { });
@@ -1070,6 +1178,8 @@ export class Orchestrator {
1070
1178
  : attemptAbort.signal;
1071
1179
  activeSessionId = runSpec.session_id;
1072
1180
  const transientStart = telemetry.transientFailures.length;
1181
+ const rateLimitStart = telemetry.rateLimits.length;
1182
+ let rawPatch = null;
1073
1183
  try {
1074
1184
  const watched = withInactivityWatchdog(adapter.run(runSpec), {
1075
1185
  timeoutMs: inactivityMs,
@@ -1084,7 +1194,31 @@ export class Orchestrator {
1084
1194
  for await (const ev of watched) {
1085
1195
  if (signal?.aborted)
1086
1196
  break;
1197
+ rawPatch = captureRawPatchEnvelope(rawContextPacket !== null, rawPatch, ev);
1198
+ if (ev.type === "patch_produced")
1199
+ continue;
1087
1200
  const safeEv = redactHarnessEvent(ev);
1201
+ // W-C4 flood guard (review sol #10): a per-character delta stream
1202
+ // would otherwise persist/SSE one journal event PER CHUNK without
1203
+ // bound. Delta messages are DISPLAY-only (the complete message
1204
+ // still follows and carries the authoritative text), so past a
1205
+ // per-attempt budget we DROP further deltas and disclose the
1206
+ // cutoff ONCE — the final answer is unaffected.
1207
+ if (safeEv.type === "message" && safeEv.payload?.["delta"] === true) {
1208
+ deltaCount += 1;
1209
+ if (deltaCount > Orchestrator.MAX_DELTAS_PER_ATTEMPT) {
1210
+ if (!deltaCutoffDisclosed) {
1211
+ deltaCutoffDisclosed = true;
1212
+ log?.emit("harness.event", {
1213
+ harness_id: adapter.id,
1214
+ attempt_id: attemptId,
1215
+ type: "status",
1216
+ title: `live delta stream capped at ${Orchestrator.MAX_DELTAS_PER_ATTEMPT} chunks; the complete message still lands`,
1217
+ });
1218
+ }
1219
+ continue; // drop this delta; never journal past the budget
1220
+ }
1221
+ }
1088
1222
  safeInvoke(onHarnessEvent, safeEv);
1089
1223
  // In-place turns run in the live tree under the native environment, so
1090
1224
  // the session they emit IS reachable for the next turn: record it. An
@@ -1092,13 +1226,17 @@ export class Orchestrator {
1092
1226
  // deletes, so observing it would poison the thread resume map with
1093
1227
  // unreachable ids — skip it there.
1094
1228
  if (inPlaceEnvelope)
1095
- this.observeNativeSession(runInput, adapter.id, safeEv);
1229
+ observeNativeSessionEvent(runInput, adapter.id, safeEv);
1096
1230
  observeAuthSwitch(log, adapter.id, attemptId, safeEv);
1097
1231
  observeAttemptTelemetry(telemetry, safeEv);
1098
1232
  // Live plan checklist: forward the adapter's typed plan
1099
1233
  // progress as a run event (LAST WINS; the UI renders the latest).
1100
1234
  if (safeEv.plan_progress) {
1101
- log?.emit("plan.progress", { attempt_id: attemptId, harness_id: adapter.id, items: safeEv.plan_progress.items });
1235
+ log?.emit("plan.progress", {
1236
+ attempt_id: attemptId,
1237
+ harness_id: adapter.id,
1238
+ items: safeEv.plan_progress.items,
1239
+ });
1102
1240
  }
1103
1241
  if (safeEv.type === "usage" && safeEv.usage?.cost_usd) {
1104
1242
  cost += safeEv.usage.cost_usd;
@@ -1113,8 +1251,9 @@ export class Orchestrator {
1113
1251
  });
1114
1252
  // Mid-flight cap enforcement: the guard raises this attempt's hold
1115
1253
  // 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)) {
1254
+ // streaming candidate overshoot the paid budget until settlement.
1255
+ const valuationOnly = safeEv.usage.estimated === true && telemetry.authMode === "local_session";
1256
+ if (!valuationOnly && budgetGuard?.(cost)) {
1118
1257
  harnessErrored = true;
1119
1258
  errors.push("budget hard cap reached mid-attempt; stream aborted");
1120
1259
  log?.emit("budget.observation", {
@@ -1132,16 +1271,21 @@ export class Orchestrator {
1132
1271
  errors.push(redactSecrets(safeEv.error ?? safeEv.text ?? "harness emitted error"));
1133
1272
  }
1134
1273
  // 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
- }
1274
+ // has an honest output artifact; a TYPED final message wins verbatim.
1275
+ answer.observe(safeEv);
1141
1276
  // Observe ALL budget/quota signals (one codex usage event carries
1142
1277
  // BOTH spend and quota); pressure disclosed once per attempt.
1143
1278
  observeBudgetSignals(ledger, log, adapter.id, attemptId, safeEv, budgetSignalState);
1279
+ this.deps.quotaEventSink?.(adapter.id, safeEv);
1144
1280
  }
1281
+ if (rawContextPacket && !harnessErrored)
1282
+ await consumeRawPatchEnvelope({
1283
+ repoRoot: envelope.repo_root,
1284
+ worktreePath: envelope.worktree_path,
1285
+ baseCommitSha: envelope.base_sha ?? "HEAD",
1286
+ context: rawContextPacket,
1287
+ envelope: rawPatch,
1288
+ });
1145
1289
  }
1146
1290
  catch (err) {
1147
1291
  // A throwing adapter must not lose the cost already streamed: record the
@@ -1151,9 +1295,35 @@ export class Orchestrator {
1151
1295
  }
1152
1296
  const transient = telemetry.transientFailures.at(-1) ?? null;
1153
1297
  const sawTransient = telemetry.transientFailures.length > transientStart;
1298
+ const sawTypedLimit = telemetry.rateLimits.length > rateLimitStart;
1154
1299
  const currentDiff = await wsm.diff(envelope);
1155
- const currentAnswer = messageParts.join("\n").trim();
1300
+ const currentAnswer = answer.text();
1156
1301
  const deliverableEmpty = currentDiff.trim().length === 0 && currentAnswer.length === 0;
1302
+ // W5.4 failover: a typed-limit hit rebuilds the spec on a NEW vendor
1303
+ // session under the next profile with provenance (vendor_limit_rejected).
1304
+ if (harnessErrored && runInput && !signal?.aborted) {
1305
+ const rotated = rotateSpecOnTypedLimit({
1306
+ spec,
1307
+ harnessId: adapter.id,
1308
+ attemptId,
1309
+ policy: this.profilePolicy(contract.repo.root, adapter.id),
1310
+ registry: this.config(contract.repo.root)?.global.credential_profiles ?? [],
1311
+ snapshots: this.deps.quotaSnapshots?.() ?? [],
1312
+ triedProfiles,
1313
+ sawTypedLimit,
1314
+ deliverableEmpty,
1315
+ lastLimit: telemetry.rateLimits.at(-1) ?? null,
1316
+ emit: (type, payload) => log?.emit(type, payload),
1317
+ newSessionId: () => newId("ses"),
1318
+ defaultRouteWasVendorNative: routed.authRouteEstimate === "local_session",
1319
+ });
1320
+ if (rotated) {
1321
+ spec = rotated;
1322
+ errors.length = 0;
1323
+ harnessErrored = false;
1324
+ continue;
1325
+ }
1326
+ }
1157
1327
  if (!harnessErrored ||
1158
1328
  !sawTransient ||
1159
1329
  !deliverableEmpty ||
@@ -1194,7 +1364,7 @@ export class Orchestrator {
1194
1364
  errors.push(`web evidence unsatisfied: ${telemetry.web.errorSummary ?? (telemetry.web.attempted ? "web tool failed without verified recovery" : "web evidence required but never attempted")}`);
1195
1365
  }
1196
1366
  const diff = await wsm.diff(envelope);
1197
- const answerText = messageParts.join("\n").trim() || undefined;
1367
+ const answerText = answer.text() || undefined;
1198
1368
  const deliverablePresent = diff.trim().length > 0 || Boolean(answerText);
1199
1369
  // Cancelled attempts skip gates entirely: the operator asked to
1200
1370
  // stop NOW; running a 600s-per-gate suite after the abort delays the ack
@@ -1202,11 +1372,14 @@ export class Orchestrator {
1202
1372
  // still land, so partial work stays inspectable.
1203
1373
  const gateSignalAborted = signal?.aborted === true;
1204
1374
  if (!gateSignalAborted) {
1205
- log?.emit("gate.started", { attempt_id: attemptId, gates: this.gateSpecs(contract).length });
1375
+ log?.emit("gate.started", {
1376
+ attempt_id: attemptId,
1377
+ gates: gateSpecsFromContract(contract).length,
1378
+ });
1206
1379
  }
1207
1380
  const gates = gateSignalAborted
1208
1381
  ? []
1209
- : await runGates(this.gateSpecs(contract), {
1382
+ : await runGates(gateSpecsFromContract(contract), {
1210
1383
  cwd: envelope.worktree_path,
1211
1384
  env: wsm.envFor(envelope),
1212
1385
  signal,
@@ -1244,12 +1417,14 @@ export class Orchestrator {
1244
1417
  throw Object.assign(err instanceof Error ? err : new Error(String(err)), { costUsd: cost });
1245
1418
  }
1246
1419
  store.writeText(join(attemptDir, "patch.diff"), diff);
1247
- // Routing metrics (one owner in runSupport; clean attempts only).
1420
+ // Routing metrics (one owner in runSupport; clean attempts only
1421
+ // auth-route evidence recorded regardless).
1248
1422
  recordCleanAttemptMetrics(globalConfigDir(), adapter.id, {
1249
1423
  costUsd: cost,
1250
1424
  streamMs: attemptStreamEndedMs - attemptStartedMs,
1251
1425
  errored,
1252
1426
  aborted: signal?.aborted === true,
1427
+ authMode: telemetry.authMode,
1253
1428
  });
1254
1429
  const attemptDiffstat = diffStats(diff);
1255
1430
  store.writeYaml(join(attemptDir, "attempt.yaml"), {
@@ -1342,8 +1517,8 @@ export class Orchestrator {
1342
1517
  }
1343
1518
  /**
1344
1519
  * 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
1520
+ * initialized in place (`git init`, deterministic baseline commit) without
1521
+ * creating or editing `.gitignore`, and the action is announced via a
1347
1522
  * `project.git.initialized` event. Returns the failure message when the
1348
1523
  * boundary cannot be established (the terminal failure events are already
1349
1524
  * emitted); null on success.
@@ -1406,8 +1581,17 @@ export class Orchestrator {
1406
1581
  const wsm = new WorkspaceManager(execRoot);
1407
1582
  safeInvoke(input.onRunStart, { runId, taskId, runDir: paths.root });
1408
1583
  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() });
1584
+ const ledger = this.rootLedger(input, contract, log);
1585
+ announce?.({
1586
+ log,
1587
+ store,
1588
+ paths,
1589
+ runId,
1590
+ taskId,
1591
+ mode,
1592
+ phase: "race",
1593
+ spend: () => ledger.spend(),
1594
+ });
1411
1595
  store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
1412
1596
  log.emit("task.contract.created", { task_contract_hash: hashJson(contract) });
1413
1597
  // Write modes need a git boundary for worktree isolation and honest diffs.
@@ -1448,7 +1632,7 @@ export class Orchestrator {
1448
1632
  writeEvidencePacket(reviewDir, {
1449
1633
  userIntent: redactSecrets(input.prompt),
1450
1634
  diff: "(per-candidate diffs are supplied to reviewers individually)\n",
1451
- tests: this.testsEvidence(contract),
1635
+ tests: renderTestsEvidence(contract),
1452
1636
  });
1453
1637
  let adapters;
1454
1638
  try {
@@ -1487,7 +1671,6 @@ export class Orchestrator {
1487
1671
  return reviewersOutcome.failed;
1488
1672
  const reviewers = reviewersOutcome.reviewers;
1489
1673
  const reviewVerified = this.routeVerified(reviewers);
1490
- const harnessLedgers = new Map();
1491
1674
  const reviewEnvelopes = [];
1492
1675
  const disposeReviewEnvelopes = async () => {
1493
1676
  const envelopes = reviewEnvelopes.splice(0);
@@ -1497,25 +1680,17 @@ export class Orchestrator {
1497
1680
  const candidateAccess = contract.access.effective_profile;
1498
1681
  let budgetStopped = false;
1499
1682
  let softWarned = false;
1500
- // The USER-requested race width, before any budget trimming.
1501
1683
  const requestedSingleCandidate = adapters.length === 1;
1502
1684
  const slots = [];
1503
1685
  for (let i = 0; i < adapters.length; i++) {
1504
1686
  const routed = adapters[i];
1505
1687
  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({
1688
+ const lease = ledger.reserve({
1508
1689
  taskId,
1509
1690
  attemptId,
1510
1691
  intent: this.candidateIntent(input),
1511
1692
  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) } : {}),
1693
+ cost: attemptCostEvidence(routed.adapter.id, attemptId, i > 0 ? this.estimateUsdFloor(input.repoRoot) : undefined, this.routeBillingKnowledge(input, routed.adapter.id)),
1519
1694
  });
1520
1695
  log.emit("budget.lease.created", {
1521
1696
  granted: lease.granted,
@@ -1538,17 +1713,16 @@ export class Orchestrator {
1538
1713
  });
1539
1714
  }
1540
1715
  const runsBySlot = new Array(slots.length);
1541
- const slotLedger = (slot) => this.harnessLedger(harnessLedgers, ledger, slot.routed);
1542
1716
  const runSlot = async (slot, slotIdx) => {
1543
1717
  if (input.signal?.aborted) {
1544
- slotLedger(slot).cancel(slot.leaseId);
1718
+ ledger.cancel(slot.leaseId);
1545
1719
  return;
1546
1720
  }
1547
1721
  // Leases are granted upfront (before spend exists); a worker still
1548
1722
  // re-checks the circuit breaker so queued slots beyond the parallel wave
1549
1723
  // do not start after earlier candidates already blew the hard cap.
1550
- if (budgetStopped || slotLedger(slot).tier() === "hard") {
1551
- slotLedger(slot).cancel(slot.leaseId);
1724
+ if (budgetStopped || ledger.tier() === "hard") {
1725
+ ledger.cancel(slot.leaseId);
1552
1726
  log.emit("budget.lease.created", {
1553
1727
  granted: false,
1554
1728
  reason: "budget exhausted (hard cap reached)",
@@ -1563,7 +1737,7 @@ export class Orchestrator {
1563
1737
  // Soft + downgrade breaker (before the hard cap): soft = a one-time
1564
1738
  // warning; downgrade = run this attempt on the per-harness fallback_model
1565
1739
  // (cheaper) instead of hard-killing — gives fallback_model a real job.
1566
- const breakerTier = slotLedger(slot).tier();
1740
+ const breakerTier = ledger.tier();
1567
1741
  if (breakerTier === "soft" && !softWarned) {
1568
1742
  softWarned = true;
1569
1743
  log.emit("budget.observation", {
@@ -1598,28 +1772,24 @@ export class Orchestrator {
1598
1772
  baseRef: contract.repo.base_ref,
1599
1773
  dirtyPolicy: "snapshot",
1600
1774
  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,
1775
+ // Direct-workspace singletons run in place. Races and patch-envelope
1776
+ // transports stay isolated and adopt through the delivery service.
1777
+ inPlace: input.inPlace === true &&
1778
+ requestedSingleCandidate &&
1779
+ slot.routed.implementationTransport !== "git_patch_envelope",
1609
1780
  });
1610
1781
  const run = await this.runCandidateInEnvelope(slot.routed, envelope, slot.attemptId, slot.label, contract, input.prompt, store, paths, wsm, ledger, candidateAccess, (ev) => {
1611
1782
  const safeEv = redactHarnessEvent(ev);
1612
1783
  safeInvoke(input.onHarnessEvent, safeEv);
1613
1784
  log.emit("harness.event", harnessEventPayload(adapter.id, slot.attemptId, safeEv));
1614
1785
  }, 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")
1786
+ ledger.updateHold(slot.leaseId, streamedUsd);
1787
+ if (ledger.tier() !== "hard")
1618
1788
  return false;
1619
1789
  budgetStopped = true;
1620
1790
  return true;
1621
- }, input);
1622
- slotLedger(slot).settle(slot.leaseId, run.cost);
1791
+ }, input, requestedSingleCandidate);
1792
+ ledger.settle(slot.leaseId, attemptUsageCostSettlement(run.cost, run.costEstimated, run.attemptId, run.harnessId, run.telemetry.authMode));
1623
1793
  log.emit("harness.completed", {
1624
1794
  harness_id: adapter.id,
1625
1795
  attempt_id: slot.attemptId,
@@ -1637,8 +1807,10 @@ export class Orchestrator {
1637
1807
  // post-stream throw (e.g. the secret-token assertion) carries its
1638
1808
  // streamed spend on the error — settle the TRUE cost, never launder
1639
1809
  // real spend down to 0.
1640
- const carriedCost = typeof err?.costUsd === "number" ? err.costUsd : 0;
1641
- slotLedger(slot).settle(slot.leaseId, carriedCost);
1810
+ const carriedCost = typeof err?.costUsd === "number"
1811
+ ? err.costUsd
1812
+ : 0;
1813
+ ledger.settle(slot.leaseId, unknownCostSettlement("post-stream-error", carriedCost));
1642
1814
  const message = safeErrorMessage(err);
1643
1815
  // envelope is still undefined when wsm.create() itself threw — that is
1644
1816
  // a workspace-phase infrastructure failure, not a harness error.
@@ -1669,7 +1841,7 @@ export class Orchestrator {
1669
1841
  errored: true,
1670
1842
  costEstimated: false,
1671
1843
  errors: [message],
1672
- telemetry: createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required, effectiveWeb),
1844
+ telemetry: createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required, effectiveWeb, [slot.routed.browserRequirement, slot.routed.denyRequirement], knobs.model),
1673
1845
  infraPhase,
1674
1846
  };
1675
1847
  }
@@ -1702,10 +1874,14 @@ export class Orchestrator {
1702
1874
  }
1703
1875
  if (input.signal?.aborted) {
1704
1876
  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());
1877
+ return cancelledResult(log, runId, taskId, mode, paths.root, cancelledCandidates(), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, runs.map((r) => ({
1878
+ attemptId: r.attemptId,
1879
+ harnessId: r.harnessId,
1880
+ telemetry: r.telemetry,
1881
+ })), null), ledger.spend(), input.signal, store);
1706
1882
  }
1707
1883
  if (runs.length === 0) {
1708
- const status = budgetStopped ? "exhausted" : "failed";
1884
+ const status = ledger.terminal() ?? (budgetStopped ? "exhausted" : "failed");
1709
1885
  const why = budgetStopped
1710
1886
  ? "budget exhausted before any candidate run"
1711
1887
  : "no candidates produced";
@@ -1721,7 +1897,7 @@ export class Orchestrator {
1721
1897
  store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${mode})\n\n- Status: ${status}\n- Phase: budget\n\n${why}\n`);
1722
1898
  writeFailure(store, paths, {
1723
1899
  phase: "budget",
1724
- category: status === "exhausted" ? "budget" : "internal",
1900
+ category: isBudgetTerminal(status) ? "budget" : "internal",
1725
1901
  safeMessage: why,
1726
1902
  runDir: paths.root,
1727
1903
  });
@@ -1831,7 +2007,11 @@ export class Orchestrator {
1831
2007
  await disposeReviewEnvelopes();
1832
2008
  }
1833
2009
  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());
2010
+ return cancelledResult(log, runId, taskId, mode, paths.root, cancelledCandidates(), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, runs.map((r) => ({
2011
+ attemptId: r.attemptId,
2012
+ harnessId: r.harnessId,
2013
+ telemetry: r.telemetry,
2014
+ })), null), ledger.spend(), input.signal, store);
1835
2015
  }
1836
2016
  // Synthesis: if worthwhile, run a synthesizer as a NEW, re-checked candidate.
1837
2017
  const synth = decideSynthesis(evidences, input.synthesis ?? "auto");
@@ -1839,14 +2019,12 @@ export class Orchestrator {
1839
2019
  log.emit("synthesis.started", { synthesize: synth.synthesize, reason: synth.reason });
1840
2020
  if (synth.synthesize && !budgetStopped) {
1841
2021
  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({
2022
+ const lease = ledger.reserve({
1846
2023
  taskId,
1847
2024
  attemptId: "synth",
1848
2025
  intent: "synthesize",
1849
2026
  harnessId: synthRouted.adapter.id,
2027
+ cost: attemptCostEvidence(synthRouted.adapter.id, "synth", undefined, this.routeBillingKnowledge(input, synthRouted.adapter.id)),
1850
2028
  });
1851
2029
  if (lease.granted) {
1852
2030
  let envelope;
@@ -1868,19 +2046,23 @@ export class Orchestrator {
1868
2046
  accessProfile: candidateAccess,
1869
2047
  });
1870
2048
  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) => {
2049
+ const run = await this.runCandidateInEnvelope(synthRouted, envelope, "synth", "Synthesis", contract, synthPrompt, store, paths, wsm, ledger, candidateAccess, (ev) => {
1872
2050
  const safeEv = redactHarnessEvent(ev);
1873
2051
  safeInvoke(input.onHarnessEvent, safeEv);
1874
2052
  log.emit("harness.event", harnessEventPayload(synthAdapter.id, "synth", safeEv));
1875
2053
  }, 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);
2054
+ ledger.settle(lease.lease?.lease_id ?? "", attemptUsageCostSettlement(run.cost, run.costEstimated, run.attemptId, run.harnessId, run.telemetry.authMode));
1877
2055
  reviewEnvelopes.push(envelope);
1878
2056
  envelope = undefined;
1879
2057
  try {
1880
2058
  const synthEvidence = await this.reviewRuns([run], reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal);
1881
2059
  evidences.push(...synthEvidence);
1882
2060
  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());
2061
+ return cancelledResult(log, runId, taskId, mode, paths.root, cancelledCandidates(), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, runs.map((r) => ({
2062
+ attemptId: r.attemptId,
2063
+ harnessId: r.harnessId,
2064
+ telemetry: r.telemetry,
2065
+ })), null), ledger.spend(), input.signal, store);
1884
2066
  }
1885
2067
  }
1886
2068
  finally {
@@ -1890,7 +2072,7 @@ export class Orchestrator {
1890
2072
  workingRuns.push(run);
1891
2073
  }
1892
2074
  catch (err) {
1893
- synthLedger.settle(lease.lease?.lease_id ?? "", 0);
2075
+ ledger.settle(lease.lease?.lease_id ?? "", unknownCostSettlement("synthesis-error"));
1894
2076
  log.emit("harness.completed", {
1895
2077
  attempt_id: "synth",
1896
2078
  status: "failed",
@@ -1904,7 +2086,11 @@ export class Orchestrator {
1904
2086
  }
1905
2087
  }
1906
2088
  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());
2089
+ return cancelledResult(log, runId, taskId, mode, paths.root, cancelledCandidates(), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, runs.map((r) => ({
2090
+ attemptId: r.attemptId,
2091
+ harnessId: r.harnessId,
2092
+ telemetry: r.telemetry,
2093
+ })), null), ledger.spend(), input.signal, store);
1908
2094
  }
1909
2095
  let result;
1910
2096
  try {
@@ -1933,35 +2119,35 @@ export class Orchestrator {
1933
2119
  ? (evidences.find((e) => e.attemptId === winnerRun.attemptId)?.reviewVerified ?? false)
1934
2120
  : evidences.length > 0 && evidences.every((e) => e.reviewVerified);
1935
2121
  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.
2122
+ if (status === "success" && ledger.terminal() !== null)
2123
+ status = ledger.terminal();
2124
+ // FinalVerifier blocks adoption until the patch and gates pass on a fresh base.
1940
2125
  let finalVerify = null;
1941
2126
  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;
2127
+ let deliveryFailureReason = null;
2128
+ let raceDeliveryReceipt = null;
2129
+ // A single in-place turn already mutated its execution tree; race adoption
2130
+ // instead defers verification until immediately before delivery.
2131
+ const inPlaceWinner = winnerRun?.reviewCwd === execRoot;
2132
+ const deferredRaceVerify = input.inPlace === true && !inPlaceWinner;
1948
2133
  if (winnerRun &&
1949
2134
  !inPlaceWinner &&
2135
+ !deferredRaceVerify &&
1950
2136
  winnerRun.diff.trim().length > 0 &&
1951
2137
  (status === "success" || status === "ungated") &&
1952
2138
  !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.
2139
+ finalVerify = await finalVerifyPatch(execRoot, winnerRun, gateSpecsFromContract(contract), log);
2140
+ // Verify errors block like proven failures; accept_risk stays available.
1956
2141
  finalVerifyFailed = finalVerifyBlocks(finalVerify);
1957
2142
  if (finalVerifyFailed)
1958
2143
  status = "blocked";
1959
2144
  }
1960
2145
  store.writeYaml(join(paths.arbitrationDir, "decision.yaml"), {
1961
2146
  ...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) : {}),
2147
+ // A blocked terminal overrides the persisted green arbitration fields.
2148
+ ...(status === "blocked"
2149
+ ? blockedDecisionOverride(result.decision.evidence_facts, finalVerify)
2150
+ : {}),
1965
2151
  review_verified: actualReviewVerified,
1966
2152
  final_verify: finalVerify,
1967
2153
  });
@@ -1977,45 +2163,55 @@ export class Orchestrator {
1977
2163
  const blockers = winnerEvidence
1978
2164
  ? winnerEvidence.findings.filter((f) => isBlocking(f)).length
1979
2165
  : 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").
2166
+ // Prose from an empty-diff winner is an answer, never a patch.
1982
2167
  const winnerAnswer = winnerRun.answerText?.trim() ?? "";
1983
2168
  const resultKind = hasDiff ? "patch" : winnerAnswer.length > 0 ? "answer" : "none";
1984
- if (!hasDiff && winnerAnswer.length > 0) {
2169
+ // The winner's final MESSAGE is the human-facing answer and materializes
2170
+ // for diff-ful runs too: the chat renders final/answer.md (the projection
2171
+ // prefers it), never the arbitration summary — "Run … Winner: a01 …" is
2172
+ // machine telemetry, not what the agent said. The diff stays in the
2173
+ // Diff tab; summary.md remains a diagnostics artifact.
2174
+ if (winnerAnswer.length > 0) {
1985
2175
  store.writeText(join(paths.finalDir, "answer.md"), winnerAnswer + "\n");
1986
2176
  }
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";
2177
+ // The run's structured-output contract: ONE engine validator, called on
2178
+ // the winner's answer regardless of diff presence (a non-conformant
2179
+ // answer stays success-with-warnings; the receipt is the truth).
2180
+ if (contract.output_schema) {
2181
+ finalizeStructuredOutput({
2182
+ store,
2183
+ finalDir: paths.finalDir,
2184
+ log,
2185
+ schema: contract.output_schema,
2186
+ answerText: winnerAnswer,
2187
+ });
2188
+ }
2189
+ // Only a fully verified success may auto-adopt; ungated remains an artifact.
2190
+ const adoptable = status === "success";
2001
2191
  let adopted = null;
2002
2192
  let applyState = "not_applied";
2003
2193
  let postTurnSha = null;
2194
+ let revertAnchorId = null;
2004
2195
  if (input.inPlace === true && hasDiff) {
2005
- if (requestedSingleCandidate) {
2196
+ if (inPlaceWinner) {
2006
2197
  // Already live: the candidate ran in-place and wrote the tree itself.
2007
2198
  adopted = true;
2008
2199
  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.
2200
+ // The pre-review fence excludes later user edits from the target.
2011
2201
  postTurnSha = earlyPostTurnSha;
2012
2202
  }
2013
2203
  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) {
2204
+ // Protected apply preserves the live tree or reports tree_mutated.
2205
+ const applied = await verifyAndDeliver(execRoot, winnerRun.diff, { mode: "apply", protectedApply: true }, gateSpecsFromContract(contract), (freshVerify) => {
2206
+ finalVerify = freshVerify;
2207
+ return finalVerifyBlocks(freshVerify)
2208
+ ? (freshVerify.reason ?? "final verify failed before race adoption")
2209
+ : null;
2210
+ }, log);
2211
+ raceDeliveryReceipt = applied;
2212
+ store.writeYaml(join(paths.finalDir, "delivery_receipt.yaml"), applied);
2213
+ finalVerify = applied.finalVerify;
2214
+ if (applied.applied) {
2019
2215
  adopted = true;
2020
2216
  applyState = "applied";
2021
2217
  log.emit("work_product.adopted", {
@@ -2023,17 +2219,21 @@ export class Orchestrator {
2023
2219
  patch_sha256: patchSha256,
2024
2220
  winner: winnerRun.attemptId,
2025
2221
  });
2026
- // Race winner: snapshot immediately after applying (minimal window).
2027
2222
  try {
2028
2223
  postTurnSha = await snapshotTree(execRoot);
2029
2224
  }
2030
2225
  catch {
2031
2226
  postTurnSha = null;
2032
2227
  }
2228
+ revertAnchorId = createRevertAnchorFromPatchOrNull(execRoot, winnerRun.diff);
2033
2229
  }
2034
2230
  else {
2035
2231
  adopted = false;
2036
2232
  applyState = "not_applied";
2233
+ deliveryFailureReason = applied.detail ?? "race adoption delivery was refused";
2234
+ status = "blocked";
2235
+ if (finalVerifyBlocks(finalVerify))
2236
+ finalVerifyFailed = true;
2037
2237
  log.emit("work_product.adopted", {
2038
2238
  applied: false,
2039
2239
  patch_sha256: patchSha256,
@@ -2043,18 +2243,30 @@ export class Orchestrator {
2043
2243
  }
2044
2244
  }
2045
2245
  }
2246
+ writeRaceDeliveryDecision(store, decisionPath, {
2247
+ decision: result.decision,
2248
+ status,
2249
+ reviewVerified: actualReviewVerified,
2250
+ finalVerify,
2251
+ deliveryFailureReason,
2252
+ deliveryReceiptPath: raceDeliveryReceipt ? "final/delivery_receipt.yaml" : null,
2253
+ });
2254
+ if (inPlaceWinner && requestedSingleCandidate && adopted === true) {
2255
+ revertAnchorId = await createRevertAnchorOrNull(execRoot, preTurnSha, postTurnSha);
2256
+ }
2046
2257
  store.writeYaml(join(paths.finalDir, "work_product.yaml"), {
2047
2258
  id: newId("wp"),
2048
2259
  kind: input.create === true ? "new_repo" : "patch",
2049
2260
  source_task_id: taskId,
2050
2261
  producer_attempt_id: winnerRun.attemptId,
2262
+ ...(raceDeliveryReceipt
2263
+ ? { files: { delivery_receipt: "final/delivery_receipt.yaml" } }
2264
+ : {}),
2051
2265
  meta: {
2052
2266
  harness_id: winnerRun.harnessId,
2053
2267
  synthesis: synth,
2054
2268
  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).
2269
+ // Artifact-only apply reads the same terminal status as the daemon.
2058
2270
  status,
2059
2271
  review_verified: actualReviewVerified,
2060
2272
  budget_stopped: budgetStopped,
@@ -2070,6 +2282,7 @@ export class Orchestrator {
2070
2282
  apply_state: applyState,
2071
2283
  pre_turn_sha: preTurnSha,
2072
2284
  post_turn_sha: postTurnSha,
2285
+ revert_anchor_id: revertAnchorId,
2073
2286
  },
2074
2287
  });
2075
2288
  store.writeText(join(paths.finalDir, "summary.md"), renderSummary(runId, mode, { ...result.decision, status }, evidences, synth.reason, actualReviewVerified));
@@ -2084,7 +2297,10 @@ export class Orchestrator {
2084
2297
  ? result.decision.winner
2085
2298
  : (winnerRun?.attemptId ?? null));
2086
2299
  const honestTerminal = status === "no_op" || status === "ungated" || status === "review_not_run";
2087
- if (finalVerifyFailed) {
2300
+ if (deliveryFailureReason && !finalVerifyFailed) {
2301
+ writeFailure(store, paths, deliveryRefusalFailure(deliveryFailureReason, paths.root));
2302
+ }
2303
+ else if (finalVerifyFailed) {
2088
2304
  writeFailure(store, paths, {
2089
2305
  phase: "verification",
2090
2306
  // RunFailure.category is a closed enum; "validation" is the honest
@@ -2112,7 +2328,7 @@ export class Orchestrator {
2112
2328
  ? "policy"
2113
2329
  : winnerRun?.errored
2114
2330
  ? "harness_error"
2115
- : status === "exhausted"
2331
+ : isBudgetTerminal(status)
2116
2332
  ? "budget"
2117
2333
  : "internal",
2118
2334
  harnessId: winnerRun?.errored ? winnerRun.harnessId : undefined,
@@ -2154,7 +2370,11 @@ export class Orchestrator {
2154
2370
  // phase "verification", not "review").
2155
2371
  log.emit("run.blocked", {
2156
2372
  status,
2157
- phase: finalVerifyFailed ? "verification" : "review",
2373
+ phase: deliveryFailureReason && !finalVerifyFailed
2374
+ ? "delivery"
2375
+ : finalVerifyFailed
2376
+ ? "verification"
2377
+ : "review",
2158
2378
  failure_ref: "final/failure.yaml",
2159
2379
  });
2160
2380
  }
@@ -2181,156 +2401,27 @@ export class Orchestrator {
2181
2401
  }
2182
2402
  /** Single-owner telemetry artifact (final/telemetry.yaml); surfaces project it, never recompute. */
2183
2403
  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,
2404
+ writeRunTelemetryArtifact({
2405
+ store,
2406
+ finalDir: paths.finalDir,
2407
+ contract,
2408
+ runId,
2409
+ taskId,
2193
2410
  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(),
2411
+ attempts,
2412
+ finalAttemptId,
2413
+ resolveAuthPreference: (harnessId) => this.authPreferenceForHarness(contract.repo.root, harnessId, contract.auth_preference),
2204
2414
  });
2205
- store.writeYaml(join(paths.finalDir, "telemetry.yaml"), telemetry);
2206
2415
  }
2207
2416
  /** 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
2417
  /**
2327
2418
  * 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).
2419
+ * scoped harness HOME (Bible §6) so reviewer scratch state and injected auth
2420
+ * routes do not enter the project or ordinary operator HOME. Native
2421
+ * Codex/Claude routes deliberately keep their vendor-owned host-user stores;
2422
+ * no credential file is copied into the scoped home. Every call site MUST go
2423
+ * through here so the non-native scoping cannot drift. Disposed once the
2424
+ * panel settles (resolve OR reject).
2334
2425
  */
2335
2426
  reviewScoped(input) {
2336
2427
  const reviewHome = new WorkspaceManager(input.cwd).readOnlyHomeEnv();
@@ -2347,7 +2438,7 @@ export class Orchestrator {
2347
2438
  const candidateCwd = run.reviewCwd ?? cwd;
2348
2439
  const candidateEvidenceDir = this.prepareReviewEvidenceDir(reviewDir, candidateCwd);
2349
2440
  try {
2350
- this.writeTestsEvidence(candidateEvidenceDir, contract, run.gates);
2441
+ writeText(join(candidateEvidenceDir, "TESTS.txt"), renderTestsEvidence(contract, run.gates).trim() + "\n");
2351
2442
  // a candidate that changed NO files has nothing to review — never
2352
2443
  // spend a reviewer panel on "(empty diff)" (a trivial greeting in agent mode used to
2353
2444
  // cost two reviewers). It still flows through policy gates and arbitration
@@ -2360,6 +2451,7 @@ export class Orchestrator {
2360
2451
  attemptId: run.attemptId,
2361
2452
  intent: "review",
2362
2453
  harnessId: "review-panel",
2454
+ cost: attemptCostEvidence("review-panel", run.attemptId),
2363
2455
  })
2364
2456
  : undefined;
2365
2457
  const result = hasDiff && reviewers.length > 0 && (reviewLease?.granted ?? true)
@@ -2387,7 +2479,7 @@ export class Orchestrator {
2387
2479
  reviewSpendEstimated: false,
2388
2480
  };
2389
2481
  if (reviewLease?.granted) {
2390
- ledger?.settle(reviewLease.lease?.lease_id ?? "", result.reviewSpendUsd ?? 0);
2482
+ ledger?.settle(reviewLease.lease?.lease_id ?? "", usageCostSettlement(result.reviewSpendUsd, result.reviewSpendEstimated, "review-usage", [`attempt:${run.attemptId}`, "review:panel"]));
2391
2483
  if ((result.reviewSpendUsd ?? 0) > 0) {
2392
2484
  log.emit("budget.observation", {
2393
2485
  harness_id: "review-panel",
@@ -2417,7 +2509,7 @@ export class Orchestrator {
2417
2509
  // Mirrors the convergence path (actualReviewVerified).
2418
2510
  const candidateReviewVerified = reviewVerified && result.crossFamilyHealthy && result.crossFamilyVerified;
2419
2511
  // 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);
2512
+ const policy = policyFindings(run, candidateReviewVerified, contract.constraints.protected_paths, contract.constraints.auto_protected_paths, contract.constraints.protected_path_approvals, contract.constraints.deny_paths);
2421
2513
  const allFindings = [...policy.findings, ...revalidated];
2422
2514
  const inconclusive = allFindings.some((f) => f.severity === "INSUFFICIENT_EVIDENCE" || f.status === "insufficient_evidence");
2423
2515
  const noBlockers = !allFindings.some((f) => isBlocking(f));
@@ -2448,23 +2540,15 @@ export class Orchestrator {
2448
2540
  }
2449
2541
  return evidences;
2450
2542
  }
2451
- prepareReviewEvidenceDir(sourceDir, candidateCwd) {
2452
- const targetDir = join(candidateCwd, REVIEW_EVIDENCE_DIRNAME);
2453
- if (sourceDir === targetDir) {
2454
- return this.requireReviewEvidence(targetDir);
2455
- }
2543
+ prepareReviewEvidenceDir(sourceDir, _candidateCwd) {
2544
+ // Evidence is an external runtime artifact. ReviewEngine builds a separate
2545
+ // reviewer workspace and copies the packet there; writing/copying it into
2546
+ // the candidate tree would contaminate the Git diff and, worse, overwrite a
2547
+ // user-owned path with the same name.
2456
2548
  if (!existsSync(sourceDir)) {
2457
2549
  throw new Error(`review evidence preflight failed for ${sourceDir}: source packet missing`);
2458
2550
  }
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);
2551
+ return this.requireReviewEvidence(sourceDir);
2468
2552
  }
2469
2553
  requireReviewEvidence(dir) {
2470
2554
  const result = preflightEvidence(dir);
@@ -2474,20 +2558,9 @@ export class Orchestrator {
2474
2558
  const empty = result.empty.length ? `empty=${result.empty.join(",")}` : "";
2475
2559
  throw new Error(`review evidence preflight failed for ${dir}: ${[missing, empty].filter(Boolean).join(" ")}`);
2476
2560
  }
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
- }
2561
+ cleanupReviewEvidenceDir(_candidateEvidenceDir, _candidateCwd) {
2562
+ // No candidate-tree packet exists in v2; external runtime retention is
2563
+ // governed by the artifact/journal lifecycle rather than best-effort rm.
2491
2564
  return null;
2492
2565
  }
2493
2566
  recordReviewEvidenceCleanup(store, metadataPath, attemptId, candidateEvidenceDir, candidateCwd) {
@@ -2521,11 +2594,20 @@ export class Orchestrator {
2521
2594
  const execRoot = this.execRootOf(input);
2522
2595
  const wsm = new WorkspaceManager(execRoot);
2523
2596
  const readiness = new ReadinessLedger();
2524
- const ledger = new BudgetLedger({ maxUsd: contract.budget.max_usd ?? null });
2597
+ const ledger = this.rootLedger(input, contract, log);
2525
2598
  store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
2526
2599
  safeInvoke(input.onRunStart, { runId, taskId, runDir: paths.root });
2527
2600
  log.emit("run.created", { mode, prompt: redactSecrets(input.prompt) });
2528
- announce?.({ log, store, paths, runId, taskId, mode, phase: "convergence", spend: () => ledger.spend() });
2601
+ announce?.({
2602
+ log,
2603
+ store,
2604
+ paths,
2605
+ runId,
2606
+ taskId,
2607
+ mode,
2608
+ phase: "convergence",
2609
+ spend: () => ledger.spend(),
2610
+ });
2529
2611
  // Live (in-place) isolation deliberately tolerates non-git stateful
2530
2612
  // environments; only envelope isolation needs the git boundary.
2531
2613
  if (!input.inPlace) {
@@ -2548,7 +2630,7 @@ export class Orchestrator {
2548
2630
  writeEvidencePacket(reviewDir, {
2549
2631
  userIntent: redactSecrets(input.prompt),
2550
2632
  diff: "(per-attempt)\n",
2551
- tests: this.testsEvidence(contract),
2633
+ tests: renderTestsEvidence(contract),
2552
2634
  });
2553
2635
  const reviewersOutcome = await this.resolveReviewersWithArtifacts(input, log, store, paths, runId, taskId, mode);
2554
2636
  if ("failed" in reviewersOutcome)
@@ -2558,7 +2640,8 @@ export class Orchestrator {
2558
2640
  // One envelope carried forward across attempts so the harness can repair its own work.
2559
2641
  let adapterPool;
2560
2642
  try {
2561
- adapterPool = await this.resolveCandidateAdapters({ ...input, n: undefined }, this.candidateIntent(input));
2643
+ adapterPool = await this.resolveCandidateAdapters({ ...input, n: undefined }, this.candidateIntent(input), ledger);
2644
+ this.requestRequirements.assertConvergenceWorkspace(input.inPlace === true, adapterPool);
2562
2645
  }
2563
2646
  catch (err) {
2564
2647
  const message = safeErrorMessage(err);
@@ -2655,7 +2738,6 @@ export class Orchestrator {
2655
2738
  const stallThreshold = input.untilClean === true ? 4 : 2;
2656
2739
  const allCooledDown = () => adapterPool.every((a) => ledger.cooldownActive(a.adapter.id));
2657
2740
  const attemptTelemetries = [];
2658
- const harnessLedgers = new Map();
2659
2741
  let lastDiffStable = true;
2660
2742
  let reviewSpendEstimated = false;
2661
2743
  try {
@@ -2707,12 +2789,12 @@ export class Orchestrator {
2707
2789
  exhausted = true;
2708
2790
  break;
2709
2791
  }
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({
2792
+ const lease = ledger.reserve({
2712
2793
  taskId,
2713
2794
  attemptId,
2714
2795
  intent: "repair",
2715
2796
  harnessId: adapter.id,
2797
+ cost: attemptCostEvidence(adapter.id, attemptId, undefined, this.routeBillingKnowledge(input, adapter.id)),
2716
2798
  });
2717
2799
  if (!lease.granted) {
2718
2800
  exhausted = true;
@@ -2733,11 +2815,10 @@ export class Orchestrator {
2733
2815
  safeInvoke(input.onHarnessEvent, safeEv);
2734
2816
  log.emit("harness.event", harnessEventPayload(adapter.id, attemptId, safeEv));
2735
2817
  }, 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);
2818
+ ledger.updateHold(lease.lease?.lease_id ?? "", streamedUsd);
2819
+ return ledger.tier() === "hard";
2820
+ }, input, true);
2821
+ ledger.settle(lease.lease?.lease_id ?? "", attemptUsageCostSettlement(run.cost, run.costEstimated, run.attemptId, run.harnessId, run.telemetry.authMode));
2741
2822
  log.emit("harness.completed", {
2742
2823
  harness_id: adapter.id,
2743
2824
  attempt_id: attemptId,
@@ -2749,7 +2830,7 @@ export class Orchestrator {
2749
2830
  catch (err) {
2750
2831
  // Envelope/setup failure before the stream; stream errors are absorbed
2751
2832
  // inside runCandidateInEnvelope with their real accumulated cost.
2752
- this.harnessLedger(harnessLedgers, ledger, routed).settle(lease.lease?.lease_id ?? "", 0);
2833
+ ledger.settle(lease.lease?.lease_id ?? "", unknownCostSettlement("attempt-error"));
2753
2834
  log.emit("harness.completed", {
2754
2835
  harness_id: adapter.id,
2755
2836
  attempt_id: attemptId,
@@ -2766,7 +2847,7 @@ export class Orchestrator {
2766
2847
  errored: true,
2767
2848
  costEstimated: false,
2768
2849
  errors: [safeErrorMessage(err)],
2769
- telemetry: createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required, effectiveWeb),
2850
+ telemetry: createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required, effectiveWeb, [routed.browserRequirement, routed.denyRequirement], knobs.model),
2770
2851
  };
2771
2852
  }
2772
2853
  lastRun = run;
@@ -2790,7 +2871,7 @@ export class Orchestrator {
2790
2871
  const candidateReviewCwd = run.reviewCwd ?? input.repoRoot;
2791
2872
  const candidateReviewEvidenceDir = this.prepareReviewEvidenceDir(reviewDir, candidateReviewCwd);
2792
2873
  try {
2793
- this.writeTestsEvidence(candidateReviewEvidenceDir, contract, run.gates);
2874
+ writeText(join(candidateReviewEvidenceDir, "TESTS.txt"), renderTestsEvidence(contract, run.gates).trim() + "\n");
2794
2875
  // Reviewer panels spend real money in convergence too: reserve before,
2795
2876
  // settle the observed cost, and surface it as a budget observation
2796
2877
  // (parity with the race path's reviewRuns metering).
@@ -2800,6 +2881,7 @@ export class Orchestrator {
2800
2881
  attemptId,
2801
2882
  intent: "review",
2802
2883
  harnessId: "review-panel",
2884
+ cost: attemptCostEvidence("review-panel", attemptId),
2803
2885
  })
2804
2886
  : null;
2805
2887
  const reviewResult = reviewers.length > 0 && (reviewLease?.granted ?? false)
@@ -2826,7 +2908,7 @@ export class Orchestrator {
2826
2908
  reviewSpendEstimated: false,
2827
2909
  };
2828
2910
  if (reviewLease?.granted) {
2829
- ledger.settle(reviewLease.lease?.lease_id ?? "", reviewResult.reviewSpendUsd ?? 0);
2911
+ ledger.settle(reviewLease.lease?.lease_id ?? "", usageCostSettlement(reviewResult.reviewSpendUsd, reviewResult.reviewSpendEstimated, "review-usage", [`attempt:${attemptId}`, "review:panel"]));
2830
2912
  if ((reviewResult.reviewSpendUsd ?? 0) > 0) {
2831
2913
  log.emit("budget.observation", {
2832
2914
  harness_id: "review-panel",
@@ -2856,7 +2938,7 @@ export class Orchestrator {
2856
2938
  evidenceDir: candidateReviewEvidenceDir,
2857
2939
  });
2858
2940
  // 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);
2941
+ const policy = policyFindings(run, actualReviewVerified, contract.constraints.protected_paths, contract.constraints.auto_protected_paths, contract.constraints.protected_path_approvals, contract.constraints.deny_paths);
2860
2942
  const allFindings = [...policy.findings, ...revalidated];
2861
2943
  lastFindings = allFindings;
2862
2944
  store.writeYaml(join(paths.reviewsDir, `${attemptId}.yaml`), {
@@ -2994,6 +3076,8 @@ export class Orchestrator {
2994
3076
  : exhausted
2995
3077
  ? "exhausted"
2996
3078
  : "not_converged";
3079
+ if (status === "success" && ledger.terminal() !== null)
3080
+ status = ledger.terminal();
2997
3081
  let decision = null;
2998
3082
  if (lastRun) {
2999
3083
  const arb = arbitrate([
@@ -3027,7 +3111,7 @@ export class Orchestrator {
3027
3111
  lastRun.diff.trim().length > 0 &&
3028
3112
  (status === "success" || status === "ungated") &&
3029
3113
  !input.signal?.aborted) {
3030
- convFinalVerify = await finalVerifyPatch(execRoot, lastRun, this.gateSpecs(contract), log);
3114
+ convFinalVerify = await finalVerifyPatch(execRoot, lastRun, gateSpecsFromContract(contract), log);
3031
3115
  if (finalVerifyBlocks(convFinalVerify))
3032
3116
  status = "blocked";
3033
3117
  }
@@ -3036,7 +3120,9 @@ export class Orchestrator {
3036
3120
  // overrides the persisted decision; final_verify is recorded either way.
3037
3121
  decision = {
3038
3122
  ...decision,
3039
- ...(status === "blocked" ? blockedDecisionOverride(decision.evidence_facts, convFinalVerify) : {}),
3123
+ ...(status === "blocked"
3124
+ ? blockedDecisionOverride(decision.evidence_facts, convFinalVerify)
3125
+ : {}),
3040
3126
  final_verify: convFinalVerify,
3041
3127
  };
3042
3128
  store.writeYaml(join(paths.arbitrationDir, "decision.yaml"), decision);
@@ -3058,6 +3144,9 @@ export class Orchestrator {
3058
3144
  ? "applied"
3059
3145
  : "applied_review_blocked"
3060
3146
  : "not_applied";
3147
+ const revertAnchorId = convAdopted === true
3148
+ ? await createRevertAnchorOrNull(execRoot, preTurnSha, lastPostTurnSha)
3149
+ : null;
3061
3150
  store.writeYaml(join(paths.finalDir, "work_product.yaml"), {
3062
3151
  id: newId("wp"),
3063
3152
  kind: "patch",
@@ -3065,6 +3154,7 @@ export class Orchestrator {
3065
3154
  producer_attempt_id: lastRun.attemptId,
3066
3155
  meta: {
3067
3156
  harness_id: lastRun.harnessId,
3157
+ result_kind: "patch",
3068
3158
  mode,
3069
3159
  attempts: attempt,
3070
3160
  status,
@@ -3074,6 +3164,7 @@ export class Orchestrator {
3074
3164
  apply_state: convApplyState,
3075
3165
  pre_turn_sha: convAdopted === true ? preTurnSha : null,
3076
3166
  post_turn_sha: convAdopted === true ? lastPostTurnSha : null,
3167
+ revert_anchor_id: revertAnchorId,
3077
3168
  },
3078
3169
  });
3079
3170
  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 +3179,7 @@ export class Orchestrator {
3088
3179
  if (!converged) {
3089
3180
  writeFailure(store, paths, {
3090
3181
  phase: "convergence",
3091
- category: status === "exhausted"
3182
+ category: isBudgetTerminal(status)
3092
3183
  ? "budget"
3093
3184
  : status === "cancelled"
3094
3185
  ? "cancelled"
@@ -3204,19 +3295,32 @@ export class Orchestrator {
3204
3295
  const log = new EventLog(paths.eventsPath, runId, taskId, input.onEvent, input.threadId);
3205
3296
  safeInvoke(input.onRunStart, { runId, taskId, runDir: paths.root });
3206
3297
  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() });
3298
+ const ledger = this.rootLedger(input, contract, log);
3299
+ announce?.({
3300
+ log,
3301
+ store,
3302
+ paths,
3303
+ runId,
3304
+ taskId,
3305
+ mode: "plan",
3306
+ phase: "plan",
3307
+ spend: () => ledger.spend(),
3308
+ });
3209
3309
  store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
3210
3310
  log.emit("task.contract.created", { task_contract_hash: hashJson(contract) });
3211
3311
  const reviewersOutcome = await this.resolveReviewersWithArtifacts(input, log, store, paths, runId, taskId, "plan");
3212
3312
  if ("failed" in reviewersOutcome)
3213
3313
  return reviewersOutcome.failed;
3214
3314
  const reviewers = reviewersOutcome.reviewers;
3315
+ // W3.3: ONE resolved read-only context — the routing point-probe and every
3316
+ // planner spawn consume the SAME scoped env (see routeContext.ts).
3317
+ const roHome = resolveReadOnlyRouteContext(this.execRootOf(input));
3215
3318
  let adapters;
3216
3319
  try {
3217
- adapters = await this.resolveCandidateAdapters({ ...input, n: undefined }, "plan");
3320
+ adapters = await this.resolveCandidateAdapters({ ...input, n: undefined }, "plan", ledger, roHome);
3218
3321
  }
3219
3322
  catch (err) {
3323
+ roHome.dispose();
3220
3324
  const message = safeErrorMessage(err);
3221
3325
  store.writeText(join(paths.contextDir, "context_error.md"), `# Routing Error\n\n${message}\n`);
3222
3326
  writeFailure(store, paths, {
@@ -3251,6 +3355,7 @@ export class Orchestrator {
3251
3355
  contextSection = await this.lazyContextSection(input, contract, store, paths, log);
3252
3356
  }
3253
3357
  catch (err) {
3358
+ roHome.dispose();
3254
3359
  const message = safeErrorMessage(err);
3255
3360
  store.writeText(join(paths.contextDir, "context_error.md"), `# Context Error\n\n${message}\n`);
3256
3361
  writeFailure(store, paths, {
@@ -3282,17 +3387,19 @@ export class Orchestrator {
3282
3387
  const plans = [];
3283
3388
  const planAttempts = [];
3284
3389
  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
3390
  try {
3290
3391
  for (const [idx, routed] of adapters.entries()) {
3291
3392
  if (input.signal?.aborted)
3292
3393
  break;
3293
3394
  const adapter = routed.adapter;
3294
3395
  const attemptId = `p${String(idx + 1).padStart(2, "0")}`;
3295
- const lease = ledger.reserve({ taskId, attemptId, intent: "plan", harnessId: adapter.id });
3396
+ const lease = ledger.reserve({
3397
+ taskId,
3398
+ attemptId,
3399
+ intent: "plan",
3400
+ harnessId: adapter.id,
3401
+ cost: attemptCostEvidence(adapter.id, attemptId, undefined, this.routeBillingKnowledge(input, adapter.id)),
3402
+ });
3296
3403
  if (!lease.granted) {
3297
3404
  log.emit("budget.lease.created", {
3298
3405
  granted: false,
@@ -3313,16 +3420,8 @@ export class Orchestrator {
3313
3420
  // Planners must SEE any image/file the user attached (e.g. "plan a fix for
3314
3421
  // what's in this screenshot"), not just agent/race runs.
3315
3422
  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,
3423
+ ...this.sessionSpecFields(input, adapter.id, log),
3424
+ ...this.harnessSpecKnobs(contract, knobs, "plan"),
3326
3425
  env_inheritance: envInheritance(this.config(input.repoRoot)),
3327
3426
  env: roHome.env,
3328
3427
  });
@@ -3334,10 +3433,13 @@ export class Orchestrator {
3334
3433
  if (planInteraction)
3335
3434
  spec.extra["interactionChannel"] = planInteraction;
3336
3435
  const attemptEventsPath = join(paths.attemptsDir, attemptId, "events.jsonl");
3337
- const parts = [];
3436
+ const answer = new AnswerAssembly();
3338
3437
  const telemetry = createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required ||
3339
3438
  knobs.webPolicy === "cached" ||
3340
- knobs.webPolicy === "live", effectiveWeb);
3439
+ knobs.webPolicy === "live", effectiveWeb, [],
3440
+ // Requested-model capture: a plan lane silently downgraded to another
3441
+ // model surfaces the mismatch in its route receipt, just like agent.
3442
+ knobs.model);
3341
3443
  const onAbort = () => {
3342
3444
  void adapter.cancel?.(spec.session_id)?.catch(() => { });
3343
3445
  };
@@ -3348,6 +3450,7 @@ export class Orchestrator {
3348
3450
  input.signal.addEventListener("abort", onAbort, { once: true });
3349
3451
  }
3350
3452
  let cost = 0;
3453
+ let costEstimated = false;
3351
3454
  let harnessError = null;
3352
3455
  const budgetSignalState = { quotaPressureDisclosed: false };
3353
3456
  try {
@@ -3371,23 +3474,29 @@ export class Orchestrator {
3371
3474
  break;
3372
3475
  const safeEv = redactHarnessEvent(ev);
3373
3476
  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.
3477
+ // NOT observed for resume: a read-only planner is not a chat turn,
3478
+ // and attaching its session id would poison thread continuity (and
3479
+ // race parallel planner/reviewer sessions), regardless of whether
3480
+ // the vendor stored that session in the scoped or native store.
3379
3481
  observeAuthSwitch(log, adapter.id, attemptId, safeEv);
3380
3482
  log.emit("harness.event", harnessEventPayload(adapter.id, attemptId, safeEv));
3381
3483
  appendLine(attemptEventsPath, JSON.stringify(safeEv));
3382
3484
  observeAttemptTelemetry(telemetry, safeEv);
3383
3485
  if (safeEv.plan_progress) {
3384
- log.emit("plan.progress", { attempt_id: attemptId, harness_id: adapter.id, items: safeEv.plan_progress.items });
3486
+ log.emit("plan.progress", {
3487
+ attempt_id: attemptId,
3488
+ harness_id: adapter.id,
3489
+ items: safeEv.plan_progress.items,
3490
+ });
3385
3491
  }
3386
3492
  // read-only routes burn quota too (the orchestrate PLANNER is
3387
3493
  // the loudest) — same single owner as the agent loop.
3388
3494
  observeBudgetSignals(ledger, log, adapter.id, attemptId, safeEv, budgetSignalState);
3495
+ this.deps.quotaEventSink?.(adapter.id, safeEv);
3389
3496
  if (safeEv.type === "usage" && safeEv.usage?.cost_usd) {
3390
3497
  cost += safeEv.usage.cost_usd;
3498
+ if (safeEv.usage.estimated)
3499
+ costEstimated = true;
3391
3500
  log.emit("budget.observation", {
3392
3501
  harness_id: adapter.id,
3393
3502
  attempt_id: attemptId,
@@ -3396,11 +3505,8 @@ export class Orchestrator {
3396
3505
  estimated: safeEv.usage.estimated === true,
3397
3506
  });
3398
3507
  }
3399
- if (safeEv.type === "message" &&
3400
- safeEv.text &&
3401
- safeEv.payload?.["auth_switched"] !== true) {
3402
- pushUniqueText(parts, safeEv.text);
3403
- }
3508
+ // A TYPED final message wins verbatim over joined narration.
3509
+ answer.observe(safeEv);
3404
3510
  if (safeEv.type === "error")
3405
3511
  harnessError = safeEv.error
3406
3512
  ? redactSecrets(safeEv.error)
@@ -3413,7 +3519,7 @@ export class Orchestrator {
3413
3519
  }
3414
3520
  finally {
3415
3521
  input.signal?.removeEventListener("abort", onAbort);
3416
- ledger.settle(lease.lease?.lease_id ?? "", cost);
3522
+ ledger.settle(lease.lease?.lease_id ?? "", attemptUsageCostSettlement(cost, costEstimated, attemptId, adapter.id, telemetry.authMode));
3417
3523
  }
3418
3524
  attemptTelemetries.push({ attemptId, harnessId: adapter.id, telemetry });
3419
3525
  const unrecovered = unrecoveredToolErrors(telemetry);
@@ -3443,7 +3549,7 @@ export class Orchestrator {
3443
3549
  });
3444
3550
  continue;
3445
3551
  }
3446
- const text = parts.join("\n").trim() || "(no output)";
3552
+ const text = answer.text() || "(no output)";
3447
3553
  log.emit("harness.completed", {
3448
3554
  harness_id: adapter.id,
3449
3555
  attempt_id: attemptId,
@@ -3456,7 +3562,7 @@ export class Orchestrator {
3456
3562
  }
3457
3563
  }
3458
3564
  finally {
3459
- // Planners done (or threw) — always reclaim the scoped home (it may hold seeded creds).
3565
+ // Planners done (or threw) — reclaim scoped scratch/API-route state.
3460
3566
  roHome.dispose();
3461
3567
  }
3462
3568
  if (input.signal?.aborted) {
@@ -3464,7 +3570,7 @@ export class Orchestrator {
3464
3570
  attemptId: p.attemptId,
3465
3571
  harnessId: p.harnessId,
3466
3572
  status: p.status,
3467
- })), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, "plan", attemptTelemetries, null), ledger.spend());
3573
+ })), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, "plan", attemptTelemetries, null), ledger.spend(), input.signal, store);
3468
3574
  }
3469
3575
  if (plans.length === 0) {
3470
3576
  const blocked = planAttempts.some((p) => p.status === "blocked");
@@ -3523,7 +3629,7 @@ export class Orchestrator {
3523
3629
  userIntent: redactSecrets(input.prompt),
3524
3630
  planAccepted: planEvidence,
3525
3631
  diff: planReviewDiff,
3526
- tests: this.testsEvidence(contract),
3632
+ tests: renderTestsEvidence(contract),
3527
3633
  });
3528
3634
  // Reserve BEFORE spending: a hard budget tier must stop the paid plan
3529
3635
  // review from starting, not account for it after the fact.
@@ -3532,6 +3638,7 @@ export class Orchestrator {
3532
3638
  attemptId: "plan-review",
3533
3639
  intent: "review",
3534
3640
  harnessId: "review-panel",
3641
+ cost: attemptCostEvidence("review-panel", "plan-review"),
3535
3642
  });
3536
3643
  if (lease.granted) {
3537
3644
  const res = await this.reviewScoped({
@@ -3555,7 +3662,10 @@ export class Orchestrator {
3555
3662
  route_proofs: res.routeProofs,
3556
3663
  reviewer_requests: res.reviewerRequests,
3557
3664
  });
3558
- ledger.settle(lease.lease?.lease_id ?? "", res.reviewSpendUsd ?? 0);
3665
+ ledger.settle(lease.lease?.lease_id ?? "", usageCostSettlement(res.reviewSpendUsd, res.reviewSpendEstimated, "review-usage", [
3666
+ "attempt:plan-review",
3667
+ "review:panel",
3668
+ ]));
3559
3669
  if ((res.reviewSpendUsd ?? 0) > 0) {
3560
3670
  log.emit("budget.observation", {
3561
3671
  harness_id: "review-panel",
@@ -3579,7 +3689,7 @@ export class Orchestrator {
3579
3689
  attemptId: p.attemptId,
3580
3690
  harnessId: p.harnessId,
3581
3691
  status: p.status,
3582
- })), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, "plan", attemptTelemetries, null), ledger.spend());
3692
+ })), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, "plan", attemptTelemetries, null), ledger.spend(), input.signal, store);
3583
3693
  }
3584
3694
  const failedPlanners = planAttempts.filter((p) => p.status !== "success");
3585
3695
  // ALL review findings are shown (severity-marked), so a BLOCK like "the
@@ -3690,18 +3800,40 @@ export class Orchestrator {
3690
3800
  defaultPrompt: "audit this repository",
3691
3801
  }, announce);
3692
3802
  }
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;
3803
+ resolvePaidBudget(inputBudget, cfg) {
3804
+ return inputBudget ?? this.deps.paidBudget ?? cfg.global.budget.paid_budget_per_run;
3805
+ }
3806
+ rootLedger(input, contract, log) {
3807
+ // A passed-in ledger (orchestrate sub-runs) keeps its OWNER's cash
3808
+ // disclosure the parent run owns the budget, so its event log gets the
3809
+ // budget.cash events. A fresh root ledger discloses into THIS run's log:
3810
+ // the ledger is the one owner of the cash fact (subscription-entitled
3811
+ // work settles to 0 there), and the UI renders `budget.cash` verbatim —
3812
+ // never inferring money from route labels (W4.3 sol #15).
3813
+ const ledger = input.budgetLedger ??
3814
+ new BudgetLedger(contract.budget.paid_budget, undefined, {
3815
+ onCashSettled: (cashSpendUsd, valuationUsd) => log.emit("budget.cash", {
3816
+ cash_spend_usd: cashSpendUsd,
3817
+ valuation_usd: valuationUsd,
3818
+ }),
3819
+ });
3820
+ for (const snapshot of this.deps.quotaSnapshots?.() ?? []) {
3821
+ ledger.observeQuotaSnapshot(snapshot);
3822
+ }
3823
+ return ledger;
3824
+ }
3825
+ routeBillingKnowledge(input, harnessId) {
3826
+ // A selected profile's credential_kind decides billing (round-18 #2).
3827
+ const profileRoute = this.profileAuthRoute(input, harnessId);
3828
+ if (profileRoute)
3829
+ return profileRoute === "api_key" ? "metered" : "unknown";
3830
+ if (input.authPreference === "api_key")
3831
+ return "metered";
3832
+ if (input.authPreference === "subscription")
3833
+ return "unknown";
3834
+ return loadHarnessMetrics(globalConfigDir())[harnessId]?.last_auth_mode === "api_key"
3835
+ ? "metered"
3836
+ : "unknown";
3705
3837
  }
3706
3838
  async runOrchestrate(input, announce) {
3707
3839
  // "Doctor-verified" must mean status ok — degraded key-present routes are
@@ -3715,15 +3847,11 @@ export class Orchestrator {
3715
3847
  // the executor below is its consumer. Default `suggest` (plan-only) preserves
3716
3848
  // the read-only contract when no autonomy is requested.
3717
3849
  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));
3850
+ const paidBudget = this.resolvePaidBudget(input.paidBudget, this.config(input.repoRoot));
3723
3851
  const orchestrateContract = OrchestrateContractSchema.parse({
3724
3852
  thread_id: input.threadId ?? newId("th"),
3725
3853
  goal,
3726
- budget: { max_usd: aggregateMaxUsd, max_tool_calls: input.maxToolCalls ?? null },
3854
+ budget: { paid_budget: paidBudget, max_tool_calls: input.maxToolCalls ?? null },
3727
3855
  autonomy,
3728
3856
  });
3729
3857
  const plannerPrompt = buildOrchestratePlannerPrompt(goal, pool, crossFamily, orchestrateContract);
@@ -3762,8 +3890,17 @@ export class Orchestrator {
3762
3890
  const log = new EventLog(paths.eventsPath, runId, taskId, input.onEvent, input.threadId);
3763
3891
  safeInvoke(input.onRunStart, { runId, taskId, runDir: paths.root });
3764
3892
  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() });
3893
+ const ledger = this.rootLedger(input, contract, log);
3894
+ announce?.({
3895
+ log,
3896
+ store,
3897
+ paths,
3898
+ runId,
3899
+ taskId,
3900
+ mode: opts.mode,
3901
+ phase: "report",
3902
+ spend: () => ledger.spend(),
3903
+ });
3767
3904
  store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
3768
3905
  log.emit("task.contract.created", { task_contract_hash: hashJson(contract) });
3769
3906
  if (opts.orchestrateContract) {
@@ -3814,9 +3951,12 @@ export class Orchestrator {
3814
3951
  : externalContextPolicy === "off"
3815
3952
  ? 1
3816
3953
  : Math.min(Math.max(input.n ?? 2, 1), 3);
3954
+ // W3.3: ONE resolved read-only context — the routing point-probe and every
3955
+ // read-only attempt spawn consume the SAME scoped env (see routeContext.ts).
3956
+ const roHome = resolveReadOnlyRouteContext(this.execRootOf(input));
3817
3957
  let adapters;
3818
3958
  try {
3819
- adapters = await this.resolveCandidateAdapters({ ...input, prompt, n: width }, opts.intent);
3959
+ adapters = await this.resolveCandidateAdapters({ ...input, prompt, n: width }, opts.intent, ledger, roHome);
3820
3960
  if (!opts.swarm) {
3821
3961
  const seen = new Set();
3822
3962
  adapters = adapters.filter((routed) => {
@@ -3828,6 +3968,7 @@ export class Orchestrator {
3828
3968
  }
3829
3969
  }
3830
3970
  catch (err) {
3971
+ roHome.dispose();
3831
3972
  const message = safeErrorMessage(err);
3832
3973
  store.writeText(join(paths.contextDir, "context_error.md"), `# Routing Error\n\n${message}\n`);
3833
3974
  writeFailure(store, paths, {
@@ -3855,11 +3996,6 @@ export class Orchestrator {
3855
3996
  candidates: [],
3856
3997
  };
3857
3998
  }
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
3999
  const attempts = [];
3864
4000
  const attemptTelemetries = [];
3865
4001
  let fallbackOpen = false;
@@ -3880,6 +4016,7 @@ export class Orchestrator {
3880
4016
  attemptId,
3881
4017
  intent: opts.intent,
3882
4018
  harnessId: adapter.id,
4019
+ cost: attemptCostEvidence(adapter.id, attemptId, undefined, this.routeBillingKnowledge(input, adapter.id)),
3883
4020
  });
3884
4021
  if (!lease.granted) {
3885
4022
  log.emit("budget.lease.created", {
@@ -3896,11 +4033,11 @@ export class Orchestrator {
3896
4033
  const explorerPrompt = (opts.swarm
3897
4034
  ? `${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
4035
  : prompt) + contextSection;
3899
- const sessionFields = this.sessionSpecFields(input, adapter.id);
4036
+ const sessionFields = this.sessionSpecFields(input, adapter.id, log);
3900
4037
  const grantResume = sessionFields.resume_session_id !== null && !resumeGranted.has(adapter.id);
3901
4038
  if (grantResume)
3902
4039
  resumeGranted.add(adapter.id);
3903
- const spec = HarnessRunSpec.parse({
4040
+ let spec = HarnessRunSpec.parse({
3904
4041
  session_id: newId("ses"),
3905
4042
  intent: opts.intent,
3906
4043
  prompt: explorerPrompt,
@@ -3911,16 +4048,9 @@ export class Orchestrator {
3911
4048
  // the model honestly reported it saw nothing (the v0.13 attachment bug).
3912
4049
  attachments: input.attachments ?? [],
3913
4050
  auth_preference: sessionFields.auth_preference,
4051
+ credential_profile: sessionFields.credential_profile,
3914
4052
  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,
4053
+ ...this.harnessSpecKnobs(contract, knobs, opts.intent),
3924
4054
  env_inheritance: envInheritance(this.config(input.repoRoot)),
3925
4055
  env: roHome.env,
3926
4056
  // Structured output: the orchestrate PLANNER's deliverable IS the
@@ -3944,10 +4074,13 @@ export class Orchestrator {
3944
4074
  if (reportInteraction)
3945
4075
  spec.extra["interactionChannel"] = reportInteraction;
3946
4076
  const attemptEventsPath = join(paths.attemptsDir, attemptId, "events.jsonl");
3947
- const parts = [];
4077
+ const answer = new AnswerAssembly();
3948
4078
  const telemetry = createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required ||
3949
4079
  knobs.webPolicy === "cached" ||
3950
- knobs.webPolicy === "live", effectiveWeb);
4080
+ knobs.webPolicy === "live", effectiveWeb, [],
4081
+ // Requested-model capture so ask/audit route receipts detect a silent
4082
+ // model downgrade (typed model_mismatch), not just agent runs.
4083
+ knobs.model);
3951
4084
  const retryPolicy = transientRetryPolicy(this.config(input.repoRoot));
3952
4085
  let activeSessionId = spec.session_id;
3953
4086
  const onAbort = () => {
@@ -3960,8 +4093,10 @@ export class Orchestrator {
3960
4093
  input.signal.addEventListener("abort", onAbort, { once: true });
3961
4094
  }
3962
4095
  let cost = 0;
4096
+ let costEstimated = false;
3963
4097
  let harnessError = null;
3964
4098
  try {
4099
+ const triedProfiles = new Set(); // W5.4 failover: each profile at most once
3965
4100
  for (let nativeTry = 0; !input.signal?.aborted; nativeTry += 1) {
3966
4101
  const runSpec = nativeTry === 0
3967
4102
  ? spec
@@ -3973,6 +4108,7 @@ export class Orchestrator {
3973
4108
  });
3974
4109
  activeSessionId = runSpec.session_id;
3975
4110
  const transientStart = telemetry.transientFailures.length;
4111
+ const rateLimitStart = telemetry.rateLimits.length;
3976
4112
  log.emit("harness.started", {
3977
4113
  harness_id: adapter.id,
3978
4114
  attempt_id: attemptId,
@@ -3995,23 +4131,28 @@ export class Orchestrator {
3995
4131
  break;
3996
4132
  const safeEv = redactHarnessEvent(ev);
3997
4133
  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.
4134
+ // NOT observed for resume: this read-only attempt is not a chat
4135
+ // turn. Recording its id would poison thread continuity and can
4136
+ // race parallel read-only sessions, regardless of storage route.
4003
4137
  observeAuthSwitch(log, adapter.id, attemptId, safeEv);
4004
4138
  log.emit("harness.event", harnessEventPayload(adapter.id, attemptId, safeEv));
4005
4139
  appendLine(attemptEventsPath, JSON.stringify(safeEv));
4006
4140
  observeAttemptTelemetry(telemetry, safeEv);
4007
4141
  if (safeEv.plan_progress) {
4008
- log.emit("plan.progress", { attempt_id: attemptId, harness_id: adapter.id, items: safeEv.plan_progress.items });
4142
+ log.emit("plan.progress", {
4143
+ attempt_id: attemptId,
4144
+ harness_id: adapter.id,
4145
+ items: safeEv.plan_progress.items,
4146
+ });
4009
4147
  }
4010
4148
  // read-only routes burn quota too (the orchestrate PLANNER is
4011
4149
  // the loudest) — same single owner as the agent loop.
4012
4150
  observeBudgetSignals(ledger, log, adapter.id, attemptId, safeEv, budgetSignalState);
4151
+ this.deps.quotaEventSink?.(adapter.id, safeEv);
4013
4152
  if (safeEv.type === "usage" && safeEv.usage?.cost_usd) {
4014
4153
  cost += safeEv.usage.cost_usd;
4154
+ if (safeEv.usage.estimated)
4155
+ costEstimated = true;
4015
4156
  log.emit("budget.observation", {
4016
4157
  harness_id: adapter.id,
4017
4158
  attempt_id: attemptId,
@@ -4020,11 +4161,8 @@ export class Orchestrator {
4020
4161
  estimated: safeEv.usage.estimated === true,
4021
4162
  });
4022
4163
  }
4023
- if (safeEv.type === "message" &&
4024
- safeEv.text &&
4025
- safeEv.payload?.["auth_switched"] !== true) {
4026
- pushUniqueText(parts, safeEv.text);
4027
- }
4164
+ // A TYPED final message wins verbatim over joined narration.
4165
+ answer.observe(safeEv);
4028
4166
  if (safeEv.type === "error")
4029
4167
  harnessError = safeEv.error
4030
4168
  ? redactSecrets(safeEv.error)
@@ -4036,7 +4174,32 @@ export class Orchestrator {
4036
4174
  }
4037
4175
  const transient = telemetry.transientFailures.at(-1) ?? null;
4038
4176
  const sawTransient = telemetry.transientFailures.length > transientStart;
4039
- const reportSoFar = parts.join("\n").trim();
4177
+ const sawTypedLimit = telemetry.rateLimits.length > rateLimitStart;
4178
+ const reportSoFar = answer.text();
4179
+ // W5.4 reactive failover, READ-ONLY lane (same contract as the
4180
+ // candidate lane; typed limits only, never plain transients).
4181
+ if (harnessError && !input.signal?.aborted) {
4182
+ const rotated = rotateSpecOnTypedLimit({
4183
+ spec,
4184
+ harnessId: adapter.id,
4185
+ attemptId,
4186
+ policy: this.profilePolicy(input.repoRoot, adapter.id),
4187
+ registry: this.config(input.repoRoot)?.global.credential_profiles ?? [],
4188
+ snapshots: this.deps.quotaSnapshots?.() ?? [],
4189
+ triedProfiles,
4190
+ sawTypedLimit,
4191
+ deliverableEmpty: reportSoFar.length === 0,
4192
+ lastLimit: telemetry.rateLimits.at(-1) ?? null,
4193
+ emit: (type, payload) => log.emit(type, payload),
4194
+ newSessionId: () => newId("ses"),
4195
+ defaultRouteWasVendorNative: routed.authRouteEstimate === "local_session",
4196
+ });
4197
+ if (rotated) {
4198
+ spec = rotated;
4199
+ harnessError = null;
4200
+ continue;
4201
+ }
4202
+ }
4040
4203
  if (!harnessError ||
4041
4204
  !sawTransient ||
4042
4205
  reportSoFar.length > 0 ||
@@ -4063,7 +4226,7 @@ export class Orchestrator {
4063
4226
  }
4064
4227
  finally {
4065
4228
  input.signal?.removeEventListener("abort", onAbort);
4066
- ledger.settle(lease.lease?.lease_id ?? "", cost);
4229
+ ledger.settle(lease.lease?.lease_id ?? "", attemptUsageCostSettlement(cost, costEstimated, attemptId, adapter.id, telemetry.authMode));
4067
4230
  }
4068
4231
  if (harnessError && telemetry.transientFailures.length > 0) {
4069
4232
  log.emit("route.transient.exhausted", {
@@ -4073,7 +4236,7 @@ export class Orchestrator {
4073
4236
  });
4074
4237
  }
4075
4238
  attemptTelemetries.push({ attemptId, harnessId: adapter.id, telemetry });
4076
- const report = redactSecrets(parts.join("\n").trim());
4239
+ const report = redactSecrets(answer.text());
4077
4240
  const unrecovered = unrecoveredToolErrors(telemetry);
4078
4241
  const webBlocked = webUnsatisfied(telemetry);
4079
4242
  const deliverablePresent = report.length > 0;
@@ -4213,8 +4376,8 @@ export class Orchestrator {
4213
4376
  }
4214
4377
  }
4215
4378
  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).
4379
+ // All read-only attempts done (or threw) — reclaim scoped scratch and
4380
+ // injected API-route state. Vendor-owned native credentials were not copied.
4218
4381
  roHome.dispose();
4219
4382
  }
4220
4383
  if (input.signal?.aborted) {
@@ -4222,7 +4385,7 @@ export class Orchestrator {
4222
4385
  attemptId: a.attemptId,
4223
4386
  harnessId: a.harnessId,
4224
4387
  status: a.status,
4225
- })), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, opts.mode, attemptTelemetries, null), ledger.spend());
4388
+ })), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, opts.mode, attemptTelemetries, null), ledger.spend(), input.signal, store);
4226
4389
  }
4227
4390
  const succeededReadonly = attempts.filter((a) => a.status === "success");
4228
4391
  if (!opts.swarm && succeededReadonly.length === 0) {
@@ -4377,6 +4540,18 @@ export class Orchestrator {
4377
4540
  ].join("\n")
4378
4541
  : (succeeded[0]?.report ?? "(no output)");
4379
4542
  store.writeText(join(paths.finalDir, opts.artifactName), `# ${opts.title}\n\n${report}\n`);
4543
+ // ask is the only read-only strategy that can carry a structured-output
4544
+ // contract (the boundary refuses the rest); validate the RAW answer text,
4545
+ // not the titled artifact wrapper.
4546
+ if (opts.mode === "ask" && contract.output_schema) {
4547
+ finalizeStructuredOutput({
4548
+ store,
4549
+ finalDir: paths.finalDir,
4550
+ log,
4551
+ schema: contract.output_schema,
4552
+ answerText: succeeded[0]?.report ?? "",
4553
+ });
4554
+ }
4380
4555
  // orchestrate: the planner's plan is a TYPED artifact, not just prose. Extract
4381
4556
  // the required fenced JSON block, validate it against the tool belt, and
4382
4557
  // persist final/orchestration.yaml; a missing/invalid block is disclosed in
@@ -4436,6 +4611,10 @@ export class Orchestrator {
4436
4611
  // terminal outcome (success / blocked / failed) becomes the run's terminal.
4437
4612
  const autonomy = opts.orchestrateContract?.autonomy ?? input.autonomy ?? "suggest";
4438
4613
  let terminal = "success";
4614
+ let orchestrateReadOnly = true;
4615
+ let orchestrateReceiptRefs = [];
4616
+ if (ledger.terminal() !== null)
4617
+ terminal = ledger.terminal();
4439
4618
  // orchestrate's contract output IS the typed plan. If the planner failed to
4440
4619
  // produce a valid one, the run is NOT a clean success — disclose it honestly
4441
4620
  // (the markdown plan stays as a diagnostic artifact) rather than reporting
@@ -4446,11 +4625,22 @@ export class Orchestrator {
4446
4625
  // Thread the GENERATED runId onto input so the executor's answer_question
4447
4626
  // step keys the interaction registry by this orchestrate run's id (callers
4448
4627
  // 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());
4628
+ const executionInput = { ...input, runId };
4629
+ const exec = await executeOrchestratePlan({
4630
+ plan: orchestratePlan,
4631
+ autonomy,
4632
+ maxToolCalls: opts.orchestrateContract?.budget.max_tool_calls ?? null,
4633
+ signal: input.signal,
4634
+ store,
4635
+ paths,
4636
+ log,
4637
+ ledger,
4638
+ executeSafeStep: (call) => this.executeSafeStep(executionInput, call, log, store, paths, ledger),
4639
+ executeApplyStep: (call) => this.executeApplyStep(executionInput, call, log),
4640
+ });
4453
4641
  terminal = exec.terminal;
4642
+ orchestrateReadOnly = exec.readOnly;
4643
+ orchestrateReceiptRefs = exec.receiptRefs;
4454
4644
  typedPlanNote += `\n- Executor (${autonomy}): ${exec.note}`;
4455
4645
  }
4456
4646
  const harnessLabel = attempts
@@ -4462,69 +4652,32 @@ export class Orchestrator {
4462
4652
  kind: "report",
4463
4653
  source_task_id: taskId,
4464
4654
  producer_attempt_id: succeeded[0]?.attemptId ?? "a01",
4465
- files: { [opts.artifactName]: join(paths.finalDir, opts.artifactName) },
4655
+ files: Object.fromEntries([
4656
+ [opts.artifactName, join(paths.finalDir, opts.artifactName)],
4657
+ ...orchestrateReceiptRefs.map((ref, index) => [`delivery_receipt_${index + 1}`, ref]),
4658
+ ]),
4466
4659
  meta: {
4467
4660
  harnesses: attempts.map((a) => a.harnessId),
4468
4661
  mode: opts.mode,
4469
4662
  intent: opts.intent,
4470
- read_only: true,
4663
+ read_only: orchestrateReadOnly,
4471
4664
  },
4472
4665
  });
4473
4666
  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
- });
4667
+ const orchestrateFailure = orchestrateFailureFor(terminal);
4668
+ if (terminal === "blocked" && orchestrateFailure) {
4669
+ writeFailure(store, paths, { ...orchestrateFailure, runDir: paths.root });
4486
4670
  log.emit("run.blocked", {
4487
4671
  status: terminal,
4488
- phase: "executor",
4672
+ phase: orchestrateFailure.phase,
4489
4673
  failure_ref: "final/failure.yaml",
4490
4674
  });
4491
4675
  }
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
- });
4504
- log.emit("run.failed", {
4505
- 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
- });
4676
+ else if (orchestrateFailure) {
4677
+ writeFailure(store, paths, { ...orchestrateFailure, runDir: paths.root });
4525
4678
  log.emit("run.failed", {
4526
4679
  status: terminal,
4527
- phase: "plan",
4680
+ phase: orchestrateFailure.phase,
4528
4681
  failure_ref: "final/failure.yaml",
4529
4682
  });
4530
4683
  }
@@ -4534,26 +4687,6 @@ export class Orchestrator {
4534
4687
  // mistake an operator abort for a clean report.
4535
4688
  log.emit("run.failed", { status: terminal });
4536
4689
  }
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
4690
  else {
4558
4691
  log.emit("run.completed", { status: terminal });
4559
4692
  }
@@ -4573,190 +4706,20 @@ export class Orchestrator {
4573
4706
  })),
4574
4707
  };
4575
4708
  }
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
4709
  /**
4749
4710
  * Run one SAFE plan step. start_run/race spawn ISOLATED ENVELOPE sub-runs
4750
4711
  * (inPlace=false, ASSERTED); review/status/answer_question are pure reads /
4751
4712
  * answer delivery that never mutate the live tree.
4752
4713
  */
4753
- async executeSafeStep(input, call, log, store, paths, remainingUsd) {
4714
+ async executeSafeStep(input, call, log, store, paths, ledger) {
4754
4715
  switch (call.tool) {
4755
4716
  case "start_run":
4756
4717
  case "race": {
4757
4718
  // Isolated envelope sub-run (construction owned by runSupport);
4758
4719
  // recursion guard via orchestrateDepth+1, may NOT orchestrate.
4759
- const subInput = { ...buildEnvelopeSubInput(input, call, remainingUsd) };
4720
+ const subInput = {
4721
+ ...buildEnvelopeSubInput(input, call, ledger),
4722
+ };
4760
4723
  // SAFETY INVARIANT 1 (asserted, not convention): a safe sub-run is an
4761
4724
  // isolated envelope — never a live in-place turn.
4762
4725
  assertEnvelopeSubRun(subInput);
@@ -4768,6 +4731,9 @@ export class Orchestrator {
4768
4731
  const res = await this.run(subInput);
4769
4732
  return {
4770
4733
  status: res.status === "failed" || res.status === "cancelled" ? "failed" : "done",
4734
+ terminalStatus: res.status,
4735
+ terminalSource: "subrun",
4736
+ evidenceRefs: [`run:${res.runId}`],
4771
4737
  runId: res.runId,
4772
4738
  spendUsd: res.spendUsd ?? null,
4773
4739
  detail: `${call.tool} sub-run ${res.runId} -> ${res.status}`,
@@ -4778,8 +4744,11 @@ export class Orchestrator {
4778
4744
  const read = readRunStatus(input.repoRoot, call.run_id);
4779
4745
  return {
4780
4746
  status: read ? "done" : "skipped",
4747
+ terminalStatus: read?.status ?? null,
4748
+ terminalSource: "subrun",
4749
+ evidenceRefs: read?.evidenceRefs ?? [],
4781
4750
  runId: call.run_id,
4782
- detail: read ?? `run ${call.run_id} has no readable status artifacts`,
4751
+ detail: read?.detail ?? `run ${call.run_id} has no readable status artifacts`,
4783
4752
  };
4784
4753
  }
4785
4754
  case "review": {
@@ -4791,15 +4760,21 @@ export class Orchestrator {
4791
4760
  if (diff === null)
4792
4761
  return {
4793
4762
  status: "skipped",
4763
+ terminalStatus: null,
4764
+ terminalSource: "review",
4765
+ evidenceRefs: [],
4794
4766
  runId: call.run_id,
4795
4767
  detail: `run ${call.run_id} has no patch.diff to review`,
4796
4768
  };
4797
4769
  // Aggregate honesty: reviewer panels spend real money on
4798
4770
  // API-keyed routes and the spend is charged AFTER the fact — with no
4799
4771
  // remaining headroom the review must not start at all.
4800
- if (remainingUsd !== null && remainingUsd <= 0) {
4772
+ if (ledger.terminal() !== null) {
4801
4773
  return {
4802
4774
  status: "skipped",
4775
+ terminalStatus: null,
4776
+ terminalSource: "review",
4777
+ evidenceRefs: [],
4803
4778
  runId: call.run_id,
4804
4779
  detail: "aggregate budget exhausted before the review step",
4805
4780
  };
@@ -4808,9 +4783,28 @@ export class Orchestrator {
4808
4783
  if (reviewers.length === 0)
4809
4784
  return {
4810
4785
  status: "skipped",
4786
+ terminalStatus: null,
4787
+ terminalSource: "review",
4788
+ evidenceRefs: [],
4811
4789
  runId: call.run_id,
4812
4790
  detail: "no doctor-OK reviewers available",
4813
4791
  };
4792
+ const reviewLease = ledger.reserve({
4793
+ taskId: input.taskId ?? "orchestrate",
4794
+ attemptId: `review-${call.run_id}`,
4795
+ intent: "review",
4796
+ harnessId: "review-panel",
4797
+ cost: attemptCostEvidence("review-panel", `review-${call.run_id}`),
4798
+ });
4799
+ if (!reviewLease.granted)
4800
+ return {
4801
+ status: "skipped",
4802
+ terminalStatus: ledger.terminal(),
4803
+ terminalSource: "review",
4804
+ evidenceRefs: [],
4805
+ runId: call.run_id,
4806
+ detail: reviewLease.reason ?? "root paid budget refused the review step",
4807
+ };
4814
4808
  const evidenceDir = join(paths.reviewsDir, `orchestrate-${call.run_id}`, "evidence");
4815
4809
  writeEvidencePacket(evidenceDir, {
4816
4810
  userIntent: redactSecrets(input.prompt),
@@ -4829,7 +4823,13 @@ export class Orchestrator {
4829
4823
  envInheritance: envInheritance(this.config(input.repoRoot)),
4830
4824
  signal: input.signal,
4831
4825
  onReviewerEvent: (event) => log.emit(event.type, { ...event }),
4826
+ }).catch((error) => {
4827
+ ledger.settle(reviewLease.lease?.lease_id ?? "", unknownCostSettlement("review-error"));
4828
+ throw error;
4832
4829
  });
4830
+ ledger.settle(reviewLease.lease?.lease_id ?? "", usageCostSettlement(result.reviewSpendUsd, result.reviewSpendEstimated, "review-usage", [
4831
+ `orchestrate:review:${call.run_id}`,
4832
+ ]));
4833
4833
  const revalidated = await revalidateFindings(result.findings, {
4834
4834
  candidateRoot: input.repoRoot,
4835
4835
  evidenceDir,
@@ -4844,6 +4844,9 @@ export class Orchestrator {
4844
4844
  const blockers = revalidated.filter((f) => isBlocking(f)).length;
4845
4845
  return {
4846
4846
  status: "done",
4847
+ terminalStatus: result.crossFamilyVerified && blockers === 0 ? "success" : "blocked",
4848
+ terminalSource: "review",
4849
+ evidenceRefs: [`reviews/orchestrate-${call.run_id}.yaml`],
4847
4850
  runId: call.run_id,
4848
4851
  detail: `reviewed ${call.run_id}: ${result.distinctProviders.length} family(ies), ${revalidated.length} finding(s), ${blockers} blocker(s)`,
4849
4852
  // Reviewer panels can spend real money on API-keyed routes; the
@@ -4853,7 +4856,13 @@ export class Orchestrator {
4853
4856
  }
4854
4857
  case "answer_question": {
4855
4858
  // Delivery + registry-keying rationale owned by runSupport.
4856
- return deliverPlanAnswer(input, call);
4859
+ const answer = await deliverPlanAnswer(input, call);
4860
+ return {
4861
+ ...answer,
4862
+ terminalStatus: answer.status === "done" ? "success" : null,
4863
+ terminalSource: "executor",
4864
+ evidenceRefs: [],
4865
+ };
4857
4866
  }
4858
4867
  default: {
4859
4868
  // FAIL-CLOSED: a risky tool (apply) must never reach the safe executor;
@@ -4862,13 +4871,8 @@ export class Orchestrator {
4862
4871
  }
4863
4872
  }
4864
4873
  }
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) {
4874
+ /** Execute an auto_full apply through the shared fresh-verification gate. */
4875
+ async executeApplyStep(input, call, log) {
4872
4876
  const store = new ArtifactStore(input.repoRoot);
4873
4877
  const sub = store.runPaths(call.run_id);
4874
4878
  const patchPath = join(sub.finalDir, "patch.diff");
@@ -4877,25 +4881,29 @@ export class Orchestrator {
4877
4881
  return {
4878
4882
  ok: false,
4879
4883
  runId: call.run_id,
4880
- detail: `run ${call.run_id} has no patch.diff to apply`,
4884
+ detail: `run ${call.run_id} has no patch.diff`,
4885
+ receipt: null,
4881
4886
  };
4882
4887
  if (containsSecretLikeToken(patchText))
4883
4888
  return {
4884
4889
  ok: false,
4885
4890
  runId: call.run_id,
4886
4891
  detail: "patch contains a secret-like token; refusing apply",
4892
+ receipt: null,
4887
4893
  };
4888
4894
  const decision = store.readYaml(join(sub.arbitrationDir, "decision.yaml"));
4889
4895
  const workProduct = store.readYaml(join(sub.finalDir, "work_product.yaml"));
4896
+ const taskContract = TaskContractSchema.safeParse(store.readYaml(join(sub.contextDir, "task.yaml")));
4890
4897
  const parsedDecision = decision ? DecisionRecordSchema.safeParse(decision) : null;
4891
4898
  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.
4899
+ if (!taskContract.success)
4900
+ return {
4901
+ ok: false,
4902
+ runId: call.run_id,
4903
+ detail: "fresh verification contract is missing",
4904
+ receipt: null,
4905
+ };
4906
+ const applyGateInput = {
4899
4907
  state: null,
4900
4908
  decision: parsedDecision?.success ? parsedDecision.data : null,
4901
4909
  workProduct: parsedWp?.success ? parsedWp.data : null,
@@ -4903,16 +4911,23 @@ export class Orchestrator {
4903
4911
  originalRepoRoot: input.repoRoot,
4904
4912
  targetRepoRoot: input.repoRoot,
4905
4913
  operatorDecision: null,
4906
- });
4914
+ };
4915
+ const gateError = validateApplyGate(applyGateInput);
4907
4916
  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 });
4917
+ return {
4918
+ ok: false,
4919
+ runId: call.run_id,
4920
+ detail: `apply gate refused: ${gateError}`,
4921
+ receipt: null,
4922
+ };
4923
+ const delivered = await verifyAndDeliver(input.repoRoot, patchText, { mode: call.mode }, gateSpecsFromContract(taskContract.data), (finalVerify) => validateApplyGate({ ...applyGateInput, finalVerify }), log);
4910
4924
  return {
4911
4925
  ok: delivered.applied,
4912
4926
  runId: call.run_id,
4913
4927
  detail: delivered.applied
4914
4928
  ? `applied (${call.mode})`
4915
4929
  : `deliver failed: ${delivered.detail ?? "unknown"}`,
4930
+ receipt: delivered,
4916
4931
  };
4917
4932
  }
4918
4933
  }