@claudexor/orchestrator 3.1.1 → 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 (92) hide show
  1. package/dist/attemptTelemetry.d.ts +9 -20
  2. package/dist/attemptTelemetry.d.ts.map +1 -1
  3. package/dist/attemptTelemetry.js +36 -69
  4. package/dist/attemptTelemetry.js.map +1 -1
  5. package/dist/attemptUsageCost.d.ts +31 -0
  6. package/dist/attemptUsageCost.d.ts.map +1 -0
  7. package/dist/attemptUsageCost.js +181 -0
  8. package/dist/attemptUsageCost.js.map +1 -0
  9. package/dist/budgetFailure.d.ts +3 -1
  10. package/dist/budgetFailure.d.ts.map +1 -1
  11. package/dist/budgetFailure.js.map +1 -1
  12. package/dist/candidateEvidence.d.ts +5 -0
  13. package/dist/candidateEvidence.d.ts.map +1 -1
  14. package/dist/candidateEvidence.js +3 -1
  15. package/dist/candidateEvidence.js.map +1 -1
  16. package/dist/candidateOutputs.d.ts +19 -0
  17. package/dist/candidateOutputs.d.ts.map +1 -1
  18. package/dist/candidateOutputs.js +201 -67
  19. package/dist/candidateOutputs.js.map +1 -1
  20. package/dist/decisionBudget.d.ts +43 -0
  21. package/dist/decisionBudget.d.ts.map +1 -0
  22. package/dist/decisionBudget.js +84 -0
  23. package/dist/decisionBudget.js.map +1 -0
  24. package/dist/decisionTerminalReconciliation.d.ts +18 -0
  25. package/dist/decisionTerminalReconciliation.d.ts.map +1 -0
  26. package/dist/decisionTerminalReconciliation.js +26 -0
  27. package/dist/decisionTerminalReconciliation.js.map +1 -0
  28. package/dist/delegation-parent-activation.d.ts +6 -0
  29. package/dist/delegation-parent-activation.d.ts.map +1 -0
  30. package/dist/delegation-parent-activation.js +13 -0
  31. package/dist/delegation-parent-activation.js.map +1 -0
  32. package/dist/delegationBudgetAuthority.d.ts +30 -0
  33. package/dist/delegationBudgetAuthority.d.ts.map +1 -0
  34. package/dist/delegationBudgetAuthority.js +157 -0
  35. package/dist/delegationBudgetAuthority.js.map +1 -0
  36. package/dist/delegationFailure.d.ts +58 -0
  37. package/dist/delegationFailure.d.ts.map +1 -0
  38. package/dist/delegationFailure.js +184 -0
  39. package/dist/delegationFailure.js.map +1 -0
  40. package/dist/delegationToolEvidence.d.ts +20 -0
  41. package/dist/delegationToolEvidence.d.ts.map +1 -0
  42. package/dist/delegationToolEvidence.js +46 -0
  43. package/dist/delegationToolEvidence.js.map +1 -0
  44. package/dist/index.d.ts +2 -0
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +2 -0
  47. package/dist/index.js.map +1 -1
  48. package/dist/orchestrator.d.ts +23 -32
  49. package/dist/orchestrator.d.ts.map +1 -1
  50. package/dist/orchestrator.js +248 -346
  51. package/dist/orchestrator.js.map +1 -1
  52. package/dist/plan-prompt.d.ts +3 -0
  53. package/dist/plan-prompt.d.ts.map +1 -0
  54. package/dist/plan-prompt.js +28 -0
  55. package/dist/plan-prompt.js.map +1 -0
  56. package/dist/requestRequirements.d.ts +17 -0
  57. package/dist/requestRequirements.d.ts.map +1 -1
  58. package/dist/requestRequirements.js +69 -1
  59. package/dist/requestRequirements.js.map +1 -1
  60. package/dist/root-ledger.d.ts +13 -0
  61. package/dist/root-ledger.d.ts.map +1 -0
  62. package/dist/root-ledger.js +19 -0
  63. package/dist/root-ledger.js.map +1 -0
  64. package/dist/routing-failure.d.ts +6 -0
  65. package/dist/routing-failure.d.ts.map +1 -0
  66. package/dist/routing-failure.js +12 -0
  67. package/dist/routing-failure.js.map +1 -0
  68. package/dist/run-bounded.d.ts +3 -0
  69. package/dist/run-bounded.d.ts.map +1 -0
  70. package/dist/run-bounded.js +17 -0
  71. package/dist/run-bounded.js.map +1 -0
  72. package/dist/run-input-resolution.d.ts +10 -0
  73. package/dist/run-input-resolution.d.ts.map +1 -0
  74. package/dist/run-input-resolution.js +70 -0
  75. package/dist/run-input-resolution.js.map +1 -0
  76. package/dist/runTelemetryWriter.d.ts +5 -2
  77. package/dist/runTelemetryWriter.d.ts.map +1 -1
  78. package/dist/runTelemetryWriter.js +72 -1
  79. package/dist/runTelemetryWriter.js.map +1 -1
  80. package/dist/runTerminals.d.ts +34 -7
  81. package/dist/runTerminals.d.ts.map +1 -1
  82. package/dist/runTerminals.js +215 -24
  83. package/dist/runTerminals.js.map +1 -1
  84. package/dist/secretDiff.d.ts +29 -0
  85. package/dist/secretDiff.d.ts.map +1 -0
  86. package/dist/secretDiff.js +133 -0
  87. package/dist/secretDiff.js.map +1 -0
  88. package/dist/terminalOutcome.d.ts +22 -0
  89. package/dist/terminalOutcome.d.ts.map +1 -0
  90. package/dist/terminalOutcome.js +39 -0
  91. package/dist/terminalOutcome.js.map +1 -0
  92. 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";
@@ -13,9 +14,17 @@ 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
16
  import { governRouteEffort } from "./effortGovernance.js";
16
- import { RequestRequirementsResolver } from "./requestRequirements.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";
17
26
  import { buildRevisePrompt } from "./revisePrompt.js";
18
- import { cancelledResult, failTerminally, guardAnnouncedRun, writeFailure, } from "./runTerminals.js";
27
+ import { announcedRunContext, cancelledResult, failTerminally, guardAnnouncedRun, writeFailure, } from "./runTerminals.js";
19
28
  import { budgetFailureRecord, classifyBudgetFailure } from "./budgetFailure.js";
20
29
  import { assertOutputSchemaCompiles, finalizeStructuredOutput } from "./structuredOutput.js";
21
30
  import { transientRetryDelayMs, promptWithEngineConstraints, sleep, redactHarnessEvent, harnessEventPayload, safeErrorMessage, renderSummary, observeBudgetSignals, rotateOnStall, recordCleanAttemptMetrics, envInheritance, transientRetryPolicy, reviewerTimeoutMs, harnessInactivityTimeoutMs, observeAuthSwitch, emitPrimaryDivergence, emitPoolDegraded, deliveryRefusalFailure, writeRaceDeliveryDecision, } from "./runSupport.js";
@@ -26,6 +35,8 @@ import { activePlanPointer, resolveContinuitySummary, workspaceAnchor, } from ".
26
35
  import { runDiffReview } from "./diffReview.js";
27
36
  import { rawScoutBundle, resolveDeepScanSynthesis, } from "./deepScanReducer.js";
28
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";
29
40
  import { dominantHarnessFailureCategory, harnessFailureNextActions } from "./harnessFailure.js";
30
41
  import { finalizeAttempt, readOnlyNoSuccessTerminal, resolveWorkReportEnvelope, unrecoveredToolErrorFailure, unwrapWorkReportEnvelope, webEvidenceFailure, } from "./attemptFinalize.js";
31
42
  import { buildContinuationPacket, decideContinuation, synthesizeContinuationRequest, } from "./continuation.js";
@@ -40,57 +51,15 @@ import { HarnessGateway } from "@claudexor/gateway";
40
51
  import { ReadinessLedger, evaluateConvergence, failureSignature, gatesPassed, reviewCandidate, revalidateFindings, runGates, } from "@claudexor/review";
41
52
  import { arbitrate } from "@claudexor/arbitration";
42
53
  import { buildSynthesisPlan, decideSynthesis } from "@claudexor/synthesis";
43
- 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";
44
55
  import { readTextSafe, appendLine, assertNoInlineSecretValues, containsSecretLikeToken, DELEGATION_ENV, hashJson, newId, noProjectRepoRoot, nowIso, redactSecrets, safeInvoke, sha256, userConfigDir, writeText, } from "@claudexor/util";
45
56
  import { assertWriteIsolation } from "./write-isolation.js";
46
- /** A routed candidate adapter plus its manifest capabilities and user settings. */
47
- /** The two access profiles that map to codex `danger-full-access` / an
48
- * unsandboxed lane — the only ones under which a full-access-requiring MCP
49
- * injection (the belt on codex) can reach the daemon. */
50
- export function isFullAccess(access) {
51
- return access === "full" || access === "external_sandbox_full";
52
- }
53
- /**
54
- * A routing preflight refusal (`RoutingPreflightError`: quality routing with no
55
- * comparable user-declared tier for the intent) is a CONFIGURATION error, not a
56
- * harness-availability problem (A-1/D-9/#22). Classifying it as
57
- * `harness_unavailable` sent the operator to re-auth or wait for a harness; the
58
- * real fix is to configure a tier or change the routing goal. Detected by the
59
- * typed `code` (robust across duplicate `@claudexor/budget` package copies) so
60
- * EVERY strategy's routing catch (ask/agent/plan/deep-scan/council) classifies
61
- * it identically. Returns the failure category + matching remediation.
62
- */
63
- export function routingFailureClassification(err) {
64
- const isPreflightRefusal = !!err &&
65
- typeof err === "object" &&
66
- err.code === "routing_preflight_refused";
67
- if (isPreflightRefusal) {
68
- return { category: "config_error", nextActions: harnessFailureNextActions("config_error") };
69
- }
70
- return { category: "harness_unavailable" };
71
- }
72
57
  const LABELS = "ABCDEFGHIJ".split("");
73
58
  const NO_PROJECT_ROOT = noProjectRepoRoot();
74
59
  /** Concurrency cap for parallel candidates/explorers (locked decision: min(n, 4)). */
75
60
  const MAX_PARALLEL_CANDIDATES = 4;
76
61
  /** Default wait for one interactive answer before a benign decline. */
77
62
  const DEFAULT_INTERACTION_TIMEOUT_MS = 900_000;
78
- /** Run `work` over `items` with bounded concurrency, preserving item order via index. */
79
- async function runBounded(items, limit, work) {
80
- if (items.length === 0)
81
- return;
82
- const concurrency = Math.max(1, Math.min(limit, items.length));
83
- let next = 0;
84
- const workers = Array.from({ length: concurrency }, async () => {
85
- for (;;) {
86
- const idx = next++;
87
- if (idx >= items.length)
88
- return;
89
- await work(items[idx], idx);
90
- }
91
- });
92
- await Promise.all(workers);
93
- }
94
63
  export class Orchestrator {
95
64
  deps;
96
65
  gateway;
@@ -118,7 +87,7 @@ export class Orchestrator {
118
87
  });
119
88
  }
120
89
  async run(input) {
121
- const resolved = this.resolveRunInput(input);
90
+ let resolved = this.resolveRunInput(input);
122
91
  // INV-062 at the ENGINE boundary: every surface fences prompts already,
123
92
  // but a direct embedder (or the daemon-less local REPL fallback) reaches
124
93
  // this entry without one. Prompts, per-run instructions, AND outputSchema
@@ -137,6 +106,21 @@ export class Orchestrator {
137
106
  throw new Error(`unknown mode: ${String(resolved.mode)}`);
138
107
  }
139
108
  const mode = parsedMode.data;
109
+ if (resolved.delegate === true && mode !== "agent") {
110
+ throw new Error(`Delegate is an agent-only strategy (got mode=${mode})`);
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
+ }
140
124
  const projectProtectedPaths = mode === "agent" ? this.projectConfig(resolved.repoRoot).constraints.protected_paths : [];
141
125
  assertWriteIsolation({
142
126
  mode,
@@ -203,11 +187,20 @@ export class Orchestrator {
203
187
  case "plan":
204
188
  return this.runPlan(resolved, announce);
205
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);
206
196
  },
207
197
  // Single per-run terminalization hook: release the routing-rationale map
208
198
  // entry on EVERY terminal (incl. a run that died before its telemetry
209
199
  // writer ran, which is the leak this closes).
210
- (runId) => this.routingRationaleByRun.delete(runId));
200
+ (runId) => {
201
+ this.routingRationaleByRun.delete(runId);
202
+ this.deps.delegationBudgetAuthority?.releaseRun(runId);
203
+ });
211
204
  }
212
205
  async resolveReviewers(cwd, runAuthPreference, onIgnoredSetting) {
213
206
  if (this.deps.reviewers)
@@ -416,89 +409,11 @@ export class Orchestrator {
416
409
  * expand to n. Fails loudly if nothing can perform the intent.
417
410
  */
418
411
  resolveRunInput(input) {
419
- if (input.contextMode === "off" &&
420
- !(input.mode === "ask" && input.repoRoot === NO_PROJECT_ROOT)) {
421
- throw new Error("contextMode 'off' is only supported for Ask without a repoRoot");
422
- }
423
- const cfg = this.config(input.repoRoot);
424
- const configuredPool = cfg?.global.routing.eligible_harnesses;
425
- const harnesses = input.harnesses ?? (configuredPool && configuredPool.length > 0 ? configuredPool : undefined);
426
- // GH #25 precedence: an explicit --primary-harness wins and is validated
427
- // against the pool; else a single-item explicit pool infers itself as
428
- // primary (shipped in #34); else the configured default primary applies.
429
- const explicitPrimary = input.primaryHarness;
430
- const configPrimary = cfg?.global.routing.primary_harness;
431
- const primaryHarness = explicitPrimary ??
432
- (input.harnesses?.length === 1 ? input.harnesses[0] : undefined) ??
433
- configPrimary ??
434
- undefined;
435
- if (primaryHarness &&
436
- harnesses &&
437
- harnesses.length > 0 &&
438
- !harnesses.includes(primaryHarness)) {
439
- if (explicitPrimary) {
440
- // An explicit primary must be a member of the eligible pool (authoritative).
441
- throw new Error(`primary harness '${explicitPrimary}' is not in the eligible harness pool (${harnesses.join(", ")}); ` +
442
- `pass --primary-harness as one of [${harnesses.join(", ")}], or add '${explicitPrimary}' to --harness`);
443
- }
444
- // GH #25 remainder: a MULTI-harness pool whose CONFIGURED default primary
445
- // is absent, with no --primary-harness pinned, is ambiguous — the engine
446
- // must not silently reroute. Refuse with a structured, copy-pasteable fix
447
- // naming the pool, the missing primary, and the exact flag to add.
448
- throw new HarnessUnavailableError(`ambiguous primary harness: the configured default primary '${primaryHarness}' is not in the selected pool [${harnesses.join(", ")}], ` +
449
- `and no --primary-harness was given. Pin one explicitly, e.g. \`--primary-harness ${harnesses[0]}\` ` +
450
- `(or another of [${harnesses.join(", ")}]).`);
451
- }
452
- if (input.web && input.externalContextPolicy && input.web !== input.externalContextPolicy) {
453
- throw new Error(`contradictory web policy: web='${input.web}' vs externalContextPolicy='${input.externalContextPolicy}' (pass one, or equal values)`);
454
- }
455
- const web = input.web ?? input.externalContextPolicy ?? "auto";
456
- // INV-103: scalar `model` expands only to the resolved primary, never the pool;
457
- // an explicit per-harness map wins. Unknown map keys fail loudly (INV-021).
458
- const knownHarnessIds = new Set(this.deps.registry.keys());
459
- for (const key of Object.keys(input.models ?? {})) {
460
- if (!knownHarnessIds.has(key)) {
461
- throw new Error(`models map names unknown harness '${key}' (registered: ${[...knownHarnessIds].sort().join(", ")}); ` +
462
- `run \`claudexor harness list --all\``);
463
- }
464
- }
465
- const models = { ...input.models };
466
- if (input.model) {
467
- const scalarTarget = primaryHarness ?? (harnesses && harnesses.length === 1 ? harnesses[0] : undefined);
468
- if (!scalarTarget) {
469
- throw new Error(`a scalar model ('${input.model}') is ambiguous without a primary harness: ` +
470
- `the pool is ${harnesses && harnesses.length > 0 ? `[${harnesses.join(", ")}]` : "auto-resolved"} — ` +
471
- `set a primary harness, pass exactly one --harness, or use a harness-scoped model map`);
472
- }
473
- models[scalarTarget] ??= input.model;
474
- }
475
- // QA-035: FREEZE the config-derived per-harness default_model into the
476
- // resolved model map at initial normalization, exactly like an explicit
477
- // input. Without this the TaskContract records `routing_models: {}` and an
478
- // Exact Retry re-resolves the model against CURRENT settings — silently
479
- // changing the route after a settings edit. A per-turn/scalar value already
480
- // set wins (??=). Only a known resolved pool can be frozen here; a pure
481
- // auto pool's lanes are not yet known (documented seam).
482
- const harnessCfg = cfg?.global.harnesses ?? {};
483
- for (const hid of harnesses ?? []) {
484
- const def = harnessCfg[hid]?.default_model;
485
- if (def)
486
- models[hid] ??= def;
487
- }
488
- return {
489
- ...input,
490
- harnesses,
491
- primaryHarness,
492
- model: undefined,
493
- models,
494
- routingGoal: input.routingGoal ??
495
- this.deps.routingGoal ??
496
- cfg?.project.budget?.routing_goal ??
497
- cfg?.global.routing.goal ??
498
- "auto",
499
- web,
500
- externalContextPolicy: web,
501
- };
412
+ return resolveRunInputDefaults(input, {
413
+ config: this.config(input.repoRoot),
414
+ registryIds: this.deps.registry.keys(),
415
+ routingGoal: this.deps.routingGoal,
416
+ });
502
417
  }
503
418
  async resolveCandidateAdapters(input, intent, ledger, log, routeContext,
504
419
  /** QA-034: when provided, the pool-ordering rationale is recorded under this
@@ -717,6 +632,14 @@ export class Orchestrator {
717
632
  access: requiredAccess,
718
633
  }),
719
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
+ }),
720
643
  effortLevels: manifest.capabilities.effort_levels,
721
644
  knownModels: manifest.capabilities.known_models,
722
645
  // A selected profile's credential_kind IS the route (round-18 #2);
@@ -763,6 +686,7 @@ export class Orchestrator {
763
686
  }
764
687
  emitPrimaryDivergence(log, input.primaryHarness, ordered, pool, dropped);
765
688
  const n = input.n ?? ordered.length;
689
+ const selectionOrder = ordered;
766
690
  const out = [];
767
691
  if (droppedLanes.length > 0 && !allowDuplicateFill) {
768
692
  // QA-043: lanes were dropped from an AUTO best-of pool (an explicit pool
@@ -771,15 +695,15 @@ export class Orchestrator {
771
695
  // masks the omission. Clamp to distinct survivors and disclose below.
772
696
  // (Deep-scan sets allowDuplicateFill: its width is scout coverage, not
773
697
  // harness diversity, so a dropped lane must not cut the scout count.)
774
- for (let i = 0; i < Math.min(n, ordered.length); i++)
775
- out.push(ordered[i]);
698
+ for (let i = 0; i < Math.min(n, selectionOrder.length); i++)
699
+ out.push(selectionOrder[i]);
776
700
  }
777
701
  else {
778
702
  // No lane was dropped: a pool smaller than `n` is an intentional
779
703
  // best-of-N on the available harness(es) (e.g. explicit `--harness codex
780
704
  // -n 3`), so the historical width fill is preserved.
781
705
  for (let i = 0; i < n; i++)
782
- out.push(ordered[i % ordered.length]);
706
+ out.push(selectionOrder[i % selectionOrder.length]);
783
707
  }
784
708
  // Disclose an auto-pool omission / width clamp once, with the
785
709
  // requested-vs-effective route receipt (never silent — QA-043).
@@ -791,25 +715,24 @@ export class Orchestrator {
791
715
  droppedLanes,
792
716
  });
793
717
  this.requestRequirements.requireEffectiveBrowser(input.browser === true, out.map((lane) => lane.browserRequirement));
794
- // Delegation belt (D32): agent-only, and only on a lane whose adapter can
795
- // inject MCP servers. A requested delegate with NO injecting lane is a typed
796
- // preflight refusal naming the harness(es) never a silently dropped belt.
797
- if (input.delegate === true && !out.some((lane) => lane.supportsMcpInjection)) {
798
- const names = [...new Set(out.map((lane) => lane.adapter.id))].join(", ");
799
- 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`);
800
- }
801
- // A belt-injecting lane may still be UNABLE to reach the daemon at its
802
- // access: codex's workspace-write seatbelt cancels the belt's daemon-crossing
803
- // MCP call, so codex only hosts the belt at FULL access (same as its browser
804
- // MCP). If EVERY injecting lane requires full access but runs below it, the
805
- // belt would be injected only to be silently cancelled by the sandbox — the
806
- // 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).
807
722
  if (input.delegate === true) {
808
- const injecting = out.filter((lane) => lane.supportsMcpInjection);
809
- const canHostBelt = injecting.some((lane) => !lane.mcpInjectionRequiresFullAccess || isFullAccess(lane.adapterAccess));
810
- if (!canHostBelt) {
811
- const names = [...new Set(injecting.map((lane) => lane.adapter.id))].join(", ");
812
- 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
+ });
813
736
  }
814
737
  }
815
738
  // outputSchema is MANDATORY (Quiz-6a): a selected lane that cannot
@@ -846,6 +769,8 @@ export class Orchestrator {
846
769
  */
847
770
  orderPool(pool, input, intent, statusById, ledger, runId) {
848
771
  let ordered = pool;
772
+ let rationale = null;
773
+ let selectionReason = null;
849
774
  if (pool.length > 0) {
850
775
  const routeLedger = ledger ?? new BudgetLedger();
851
776
  const config = this.config(input.repoRoot).global;
@@ -927,16 +852,35 @@ export class Orchestrator {
927
852
  const ranked = rankHarnesses(remaining, routeCtx)
928
853
  .map((candidate) => byId.get(candidate.harnessId))
929
854
  .filter((candidate) => Boolean(candidate));
930
- // QA-034: the rationale is run evidence recorded ONCE at pool ordering,
931
- // pinned to routeCtx.now so it cannot disagree with the order just taken.
932
- if (runId)
933
- this.routingRationaleByRun.set(runId, explainRanking(remaining, routeCtx));
855
+ rationale = explainRanking(remaining, routeCtx);
934
856
  ordered = ranked;
935
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
+ }
936
868
  if (input.primaryHarness) {
937
869
  const primary = ordered.find((r) => r.adapter.id === input.primaryHarness);
938
- if (primary)
870
+ if (primary && primary !== ordered[0]) {
939
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
+ });
940
884
  }
941
885
  return ordered;
942
886
  }
@@ -1083,6 +1027,11 @@ export class Orchestrator {
1083
1027
  created_at: nowIso(),
1084
1028
  repo: { root: input.repoRoot, base_ref: input.baseRef ?? "HEAD", dirty_policy: "snapshot" },
1085
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
+ },
1086
1035
  user_intent: { raw: redactSecrets(input.prompt) },
1087
1036
  // Redacted for symmetry with user_intent.raw — a no-op on fenced input
1088
1037
  // (the inline-secret fence already blocked any secret-like value at every
@@ -1168,13 +1117,15 @@ export class Orchestrator {
1168
1117
  * a WRITING agent intent (the delegator integrates results in its workspace;
1169
1118
  * read lanes and reviewers have nothing to delegate). */
1170
1119
  delegationBeltFor(input, intent, routed, resolvedBudget) {
1171
- if (!input?.delegate || !input.delegationBelt || !routed.supportsMcpInjection)
1120
+ if (!input?.delegate ||
1121
+ !input.delegationBelt ||
1122
+ !input.delegationParentRunId ||
1123
+ !routed.delegationRequirement.effective)
1172
1124
  return [];
1173
1125
  // A lane that sandbox-cancels the belt below full access (codex) must NOT
1174
- // receive a belt it cannot use that is the silent non-delegation. The
1175
- // preflight already refused a run whose ONLY injecting lanes are such lanes
1176
- // below full access; here we simply skip injecting into an individual lane
1177
- // 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.
1178
1129
  if (routed.mcpInjectionRequiresFullAccess && !isFullAccess(routed.adapterAccess))
1179
1130
  return [];
1180
1131
  const writingIntents = ["implement", "create_from_scratch", "repair"];
@@ -1190,6 +1141,8 @@ export class Orchestrator {
1190
1141
  ...input.delegationBelt,
1191
1142
  env: {
1192
1143
  ...input.delegationBelt.env,
1144
+ [DELEGATION_ENV.parentRunId]: input.delegationParentRunId,
1145
+ [DELEGATION_ENV.repoRoot]: input.repoRoot,
1193
1146
  [DELEGATION_ENV.budget]: JSON.stringify(resolvedBudget),
1194
1147
  },
1195
1148
  },
@@ -1493,7 +1446,7 @@ export class Orchestrator {
1493
1446
  const browserServerName = spec.browser ? "browser" : null;
1494
1447
  const telemetry = createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required ||
1495
1448
  knobs.webPolicy === "cached" ||
1496
- 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);
1497
1450
  let activeSessionId = spec.session_id;
1498
1451
  const onAbort = () => {
1499
1452
  void adapter.cancel?.(activeSessionId)?.catch(() => { });
@@ -1573,10 +1526,9 @@ export class Orchestrator {
1573
1526
  observeNativeSessionEvent(runInput, adapter.id, safeEv);
1574
1527
  observeAuthSwitch(log, adapter.id, attemptId, safeEv);
1575
1528
  observeAttemptTelemetry(telemetry, safeEv);
1576
- // QA-024: the injected delegation belt's MCP server reported `failed`
1577
- // to start. Disclose it ONCE as a typed run event the moment the
1578
- // `started` frame reveals it the harness is about to run without
1579
- // `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.
1580
1532
  // The terminal outcome axis (delegationBeltUnavailable) reflects it
1581
1533
  // too; this event makes the failure visible while the run is live.
1582
1534
  if (telemetry.delegationBelt.requested &&
@@ -1727,20 +1679,20 @@ export class Orchestrator {
1727
1679
  if (webUnsatisfied(telemetry)) {
1728
1680
  errors.push(webEvidenceFailure(telemetry.web));
1729
1681
  }
1730
- const diff = await wsm.diff(envelope);
1731
1682
  // D-16: un-nest {work_report, output} so answer.md persists the OUTPUT, not the envelope.
1732
1683
  const unwrapped = unwrapWorkReportEnvelope(answer.machineText() ?? "", workReportMode, {
1733
1684
  sideToolReport: telemetry.sideToolWorkReport ?? undefined,
1734
1685
  });
1735
- // X119: persist the VERBATIM redacted bytes; trim ONLY for the emptiness check.
1736
1686
  const redacted = redactSecrets(unwrapped.deliverable);
1737
- 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;
1738
1691
  const deliverableEvidence = diff.trim().length > 0 || Boolean(answerText);
1739
- // Cancelled attempts skip gates entirely: the operator asked to
1740
- // 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
1741
1693
  // and burns compute on a result nobody will adopt. Diff/attempt.yaml
1742
1694
  // still land, so partial work stays inspectable.
1743
- const gateSignalAborted = signal?.aborted === true;
1695
+ const gateSignalAborted = signal?.aborted === true || secretDiffRefusal !== undefined;
1744
1696
  if (!gateSignalAborted) {
1745
1697
  log?.emit("gate.started", {
1746
1698
  attempt_id: attemptId,
@@ -1770,6 +1722,14 @@ export class Orchestrator {
1770
1722
  });
1771
1723
  }
1772
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
+ }
1773
1733
  // D-16 unified finalizer: fold the WorkReport / context signals into the
1774
1734
  // deliverable + work_state. A broken contract on a constrained route
1775
1735
  // elevates harnessErrored (never a prose success).
@@ -1794,14 +1754,6 @@ export class Orchestrator {
1794
1754
  workState: finalized.workState,
1795
1755
  });
1796
1756
  const attemptDir = join(paths.attemptsDir, attemptId);
1797
- try {
1798
- assertNoSecretLikeTokens("candidate patch diff", diff);
1799
- }
1800
- catch (err) {
1801
- // The stream already settled real spend; a post-stream assertion throw
1802
- // must carry it so the slot catch settles the TRUE cost, not 0.
1803
- throw Object.assign(err instanceof Error ? err : new Error(String(err)), { costUsd: cost });
1804
- }
1805
1757
  recordCleanAttemptMetrics(globalConfigDir(), adapter.id, {
1806
1758
  costUsd: cost,
1807
1759
  streamMs: attemptStreamEndedMs - attemptStartedMs,
@@ -1809,11 +1761,13 @@ export class Orchestrator {
1809
1761
  aborted: signal?.aborted === true,
1810
1762
  authMode: telemetry.authMode,
1811
1763
  });
1812
- const producedFiles = writeCandidateAttemptArtifacts({
1764
+ const producedFiles = AC.withAttemptFailureCost(() => writeCandidateAttemptArtifacts({
1813
1765
  store,
1814
1766
  attemptDir,
1815
1767
  worktreePath: envelope.worktree_path,
1816
1768
  diff,
1769
+ persistPatch: secretDiffRefusal === undefined,
1770
+ persistProducedMedia: secretDiffRefusal === undefined,
1817
1771
  answerText,
1818
1772
  record: {
1819
1773
  attempt_id: attemptId,
@@ -1825,9 +1779,14 @@ export class Orchestrator {
1825
1779
  errors: errors.slice(0, 5),
1826
1780
  ...telemetrySummary(telemetry),
1827
1781
  outcome: telemetry.outcome,
1782
+ ...(secretDiffRefusal ? { secret_diff_refusal: secretDiffRefusal } : {}),
1828
1783
  gates: gates.map((g) => ({ id: g.id, status: g.status })),
1829
1784
  branch: envelope.branch_name,
1830
1785
  },
1786
+ }), {
1787
+ totalUsd: cost,
1788
+ estimated: costEstimated,
1789
+ settlement: attemptUsageCostSettlement(cost, costEstimated, attemptId, adapter.id, telemetry.authMode, telemetry.usageCost),
1831
1790
  });
1832
1791
  return {
1833
1792
  attemptId,
@@ -1844,6 +1803,7 @@ export class Orchestrator {
1844
1803
  costEstimated,
1845
1804
  errors: errors.slice(0, 8),
1846
1805
  telemetry,
1806
+ ...(secretDiffRefusal ? { secretDiffRefusal } : {}),
1847
1807
  outcomeClass: finalized.outcomeClass,
1848
1808
  };
1849
1809
  }
@@ -1989,16 +1949,7 @@ export class Orchestrator {
1989
1949
  safeInvoke(input.onRunStart, { runId, taskId, runDir: paths.root });
1990
1950
  log.emit("run.created", { mode, prompt: redactSecrets(input.prompt) });
1991
1951
  const ledger = this.rootLedger(input, contract, log);
1992
- announce?.({
1993
- log,
1994
- store,
1995
- paths,
1996
- runId,
1997
- taskId,
1998
- mode,
1999
- phase: "race",
2000
- spend: () => ledger.spend(),
2001
- });
1952
+ announce?.(announcedRunContext({ log, store, paths, runId, taskId, mode, phase: "race" }, ledger, () => this.deps.delegationBudgetAuthority?.hasParent(runId) === true));
2002
1953
  store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
2003
1954
  log.emit("task.contract.created", { task_contract_hash: hashJson(contract) });
2004
1955
  // Write modes need a git boundary for worktree isolation and honest diffs.
@@ -2087,6 +2038,7 @@ export class Orchestrator {
2087
2038
  candidates: [],
2088
2039
  };
2089
2040
  }
2041
+ activateDelegationParent(this.deps.delegationBudgetAuthority, input, runId, ledger, adapters, log);
2090
2042
  const reviewersOutcome = await this.resolveReviewersWithArtifacts(input, log, store, paths, runId, taskId, mode);
2091
2043
  if ("failed" in reviewersOutcome)
2092
2044
  return reviewersOutcome.failed;
@@ -2311,7 +2263,7 @@ export class Orchestrator {
2311
2263
  effectiveRun = contRun;
2312
2264
  }
2313
2265
  catch (err) {
2314
- ledger.settle(contLeaseId, unknownCostSettlement("continuation-error", 0));
2266
+ ledger.settle(contLeaseId, AC.attemptFailureCost(err, "continuation-error", 0).settlement);
2315
2267
  log.emit("harness.completed", {
2316
2268
  harness_id: adapter.id,
2317
2269
  attempt_id: contAttemptId,
@@ -2335,18 +2287,9 @@ export class Orchestrator {
2335
2287
  envelope = undefined;
2336
2288
  }
2337
2289
  catch (err) {
2338
- // Envelope creation (or another pre-stream step) failed; stream errors
2339
- // are absorbed inside runCandidateInEnvelope with their real cost. A
2340
- // post-stream throw (e.g. the secret-token assertion) carries its
2341
- // streamed spend on the error — settle the TRUE cost, never launder
2342
- // real spend down to 0.
2343
- const carriedCost = typeof err?.costUsd === "number"
2344
- ? err.costUsd
2345
- : 0;
2346
- 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);
2347
2292
  const message = safeErrorMessage(err);
2348
- // envelope is still undefined when wsm.create() itself threw — that is
2349
- // a workspace-phase infrastructure failure, not a harness error.
2350
2293
  const infraPhase = envelope === undefined ? "workspace" : "harness";
2351
2294
  log.emit("harness.completed", {
2352
2295
  harness_id: adapter.id,
@@ -2355,26 +2298,22 @@ export class Orchestrator {
2355
2298
  error: message,
2356
2299
  phase: infraPhase,
2357
2300
  });
2358
- // Minimal attempt record so failure.yaml's rawDetailRef never dangles.
2359
- store.writeYaml(join(paths.attemptsDir, slot.attemptId, "attempt.yaml"), {
2360
- attempt_id: slot.attemptId,
2361
- harness_id: adapter.id,
2362
- cost_usd: carriedCost,
2363
- errored: true,
2364
- phase: infraPhase,
2365
- errors: [message],
2366
- });
2301
+ store.writeYaml(join(paths.attemptsDir, slot.attemptId, "attempt.yaml"), AC.attemptFailureRecord(slot.attemptId, adapter.id, failureCost, infraPhase, message));
2367
2302
  runsBySlot[slotIdx] = {
2368
2303
  attemptId: slot.attemptId,
2369
2304
  harnessId: adapter.id,
2370
2305
  label: slot.label,
2371
2306
  diff: "",
2372
2307
  gates: [],
2373
- cost: 0,
2308
+ cost: failureCost.totalUsd,
2374
2309
  errored: true,
2375
- costEstimated: false,
2310
+ costEstimated: failureCost.estimated,
2376
2311
  errors: [message],
2377
- 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),
2378
2317
  infraPhase,
2379
2318
  };
2380
2319
  }
@@ -2397,7 +2336,7 @@ export class Orchestrator {
2397
2336
  // arbitration (as the race-adoption path does) would fold those user edits
2398
2337
  // into the revert target and let a later revert clobber them.
2399
2338
  let earlyPostTurnSha = null;
2400
- if (input.inPlace === true && requestedSingleCandidate) {
2339
+ if (input.inPlace && requestedSingleCandidate && runs.every((run) => !run.secretDiffRefusal)) {
2401
2340
  try {
2402
2341
  earlyPostTurnSha = await snapshotTree(execRoot);
2403
2342
  }
@@ -2409,6 +2348,20 @@ export class Orchestrator {
2409
2348
  await disposeReviewEnvelopes();
2410
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);
2411
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
+ }
2412
2365
  if (runs.length === 0) {
2413
2366
  const budgetReason = ledger.terminal();
2414
2367
  // QA-050: when the zero-candidate cause is a budget refusal, the shared
@@ -2428,12 +2381,7 @@ export class Orchestrator {
2428
2381
  why_winner: why,
2429
2382
  evidence_facts: ["no candidates were produced"],
2430
2383
  apply_recommendation: "continue",
2431
- budget_summary: {
2432
- spend_usd: ledger.spend(),
2433
- estimated: false,
2434
- cash_usd: ledger.spend(),
2435
- valuation_usd: ledger.valuation(),
2436
- },
2384
+ budget_summary: decisionBudgetSummary(ledger),
2437
2385
  });
2438
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`);
2439
2387
  if (agentBudgetMapping) {
@@ -2480,7 +2428,7 @@ export class Orchestrator {
2480
2428
  if (workingRuns.length === 0) {
2481
2429
  await disposeReviewEnvelopes();
2482
2430
  const first = runs[0];
2483
- const phase = first.infraPhase ?? "harness";
2431
+ const phase = first.secretDiffRefusal ? "artifact_security" : (first.infraPhase ?? "harness");
2484
2432
  const { facts, why: rootCause } = partitionCandidates(runs);
2485
2433
  store.writeYaml(join(paths.arbitrationDir, "decision.yaml"), {
2486
2434
  winner: null,
@@ -2488,12 +2436,7 @@ export class Orchestrator {
2488
2436
  why_winner: rootCause,
2489
2437
  evidence_facts: runs.map((r) => `${r.attemptId} produced no work: ${r.errors[0] ?? "unknown"}`),
2490
2438
  apply_recommendation: "continue",
2491
- budget_summary: {
2492
- spend_usd: ledger.spend(),
2493
- estimated: false,
2494
- cash_usd: ledger.spend(),
2495
- valuation_usd: ledger.valuation(),
2496
- },
2439
+ budget_summary: decisionBudgetSummary(ledger),
2497
2440
  });
2498
2441
  this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null);
2499
2442
  store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${mode})\n\n- Lifecycle: ${facts.lifecycle}\n- Phase: ${phase}\n\n${rootCause}\n`);
@@ -2513,9 +2456,11 @@ export class Orchestrator {
2513
2456
  rawDetailRef: `attempts/${first.attemptId}/attempt.yaml`,
2514
2457
  eventRefs: existingEventRefs,
2515
2458
  runDir: paths.root,
2516
- nextActions: phase === "workspace"
2517
- ? ["Check the project folder", "Open diagnostics", "Retry the run"]
2518
- : 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),
2519
2464
  });
2520
2465
  log.emit("output.ready", { kind: "summary", path: "final/summary.md", state: "diagnostic" });
2521
2466
  log.emit("run.failed", {
@@ -2648,7 +2593,7 @@ export class Orchestrator {
2648
2593
  }
2649
2594
  }
2650
2595
  catch (err) {
2651
- ledger.settle(lease.lease?.lease_id ?? "", unknownCostSettlement("synthesis-error"));
2596
+ ledger.settle(lease.lease?.lease_id ?? "", AC.attemptFailureCost(err, "synthesis-error").settlement);
2652
2597
  log.emit("harness.completed", {
2653
2598
  attempt_id: "synth",
2654
2599
  status: "failed",
@@ -2666,14 +2611,7 @@ export class Orchestrator {
2666
2611
  }
2667
2612
  let result;
2668
2613
  try {
2669
- result = arbitrate(evidences, {
2670
- spendUsd: ledger.spend(),
2671
- estimatedSpend: runs.some((r) => r.costEstimated),
2672
- // QA-010b: carry the settled cash + subscription-valuation totals
2673
- // (reviewer panel included) onto the decision record.
2674
- cashUsd: ledger.spend(),
2675
- valuationUsd: ledger.valuation(),
2676
- });
2614
+ result = arbitrate(evidences, arbitrationBudgetOptions(ledger));
2677
2615
  }
2678
2616
  catch (err) {
2679
2617
  // Arbitration throws end terminally with artifacts, never as an orphan.
@@ -3086,7 +3024,7 @@ export class Orchestrator {
3086
3024
  // (so a failing test gate or no_op outcome is unchanged), just unreviewed.
3087
3025
  const hasDiff = run.diff.trim().length > 0;
3088
3026
  // Reviewer panels spend real money: reserve before, settle the observed cost.
3089
- const reviewLease = hasDiff
3027
+ const reviewLease = hasDiff && reviewers.length > 0
3090
3028
  ? ledger?.reserve({
3091
3029
  taskId: taskId ?? "task",
3092
3030
  attemptId: run.attemptId,
@@ -3119,11 +3057,16 @@ export class Orchestrator {
3119
3057
  reviewSpendUsd: 0,
3120
3058
  reviewSpendEstimated: false,
3121
3059
  reviewCashUsd: 0,
3060
+ reviewCashKnowledge: "unknown",
3122
3061
  reviewValuationUsd: 0,
3062
+ reviewValuationKnowledge: "unknown",
3123
3063
  reviewUnknownUsd: 0,
3124
3064
  };
3125
3065
  if (reviewLease?.granted) {
3126
- 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));
3127
3070
  if ((result.reviewSpendUsd ?? 0) > 0) {
3128
3071
  log.emit("budget.observation", {
3129
3072
  harness_id: "review-panel",
@@ -3243,20 +3186,23 @@ export class Orchestrator {
3243
3186
  const execRoot = this.execRootOf(input);
3244
3187
  const wsm = new WorkspaceManager(execRoot);
3245
3188
  const readiness = new ReadinessLedger();
3246
- const ledger = this.rootLedger(input, contract, log);
3247
- store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
3248
- safeInvoke(input.onRunStart, { runId, taskId, runDir: paths.root });
3249
- log.emit("run.created", { mode, prompt: redactSecrets(input.prompt) });
3250
- announce?.({
3251
- log,
3252
- store,
3253
- paths,
3254
- runId,
3255
- taskId,
3256
- mode,
3257
- phase: "convergence",
3258
- spend: () => ledger.spend(),
3259
- });
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
+ }
3260
3206
  // Live (in-place) isolation deliberately tolerates non-git stateful
3261
3207
  // environments; only envelope isolation needs the git boundary.
3262
3208
  if (!input.inPlace) {
@@ -3334,6 +3280,7 @@ export class Orchestrator {
3334
3280
  candidates: [],
3335
3281
  };
3336
3282
  }
3283
+ activateDelegationParent(this.deps.delegationBudgetAuthority, input, runId, ledger, adapterPool, log);
3337
3284
  // Fail fast on a provably unwinnable predicate instead of burning paid
3338
3285
  // rounds: the default convergence predicate requires a clean cross-family
3339
3286
  // review, which needs >=2 healthy reviewer provider families.
@@ -3405,7 +3352,6 @@ export class Orchestrator {
3405
3352
  const allCooledDown = () => adapterPool.every((a) => ledger.cooldownActive(a.adapter.id));
3406
3353
  const attemptTelemetries = [];
3407
3354
  let lastDiffStable = true;
3408
- let reviewSpendEstimated = false;
3409
3355
  try {
3410
3356
  // The contract's ENGINE-COMPUTED effective profile drives the envelope and
3411
3357
  // every attempt spec (parity with runRace); telemetry must never claim an
@@ -3495,30 +3441,48 @@ export class Orchestrator {
3495
3441
  });
3496
3442
  }
3497
3443
  catch (err) {
3498
- // Envelope/setup failure before the stream; stream errors are absorbed
3499
- // inside runCandidateInEnvelope with their real accumulated cost.
3500
- 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);
3501
3449
  log.emit("harness.completed", {
3502
3450
  harness_id: adapter.id,
3503
3451
  attempt_id: attemptId,
3504
3452
  status: "failed",
3505
- error: safeErrorMessage(err),
3453
+ error: message,
3506
3454
  });
3455
+ store.writeYaml(join(paths.attemptsDir, attemptId, "attempt.yaml"), AC.attemptFailureRecord(attemptId, adapter.id, failureCost, "harness", message));
3507
3456
  run = {
3508
3457
  attemptId,
3509
3458
  harnessId: adapter.id,
3510
3459
  label: `Attempt ${attempt}`,
3511
3460
  diff: "",
3512
3461
  gates: [],
3513
- cost: 0,
3462
+ cost: failureCost.totalUsd,
3514
3463
  errored: true,
3515
- costEstimated: false,
3516
- errors: [safeErrorMessage(err)],
3517
- 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),
3518
3467
  };
3519
3468
  }
3520
3469
  lastRun = run;
3521
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
+ }
3522
3486
  // D-16 r8: interrupted (errored===false) would CONVERGE a partial diff
3523
3487
  // as clean — break BEFORE review; a harness error still gate-retries.
3524
3488
  if (run.outcomeClass === "interrupted") {
@@ -3580,11 +3544,16 @@ export class Orchestrator {
3580
3544
  reviewSpendUsd: 0,
3581
3545
  reviewSpendEstimated: false,
3582
3546
  reviewCashUsd: 0,
3547
+ reviewCashKnowledge: "unknown",
3583
3548
  reviewValuationUsd: 0,
3549
+ reviewValuationKnowledge: "unknown",
3584
3550
  reviewUnknownUsd: 0,
3585
3551
  };
3586
3552
  if (reviewLease?.granted) {
3587
- 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));
3588
3557
  if ((reviewResult.reviewSpendUsd ?? 0) > 0) {
3589
3558
  log.emit("budget.observation", {
3590
3559
  harness_id: "review-panel",
@@ -3596,8 +3565,6 @@ export class Orchestrator {
3596
3565
  unknown_usd: reviewResult.reviewUnknownUsd,
3597
3566
  estimated: reviewResult.reviewSpendEstimated === true,
3598
3567
  });
3599
- if (reviewResult.reviewSpendEstimated === true)
3600
- reviewSpendEstimated = true;
3601
3568
  }
3602
3569
  }
3603
3570
  else if (reviewLease && !reviewLease.granted) {
@@ -3779,13 +3746,7 @@ export class Orchestrator {
3779
3746
  if (lastRun && !interrupted) {
3780
3747
  const arb = arbitrate([
3781
3748
  toCandidateEvidence(lastRun, contract, lastFindings, lastFinalReviewClean, actualReviewVerified),
3782
- ], {
3783
- spendUsd: ledger.spend(),
3784
- estimatedSpend: lastRun.costEstimated || reviewSpendEstimated,
3785
- // QA-010b: settled cash + valuation (reviewer panel included).
3786
- cashUsd: ledger.spend(),
3787
- valuationUsd: ledger.valuation(),
3788
- });
3749
+ ], arbitrationBudgetOptions(ledger));
3789
3750
  decision = arb.decision;
3790
3751
  store.writeYaml(join(paths.arbitrationDir, "decision.yaml"), decision);
3791
3752
  // A converged run adopts the arbitration axes (checks/review); an
@@ -4017,40 +3978,6 @@ export class Orchestrator {
4017
3978
  reviewVerified: actualReviewVerified,
4018
3979
  };
4019
3980
  }
4020
- /** plan mode: multi-harness planning -> aggregate -> (optional) plan review -> plan. Read-only. */
4021
- /**
4022
- * Wrap the user's goal in an explicit "plan, do not implement" instruction.
4023
- * Without this the raw prompt ("make a racing game") reaches the harness with
4024
- * only a read-only sandbox, so the model tries to BUILD it and dumps code into
4025
- * the plan when writes are blocked — the v0.9 "HTML in the plan" bug. The
4026
- * read-only access still enforces it; this gives the model the right job.
4027
- */
4028
- planPrompt(goal) {
4029
- return [
4030
- `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.`,
4031
- ``,
4032
- `## Goal`,
4033
- goal,
4034
- ``,
4035
- `## Required output (markdown)`,
4036
- `1. Approach — 2-3 sentences on how you'd solve this.`,
4037
- `2. Steps — a numbered list; each step names the file(s) it touches and what changes.`,
4038
- `3. Risks & edge cases.`,
4039
- `4. End your response with a section titled exactly:`,
4040
- ``,
4041
- `## Open Questions`,
4042
- ``,
4043
- `List every decision the user must make before implementation, one per bullet, in EXACTLY this format:`,
4044
- ``,
4045
- `- [single] <question> :: <option A> :: <option B>`,
4046
- `- [multi] <question> :: <option A> :: <option B>`,
4047
- `- [text] <question that has no good fixed options>`,
4048
- ``,
4049
- `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)`,
4050
- ``,
4051
- `Keep it concise. Reference real paths you found. Do NOT paste large code blocks; describe the change instead.`,
4052
- ].join("\n");
4053
- }
4054
3981
  /** One read-only planner spawn shared by solo fallback, Council drafts, and merge. */
4055
3982
  async runPlannerAttempt(args) {
4056
3983
  const { input, contract, taskId, runId, log, store, paths, ledger, routed, attemptId } = args;
@@ -4309,16 +4236,7 @@ export class Orchestrator {
4309
4236
  safeInvoke(input.onRunStart, { runId, taskId, runDir: paths.root });
4310
4237
  log.emit("run.created", { mode: "plan", prompt: redactSecrets(input.prompt) });
4311
4238
  const ledger = this.rootLedger(input, contract, log);
4312
- announce?.({
4313
- log,
4314
- store,
4315
- paths,
4316
- runId,
4317
- taskId,
4318
- mode: "plan",
4319
- phase: "plan",
4320
- spend: () => ledger.spend(),
4321
- });
4239
+ announce?.(announcedRunContext({ log, store, paths, runId, taskId, mode: "plan", phase: "plan" }, ledger, () => this.deps.delegationBudgetAuthority?.hasParent(runId) === true));
4322
4240
  store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
4323
4241
  log.emit("task.contract.created", { task_contract_hash: hashJson(contract) });
4324
4242
  // W3.3: ONE resolved read-only context — the routing point-probe and every
@@ -4453,7 +4371,7 @@ export class Orchestrator {
4453
4371
  attemptId,
4454
4372
  laneRun,
4455
4373
  fallbackHome: roHome.env,
4456
- promptBody: this.planPrompt(input.prompt) + contextSection,
4374
+ promptBody: planPrompt(input.prompt) + contextSection,
4457
4375
  intent: "plan",
4458
4376
  });
4459
4377
  if (outcome.budgetDenied) {
@@ -4576,7 +4494,7 @@ export class Orchestrator {
4576
4494
  runPlannerAttempt: (a) => this.runPlannerAttempt(a),
4577
4495
  writeRunTelemetry: (store, paths, contract, runId, taskId, mode, attempts, finalAttemptId) => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, attempts, finalAttemptId),
4578
4496
  execRootOf: (input) => this.execRootOf(input),
4579
- planPrompt: (goal) => this.planPrompt(goal),
4497
+ planPrompt,
4580
4498
  };
4581
4499
  }
4582
4500
  /** ask: one selected harness answers read-only questions; no patch/apply controls. */
@@ -4605,21 +4523,14 @@ export class Orchestrator {
4605
4523
  resolvePaidBudget(inputBudget, cfg) {
4606
4524
  return inputBudget ?? this.deps.paidBudget ?? cfg.global.budget.paid_budget_per_run;
4607
4525
  }
4608
- rootLedger(_input, contract, log) {
4609
- // The root ledger discloses into THIS run's log: the ledger is the one
4610
- // owner of the cash fact (subscription-entitled work settles to 0 there),
4611
- // and the UI renders `budget.cash` verbatim — never inferring money from
4612
- // route labels (W4.3 sol #15).
4613
- const ledger = new BudgetLedger(contract.budget.paid_budget, undefined, {
4614
- onCashSettled: (cashSpendUsd, valuationUsd) => log.emit("budget.cash", {
4615
- cash_spend_usd: cashSpendUsd,
4616
- valuation_usd: valuationUsd,
4617
- }),
4526
+ rootLedger(input, contract, log) {
4527
+ return createRootLedger({
4528
+ input,
4529
+ contract,
4530
+ log,
4531
+ authority: this.deps.delegationBudgetAuthority,
4532
+ quotaSnapshots: this.deps.quotaSnapshots?.() ?? [],
4618
4533
  });
4619
- for (const snapshot of this.deps.quotaSnapshots?.() ?? []) {
4620
- ledger.observeQuotaSnapshot(snapshot);
4621
- }
4622
- return ledger;
4623
4534
  }
4624
4535
  routeBillingKnowledge(input, harnessId) {
4625
4536
  // A selected profile's credential_kind decides billing (round-18 #2).
@@ -4699,16 +4610,7 @@ export class Orchestrator {
4699
4610
  safeInvoke(input.onRunStart, { runId, taskId, runDir: paths.root });
4700
4611
  log.emit("run.created", { mode: opts.mode, prompt: redactSecrets(prompt) });
4701
4612
  const ledger = this.rootLedger(input, contract, log);
4702
- announce?.({
4703
- log,
4704
- store,
4705
- paths,
4706
- runId,
4707
- taskId,
4708
- mode: opts.mode,
4709
- phase: "report",
4710
- spend: () => ledger.spend(),
4711
- });
4613
+ announce?.(announcedRunContext({ log, store, paths, runId, taskId, mode: opts.mode, phase: "report" }, ledger, () => this.deps.delegationBudgetAuthority?.hasParent(runId) === true));
4712
4614
  store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
4713
4615
  log.emit("task.contract.created", { task_contract_hash: hashJson(contract) });
4714
4616
  // The ask/deep-scan report stays bare (its scouts read the tree themselves);