@claudexor/orchestrator 3.10.4 → 3.11.0

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 (114) hide show
  1. package/dist/attemptTelemetry.d.ts +2 -1
  2. package/dist/attemptTelemetry.d.ts.map +1 -1
  3. package/dist/attemptTelemetry.js +11 -0
  4. package/dist/attemptTelemetry.js.map +1 -1
  5. package/dist/attemptUsage.d.ts.map +1 -1
  6. package/dist/attemptUsage.js +2 -1
  7. package/dist/attemptUsage.js.map +1 -1
  8. package/dist/attemptUsageCost.d.ts +1 -1
  9. package/dist/attemptUsageCost.d.ts.map +1 -1
  10. package/dist/attemptUsageCost.js +19 -110
  11. package/dist/attemptUsageCost.js.map +1 -1
  12. package/dist/candidateEvidence.d.ts +8 -0
  13. package/dist/candidateEvidence.d.ts.map +1 -1
  14. package/dist/candidateEvidence.js +22 -4
  15. package/dist/candidateEvidence.js.map +1 -1
  16. package/dist/candidateOutputs.d.ts +6 -0
  17. package/dist/candidateOutputs.d.ts.map +1 -1
  18. package/dist/candidateOutputs.js +6 -1
  19. package/dist/candidateOutputs.js.map +1 -1
  20. package/dist/candidateReview.d.ts +2 -0
  21. package/dist/candidateReview.d.ts.map +1 -1
  22. package/dist/candidateReview.js +27 -3
  23. package/dist/candidateReview.js.map +1 -1
  24. package/dist/candidateTerminals.d.ts +34 -0
  25. package/dist/candidateTerminals.d.ts.map +1 -0
  26. package/dist/candidateTerminals.js +183 -0
  27. package/dist/candidateTerminals.js.map +1 -0
  28. package/dist/continuity-facts.d.ts +4 -1
  29. package/dist/continuity-facts.d.ts.map +1 -1
  30. package/dist/continuity-facts.js +3 -0
  31. package/dist/continuity-facts.js.map +1 -1
  32. package/dist/continuity-summary.d.ts +7 -2
  33. package/dist/continuity-summary.d.ts.map +1 -1
  34. package/dist/continuity-summary.js +19 -2
  35. package/dist/continuity-summary.js.map +1 -1
  36. package/dist/deepScanOutput.d.ts +19 -0
  37. package/dist/deepScanOutput.d.ts.map +1 -0
  38. package/dist/deepScanOutput.js +38 -0
  39. package/dist/deepScanOutput.js.map +1 -0
  40. package/dist/deepScanReducer.d.ts +2 -17
  41. package/dist/deepScanReducer.d.ts.map +1 -1
  42. package/dist/deepScanReducer.js +19 -40
  43. package/dist/deepScanReducer.js.map +1 -1
  44. package/dist/delegationBelt.d.ts +9 -0
  45. package/dist/delegationBelt.d.ts.map +1 -0
  46. package/dist/delegationBelt.js +54 -0
  47. package/dist/delegationBelt.js.map +1 -0
  48. package/dist/delegationBudgetAuthority.d.ts +1 -1
  49. package/dist/delegationBudgetAuthority.d.ts.map +1 -1
  50. package/dist/delegationBudgetAuthority.js.map +1 -1
  51. package/dist/directoryCandidate.d.ts +75 -0
  52. package/dist/directoryCandidate.d.ts.map +1 -0
  53. package/dist/directoryCandidate.js +171 -0
  54. package/dist/directoryCandidate.js.map +1 -0
  55. package/dist/git-precondition.d.ts +2 -0
  56. package/dist/git-precondition.d.ts.map +1 -1
  57. package/dist/git-precondition.js +20 -1
  58. package/dist/git-precondition.js.map +1 -1
  59. package/dist/index.d.ts +1 -0
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +1 -0
  62. package/dist/index.js.map +1 -1
  63. package/dist/modelGovernance.d.ts +2 -1
  64. package/dist/modelGovernance.d.ts.map +1 -1
  65. package/dist/modelGovernance.js +33 -2
  66. package/dist/modelGovernance.js.map +1 -1
  67. package/dist/orchestrator.d.ts +8 -33
  68. package/dist/orchestrator.d.ts.map +1 -1
  69. package/dist/orchestrator.js +468 -513
  70. package/dist/orchestrator.js.map +1 -1
  71. package/dist/plannerAttempt.d.ts +1 -1
  72. package/dist/plannerAttempt.d.ts.map +1 -1
  73. package/dist/plannerAttempt.js +17 -2
  74. package/dist/plannerAttempt.js.map +1 -1
  75. package/dist/policyFindings.d.ts +2 -0
  76. package/dist/policyFindings.d.ts.map +1 -1
  77. package/dist/policyFindings.js +11 -1
  78. package/dist/policyFindings.js.map +1 -1
  79. package/dist/processing-dispatch.d.ts +25 -0
  80. package/dist/processing-dispatch.d.ts.map +1 -0
  81. package/dist/processing-dispatch.js +122 -0
  82. package/dist/processing-dispatch.js.map +1 -0
  83. package/dist/processing-routing.d.ts +9 -0
  84. package/dist/processing-routing.d.ts.map +1 -0
  85. package/dist/processing-routing.js +86 -0
  86. package/dist/processing-routing.js.map +1 -0
  87. package/dist/processing-telemetry.d.ts +11 -0
  88. package/dist/processing-telemetry.d.ts.map +1 -0
  89. package/dist/processing-telemetry.js +31 -0
  90. package/dist/processing-telemetry.js.map +1 -0
  91. package/dist/reviewerPanel.d.ts.map +1 -1
  92. package/dist/reviewerPanel.js +12 -9
  93. package/dist/reviewerPanel.js.map +1 -1
  94. package/dist/root-ledger.d.ts.map +1 -1
  95. package/dist/root-ledger.js +2 -1
  96. package/dist/root-ledger.js.map +1 -1
  97. package/dist/runFactsBuilder.d.ts.map +1 -1
  98. package/dist/runFactsBuilder.js +23 -10
  99. package/dist/runFactsBuilder.js.map +1 -1
  100. package/dist/runFactsDeliverable.d.ts.map +1 -1
  101. package/dist/runFactsDeliverable.js +14 -1
  102. package/dist/runFactsDeliverable.js.map +1 -1
  103. package/dist/runFactsPresentation.d.ts.map +1 -1
  104. package/dist/runFactsPresentation.js +1 -0
  105. package/dist/runFactsPresentation.js.map +1 -1
  106. package/dist/runSupport.d.ts +1 -1
  107. package/dist/runTerminalResults.d.ts.map +1 -1
  108. package/dist/runTerminalResults.js +14 -6
  109. package/dist/runTerminalResults.js.map +1 -1
  110. package/dist/task-contract-builder.d.ts +2 -1
  111. package/dist/task-contract-builder.d.ts.map +1 -1
  112. package/dist/task-contract-builder.js +1 -0
  113. package/dist/task-contract-builder.js.map +1 -1
  114. package/package.json +17 -17
@@ -1,3 +1,6 @@
1
+ import { delegationBeltFor } from "./delegationBelt.js";
2
+ import { bindProcessingAdmission, processingAdmissionForLease, updateProcessingStreamHold, ProcessingBudgetAdmissionError, } from "./processing-dispatch.js";
3
+ import { cancelledCandidatesResult, emptyCandidateResult, failedCandidatesResult, } from "./candidateTerminals.js";
1
4
  import { existsSync } from "node:fs";
2
5
  import { effectiveAuthPreference, observeNativeSessionEvent, resumeSessionForProfile, rotateSpecOnTypedLimit, selectedProfileAvailability, } from "./credential-profiles.js";
3
6
  import { OrchestratorCredentials, reviewerProfileResolver, rotatedSpecInLaneHome, } from "./orchestrator-credentials.js";
@@ -5,17 +8,18 @@ import { accountPoolRows } from "./account-pool.js";
5
8
  import { writeRunTelemetryArtifact } from "./runTelemetryWriter.js";
6
9
  import { buildFileBackedSynthesisInput, materializeWinnerOutputs, stageFileBackedContext, writeCandidateAttemptArtifacts, } from "./candidateOutputs.js";
7
10
  import { processAttemptUsage } from "./attemptUsage.js";
11
+ import { captureDirectoryCandidate, directoryHasOutput, directoryCandidateStable, observeDirectoryPaths, publishDirectoryCandidate, } from "./directoryCandidate.js";
8
12
  import { appliedAttemptFacts, assertDelegatedEvidence, isMutatingAccess, outerBoundaryNotice, scopedHarnessHome, } from "./delegatedHome.js";
9
13
  import * as AC from "./attemptUsageCost.js";
10
- import { candidateRoster, convergenceOutcomeFacts, isWorkingCandidate, partitionCandidates, toCandidateEvidence, unanimousDeclaredFailure, } from "./candidateEvidence.js";
14
+ import { candidateRoster, candidateStatuses, convergenceOutcomeFacts, isWorkingCandidate, partitionCandidates, toCandidateEvidence, } from "./candidateEvidence.js";
11
15
  import { capabilityIntents } from "@claudexor/gateway";
12
- import { policyFindings } from "./policyFindings.js";
13
16
  import { reviewCandidateRuns, resolveEngineReview, unreviewedCandidateEvidence, evaluateUnreviewedConvergence, } from "./candidateReview.js";
14
17
  import { join } from "node:path";
15
18
  import { finalizePlanRun, runCouncilPlan, writePlanHarnessFailure, } from "./planRun.js";
16
19
  import { runPlannerAttempt as executePlannerAttempt, } from "./plannerAttempt.js";
17
20
  import { HarnessRunSpec, ModeKind as ModeKindSchema, QuotaSnapshot as QuotaSnapshotSchema, isBlocking, makeOutcomeFacts, reviewAllowsApply, workStateVetoes, strictifyOutputSchema, estimateEffectiveAuthRoute, } from "@claudexor/schema";
18
21
  import { globalConfigDir, loadConfig } from "@claudexor/config";
22
+ import { prepareRoutedProcessing, processingCostEvidence, prepareReviewerProcessing, } from "./processing-routing.js";
19
23
  import { acceptedTryOutput, AccessProfileIncompatibleError, AnswerAssembly, CLAUDEXOR_BROWSER_ARTIFACT_SUBDIR, countsAsAgentProgress, HarnessUnavailableError, summarizeDiffPaths as diffStats, withInactivityWatchdog, } from "@claudexor/core";
20
24
  import { assertRouteModelsAllowed, runModelGovernedRoute } from "./modelGovernance.js";
21
25
  import { authModeForCredentialRoute, authModeForPreference } from "./auth-route-classification.js";
@@ -29,7 +33,7 @@ import { planPrompt } from "./plan-prompt.js";
29
33
  import { verifiedPlanBrief, withPlanBrief } from "./planBrief.js";
30
34
  import { resolveRunInputDefaults } from "./run-input-resolution.js";
31
35
  import { beginAnnouncedRun } from "./runEventLog.js";
32
- import { arbitrationBudgetOptions, decisionBudgetSummary } from "./decisionBudget.js";
36
+ import { arbitrationBudgetOptions } from "./decisionBudget.js";
33
37
  import { buildRevisePrompt } from "./revisePrompt.js";
34
38
  import { cancelledResult, declaredFailure, failTerminally, guardAnnouncedRun, writeFailure, cancelReasonFromSignalToken, } from "./runTerminals.js";
35
39
  import { budgetFailureRecord, classifyBudgetFailure } from "./budgetFailure.js";
@@ -39,7 +43,7 @@ import { dropDeltaPastBudget, emitPlanProgress, emitTransientExhausted, emitTran
39
43
  import { promptWithEngineConstraints, sleep, redactHarnessEvent, harnessEventPayload, safeErrorMessage, renderSummary, observeBudgetSignals, rotateOnStall, recordCleanAttemptMetrics, envInheritance, transientRetryPolicy, reviewerTimeoutMs, harnessInactivityTimeoutMs, observeAuthSwitch, emitPrimaryDivergence, emitPoolDegraded, deliveryRefusalFailure, winnerNeedsHuman, writeRaceDeliveryDecision, } from "./runSupport.js";
40
44
  import { candidateStatusInRouteContext, resolveReadOnlyRouteContext, } from "./routeContext.js";
41
45
  import { resolveAutoReviewerPanel, resolveExplicitReviewerPanel } from "./reviewerPanel.js";
42
- import { ensureWriteModeGitBoundary } from "./git-precondition.js";
46
+ import { ensureClaudeBridgeForRun, ensureWriteModeGitBoundary } from "./git-precondition.js";
43
47
  import { buildContinuation, } from "./continuity.js";
44
48
  import { activePlanPointer, resolveContinuitySummary, workspaceAnchor, } from "./continuity-facts.js";
45
49
  import { runDiffReview } from "./diffReview.js";
@@ -56,14 +60,14 @@ import { gateSpecsFromContract, renderTestsEvidence } from "./contract-gates.js"
56
60
  import { buildTaskContract } from "./task-contract-builder.js";
57
61
  import { ArtifactStore } from "@claudexor/artifact-store";
58
62
  import { buildContextPack, rawContextForEnvelope, preflightEvidence, writeEvidencePacket, } from "@claudexor/context";
59
- import { WorkspaceManager, captureRawPatchEnvelope, createRevertAnchorFromPatchOrNull, createRevertAnchorOrNull, ensureClaudeBridge, consumeRawPatchEnvelope, snapshotTree, } from "@claudexor/workspace";
60
- import { blockedDecisionOverride, finalVerifyBlocks, finalVerifyPatch, verifyAndDeliver, } from "@claudexor/delivery";
63
+ import { WorkspaceManager, captureRawPatchEnvelope, createRevertAnchorFromPatchOrNull, createRevertAnchorOrNull, consumeRawPatchEnvelope, snapshotTree, } from "@claudexor/workspace";
64
+ import { blockedDecisionOverride, finalVerifyBlocks, finalVerifyPatch, finalVerifyFiles, verifyAndDeliverFiles, verifyAndDeliver, } from "@claudexor/delivery";
61
65
  import { HarnessGateway } from "@claudexor/gateway";
62
- import { ReadinessLedger, evaluateConvergence, failureSignature, gatesPassed, reviewCandidate, revalidateFindings, runGates, } from "@claudexor/review";
66
+ import { ReadinessLedger, evaluateConvergence, failureSignature, gatesPassed, reviewCandidate, runGates, } from "@claudexor/review";
63
67
  import { arbitrate } from "@claudexor/arbitration";
64
68
  import { buildSynthesisPlan, decideSynthesis } from "@claudexor/synthesis";
65
- import { attemptCostEvidence, attemptUsageCostSettlement, BudgetLedger, isBudgetTerminal, explainRanking, loadHarnessMetrics, promptFingerprint, rankHarnesses, reviewUsageCostSettlement, } from "@claudexor/budget";
66
- import { readTextSafe, appendLine, assertNoInlineSecretValues, DELEGATION_ENV, hashJson, newId, noProjectRepoRoot, redactSecrets, safeInvoke, sha256, userConfigDir, writeText, } from "@claudexor/util";
69
+ import { attemptCostEvidence, billingKnowledgeForAuthRoute, attemptUsageCostSettlement, BudgetLedger, isBudgetTerminal, explainRanking, loadHarnessMetrics, promptFingerprint, rankHarnesses, } from "@claudexor/budget";
70
+ import { readTextSafe, appendLine, assertNoInlineSecretValues, hashJson, newId, noProjectRepoRoot, redactSecrets, safeInvoke, sha256, userConfigDir, } from "@claudexor/util";
67
71
  const LABELS = "ABCDEFGHIJ".split("");
68
72
  const NO_PROJECT_ROOT = noProjectRepoRoot();
69
73
  /** Concurrency cap for parallel candidates/explorers (locked decision: min(n, 4)). */
@@ -228,7 +232,11 @@ export class Orchestrator {
228
232
  try {
229
233
  // Auto-panel dropped knobs (reviewerEfforts) → ignored-settings channel (QA-070):
230
234
  const warn = (d) => void log.emit("review.preflight", { ignored_settings: [d] });
231
- return { reviewers: await this.resolveReviewers(input.repoRoot, input.authPreference, warn) };
235
+ const config = this.config(input.repoRoot).global;
236
+ const reviewers = await this.resolveReviewers(input.repoRoot, input.authPreference, warn);
237
+ return {
238
+ reviewers: await prepareReviewerProcessing(reviewers, input, config, input.paidBudget ?? this.deps.paidBudget ?? config.budget.paid_budget_per_run),
239
+ };
232
240
  }
233
241
  catch (err) {
234
242
  const message = safeErrorMessage(err);
@@ -664,6 +672,7 @@ export class Orchestrator {
664
672
  }),
665
673
  effortLevels: manifest.capabilities.effort_levels,
666
674
  knownModels: manifest.capabilities.known_models,
675
+ modelInventoryRoutes: manifest.capabilities.model_inventory_routes,
667
676
  // A selected profile's credential_kind IS the route (round-18 #2);
668
677
  // the default store's sources apply only to profile-less runs.
669
678
  authRouteEstimate: this.credentials.profileAuthRoute(input, id) ??
@@ -751,6 +760,8 @@ export class Orchestrator {
751
760
  }
752
761
  for (const { routed, error } of quotaRefusals)
753
762
  dropLane(routed.adapter.id, "credential", safeErrorMessage(error));
763
+ const processingConfig = this.config(input.repoRoot).global;
764
+ await prepareRoutedProcessing(quotaPreparedPool, input, this.execRootOf(input), processingConfig, input.paidBudget ?? this.deps.paidBudget ?? processingConfig.budget.paid_budget_per_run, (id) => (routeContext ? (routeContext.envForHarness?.(id) ?? routeContext.env) : undefined));
754
765
  const ordered = this.orderPool(quotaPreparedPool, input, intent, statusById, ledger, runId);
755
766
  if (ordered.length === 0) {
756
767
  if (primaryQuotaRefusal)
@@ -873,6 +884,7 @@ export class Orchestrator {
873
884
  // reading as unknown/paid. Absent (unknown route) falls back to the
874
885
  // metric-derived billingKnowledge below.
875
886
  const authRoute = this.authRouteEvidenceFor(authMode, status?.authSources ?? []);
887
+ const processingCost = processingCostEvidence(r.processing, authRoute ? billingKnowledgeForAuthRoute(authRoute) : "unknown", [`harness:${r.adapter.id}`, `profile:${credentialSubjectId ?? "default"}`]);
876
888
  return {
877
889
  harnessId: r.adapter.id,
878
890
  available: true,
@@ -882,7 +894,12 @@ export class Orchestrator {
882
894
  config.harnesses[r.adapter.id]?.effort ??
883
895
  undefined,
884
896
  billingKnowledge: authMode === "api_key" ? "metered" : "unknown",
885
- incrementalCostUsd: authMode === "api_key" ? (metric?.avg_cost_usd ?? null) : null,
897
+ costEvidence: processingCost,
898
+ incrementalCostUsd: processingCost
899
+ ? processingCost.estimatedUsd
900
+ : authMode === "api_key"
901
+ ? (metric?.avg_cost_usd ?? null)
902
+ : null,
886
903
  credentialRoute: r.quotaAdmission.route ??
887
904
  (authMode === "api_key"
888
905
  ? "managed_api_key"
@@ -1068,11 +1085,6 @@ export class Orchestrator {
1068
1085
  routingGoal: this.deps.routingGoal,
1069
1086
  });
1070
1087
  }
1071
- /**
1072
- * Per-harness settings applied to one route's run spec (model/effort/web
1073
- * defaults, max_turns, tool lists). Knobs the manifest does not support are
1074
- * RETURNED as ignored reasons (disclosed by the caller), never silently sent.
1075
- */
1076
1088
  /**
1077
1089
  * The HarnessRunSpec fields every TASK-PRODUCING lane shares (primary,
1078
1090
  * candidate, planner, explorer, orchestrate-planner). Extracting the identical
@@ -1083,43 +1095,6 @@ export class Orchestrator {
1083
1095
  * execution — owner Quiz-5a); reviewers and the auth smoke build their own
1084
1096
  * specs and never call this.
1085
1097
  */
1086
- /** The extra MCP servers injected into one agent lane's sandbox. Today only
1087
- * the delegation belt (D32): present when `--delegate` is on, the daemon built
1088
- * a belt descriptor, the lane's adapter can inject MCP servers, and the lane is
1089
- * a WRITING agent intent (the delegator integrates results in its workspace;
1090
- * read lanes and reviewers have nothing to delegate). */
1091
- delegationBeltFor(input, intent, routed, resolvedBudget) {
1092
- if (!input?.delegate ||
1093
- !input.delegationBelt ||
1094
- !input.delegationParentRunId ||
1095
- !routed.delegationRequirement.effective)
1096
- return [];
1097
- // A lane that sandbox-cancels the belt below full access (codex) must NOT
1098
- // receive a belt it cannot use. Per-lane requirement resolution records the
1099
- // typed degradation, while a mixed pool keeps the belt on lanes that can
1100
- // host it.
1101
- if (routed.mcpInjectionRequiresFullAccess && !isFullAccess(routed.adapterAccess))
1102
- return [];
1103
- const writingIntents = ["implement", "create_from_scratch", "repair"];
1104
- if (!writingIntents.includes(intent))
1105
- return [];
1106
- // The CLI built the descriptor from the RAW request budget (undefined when
1107
- // the caller relied on a config/dep default), which would leave the belt
1108
- // unlimited while the real run is capped. Rebind the belt's parent-budget
1109
- // env to the RESOLVED budget (resolvePaidBudget output) so sub-run draws are
1110
- // bounded by the same headroom the parent run enforces — one budget owner.
1111
- return [
1112
- {
1113
- ...input.delegationBelt,
1114
- env: {
1115
- ...input.delegationBelt.env,
1116
- [DELEGATION_ENV.parentRunId]: input.delegationParentRunId,
1117
- [DELEGATION_ENV.repoRoot]: input.repoRoot,
1118
- [DELEGATION_ENV.budget]: JSON.stringify(resolvedBudget),
1119
- },
1120
- },
1121
- ];
1122
- }
1123
1098
  harnessSpecKnobs(contract, knobs, intent) {
1124
1099
  return {
1125
1100
  external_context_policy: knobs.webPolicy,
@@ -1130,6 +1105,10 @@ export class Orchestrator {
1130
1105
  },
1131
1106
  model_hint: knobs.model,
1132
1107
  effort_hint: knobs.effort,
1108
+ processing_preference: contract.processing_preference,
1109
+ processing: knobs.processing?.receipt,
1110
+ processing_cost_basis: knobs.processing?.costBasis,
1111
+ processing_allow_paid: knobs.processingAllowPaid,
1133
1112
  max_turns: knobs.maxTurns,
1134
1113
  ...(intent === "synthesize" ? {} : { instructions: contract.instructions }),
1135
1114
  // The user's answer contract rides every answer-producing lane INCLUDING
@@ -1221,6 +1200,8 @@ export class Orchestrator {
1221
1200
  return {
1222
1201
  model,
1223
1202
  effort,
1203
+ processing: routed.processing,
1204
+ processingAllowPaid: routed.processingAllowPaid,
1224
1205
  webPolicy,
1225
1206
  maxTurns,
1226
1207
  toolsAllow,
@@ -1237,7 +1218,7 @@ export class Orchestrator {
1237
1218
  * Returns null (no packet, no pointer) for native resume, a fresh thread, or
1238
1219
  * a non-thread run. Never throws: continuity failure degrades to no packet.
1239
1220
  */
1240
- async resolveContinuity(runInput, harnessId, resolvedProfileId, nativeResumeAvailable, sessionFields, store, paths, repoRoot, log) {
1221
+ async resolveContinuity(runInput, harnessId, resolvedProfileId, nativeResumeAvailable, sessionFields, store, paths, repoRoot, log, processing, processingAdmission, physicalDispatchStarted) {
1241
1222
  const ctx = runInput.threadContinuity;
1242
1223
  if (!runInput.threadId || !ctx)
1243
1224
  return null;
@@ -1286,6 +1267,9 @@ export class Orchestrator {
1286
1267
  laneEnv: this.laneHomeEnvFor(runInput, harnessId, profileId) ?? {},
1287
1268
  envInheritance: envInheritance(this.config(runInput.repoRoot)),
1288
1269
  signal: runInput.signal,
1270
+ processing,
1271
+ processingAdmission,
1272
+ physicalDispatchStarted,
1289
1273
  });
1290
1274
  const result = buildContinuation(req);
1291
1275
  // Disclose on every lane and stamp the turn (INV-137: never silent).
@@ -1343,7 +1327,7 @@ export class Orchestrator {
1343
1327
  // Refuses (never silently degrades) when a delegated run cannot be confined.
1344
1328
  return scopedHarnessHome(wsm, envelope, envelope.worktree_path === envelope.repo_root, runInput?.delegated === true, routed.adapterAccess);
1345
1329
  }
1346
- async runCandidateInEnvelope(routed, envelope, attemptId, label, contract, prompt, store, paths, wsm, ledger, access, onHarnessEvent, signal, modelHint, effortHint, intent, log, effectiveWebMode, interaction, budgetGuard, runInput, streamDeltas, fileBackedContext,
1330
+ async runCandidateInEnvelope(routed, envelope, attemptId, label, contract, prompt, store, paths, wsm, ledger, processingLease, access, onHarnessEvent, signal, modelHint, effortHint, intent, log, effectiveWebMode, interaction, budgetGuard, runInput, streamDeltas, fileBackedContext,
1347
1331
  /** D-16d: when set, the mechanical continuation checkpoint pointer for a
1348
1332
  * one-shot fresh-session continuation — appended to the prompt so the model
1349
1333
  * (and the offline fake) re-grounds in the exhausted attempt's partial work. */
@@ -1353,21 +1337,33 @@ export class Orchestrator {
1353
1337
  * must record what it ran under, not just why it stopped. REQUIRED (no
1354
1338
  * default): a silent fallback here would spawn a delegated attempt on the
1355
1339
  * operator's real home while the record still claimed scoped state. */
1356
- harnessHome) {
1340
+ harnessHome, observedPaths = new Set()) {
1357
1341
  const adapter = routed.adapter;
1358
1342
  const knobs = this.routeSpecKnobs(routed, contract, modelHint, effortHint);
1359
1343
  // Isolated scoped-home sessions are never retained after disposal.
1360
1344
  const inPlaceEnvelope = envelope.worktree_path === envelope.repo_root;
1345
+ const directory = envelope.workspace_kind === "directory";
1346
+ let directoryCapture = {};
1347
+ const captureDirectory = async () => (directoryCapture = await captureDirectoryCandidate({
1348
+ manager: wsm,
1349
+ envelope,
1350
+ artifactRoot: join(paths.attemptsDir, attemptId),
1351
+ sourceRoot: contract.repo.root,
1352
+ observedPaths: [...observedPaths],
1353
+ }));
1361
1354
  const rawContextPacket = await rawContextForEnvelope(routed.implementationTransport, envelope);
1362
1355
  const sessionFields = runInput
1363
1356
  ? await this.sessionSpecFields(runInput, adapter.id, knobs.model, log, routed.authRouteEstimate, routed.quotaAdmission)
1364
1357
  : undefined;
1365
- // Continuity (INV-137): once the lane (harness + resolved profile) is known,
1366
- // build the continuation packet, materialize context/THREAD.md, and point
1367
- // the prompt at it — never embed the packet body in the prompt. Replaces the
1368
- // old static session.rebound "not_portable" phrase with a real disclosure.
1358
+ const processingAdmission = processingAdmissionForLease(ledger, processingLease.id, adapter.id, attemptId, processingLease.onDenied);
1359
+ const capturedProcessing = {
1360
+ processing_preference: contract.processing_preference,
1361
+ processing: knobs.processing?.receipt,
1362
+ processing_cost_basis: knobs.processing?.costBasis,
1363
+ processing_allow_paid: knobs.processingAllowPaid,
1364
+ };
1369
1365
  const laneContinuity = runInput
1370
- ? await this.resolveContinuity(runInput, adapter.id, sessionFields?.credential_profile?.profile_id ?? runInput.credentialProfileId ?? null, inPlaceEnvelope && !!sessionFields?.resume_session_id, sessionFields, store, paths, envelope.repo_root, log)
1366
+ ? await this.resolveContinuity(runInput, adapter.id, sessionFields?.credential_profile?.profile_id ?? runInput.credentialProfileId ?? null, inPlaceEnvelope && !!sessionFields?.resume_session_id, sessionFields, store, paths, envelope.repo_root, log, capturedProcessing, processingAdmission, () => ledger.markPhysicalDispatchStarted(processingLease.id))
1371
1367
  : null;
1372
1368
  const artifactRelativeDir = routed.browserRequirement.effective
1373
1369
  ? (wsm.ensureArtifactDirectory(envelope), wsm.ownedArtifactRelativeDirectory(envelope))
@@ -1392,7 +1388,7 @@ export class Orchestrator {
1392
1388
  artifactRelativeDir === null
1393
1389
  ? ""
1394
1390
  : join(envelope.worktree_path, artifactRelativeDir, CLAUDEXOR_BROWSER_ARTIFACT_SUBDIR)),
1395
- extra_mcp_servers: this.delegationBeltFor(runInput, intent, routed, contract.budget.paid_budget),
1391
+ extra_mcp_servers: delegationBeltFor(runInput, intent, routed, contract.budget.paid_budget),
1396
1392
  cwd: envelope.worktree_path,
1397
1393
  access: routed.adapterAccess,
1398
1394
  ...this.harnessSpecKnobs(contract, knobs, intent),
@@ -1406,18 +1402,22 @@ export class Orchestrator {
1406
1402
  ...(inPlaceEnvelope && sessionFields?.resume_session_id
1407
1403
  ? { resume_session_id: sessionFields.resume_session_id }
1408
1404
  : {}),
1409
- // Scoped harness home for isolated envelopes AND for every delegated run;
1410
- // an ordinary in-place run keeps the native environment so the resumed
1411
- // vendor session is reachable. See scopedHarnessHome for the cost a
1412
- // delegated in-place attempt pays for that scoped state.
1413
1405
  ...(harnessHome.env ? { env: harnessHome.env } : {}),
1414
1406
  raw_context_packet: rawContextPacket,
1415
1407
  stream_deltas: streamDeltas,
1416
1408
  });
1409
+ bindProcessingAdmission(spec, ledger, processingLease.id, adapter.id, attemptId, processingLease.onDenied, processingAdmission);
1410
+ const billingInput = runInput ?? { repoRoot: contract.repo.root };
1411
+ // Keep billing route-bound until admission observes the actual prepared
1412
+ // profile. The resolver is evaluated per physical dispatch; preparedCost
1413
+ // gives an actual profile precedence over this pool fallback.
1414
+ spec.extra["routeBillingKnowledge"] = (actual) => actual.credential_profile?.credential_kind === "api_key"
1415
+ ? "metered"
1416
+ : actual.credential_profile
1417
+ ? "subscription_entitlement"
1418
+ : this.routeBillingKnowledge(billingInput, adapter.id);
1417
1419
  if (interaction)
1418
1420
  spec.extra["interactionChannel"] = interaction;
1419
- // D-16: compile the WorkReport envelope onto the spec (overriding the plain
1420
- // caller-schema transport) and keep the mode for the answer unwrap.
1421
1421
  const workEnvelope = this.workReportEnvelopeFor(routed, contract, Boolean(interaction));
1422
1422
  const workReportMode = this.applyWorkEnvelope(spec, workEnvelope);
1423
1423
  const inactivityMs = harnessInactivityTimeoutMs(this.config(contract.repo.root));
@@ -1434,6 +1434,7 @@ export class Orchestrator {
1434
1434
  let cost = 0;
1435
1435
  let costEstimated = false;
1436
1436
  let harnessErrored = false;
1437
+ let processingRefusal = null;
1437
1438
  let poolExhausted = null; // A5: typed pool-exhausted refusal
1438
1439
  const deltaFlood = { count: 0, disclosed: false }; // W-C4 per-attempt delta budget
1439
1440
  // QA-024: emit the belt-failure disclosure event at most once per attempt.
@@ -1509,6 +1510,8 @@ export class Orchestrator {
1509
1510
  if (ev.type === "patch_produced")
1510
1511
  continue;
1511
1512
  const safeEv = redactHarnessEvent(ev);
1513
+ if (directory)
1514
+ observeDirectoryPaths(observedPaths, safeEv, envelope.worktree_path);
1512
1515
  if (dropDeltaPastBudget(safeEv, deltaFlood, Orchestrator.MAX_DELTAS_PER_ATTEMPT, (t, p) => log?.emit(t, p), adapter.id, attemptId))
1513
1516
  continue;
1514
1517
  safeInvoke(onHarnessEvent, safeEv);
@@ -1586,6 +1589,10 @@ export class Orchestrator {
1586
1589
  // gate and required-actions read a typed category, not a bare boolean.
1587
1590
  harnessErrored = true;
1588
1591
  errors.push(safeErrorMessage(err));
1592
+ if (err instanceof ProcessingBudgetAdmissionError) {
1593
+ processingRefusal = err;
1594
+ break;
1595
+ }
1589
1596
  telemetry.transientFailures.push(classifyAdapterThrow({ errorName: err instanceof Error ? err.name : null }));
1590
1597
  }
1591
1598
  finally {
@@ -1596,8 +1603,13 @@ export class Orchestrator {
1596
1603
  // #31: the centralized retry gate reads the classified `retryable` verdict.
1597
1604
  const sawRetryable = newTransients.some((f) => f.retryable);
1598
1605
  const sawTypedLimit = telemetry.rateLimits.length > rateLimitStart;
1599
- const currentDiff = await wsm.diff(envelope);
1600
- const deliverableEmpty = currentDiff.trim().length === 0 && answer.text().length === 0;
1606
+ if (directory)
1607
+ await captureDirectory();
1608
+ const currentDiff = directory ? "" : await wsm.diff(envelope);
1609
+ const workspaceUnchanged = directory
1610
+ ? directoryCapture.files?.noChanges === true
1611
+ : currentDiff.trim().length === 0;
1612
+ const deliverableEmpty = workspaceUnchanged && answer.text().length === 0;
1601
1613
  // W5.4 + A2 failover: a typed-limit hit OR a structural pre-progress
1602
1614
  // death rebuilds the spec on a NEW session under the next profile.
1603
1615
  if (harnessErrored && runInput && !signal?.aborted) {
@@ -1618,9 +1630,8 @@ export class Orchestrator {
1618
1630
  // Rotation evidence reads the POLICY-accepted try output: refusal
1619
1631
  // prose arriving as mid-stream MESSAGE events (claude org-disabled)
1620
1632
  // is no deliverable; the transient gate keeps RAW deliverableEmpty.
1621
- deliverableEmpty: currentDiff.trim().length === 0 &&
1622
- acceptedTryOutput(answer, harnessErrored).length === 0,
1623
- workspaceDiffNonEmpty: currentDiff.trim().length > 0,
1633
+ deliverableEmpty: workspaceUnchanged && acceptedTryOutput(answer, harnessErrored).length === 0,
1634
+ workspaceDiffNonEmpty: !workspaceUnchanged,
1624
1635
  lastLimit: telemetry.rateLimits.at(-1) ?? null,
1625
1636
  emit: (type, payload) => log?.emit(type, payload),
1626
1637
  newSessionId: () => newId("ses"),
@@ -1663,7 +1674,7 @@ export class Orchestrator {
1663
1674
  }
1664
1675
  // A pool-exhausted terminal is rotation's verdict, not the transient
1665
1676
  // machinery's — no `route.transient.exhausted` rides along with it.
1666
- if (harnessErrored && !poolExhausted) {
1677
+ if (harnessErrored && !poolExhausted && !processingRefusal) {
1667
1678
  emitTransientExhausted((t, p) => log?.emit(t, p), adapter.id, attemptId, telemetry, retryPolicy.maxRetries);
1668
1679
  }
1669
1680
  const attemptStreamEndedMs = Date.now();
@@ -1675,10 +1686,14 @@ export class Orchestrator {
1675
1686
  const unwrapped = unwrapWorkReportEnvelope(acceptedTryOutput(answer, harnessErrored), workReportMode, { sideToolReport: telemetry.sideToolWorkReport ?? undefined });
1676
1687
  const redacted = redactSecrets(unwrapped.deliverable);
1677
1688
  const candidateAnswer = redacted.trim().length > 0 ? redacted : undefined;
1678
- const { diff, refusal: secretDiffRefusal } = await secretDiff.quarantineCandidateWorkspace(wsm, envelope, inPlaceEnvelope, candidateAnswer);
1689
+ if (directory)
1690
+ await captureDirectory();
1691
+ const { diff, refusal: secretDiffRefusal } = directory
1692
+ ? { diff: "", refusal: directoryCapture.refusal }
1693
+ : await secretDiff.quarantineCandidateWorkspace(wsm, envelope, inPlaceEnvelope, candidateAnswer);
1679
1694
  harnessErrored = secretDiff.recordSecretDiffRefusal(secretDiffRefusal, errors, harnessErrored);
1680
1695
  const answerText = secretDiffRefusal ? undefined : candidateAnswer;
1681
- const deliverableEvidence = diff.trim().length > 0 || Boolean(answerText);
1696
+ const deliverableEvidence = diff.trim().length > 0 || Boolean(answerText) || directoryHasOutput(directoryCapture.files);
1682
1697
  // Cancelled attempts skip gates: running a 600s-per-gate suite delays the ack
1683
1698
  // and burns compute on a result nobody will adopt. Diff/attempt.yaml
1684
1699
  // still land, so partial work stays inspectable.
@@ -1757,8 +1772,8 @@ export class Orchestrator {
1757
1772
  worktreePath: envelope.worktree_path,
1758
1773
  artifactRelativeDir,
1759
1774
  diff,
1760
- persistPatch: secretDiffRefusal === undefined && isMutatingAccess(access),
1761
- persistProducedMedia: secretDiffRefusal === undefined,
1775
+ persistPatch: !directory && secretDiffRefusal === undefined && isMutatingAccess(access),
1776
+ persistProducedMedia: !directory && secretDiffRefusal === undefined,
1762
1777
  answerText,
1763
1778
  record: {
1764
1779
  attempt_id: attemptId,
@@ -1773,6 +1788,15 @@ export class Orchestrator {
1773
1788
  ...(secretDiffRefusal ? { secret_diff_refusal: secretDiffRefusal } : {}),
1774
1789
  gates: gates.map((g) => ({ id: g.id, status: g.status })),
1775
1790
  branch: envelope.branch_name,
1791
+ ...(directoryCapture.files
1792
+ ? {
1793
+ files_manifest: directoryCapture.files.manifestPath,
1794
+ manifest_sha256: directoryCapture.files.manifestSha256,
1795
+ source_root: contract.repo.root,
1796
+ execution_root: envelope.worktree_path,
1797
+ no_changes: directoryCapture.files.noChanges,
1798
+ }
1799
+ : {}),
1776
1800
  // Applied facts, not promises: historical proofs stay readable and
1777
1801
  // current delegated runs state deliberate outer-boundary absence.
1778
1802
  // Built by the SAME shape the failure path writes.
@@ -1788,6 +1812,7 @@ export class Orchestrator {
1788
1812
  harnessId: adapter.id,
1789
1813
  label,
1790
1814
  diff,
1815
+ ...(directoryCapture.files ? { files: directoryCapture.files } : {}),
1791
1816
  answerText,
1792
1817
  reviewCwd: envelope.worktree_path,
1793
1818
  baseSha: envelope.base_sha ?? undefined,
@@ -1800,7 +1825,9 @@ export class Orchestrator {
1800
1825
  telemetry,
1801
1826
  ...(secretDiffRefusal ? { secretDiffRefusal } : {}),
1802
1827
  // A5: the typed refusal survives NORMAL attempt finalization (no throw).
1803
- ...(poolExhausted ? { declaredFailure: declaredFailure(poolExhausted) } : {}),
1828
+ ...(poolExhausted || processingRefusal
1829
+ ? { declaredFailure: declaredFailure(processingRefusal ?? poolExhausted) }
1830
+ : {}),
1804
1831
  outcomeClass: finalized.outcomeClass,
1805
1832
  applied,
1806
1833
  };
@@ -1812,41 +1839,6 @@ export class Orchestrator {
1812
1839
  // Thin delegate — the channel mechanics live in interaction.ts.
1813
1840
  return interactionChannelFor(input, log, runId, taskId, attemptId, harnessId, supportsInteractive, DEFAULT_INTERACTION_TIMEOUT_MS);
1814
1841
  }
1815
- /**
1816
- * D-14 layer 3 (AGENTS.md unification, INV-113): the ONE new live-tree write.
1817
- * When the PROJECT root has `AGENTS.md` and no `CLAUDE.md`, drop a thin
1818
- * `CLAUDE.md` (`@AGENTS.md` import + Claudexor ownership marker) so a Claude
1819
- * Code route reads the same instruction file codex/cursor read natively.
1820
- *
1821
- * The project-root bridge has its own narrower fence: read-only modes never
1822
- * reach this run-prep stage and `--in-place` stateful targets are left
1823
- * untouched. Git admission is independently owned by `runStartRequiresGit`.
1824
- * The write targets the PROJECT root (`repoRoot`), never a worktree envelope.
1825
- * The workspace helper adds exclusive-create + no-follow +
1826
- * idempotency, so a hand-written or symlinked `CLAUDE.md` is never overwritten
1827
- * and a concurrent/second prep is a no-op. Announced via a typed
1828
- * `project.claude_bridge.created` event on an actual create only — the git-init
1829
- * pattern. A bridge is a convenience, not a precondition: any failure is
1830
- * swallowed so it can never fail an otherwise-valid write run.
1831
- */
1832
- ensureClaudeBridgeForRun(repoRoot, inPlace, log) {
1833
- if (repoRoot === NO_PROJECT_ROOT || inPlace)
1834
- return;
1835
- let result;
1836
- try {
1837
- result = ensureClaudeBridge(repoRoot);
1838
- }
1839
- catch {
1840
- return;
1841
- }
1842
- if (result.created) {
1843
- log.emit("project.claude_bridge.created", {
1844
- project_root: repoRoot,
1845
- path: "CLAUDE.md",
1846
- source: "AGENTS.md",
1847
- });
1848
- }
1849
- }
1850
1842
  async runRace(input, mode, announce) {
1851
1843
  const taskId = input.taskId ?? newId("task");
1852
1844
  const runId = input.runId ?? newId("run");
@@ -1885,7 +1877,7 @@ export class Orchestrator {
1885
1877
  // directory, a filesystem root, or one that cannot be classified), which
1886
1878
  // gets a typed refusal BEFORE any mutation (INV-075). For an isolated
1887
1879
  // thread the execution root is already a git worktree: a no-op there.
1888
- if (mutatingRun) {
1880
+ if (mutatingRun && input.workspaceKind !== "directory") {
1889
1881
  const gitPreconditionError = await ensureWriteModeGitBoundary(execRoot, log, store, paths, runId, mode);
1890
1882
  if (gitPreconditionError) {
1891
1883
  return {
@@ -1903,14 +1895,14 @@ export class Orchestrator {
1903
1895
  }
1904
1896
  // Same run-prep stage as the git boundary: if the PROJECT root uses AGENTS.md
1905
1897
  // with no CLAUDE.md, bridge it so a Claude Code candidate reads it (INV-113).
1906
- if (mutatingRun) {
1907
- this.ensureClaudeBridgeForRun(input.repoRoot, input.inPlace === true, log);
1898
+ if (mutatingRun && input.workspaceKind !== "directory") {
1899
+ ensureClaudeBridgeForRun(input.repoRoot, input.inPlace === true, log);
1908
1900
  }
1909
1901
  // Pre-turn snapshot of the live tree for in-place runs: the revert restore
1910
1902
  // target (server-owned revertInPlace). A snapshot failure must never fail the
1911
1903
  // run — revert is simply unavailable then.
1912
1904
  let preTurnSha = null;
1913
- if (mutatingRun && input.inPlace === true) {
1905
+ if (mutatingRun && input.inPlace === true && input.workspaceKind !== "directory") {
1914
1906
  try {
1915
1907
  preTurnSha = await snapshotTree(execRoot);
1916
1908
  }
@@ -1983,7 +1975,7 @@ export class Orchestrator {
1983
1975
  attemptId,
1984
1976
  intent: this.candidateIntent(input),
1985
1977
  harnessId: routed.adapter.id,
1986
- cost: attemptCostEvidence(routed.adapter.id, attemptId, this.reservationEstimateUsd(input, i > 0), this.routeBillingKnowledge(input, routed.adapter.id)),
1978
+ cost: attemptCostEvidence(routed.adapter.id, attemptId, this.reservationEstimateUsd(input, i > 0), this.routeBillingKnowledge(input, routed.adapter.id), processingCostEvidence(routed.processing, this.routeBillingKnowledge(input, routed.adapter.id), [`harness:${routed.adapter.id}`])),
1987
1979
  });
1988
1980
  log.emit("budget.lease.created", {
1989
1981
  granted: lease.granted,
@@ -2079,6 +2071,8 @@ export class Orchestrator {
2079
2071
  baseRef: contract.repo.base_ref,
2080
2072
  dirtyPolicy: "snapshot",
2081
2073
  accessProfile: candidateAccess,
2074
+ workspaceKind: input.workspaceKind,
2075
+ scopePaths: input.scopePaths,
2082
2076
  // Direct-workspace singletons run in place. Races and patch-envelope
2083
2077
  // transports stay isolated and adopt through the delivery service.
2084
2078
  inPlace: !mutatingRun ||
@@ -2087,7 +2081,13 @@ export class Orchestrator {
2087
2081
  slot.routed.implementationTransport !== "git_patch_envelope"),
2088
2082
  });
2089
2083
  harnessHome = this.harnessHomeFor(wsm, envelope, slot.routed, input);
2090
- const run = await this.runCandidateInEnvelope(slot.routed, envelope, slot.attemptId, slot.label, contract, input.prompt, store, paths, wsm, ledger, candidateAccess, (ev) => {
2084
+ const run = await this.runCandidateInEnvelope(slot.routed, envelope, slot.attemptId, slot.label, contract, input.prompt, store, paths, wsm, ledger, {
2085
+ id: slot.leaseId,
2086
+ onDenied: (denial) => {
2087
+ budgetStopped = true;
2088
+ budgetDenial ??= denial;
2089
+ },
2090
+ }, candidateAccess, (ev) => {
2091
2091
  const safeEv = redactHarnessEvent(ev);
2092
2092
  safeInvoke(input.onHarnessEvent, safeEv);
2093
2093
  log.emit("harness.event", harnessEventPayload(adapter.id, slot.attemptId, safeEv));
@@ -2145,7 +2145,7 @@ export class Orchestrator {
2145
2145
  attemptId: contAttemptId,
2146
2146
  intent: this.candidateIntent(input),
2147
2147
  harnessId: adapter.id,
2148
- cost: attemptCostEvidence(adapter.id, contAttemptId, this.estimateUsdFloor(input.repoRoot), this.routeBillingKnowledge(input, adapter.id)),
2148
+ cost: attemptCostEvidence(adapter.id, contAttemptId, this.estimateUsdFloor(input.repoRoot), this.routeBillingKnowledge(input, adapter.id), processingCostEvidence(slot.routed.processing, this.routeBillingKnowledge(input, slot.routed.adapter.id), [`harness:${adapter.id}`])),
2149
2149
  });
2150
2150
  if (contLease.granted) {
2151
2151
  candidateContinuationCount += 1; // claim the one-shot only once it launches
@@ -2164,7 +2164,13 @@ export class Orchestrator {
2164
2164
  });
2165
2165
  const contLeaseId = contLease.lease?.lease_id ?? "";
2166
2166
  try {
2167
- const contRun = await this.runCandidateInEnvelope(slot.routed, envelope, contAttemptId, slot.label, contract, input.prompt, store, paths, wsm, ledger, candidateAccess, (ev) => {
2167
+ const contRun = await this.runCandidateInEnvelope(slot.routed, envelope, contAttemptId, slot.label, contract, input.prompt, store, paths, wsm, ledger, {
2168
+ id: contLeaseId,
2169
+ onDenied: (denial) => {
2170
+ budgetStopped = true;
2171
+ budgetDenial ??= denial;
2172
+ },
2173
+ }, candidateAccess, (ev) => {
2168
2174
  const safeEv = redactHarnessEvent(ev);
2169
2175
  safeInvoke(input.onHarnessEvent, safeEv);
2170
2176
  log.emit("harness.event", harnessEventPayload(adapter.id, contAttemptId, safeEv));
@@ -2263,18 +2269,18 @@ export class Orchestrator {
2263
2269
  // Fail-closed terminal: a delegated mutating run whose attempts state
2264
2270
  // neither historical proof nor deliberate absence refuses instead of passing.
2265
2271
  assertDelegatedEvidence(input.delegated === true, candidateAccess, runs);
2266
- const cancelledCandidates = () => runs.map((r) => ({
2267
- attemptId: r.attemptId,
2268
- harnessId: r.harnessId,
2269
- // gatesPassed([]) is vacuously true: a successful zero-gate run is a
2270
- // legitimate green STATUS (never a "gates passed" claim — the label
2271
- // surfaces render that honestly as n/a).
2272
- status: gatesPassed(r.gates) && !r.errored ? "green" : "red",
2273
- }));
2274
- /** The one cancellation terminal this race can reach, from any of its three
2275
- * abort checks. Every argument is re-read at call time, exactly as it was
2276
- * when each check spelled the whole call out for itself. */
2277
- const cancelledRaceResult = () => cancelledResult(log, runId, taskId, mode, paths.root, cancelledCandidates(), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null), ledger.spend(), input.signal, store);
2272
+ const cancelledRaceResult = () => cancelledCandidatesResult({
2273
+ store,
2274
+ paths,
2275
+ log,
2276
+ runId,
2277
+ taskId,
2278
+ mode,
2279
+ ledger,
2280
+ runs,
2281
+ signal: input.signal,
2282
+ writeTelemetry: () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null),
2283
+ });
2278
2284
  // Revert divergence fence for the single-candidate in-place path: the
2279
2285
  // candidate mutated the LIVE tree during execution above, so the post-turn
2280
2286
  // snapshot must be taken NOW — before review/synthesis/arbitration, which can
@@ -2283,6 +2289,7 @@ export class Orchestrator {
2283
2289
  // into the revert target and let a later revert clobber them.
2284
2290
  let earlyPostTurnSha = null;
2285
2291
  if (mutatingRun &&
2292
+ input.workspaceKind !== "directory" &&
2286
2293
  input.inPlace &&
2287
2294
  requestedSingleCandidate &&
2288
2295
  runs.every((run) => !run.secretDiffRefusal)) {
@@ -2301,7 +2308,19 @@ export class Orchestrator {
2301
2308
  if (failedDelegation) {
2302
2309
  const failure = delegateFailure.candidateFailureTerminal(failedDelegation, "race");
2303
2310
  await disposeReviewEnvelopes();
2304
- if (mutatingRun) {
2311
+ if (mutatingRun && failedDelegation.files) {
2312
+ await publishDirectoryCandidate({
2313
+ files: failedDelegation.files,
2314
+ store,
2315
+ paths,
2316
+ taskId,
2317
+ attemptId: failedDelegation.attemptId,
2318
+ harnessId: failedDelegation.harnessId,
2319
+ facts: makeOutcomeFacts("failed", { noChanges: failedDelegation.files.noChanges }),
2320
+ log,
2321
+ });
2322
+ }
2323
+ else if (mutatingRun && input.workspaceKind !== "directory") {
2305
2324
  await delegateFailure.persistFailedInPlaceWorkProduct({
2306
2325
  ...{ store, log, paths, execRoot, preTurnSha, taskId, mode },
2307
2326
  live: input.inPlace === true && failedDelegation.reviewCwd === execRoot,
@@ -2313,63 +2332,18 @@ export class Orchestrator {
2313
2332
  this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null);
2314
2333
  return failTerminally(log, store, paths, runId, taskId, mode, failure.phase, failure.error, ledger.spend(), failure.metadata);
2315
2334
  }
2316
- if (runs.length === 0) {
2317
- const budgetReason = ledger.terminal();
2318
- // QA-050: when the zero-candidate cause is a budget refusal, the shared
2319
- // classifier owns the typed code, the refused route/slot, and actionable
2320
- // budget remediation (previously an empty nextActions array).
2321
- const agentBudgetMapping = budgetStopped || budgetReason
2322
- ? classifyBudgetFailure({ denial: budgetDenial, terminal: budgetReason })
2323
- : null;
2324
- const facts = makeOutcomeFacts("failed", {
2325
- reason: agentBudgetMapping?.reason ?? (budgetStopped ? "budget_exhausted" : "harness_failed"),
2326
- noChanges: true,
2327
- });
2328
- const why = agentBudgetMapping?.safeMessage ?? "no candidates produced";
2329
- store.writeYaml(join(paths.arbitrationDir, "decision.yaml"), {
2330
- winner: null,
2331
- facts,
2332
- why_winner: why,
2333
- evidence_facts: ["no candidates were produced"],
2334
- apply_recommendation: "continue",
2335
- budget_summary: decisionBudgetSummary(ledger),
2336
- });
2337
- 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`);
2338
- if (agentBudgetMapping) {
2339
- writeFailure(store, paths, budgetFailureRecord(agentBudgetMapping, { runDir: paths.root }));
2340
- }
2341
- else {
2342
- writeFailure(store, paths, {
2343
- phase: "executor",
2344
- category: "internal",
2345
- safeMessage: why,
2346
- runDir: paths.root,
2347
- nextActions: ["Open diagnostics", "Retry the run"],
2348
- });
2349
- }
2350
- log.emit("output.ready", { kind: "summary", path: "final/summary.md", state: "diagnostic" });
2351
- log.emit("run.failed", {
2352
- lifecycle: facts.lifecycle,
2353
- facts,
2354
- reason: facts.reason,
2355
- phase: agentBudgetMapping ? "budget" : "executor",
2356
- ...(agentBudgetMapping?.harnessId ? { harness_id: agentBudgetMapping.harnessId } : {}),
2357
- error: why,
2358
- failure_ref: "final/failure.yaml",
2359
- });
2360
- return {
2335
+ if (runs.length === 0)
2336
+ return emptyCandidateResult({
2337
+ ledger,
2338
+ budgetStopped,
2339
+ budgetDenial,
2340
+ mode,
2341
+ store,
2342
+ paths,
2343
+ log,
2361
2344
  runId,
2362
2345
  taskId,
2363
- mode,
2364
- lifecycle: facts.lifecycle,
2365
- facts,
2366
- winner: null,
2367
- runDir: paths.root,
2368
- summary: why,
2369
- candidates: [],
2370
- spendUsd: ledger.spend(),
2371
- };
2372
- }
2346
+ });
2373
2347
  // Reviewers, synthesis, and arbitration only ever see candidates WITH
2374
2348
  // work (a real diff or a completed stream). Attempts that died before
2375
2349
  // producing anything are corpses: reviewing "(empty diff)" spends real
@@ -2378,72 +2352,18 @@ export class Orchestrator {
2378
2352
  const workingRuns = partitionCandidates(runs).working;
2379
2353
  if (workingRuns.length === 0) {
2380
2354
  await disposeReviewEnvelopes();
2381
- const first = runs[0];
2382
- const phase = first.secretDiffRefusal ? "artifact_security" : (first.infraPhase ?? "harness");
2383
- const { facts, why: rootCause } = partitionCandidates(runs);
2384
- store.writeYaml(join(paths.arbitrationDir, "decision.yaml"), {
2385
- winner: null,
2386
- facts,
2387
- why_winner: rootCause,
2388
- evidence_facts: runs.map((r) => `${r.attemptId} produced no work: ${r.errors[0] ?? "unknown"}`),
2389
- apply_recommendation: "continue",
2390
- budget_summary: decisionBudgetSummary(ledger),
2391
- });
2392
- this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null);
2393
- store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${mode})\n\n- Lifecycle: ${facts.lifecycle}\n- Phase: ${phase}\n\n${rootCause}\n`);
2394
- const existingEventRefs = runs
2395
- .map((r) => `attempts/${r.attemptId}/events.jsonl`)
2396
- .filter((rel) => existsSync(join(paths.root, rel)));
2397
- // #31: auth guidance only on a classified auth failure; every other
2398
- // harness cause (timeout, rate limit, crash, config) gets remediation that
2399
- // fits it, instead of a doomed "Check harness authentication".
2400
- const harnessCategory = dominantHarnessFailureCategory(first.telemetry.transientFailures);
2401
- // A run speaks with a candidate's TYPED refusal only when EVERY candidate
2402
- // died of the same one (candidateEvidence owns that rule); mixed causes
2403
- // keep the honest harness terminal.
2404
- const unanimous = unanimousDeclaredFailure(runs);
2405
- writeFailure(store, paths, {
2406
- phase,
2407
- category: unanimous?.category ?? (phase === "workspace" ? "project" : "harness_error"),
2408
- code: unanimous?.code ?? null,
2409
- harnessId: first.harnessId,
2410
- attemptId: first.attemptId,
2411
- safeMessage: rootCause,
2412
- rawDetailRef: `attempts/${first.attemptId}/attempt.yaml`,
2413
- eventRefs: existingEventRefs,
2414
- runDir: paths.root,
2415
- resetsAt: unanimous?.resetsAt ?? null,
2416
- nextActions: first.secretDiffRefusal
2417
- ? secretDiff.secretDiffNextActions(first.secretDiffRefusal)
2418
- : phase === "workspace"
2419
- ? ["Check the project folder", "Open diagnostics", "Retry the run"]
2420
- : harnessFailureNextActions(harnessCategory),
2421
- });
2422
- log.emit("output.ready", { kind: "summary", path: "final/summary.md", state: "diagnostic" });
2423
- log.emit("run.failed", {
2424
- lifecycle: facts.lifecycle,
2425
- facts,
2426
- reason: facts.reason,
2427
- phase,
2428
- error: rootCause,
2429
- failure_ref: "final/failure.yaml",
2430
- });
2431
- return {
2355
+ return failedCandidatesResult({
2356
+ store,
2357
+ paths,
2358
+ log,
2432
2359
  runId,
2433
2360
  taskId,
2434
2361
  mode,
2435
- lifecycle: facts.lifecycle,
2436
- facts,
2437
- winner: null,
2438
- runDir: paths.root,
2439
- summary: rootCause,
2440
- candidates: runs.map((r) => ({
2441
- attemptId: r.attemptId,
2442
- harnessId: r.harnessId,
2443
- status: "red",
2444
- })),
2445
- spendUsd: ledger.spend(),
2446
- };
2362
+ ledger,
2363
+ runs,
2364
+ budgetDenial,
2365
+ writeTelemetry: () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null),
2366
+ });
2447
2367
  }
2448
2368
  // QA-025: only announce that review STARTED when the panel will actually
2449
2369
  // run. A candidate that changed no files is skipped inside reviewRuns; a
@@ -2452,7 +2372,7 @@ export class Orchestrator {
2452
2372
  // real verification). Compute the reviewable set first and emit a typed
2453
2373
  // `review.skipped` when nothing is reviewable, so every start has a matching
2454
2374
  // terminal and the no-diff path records `not_run` consistently.
2455
- const reviewableRuns = workingRuns.filter((r) => r.diff.trim().length > 0);
2375
+ const reviewableRuns = workingRuns.filter((r) => r.files ? r.files.noChanges !== true : r.diff.trim().length > 0);
2456
2376
  const configuredFamilies = new Set(reviewers.map((r) => r.providerFamily)).size;
2457
2377
  if (reviewableRuns.length === 0 || reviewers.length === 0) {
2458
2378
  log.emit("review.skipped", {
@@ -2511,7 +2431,7 @@ export class Orchestrator {
2511
2431
  attemptId: "synth",
2512
2432
  intent: "synthesize",
2513
2433
  harnessId: synthRouted.adapter.id,
2514
- cost: attemptCostEvidence(synthRouted.adapter.id, "synth", this.reservationEstimateUsd(input), this.routeBillingKnowledge(input, synthRouted.adapter.id)),
2434
+ cost: attemptCostEvidence(synthRouted.adapter.id, "synth", this.reservationEstimateUsd(input), this.routeBillingKnowledge(input, synthRouted.adapter.id), processingCostEvidence(synthRouted.processing, this.routeBillingKnowledge(input, synthRouted.adapter.id), [`harness:${synthRouted.adapter.id}`])),
2515
2435
  });
2516
2436
  if (lease.granted) {
2517
2437
  let envelope;
@@ -2533,9 +2453,17 @@ export class Orchestrator {
2533
2453
  baseRef: contract.repo.base_ref,
2534
2454
  dirtyPolicy: "snapshot",
2535
2455
  accessProfile: candidateAccess,
2456
+ workspaceKind: input.workspaceKind,
2457
+ scopePaths: input.scopePaths,
2536
2458
  });
2537
2459
  const synthHome = this.harnessHomeFor(wsm, envelope, synthRouted, input);
2538
- const run = await this.runCandidateInEnvelope(synthRouted, envelope, "synth", "Synthesis", contract, synthesisInput.prompt, store, paths, wsm, ledger, candidateAccess, (ev) => {
2460
+ const run = await this.runCandidateInEnvelope(synthRouted, envelope, "synth", "Synthesis", contract, synthesisInput.prompt, store, paths, wsm, ledger, {
2461
+ id: lease.lease.lease_id,
2462
+ onDenied: (denial) => {
2463
+ budgetStopped = true;
2464
+ budgetDenial ??= denial;
2465
+ },
2466
+ }, candidateAccess, (ev) => {
2539
2467
  const safeEv = redactHarnessEvent(ev);
2540
2468
  safeInvoke(input.onHarnessEvent, safeEv);
2541
2469
  log.emit("harness.event", harnessEventPayload(synthAdapter.id, "synth", safeEv));
@@ -2553,7 +2481,7 @@ export class Orchestrator {
2553
2481
  workingRuns.push(run);
2554
2482
  }
2555
2483
  if (input.signal?.aborted) {
2556
- 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);
2484
+ return cancelledResult(log, runId, taskId, mode, paths.root, candidateStatuses(runs), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null), ledger.spend(), input.signal, store);
2557
2485
  }
2558
2486
  }
2559
2487
  finally {
@@ -2613,6 +2541,12 @@ export class Orchestrator {
2613
2541
  ? (evidences.find((e) => e.attemptId === winnerRun.attemptId)?.reviewVerified ?? false)
2614
2542
  : evidences.length > 0 && evidences.every((e) => e.reviewVerified);
2615
2543
  let facts = result.decision.facts;
2544
+ if (winnerRun?.files)
2545
+ facts = {
2546
+ ...facts,
2547
+ noChanges: winnerRun.files.noChanges,
2548
+ reason: facts.reason === "no_changes" && winnerRun.files.noChanges !== true ? null : facts.reason,
2549
+ };
2616
2550
  // A reviewer NEEDS_HUMAN escalation forces the REVIEW axis to blocked (a
2617
2551
  // needs-decision terminal), unless the decision is already applyable-clean.
2618
2552
  if (needsHuman && facts.lifecycle === "succeeded" && facts.review !== "blocked") {
@@ -2633,27 +2567,24 @@ export class Orchestrator {
2633
2567
  let finalVerifyFailed = false;
2634
2568
  let deliveryFailureReason = null;
2635
2569
  let raceDeliveryReceipt = null;
2636
- // A single in-place turn already mutated its execution tree; race adoption
2637
- // instead defers verification until immediately before delivery.
2570
+ let directoryDeliveryReceipt = false;
2638
2571
  const inPlaceWinner = winnerRun?.reviewCwd === execRoot;
2639
2572
  const deferredRaceVerify = input.inPlace === true && !inPlaceWinner;
2640
2573
  if (mutatingRun &&
2641
2574
  winnerRun &&
2642
2575
  !inPlaceWinner &&
2643
2576
  !deferredRaceVerify &&
2644
- winnerRun.diff.trim().length > 0 &&
2577
+ (winnerRun.files ? winnerRun.files.noChanges !== true : winnerRun.diff.trim().length > 0) &&
2645
2578
  facts.lifecycle === "succeeded" &&
2646
2579
  facts.review !== "blocked" &&
2647
2580
  !input.signal?.aborted) {
2648
- finalVerify = await finalVerifyPatch(execRoot, winnerRun, gateSpecsFromContract(contract), log);
2649
- // Verify errors block like proven failures; accept_risk stays available.
2650
- // A failed fresh verify lands on the CHECKS axis (a needs-decision block).
2581
+ finalVerify = winnerRun.files
2582
+ ? await finalVerifyFiles(winnerRun.files, undefined, gateSpecsFromContract(contract), log)
2583
+ : await finalVerifyPatch(execRoot, winnerRun, gateSpecsFromContract(contract), log);
2651
2584
  finalVerifyFailed = finalVerifyBlocks(finalVerify);
2652
2585
  if (finalVerifyFailed)
2653
2586
  facts = { ...facts, checks: "failed", reason: "checks_failed" };
2654
2587
  }
2655
- // A needs-decision terminal (review blocked or checks failed) overrides the
2656
- // persisted green arbitration fields; otherwise the facts pass through.
2657
2588
  const needsDec = facts.review === "blocked" || facts.checks === "failed";
2658
2589
  store.writeYaml(join(paths.arbitrationDir, "decision.yaml"), {
2659
2590
  ...result.decision,
@@ -2674,17 +2605,9 @@ export class Orchestrator {
2674
2605
  log.emit("output.ready", { kind: "artifact", path });
2675
2606
  }
2676
2607
  const winnerAnswer = winnerRun.answerText ?? "";
2677
- // The winner's final MESSAGE is the human-facing answer and materializes
2678
- // for diff-ful runs too: the chat renders final/answer.md (the projection
2679
- // prefers it), never the arbitration summary — "Run … Winner: a01 …" is
2680
- // machine telemetry, not what the agent said. The diff stays in the
2681
- // Diff tab; summary.md remains a diagnostics artifact.
2682
2608
  if (winnerAnswer.length > 0) {
2683
2609
  store.writeText(join(paths.finalDir, "answer.md"), winnerAnswer + "\n");
2684
2610
  }
2685
- // The run's structured-output contract: ONE engine validator, called on
2686
- // the winner's answer regardless of diff presence (a non-conformant
2687
- // answer stays success-with-warnings; the receipt is the truth).
2688
2611
  if (contract.output_schema) {
2689
2612
  finalizeStructuredOutput({
2690
2613
  store,
@@ -2694,7 +2617,48 @@ export class Orchestrator {
2694
2617
  answerText: winnerAnswer,
2695
2618
  });
2696
2619
  }
2697
- if (mutatingRun) {
2620
+ if (mutatingRun && winnerRun.files) {
2621
+ let directoryDelivery;
2622
+ const directoryAdoptable = facts.lifecycle === "succeeded" &&
2623
+ reviewAllowsApply(facts) &&
2624
+ facts.checks !== "failed" &&
2625
+ !workStateVetoes(facts);
2626
+ if (input.inPlace === true && !inPlaceWinner && directoryAdoptable) {
2627
+ const delivered = await verifyAndDeliverFiles(execRoot, winnerRun.files, {}, gateSpecsFromContract(contract), (freshVerify) => finalVerifyBlocks(freshVerify)
2628
+ ? (freshVerify.reason ?? "final verify failed before directory race adoption")
2629
+ : null, log);
2630
+ store.writeYaml(join(paths.finalDir, "delivery_receipt.yaml"), delivered);
2631
+ directoryDeliveryReceipt = true;
2632
+ directoryDelivery = delivered;
2633
+ finalVerify = delivered.finalVerify;
2634
+ if (finalVerifyBlocks(finalVerify))
2635
+ finalVerifyFailed = true;
2636
+ if (!delivered.applied) {
2637
+ deliveryFailureReason = delivered.detail ?? "directory race adoption was refused";
2638
+ facts = { ...facts, checks: "failed", reason: "checks_failed" };
2639
+ }
2640
+ writeRaceDeliveryDecision(store, decisionPath, {
2641
+ decision: result.decision,
2642
+ facts,
2643
+ reviewVerified: actualReviewVerified,
2644
+ finalVerify,
2645
+ deliveryFailureReason,
2646
+ deliveryReceiptPath: "final/delivery_receipt.yaml",
2647
+ });
2648
+ }
2649
+ await publishDirectoryCandidate({
2650
+ files: winnerRun.files,
2651
+ store,
2652
+ paths,
2653
+ taskId,
2654
+ attemptId: winnerRun.attemptId,
2655
+ harnessId: winnerRun.harnessId,
2656
+ facts,
2657
+ log,
2658
+ delivery: directoryDelivery,
2659
+ });
2660
+ }
2661
+ else if (mutatingRun) {
2698
2662
  secretDiff.assertNoSecretLikeTokens("final patch diff", winnerRun.diff);
2699
2663
  const patchSha256 = sha256(winnerRun.diff);
2700
2664
  store.writeText(join(paths.finalDir, "patch.diff"), winnerRun.diff);
@@ -2771,7 +2735,7 @@ export class Orchestrator {
2771
2735
  reviewVerified: actualReviewVerified,
2772
2736
  finalVerify,
2773
2737
  deliveryFailureReason,
2774
- deliveryReceiptPath: raceDeliveryReceipt ? "final/delivery_receipt.yaml" : null,
2738
+ deliveryReceiptPath: raceDeliveryReceipt || directoryDeliveryReceipt ? "final/delivery_receipt.yaml" : null,
2775
2739
  });
2776
2740
  if (inPlaceWinner && requestedSingleCandidate && adopted === true) {
2777
2741
  revertAnchorId = await createRevertAnchorOrNull(execRoot, preTurnSha, postTurnSha);
@@ -2781,7 +2745,7 @@ export class Orchestrator {
2781
2745
  kind: input.create === true ? "new_repo" : "patch",
2782
2746
  source_task_id: taskId,
2783
2747
  producer_attempt_id: winnerRun.attemptId,
2784
- ...(raceDeliveryReceipt
2748
+ ...(raceDeliveryReceipt || directoryDeliveryReceipt
2785
2749
  ? { files: { delivery_receipt: "final/delivery_receipt.yaml" } }
2786
2750
  : {}),
2787
2751
  meta: {
@@ -2948,20 +2912,12 @@ export class Orchestrator {
2948
2912
  winner: result.decision.winner,
2949
2913
  runDir: paths.root,
2950
2914
  summary: result.decision.why_winner,
2951
- candidates: runs.map((r) => ({
2952
- attemptId: r.attemptId,
2953
- harnessId: r.harnessId,
2954
- // gatesPassed([]) is vacuously true: a successful zero-gate run is a
2955
- // legitimate green STATUS (never a "gates passed" claim — the label
2956
- // surfaces render that honestly as n/a).
2957
- status: gatesPassed(r.gates) && !r.errored ? "green" : "red",
2958
- })),
2915
+ candidates: candidateStatuses(runs),
2959
2916
  decisionPath,
2960
2917
  reviewVerified: actualReviewVerified,
2961
2918
  spendUsd: ledger.spend(),
2962
2919
  };
2963
2920
  }
2964
- /** Single-owner telemetry artifact (final/telemetry.yaml); surfaces project it, never recompute. */
2965
2921
  writeRunTelemetry(store, paths, contract, runId, taskId, mode, attempts, finalAttemptId, deepScanSynthesis) {
2966
2922
  // QA-034: attach the routing rationale recorded at pool ordering (if this
2967
2923
  // run computed one), then clear it — telemetry is written once at terminal.
@@ -2981,7 +2937,6 @@ export class Orchestrator {
2981
2937
  resolveAuthPreference: (harnessId) => this.authPreferenceForHarness(contract.repo.root, harnessId, contract.auth_preference),
2982
2938
  });
2983
2939
  }
2984
- /** Review a set of runs and return their evidence (with finalReviewClean + review_verified caveat). */
2985
2940
  /**
2986
2941
  * SINGLE funnel for every reviewer-panel invocation: run it inside a per-review
2987
2942
  * scoped harness HOME (Bible §6) so reviewer scratch state and injected auth
@@ -2998,7 +2953,7 @@ export class Orchestrator {
2998
2953
  transientRetryPolicy: input.transientRetryPolicy ?? transientRetryPolicy(this.config(input.cwd)),
2999
2954
  });
3000
2955
  }
3001
- async reviewRuns(runs, reviewers, reviewVerified, reviewDir, cwd, contract, store, paths, log, ledger, taskId, signal, reservationEstimateUsd) {
2956
+ async reviewRuns(runs, reviewers, reviewVerified, reviewDir, cwd, contract, store, paths, log, ledger, taskId, signal, reservationEstimateUsd, reviewUnchanged = false) {
3002
2957
  return reviewCandidateRuns({
3003
2958
  runs,
3004
2959
  reviewers,
@@ -3013,6 +2968,7 @@ export class Orchestrator {
3013
2968
  taskId,
3014
2969
  signal,
3015
2970
  reservationEstimateUsd,
2971
+ reviewUnchanged,
3016
2972
  }, {
3017
2973
  prepareReviewEvidenceDir: this.prepareReviewEvidenceDir.bind(this),
3018
2974
  recordReviewEvidenceCleanup: this.recordReviewEvidenceCleanup.bind(this),
@@ -3087,9 +3043,9 @@ export class Orchestrator {
3087
3043
  const wsm = new WorkspaceManager(execRoot);
3088
3044
  const readiness = new ReadinessLedger();
3089
3045
  store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
3090
- // Live (in-place) isolation deliberately tolerates non-git stateful
3091
- // environments; only envelope isolation needs the git boundary.
3092
- if (!input.inPlace) {
3046
+ // Legacy Git envelopes need their boundary; explicit directory execution
3047
+ // keeps the requested strategy without initializing the selected folder.
3048
+ if (!input.inPlace && input.workspaceKind !== "directory") {
3093
3049
  const gitPreconditionError = await ensureWriteModeGitBoundary(execRoot, log, store, paths, runId, mode);
3094
3050
  if (gitPreconditionError) {
3095
3051
  return {
@@ -3109,7 +3065,7 @@ export class Orchestrator {
3109
3065
  // exclusion — we are inside that branch, so inPlace is false here): bridge
3110
3066
  // an AGENTS.md-only PROJECT root so a Claude Code convergence attempt reads
3111
3067
  // it (INV-113).
3112
- this.ensureClaudeBridgeForRun(input.repoRoot, false, log);
3068
+ ensureClaudeBridgeForRun(input.repoRoot, false, log);
3113
3069
  }
3114
3070
  const reviewDir = join(paths.root, "review-evidence");
3115
3071
  if (contract.review_requested !== false)
@@ -3198,6 +3154,7 @@ export class Orchestrator {
3198
3154
  let attempt = 0;
3199
3155
  let converged = false;
3200
3156
  let exhausted = false;
3157
+ let processingBudgetDenial = null;
3201
3158
  let interrupted = false; // D-16 r8: terminalizes the run interrupted
3202
3159
  let lastFindings = [];
3203
3160
  let lastRun = null;
@@ -3223,12 +3180,13 @@ export class Orchestrator {
3223
3180
  const allCooledDown = () => adapterPool.every((a) => convergenceQuotaLedger.cooldownActive(a.adapter.id));
3224
3181
  const attemptTelemetries = [];
3225
3182
  let lastDiffStable = true;
3183
+ const observedPaths = new Set();
3226
3184
  try {
3227
3185
  // The contract's ENGINE-COMPUTED effective profile drives the envelope and
3228
3186
  // every attempt spec (parity with runRace); telemetry must never claim an
3229
3187
  // access level the envelope did not actually run with.
3230
3188
  const convergenceAccess = contract.access.effective_profile;
3231
- if (input.inPlace === true) {
3189
+ if (input.inPlace === true && input.workspaceKind !== "directory") {
3232
3190
  try {
3233
3191
  preTurnSha = await snapshotTree(execRoot);
3234
3192
  }
@@ -3243,6 +3201,8 @@ export class Orchestrator {
3243
3201
  dirtyPolicy: "snapshot",
3244
3202
  inPlace: input.inPlace ?? false,
3245
3203
  accessProfile: convergenceAccess,
3204
+ workspaceKind: input.workspaceKind,
3205
+ scopePaths: input.scopePaths,
3246
3206
  });
3247
3207
  for (;;) {
3248
3208
  if (input.signal?.aborted)
@@ -3277,7 +3237,7 @@ export class Orchestrator {
3277
3237
  attemptId,
3278
3238
  intent: "repair",
3279
3239
  harnessId: adapter.id,
3280
- cost: attemptCostEvidence(adapter.id, attemptId, this.reservationEstimateUsd(input), this.routeBillingKnowledge(input, adapter.id)),
3240
+ cost: attemptCostEvidence(adapter.id, attemptId, this.reservationEstimateUsd(input), this.routeBillingKnowledge(input, adapter.id), processingCostEvidence(routed.processing, this.routeBillingKnowledge(input, routed.adapter.id), [`harness:${adapter.id}`])),
3281
3241
  });
3282
3242
  if (!lease.granted) {
3283
3243
  exhausted = true;
@@ -3295,7 +3255,13 @@ export class Orchestrator {
3295
3255
  external_context_policy: knobs.webPolicy,
3296
3256
  ...(knobs.ignored.length > 0 ? { ignored_settings: knobs.ignored } : {}),
3297
3257
  });
3298
- run = await this.runCandidateInEnvelope(routed, envelope, attemptId, `Attempt ${attempt}`, contract, prompt, store, paths, wsm, ledger, convergenceAccess, (ev) => {
3258
+ run = await this.runCandidateInEnvelope(routed, envelope, attemptId, `Attempt ${attempt}`, contract, prompt, store, paths, wsm, ledger, {
3259
+ id: lease.lease.lease_id,
3260
+ onDenied: (denial) => {
3261
+ exhausted = true;
3262
+ processingBudgetDenial ??= denial;
3263
+ },
3264
+ }, convergenceAccess, (ev) => {
3299
3265
  const safeEv = redactHarnessEvent(ev);
3300
3266
  safeInvoke(input.onHarnessEvent, safeEv);
3301
3267
  log.emit("harness.event", harnessEventPayload(adapter.id, attemptId, safeEv));
@@ -3303,7 +3269,7 @@ export class Orchestrator {
3303
3269
  ledger.updateHold(lease.lease?.lease_id ?? "", streamedUsd);
3304
3270
  return ledger.tier() === "hard";
3305
3271
  }, input, true, // convergence runs one candidate: live deltas on (W-C4)
3306
- undefined, undefined, harnessHome);
3272
+ undefined, undefined, harnessHome, observedPaths);
3307
3273
  ledger.settle(lease.lease?.lease_id ?? "", attemptUsageCostSettlement(run.cost, run.costEstimated, run.attemptId, run.harnessId, run.telemetry.authMode, run.telemetry.usageCost));
3308
3274
  log.emit("harness.completed", {
3309
3275
  harness_id: adapter.id,
@@ -3343,6 +3309,17 @@ export class Orchestrator {
3343
3309
  ...(declared.code ? { declaredFailure: declared } : {}),
3344
3310
  telemetry: createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required, effectiveWeb, [routed.browserRequirement, routed.denyRequirement, routed.delegationRequirement], knobs.model),
3345
3311
  };
3312
+ if (envelope.workspace_kind === "directory") {
3313
+ const captured = await captureDirectoryCandidate({
3314
+ manager: wsm,
3315
+ envelope,
3316
+ artifactRoot: join(paths.attemptsDir, attemptId),
3317
+ sourceRoot: contract.repo.root,
3318
+ observedPaths: [...observedPaths],
3319
+ });
3320
+ run.files = captured.files;
3321
+ run.secretDiffRefusal = captured.refusal;
3322
+ }
3346
3323
  }
3347
3324
  lastRun = run;
3348
3325
  // Fail-closed twin of the candidate lane's gate: this loop terminalizes
@@ -3350,20 +3327,47 @@ export class Orchestrator {
3350
3327
  assertDelegatedEvidence(input.delegated === true, convergenceAccess, [run]);
3351
3328
  attemptTelemetries.push({ attemptId, harnessId: adapter.id, telemetry: run.telemetry });
3352
3329
  // Cancellation/deadline keeps priority over a belt failure finalized concurrently.
3353
- if (input.signal?.aborted)
3330
+ if (input.signal?.aborted || processingBudgetDenial)
3354
3331
  break;
3355
3332
  if (delegateFailure.candidateFailureKind(run)) {
3356
3333
  const failure = delegateFailure.candidateFailureTerminal(run, "convergence");
3357
- await delegateFailure.persistFailedInPlaceWorkProduct({
3358
- ...{ store, log, paths, execRoot, preTurnSha, taskId, mode },
3359
- live: input.inPlace === true,
3360
- run,
3361
- kind: input.create === true ? "new_repo" : "patch",
3362
- attempts: attempt,
3363
- });
3334
+ if (run.files) {
3335
+ await publishDirectoryCandidate({
3336
+ files: run.files,
3337
+ store,
3338
+ paths,
3339
+ taskId,
3340
+ attemptId,
3341
+ harnessId: run.harnessId,
3342
+ facts: makeOutcomeFacts("failed", { noChanges: run.files.noChanges }),
3343
+ log,
3344
+ });
3345
+ }
3346
+ else if (input.workspaceKind !== "directory") {
3347
+ await delegateFailure.persistFailedInPlaceWorkProduct({
3348
+ ...{ store, log, paths, execRoot, preTurnSha, taskId, mode },
3349
+ live: input.inPlace === true,
3350
+ run,
3351
+ kind: input.create === true ? "new_repo" : "patch",
3352
+ attempts: attempt,
3353
+ });
3354
+ }
3364
3355
  this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, attemptTelemetries, null);
3365
3356
  return failTerminally(log, store, paths, runId, taskId, mode, failure.phase, failure.error, ledger.spend(), failure.metadata);
3366
3357
  }
3358
+ if (input.workspaceKind === "directory" && run.secretDiffRefusal) {
3359
+ return failedCandidatesResult({
3360
+ ledger,
3361
+ mode,
3362
+ store,
3363
+ paths,
3364
+ log,
3365
+ runId,
3366
+ taskId,
3367
+ runs: [run],
3368
+ writeTelemetry: () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, attemptTelemetries, null),
3369
+ });
3370
+ }
3367
3371
  // D-16 r8: interrupted (errored===false) would CONVERGE a partial diff
3368
3372
  // as clean — break BEFORE review; a harness error still gate-retries.
3369
3373
  if (run.outcomeClass === "interrupted") {
@@ -3373,7 +3377,7 @@ export class Orchestrator {
3373
3377
  // Post-mutation fence for in-place: snapshot the live tree NOW (after the
3374
3378
  // harness mutated it, before this attempt's review). The last attempt's
3375
3379
  // value is the revert target persisted into work_product.yaml.
3376
- if (input.inPlace === true) {
3380
+ if (input.inPlace === true && input.workspaceKind !== "directory") {
3377
3381
  try {
3378
3382
  lastPostTurnSha = await snapshotTree(execRoot);
3379
3383
  }
@@ -3394,148 +3398,43 @@ export class Orchestrator {
3394
3398
  log.emit("review.skipped", { reason: "not_requested", attempt_id: attemptId });
3395
3399
  return evaluateUnreviewedConvergence(evidence, contract);
3396
3400
  }
3397
- const candidateReviewCwd = run.reviewCwd ?? input.repoRoot;
3398
- const candidateReviewEvidenceDir = this.prepareReviewEvidenceDir(reviewDir, candidateReviewCwd);
3399
- try {
3400
- writeText(join(candidateReviewEvidenceDir, "TESTS.txt"), renderTestsEvidence(contract, run.gates).trim() + "\n");
3401
- // Reviewer panels spend real money in convergence too: reserve before,
3402
- // settle the observed cost, and surface it as a budget observation
3403
- // (parity with the race path's reviewRuns metering).
3404
- const reviewLease = reviewers.length > 0
3405
- ? ledger.reserve({
3406
- taskId,
3407
- attemptId,
3408
- intent: "review",
3409
- harnessId: "review-panel",
3410
- cost: attemptCostEvidence("review-panel", attemptId, this.reservationEstimateUsd(input)),
3411
- })
3412
- : null;
3413
- const reviewResult = reviewers.length > 0 && (reviewLease?.granted ?? false)
3414
- ? await this.reviewScoped({
3415
- candidateLabel: `Attempt ${attempt}`,
3416
- diff: run.diff,
3417
- evidenceDir: candidateReviewEvidenceDir,
3418
- artifactsDir: join(paths.reviewsDir, `${attemptId}-reviewers`),
3419
- cwd: candidateReviewCwd,
3420
- reviewers,
3421
- envInheritance: envInheritance(this.config(input.repoRoot)),
3422
- signal: input.signal,
3423
- onReviewerEvent: (event) => log.emit(event.type, { ...event }),
3424
- })
3425
- : {
3426
- findings: [],
3427
- routeProofs: [],
3428
- reviewerRequests: [],
3429
- crossFamilyHealthy: false,
3430
- healthyProviders: [],
3431
- crossFamilyVerified: false,
3432
- distinctProviders: [],
3433
- reviewSpendUsd: 0,
3434
- reviewSpendEstimated: false,
3435
- reviewCashUsd: 0,
3436
- reviewCashKnowledge: "unknown",
3437
- reviewValuationUsd: 0,
3438
- reviewValuationKnowledge: "unknown",
3439
- reviewUnknownUsd: 0,
3440
- };
3441
- if (reviewLease?.granted) {
3442
- ledger.settle(reviewLease.lease?.lease_id ?? "", reviewUsageCostSettlement(reviewResult.reviewCashUsd, reviewResult.reviewValuationUsd, {
3443
- cash: reviewResult.reviewCashKnowledge,
3444
- valuation: reviewResult.reviewValuationKnowledge,
3445
- }, [`attempt:${attemptId}`, "review:panel"], reviewResult.reviewUnknownUsd));
3446
- if ((reviewResult.reviewSpendUsd ?? 0) > 0) {
3447
- log.emit("budget.observation", {
3448
- harness_id: "review-panel",
3449
- attempt_id: attemptId,
3450
- kind: "spend",
3451
- usd: reviewResult.reviewSpendUsd,
3452
- cash_usd: reviewResult.reviewCashUsd,
3453
- valuation_usd: reviewResult.reviewValuationUsd,
3454
- unknown_usd: reviewResult.reviewUnknownUsd,
3455
- estimated: reviewResult.reviewSpendEstimated === true,
3456
- });
3457
- }
3458
- }
3459
- else if (reviewLease && !reviewLease.granted) {
3460
- log.emit("budget.lease.created", {
3461
- granted: false,
3462
- reason: reviewLease.reason,
3463
- attempt_id: attemptId,
3464
- harness_id: "review-panel",
3465
- });
3466
- }
3467
- actualReviewVerified =
3468
- reviewVerified &&
3469
- reviewResult.crossFamilyHealthy &&
3470
- reviewResult.crossFamilyVerified;
3471
- const revalidated = await revalidateFindings(reviewResult.findings, {
3472
- candidateRoot: candidateReviewCwd,
3473
- evidenceDir: candidateReviewEvidenceDir,
3474
- });
3475
- // Typed policy gate (risk + protected paths) merges with reviewer findings.
3476
- const policy = policyFindings(run, actualReviewVerified, contract.constraints.protected_paths, contract.constraints.auto_protected_paths, contract.constraints.protected_path_approvals, contract.constraints.deny_paths);
3477
- const allFindings = [...policy.findings, ...revalidated];
3478
- lastFindings = allFindings;
3479
- const inconclusive = allFindings.some((f) => f.severity === "INSUFFICIENT_EVIDENCE" || f.status === "insufficient_evidence");
3480
- const finalReviewClean = reviewResult.crossFamilyHealthy &&
3481
- reviewResult.crossFamilyVerified &&
3482
- !inconclusive &&
3483
- !allFindings.some((f) => isBlocking(f));
3484
- store.writeYaml(join(paths.reviewsDir, `${attemptId}.yaml`), {
3485
- attempt_id: attemptId,
3486
- review_verified: actualReviewVerified,
3487
- final_review_clean: finalReviewClean,
3488
- cross_family_healthy: reviewResult.crossFamilyHealthy,
3489
- cross_family_verified: reviewResult.crossFamilyVerified,
3490
- healthy_providers: reviewResult.healthyProviders,
3491
- verified_providers: reviewResult.distinctProviders,
3492
- reviewer_requests: reviewResult.reviewerRequests,
3493
- risk: policy.risk,
3494
- findings: allFindings,
3495
- route_proofs: reviewResult.routeProofs,
3496
- });
3497
- lastFinalReviewClean = finalReviewClean;
3498
- // Measure diff stability instead of asserting it: the tree must not have
3499
- // changed between the candidate diff capture and the end of review.
3500
- const postReviewDiff = await wsm.diff(envelope);
3501
- const diffStableAfterReview = sha256(postReviewDiff) === sha256(run.diff);
3502
- lastDiffStable = diffStableAfterReview;
3503
- const evaluated = evaluateConvergence({
3504
- predicate: contract.convergence,
3505
- gates: run.errored
3506
- ? [
3507
- ...run.gates,
3508
- {
3509
- id: "harness",
3510
- command: "harness",
3511
- exit_code: 1,
3512
- status: "failed",
3513
- duration_ms: 0,
3514
- required: true,
3515
- stdout_tail: null,
3516
- stderr_tail: null,
3517
- output_truncated: false,
3518
- },
3519
- ]
3520
- : run.gates,
3521
- findings: allFindings,
3522
- finalReviewClean,
3523
- diffStableAfterReview,
3524
- });
3525
- log.emit("finding.revalidated", {
3526
- attempt_id: attemptId,
3527
- converged: evaluated.converged,
3528
- reasons: evaluated.reasons,
3529
- diff_stable_after_review: diffStableAfterReview,
3530
- });
3531
- return evaluated;
3532
- }
3533
- finally {
3534
- this.recordReviewEvidenceCleanup(store, join(paths.reviewsDir, `${attemptId}-evidence-cleanup.yaml`), attemptId, candidateReviewEvidenceDir, candidateReviewCwd);
3535
- }
3401
+ const [evidence] = await this.reviewRuns([run], reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal, this.reservationEstimateUsd(input), true);
3402
+ if (!evidence)
3403
+ throw new Error("Convergence review produced no evidence");
3404
+ lastFindings = evidence.findings;
3405
+ lastFinalReviewClean = evidence.finalReviewClean;
3406
+ actualReviewVerified = evidence.reviewVerified ?? false;
3407
+ lastDiffStable = run.files
3408
+ ? await directoryCandidateStable(wsm, envelope, run.files)
3409
+ : sha256(await wsm.diff(envelope)) === sha256(run.diff);
3410
+ const evaluated = evaluateConvergence({
3411
+ predicate: contract.convergence,
3412
+ gates: evidence.gates,
3413
+ findings: evidence.findings,
3414
+ finalReviewClean: evidence.finalReviewClean,
3415
+ diffStableAfterReview: lastDiffStable,
3416
+ });
3417
+ log.emit("finding.revalidated", {
3418
+ attempt_id: attemptId,
3419
+ converged: evaluated.converged,
3420
+ reasons: evaluated.reasons,
3421
+ diff_stable_after_review: lastDiffStable,
3422
+ });
3423
+ return evaluated;
3536
3424
  })();
3537
3425
  }
3538
3426
  catch (err) {
3427
+ if (run.files)
3428
+ await publishDirectoryCandidate({
3429
+ files: run.files,
3430
+ store,
3431
+ paths,
3432
+ taskId,
3433
+ attemptId,
3434
+ harnessId: run.harnessId,
3435
+ facts: makeOutcomeFacts("failed", { noChanges: run.files.noChanges }),
3436
+ log,
3437
+ });
3539
3438
  return failTerminally(log, store, paths, runId, taskId, mode, "review", err, ledger.spend());
3540
3439
  }
3541
3440
  if (conv.converged) {
@@ -3543,13 +3442,17 @@ export class Orchestrator {
3543
3442
  break;
3544
3443
  }
3545
3444
  const requiredGateFailing = run.gates.length > 0 && !gatesPassed(run.gates);
3546
- const diffHash = sha256(run.diff);
3547
- if (requiredGateFailing && diffHash === lastFailingGateDiffHash) {
3445
+ const diffHash = run.files
3446
+ ? run.files.manifestSha256
3447
+ : input.workspaceKind === "directory"
3448
+ ? null
3449
+ : sha256(run.diff);
3450
+ if (requiredGateFailing && diffHash !== null && diffHash === lastFailingGateDiffHash) {
3548
3451
  sameFailingGateDiffs += 1;
3549
3452
  }
3550
3453
  else {
3551
3454
  sameFailingGateDiffs = requiredGateFailing ? 1 : 0;
3552
- lastFailingGateDiffHash = requiredGateFailing ? diffHash : "";
3455
+ lastFailingGateDiffHash = requiredGateFailing ? (diffHash ?? "") : "";
3553
3456
  }
3554
3457
  if (sameFailingGateDiffs >= 2) {
3555
3458
  stuckNoProgress = true;
@@ -3565,7 +3468,10 @@ export class Orchestrator {
3565
3468
  });
3566
3469
  break;
3567
3470
  }
3568
- const sig = failureSignature(conv.reasons);
3471
+ const sig = failureSignature([
3472
+ ...conv.reasons,
3473
+ ...(run.files ? [`files_manifest:${run.files.manifestSha256}`] : []),
3474
+ ]);
3569
3475
  readiness.recordRound(sig, conv.reasons.join("; "));
3570
3476
  if (sig !== lastSig) {
3571
3477
  triedSinceProgress = new Set();
@@ -3648,6 +3554,12 @@ export class Orchestrator {
3648
3554
  facts = decision.facts;
3649
3555
  }
3650
3556
  }
3557
+ if (lastRun?.files)
3558
+ facts = {
3559
+ ...facts,
3560
+ noChanges: lastRun.files.noChanges,
3561
+ reason: facts.reason === "no_changes" && lastRun.files.noChanges !== true ? null : facts.reason,
3562
+ };
3651
3563
  // A budget terminal turns a succeeded lifecycle into a failed one (D8).
3652
3564
  const convBudgetTerminal = ledger.terminal();
3653
3565
  if (facts.lifecycle === "succeeded" && convBudgetTerminal) {
@@ -3671,11 +3583,13 @@ export class Orchestrator {
3671
3583
  let convFinalVerify = null;
3672
3584
  if (input.inPlace !== true &&
3673
3585
  lastRun &&
3674
- lastRun.diff.trim().length > 0 &&
3586
+ (lastRun.files ? lastRun.files.noChanges !== true : lastRun.diff.trim().length > 0) &&
3675
3587
  facts.lifecycle === "succeeded" &&
3676
3588
  facts.review !== "blocked" &&
3677
3589
  !input.signal?.aborted) {
3678
- convFinalVerify = await finalVerifyPatch(execRoot, lastRun, gateSpecsFromContract(contract), log);
3590
+ convFinalVerify = lastRun.files
3591
+ ? await finalVerifyFiles(lastRun.files, undefined, gateSpecsFromContract(contract), log)
3592
+ : await finalVerifyPatch(execRoot, lastRun, gateSpecsFromContract(contract), log);
3679
3593
  if (finalVerifyBlocks(convFinalVerify))
3680
3594
  facts = { ...facts, checks: "failed", reason: "checks_failed" };
3681
3595
  }
@@ -3694,51 +3608,68 @@ export class Orchestrator {
3694
3608
  }
3695
3609
  this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, attemptTelemetries, lastRun?.attemptId ?? null);
3696
3610
  // Deliver the converged/last work to final/ so `apply` and `inspect` can
3697
- // use it. D-16 r8: an INTERRUPTED envelope run delivers no applyable
3698
- // work_product (its partial patch.diff stays diagnostic via attempts/);
3699
- // in-place keeps the product so the honest Revert offer survives.
3700
- if (lastRun && (!interrupted || input.inPlace === true)) {
3701
- secretDiff.assertNoSecretLikeTokens("final patch diff", lastRun.diff);
3702
- const patchSha256 = sha256(lastRun.diff);
3703
- store.writeText(join(paths.finalDir, "patch.diff"), lastRun.diff);
3704
- // Honest apply-state (parity with runRace single-candidate in-place): a
3705
- // convergence run with inPlace mutated the live tree directly across its
3706
- // attempts, so it is "applied" even when review blocked (Revert offered).
3707
- const convHasDiff = lastRun.diff.trim().length > 0;
3611
+ // use it. Directory results retain complete partial files with their actual
3612
+ // lifecycle; direct effects are already applied and promise no rollback.
3613
+ if (lastRun && (lastRun.files || !interrupted || input.inPlace === true)) {
3708
3614
  const convAdoptable = facts.lifecycle === "succeeded" &&
3709
3615
  reviewAllowsApply(facts) &&
3710
3616
  facts.checks !== "failed" &&
3711
3617
  !workStateVetoes(facts);
3712
- const convAdopted = input.inPlace === true && convHasDiff ? true : null;
3713
- const convApplyState = convAdopted === true
3714
- ? convAdoptable
3715
- ? "applied"
3716
- : "applied_review_blocked"
3717
- : "not_applied";
3718
- const revertAnchorId = convAdopted === true
3719
- ? await createRevertAnchorOrNull(execRoot, preTurnSha, lastPostTurnSha)
3720
- : null;
3721
- store.writeYaml(join(paths.finalDir, "work_product.yaml"), {
3722
- id: newId("wp"),
3723
- kind: "patch",
3724
- source_task_id: taskId,
3725
- producer_attempt_id: lastRun.attemptId,
3726
- meta: {
3727
- harness_id: lastRun.harnessId,
3728
- result_kind: "patch",
3729
- mode,
3730
- attempts: attempt,
3731
- lifecycle: facts.lifecycle,
3732
- outcome_facts: facts,
3733
- review_verified: actualReviewVerified,
3734
- patch_sha256: patchSha256,
3735
- adopted: convAdopted,
3736
- apply_state: convApplyState,
3737
- pre_turn_sha: convAdopted === true ? preTurnSha : null,
3738
- post_turn_sha: convAdopted === true ? lastPostTurnSha : null,
3739
- revert_anchor_id: revertAnchorId,
3740
- },
3741
- });
3618
+ if (lastRun.files) {
3619
+ await publishDirectoryCandidate({
3620
+ files: lastRun.files,
3621
+ store,
3622
+ paths,
3623
+ taskId,
3624
+ attemptId: lastRun.attemptId,
3625
+ harnessId: lastRun.harnessId,
3626
+ facts,
3627
+ log,
3628
+ });
3629
+ if (lastRun.answerText) {
3630
+ store.writeText(join(paths.finalDir, "answer.md"), lastRun.answerText);
3631
+ log.emit("output.ready", { kind: "answer", path: "final/answer.md" });
3632
+ }
3633
+ }
3634
+ else {
3635
+ secretDiff.assertNoSecretLikeTokens("final patch diff", lastRun.diff);
3636
+ const patchSha256 = sha256(lastRun.diff);
3637
+ store.writeText(join(paths.finalDir, "patch.diff"), lastRun.diff);
3638
+ // Honest apply-state (parity with runRace single-candidate in-place): a
3639
+ // convergence run with inPlace mutated the live tree directly across its
3640
+ // attempts, so it is "applied" even when review blocked (Revert offered).
3641
+ const convHasDiff = lastRun.diff.trim().length > 0;
3642
+ const convAdopted = input.inPlace === true && convHasDiff ? true : null;
3643
+ const convApplyState = convAdopted === true
3644
+ ? convAdoptable
3645
+ ? "applied"
3646
+ : "applied_review_blocked"
3647
+ : "not_applied";
3648
+ const revertAnchorId = convAdopted === true
3649
+ ? await createRevertAnchorOrNull(execRoot, preTurnSha, lastPostTurnSha)
3650
+ : null;
3651
+ store.writeYaml(join(paths.finalDir, "work_product.yaml"), {
3652
+ id: newId("wp"),
3653
+ kind: "patch",
3654
+ source_task_id: taskId,
3655
+ producer_attempt_id: lastRun.attemptId,
3656
+ meta: {
3657
+ harness_id: lastRun.harnessId,
3658
+ result_kind: "patch",
3659
+ mode,
3660
+ attempts: attempt,
3661
+ lifecycle: facts.lifecycle,
3662
+ outcome_facts: facts,
3663
+ review_verified: actualReviewVerified,
3664
+ patch_sha256: patchSha256,
3665
+ adopted: convAdopted,
3666
+ apply_state: convApplyState,
3667
+ pre_turn_sha: convAdopted === true ? preTurnSha : null,
3668
+ post_turn_sha: convAdopted === true ? lastPostTurnSha : null,
3669
+ revert_anchor_id: revertAnchorId,
3670
+ },
3671
+ });
3672
+ }
3742
3673
  store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${mode})\n\n- Lifecycle: ${facts.lifecycle}${facts.reason ? ` (${facts.reason})` : ""}\n- Attempts: ${attempt}\n- Winner: ${lastRun.attemptId}\n- Review verified (cross-family): ${actualReviewVerified}\n- Apply recommendation: ${decision?.apply_recommendation ?? "inspect"}${stuckNoProgressReason ? `\n- No-progress reason: ${stuckNoProgressReason}` : ""}\n`);
3743
3674
  // Lifecycle invariant (all modes): output.ready precedes the terminal
3744
3675
  // event so a client that applied the terminal event has the output.
@@ -3757,6 +3688,9 @@ export class Orchestrator {
3757
3688
  const convDeclared = !convNeedsDecision && facts.lifecycle === "failed" && !isBudgetTerminal(facts.reason)
3758
3689
  ? lastRun?.declaredFailure
3759
3690
  : undefined;
3691
+ const processingBudgetMapping = processingBudgetDenial
3692
+ ? classifyBudgetFailure({ denial: processingBudgetDenial, terminal: ledger.terminal() })
3693
+ : null;
3760
3694
  writeFailure(store, paths, {
3761
3695
  phase: convNeedsDecision ? "review" : "convergence",
3762
3696
  category: isBudgetTerminal(facts.reason)
@@ -3766,7 +3700,7 @@ export class Orchestrator {
3766
3700
  : convNeedsDecision
3767
3701
  ? "policy"
3768
3702
  : (convDeclared?.category ?? "internal"),
3769
- code: convDeclared?.code ?? null,
3703
+ code: processingBudgetMapping?.code ?? convDeclared?.code ?? null,
3770
3704
  resetsAt: convDeclared?.resetsAt ?? null,
3771
3705
  safeMessage: convNeedsDecision
3772
3706
  ? `review escalated to a human decision after ${attempt} attempt(s)`
@@ -3799,12 +3733,15 @@ export class Orchestrator {
3799
3733
  "Inspect latest patch and review findings",
3800
3734
  "Retry with more attempts or a narrower prompt",
3801
3735
  ],
3736
+ ...(processingBudgetMapping
3737
+ ? budgetFailureRecord(processingBudgetMapping, { runDir: paths.root })
3738
+ : {}),
3802
3739
  });
3803
3740
  // D-16 r8/r9: an INTERRUPTED envelope run still gets its diagnostic
3804
3741
  // summary + output.ready (only patch/work_product are withheld) — the
3805
3742
  // ARCHITECTURE event contract guarantees output.ready precedes the
3806
3743
  // terminal in every mode.
3807
- if (!lastRun || (interrupted && input.inPlace !== true)) {
3744
+ if (!lastRun || (interrupted && input.inPlace !== true && !lastRun.files)) {
3808
3745
  store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${mode})\n\n- Lifecycle: ${facts.lifecycle}${facts.reason ? ` (${facts.reason})` : ""}\n- Attempts: ${attempt}\n`);
3809
3746
  log.emit("output.ready", {
3810
3747
  kind: "summary",
@@ -3814,7 +3751,7 @@ export class Orchestrator {
3814
3751
  }
3815
3752
  }
3816
3753
  // work_product.emitted only when a product was actually written (r9).
3817
- if (lastRun && (!interrupted || input.inPlace === true)) {
3754
+ if (lastRun && (lastRun.files || !interrupted || input.inPlace === true)) {
3818
3755
  log.emit("work_product.emitted", { winner: lastRun.attemptId });
3819
3756
  }
3820
3757
  if (!convIsFailureTerminal) {
@@ -4226,16 +4163,19 @@ export class Orchestrator {
4226
4163
  // A selected profile's credential_kind decides billing (round-18 #2).
4227
4164
  const profileRoute = this.credentials.profileAuthRoute(input, harnessId);
4228
4165
  if (profileRoute)
4229
- return profileRoute === "api_key" ? "metered" : "unknown";
4166
+ return profileRoute === "api_key" ? "metered" : "subscription_entitlement";
4230
4167
  // Deps-closure site: no selected route exists yet, so the RESOLVED
4231
4168
  // preference (per-run > per-harness config > global) speaks — never the
4232
4169
  // raw run input (#121).
4233
4170
  const mode = authModeForPreference(this.authPreferenceForHarness(input.repoRoot, harnessId, input.authPreference));
4234
4171
  if (mode)
4235
- return mode === "api_key" ? "metered" : "unknown";
4236
- return loadHarnessMetrics(globalConfigDir())[harnessId]?.last_auth_mode === "api_key"
4172
+ return mode === "api_key" ? "metered" : "subscription_entitlement";
4173
+ const lastAuth = loadHarnessMetrics(globalConfigDir())[harnessId]?.last_auth_mode;
4174
+ return lastAuth === "api_key"
4237
4175
  ? "metered"
4238
- : "unknown";
4176
+ : lastAuth === "local_session"
4177
+ ? "subscription_entitlement"
4178
+ : "unknown";
4239
4179
  }
4240
4180
  /**
4241
4181
  * #27 / D-6: build the engine-side deps closure for the deep-scan bounded
@@ -4247,11 +4187,13 @@ export class Orchestrator {
4247
4187
  deepScanReducerDeps(input, contract, log) {
4248
4188
  return {
4249
4189
  newReadOnlyHome: () => resolveReadOnlyRouteContext(this.execRootOf(input)),
4250
- costEvidence: (harnessId, attemptId) =>
4190
+ costEvidence: (harnessId, attemptId, routed) =>
4251
4191
  // The reducer admits under a finite estimate floor (mirror of the n>1
4252
4192
  // scout reserve) so a subscription route is not refused for lacking a
4253
4193
  // cash quote.
4254
- attemptCostEvidence(harnessId, attemptId, this.estimateUsdFloor(input.repoRoot), this.routeBillingKnowledge(input, harnessId)),
4194
+ attemptCostEvidence(harnessId, attemptId, this.estimateUsdFloor(input.repoRoot), this.routeBillingKnowledge(input, harnessId), processingCostEvidence(routed?.processing, this.routeBillingKnowledge(input, harnessId), [
4195
+ `harness:${harnessId}`,
4196
+ ])),
4255
4197
  buildSpec: async (routed, homeEnv, prompt, attemptId) => {
4256
4198
  const knobs = this.routeSpecKnobs(routed, contract, undefined, input.effort);
4257
4199
  const effectiveWeb = this.discloseWebUpgrade(log, routed, knobs.webPolicy, attemptId);
@@ -4434,7 +4376,7 @@ export class Orchestrator {
4434
4376
  // (mirror of the candidate loop): the first top-level scout reserves
4435
4377
  // without a floor; later scouts and every real Delegate child pass the
4436
4378
  // repo floor because they overlap an existing family unit.
4437
- cost: attemptCostEvidence(adapter.id, attemptId, this.reservationEstimateUsd(input, opts.deepScan && idx > 0), this.routeBillingKnowledge(input, adapter.id)),
4379
+ cost: attemptCostEvidence(adapter.id, attemptId, this.reservationEstimateUsd(input, opts.deepScan && idx > 0), this.routeBillingKnowledge(input, adapter.id), processingCostEvidence(routed.processing, this.routeBillingKnowledge(input, routed.adapter.id), [`harness:${adapter.id}`])),
4438
4380
  });
4439
4381
  if (!lease.granted) {
4440
4382
  log.emit("budget.lease.created", {
@@ -4580,6 +4522,10 @@ export class Orchestrator {
4580
4522
  const { knobs, spec: preparedSpec, reportAbort, reportInteraction, readonlyWorkMode, attemptEventsPath, telemetry, } = preparation.value;
4581
4523
  let { answer } = preparation.value;
4582
4524
  let spec = preparedSpec;
4525
+ bindProcessingAdmission(spec, ledger, lease.lease.lease_id, adapter.id, attemptId, (denial) => {
4526
+ budgetStopped = true;
4527
+ budgetDenial ??= denial;
4528
+ });
4583
4529
  const retryPolicy = transientRetryPolicy(this.config(input.repoRoot));
4584
4530
  let activeSessionId = spec.session_id;
4585
4531
  const onAbort = () => {
@@ -4594,6 +4540,8 @@ export class Orchestrator {
4594
4540
  let cost = 0;
4595
4541
  let costEstimated = false;
4596
4542
  let harnessError = null;
4543
+ let streamBudgetDenied = false;
4544
+ let processingRefusal = null;
4597
4545
  let poolExhausted = null; // A5: typed pool-exhausted refusal
4598
4546
  try {
4599
4547
  const triedProfiles = new Set(); // W5.4 failover: each profile at most once
@@ -4659,6 +4607,15 @@ export class Orchestrator {
4659
4607
  const spend = observeReadonlySpend(safeEv, (t, p) => log.emit(t, p), adapter.id, attemptId);
4660
4608
  cost += spend.costUsd;
4661
4609
  costEstimated ||= spend.estimated;
4610
+ const streamDenial = updateProcessingStreamHold(runSpec, telemetry.usageCost, ledger, lease.lease.lease_id, adapter.id, attemptId);
4611
+ if (streamDenial) {
4612
+ budgetStopped = streamBudgetDenied = true;
4613
+ budgetDenial ??= streamDenial;
4614
+ harnessError = streamDenial.reason;
4615
+ reportAbort.abort();
4616
+ void adapter.cancel?.(activeSessionId)?.catch(() => { });
4617
+ break;
4618
+ }
4662
4619
  // A TYPED final message wins verbatim over joined narration.
4663
4620
  answer.observe(safeEv);
4664
4621
  if (safeEv.type === "error")
@@ -4669,9 +4626,15 @@ export class Orchestrator {
4669
4626
  }
4670
4627
  catch (err) {
4671
4628
  harnessError = safeErrorMessage(err);
4629
+ if (err instanceof ProcessingBudgetAdmissionError) {
4630
+ processingRefusal = err;
4631
+ break;
4632
+ }
4672
4633
  // #31: classify the throw (watchdog timeout vs process crash) as typed.
4673
4634
  telemetry.transientFailures.push(classifyAdapterThrow({ errorName: err instanceof Error ? err.name : null }));
4674
4635
  }
4636
+ if (streamBudgetDenied)
4637
+ break;
4675
4638
  const newTransients = telemetry.transientFailures.slice(transientStart);
4676
4639
  const transient = newTransients.at(-1) ?? null;
4677
4640
  const sawRetryable = newTransients.some((f) => f.retryable);
@@ -4748,7 +4711,7 @@ export class Orchestrator {
4748
4711
  preStreamFailureSource: "readonly-pre-stream",
4749
4712
  });
4750
4713
  }
4751
- if (harnessError && !poolExhausted) {
4714
+ if (harnessError && !poolExhausted && !processingRefusal) {
4752
4715
  emitTransientExhausted((t, p) => log.emit(t, p), adapter.id, attemptId, telemetry, retryPolicy.maxRetries);
4753
4716
  }
4754
4717
  attemptTelemetries.push({ attemptId, harnessId: adapter.id, telemetry });
@@ -4809,7 +4772,9 @@ export class Orchestrator {
4809
4772
  report,
4810
4773
  error: harnessError,
4811
4774
  telemetry,
4812
- ...(poolExhausted ? { declaredFailure: declaredFailure(poolExhausted) } : {}),
4775
+ ...(poolExhausted || processingRefusal
4776
+ ? { declaredFailure: declaredFailure(processingRefusal ?? poolExhausted) }
4777
+ : {}),
4813
4778
  });
4814
4779
  if (opts.deepScan) {
4815
4780
  store.writeText(join(paths.findingsDir, `${attemptId}-error.md`), `# Explorer ${attemptId} failed\n\n${harnessError}\n`);
@@ -5062,7 +5027,7 @@ export class Orchestrator {
5062
5027
  });
5063
5028
  const terminalFacts = makeOutcomeFacts(roTerminal.lifecycle, {
5064
5029
  ...(roTerminal.review ? { review: roTerminal.review } : {}),
5065
- reason: roTerminal.reason,
5030
+ reason: budgetMapping?.reason ?? roTerminal.reason,
5066
5031
  });
5067
5032
  const terminalHarnessId = budgetMapping?.harnessId ?? last?.harnessId;
5068
5033
  store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${opts.mode})\n\n- Harness: ${terminalHarnessId ?? "none"}\n- Lifecycle: ${terminalFacts.lifecycle}${terminalFacts.reason ? ` (${terminalFacts.reason})` : ""}\n\n${singleError}\n`);
@@ -5254,19 +5219,12 @@ export class Orchestrator {
5254
5219
  });
5255
5220
  store.writeText(join(paths.finalDir, "omissions.md"), `# Omissions\n\n${unsuccessful.map((a) => `- ${a.attemptId} / ${a.harnessId} (${a.status}): ${a.error}`).join("\n") || "- None recorded by the runner. Synthesis claims still require evidence checks."}\n`);
5256
5221
  }
5257
- // A read-only report (ask / deep-scan) has no live-tree work; the only
5258
- // non-clean terminal is an aggregate paid-budget stop.
5259
5222
  let terminalFacts = makeOutcomeFacts("succeeded");
5260
5223
  const reportBudgetTerminal = ledger.terminal();
5261
5224
  if (reportBudgetTerminal) {
5262
5225
  terminalFacts = makeOutcomeFacts("failed", { reason: reportBudgetTerminal });
5263
5226
  }
5264
5227
  else if (!opts.deepScan) {
5265
- // D-16: fold the winning read-only attempt's work_state into the terminal.
5266
- // A terminal context exhaustion with no completed report ⇒ interrupted;
5267
- // a needs_input/incomplete report ⇒ a succeeded run whose work_state
5268
- // vetoes applyability and a clean exit (INV-116). answer.md was already
5269
- // persisted from the unwrapped OUTPUT.
5270
5228
  const winnerTelemetry = succeeded[0]?.telemetry;
5271
5229
  const winnerWorkState = winnerTelemetry?.outcome?.workState;
5272
5230
  if (winnerTelemetry?.contextExhausted && winnerWorkState?.state !== "completed") {
@@ -5327,9 +5285,6 @@ export class Orchestrator {
5327
5285
  });
5328
5286
  }
5329
5287
  else if (workVetoed) {
5330
- // D-16: a succeeded lifecycle whose work_state vetoes is a needs-me
5331
- // terminal — run.blocked (not run.completed); the outcome-aware exit
5332
- // projection returns non-zero from the same facts.
5333
5288
  log.emit("run.blocked", {
5334
5289
  lifecycle: terminalFacts.lifecycle,
5335
5290
  facts: terminalFacts,