@claudexor/orchestrator 3.1.0 → 3.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 (119) hide show
  1. package/dist/attemptFinalize.d.ts +38 -0
  2. package/dist/attemptFinalize.d.ts.map +1 -1
  3. package/dist/attemptFinalize.js +44 -0
  4. package/dist/attemptFinalize.js.map +1 -1
  5. package/dist/attemptTelemetry.d.ts +9 -20
  6. package/dist/attemptTelemetry.d.ts.map +1 -1
  7. package/dist/attemptTelemetry.js +36 -69
  8. package/dist/attemptTelemetry.js.map +1 -1
  9. package/dist/attemptUsageCost.d.ts +31 -0
  10. package/dist/attemptUsageCost.d.ts.map +1 -0
  11. package/dist/attemptUsageCost.js +181 -0
  12. package/dist/attemptUsageCost.js.map +1 -0
  13. package/dist/budgetFailure.d.ts +3 -1
  14. package/dist/budgetFailure.d.ts.map +1 -1
  15. package/dist/budgetFailure.js.map +1 -1
  16. package/dist/candidateEvidence.d.ts +5 -0
  17. package/dist/candidateEvidence.d.ts.map +1 -1
  18. package/dist/candidateEvidence.js +3 -1
  19. package/dist/candidateEvidence.js.map +1 -1
  20. package/dist/candidateOutputs.d.ts +19 -0
  21. package/dist/candidateOutputs.d.ts.map +1 -1
  22. package/dist/candidateOutputs.js +201 -67
  23. package/dist/candidateOutputs.js.map +1 -1
  24. package/dist/decisionBudget.d.ts +43 -0
  25. package/dist/decisionBudget.d.ts.map +1 -0
  26. package/dist/decisionBudget.js +84 -0
  27. package/dist/decisionBudget.js.map +1 -0
  28. package/dist/decisionTerminalReconciliation.d.ts +18 -0
  29. package/dist/decisionTerminalReconciliation.d.ts.map +1 -0
  30. package/dist/decisionTerminalReconciliation.js +26 -0
  31. package/dist/decisionTerminalReconciliation.js.map +1 -0
  32. package/dist/deepScanReducer.d.ts.map +1 -1
  33. package/dist/deepScanReducer.js +1 -5
  34. package/dist/deepScanReducer.js.map +1 -1
  35. package/dist/delegation-parent-activation.d.ts +6 -0
  36. package/dist/delegation-parent-activation.d.ts.map +1 -0
  37. package/dist/delegation-parent-activation.js +13 -0
  38. package/dist/delegation-parent-activation.js.map +1 -0
  39. package/dist/delegationBudgetAuthority.d.ts +30 -0
  40. package/dist/delegationBudgetAuthority.d.ts.map +1 -0
  41. package/dist/delegationBudgetAuthority.js +157 -0
  42. package/dist/delegationBudgetAuthority.js.map +1 -0
  43. package/dist/delegationFailure.d.ts +58 -0
  44. package/dist/delegationFailure.d.ts.map +1 -0
  45. package/dist/delegationFailure.js +184 -0
  46. package/dist/delegationFailure.js.map +1 -0
  47. package/dist/delegationToolEvidence.d.ts +20 -0
  48. package/dist/delegationToolEvidence.d.ts.map +1 -0
  49. package/dist/delegationToolEvidence.js +46 -0
  50. package/dist/delegationToolEvidence.js.map +1 -0
  51. package/dist/effortGovernance.d.ts +49 -0
  52. package/dist/effortGovernance.d.ts.map +1 -0
  53. package/dist/effortGovernance.js +35 -0
  54. package/dist/effortGovernance.js.map +1 -0
  55. package/dist/index.d.ts +3 -1
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +2 -0
  58. package/dist/index.js.map +1 -1
  59. package/dist/orchestrator.d.ts +29 -38
  60. package/dist/orchestrator.d.ts.map +1 -1
  61. package/dist/orchestrator.js +294 -392
  62. package/dist/orchestrator.js.map +1 -1
  63. package/dist/plan-prompt.d.ts +3 -0
  64. package/dist/plan-prompt.d.ts.map +1 -0
  65. package/dist/plan-prompt.js +28 -0
  66. package/dist/plan-prompt.js.map +1 -0
  67. package/dist/requestRequirements.d.ts +17 -0
  68. package/dist/requestRequirements.d.ts.map +1 -1
  69. package/dist/requestRequirements.js +69 -1
  70. package/dist/requestRequirements.js.map +1 -1
  71. package/dist/reviewerPanel.d.ts +4 -0
  72. package/dist/reviewerPanel.d.ts.map +1 -1
  73. package/dist/reviewerPanel.js +54 -9
  74. package/dist/reviewerPanel.js.map +1 -1
  75. package/dist/revisePrompt.d.ts +17 -0
  76. package/dist/revisePrompt.d.ts.map +1 -0
  77. package/dist/revisePrompt.js +15 -0
  78. package/dist/revisePrompt.js.map +1 -0
  79. package/dist/root-ledger.d.ts +13 -0
  80. package/dist/root-ledger.d.ts.map +1 -0
  81. package/dist/root-ledger.js +19 -0
  82. package/dist/root-ledger.js.map +1 -0
  83. package/dist/routing-failure.d.ts +6 -0
  84. package/dist/routing-failure.d.ts.map +1 -0
  85. package/dist/routing-failure.js +12 -0
  86. package/dist/routing-failure.js.map +1 -0
  87. package/dist/run-bounded.d.ts +3 -0
  88. package/dist/run-bounded.d.ts.map +1 -0
  89. package/dist/run-bounded.js +17 -0
  90. package/dist/run-bounded.js.map +1 -0
  91. package/dist/run-input-resolution.d.ts +10 -0
  92. package/dist/run-input-resolution.d.ts.map +1 -0
  93. package/dist/run-input-resolution.js +70 -0
  94. package/dist/run-input-resolution.js.map +1 -0
  95. package/dist/runSupport.d.ts +17 -0
  96. package/dist/runSupport.d.ts.map +1 -1
  97. package/dist/runSupport.js +26 -0
  98. package/dist/runSupport.js.map +1 -1
  99. package/dist/runTelemetryWriter.d.ts +5 -2
  100. package/dist/runTelemetryWriter.d.ts.map +1 -1
  101. package/dist/runTelemetryWriter.js +72 -1
  102. package/dist/runTelemetryWriter.js.map +1 -1
  103. package/dist/runTerminals.d.ts +34 -7
  104. package/dist/runTerminals.d.ts.map +1 -1
  105. package/dist/runTerminals.js +215 -24
  106. package/dist/runTerminals.js.map +1 -1
  107. package/dist/secretDiff.d.ts +29 -0
  108. package/dist/secretDiff.d.ts.map +1 -0
  109. package/dist/secretDiff.js +133 -0
  110. package/dist/secretDiff.js.map +1 -0
  111. package/dist/terminalOutcome.d.ts +22 -0
  112. package/dist/terminalOutcome.d.ts.map +1 -0
  113. package/dist/terminalOutcome.js +39 -0
  114. package/dist/terminalOutcome.js.map +1 -0
  115. package/dist/write-isolation.d.ts +11 -0
  116. package/dist/write-isolation.d.ts.map +1 -0
  117. package/dist/write-isolation.js +13 -0
  118. package/dist/write-isolation.js.map +1 -0
  119. package/package.json +17 -17
@@ -3,6 +3,7 @@ import { observeNativeSessionEvent, preflightCredentialProfile, preflightDefault
3
3
  import { writeRunTelemetryArtifact } from "./runTelemetryWriter.js";
4
4
  import { buildFileBackedSynthesisInput, materializeWinnerOutputs, stageFileBackedContext, writeCandidateAttemptArtifacts, } from "./candidateOutputs.js";
5
5
  import { processAttemptUsage } from "./attemptUsage.js";
6
+ import * as AC from "./attemptUsageCost.js";
6
7
  import { candidateRoster, convergenceOutcomeFacts, isWorkingCandidate, partitionCandidates, toCandidateEvidence, } from "./candidateEvidence.js";
7
8
  import { capabilityIntents } from "@claudexor/gateway";
8
9
  import { policyFindings } from "./policyFindings.js";
@@ -12,11 +13,21 @@ import { HarnessRunSpec, ModeKind as ModeKindSchema, SCHEMA_VERSION, TRUST_FULL_
12
13
  import { globalConfigDir, loadConfig, trustConfigPath } from "@claudexor/config";
13
14
  import { AnswerAssembly, CLAUDEXOR_ARTIFACT_DIR, CLAUDEXOR_BROWSER_ARTIFACT_SUBDIR, HarnessUnavailableError, summarizeDiffPaths as diffStats, withInactivityWatchdog, } from "@claudexor/core";
14
15
  import { assertRouteModelsAllowed } from "./modelGovernance.js";
15
- import { RequestRequirementsResolver } from "./requestRequirements.js";
16
- import { cancelledResult, failTerminally, guardAnnouncedRun, writeFailure, } from "./runTerminals.js";
16
+ import { governRouteEffort } from "./effortGovernance.js";
17
+ import { isFullAccess, RequestRequirementsResolver } from "./requestRequirements.js";
18
+ import { activateDelegationParent } from "./delegation-parent-activation.js";
19
+ import { routingFailureClassification } from "./routing-failure.js";
20
+ export { routingFailureClassification } from "./routing-failure.js";
21
+ import { runBounded } from "./run-bounded.js";
22
+ import { planPrompt } from "./plan-prompt.js";
23
+ import { resolveRunInputDefaults } from "./run-input-resolution.js";
24
+ import { createRootLedger } from "./root-ledger.js";
25
+ import { arbitrationBudgetOptions, decisionBudgetSummary } from "./decisionBudget.js";
26
+ import { buildRevisePrompt } from "./revisePrompt.js";
27
+ import { announcedRunContext, cancelledResult, failTerminally, guardAnnouncedRun, writeFailure, } from "./runTerminals.js";
17
28
  import { budgetFailureRecord, classifyBudgetFailure } from "./budgetFailure.js";
18
29
  import { assertOutputSchemaCompiles, finalizeStructuredOutput } from "./structuredOutput.js";
19
- import { transientRetryDelayMs, promptWithEngineConstraints, sleep, redactHarnessEvent, harnessEventPayload, formatFindings, renderSummary, observeBudgetSignals, rotateOnStall, recordCleanAttemptMetrics, envInheritance, transientRetryPolicy, reviewerTimeoutMs, harnessInactivityTimeoutMs, observeAuthSwitch, emitPrimaryDivergence, emitPoolDegraded, deliveryRefusalFailure, writeRaceDeliveryDecision, } from "./runSupport.js";
30
+ import { transientRetryDelayMs, promptWithEngineConstraints, sleep, redactHarnessEvent, harnessEventPayload, safeErrorMessage, renderSummary, observeBudgetSignals, rotateOnStall, recordCleanAttemptMetrics, envInheritance, transientRetryPolicy, reviewerTimeoutMs, harnessInactivityTimeoutMs, observeAuthSwitch, emitPrimaryDivergence, emitPoolDegraded, deliveryRefusalFailure, writeRaceDeliveryDecision, } from "./runSupport.js";
20
31
  import { candidateStatusInRouteContext, resolveReadOnlyRouteContext, } from "./routeContext.js";
21
32
  import { resolveAutoReviewerPanel, resolveExplicitReviewerPanel } from "./reviewerPanel.js";
22
33
  import { buildContinuation, } from "./continuity.js";
@@ -24,8 +35,10 @@ import { activePlanPointer, resolveContinuitySummary, workspaceAnchor, } from ".
24
35
  import { runDiffReview } from "./diffReview.js";
25
36
  import { rawScoutBundle, resolveDeepScanSynthesis, } from "./deepScanReducer.js";
26
37
  import { classifyAdapterThrow, createAttemptTelemetry, observeAttemptTelemetry, setAttemptOutcome, telemetrySummary, toolWarnings, unrecoveredToolErrors, webUnsatisfied, } from "./attemptTelemetry.js";
38
+ import * as delegateFailure from "./delegationFailure.js";
39
+ import * as secretDiff from "./secretDiff.js";
27
40
  import { dominantHarnessFailureCategory, harnessFailureNextActions } from "./harnessFailure.js";
28
- import { finalizeAttempt, readOnlyNoSuccessTerminal, resolveWorkReportEnvelope, unwrapWorkReportEnvelope, } from "./attemptFinalize.js";
41
+ import { finalizeAttempt, readOnlyNoSuccessTerminal, resolveWorkReportEnvelope, unrecoveredToolErrorFailure, unwrapWorkReportEnvelope, webEvidenceFailure, } from "./attemptFinalize.js";
29
42
  import { buildContinuationPacket, decideContinuation, synthesizeContinuationRequest, } from "./continuation.js";
30
43
  import { interactionChannelFor } from "./interaction.js";
31
44
  import { gateSpecsFromContract, renderTestsEvidence, resolveContractGates, } from "./contract-gates.js";
@@ -38,56 +51,15 @@ import { HarnessGateway } from "@claudexor/gateway";
38
51
  import { ReadinessLedger, evaluateConvergence, failureSignature, gatesPassed, reviewCandidate, revalidateFindings, runGates, } from "@claudexor/review";
39
52
  import { arbitrate } from "@claudexor/arbitration";
40
53
  import { buildSynthesisPlan, decideSynthesis } from "@claudexor/synthesis";
41
- import { attemptCostEvidence, attemptUsageCostSettlement, BudgetLedger, isBudgetTerminal, explainRanking, loadHarnessMetrics, promptFingerprint, unknownCostSettlement, rankHarnesses, reviewUsageCostSettlement, } from "@claudexor/budget";
54
+ import { attemptCostEvidence, attemptUsageCostSettlement, BudgetLedger, isBudgetTerminal, explainRanking, loadHarnessMetrics, promptFingerprint, rankHarnesses, reviewUsageCostSettlement, } from "@claudexor/budget";
42
55
  import { readTextSafe, appendLine, assertNoInlineSecretValues, containsSecretLikeToken, DELEGATION_ENV, hashJson, newId, noProjectRepoRoot, nowIso, redactSecrets, safeInvoke, sha256, userConfigDir, writeText, } from "@claudexor/util";
43
- /** A routed candidate adapter plus its manifest capabilities and user settings. */
44
- /** The two access profiles that map to codex `danger-full-access` / an
45
- * unsandboxed lane — the only ones under which a full-access-requiring MCP
46
- * injection (the belt on codex) can reach the daemon. */
47
- export function isFullAccess(access) {
48
- return access === "full" || access === "external_sandbox_full";
49
- }
50
- /**
51
- * A routing preflight refusal (`RoutingPreflightError`: quality routing with no
52
- * comparable user-declared tier for the intent) is a CONFIGURATION error, not a
53
- * harness-availability problem (A-1/D-9/#22). Classifying it as
54
- * `harness_unavailable` sent the operator to re-auth or wait for a harness; the
55
- * real fix is to configure a tier or change the routing goal. Detected by the
56
- * typed `code` (robust across duplicate `@claudexor/budget` package copies) so
57
- * EVERY strategy's routing catch (ask/agent/plan/deep-scan/council) classifies
58
- * it identically. Returns the failure category + matching remediation.
59
- */
60
- export function routingFailureClassification(err) {
61
- const isPreflightRefusal = !!err &&
62
- typeof err === "object" &&
63
- err.code === "routing_preflight_refused";
64
- if (isPreflightRefusal) {
65
- return { category: "config_error", nextActions: harnessFailureNextActions("config_error") };
66
- }
67
- return { category: "harness_unavailable" };
68
- }
56
+ import { assertWriteIsolation } from "./write-isolation.js";
69
57
  const LABELS = "ABCDEFGHIJ".split("");
70
58
  const NO_PROJECT_ROOT = noProjectRepoRoot();
71
59
  /** Concurrency cap for parallel candidates/explorers (locked decision: min(n, 4)). */
72
60
  const MAX_PARALLEL_CANDIDATES = 4;
73
61
  /** Default wait for one interactive answer before a benign decline. */
74
62
  const DEFAULT_INTERACTION_TIMEOUT_MS = 900_000;
75
- /** Run `work` over `items` with bounded concurrency, preserving item order via index. */
76
- async function runBounded(items, limit, work) {
77
- if (items.length === 0)
78
- return;
79
- const concurrency = Math.max(1, Math.min(limit, items.length));
80
- let next = 0;
81
- const workers = Array.from({ length: concurrency }, async () => {
82
- for (;;) {
83
- const idx = next++;
84
- if (idx >= items.length)
85
- return;
86
- await work(items[idx], idx);
87
- }
88
- });
89
- await Promise.all(workers);
90
- }
91
63
  export class Orchestrator {
92
64
  deps;
93
65
  gateway;
@@ -115,7 +87,7 @@ export class Orchestrator {
115
87
  });
116
88
  }
117
89
  async run(input) {
118
- const resolved = this.resolveRunInput(input);
90
+ let resolved = this.resolveRunInput(input);
119
91
  // INV-062 at the ENGINE boundary: every surface fences prompts already,
120
92
  // but a direct embedder (or the daemon-less local REPL fallback) reaches
121
93
  // this entry without one. Prompts, per-run instructions, AND outputSchema
@@ -134,13 +106,30 @@ export class Orchestrator {
134
106
  throw new Error(`unknown mode: ${String(resolved.mode)}`);
135
107
  }
136
108
  const mode = parsedMode.data;
137
- // denyPaths is enforced by the post-diff policy gate BEFORE delivery, which
138
- // only exists on envelope/isolated runs — an in-place run mutates the live
139
- // tree directly, so the gate could not contain a violation. Refuse loudly
140
- // rather than accept a knob the engine cannot honor (INV-023).
141
- if ((resolved.denyPaths?.length ?? 0) > 0 && resolved.inPlace === true) {
142
- 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");
109
+ if (resolved.delegate === true && mode !== "agent") {
110
+ throw new Error(`Delegate is an agent-only strategy (got mode=${mode})`);
143
111
  }
112
+ const runId = resolved.runId ?? newId("run");
113
+ resolved = {
114
+ ...resolved,
115
+ runId,
116
+ taskId: resolved.taskId ?? newId("task"),
117
+ };
118
+ if (resolved.delegate === true) {
119
+ resolved = {
120
+ ...resolved,
121
+ delegationParentRunId: runId,
122
+ };
123
+ }
124
+ const projectProtectedPaths = mode === "agent" ? this.projectConfig(resolved.repoRoot).constraints.protected_paths : [];
125
+ assertWriteIsolation({
126
+ mode,
127
+ protectedPaths: projectProtectedPaths,
128
+ denyPaths: resolved.denyPaths,
129
+ inPlace: resolved.inPlace,
130
+ repoRoot: resolved.repoRoot,
131
+ executionRoot: this.execRootOf(resolved),
132
+ });
144
133
  // outputSchema constrains the run's final ANSWER. It is honored exactly
145
134
  // where a final answer is delivered (agent race incl. synthesis, and ask);
146
135
  // every other strategy refuses loudly rather than carrying a contract the
@@ -198,13 +187,22 @@ export class Orchestrator {
198
187
  case "plan":
199
188
  return this.runPlan(resolved, announce);
200
189
  }
190
+ }, async ({ runId }) => {
191
+ const authority = this.deps.delegationBudgetAuthority;
192
+ if (!authority?.hasParent(runId))
193
+ return;
194
+ authority.beginParentClose(runId);
195
+ await authority.waitForChildren(runId);
201
196
  },
202
197
  // Single per-run terminalization hook: release the routing-rationale map
203
198
  // entry on EVERY terminal (incl. a run that died before its telemetry
204
199
  // writer ran, which is the leak this closes).
205
- (runId) => this.routingRationaleByRun.delete(runId));
200
+ (runId) => {
201
+ this.routingRationaleByRun.delete(runId);
202
+ this.deps.delegationBudgetAuthority?.releaseRun(runId);
203
+ });
206
204
  }
207
- async resolveReviewers(cwd, runAuthPreference) {
205
+ async resolveReviewers(cwd, runAuthPreference, onIgnoredSetting) {
208
206
  if (this.deps.reviewers)
209
207
  return this.deps.reviewers;
210
208
  if (this.deps.reviewerPanel && this.deps.reviewerPanel.length > 0) {
@@ -215,10 +213,8 @@ export class Orchestrator {
215
213
  registry: this.deps.registry,
216
214
  harnessSettings: this.config(cwd)?.global.harnesses ?? {},
217
215
  authPreferenceFor: (id) => this.authPreferenceForHarness(cwd, id, runAuthPreference),
218
- }, {
219
- reviewerModels: this.deps.reviewerModels,
220
- reviewerEfforts: this.deps.reviewerEfforts,
221
- });
216
+ onIgnoredSetting,
217
+ }, { reviewerModels: this.deps.reviewerModels, reviewerEfforts: this.deps.reviewerEfforts });
222
218
  }
223
219
  /**
224
220
  * Resolve reviewers INSIDE a strategy, after run-dir creation: an explicit
@@ -229,7 +225,9 @@ export class Orchestrator {
229
225
  */
230
226
  async resolveReviewersWithArtifacts(input, log, store, paths, runId, taskId, mode) {
231
227
  try {
232
- return { reviewers: await this.resolveReviewers(input.repoRoot, input.authPreference) };
228
+ // Auto-panel dropped knobs (reviewerEfforts) → ignored-settings channel (QA-070):
229
+ const warn = (d) => void log.emit("review.preflight", { ignored_settings: [d] });
230
+ return { reviewers: await this.resolveReviewers(input.repoRoot, input.authPreference, warn) };
233
231
  }
234
232
  catch (err) {
235
233
  const message = safeErrorMessage(err);
@@ -411,89 +409,11 @@ export class Orchestrator {
411
409
  * expand to n. Fails loudly if nothing can perform the intent.
412
410
  */
413
411
  resolveRunInput(input) {
414
- if (input.contextMode === "off" &&
415
- !(input.mode === "ask" && input.repoRoot === NO_PROJECT_ROOT)) {
416
- throw new Error("contextMode 'off' is only supported for Ask without a repoRoot");
417
- }
418
- const cfg = this.config(input.repoRoot);
419
- const configuredPool = cfg?.global.routing.eligible_harnesses;
420
- const harnesses = input.harnesses ?? (configuredPool && configuredPool.length > 0 ? configuredPool : undefined);
421
- // GH #25 precedence: an explicit --primary-harness wins and is validated
422
- // against the pool; else a single-item explicit pool infers itself as
423
- // primary (shipped in #34); else the configured default primary applies.
424
- const explicitPrimary = input.primaryHarness;
425
- const configPrimary = cfg?.global.routing.primary_harness;
426
- const primaryHarness = explicitPrimary ??
427
- (input.harnesses?.length === 1 ? input.harnesses[0] : undefined) ??
428
- configPrimary ??
429
- undefined;
430
- if (primaryHarness &&
431
- harnesses &&
432
- harnesses.length > 0 &&
433
- !harnesses.includes(primaryHarness)) {
434
- if (explicitPrimary) {
435
- // An explicit primary must be a member of the eligible pool (authoritative).
436
- throw new Error(`primary harness '${explicitPrimary}' is not in the eligible harness pool (${harnesses.join(", ")}); ` +
437
- `pass --primary-harness as one of [${harnesses.join(", ")}], or add '${explicitPrimary}' to --harness`);
438
- }
439
- // GH #25 remainder: a MULTI-harness pool whose CONFIGURED default primary
440
- // is absent, with no --primary-harness pinned, is ambiguous — the engine
441
- // must not silently reroute. Refuse with a structured, copy-pasteable fix
442
- // naming the pool, the missing primary, and the exact flag to add.
443
- throw new HarnessUnavailableError(`ambiguous primary harness: the configured default primary '${primaryHarness}' is not in the selected pool [${harnesses.join(", ")}], ` +
444
- `and no --primary-harness was given. Pin one explicitly, e.g. \`--primary-harness ${harnesses[0]}\` ` +
445
- `(or another of [${harnesses.join(", ")}]).`);
446
- }
447
- if (input.web && input.externalContextPolicy && input.web !== input.externalContextPolicy) {
448
- throw new Error(`contradictory web policy: web='${input.web}' vs externalContextPolicy='${input.externalContextPolicy}' (pass one, or equal values)`);
449
- }
450
- const web = input.web ?? input.externalContextPolicy ?? "auto";
451
- // INV-103: scalar `model` expands only to the resolved primary, never the pool;
452
- // an explicit per-harness map wins. Unknown map keys fail loudly (INV-021).
453
- const knownHarnessIds = new Set(this.deps.registry.keys());
454
- for (const key of Object.keys(input.models ?? {})) {
455
- if (!knownHarnessIds.has(key)) {
456
- throw new Error(`models map names unknown harness '${key}' (registered: ${[...knownHarnessIds].sort().join(", ")}); ` +
457
- `run \`claudexor harness list --all\``);
458
- }
459
- }
460
- const models = { ...input.models };
461
- if (input.model) {
462
- const scalarTarget = primaryHarness ?? (harnesses && harnesses.length === 1 ? harnesses[0] : undefined);
463
- if (!scalarTarget) {
464
- throw new Error(`a scalar model ('${input.model}') is ambiguous without a primary harness: ` +
465
- `the pool is ${harnesses && harnesses.length > 0 ? `[${harnesses.join(", ")}]` : "auto-resolved"} — ` +
466
- `set a primary harness, pass exactly one --harness, or use a harness-scoped model map`);
467
- }
468
- models[scalarTarget] ??= input.model;
469
- }
470
- // QA-035: FREEZE the config-derived per-harness default_model into the
471
- // resolved model map at initial normalization, exactly like an explicit
472
- // input. Without this the TaskContract records `routing_models: {}` and an
473
- // Exact Retry re-resolves the model against CURRENT settings — silently
474
- // changing the route after a settings edit. A per-turn/scalar value already
475
- // set wins (??=). Only a known resolved pool can be frozen here; a pure
476
- // auto pool's lanes are not yet known (documented seam).
477
- const harnessCfg = cfg?.global.harnesses ?? {};
478
- for (const hid of harnesses ?? []) {
479
- const def = harnessCfg[hid]?.default_model;
480
- if (def)
481
- models[hid] ??= def;
482
- }
483
- return {
484
- ...input,
485
- harnesses,
486
- primaryHarness,
487
- model: undefined,
488
- models,
489
- routingGoal: input.routingGoal ??
490
- this.deps.routingGoal ??
491
- cfg?.project.budget?.routing_goal ??
492
- cfg?.global.routing.goal ??
493
- "auto",
494
- web,
495
- externalContextPolicy: web,
496
- };
412
+ return resolveRunInputDefaults(input, {
413
+ config: this.config(input.repoRoot),
414
+ registryIds: this.deps.registry.keys(),
415
+ routingGoal: this.deps.routingGoal,
416
+ });
497
417
  }
498
418
  async resolveCandidateAdapters(input, intent, ledger, log, routeContext,
499
419
  /** QA-034: when provided, the pool-ordering rationale is recorded under this
@@ -712,6 +632,14 @@ export class Orchestrator {
712
632
  access: requiredAccess,
713
633
  }),
714
634
  denyRequirement: this.requestRequirements.resolveDenyPaths(id, (input.denyPaths?.length ?? 0) > 0),
635
+ delegationRequirement: this.requestRequirements.resolveDelegation({
636
+ harnessId: id,
637
+ requested: input.delegate === true,
638
+ runtimeAvailable: input.delegationBelt != null,
639
+ manifestCapable: manifest.capability_profile.mcp_injection,
640
+ requiresFullAccess: manifest.capability_profile.mcp_injection_requires_full_access,
641
+ fullAccess: isFullAccess(requiredAccess),
642
+ }),
715
643
  effortLevels: manifest.capabilities.effort_levels,
716
644
  knownModels: manifest.capabilities.known_models,
717
645
  // A selected profile's credential_kind IS the route (round-18 #2);
@@ -758,6 +686,7 @@ export class Orchestrator {
758
686
  }
759
687
  emitPrimaryDivergence(log, input.primaryHarness, ordered, pool, dropped);
760
688
  const n = input.n ?? ordered.length;
689
+ const selectionOrder = ordered;
761
690
  const out = [];
762
691
  if (droppedLanes.length > 0 && !allowDuplicateFill) {
763
692
  // QA-043: lanes were dropped from an AUTO best-of pool (an explicit pool
@@ -766,15 +695,15 @@ export class Orchestrator {
766
695
  // masks the omission. Clamp to distinct survivors and disclose below.
767
696
  // (Deep-scan sets allowDuplicateFill: its width is scout coverage, not
768
697
  // harness diversity, so a dropped lane must not cut the scout count.)
769
- for (let i = 0; i < Math.min(n, ordered.length); i++)
770
- out.push(ordered[i]);
698
+ for (let i = 0; i < Math.min(n, selectionOrder.length); i++)
699
+ out.push(selectionOrder[i]);
771
700
  }
772
701
  else {
773
702
  // No lane was dropped: a pool smaller than `n` is an intentional
774
703
  // best-of-N on the available harness(es) (e.g. explicit `--harness codex
775
704
  // -n 3`), so the historical width fill is preserved.
776
705
  for (let i = 0; i < n; i++)
777
- out.push(ordered[i % ordered.length]);
706
+ out.push(selectionOrder[i % selectionOrder.length]);
778
707
  }
779
708
  // Disclose an auto-pool omission / width clamp once, with the
780
709
  // requested-vs-effective route receipt (never silent — QA-043).
@@ -786,25 +715,24 @@ export class Orchestrator {
786
715
  droppedLanes,
787
716
  });
788
717
  this.requestRequirements.requireEffectiveBrowser(input.browser === true, out.map((lane) => lane.browserRequirement));
789
- // Delegation belt (D32): agent-only, and only on a lane whose adapter can
790
- // inject MCP servers. A requested delegate with NO injecting lane is a typed
791
- // preflight refusal naming the harness(es) never a silently dropped belt.
792
- if (input.delegate === true && !out.some((lane) => lane.supportsMcpInjection)) {
793
- const names = [...new Set(out.map((lane) => lane.adapter.id))].join(", ");
794
- throw new HarnessUnavailableError(`--delegate requires a harness that can host the Claudexor delegation belt (capability_profile.mcp_injection); the routed harness(es) [${names}] cannot inject MCP servers — choose claude or codex, or drop --delegate`);
795
- }
796
- // A belt-injecting lane may still be UNABLE to reach the daemon at its
797
- // access: codex's workspace-write seatbelt cancels the belt's daemon-crossing
798
- // MCP call, so codex only hosts the belt at FULL access (same as its browser
799
- // MCP). If EVERY injecting lane requires full access but runs below it, the
800
- // belt would be injected only to be silently cancelled by the sandbox — the
801
- // exact non-delegation this guard prevents. Refuse with the real remedy.
718
+ // Owner decision (2026-07-26): known PRE-START belt unavailability does
719
+ // not discard the requested Agent work. Continue without Delegate and emit
720
+ // a durable typed warning. Once a descriptor is injected, typed startup
721
+ // failure stays terminal in attemptTelemetry (no mid-attempt downgrade).
802
722
  if (input.delegate === true) {
803
- const injecting = out.filter((lane) => lane.supportsMcpInjection);
804
- const canHostBelt = injecting.some((lane) => !lane.mcpInjectionRequiresFullAccess || isFullAccess(lane.adapterAccess));
805
- if (!canHostBelt) {
806
- const names = [...new Set(injecting.map((lane) => lane.adapter.id))].join(", ");
807
- throw new HarnessUnavailableError(`--delegate needs a belt-hosting lane at full access: [${names}] can inject MCP servers but sandbox-cancel the delegation belt below full access (capability_profile.mcp_injection_requires_full_access) — re-run with --access full, or route a lane (e.g. claude) that hosts the belt at workspace_write`);
723
+ const unavailable = out
724
+ .map((lane) => lane.delegationRequirement)
725
+ .filter((resolution) => !resolution.effective);
726
+ if (unavailable.length > 0) {
727
+ log?.emit("delegation.belt.degraded", {
728
+ requested: true,
729
+ effective: out.some((lane) => lane.delegationRequirement.effective),
730
+ reason: unavailable[0]?.reason ?? "runtime_unavailable",
731
+ lanes: unavailable.map((resolution) => ({
732
+ harness_id: resolution.harness_id,
733
+ reason: resolution.reason,
734
+ })),
735
+ });
808
736
  }
809
737
  }
810
738
  // outputSchema is MANDATORY (Quiz-6a): a selected lane that cannot
@@ -841,6 +769,8 @@ export class Orchestrator {
841
769
  */
842
770
  orderPool(pool, input, intent, statusById, ledger, runId) {
843
771
  let ordered = pool;
772
+ let rationale = null;
773
+ let selectionReason = null;
844
774
  if (pool.length > 0) {
845
775
  const routeLedger = ledger ?? new BudgetLedger();
846
776
  const config = this.config(input.repoRoot).global;
@@ -917,21 +847,40 @@ export class Orchestrator {
917
847
  intent,
918
848
  qualityTiers: config.routing.quality_tiers,
919
849
  ledger: routeLedger,
850
+ now: Date.now(), // ONE instant for the sort AND the rationale below
920
851
  };
921
852
  const ranked = rankHarnesses(remaining, routeCtx)
922
853
  .map((candidate) => byId.get(candidate.harnessId))
923
854
  .filter((candidate) => Boolean(candidate));
924
- // QA-034: record the typed rationale ONCE at pool ordering (run evidence,
925
- // not an event). Axis-aligned with rankHarnesses above so the persisted
926
- // reason can never disagree with the order actually taken.
927
- if (runId)
928
- this.routingRationaleByRun.set(runId, explainRanking(remaining, routeCtx));
855
+ rationale = explainRanking(remaining, routeCtx);
929
856
  ordered = ranked;
930
857
  }
858
+ if (input.delegate === true && input.primaryHarnessExplicit !== true) {
859
+ const delegateFirst = [
860
+ ...ordered.filter((lane) => lane.delegationRequirement.effective),
861
+ ...ordered.filter((lane) => !lane.delegationRequirement.effective),
862
+ ];
863
+ if (delegateFirst.some((lane, index) => lane !== ordered[index])) {
864
+ ordered = delegateFirst;
865
+ selectionReason = "delegate_effective_first";
866
+ }
867
+ }
931
868
  if (input.primaryHarness) {
932
869
  const primary = ordered.find((r) => r.adapter.id === input.primaryHarness);
933
- if (primary)
870
+ if (primary && primary !== ordered[0]) {
934
871
  ordered = [primary, ...ordered.filter((r) => r !== primary)];
872
+ selectionReason = "explicit_primary";
873
+ }
874
+ }
875
+ // QA-034: persist the FINAL selected order, including request constraints
876
+ // that intentionally override the underlying cost/quota ranking. This is
877
+ // what keeps route evidence aligned with the lane actually executed.
878
+ if (runId && rationale) {
879
+ this.routingRationaleByRun.set(runId, {
880
+ ...rationale,
881
+ order: ordered.map((lane) => lane.adapter.id),
882
+ reason: selectionReason ?? rationale.reason,
883
+ });
935
884
  }
936
885
  return ordered;
937
886
  }
@@ -1067,7 +1016,7 @@ export class Orchestrator {
1067
1016
  projectCommands: cfg.tests?.commands ?? [],
1068
1017
  });
1069
1018
  const commands = resolvedGates.commands;
1070
- const protectedPaths = [];
1019
+ const protectedPaths = [...new Set(cfg.constraints.protected_paths)];
1071
1020
  const autoProtectedPaths = resolvedGates.autoProtectedPaths;
1072
1021
  const protectedPathApprovals = [
1073
1022
  ...new Map([...(input.protectedPathApprovals ?? [])].map((approval) => [approval.path, approval])).values(),
@@ -1078,6 +1027,11 @@ export class Orchestrator {
1078
1027
  created_at: nowIso(),
1079
1028
  repo: { root: input.repoRoot, base_ref: input.baseRef ?? "HEAD", dirty_policy: "snapshot" },
1080
1029
  mode: { kind: mode },
1030
+ delegation_requested: input.delegate === true,
1031
+ run_lineage: {
1032
+ parent_run_id: input.parentRunId ?? null,
1033
+ delegated_from_run_id: input.delegatedFromRunId ?? null,
1034
+ },
1081
1035
  user_intent: { raw: redactSecrets(input.prompt) },
1082
1036
  // Redacted for symmetry with user_intent.raw — a no-op on fenced input
1083
1037
  // (the inline-secret fence already blocked any secret-like value at every
@@ -1163,13 +1117,15 @@ export class Orchestrator {
1163
1117
  * a WRITING agent intent (the delegator integrates results in its workspace;
1164
1118
  * read lanes and reviewers have nothing to delegate). */
1165
1119
  delegationBeltFor(input, intent, routed, resolvedBudget) {
1166
- if (!input?.delegate || !input.delegationBelt || !routed.supportsMcpInjection)
1120
+ if (!input?.delegate ||
1121
+ !input.delegationBelt ||
1122
+ !input.delegationParentRunId ||
1123
+ !routed.delegationRequirement.effective)
1167
1124
  return [];
1168
1125
  // A lane that sandbox-cancels the belt below full access (codex) must NOT
1169
- // receive a belt it cannot use that is the silent non-delegation. The
1170
- // preflight already refused a run whose ONLY injecting lanes are such lanes
1171
- // below full access; here we simply skip injecting into an individual lane
1172
- // that cannot host it, so a mixed pool keeps the belt on the lanes that can.
1126
+ // receive a belt it cannot use. Per-lane requirement resolution records the
1127
+ // typed degradation, while a mixed pool keeps the belt on lanes that can
1128
+ // host it.
1173
1129
  if (routed.mcpInjectionRequiresFullAccess && !isFullAccess(routed.adapterAccess))
1174
1130
  return [];
1175
1131
  const writingIntents = ["implement", "create_from_scratch", "repair"];
@@ -1185,6 +1141,8 @@ export class Orchestrator {
1185
1141
  ...input.delegationBelt,
1186
1142
  env: {
1187
1143
  ...input.delegationBelt.env,
1144
+ [DELEGATION_ENV.parentRunId]: input.delegationParentRunId,
1145
+ [DELEGATION_ENV.repoRoot]: input.repoRoot,
1188
1146
  [DELEGATION_ENV.budget]: JSON.stringify(resolvedBudget),
1189
1147
  },
1190
1148
  },
@@ -1278,17 +1236,16 @@ export class Orchestrator {
1278
1236
  // per-harness map, which beats the per-harness settings default. There is
1279
1237
  // no run-global model.
1280
1238
  const model = overrideModel ?? contract.routing_models[routed.adapter.id] ?? s?.defaultModel ?? null;
1281
- // Effort disclosure (INV-105): a requested effort on a harness with no
1282
- // declared ladder is DISCLOSED as ignored, never silently dropped.
1283
- // Harness-scoped resolution mirrors the model line above: the contract's
1284
- // FROZEN per-lane effort (QA-035) is authoritative so Exact Retry replays it
1285
- // without re-reading settings; a per-attempt `effortHint` (or settings
1286
- // default) applies only to a lane the contract did not freeze.
1287
- let effort = contract.routing_efforts[routed.adapter.id] ?? effortHint ?? s?.effort ?? null;
1288
- if (effort && routed.effortLevels.length === 0) {
1289
- ignored.push(`effort=${effort} (manifest capabilities.effort_levels is empty for ${routed.adapter.id})`);
1290
- effort = null;
1291
- }
1239
+ // Effort disclosure (INV-105) against the harness's advertised ladder. This
1240
+ // gate only DISCLOSES an unplaceable level; the clamp belongs to the adapter,
1241
+ // which resolves against the catalog for the profile env the child runs in
1242
+ // (the manifest here is the DEFAULT account's see effortGovernance.ts). The
1243
+ // contract's FROZEN per-lane effort (QA-035) wins so Exact Retry replays it
1244
+ // without re-reading settings; `effortHint`/settings apply only to an unfrozen lane.
1245
+ const governed = governRouteEffort(contract.routing_efforts[routed.adapter.id] ?? effortHint ?? s?.effort ?? null, { id: routed.adapter.id, ...routed });
1246
+ const effort = governed.effort;
1247
+ if (governed.ignored)
1248
+ ignored.push(governed.ignored);
1292
1249
  return {
1293
1250
  model,
1294
1251
  effort,
@@ -1489,7 +1446,7 @@ export class Orchestrator {
1489
1446
  const browserServerName = spec.browser ? "browser" : null;
1490
1447
  const telemetry = createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required ||
1491
1448
  knobs.webPolicy === "cached" ||
1492
- knobs.webPolicy === "live", effectiveWebMode ?? knobs.webPolicy, [routed.browserRequirement, routed.denyRequirement], knobs.model, beltServerName, browserServerName);
1449
+ knobs.webPolicy === "live", effectiveWebMode ?? knobs.webPolicy, [routed.browserRequirement, routed.denyRequirement, routed.delegationRequirement], knobs.model, beltServerName, browserServerName);
1493
1450
  let activeSessionId = spec.session_id;
1494
1451
  const onAbort = () => {
1495
1452
  void adapter.cancel?.(activeSessionId)?.catch(() => { });
@@ -1569,10 +1526,9 @@ export class Orchestrator {
1569
1526
  observeNativeSessionEvent(runInput, adapter.id, safeEv);
1570
1527
  observeAuthSwitch(log, adapter.id, attemptId, safeEv);
1571
1528
  observeAttemptTelemetry(telemetry, safeEv);
1572
- // QA-024: the injected delegation belt's MCP server reported `failed`
1573
- // to start. Disclose it ONCE as a typed run event the moment the
1574
- // `started` frame reveals it the harness is about to run without
1575
- // `mcp__<belt>__*` tools and may degrade to its own native subagent.
1529
+ // QA-024: the injected delegation belt's MCP server reported a
1530
+ // terminal startup failure. Disclose it ONCE while live; recoverable
1531
+ // exact tool-result failures are evaluated at attempt finalization.
1576
1532
  // The terminal outcome axis (delegationBeltUnavailable) reflects it
1577
1533
  // too; this event makes the failure visible while the run is live.
1578
1534
  if (telemetry.delegationBelt.requested &&
@@ -1721,22 +1677,22 @@ export class Orchestrator {
1721
1677
  }
1722
1678
  const attemptStreamEndedMs = Date.now();
1723
1679
  if (webUnsatisfied(telemetry)) {
1724
- errors.push(`web evidence unsatisfied: ${telemetry.web.errorSummary ?? (telemetry.web.attempted ? "web tool failed without verified recovery" : "web evidence required but never attempted")}`);
1680
+ errors.push(webEvidenceFailure(telemetry.web));
1725
1681
  }
1726
- const diff = await wsm.diff(envelope);
1727
1682
  // D-16: un-nest {work_report, output} so answer.md persists the OUTPUT, not the envelope.
1728
1683
  const unwrapped = unwrapWorkReportEnvelope(answer.machineText() ?? "", workReportMode, {
1729
1684
  sideToolReport: telemetry.sideToolWorkReport ?? undefined,
1730
1685
  });
1731
- // X119: persist the VERBATIM redacted bytes; trim ONLY for the emptiness check.
1732
1686
  const redacted = redactSecrets(unwrapped.deliverable);
1733
- const answerText = redacted.trim().length > 0 ? redacted : undefined;
1687
+ const candidateAnswer = redacted.trim().length > 0 ? redacted : undefined;
1688
+ const { diff, refusal: secretDiffRefusal } = await secretDiff.quarantineCandidateWorkspace(wsm, envelope, inPlaceEnvelope, candidateAnswer);
1689
+ harnessErrored = secretDiff.recordSecretDiffRefusal(secretDiffRefusal, errors, harnessErrored);
1690
+ const answerText = secretDiffRefusal ? undefined : candidateAnswer;
1734
1691
  const deliverableEvidence = diff.trim().length > 0 || Boolean(answerText);
1735
- // Cancelled attempts skip gates entirely: the operator asked to
1736
- // stop NOW; running a 600s-per-gate suite after the abort delays the ack
1692
+ // Cancelled attempts skip gates: running a 600s-per-gate suite delays the ack
1737
1693
  // and burns compute on a result nobody will adopt. Diff/attempt.yaml
1738
1694
  // still land, so partial work stays inspectable.
1739
- const gateSignalAborted = signal?.aborted === true;
1695
+ const gateSignalAborted = signal?.aborted === true || secretDiffRefusal !== undefined;
1740
1696
  if (!gateSignalAborted) {
1741
1697
  log?.emit("gate.started", {
1742
1698
  attempt_id: attemptId,
@@ -1766,6 +1722,14 @@ export class Orchestrator {
1766
1722
  });
1767
1723
  }
1768
1724
  const webBlocked = webUnsatisfied(telemetry);
1725
+ // A descriptor that was injected and then reported failed is past the
1726
+ // pre-start degradation boundary. Hard-fail this attempt; never continue as
1727
+ // ordinary Agent or let a native vendor subagent masquerade as belt work.
1728
+ const delegationError = delegateFailure.delegationFailureError(telemetry);
1729
+ if (delegationError) {
1730
+ harnessErrored = true;
1731
+ errors.push(delegationError);
1732
+ }
1769
1733
  // D-16 unified finalizer: fold the WorkReport / context signals into the
1770
1734
  // deliverable + work_state. A broken contract on a constrained route
1771
1735
  // elevates harnessErrored (never a prose success).
@@ -1790,14 +1754,6 @@ export class Orchestrator {
1790
1754
  workState: finalized.workState,
1791
1755
  });
1792
1756
  const attemptDir = join(paths.attemptsDir, attemptId);
1793
- try {
1794
- assertNoSecretLikeTokens("candidate patch diff", diff);
1795
- }
1796
- catch (err) {
1797
- // The stream already settled real spend; a post-stream assertion throw
1798
- // must carry it so the slot catch settles the TRUE cost, not 0.
1799
- throw Object.assign(err instanceof Error ? err : new Error(String(err)), { costUsd: cost });
1800
- }
1801
1757
  recordCleanAttemptMetrics(globalConfigDir(), adapter.id, {
1802
1758
  costUsd: cost,
1803
1759
  streamMs: attemptStreamEndedMs - attemptStartedMs,
@@ -1805,11 +1761,13 @@ export class Orchestrator {
1805
1761
  aborted: signal?.aborted === true,
1806
1762
  authMode: telemetry.authMode,
1807
1763
  });
1808
- const producedFiles = writeCandidateAttemptArtifacts({
1764
+ const producedFiles = AC.withAttemptFailureCost(() => writeCandidateAttemptArtifacts({
1809
1765
  store,
1810
1766
  attemptDir,
1811
1767
  worktreePath: envelope.worktree_path,
1812
1768
  diff,
1769
+ persistPatch: secretDiffRefusal === undefined,
1770
+ persistProducedMedia: secretDiffRefusal === undefined,
1813
1771
  answerText,
1814
1772
  record: {
1815
1773
  attempt_id: attemptId,
@@ -1821,9 +1779,14 @@ export class Orchestrator {
1821
1779
  errors: errors.slice(0, 5),
1822
1780
  ...telemetrySummary(telemetry),
1823
1781
  outcome: telemetry.outcome,
1782
+ ...(secretDiffRefusal ? { secret_diff_refusal: secretDiffRefusal } : {}),
1824
1783
  gates: gates.map((g) => ({ id: g.id, status: g.status })),
1825
1784
  branch: envelope.branch_name,
1826
1785
  },
1786
+ }), {
1787
+ totalUsd: cost,
1788
+ estimated: costEstimated,
1789
+ settlement: attemptUsageCostSettlement(cost, costEstimated, attemptId, adapter.id, telemetry.authMode, telemetry.usageCost),
1827
1790
  });
1828
1791
  return {
1829
1792
  attemptId,
@@ -1840,6 +1803,7 @@ export class Orchestrator {
1840
1803
  costEstimated,
1841
1804
  errors: errors.slice(0, 8),
1842
1805
  telemetry,
1806
+ ...(secretDiffRefusal ? { secretDiffRefusal } : {}),
1843
1807
  outcomeClass: finalized.outcomeClass,
1844
1808
  };
1845
1809
  }
@@ -1985,16 +1949,7 @@ export class Orchestrator {
1985
1949
  safeInvoke(input.onRunStart, { runId, taskId, runDir: paths.root });
1986
1950
  log.emit("run.created", { mode, prompt: redactSecrets(input.prompt) });
1987
1951
  const ledger = this.rootLedger(input, contract, log);
1988
- announce?.({
1989
- log,
1990
- store,
1991
- paths,
1992
- runId,
1993
- taskId,
1994
- mode,
1995
- phase: "race",
1996
- spend: () => ledger.spend(),
1997
- });
1952
+ announce?.(announcedRunContext({ log, store, paths, runId, taskId, mode, phase: "race" }, ledger, () => this.deps.delegationBudgetAuthority?.hasParent(runId) === true));
1998
1953
  store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
1999
1954
  log.emit("task.contract.created", { task_contract_hash: hashJson(contract) });
2000
1955
  // Write modes need a git boundary for worktree isolation and honest diffs.
@@ -2083,6 +2038,7 @@ export class Orchestrator {
2083
2038
  candidates: [],
2084
2039
  };
2085
2040
  }
2041
+ activateDelegationParent(this.deps.delegationBudgetAuthority, input, runId, ledger, adapters, log);
2086
2042
  const reviewersOutcome = await this.resolveReviewersWithArtifacts(input, log, store, paths, runId, taskId, mode);
2087
2043
  if ("failed" in reviewersOutcome)
2088
2044
  return reviewersOutcome.failed;
@@ -2307,7 +2263,7 @@ export class Orchestrator {
2307
2263
  effectiveRun = contRun;
2308
2264
  }
2309
2265
  catch (err) {
2310
- ledger.settle(contLeaseId, unknownCostSettlement("continuation-error", 0));
2266
+ ledger.settle(contLeaseId, AC.attemptFailureCost(err, "continuation-error", 0).settlement);
2311
2267
  log.emit("harness.completed", {
2312
2268
  harness_id: adapter.id,
2313
2269
  attempt_id: contAttemptId,
@@ -2331,18 +2287,9 @@ export class Orchestrator {
2331
2287
  envelope = undefined;
2332
2288
  }
2333
2289
  catch (err) {
2334
- // Envelope creation (or another pre-stream step) failed; stream errors
2335
- // are absorbed inside runCandidateInEnvelope with their real cost. A
2336
- // post-stream throw (e.g. the secret-token assertion) carries its
2337
- // streamed spend on the error — settle the TRUE cost, never launder
2338
- // real spend down to 0.
2339
- const carriedCost = typeof err?.costUsd === "number"
2340
- ? err.costUsd
2341
- : 0;
2342
- ledger.settle(slot.leaseId, unknownCostSettlement("post-stream-error", carriedCost));
2290
+ const failureCost = AC.attemptFailureCost(err, "post-stream-error", 0);
2291
+ ledger.settle(slot.leaseId, failureCost.settlement);
2343
2292
  const message = safeErrorMessage(err);
2344
- // envelope is still undefined when wsm.create() itself threw — that is
2345
- // a workspace-phase infrastructure failure, not a harness error.
2346
2293
  const infraPhase = envelope === undefined ? "workspace" : "harness";
2347
2294
  log.emit("harness.completed", {
2348
2295
  harness_id: adapter.id,
@@ -2351,26 +2298,22 @@ export class Orchestrator {
2351
2298
  error: message,
2352
2299
  phase: infraPhase,
2353
2300
  });
2354
- // Minimal attempt record so failure.yaml's rawDetailRef never dangles.
2355
- store.writeYaml(join(paths.attemptsDir, slot.attemptId, "attempt.yaml"), {
2356
- attempt_id: slot.attemptId,
2357
- harness_id: adapter.id,
2358
- cost_usd: carriedCost,
2359
- errored: true,
2360
- phase: infraPhase,
2361
- errors: [message],
2362
- });
2301
+ store.writeYaml(join(paths.attemptsDir, slot.attemptId, "attempt.yaml"), AC.attemptFailureRecord(slot.attemptId, adapter.id, failureCost, infraPhase, message));
2363
2302
  runsBySlot[slotIdx] = {
2364
2303
  attemptId: slot.attemptId,
2365
2304
  harnessId: adapter.id,
2366
2305
  label: slot.label,
2367
2306
  diff: "",
2368
2307
  gates: [],
2369
- cost: 0,
2308
+ cost: failureCost.totalUsd,
2370
2309
  errored: true,
2371
- costEstimated: false,
2310
+ costEstimated: failureCost.estimated,
2372
2311
  errors: [message],
2373
- telemetry: createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required, effectiveWeb, [slot.routed.browserRequirement, slot.routed.denyRequirement], knobs.model),
2312
+ telemetry: createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required, effectiveWeb, [
2313
+ slot.routed.browserRequirement,
2314
+ slot.routed.denyRequirement,
2315
+ slot.routed.delegationRequirement,
2316
+ ], knobs.model),
2374
2317
  infraPhase,
2375
2318
  };
2376
2319
  }
@@ -2393,7 +2336,7 @@ export class Orchestrator {
2393
2336
  // arbitration (as the race-adoption path does) would fold those user edits
2394
2337
  // into the revert target and let a later revert clobber them.
2395
2338
  let earlyPostTurnSha = null;
2396
- if (input.inPlace === true && requestedSingleCandidate) {
2339
+ if (input.inPlace && requestedSingleCandidate && runs.every((run) => !run.secretDiffRefusal)) {
2397
2340
  try {
2398
2341
  earlyPostTurnSha = await snapshotTree(execRoot);
2399
2342
  }
@@ -2405,6 +2348,20 @@ export class Orchestrator {
2405
2348
  await disposeReviewEnvelopes();
2406
2349
  return cancelledResult(log, runId, taskId, mode, paths.root, cancelledCandidates(), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null), ledger.spend(), input.signal, store);
2407
2350
  }
2351
+ const failedDelegation = delegateFailure.dominantRaceCandidateFailure(runs);
2352
+ if (failedDelegation) {
2353
+ const failure = delegateFailure.candidateFailureTerminal(failedDelegation, "race");
2354
+ await disposeReviewEnvelopes();
2355
+ await delegateFailure.persistFailedInPlaceWorkProduct({
2356
+ ...{ store, log, paths, execRoot, preTurnSha, taskId, mode },
2357
+ live: input.inPlace === true && failedDelegation.reviewCwd === execRoot,
2358
+ run: failedDelegation,
2359
+ postTurnSha: earlyPostTurnSha,
2360
+ kind: input.create === true ? "new_repo" : "patch",
2361
+ });
2362
+ this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null);
2363
+ return failTerminally(log, store, paths, runId, taskId, mode, failure.phase, failure.error, ledger.spend(), failure.metadata);
2364
+ }
2408
2365
  if (runs.length === 0) {
2409
2366
  const budgetReason = ledger.terminal();
2410
2367
  // QA-050: when the zero-candidate cause is a budget refusal, the shared
@@ -2424,12 +2381,7 @@ export class Orchestrator {
2424
2381
  why_winner: why,
2425
2382
  evidence_facts: ["no candidates were produced"],
2426
2383
  apply_recommendation: "continue",
2427
- budget_summary: {
2428
- spend_usd: ledger.spend(),
2429
- estimated: false,
2430
- cash_usd: ledger.spend(),
2431
- valuation_usd: ledger.valuation(),
2432
- },
2384
+ budget_summary: decisionBudgetSummary(ledger),
2433
2385
  });
2434
2386
  store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${mode})\n\n- Lifecycle: ${facts.lifecycle}${facts.reason ? ` (${facts.reason})` : ""}\n- Phase: ${agentBudgetMapping ? "budget" : "executor"}\n\n${why}\n`);
2435
2387
  if (agentBudgetMapping) {
@@ -2476,7 +2428,7 @@ export class Orchestrator {
2476
2428
  if (workingRuns.length === 0) {
2477
2429
  await disposeReviewEnvelopes();
2478
2430
  const first = runs[0];
2479
- const phase = first.infraPhase ?? "harness";
2431
+ const phase = first.secretDiffRefusal ? "artifact_security" : (first.infraPhase ?? "harness");
2480
2432
  const { facts, why: rootCause } = partitionCandidates(runs);
2481
2433
  store.writeYaml(join(paths.arbitrationDir, "decision.yaml"), {
2482
2434
  winner: null,
@@ -2484,12 +2436,7 @@ export class Orchestrator {
2484
2436
  why_winner: rootCause,
2485
2437
  evidence_facts: runs.map((r) => `${r.attemptId} produced no work: ${r.errors[0] ?? "unknown"}`),
2486
2438
  apply_recommendation: "continue",
2487
- budget_summary: {
2488
- spend_usd: ledger.spend(),
2489
- estimated: false,
2490
- cash_usd: ledger.spend(),
2491
- valuation_usd: ledger.valuation(),
2492
- },
2439
+ budget_summary: decisionBudgetSummary(ledger),
2493
2440
  });
2494
2441
  this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null);
2495
2442
  store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${mode})\n\n- Lifecycle: ${facts.lifecycle}\n- Phase: ${phase}\n\n${rootCause}\n`);
@@ -2509,9 +2456,11 @@ export class Orchestrator {
2509
2456
  rawDetailRef: `attempts/${first.attemptId}/attempt.yaml`,
2510
2457
  eventRefs: existingEventRefs,
2511
2458
  runDir: paths.root,
2512
- nextActions: phase === "workspace"
2513
- ? ["Check the project folder", "Open diagnostics", "Retry the run"]
2514
- : harnessFailureNextActions(harnessCategory),
2459
+ nextActions: first.secretDiffRefusal
2460
+ ? secretDiff.secretDiffNextActions(first.secretDiffRefusal)
2461
+ : phase === "workspace"
2462
+ ? ["Check the project folder", "Open diagnostics", "Retry the run"]
2463
+ : harnessFailureNextActions(harnessCategory),
2515
2464
  });
2516
2465
  log.emit("output.ready", { kind: "summary", path: "final/summary.md", state: "diagnostic" });
2517
2466
  log.emit("run.failed", {
@@ -2644,7 +2593,7 @@ export class Orchestrator {
2644
2593
  }
2645
2594
  }
2646
2595
  catch (err) {
2647
- ledger.settle(lease.lease?.lease_id ?? "", unknownCostSettlement("synthesis-error"));
2596
+ ledger.settle(lease.lease?.lease_id ?? "", AC.attemptFailureCost(err, "synthesis-error").settlement);
2648
2597
  log.emit("harness.completed", {
2649
2598
  attempt_id: "synth",
2650
2599
  status: "failed",
@@ -2662,14 +2611,7 @@ export class Orchestrator {
2662
2611
  }
2663
2612
  let result;
2664
2613
  try {
2665
- result = arbitrate(evidences, {
2666
- spendUsd: ledger.spend(),
2667
- estimatedSpend: runs.some((r) => r.costEstimated),
2668
- // QA-010b: carry the settled cash + subscription-valuation totals
2669
- // (reviewer panel included) onto the decision record.
2670
- cashUsd: ledger.spend(),
2671
- valuationUsd: ledger.valuation(),
2672
- });
2614
+ result = arbitrate(evidences, arbitrationBudgetOptions(ledger));
2673
2615
  }
2674
2616
  catch (err) {
2675
2617
  // Arbitration throws end terminally with artifacts, never as an orphan.
@@ -3082,7 +3024,7 @@ export class Orchestrator {
3082
3024
  // (so a failing test gate or no_op outcome is unchanged), just unreviewed.
3083
3025
  const hasDiff = run.diff.trim().length > 0;
3084
3026
  // Reviewer panels spend real money: reserve before, settle the observed cost.
3085
- const reviewLease = hasDiff
3027
+ const reviewLease = hasDiff && reviewers.length > 0
3086
3028
  ? ledger?.reserve({
3087
3029
  taskId: taskId ?? "task",
3088
3030
  attemptId: run.attemptId,
@@ -3115,11 +3057,16 @@ export class Orchestrator {
3115
3057
  reviewSpendUsd: 0,
3116
3058
  reviewSpendEstimated: false,
3117
3059
  reviewCashUsd: 0,
3060
+ reviewCashKnowledge: "unknown",
3118
3061
  reviewValuationUsd: 0,
3062
+ reviewValuationKnowledge: "unknown",
3119
3063
  reviewUnknownUsd: 0,
3120
3064
  };
3121
3065
  if (reviewLease?.granted) {
3122
- ledger?.settle(reviewLease.lease?.lease_id ?? "", reviewUsageCostSettlement(result.reviewCashUsd, result.reviewValuationUsd, result.reviewSpendEstimated, [`attempt:${run.attemptId}`, "review:panel"], result.reviewUnknownUsd));
3066
+ ledger?.settle(reviewLease.lease?.lease_id ?? "", reviewUsageCostSettlement(result.reviewCashUsd, result.reviewValuationUsd, {
3067
+ cash: result.reviewCashKnowledge,
3068
+ valuation: result.reviewValuationKnowledge,
3069
+ }, [`attempt:${run.attemptId}`, "review:panel"], result.reviewUnknownUsd));
3123
3070
  if ((result.reviewSpendUsd ?? 0) > 0) {
3124
3071
  log.emit("budget.observation", {
3125
3072
  harness_id: "review-panel",
@@ -3239,20 +3186,23 @@ export class Orchestrator {
3239
3186
  const execRoot = this.execRootOf(input);
3240
3187
  const wsm = new WorkspaceManager(execRoot);
3241
3188
  const readiness = new ReadinessLedger();
3242
- const ledger = this.rootLedger(input, contract, log);
3243
- store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
3244
- safeInvoke(input.onRunStart, { runId, taskId, runDir: paths.root });
3245
- log.emit("run.created", { mode, prompt: redactSecrets(input.prompt) });
3246
- announce?.({
3247
- log,
3248
- store,
3249
- paths,
3250
- runId,
3251
- taskId,
3252
- mode,
3253
- phase: "convergence",
3254
- spend: () => ledger.spend(),
3255
- });
3189
+ let ledger;
3190
+ try {
3191
+ ledger = this.rootLedger(input, contract, log);
3192
+ store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
3193
+ safeInvoke(input.onRunStart, { runId, taskId, runDir: paths.root });
3194
+ log.emit("run.created", { mode, prompt: redactSecrets(input.prompt) });
3195
+ announce?.(announcedRunContext({ log, store, paths, runId, taskId, mode, phase: "convergence" }, ledger, () => this.deps.delegationBudgetAuthority?.hasParent(runId) === true));
3196
+ }
3197
+ catch (error) {
3198
+ // A delegated child attaches its scoped financial view before the run is
3199
+ // announced. If any fallible artifact/start callback in that narrow gap
3200
+ // throws, the terminal net has no run context, so detach here explicitly.
3201
+ if (input.delegatedFromRunId) {
3202
+ this.deps.delegationBudgetAuthority?.releaseRun(runId);
3203
+ }
3204
+ throw error;
3205
+ }
3256
3206
  // Live (in-place) isolation deliberately tolerates non-git stateful
3257
3207
  // environments; only envelope isolation needs the git boundary.
3258
3208
  if (!input.inPlace) {
@@ -3330,6 +3280,7 @@ export class Orchestrator {
3330
3280
  candidates: [],
3331
3281
  };
3332
3282
  }
3283
+ activateDelegationParent(this.deps.delegationBudgetAuthority, input, runId, ledger, adapterPool, log);
3333
3284
  // Fail fast on a provably unwinnable predicate instead of burning paid
3334
3285
  // rounds: the default convergence predicate requires a clean cross-family
3335
3286
  // review, which needs >=2 healthy reviewer provider families.
@@ -3401,7 +3352,6 @@ export class Orchestrator {
3401
3352
  const allCooledDown = () => adapterPool.every((a) => ledger.cooldownActive(a.adapter.id));
3402
3353
  const attemptTelemetries = [];
3403
3354
  let lastDiffStable = true;
3404
- let reviewSpendEstimated = false;
3405
3355
  try {
3406
3356
  // The contract's ENGINE-COMPUTED effective profile drives the envelope and
3407
3357
  // every attempt spec (parity with runRace); telemetry must never claim an
@@ -3436,7 +3386,7 @@ export class Orchestrator {
3436
3386
  : "";
3437
3387
  const prompt = attempt === 1
3438
3388
  ? input.prompt
3439
- : `${input.prompt}\n\nThe previous attempt did not converge. Address these review findings (verify each against the code; fix valid ones, rebut invalid ones with evidence):\n${formatFindings(lastFindings)}${runtimeErrors}`;
3389
+ : buildRevisePrompt(input.prompt, lastFindings, runtimeErrors);
3440
3390
  // Loop detection (budget router): the 3rd identical repair prompt means
3441
3391
  // findings/errors are not changing — stop burning paid attempts.
3442
3392
  const fingerprint = promptFingerprint(prompt);
@@ -3491,30 +3441,48 @@ export class Orchestrator {
3491
3441
  });
3492
3442
  }
3493
3443
  catch (err) {
3494
- // Envelope/setup failure before the stream; stream errors are absorbed
3495
- // inside runCandidateInEnvelope with their real accumulated cost.
3496
- ledger.settle(lease.lease?.lease_id ?? "", unknownCostSettlement("attempt-error"));
3444
+ // Setup failures remain unknown; post-stream persistence failures
3445
+ // carry their route-specific settlement from runCandidateInEnvelope.
3446
+ const failureCost = AC.attemptFailureCost(err, "attempt-error");
3447
+ const message = safeErrorMessage(err);
3448
+ ledger.settle(lease.lease?.lease_id ?? "", failureCost.settlement);
3497
3449
  log.emit("harness.completed", {
3498
3450
  harness_id: adapter.id,
3499
3451
  attempt_id: attemptId,
3500
3452
  status: "failed",
3501
- error: safeErrorMessage(err),
3453
+ error: message,
3502
3454
  });
3455
+ store.writeYaml(join(paths.attemptsDir, attemptId, "attempt.yaml"), AC.attemptFailureRecord(attemptId, adapter.id, failureCost, "harness", message));
3503
3456
  run = {
3504
3457
  attemptId,
3505
3458
  harnessId: adapter.id,
3506
3459
  label: `Attempt ${attempt}`,
3507
3460
  diff: "",
3508
3461
  gates: [],
3509
- cost: 0,
3462
+ cost: failureCost.totalUsd,
3510
3463
  errored: true,
3511
- costEstimated: false,
3512
- errors: [safeErrorMessage(err)],
3513
- telemetry: createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required, effectiveWeb, [routed.browserRequirement, routed.denyRequirement], knobs.model),
3464
+ costEstimated: failureCost.estimated,
3465
+ errors: [message],
3466
+ telemetry: createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required, effectiveWeb, [routed.browserRequirement, routed.denyRequirement, routed.delegationRequirement], knobs.model),
3514
3467
  };
3515
3468
  }
3516
3469
  lastRun = run;
3517
3470
  attemptTelemetries.push({ attemptId, harnessId: adapter.id, telemetry: run.telemetry });
3471
+ // Cancellation/deadline keeps priority over a belt failure finalized concurrently.
3472
+ if (input.signal?.aborted)
3473
+ break;
3474
+ if (delegateFailure.candidateFailureKind(run)) {
3475
+ const failure = delegateFailure.candidateFailureTerminal(run, "convergence");
3476
+ await delegateFailure.persistFailedInPlaceWorkProduct({
3477
+ ...{ store, log, paths, execRoot, preTurnSha, taskId, mode },
3478
+ live: input.inPlace === true,
3479
+ run,
3480
+ kind: input.create === true ? "new_repo" : "patch",
3481
+ attempts: attempt,
3482
+ });
3483
+ this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, attemptTelemetries, null);
3484
+ return failTerminally(log, store, paths, runId, taskId, mode, failure.phase, failure.error, ledger.spend(), failure.metadata);
3485
+ }
3518
3486
  // D-16 r8: interrupted (errored===false) would CONVERGE a partial diff
3519
3487
  // as clean — break BEFORE review; a harness error still gate-retries.
3520
3488
  if (run.outcomeClass === "interrupted") {
@@ -3576,11 +3544,16 @@ export class Orchestrator {
3576
3544
  reviewSpendUsd: 0,
3577
3545
  reviewSpendEstimated: false,
3578
3546
  reviewCashUsd: 0,
3547
+ reviewCashKnowledge: "unknown",
3579
3548
  reviewValuationUsd: 0,
3549
+ reviewValuationKnowledge: "unknown",
3580
3550
  reviewUnknownUsd: 0,
3581
3551
  };
3582
3552
  if (reviewLease?.granted) {
3583
- ledger.settle(reviewLease.lease?.lease_id ?? "", reviewUsageCostSettlement(reviewResult.reviewCashUsd, reviewResult.reviewValuationUsd, reviewResult.reviewSpendEstimated, [`attempt:${attemptId}`, "review:panel"], reviewResult.reviewUnknownUsd));
3553
+ ledger.settle(reviewLease.lease?.lease_id ?? "", reviewUsageCostSettlement(reviewResult.reviewCashUsd, reviewResult.reviewValuationUsd, {
3554
+ cash: reviewResult.reviewCashKnowledge,
3555
+ valuation: reviewResult.reviewValuationKnowledge,
3556
+ }, [`attempt:${attemptId}`, "review:panel"], reviewResult.reviewUnknownUsd));
3584
3557
  if ((reviewResult.reviewSpendUsd ?? 0) > 0) {
3585
3558
  log.emit("budget.observation", {
3586
3559
  harness_id: "review-panel",
@@ -3592,8 +3565,6 @@ export class Orchestrator {
3592
3565
  unknown_usd: reviewResult.reviewUnknownUsd,
3593
3566
  estimated: reviewResult.reviewSpendEstimated === true,
3594
3567
  });
3595
- if (reviewResult.reviewSpendEstimated === true)
3596
- reviewSpendEstimated = true;
3597
3568
  }
3598
3569
  }
3599
3570
  else if (reviewLease && !reviewLease.granted) {
@@ -3775,13 +3746,7 @@ export class Orchestrator {
3775
3746
  if (lastRun && !interrupted) {
3776
3747
  const arb = arbitrate([
3777
3748
  toCandidateEvidence(lastRun, contract, lastFindings, lastFinalReviewClean, actualReviewVerified),
3778
- ], {
3779
- spendUsd: ledger.spend(),
3780
- estimatedSpend: lastRun.costEstimated || reviewSpendEstimated,
3781
- // QA-010b: settled cash + valuation (reviewer panel included).
3782
- cashUsd: ledger.spend(),
3783
- valuationUsd: ledger.valuation(),
3784
- });
3749
+ ], arbitrationBudgetOptions(ledger));
3785
3750
  decision = arb.decision;
3786
3751
  store.writeYaml(join(paths.arbitrationDir, "decision.yaml"), decision);
3787
3752
  // A converged run adopts the arbitration axes (checks/review); an
@@ -4013,40 +3978,6 @@ export class Orchestrator {
4013
3978
  reviewVerified: actualReviewVerified,
4014
3979
  };
4015
3980
  }
4016
- /** plan mode: multi-harness planning -> aggregate -> (optional) plan review -> plan. Read-only. */
4017
- /**
4018
- * Wrap the user's goal in an explicit "plan, do not implement" instruction.
4019
- * Without this the raw prompt ("make a racing game") reaches the harness with
4020
- * only a read-only sandbox, so the model tries to BUILD it and dumps code into
4021
- * the plan when writes are blocked — the v0.9 "HTML in the plan" bug. The
4022
- * read-only access still enforces it; this gives the model the right job.
4023
- */
4024
- planPrompt(goal) {
4025
- return [
4026
- `You are planning, NOT implementing. Explore the repository read-only and produce a plan another agent will execute later. Do not write files or output full implementations.`,
4027
- ``,
4028
- `## Goal`,
4029
- goal,
4030
- ``,
4031
- `## Required output (markdown)`,
4032
- `1. Approach — 2-3 sentences on how you'd solve this.`,
4033
- `2. Steps — a numbered list; each step names the file(s) it touches and what changes.`,
4034
- `3. Risks & edge cases.`,
4035
- `4. End your response with a section titled exactly:`,
4036
- ``,
4037
- `## Open Questions`,
4038
- ``,
4039
- `List every decision the user must make before implementation, one per bullet, in EXACTLY this format:`,
4040
- ``,
4041
- `- [single] <question> :: <option A> :: <option B>`,
4042
- `- [multi] <question> :: <option A> :: <option B>`,
4043
- `- [text] <question that has no good fixed options>`,
4044
- ``,
4045
- `Rules: [single] = pick exactly one; [multi] = pick one or more; [text] = free-form (no "::" options). Ground every option in THIS repository. If nothing is ambiguous, write a single bullet: - (none)`,
4046
- ``,
4047
- `Keep it concise. Reference real paths you found. Do NOT paste large code blocks; describe the change instead.`,
4048
- ].join("\n");
4049
- }
4050
3981
  /** One read-only planner spawn shared by solo fallback, Council drafts, and merge. */
4051
3982
  async runPlannerAttempt(args) {
4052
3983
  const { input, contract, taskId, runId, log, store, paths, ledger, routed, attemptId } = args;
@@ -4208,20 +4139,22 @@ export class Orchestrator {
4208
4139
  input.signal?.removeEventListener("abort", onAbort);
4209
4140
  ledger.settle(lease.lease?.lease_id ?? "", attemptUsageCostSettlement(cost, costEstimated, attemptId, adapter.id, telemetry.authMode, telemetry.usageCost));
4210
4141
  }
4211
- const unrecovered = unrecoveredToolErrors(telemetry);
4212
- const webBlocked = webUnsatisfied(telemetry);
4213
- if (!harnessError && webBlocked) {
4214
- harnessError = `web evidence unsatisfied: ${telemetry.web.errorSummary ?? (telemetry.web.attempted ? "web tool failed without verified recovery" : "web evidence required but never attempted")}`;
4215
- }
4216
- if (!harnessError && unrecovered.length > 0) {
4217
- const first = unrecovered[0];
4218
- harnessError = `${first.tool} failed without recovery: ${first.summary}`;
4219
- }
4220
4142
  // D-16: unwrap and require PLAN TEXT — a plan with no text is not delivered.
4143
+ // The unwrap runs BEFORE the error axes: the deliverable it yields is what
4144
+ // decides whether an unrecovered tool error is fatal (explorer parity).
4221
4145
  const planUnwrapped = unwrapWorkReportEnvelope(answer.machineText() ?? "", planWorkMode, {
4222
4146
  sideToolReport: telemetry.sideToolWorkReport ?? undefined,
4223
4147
  });
4224
4148
  const planText = redactSecrets(planUnwrapped.deliverable).trim();
4149
+ const unrecovered = unrecoveredToolErrors(telemetry);
4150
+ const webBlocked = webUnsatisfied(telemetry);
4151
+ if (!harnessError && webBlocked) {
4152
+ harnessError = webEvidenceFailure(telemetry.web);
4153
+ }
4154
+ // INV-043/INV-044, explorer parity: a DELIVERED plan keeps an unrecovered
4155
+ // non-web tool error as warning evidence instead of discarding the plan (see
4156
+ // the helper). Web keeps its hard gate above; the finalizer outranks both.
4157
+ harnessError ??= unrecoveredToolErrorFailure(unrecovered, planText.length > 0);
4225
4158
  const planFinalized = finalizeAttempt({
4226
4159
  deliverableEvidence: planText.length > 0,
4227
4160
  harnessErrored: harnessError !== null && !webBlocked,
@@ -4303,16 +4236,7 @@ export class Orchestrator {
4303
4236
  safeInvoke(input.onRunStart, { runId, taskId, runDir: paths.root });
4304
4237
  log.emit("run.created", { mode: "plan", prompt: redactSecrets(input.prompt) });
4305
4238
  const ledger = this.rootLedger(input, contract, log);
4306
- announce?.({
4307
- log,
4308
- store,
4309
- paths,
4310
- runId,
4311
- taskId,
4312
- mode: "plan",
4313
- phase: "plan",
4314
- spend: () => ledger.spend(),
4315
- });
4239
+ announce?.(announcedRunContext({ log, store, paths, runId, taskId, mode: "plan", phase: "plan" }, ledger, () => this.deps.delegationBudgetAuthority?.hasParent(runId) === true));
4316
4240
  store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
4317
4241
  log.emit("task.contract.created", { task_contract_hash: hashJson(contract) });
4318
4242
  // W3.3: ONE resolved read-only context — the routing point-probe and every
@@ -4447,7 +4371,7 @@ export class Orchestrator {
4447
4371
  attemptId,
4448
4372
  laneRun,
4449
4373
  fallbackHome: roHome.env,
4450
- promptBody: this.planPrompt(input.prompt) + contextSection,
4374
+ promptBody: planPrompt(input.prompt) + contextSection,
4451
4375
  intent: "plan",
4452
4376
  });
4453
4377
  if (outcome.budgetDenied) {
@@ -4570,7 +4494,7 @@ export class Orchestrator {
4570
4494
  runPlannerAttempt: (a) => this.runPlannerAttempt(a),
4571
4495
  writeRunTelemetry: (store, paths, contract, runId, taskId, mode, attempts, finalAttemptId) => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, attempts, finalAttemptId),
4572
4496
  execRootOf: (input) => this.execRootOf(input),
4573
- planPrompt: (goal) => this.planPrompt(goal),
4497
+ planPrompt,
4574
4498
  };
4575
4499
  }
4576
4500
  /** ask: one selected harness answers read-only questions; no patch/apply controls. */
@@ -4599,21 +4523,14 @@ export class Orchestrator {
4599
4523
  resolvePaidBudget(inputBudget, cfg) {
4600
4524
  return inputBudget ?? this.deps.paidBudget ?? cfg.global.budget.paid_budget_per_run;
4601
4525
  }
4602
- rootLedger(_input, contract, log) {
4603
- // The root ledger discloses into THIS run's log: the ledger is the one
4604
- // owner of the cash fact (subscription-entitled work settles to 0 there),
4605
- // and the UI renders `budget.cash` verbatim — never inferring money from
4606
- // route labels (W4.3 sol #15).
4607
- const ledger = new BudgetLedger(contract.budget.paid_budget, undefined, {
4608
- onCashSettled: (cashSpendUsd, valuationUsd) => log.emit("budget.cash", {
4609
- cash_spend_usd: cashSpendUsd,
4610
- valuation_usd: valuationUsd,
4611
- }),
4526
+ rootLedger(input, contract, log) {
4527
+ return createRootLedger({
4528
+ input,
4529
+ contract,
4530
+ log,
4531
+ authority: this.deps.delegationBudgetAuthority,
4532
+ quotaSnapshots: this.deps.quotaSnapshots?.() ?? [],
4612
4533
  });
4613
- for (const snapshot of this.deps.quotaSnapshots?.() ?? []) {
4614
- ledger.observeQuotaSnapshot(snapshot);
4615
- }
4616
- return ledger;
4617
4534
  }
4618
4535
  routeBillingKnowledge(input, harnessId) {
4619
4536
  // A selected profile's credential_kind decides billing (round-18 #2).
@@ -4693,16 +4610,7 @@ export class Orchestrator {
4693
4610
  safeInvoke(input.onRunStart, { runId, taskId, runDir: paths.root });
4694
4611
  log.emit("run.created", { mode: opts.mode, prompt: redactSecrets(prompt) });
4695
4612
  const ledger = this.rootLedger(input, contract, log);
4696
- announce?.({
4697
- log,
4698
- store,
4699
- paths,
4700
- runId,
4701
- taskId,
4702
- mode: opts.mode,
4703
- phase: "report",
4704
- spend: () => ledger.spend(),
4705
- });
4613
+ announce?.(announcedRunContext({ log, store, paths, runId, taskId, mode: opts.mode, phase: "report" }, ledger, () => this.deps.delegationBudgetAuthority?.hasParent(runId) === true));
4706
4614
  store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
4707
4615
  log.emit("task.contract.created", { task_contract_hash: hashJson(contract) });
4708
4616
  // The ask/deep-scan report stays bare (its scouts read the tree themselves);
@@ -5112,12 +5020,9 @@ export class Orchestrator {
5112
5020
  const webBlocked = webUnsatisfied(telemetry);
5113
5021
  const reportPresent = report.length > 0;
5114
5022
  if (!harnessError && webBlocked) {
5115
- harnessError = `web evidence unsatisfied: ${telemetry.web.errorSummary ?? (telemetry.web.attempted ? "web tool failed without verified recovery" : "web evidence required but never attempted")}`;
5116
- }
5117
- if (!harnessError && unrecovered.length > 0 && !reportPresent) {
5118
- const first = unrecovered[0];
5119
- harnessError = `${first.tool} failed without recovery: ${first.summary}`;
5023
+ harnessError = webEvidenceFailure(telemetry.web);
5120
5024
  }
5025
+ harnessError ??= unrecoveredToolErrorFailure(unrecovered, reportPresent);
5121
5026
  const roFinalized = finalizeAttempt({
5122
5027
  deliverableEvidence: reportPresent,
5123
5028
  harnessErrored: harnessError !== null && !webBlocked,
@@ -5696,7 +5601,4 @@ function assertNoSecretLikeTokens(label, text) {
5696
5601
  throw new Error(`${label} contains secret-like token; refusing to persist artifact`);
5697
5602
  }
5698
5603
  }
5699
- function safeErrorMessage(err) {
5700
- return redactSecrets(err instanceof Error ? err.message : String(err));
5701
- }
5702
5604
  //# sourceMappingURL=orchestrator.js.map