@claudexor/orchestrator 4.1.0 → 4.2.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 (121) hide show
  1. package/dist/account-pool.d.ts +11 -0
  2. package/dist/account-pool.d.ts.map +1 -1
  3. package/dist/account-pool.js +9 -0
  4. package/dist/account-pool.js.map +1 -1
  5. package/dist/account-resolution.d.ts +3 -0
  6. package/dist/account-resolution.d.ts.map +1 -1
  7. package/dist/account-resolution.js +1 -0
  8. package/dist/account-resolution.js.map +1 -1
  9. package/dist/attemptTelemetry.d.ts +4 -6
  10. package/dist/attemptTelemetry.d.ts.map +1 -1
  11. package/dist/attemptTelemetry.js +1 -1
  12. package/dist/attemptTelemetry.js.map +1 -1
  13. package/dist/budgetFailure.d.ts +4 -1
  14. package/dist/budgetFailure.d.ts.map +1 -1
  15. package/dist/budgetFailure.js +4 -2
  16. package/dist/budgetFailure.js.map +1 -1
  17. package/dist/candidateEvidence.d.ts.map +1 -1
  18. package/dist/candidateEvidence.js +7 -2
  19. package/dist/candidateEvidence.js.map +1 -1
  20. package/dist/candidateOutputs.d.ts.map +1 -1
  21. package/dist/candidateOutputs.js +6 -0
  22. package/dist/candidateOutputs.js.map +1 -1
  23. package/dist/candidateReview.d.ts +3 -1
  24. package/dist/candidateReview.d.ts.map +1 -1
  25. package/dist/candidateReview.js +6 -1
  26. package/dist/candidateReview.js.map +1 -1
  27. package/dist/candidateTerminals.d.ts +12 -1
  28. package/dist/candidateTerminals.d.ts.map +1 -1
  29. package/dist/candidateTerminals.js +51 -21
  30. package/dist/candidateTerminals.js.map +1 -1
  31. package/dist/captureRefusal.d.ts +28 -0
  32. package/dist/captureRefusal.d.ts.map +1 -0
  33. package/dist/captureRefusal.js +54 -0
  34. package/dist/captureRefusal.js.map +1 -0
  35. package/dist/continuity-terminal.d.ts +3 -1
  36. package/dist/continuity-terminal.d.ts.map +1 -1
  37. package/dist/continuity-terminal.js.map +1 -1
  38. package/dist/contract-gates.d.ts +10 -2
  39. package/dist/contract-gates.d.ts.map +1 -1
  40. package/dist/contract-gates.js +14 -1
  41. package/dist/contract-gates.js.map +1 -1
  42. package/dist/credential-profile-rotation.js +2 -3
  43. package/dist/credential-profile-rotation.js.map +1 -1
  44. package/dist/delegationFailure.d.ts +104 -7
  45. package/dist/delegationFailure.d.ts.map +1 -1
  46. package/dist/delegationFailure.js +47 -31
  47. package/dist/delegationFailure.js.map +1 -1
  48. package/dist/index.d.ts +1 -0
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/inrun-continuity-types.d.ts +13 -1
  51. package/dist/inrun-continuity-types.d.ts.map +1 -1
  52. package/dist/inrun-continuity.d.ts +9 -2
  53. package/dist/inrun-continuity.d.ts.map +1 -1
  54. package/dist/inrun-continuity.js +43 -23
  55. package/dist/inrun-continuity.js.map +1 -1
  56. package/dist/inrun-continuity.test-support.d.ts +6 -2
  57. package/dist/inrun-continuity.test-support.d.ts.map +1 -1
  58. package/dist/inrun-continuity.test-support.js +4 -1
  59. package/dist/inrun-continuity.test-support.js.map +1 -1
  60. package/dist/orchestrator-credentials.d.ts +5 -4
  61. package/dist/orchestrator-credentials.d.ts.map +1 -1
  62. package/dist/orchestrator-credentials.js +7 -4
  63. package/dist/orchestrator-credentials.js.map +1 -1
  64. package/dist/orchestrator.d.ts +11 -3
  65. package/dist/orchestrator.d.ts.map +1 -1
  66. package/dist/orchestrator.js +341 -414
  67. package/dist/orchestrator.js.map +1 -1
  68. package/dist/persistedPatch.js +2 -2
  69. package/dist/persistedPatch.js.map +1 -1
  70. package/dist/pool-attempt-evidence.d.ts +62 -0
  71. package/dist/pool-attempt-evidence.d.ts.map +1 -0
  72. package/dist/pool-attempt-evidence.js +153 -0
  73. package/dist/pool-attempt-evidence.js.map +1 -0
  74. package/dist/pre-progress-refusal.d.ts +43 -6
  75. package/dist/pre-progress-refusal.d.ts.map +1 -1
  76. package/dist/pre-progress-refusal.js +53 -8
  77. package/dist/pre-progress-refusal.js.map +1 -1
  78. package/dist/readonlyTerminals.d.ts +55 -0
  79. package/dist/readonlyTerminals.d.ts.map +1 -0
  80. package/dist/readonlyTerminals.js +205 -0
  81. package/dist/readonlyTerminals.js.map +1 -0
  82. package/dist/reviewEvidence.d.ts +4 -0
  83. package/dist/reviewEvidence.d.ts.map +1 -0
  84. package/dist/reviewEvidence.js +46 -0
  85. package/dist/reviewEvidence.js.map +1 -0
  86. package/dist/rotation-predicate.d.ts +7 -23
  87. package/dist/rotation-predicate.d.ts.map +1 -1
  88. package/dist/rotation-predicate.js +45 -0
  89. package/dist/rotation-predicate.js.map +1 -1
  90. package/dist/runEventLog.d.ts +1 -0
  91. package/dist/runEventLog.d.ts.map +1 -1
  92. package/dist/runEventLog.js +3 -2
  93. package/dist/runEventLog.js.map +1 -1
  94. package/dist/runScope.d.ts +86 -0
  95. package/dist/runScope.d.ts.map +1 -0
  96. package/dist/runScope.js +181 -0
  97. package/dist/runScope.js.map +1 -0
  98. package/dist/runTerminalContext.d.ts +4 -0
  99. package/dist/runTerminalContext.d.ts.map +1 -1
  100. package/dist/runTerminalContext.js.map +1 -1
  101. package/dist/runTerminalHelpers.d.ts +50 -0
  102. package/dist/runTerminalHelpers.d.ts.map +1 -0
  103. package/dist/runTerminalHelpers.js +200 -0
  104. package/dist/runTerminalHelpers.js.map +1 -0
  105. package/dist/runTerminalResults.d.ts +25 -3
  106. package/dist/runTerminalResults.d.ts.map +1 -1
  107. package/dist/runTerminalResults.js +61 -29
  108. package/dist/runTerminalResults.js.map +1 -1
  109. package/dist/runTerminals.d.ts +1 -12
  110. package/dist/runTerminals.d.ts.map +1 -1
  111. package/dist/runTerminals.js +113 -140
  112. package/dist/runTerminals.js.map +1 -1
  113. package/dist/secretDiff.d.ts +14 -11
  114. package/dist/secretDiff.d.ts.map +1 -1
  115. package/dist/secretDiff.js +21 -28
  116. package/dist/secretDiff.js.map +1 -1
  117. package/dist/terminalOutcome.d.ts +24 -4
  118. package/dist/terminalOutcome.d.ts.map +1 -1
  119. package/dist/terminalOutcome.js +32 -5
  120. package/dist/terminalOutcome.js.map +1 -1
  121. package/package.json +17 -17
@@ -1,14 +1,16 @@
1
1
  import { applyWorkEnvelope, promptWithPointers as renderPromptWithPointers, threadContextPointer, } from "./prompt-framing.js";
2
2
  import { requestRefusalFailure } from "./runTerminalResults.js";
3
+ import { openRunScopeCustody, scopedRouteContext, scopedSpecEnv, } from "./runScope.js";
3
4
  import { delegationBeltFor } from "./delegationBelt.js";
4
5
  import { bindProcessingAdmission, processingAttemptCostEvidence, processingAdmissionForLease, updateProcessingStreamHold, ProcessingBudgetAdmissionError, } from "./processing-dispatch.js";
5
6
  import { cancelledCandidatesResult, emptyCandidateResult, failedCandidatesResult, failAfterCandidates, } from "./candidateTerminals.js";
6
- import { existsSync } from "node:fs";
7
+ import { deepScanFailureResult, readOnlyChainFailureResult, } from "./readonlyTerminals.js";
7
8
  import { effectiveAuthPreference, observeNativeSessionEvent, resumeSessionForProfile, } from "./credential-profiles.js";
8
9
  import { AdmissionProfileProbes, OrchestratorCredentials, reviewerProfileResolver, } from "./orchestrator-credentials.js";
9
10
  import { continuityWorkspace, resumableOf, resumableTerminal, } from "./continuity-terminal.js";
10
11
  import { openContinuity } from "./continue-from.js";
11
12
  import { accountPoolRows } from "./account-pool.js";
13
+ import { PoolAttemptEvidence, terminalPoolAttempts, withPoolAttempts, } from "./pool-attempt-evidence.js";
12
14
  import { writeRunTelemetryArtifact } from "./runTelemetryWriter.js";
13
15
  import { buildFileBackedSynthesisInput, materializeWinnerOutputs, stageFileBackedContext, writeCandidateAttemptArtifacts, } from "./candidateOutputs.js";
14
16
  import { processAttemptUsage } from "./attemptUsage.js";
@@ -16,7 +18,7 @@ import { captureDirectoryCandidate, directoryHasOutput, directoryCandidateStable
16
18
  import { appliedAttemptFacts, assertDelegatedEvidence, isMutatingAccess, outerBoundaryNotice, scopedHarnessHome, selectLaneHome, followSpecHome, } from "./delegatedHome.js";
17
19
  import * as AC from "./attemptUsageCost.js";
18
20
  import * as custody from "./continuation-custody.js";
19
- import { candidateRoster, candidateStatuses, convergenceOutcomeFacts, isWorkingCandidate, partitionCandidates, toCandidateEvidence, unanimousDeclaredFailure, } from "./candidateEvidence.js";
21
+ import { candidateRoster, candidateStatuses, convergenceOutcomeFacts, isWorkingCandidate, partitionCandidates, toCandidateEvidence, } from "./candidateEvidence.js";
20
22
  import { capabilityIntents } from "@claudexor/gateway";
21
23
  import { reviewCandidateRuns, resolveEngineReview, unreviewedCandidateEvidence, evaluateUnreviewedConvergence, } from "./candidateReview.js";
22
24
  import { join } from "node:path";
@@ -39,7 +41,7 @@ import { resolveRunInputDefaults } from "./run-input-resolution.js";
39
41
  import { beginAnnouncedRun } from "./runEventLog.js";
40
42
  import { arbitrationBudgetOptions } from "./decisionBudget.js";
41
43
  import { buildRevisePrompt } from "./revisePrompt.js";
42
- import { cancelledResult, declaredFailure, failTerminally, guardAnnouncedRun, writeFailure, attemptVendorFailure, cancelReasonFromSignalToken, } from "./runTerminals.js";
44
+ import { cancelledResult, declaredFailure, failTerminally, guardAnnouncedRun, writeFailure, cancelReasonFromSignalToken, } from "./runTerminals.js";
43
45
  import { budgetFailureRecord, classifyBudgetFailure } from "./budgetFailure.js";
44
46
  import { finalizeStructuredOutput } from "./structuredOutput.js";
45
47
  import { admitRun } from "./runAdmission.js";
@@ -56,18 +58,18 @@ import { createAttemptTelemetry, observeAttemptTelemetry, setAttemptOutcome, tel
56
58
  import { newAttemptOutputMarkers } from "./attemptOutputMarkers.js";
57
59
  import * as delegateFailure from "./delegationFailure.js";
58
60
  import * as secretDiff from "./secretDiff.js";
59
- import { dominantHarnessFailureCategory, harnessFailureNextActions } from "./harnessFailure.js";
60
- import { finalizeAttempt, readOnlyNoSuccessTerminal, unrecoveredToolErrorFailure, webEvidenceFailure, } from "./attemptFinalize.js";
61
+ import { finalizeAttempt, unrecoveredToolErrorFailure, webEvidenceFailure, } from "./attemptFinalize.js";
61
62
  import { resolveWorkReportEnvelope, unwrapWorkReportEnvelope, } from "./workReportEnvelope.js";
62
63
  import { buildContinuationPacket, decideContinuation, synthesizeContinuationRequest, } from "./continuation.js";
63
64
  import { interactionChannelFor } from "./interaction.js";
64
65
  import { liveAttempt } from "./live-input.js";
65
- import { gateSpecsFromContract, renderTestsEvidence } from "./contract-gates.js";
66
+ import { finalVerifyRun, gateSpecsFromContract, renderTestsEvidence } from "./contract-gates.js";
66
67
  import { buildTaskContract } from "./task-contract-builder.js";
68
+ import { prepareReviewEvidenceDir, recordReviewEvidenceCleanup } from "./reviewEvidence.js";
67
69
  import { ArtifactStore } from "@claudexor/artifact-store";
68
- import { buildContextPack, rawContextForEnvelope, preflightEvidence, writeEvidencePacket, } from "@claudexor/context";
70
+ import { buildContextPack, rawContextForEnvelope, writeEvidencePacket } from "@claudexor/context";
69
71
  import { WorkspaceManager, captureRawPatchEnvelope, createRevertAnchorFromPatchOrNull, createRevertAnchorOrNull, consumeRawPatchEnvelope, snapshotTree, } from "@claudexor/workspace";
70
- import { blockedDecisionOverride, finalVerifyBlocks, finalVerifyPatch, finalVerifyFiles, verifyAndDeliverFiles, verifyAndDeliver, } from "@claudexor/delivery";
72
+ import { blockedDecisionOverride, finalVerifyBlocks, verifyAndDeliverFiles, verifyAndDeliver, } from "@claudexor/delivery";
71
73
  import { HarnessGateway } from "@claudexor/gateway";
72
74
  import { ReadinessLedger, evaluateConvergence, failureSignature, gatesPassed, reviewCandidate, runGates, } from "@claudexor/review";
73
75
  import { arbitrate } from "@claudexor/arbitration";
@@ -162,39 +164,54 @@ export class Orchestrator {
162
164
  this.routingRationaleByRun.delete(settledRunId);
163
165
  this.deps.delegationBudgetAuthority?.releaseRun(settledRunId);
164
166
  };
167
+ // Process custody (owner 1A, #449; `runScope.ts`): the marker is minted and
168
+ // recorded BEFORE the first launch, rides every spec env (`scopedSpecEnv`),
169
+ // and every announced run HOLDS its terminal until the barrier has ended
170
+ // what still carries it. The reap never changes the verdict.
171
+ const custody = await openRunScopeCustody(this.deps.runScopes, runId);
172
+ resolved = { ...resolved, runScopeMarker: custody.marker };
165
173
  try {
166
174
  return await guardAnnouncedRun(resolved.signal, (announce) => {
175
+ const announced = custody.announced(announce);
167
176
  switch (mode) {
168
177
  case "ask":
169
178
  // `--deep-scan` widens the answer into the bounded multi-scout
170
179
  // research sweep with synthesis (the old `audit --swarm`/`explore`).
171
180
  return resolved.deepScan
172
- ? this.runDeepScan(resolved, announce)
173
- : this.runAsk(resolved, announce);
181
+ ? this.runDeepScan(resolved, announced)
182
+ : this.runAsk(resolved, announced);
174
183
  case "agent":
175
184
  // Engine strategies are FLAGS on agent (v0.9 collapse): `--until-clean`
176
185
  // and `--attempts` select the convergence loop; `--n` selects the race
177
186
  // width; `--create` switches the candidate intent to create_from_scratch.
178
187
  if (resolved.untilClean)
179
- return this.runConvergence(resolved, mode, null, announce);
188
+ return this.runConvergence(resolved, mode, null, announced);
180
189
  if (resolved.attempts !== undefined && resolved.attempts !== null) {
181
- return this.runConvergence(resolved, mode, resolved.attempts, announce);
190
+ return this.runConvergence(resolved, mode, resolved.attempts, announced);
182
191
  }
183
- return this.runRace({ ...resolved, n: resolved.n ?? 1 }, mode, announce);
192
+ return this.runRace({ ...resolved, n: resolved.n ?? 1 }, mode, announced);
184
193
  case "plan":
185
- return this.runPlan(resolved, announce);
194
+ return this.runPlan(resolved, announced);
186
195
  }
187
- }, async ({ runId }) => {
196
+ },
197
+ // The whole-run barrier: drain delegated children, then end the run's
198
+ // process scope and record its outcome before ANY terminal is flushed
199
+ // (a child-drain rejection keeps its error). Bounded by the reap deadline.
200
+ ({ runId }) => custody.barrier(async () => {
188
201
  const authority = this.deps.delegationBudgetAuthority;
189
202
  if (!authority?.hasParent(runId))
190
203
  return;
191
204
  authority.beginParentClose(runId);
192
205
  await authority.waitForChildren(runId);
193
- },
194
- // Single per-run terminalization hook: release the routing-rationale map
195
- // entry on EVERY terminal (incl. a run that died before its telemetry
196
- // writer ran, which is the leak this closes).
197
- releaseRunState);
206
+ }),
207
+ // Single per-run terminalization hook: the custody fallback for a path
208
+ // that never reached the barrier, then release per-run engine state
209
+ // (the delegated-child drain authority is released only AFTER
210
+ // settlement) on EVERY terminal.
211
+ async (settledRunId) => {
212
+ await custody.record();
213
+ releaseRunState(settledRunId);
214
+ });
198
215
  }
199
216
  catch (error) {
200
217
  // A durable startup sink may refuse `run.created` before the strategy can
@@ -206,6 +223,9 @@ export class Orchestrator {
206
223
  }
207
224
  throw error;
208
225
  }
226
+ finally {
227
+ await custody.settleUnannounced();
228
+ }
209
229
  }
210
230
  async resolveReviewers(cwd, runAuthPreference, onIgnoredSetting) {
211
231
  if (this.deps.reviewers)
@@ -1217,7 +1237,7 @@ export class Orchestrator {
1217
1237
  adapter: this.deps.registry.get(harnessId),
1218
1238
  credentialProfile: sessionFields.credential_profile,
1219
1239
  authPreference: sessionFields.auth_preference ?? "auto",
1220
- laneEnv: this.laneHomeEnvFor(runInput, harnessId, profileId) ?? {},
1240
+ laneEnv: scopedSpecEnv(runInput.runScopeMarker, this.laneHomeEnvFor(runInput, harnessId, profileId) ?? {}) ?? {},
1221
1241
  envInheritance: envInheritance(this.config(runInput.repoRoot)),
1222
1242
  signal: runInput.signal,
1223
1243
  processing,
@@ -1293,7 +1313,10 @@ export class Orchestrator {
1293
1313
  * must record what it ran under, not just why it stopped. REQUIRED (no
1294
1314
  * default): a silent fallback here would spawn a delegated attempt on the
1295
1315
  * operator's real home while the record still claimed scoped state. */
1296
- harnessHome, observedPaths = new Set()) {
1316
+ harnessHome, observedPaths = new Set(),
1317
+ /** The caller's, like `harnessHome`: its catch keeps the tries' accounts.
1318
+ * REQUIRED, so no caller can drop them by omission again. */
1319
+ poolEvidence) {
1297
1320
  const adapter = routed.adapter;
1298
1321
  const knobs = this.routeSpecKnobs(routed, contract, modelHint, effortHint);
1299
1322
  // Isolated scoped-home sessions are never retained after disposal.
@@ -1328,6 +1351,9 @@ export class Orchestrator {
1328
1351
  if (routed.browserRequirement.effective && artifactRelativeDir === null) {
1329
1352
  throw new Error("browser artifact ownership marker was not persisted");
1330
1353
  }
1354
+ // The run's scope marker rides every spec env (plus the host facts when the
1355
+ // HOME is scoped), added AFTER the lane/profile home so that patch is kept.
1356
+ const scopedEnv = scopedSpecEnv(runInput?.runScopeMarker, harnessHome.env);
1331
1357
  let spec = HarnessRunSpec.parse({
1332
1358
  session_id: newId("ses"),
1333
1359
  intent,
@@ -1359,7 +1385,7 @@ export class Orchestrator {
1359
1385
  ...(inPlaceEnvelope && sessionFields?.resume_session_id
1360
1386
  ? { resume_session_id: sessionFields.resume_session_id }
1361
1387
  : {}),
1362
- ...(harnessHome.env ? { env: harnessHome.env } : {}),
1388
+ ...(scopedEnv ? { env: scopedEnv } : {}),
1363
1389
  raw_context_packet: rawContextPacket,
1364
1390
  stream_deltas: streamDeltas,
1365
1391
  });
@@ -1396,6 +1422,7 @@ export class Orchestrator {
1396
1422
  let beltFailureDisclosed = false;
1397
1423
  const errors = [];
1398
1424
  let answer = new secretDiff.CountedAnswerAssembly();
1425
+ let currentCapture;
1399
1426
  const retryPolicy = transientRetryPolicy(this.config(contract.repo.root));
1400
1427
  // QA-024: the delegation belt is the ONLY engine-owned extra MCP server
1401
1428
  // injected into an agent lane (the browser MCP rides its own field), so its
@@ -1422,9 +1449,11 @@ export class Orchestrator {
1422
1449
  harnessHome,
1423
1450
  workOrder: prompt,
1424
1451
  firstPrompt: spec.prompt,
1452
+ poolEvidence,
1425
1453
  }), { spec, continuation: runInput?.continuation, store, telemetry }));
1426
1454
  try {
1427
1455
  for (let nativeTry = 0; !signal?.aborted; nativeTry += 1) {
1456
+ currentCapture = undefined;
1428
1457
  // A3 per-try isolation: neither output nor progress markers leak across tries.
1429
1458
  if (nativeTry > 0) {
1430
1459
  answer = new secretDiff.CountedAnswerAssembly();
@@ -1440,9 +1469,8 @@ export class Orchestrator {
1440
1469
  ? AbortSignal.any([signal, attemptAbort.signal])
1441
1470
  : attemptAbort.signal;
1442
1471
  activeSessionId = runSpec.session_id;
1443
- continuity.beginTry(runSpec, nativeTry, attemptAbort);
1444
1472
  // #363: bound before the spawn — a credential change mid-try voids it.
1445
- const refusal = this.credentials.bindPreProgressRefusal(adapter.id, runSpec);
1473
+ const refusal = continuity.beginTry(runSpec, nativeTry, attemptAbort);
1446
1474
  const transientStart = telemetry.transientFailures.length;
1447
1475
  const rateLimitStart = telemetry.rateLimits.length;
1448
1476
  let rawPatch = null;
@@ -1566,30 +1594,42 @@ export class Orchestrator {
1566
1594
  // #31: the centralized retry gate reads the classified `retryable` verdict.
1567
1595
  const sawRetryable = newTransients.some((f) => f.retryable);
1568
1596
  const sawTypedLimit = telemetry.rateLimits.length > rateLimitStart;
1597
+ const tryFacts = {
1598
+ harnessErrored,
1599
+ originalError: errors[0],
1600
+ budgetStopped: streamBudgetDenied,
1601
+ aborted: signal?.aborted === true,
1602
+ requestRefused: Boolean(telemetry.requestRefusal),
1603
+ newTransients,
1604
+ markers: telemetry.outputMarkers,
1605
+ };
1606
+ // Its account row first: an infrastructure failure below never erases it.
1607
+ continuity.settleTry(tryFacts);
1569
1608
  if (directory)
1570
1609
  await captureDirectory();
1571
- const currentDiff = directory ? "" : await wsm.diff(envelope);
1610
+ currentCapture = directory
1611
+ ? undefined
1612
+ : await secretDiff.captureCandidateDiff(wsm, envelope);
1613
+ const currentDiff = secretDiff.capturedDiff(currentCapture);
1572
1614
  const workspaceUnchanged = directory
1573
1615
  ? directoryCapture.files?.noChanges === true
1574
- : currentDiff.trim().length === 0;
1616
+ : currentDiff?.trim().length === 0;
1575
1617
  const deliverableEmpty = workspaceUnchanged && answer.text().length === 0;
1576
1618
  const delivered = !harnessErrored && !deliverableEmpty && !signal?.aborted;
1577
1619
  this.credentials.noteTryServed(refusal, telemetry.outputMarkers, delivered);
1578
1620
  const verdict = await continuity.afterTry({
1621
+ ...tryFacts,
1622
+ // A cancel or deadline that landed during capture still stops the
1623
+ // attempt; the try's own facts above stay its evidence.
1624
+ stopped: signal?.aborted === true,
1579
1625
  runSpec,
1580
1626
  nativeTry,
1581
- harnessErrored,
1582
- budgetStopped: streamBudgetDenied,
1583
- aborted: signal?.aborted === true,
1584
- requestRefused: Boolean(telemetry.requestRefusal),
1585
- newTransients,
1586
1627
  sawTypedLimit,
1587
1628
  sawRetryable,
1588
1629
  answer,
1589
1630
  rawDeliverableEmpty: deliverableEmpty,
1590
1631
  workspaceDiffNonEmpty: !workspaceUnchanged,
1591
- currentDiff,
1592
- markers: telemetry.outputMarkers,
1632
+ ...(currentDiff === undefined ? {} : { currentDiff }),
1593
1633
  lastLimit: telemetry.rateLimits.at(-1) ?? null,
1594
1634
  refusal,
1595
1635
  telemetry,
@@ -1613,8 +1653,13 @@ export class Orchestrator {
1613
1653
  signal?.removeEventListener("abort", onAbort);
1614
1654
  }
1615
1655
  // A pool-exhausted terminal is rotation's verdict, not the transient
1616
- // machinery's — no `route.transient.exhausted` rides along with it.
1617
- if (harnessErrored && !poolExhausted && !processingRefusal && !telemetry.requestRefusal) {
1656
+ // machinery's, and a stopped run exhausted no retries: no
1657
+ // `route.transient.exhausted` rides along with either.
1658
+ if (harnessErrored &&
1659
+ !poolExhausted &&
1660
+ !processingRefusal &&
1661
+ !telemetry.requestRefusal &&
1662
+ !signal?.aborted) {
1618
1663
  emitTransientExhausted((t, p) => log?.emit(t, p), adapter.id, attemptId, telemetry, retryPolicy.maxRetries);
1619
1664
  }
1620
1665
  const attemptStreamEndedMs = Date.now();
@@ -1634,9 +1679,10 @@ export class Orchestrator {
1634
1679
  : await secretDiff.captureCandidateWorkspace({
1635
1680
  ...{ wsm, envelope, inPlace: inPlaceEnvelope, projectRoot: store.repoRoot },
1636
1681
  ...{ answerText, answerMatches: answer.secretLikeMatches() },
1682
+ holder: { runId: paths.runId, runDir: paths.root },
1683
+ incompleteCapture: currentCapture?.captureIncomplete ? currentCapture : undefined,
1637
1684
  });
1638
1685
  const { diff, captureRefusal } = captured;
1639
- harnessErrored = secretDiff.recordCaptureRefusal(captureRefusal, errors, harnessErrored);
1640
1686
  const deliverableEvidence = diff.trim().length > 0 || Boolean(answerText) || directoryHasOutput(directoryCapture.files);
1641
1687
  // Cancelled attempts skip gates: running a 600s-per-gate suite delays the ack
1642
1688
  // and burns compute on a result nobody will adopt. Diff/attempt.yaml
@@ -1650,7 +1696,7 @@ export class Orchestrator {
1650
1696
  }
1651
1697
  const gates = gateSignalAborted
1652
1698
  ? []
1653
- : await runGates(gateSpecsFromContract(contract), {
1699
+ : await runGates(gateSpecsFromContract(contract, runInput?.runScopeMarker), {
1654
1700
  cwd: envelope.worktree_path,
1655
1701
  env: wsm.envFor(envelope),
1656
1702
  signal,
@@ -1695,7 +1741,9 @@ export class Orchestrator {
1695
1741
  if (finalized.outcomeClass === "contract_failure" && unwrapped.contractViolation)
1696
1742
  errors.push(`work_report contract: ${unwrapped.contractViolation}`);
1697
1743
  const deliverablePresent = finalized.deliverablePresent;
1698
- const errored = harnessErrored || webBlocked;
1744
+ if (captureRefusal)
1745
+ errors.push(captureRefusal.detail);
1746
+ const errored = harnessErrored || webBlocked || captureRefusal !== undefined;
1699
1747
  const resumable = await continuity.finish({
1700
1748
  runSpec: spec,
1701
1749
  errored,
@@ -1705,6 +1753,7 @@ export class Orchestrator {
1705
1753
  workState: finalized.workState,
1706
1754
  });
1707
1755
  setAttemptOutcome(telemetry, {
1756
+ captureFailed: captureRefusal !== undefined,
1708
1757
  deliverablePresent,
1709
1758
  gatesPassed: gates.length > 0 ? gatesPassed(gates) : null,
1710
1759
  harnessErrored,
@@ -1818,9 +1867,11 @@ export class Orchestrator {
1818
1867
  probeReadyProfiles: (spec, tried) => runInput
1819
1868
  ? this.credentials.readyProfileIdsForRotation(runInput, adapter.id, spec.credential_profile ?? null, tried, spec.model_hint ?? null)
1820
1869
  : Promise.resolve(new Set()),
1870
+ bindRefusal: (spec, producer) => this.credentials.bindPreProgressRefusal(adapter.id, spec, producer),
1821
1871
  rotationObservations: (spec, transients, refusal) => this.credentials.rotationObservations(adapter, spec, transients, refusal),
1822
1872
  emit: (type, payload) => log?.emit(type, payload),
1823
1873
  route: routed,
1874
+ poolEvidence: lane.poolEvidence,
1824
1875
  newSessionId: () => newId("ses"),
1825
1876
  thread: runInput?.threadId
1826
1877
  ? {
@@ -1842,7 +1893,7 @@ export class Orchestrator {
1842
1893
  const mutatingRun = isMutatingAccess(contract.access.effective_profile);
1843
1894
  const planBrief = verifiedPlanBrief(input);
1844
1895
  const quotaSnapshots = this.quotaSnapshotPreflight();
1845
- const { store, paths, log, ledger } = beginAnnouncedRun({
1896
+ const { store, paths, log, ledger, runContext } = beginAnnouncedRun({
1846
1897
  input,
1847
1898
  contract,
1848
1899
  quotaSnapshots,
@@ -1998,6 +2049,7 @@ export class Orchestrator {
1998
2049
  });
1999
2050
  }
2000
2051
  const runsBySlot = new Array(slots.length);
2052
+ runContext.attempts = () => runsBySlot.filter((run) => run !== undefined);
2001
2053
  // D-16d: one-shot continuation budget shared across the concurrent candidate
2002
2054
  // slots (parity with the read-only chain's single counter). Claimed
2003
2055
  // synchronously (check-then-increment with no await between), so two slots
@@ -2050,8 +2102,9 @@ export class Orchestrator {
2050
2102
  const effectiveWeb = this.discloseWebUpgrade(log, slot.routed, knobs.webPolicy, slot.attemptId);
2051
2103
  let envelope;
2052
2104
  // Declared OUTSIDE the try so the catch below can state what the attempt
2053
- // actually ran under, not merely that it stopped.
2105
+ // actually ran under, not merely that it stopped — and on which accounts.
2054
2106
  let harnessHome;
2107
+ const tried = new PoolAttemptEvidence(adapter.id, slot.attemptId);
2055
2108
  try {
2056
2109
  log.emit("harness.started", {
2057
2110
  harness_id: adapter.id,
@@ -2088,7 +2141,7 @@ export class Orchestrator {
2088
2141
  budgetStopped = true;
2089
2142
  return true;
2090
2143
  }, input, requestedSingleCandidate, // W-C4 deltas: single-candidate chat lane only (racing = noise x N)
2091
- undefined, undefined, harnessHome);
2144
+ undefined, undefined, harnessHome, undefined, tried);
2092
2145
  ledger.settle(slot.lease.lease_id, attemptUsageCostSettlement(run.cost, run.costEstimated, run.attemptId, run.harnessId, run.telemetry.authMode, run.telemetry.usageCost));
2093
2146
  log.emit("harness.completed", {
2094
2147
  harness_id: adapter.id,
@@ -2153,6 +2206,7 @@ export class Orchestrator {
2153
2206
  continuation_of: run.attemptId,
2154
2207
  });
2155
2208
  const contLeaseId = contLease.lease?.lease_id ?? "";
2209
+ const contTried = new PoolAttemptEvidence(adapter.id, contAttemptId);
2156
2210
  try {
2157
2211
  const contRun = await this.runCandidateInEnvelope(slot.routed, envelope, contAttemptId, slot.label, contract, input.prompt, store, paths, wsm, ledger, {
2158
2212
  lease: contLease.lease,
@@ -2166,7 +2220,7 @@ export class Orchestrator {
2166
2220
  return false;
2167
2221
  budgetStopped = true;
2168
2222
  return true;
2169
- }, input, requestedSingleCandidate, undefined, packet.pointerLine ?? undefined, harnessHome);
2223
+ }, input, requestedSingleCandidate, undefined, packet.pointerLine ?? undefined, harnessHome, undefined, contTried);
2170
2224
  ledger.settle(contLeaseId, attemptUsageCostSettlement(contRun.cost, contRun.costEstimated, contRun.attemptId, contRun.harnessId, contRun.telemetry.authMode, contRun.telemetry.usageCost));
2171
2225
  log.emit("harness.completed", {
2172
2226
  harness_id: adapter.id,
@@ -2193,6 +2247,8 @@ export class Orchestrator {
2193
2247
  error: safeErrorMessage(err),
2194
2248
  });
2195
2249
  }
2250
+ // Whichever attempt speaks names both attempts' failed tries, in order.
2251
+ effectiveRun.poolAttempts = [...(run.poolAttempts ?? []), ...contTried.snapshot()];
2196
2252
  }
2197
2253
  else {
2198
2254
  ledger.cancel(contLease.lease?.lease_id ?? "");
@@ -2238,6 +2294,7 @@ export class Orchestrator {
2238
2294
  slot.routed.delegationRequirement,
2239
2295
  ], knobs.model),
2240
2296
  infraPhase,
2297
+ poolAttempts: tried.snapshot(),
2241
2298
  // Keep a TYPED pre-spawn refusal (spent quota window + its reset)
2242
2299
  // alive past this catch; `message` alone would force the terminal to
2243
2300
  // read prose back out.
@@ -2252,10 +2309,17 @@ export class Orchestrator {
2252
2309
  };
2253
2310
  await runParallelCandidates(slots, this.deps.runtimeConcurrencyCaps, runSlot);
2254
2311
  const runs = runsBySlot.filter((r) => r !== undefined);
2312
+ // From here `runs` is the lane's one attempt collection (a synthesis that
2313
+ // returns joins it); a synthesis that threw first leaves only its tries.
2314
+ let thrownSynthesis = null;
2315
+ const history = () => thrownSynthesis ? [...runs, thrownSynthesis] : runs;
2316
+ runContext.attempts = history;
2255
2317
  // Fail-closed terminal: a delegated mutating run whose attempts state
2256
2318
  // neither historical proof nor deliberate absence refuses instead of passing.
2257
2319
  assertDelegatedEvidence(input.delegated === true, candidateAccess, runs);
2258
- const cancelledRaceResult = () => cancelledCandidatesResult({
2320
+ // One candidate-terminal context for every race terminal below; its
2321
+ // telemetry writer reads the live roster, so a later synth attempt counts.
2322
+ const raceTerminal = {
2259
2323
  store,
2260
2324
  paths,
2261
2325
  log,
@@ -2264,6 +2328,11 @@ export class Orchestrator {
2264
2328
  mode,
2265
2329
  ledger,
2266
2330
  runs,
2331
+ writeTelemetry: () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null),
2332
+ };
2333
+ const cancelledRaceResult = () => cancelledCandidatesResult({
2334
+ ...raceTerminal,
2335
+ attempts: history(),
2267
2336
  git: input.workspaceKind !== "directory"
2268
2337
  ? {
2269
2338
  live: input.inPlace === true &&
@@ -2275,7 +2344,6 @@ export class Orchestrator {
2275
2344
  }
2276
2345
  : undefined,
2277
2346
  signal: input.signal,
2278
- writeTelemetry: () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null),
2279
2347
  });
2280
2348
  // Revert divergence fence for the single-candidate in-place path: the
2281
2349
  // candidate mutated the LIVE tree during execution above, so the post-turn
@@ -2302,31 +2370,34 @@ export class Orchestrator {
2302
2370
  }
2303
2371
  const failedDelegation = delegateFailure.dominantRaceCandidateFailure(runs);
2304
2372
  if (failedDelegation) {
2305
- const failure = delegateFailure.candidateFailureTerminal(failedDelegation, "race");
2306
2373
  await disposeReviewEnvelopes();
2307
- if (mutatingRun && failedDelegation.files) {
2308
- await publishDirectoryCandidate({
2309
- files: failedDelegation.files,
2310
- store,
2311
- paths,
2312
- taskId,
2313
- attemptId: failedDelegation.attemptId,
2314
- harnessId: failedDelegation.harnessId,
2315
- facts: makeOutcomeFacts("failed", { noChanges: failedDelegation.files.noChanges }),
2316
- log,
2317
- });
2318
- }
2319
- else if (mutatingRun && input.workspaceKind !== "directory") {
2320
- await delegateFailure.persistFailedInPlaceWorkProduct({
2321
- ...{ store, log, paths, execRoot, preTurnSha, taskId, mode },
2322
- live: input.inPlace === true && failedDelegation.reviewCwd === execRoot,
2323
- run: failedDelegation,
2324
- postTurnSha: earlyPostTurnSha,
2325
- kind: input.create === true ? "new_repo" : "patch",
2374
+ const failedOutput = {
2375
+ ...{ store, log, paths, execRoot, preTurnSha, taskId, mode, run: failedDelegation },
2376
+ live: input.inPlace === true &&
2377
+ input.workspaceKind !== "directory" &&
2378
+ failedDelegation.reviewCwd === execRoot,
2379
+ postTurnSha: earlyPostTurnSha,
2380
+ kind: input.create === true ? "new_repo" : "patch",
2381
+ };
2382
+ // A live candidate whose capture failed (no Delegate failure) ends in the
2383
+ // shared no-working-candidate terminal, as in the isolated race and
2384
+ // convergence: a recorded budget denial, a budget terminal the ledger
2385
+ // settled (stream hard cap, exact overshoot, unverifiable cash) or a
2386
+ // unanimous typed refusal leads, its live receipt carries the same facts,
2387
+ // and the capture facts and inspection actions follow. Any other dominant
2388
+ // capture failure joins it only under a settled budget terminal.
2389
+ if (delegateFailure.candidateFailureKind(failedDelegation) === "capture" &&
2390
+ (failedOutput.live || ledger.terminal() !== null))
2391
+ return failedCandidatesResult({
2392
+ ...raceTerminal,
2393
+ budgetDenial,
2394
+ ...(mutatingRun ? { liveCapture: failedOutput } : {}),
2326
2395
  });
2327
- }
2328
- this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null);
2329
- return failTerminally(log, store, paths, runId, taskId, mode, failure.phase, failure.error, ledger.spend(), failure.metadata);
2396
+ const failure = delegateFailure.candidateFailureTerminal(failedDelegation, "race");
2397
+ if (mutatingRun)
2398
+ await delegateFailure.persistFailedCandidateOutput(failedOutput);
2399
+ raceTerminal.writeTelemetry();
2400
+ return failTerminally(log, store, paths, runId, taskId, mode, failure.phase, failure.error, ledger.spend(), { ...failure.metadata, attempts: runs });
2330
2401
  }
2331
2402
  if (runs.length === 0)
2332
2403
  return emptyCandidateResult({
@@ -2348,18 +2419,7 @@ export class Orchestrator {
2348
2419
  const workingRuns = partitionCandidates(runs).working;
2349
2420
  if (workingRuns.length === 0) {
2350
2421
  await disposeReviewEnvelopes();
2351
- return failedCandidatesResult({
2352
- store,
2353
- paths,
2354
- log,
2355
- runId,
2356
- taskId,
2357
- mode,
2358
- ledger,
2359
- runs,
2360
- budgetDenial,
2361
- writeTelemetry: () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null),
2362
- });
2422
+ return failedCandidatesResult({ ...raceTerminal, budgetDenial });
2363
2423
  }
2364
2424
  // QA-025: only announce that review STARTED when the panel will actually
2365
2425
  // run. A candidate that changed no files is skipped inside reviewRuns; a
@@ -2396,12 +2456,12 @@ export class Orchestrator {
2396
2456
  // candidates (a trivial greeting in agent mode no longer burns two reviewers on
2397
2457
  // "(empty diff)"). Candidates still flow through arbitration/gates so the
2398
2458
  // no_op/answer outcome and gate failures are unchanged.
2399
- evidences = await this.reviewRuns(workingRuns, reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal, this.reservationEstimateUsd(input));
2459
+ evidences = await this.reviewRuns(workingRuns, reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal, this.reservationEstimateUsd(input), false, input.runScopeMarker);
2400
2460
  }
2401
2461
  catch (err) {
2402
2462
  // Review preflight/evidence failures end TERMINALLY with artifacts —
2403
2463
  // never as an escaped throw that orphans the run dir.
2404
- return failAfterCandidates({ log, store, paths, runId, taskId, mode, ledger, runs }, "review", err);
2464
+ return failAfterCandidates(raceTerminal, "review", err);
2405
2465
  }
2406
2466
  finally {
2407
2467
  // Review preflight failures must not leak candidate worktrees.
@@ -2431,6 +2491,7 @@ export class Orchestrator {
2431
2491
  });
2432
2492
  if (lease.granted) {
2433
2493
  let envelope;
2494
+ const synthTried = new PoolAttemptEvidence(synthRouted.adapter.id, "synth");
2434
2495
  try {
2435
2496
  const plan = buildSynthesisPlan(evidences);
2436
2497
  const synthesisInput = buildFileBackedSynthesisInput({
@@ -2459,7 +2520,7 @@ export class Orchestrator {
2459
2520
  budgetStopped = true;
2460
2521
  budgetDenial ??= denial;
2461
2522
  },
2462
- }, candidateAccess, input.onHarnessEvent, input.signal, undefined, input.effort, "synthesize", log, effectiveWeb, interactionChannelFor(input, log, runId, taskId, "synth", synthAdapter.id, synthRouted.supportsInteractive), undefined, input, false, synthesisInput.content, undefined, synthHome);
2523
+ }, candidateAccess, input.onHarnessEvent, input.signal, undefined, input.effort, "synthesize", log, effectiveWeb, interactionChannelFor(input, log, runId, taskId, "synth", synthAdapter.id, synthRouted.supportsInteractive), undefined, input, false, synthesisInput.content, undefined, synthHome, undefined, synthTried);
2463
2524
  ledger.settle(lease.lease?.lease_id ?? "", attemptUsageCostSettlement(run.cost, run.costEstimated, run.attemptId, run.harnessId, run.telemetry.authMode, run.telemetry.usageCost));
2464
2525
  reviewEnvelopes.push(envelope);
2465
2526
  envelope = undefined;
@@ -2468,12 +2529,12 @@ export class Orchestrator {
2468
2529
  runs.push(run);
2469
2530
  try {
2470
2531
  if (isWorkingCandidate(run)) {
2471
- const synthEvidence = await this.reviewRuns([run], reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal, this.reservationEstimateUsd(input));
2532
+ const synthEvidence = await this.reviewRuns([run], reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal, this.reservationEstimateUsd(input), false, input.runScopeMarker);
2472
2533
  evidences.push(...synthEvidence);
2473
2534
  workingRuns.push(run);
2474
2535
  }
2475
2536
  if (input.signal?.aborted) {
2476
- 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);
2537
+ return cancelledResult(log, runId, taskId, mode, paths.root, candidateStatuses(runs), raceTerminal.writeTelemetry, ledger.spend(), input.signal, store, undefined, undefined, history());
2477
2538
  }
2478
2539
  }
2479
2540
  finally {
@@ -2481,6 +2542,9 @@ export class Orchestrator {
2481
2542
  }
2482
2543
  }
2483
2544
  catch (err) {
2545
+ // Not yet in `runs`: the run goes on, so its tries' accounts stay in the history.
2546
+ if (!runs.some((run) => run.attemptId === "synth"))
2547
+ thrownSynthesis = { poolAttempts: synthTried.snapshot() };
2484
2548
  ledger.settle(lease.lease?.lease_id ?? "", AC.attemptFailureCost(err, "synthesis-error").settlement);
2485
2549
  log.emit("harness.completed", {
2486
2550
  attempt_id: "synth",
@@ -2506,7 +2570,7 @@ export class Orchestrator {
2506
2570
  }
2507
2571
  catch (err) {
2508
2572
  // Arbitration throws end terminally with artifacts, never as an orphan.
2509
- return failAfterCandidates({ log, store, paths, runId, taskId, mode, ledger, runs }, "arbitration", err);
2573
+ return failAfterCandidates({ ...raceTerminal, attempts: history() }, "arbitration", err);
2510
2574
  }
2511
2575
  log.emit("arbitration.completed", {
2512
2576
  winner: result.decision.winner,
@@ -2570,9 +2634,7 @@ export class Orchestrator {
2570
2634
  facts.lifecycle === "succeeded" &&
2571
2635
  facts.review !== "blocked" &&
2572
2636
  !input.signal?.aborted) {
2573
- finalVerify = winnerRun.files
2574
- ? await finalVerifyFiles(winnerRun.files, undefined, gateSpecsFromContract(contract), log)
2575
- : await finalVerifyPatch(execRoot, winnerRun, gateSpecsFromContract(contract), log);
2637
+ finalVerify = await finalVerifyRun(execRoot, winnerRun, contract, log, input);
2576
2638
  finalVerifyFailed = finalVerifyBlocks(finalVerify);
2577
2639
  if (finalVerifyFailed)
2578
2640
  facts = { ...facts, checks: "failed", reason: "checks_failed" };
@@ -2588,6 +2650,9 @@ export class Orchestrator {
2588
2650
  });
2589
2651
  store.writeYaml(join(paths.arbitrationDir, "pairwise.yaml"), result.pairwise);
2590
2652
  const decisionPath = join(paths.arbitrationDir, "decision.yaml");
2653
+ // Every attempt's failed tries by the FINAL outcome (delivery below can
2654
+ // still fail the checks); a review or check block is not an account failure.
2655
+ const racePool = () => terminalPoolAttempts(facts, history());
2591
2656
  if (winnerRun) {
2592
2657
  for (const path of materializeWinnerOutputs({
2593
2658
  attemptDir: join(paths.attemptsDir, winnerRun.attemptId),
@@ -2617,7 +2682,7 @@ export class Orchestrator {
2617
2682
  facts.checks !== "failed" &&
2618
2683
  !workStateVetoes(facts);
2619
2684
  if (input.inPlace === true && !inPlaceWinner && directoryAdoptable) {
2620
- const delivered = await verifyAndDeliverFiles(execRoot, winnerRun.files, {}, gateSpecsFromContract(contract), (freshVerify) => finalVerifyBlocks(freshVerify)
2685
+ const delivered = await verifyAndDeliverFiles(execRoot, winnerRun.files, {}, gateSpecsFromContract(contract, input.runScopeMarker), (freshVerify) => finalVerifyBlocks(freshVerify)
2621
2686
  ? (freshVerify.reason ?? "final verify failed before directory race adoption")
2622
2687
  : null, log);
2623
2688
  store.writeYaml(join(paths.finalDir, "delivery_receipt.yaml"), delivered);
@@ -2680,7 +2745,7 @@ export class Orchestrator {
2680
2745
  }
2681
2746
  else if (adoptable) {
2682
2747
  // Protected apply preserves the live tree or reports tree_mutated.
2683
- const applied = await verifyAndDeliver(execRoot, winnerRun.diff, { mode: "apply", protectedApply: true }, gateSpecsFromContract(contract), (freshVerify) => {
2748
+ const applied = await verifyAndDeliver(execRoot, winnerRun.diff, { mode: "apply", protectedApply: true }, gateSpecsFromContract(contract, input.runScopeMarker), (freshVerify) => {
2684
2749
  finalVerify = freshVerify;
2685
2750
  return finalVerifyBlocks(freshVerify)
2686
2751
  ? (freshVerify.reason ?? "final verify failed before race adoption")
@@ -2766,7 +2831,7 @@ export class Orchestrator {
2766
2831
  },
2767
2832
  });
2768
2833
  }
2769
- store.writeText(join(paths.finalDir, "summary.md"), renderSummary(runId, mode, { ...result.decision, facts }, evidences, synth.reason, actualReviewVerified, secretDiff.secretLikeSummaryLine(winnerRun.secretLike)));
2834
+ store.writeText(join(paths.finalDir, "summary.md"), withPoolAttempts(renderSummary(runId, mode, { ...result.decision, facts }, evidences, synth.reason, actualReviewVerified, secretDiff.secretLikeSummaryLine(winnerRun.secretLike)), racePool()));
2770
2835
  // summary.md is a DIAGNOSTIC artifact only (V8/PLAN addendum 2): it no
2771
2836
  // longer carries primary-output authority. A clean applyable success or a
2772
2837
  // winner answer still marks it ready for legacy INV-116 ordering; any
@@ -2792,8 +2857,14 @@ export class Orchestrator {
2792
2857
  // (applyable, no_changes, or not-verified) is an honest completion.
2793
2858
  const needsDecisionTerminal = facts.review === "blocked" || facts.checks === "failed";
2794
2859
  const isFailureTerminal = facts.lifecycle !== "succeeded" || needsDecisionTerminal;
2860
+ const pool = racePool();
2795
2861
  if (deliveryFailureReason && !finalVerifyFailed) {
2796
- writeFailure(store, paths, deliveryRefusalFailure(deliveryFailureReason, paths.root));
2862
+ const refused = deliveryRefusalFailure(deliveryFailureReason, paths.root);
2863
+ writeFailure(store, paths, {
2864
+ ...refused,
2865
+ safeMessage: withPoolAttempts(refused.safeMessage, pool),
2866
+ poolAttempts: pool,
2867
+ });
2797
2868
  }
2798
2869
  else if (finalVerifyFailed) {
2799
2870
  writeFailure(store, paths, {
@@ -2801,9 +2872,10 @@ export class Orchestrator {
2801
2872
  // RunFailure.category is a closed enum; "validation" is the honest
2802
2873
  // bucket (the winner failed re-validation on a fresh base).
2803
2874
  category: "validation",
2804
- safeMessage: finalVerify?.applied_cleanly === null
2875
+ safeMessage: withPoolAttempts(finalVerify?.applied_cleanly === null
2805
2876
  ? `final verify ERRORED before proving the patch against a clean base: ${finalVerify?.reason ?? "verify infrastructure error"}`
2806
- : `final verify failed: ${finalVerify?.reason ?? (finalVerify?.gates_passed === false ? "deterministic gates failed on the fresh verify tree" : "unknown")}`,
2877
+ : `final verify failed: ${finalVerify?.reason ?? (finalVerify?.gates_passed === false ? "deterministic gates failed on the fresh verify tree" : "unknown")}`, pool),
2878
+ poolAttempts: pool,
2807
2879
  runDir: paths.root,
2808
2880
  nextActions: finalVerify?.applied_cleanly === null
2809
2881
  ? [
@@ -2835,11 +2907,12 @@ export class Orchestrator {
2835
2907
  : "internal",
2836
2908
  harnessId: winnerRun?.errored ? winnerRun.harnessId : undefined,
2837
2909
  attemptId: winnerRun?.errored ? winnerRun.attemptId : undefined,
2838
- safeMessage: needsHuman
2910
+ safeMessage: withPoolAttempts(needsHuman
2839
2911
  ? `review escalated to a human decision: ${result.decision.why_winner}`
2840
2912
  : needsDecisionTerminal
2841
2913
  ? `review blocked before apply: ${result.decision.why_winner}`
2842
- : result.decision.why_winner,
2914
+ : result.decision.why_winner, pool),
2915
+ poolAttempts: pool,
2843
2916
  rawDetailRef: winnerRun?.errored
2844
2917
  ? `attempts/${winnerRun.attemptId}/attempt.yaml`
2845
2918
  : undefined,
@@ -2859,7 +2932,7 @@ export class Orchestrator {
2859
2932
  ],
2860
2933
  });
2861
2934
  if (!winnerRun) {
2862
- store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${mode})\n\n- Lifecycle: ${facts.lifecycle}${facts.reason ? ` (${facts.reason})` : ""}\n- Phase: arbitration\n\n${result.decision.why_winner}\n`);
2935
+ store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${mode})\n\n- Lifecycle: ${facts.lifecycle}${facts.reason ? ` (${facts.reason})` : ""}\n- Phase: arbitration\n\n${withPoolAttempts(result.decision.why_winner, pool)}\n`);
2863
2936
  log.emit("output.ready", {
2864
2937
  kind: "summary",
2865
2938
  path: "final/summary.md",
@@ -2875,6 +2948,8 @@ export class Orchestrator {
2875
2948
  lifecycle: facts.lifecycle,
2876
2949
  facts,
2877
2950
  reason: facts.reason,
2951
+ // A work-state veto is still a block: it keeps the earlier refusals.
2952
+ ...(pool.length > 0 ? { pool_attempts: pool } : {}),
2878
2953
  ...resumableTerminal(store, paths, winnerRun ? [winnerRun] : runs, isFailureTerminal),
2879
2954
  });
2880
2955
  }
@@ -2884,6 +2959,7 @@ export class Orchestrator {
2884
2959
  log.emit("run.blocked", {
2885
2960
  lifecycle: facts.lifecycle,
2886
2961
  facts,
2962
+ pool_attempts: pool,
2887
2963
  ...resumableTerminal(store, paths, runs, isFailureTerminal),
2888
2964
  phase: deliveryFailureReason && !finalVerifyFailed
2889
2965
  ? "delivery"
@@ -2899,6 +2975,7 @@ export class Orchestrator {
2899
2975
  facts,
2900
2976
  reason: facts.reason,
2901
2977
  phase: "arbitration",
2978
+ pool_attempts: pool,
2902
2979
  failure_ref: "final/failure.yaml",
2903
2980
  ...resumableTerminal(store, paths, runs, isFailureTerminal),
2904
2981
  });
@@ -2911,7 +2988,7 @@ export class Orchestrator {
2911
2988
  facts,
2912
2989
  winner: result.decision.winner,
2913
2990
  runDir: paths.root,
2914
- summary: result.decision.why_winner,
2991
+ summary: withPoolAttempts(result.decision.why_winner, pool),
2915
2992
  candidates: candidateStatuses(runs),
2916
2993
  decisionPath,
2917
2994
  reviewVerified: actualReviewVerified,
@@ -2954,7 +3031,7 @@ export class Orchestrator {
2954
3031
  transientRetryPolicy: input.transientRetryPolicy ?? transientRetryPolicy(this.config(input.cwd)),
2955
3032
  });
2956
3033
  }
2957
- async reviewRuns(runs, reviewers, reviewVerified, reviewDir, cwd, contract, store, paths, log, ledger, taskId, signal, reservationEstimateUsd, reviewUnchanged = false) {
3034
+ async reviewRuns(runs, reviewers, reviewVerified, reviewDir, cwd, contract, store, paths, log, ledger, taskId, signal, reservationEstimateUsd, reviewUnchanged = false, runScopeMarker) {
2958
3035
  return reviewCandidateRuns({
2959
3036
  runs,
2960
3037
  reviewers,
@@ -2970,50 +3047,17 @@ export class Orchestrator {
2970
3047
  signal,
2971
3048
  reservationEstimateUsd,
2972
3049
  reviewUnchanged,
3050
+ runScopeMarker,
2973
3051
  }, {
2974
3052
  prepareReviewEvidenceDir: this.prepareReviewEvidenceDir.bind(this),
2975
- recordReviewEvidenceCleanup: this.recordReviewEvidenceCleanup.bind(this),
3053
+ recordReviewEvidenceCleanup,
2976
3054
  reviewScoped: this.reviewScoped.bind(this),
2977
3055
  config: this.config.bind(this),
2978
3056
  });
2979
3057
  }
2980
- prepareReviewEvidenceDir(sourceDir, _candidateCwd) {
2981
- // Evidence is an external runtime artifact. ReviewEngine builds a separate
2982
- // reviewer workspace and copies the packet there; writing/copying it into
2983
- // the candidate tree would contaminate the Git diff and, worse, overwrite a
2984
- // user-owned path with the same name.
2985
- if (!existsSync(sourceDir)) {
2986
- throw new Error(`review evidence preflight failed for ${sourceDir}: source packet missing`);
2987
- }
2988
- return this.requireReviewEvidence(sourceDir);
2989
- }
2990
- requireReviewEvidence(dir) {
2991
- const result = preflightEvidence(dir);
2992
- if (result.ok)
2993
- return dir;
2994
- const missing = result.missing.length ? `missing=${result.missing.join(",")}` : "";
2995
- const empty = result.empty.length ? `empty=${result.empty.join(",")}` : "";
2996
- throw new Error(`review evidence preflight failed for ${dir}: ${[missing, empty].filter(Boolean).join(" ")}`);
2997
- }
2998
- cleanupReviewEvidenceDir(_candidateEvidenceDir, _candidateCwd) {
2999
- // No candidate-tree packet exists in v2; external runtime retention is
3000
- // governed by the artifact/journal lifecycle rather than best-effort rm.
3001
- return null;
3002
- }
3003
- recordReviewEvidenceCleanup(store, metadataPath, attemptId, candidateEvidenceDir, candidateCwd) {
3004
- const cleanupMetadata = this.cleanupReviewEvidenceDir(candidateEvidenceDir, candidateCwd);
3005
- if (!cleanupMetadata)
3006
- return;
3007
- try {
3008
- store.writeYaml(metadataPath, {
3009
- ...cleanupMetadata,
3010
- attempt_id: attemptId,
3011
- });
3012
- }
3013
- catch {
3014
- // Cleanup telemetry must not mask the review/revalidation failure that
3015
- // triggered best-effort cleanup.
3016
- }
3058
+ /** The instance method is the seam tests stub to make evidence setup fail. */
3059
+ prepareReviewEvidenceDir(sourceDir, candidateCwd) {
3060
+ return prepareReviewEvidenceDir(sourceDir, candidateCwd);
3017
3061
  }
3018
3062
  async runConvergence(input, mode, maxAttempts, announce) {
3019
3063
  const taskId = input.taskId ?? newId("task");
@@ -3022,7 +3066,7 @@ export class Orchestrator {
3022
3066
  const contract = this.buildContract(input, taskId, mode);
3023
3067
  const planBrief = verifiedPlanBrief(input);
3024
3068
  const quotaSnapshots = this.quotaSnapshotPreflight();
3025
- const { store, paths, log, ledger } = beginAnnouncedRun({
3069
+ const { store, paths, log, ledger, runContext } = beginAnnouncedRun({
3026
3070
  input,
3027
3071
  contract,
3028
3072
  quotaSnapshots,
@@ -3159,6 +3203,8 @@ export class Orchestrator {
3159
3203
  let interrupted = false; // D-16 r8: terminalizes the run interrupted
3160
3204
  let lastFindings = [];
3161
3205
  let lastRun = null;
3206
+ const convergenceRuns = [];
3207
+ runContext.attempts = () => convergenceRuns;
3162
3208
  let actualReviewVerified = false;
3163
3209
  let lastFinalReviewClean = false;
3164
3210
  // Honest apply-state for in-place convergence: the attempts mutate the LIVE
@@ -3248,6 +3294,7 @@ export class Orchestrator {
3248
3294
  const effectiveWeb = this.discloseWebUpgrade(log, routed, knobs.webPolicy, attemptId);
3249
3295
  let run;
3250
3296
  let harnessHome;
3297
+ const tried = new PoolAttemptEvidence(adapter.id, attemptId);
3251
3298
  try {
3252
3299
  harnessHome = this.harnessHomeFor(wsm, envelope, routed, input);
3253
3300
  log.emit("harness.started", {
@@ -3266,7 +3313,7 @@ export class Orchestrator {
3266
3313
  ledger.updateHold(lease.lease?.lease_id ?? "", streamedUsd);
3267
3314
  return ledger.tier() === "hard";
3268
3315
  }, input, true, // convergence runs one candidate: live deltas on (W-C4)
3269
- undefined, undefined, harnessHome, observedPaths);
3316
+ undefined, undefined, harnessHome, observedPaths, tried);
3270
3317
  ledger.settle(lease.lease?.lease_id ?? "", attemptUsageCostSettlement(run.cost, run.costEstimated, run.attemptId, run.harnessId, run.telemetry.authMode, run.telemetry.usageCost));
3271
3318
  log.emit("harness.completed", {
3272
3319
  harness_id: adapter.id,
@@ -3303,6 +3350,7 @@ export class Orchestrator {
3303
3350
  errored: true,
3304
3351
  costEstimated: failureCost.estimated,
3305
3352
  errors: [message],
3353
+ poolAttempts: tried.snapshot(),
3306
3354
  ...(declared.code ? { declaredFailure: declared } : {}),
3307
3355
  telemetry: createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required, effectiveWeb, [routed.browserRequirement, routed.denyRequirement, routed.delegationRequirement], knobs.model),
3308
3356
  };
@@ -3318,6 +3366,7 @@ export class Orchestrator {
3318
3366
  }
3319
3367
  }
3320
3368
  lastRun = run;
3369
+ convergenceRuns.push(run);
3321
3370
  // Fail-closed twin of the candidate lane's gate: this loop terminalizes
3322
3371
  // per attempt, so the proof is spent per attempt.
3323
3372
  assertDelegatedEvidence(input.delegated === true, convergenceAccess, [run]);
@@ -3337,34 +3386,28 @@ export class Orchestrator {
3337
3386
  // Cancellation/deadline keeps priority over a belt failure finalized concurrently.
3338
3387
  if (input.signal?.aborted || processingBudgetDenial)
3339
3388
  break;
3389
+ // When this attempt's capture failed (and no Delegate failure leads), a
3390
+ // budget terminal the ledger settled on it (stream hard cap, exact overshoot,
3391
+ // unverifiable cash) outranks that capture failure and its typed refusal
3392
+ // (race parity): the budget-aware finalizer below names the budget cause and
3393
+ // remedy first, with the capture facts, source and inspection actions after.
3394
+ if (delegateFailure.candidateFailureKind(run) === "capture" && ledger.terminal()) {
3395
+ exhausted = true;
3396
+ break;
3397
+ }
3340
3398
  if (run.telemetry.requestRefusal ||
3341
3399
  run.telemetry.effortResolution?.resolution === "rejected")
3342
3400
  break;
3343
3401
  if (delegateFailure.candidateFailureKind(run)) {
3344
3402
  const failure = delegateFailure.candidateFailureTerminal(run, "convergence");
3345
- if (run.files) {
3346
- await publishDirectoryCandidate({
3347
- files: run.files,
3348
- store,
3349
- paths,
3350
- taskId,
3351
- attemptId,
3352
- harnessId: run.harnessId,
3353
- facts: makeOutcomeFacts("failed", { noChanges: run.files.noChanges }),
3354
- log,
3355
- });
3356
- }
3357
- else if (input.workspaceKind !== "directory") {
3358
- await delegateFailure.persistFailedInPlaceWorkProduct({
3359
- ...{ store, log, paths, execRoot, preTurnSha, taskId, mode },
3360
- live: input.inPlace === true,
3361
- run,
3362
- kind: input.create === true ? "new_repo" : "patch",
3363
- attempts: attempt,
3364
- });
3365
- }
3403
+ await delegateFailure.persistFailedCandidateOutput({
3404
+ ...{ store, log, paths, execRoot, preTurnSha, taskId, mode, run },
3405
+ live: input.inPlace === true && input.workspaceKind !== "directory",
3406
+ kind: input.create === true ? "new_repo" : "patch",
3407
+ attempts: attempt,
3408
+ });
3366
3409
  this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, attemptTelemetries, null);
3367
- return failTerminally(log, store, paths, runId, taskId, mode, failure.phase, failure.error, ledger.spend(), failure.metadata);
3410
+ return failTerminally(log, store, paths, runId, taskId, mode, failure.phase, failure.error, ledger.spend(), { ...failure.metadata, attempts: convergenceRuns });
3368
3411
  }
3369
3412
  if (input.workspaceKind === "directory" && run.captureRefusal) {
3370
3413
  return failedCandidatesResult({
@@ -3376,6 +3419,7 @@ export class Orchestrator {
3376
3419
  runId,
3377
3420
  taskId,
3378
3421
  runs: [run],
3422
+ attempts: convergenceRuns,
3379
3423
  writeTelemetry: () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, attemptTelemetries, null),
3380
3424
  });
3381
3425
  }
@@ -3398,7 +3442,7 @@ export class Orchestrator {
3398
3442
  log.emit("review.skipped", { reason: "not_requested", attempt_id: attemptId });
3399
3443
  return evaluateUnreviewedConvergence(evidence, contract);
3400
3444
  }
3401
- const [evidence] = await this.reviewRuns([run], reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal, this.reservationEstimateUsd(input), true);
3445
+ const [evidence] = await this.reviewRuns([run], reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal, this.reservationEstimateUsd(input), true, input.runScopeMarker);
3402
3446
  if (!evidence)
3403
3447
  throw new Error("Convergence review produced no evidence");
3404
3448
  lastFindings = evidence.findings;
@@ -3435,7 +3479,17 @@ export class Orchestrator {
3435
3479
  facts: makeOutcomeFacts("failed", { noChanges: run.files.noChanges }),
3436
3480
  log,
3437
3481
  });
3438
- return failAfterCandidates({ log, store, paths, runId, taskId, mode, ledger, runs: [run] }, "review", err);
3482
+ return failAfterCandidates({
3483
+ log,
3484
+ store,
3485
+ paths,
3486
+ runId,
3487
+ taskId,
3488
+ mode,
3489
+ ledger,
3490
+ runs: [run],
3491
+ attempts: convergenceRuns,
3492
+ }, "review", err);
3439
3493
  }
3440
3494
  if (conv.converged) {
3441
3495
  converged = true;
@@ -3554,11 +3608,12 @@ export class Orchestrator {
3554
3608
  facts = decision.facts;
3555
3609
  }
3556
3610
  }
3557
- if (lastRun?.files)
3611
+ // Captured files decide noChanges; an incomplete capture leaves it unknown.
3612
+ if (lastRun?.files || lastRun?.captureRefusal)
3558
3613
  facts = {
3559
3614
  ...facts,
3560
- noChanges: lastRun.files.noChanges,
3561
- reason: facts.reason === "no_changes" && lastRun.files.noChanges !== true ? null : facts.reason,
3615
+ noChanges: lastRun.files?.noChanges ?? null,
3616
+ reason: facts.reason === "no_changes" && lastRun.files?.noChanges !== true ? null : facts.reason,
3562
3617
  };
3563
3618
  // A budget terminal turns a succeeded lifecycle into a failed one (D8).
3564
3619
  const convBudgetTerminal = ledger.terminal();
@@ -3569,6 +3624,19 @@ export class Orchestrator {
3569
3624
  review_requested: contract.review_requested,
3570
3625
  });
3571
3626
  }
3627
+ // The shared classifier owns a budget terminal's cause and remedy: a
3628
+ // processing re-admission denial, or the ledger's settled terminal once it
3629
+ // stopped the loop over an incomplete capture (the capture facts and
3630
+ // inspection actions follow it in the failure record).
3631
+ const convBudgetMapping = processingBudgetDenial ||
3632
+ (convBudgetTerminal && lastRun?.captureRefusal && isBudgetTerminal(facts.reason))
3633
+ ? classifyBudgetFailure({ denial: processingBudgetDenial, terminal: convBudgetTerminal })
3634
+ : null;
3635
+ if (convBudgetMapping &&
3636
+ facts.lifecycle === "failed" &&
3637
+ isBudgetTerminal(facts.reason) &&
3638
+ facts.reason !== convBudgetMapping.reason)
3639
+ facts = { ...facts, reason: convBudgetMapping.reason };
3572
3640
  // A reviewer escalation to a human forces the REVIEW axis to blocked.
3573
3641
  const needsHuman = lastFindings.some((f) => f.severity === "NEEDS_HUMAN" && isBlocking(f));
3574
3642
  if (needsHuman && facts.lifecycle === "succeeded" && facts.review !== "blocked") {
@@ -3587,13 +3655,13 @@ export class Orchestrator {
3587
3655
  facts.lifecycle === "succeeded" &&
3588
3656
  facts.review !== "blocked" &&
3589
3657
  !input.signal?.aborted) {
3590
- convFinalVerify = lastRun.files
3591
- ? await finalVerifyFiles(lastRun.files, undefined, gateSpecsFromContract(contract), log)
3592
- : await finalVerifyPatch(execRoot, lastRun, gateSpecsFromContract(contract), log);
3658
+ convFinalVerify = await finalVerifyRun(execRoot, lastRun, contract, log, input);
3593
3659
  if (finalVerifyBlocks(convFinalVerify))
3594
3660
  facts = { ...facts, checks: "failed", reason: "checks_failed" };
3595
3661
  }
3596
3662
  const convNeedsDecision = facts.review === "blocked" || facts.checks === "failed";
3663
+ // Every attempt's failed tries by the actual outcome; a block is not an account failure.
3664
+ const convPool = terminalPoolAttempts(facts, convergenceRuns);
3597
3665
  if (decision) {
3598
3666
  // Shared honesty owner (same as the race path): a needs-decision terminal
3599
3667
  // overrides the persisted decision; final_verify is recorded either way.
@@ -3615,16 +3683,13 @@ export class Orchestrator {
3615
3683
  reviewAllowsApply(facts) &&
3616
3684
  facts.checks !== "failed" &&
3617
3685
  !workStateVetoes(facts);
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,
3686
+ if (lastRun.files || lastRun.captureRefusal) {
3687
+ // An incomplete capture has no exact patch: never publish an empty one.
3688
+ await delegateFailure.persistFailedCandidateOutput({
3689
+ ...{ store, log, paths, execRoot, preTurnSha, taskId, mode, facts, run: lastRun },
3690
+ live: input.inPlace === true && input.workspaceKind !== "directory",
3691
+ kind: input.create === true ? "new_repo" : "patch",
3692
+ attempts: attempt,
3628
3693
  });
3629
3694
  }
3630
3695
  else {
@@ -3671,7 +3736,7 @@ export class Orchestrator {
3671
3736
  store.writeText(join(paths.finalDir, "answer.md"), lastRun.answerText);
3672
3737
  log.emit("output.ready", { kind: "answer", path: "final/answer.md" });
3673
3738
  }
3674
- 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}` : ""}${secretDiff.summaryDisclosure(lastRun.secretLike)}\n`);
3739
+ store.writeText(join(paths.finalDir, "summary.md"), `${withPoolAttempts(`# 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}` : ""}${secretDiff.summaryDisclosure(lastRun.secretLike)}`, convPool)}\n`);
3675
3740
  // Lifecycle invariant (all modes): output.ready precedes the terminal
3676
3741
  // event so a client that applied the terminal event has the output.
3677
3742
  log.emit("output.ready", {
@@ -3690,9 +3755,7 @@ export class Orchestrator {
3690
3755
  const convDeclared = !convNeedsDecision && facts.lifecycle === "failed" && !isBudgetTerminal(facts.reason)
3691
3756
  ? lastRun?.declaredFailure
3692
3757
  : undefined;
3693
- const processingBudgetMapping = processingBudgetDenial
3694
- ? classifyBudgetFailure({ denial: processingBudgetDenial, terminal: ledger.terminal() })
3695
- : null;
3758
+ const inspect = secretDiff.captureInspectionActions(lastRun?.captureRefusal);
3696
3759
  writeFailure(store, paths, {
3697
3760
  phase: convNeedsDecision ? "review" : "convergence",
3698
3761
  category: isBudgetTerminal(facts.reason)
@@ -3702,50 +3765,58 @@ export class Orchestrator {
3702
3765
  : convNeedsDecision
3703
3766
  ? "policy"
3704
3767
  : (convDeclared?.category ?? "internal"),
3705
- code: processingBudgetMapping?.code ?? convDeclared?.code ?? null,
3768
+ code: convBudgetMapping?.code ?? convDeclared?.code ?? null,
3706
3769
  resetsAt: convDeclared?.resetsAt ?? null,
3707
3770
  requestRefusal: convDeclared?.requestRefusal,
3708
- safeMessage: convNeedsDecision
3709
- ? `review escalated to a human decision after ${attempt} attempt(s)`
3710
- : facts.reason === "stuck_no_progress"
3711
- ? (stuckNoProgressReason ?? `stuck_no_progress after ${attempt} attempt(s)`)
3712
- : `${facts.lifecycle}${facts.reason ? ` (${facts.reason})` : ""} after ${attempt} attempt(s)${lastDiffStable ? "" : " (diff changed after review; review is stale)"}`,
3713
3771
  harnessId: lastRun?.harnessId,
3714
3772
  attemptId: lastRun?.attemptId,
3715
3773
  runDir: paths.root,
3716
- nextActions: facts.lifecycle === "cancelled"
3717
- ? facts.reason === "wall_clock_exceeded"
3718
- ? [
3719
- "Inspect the partial work kept from before the deadline",
3720
- "Increase --max-seconds or narrow the scope, then re-run",
3721
- ]
3722
- : ["Retry if cancellation was accidental"]
3723
- : convNeedsDecision
3724
- ? [
3725
- "Review the blocking findings on the run's turn",
3726
- "Accept the risk to apply this exact patch, or discard the change",
3727
- ]
3728
- : facts.reason === "stuck_no_progress"
3774
+ captureFailure: lastRun?.captureRefusal?.failure,
3775
+ nextActions: [
3776
+ ...(facts.lifecycle === "cancelled"
3777
+ ? facts.reason === "wall_clock_exceeded"
3778
+ ? [
3779
+ "Inspect the partial work kept from before the deadline",
3780
+ "Increase --max-seconds or narrow the scope, then re-run",
3781
+ ]
3782
+ : ["Retry if cancellation was accidental"]
3783
+ : convNeedsDecision
3729
3784
  ? [
3730
- "Inspect the stable patch",
3731
- "Inspect the failing gate output",
3732
- "Fix the gate or provide a different repair instruction",
3785
+ "Review the blocking findings on the run's turn",
3786
+ "Accept the risk to apply this exact patch, or discard the change",
3733
3787
  ]
3734
- : [
3735
- "Open diagnostics",
3736
- "Inspect latest patch and review findings",
3737
- "Retry with more attempts or a narrower prompt",
3738
- ],
3739
- ...(processingBudgetMapping
3740
- ? budgetFailureRecord(processingBudgetMapping, { runDir: paths.root })
3788
+ : facts.reason === "stuck_no_progress"
3789
+ ? [
3790
+ "Inspect the stable patch",
3791
+ "Inspect the failing gate output",
3792
+ "Fix the gate or provide a different repair instruction",
3793
+ ]
3794
+ : lastRun?.captureRefusal // no latest patch exists to inspect
3795
+ ? secretDiff.captureRefusalNextActions(lastRun.captureRefusal)
3796
+ : [
3797
+ "Open diagnostics",
3798
+ "Inspect latest patch and review findings",
3799
+ "Retry with more attempts or a narrower prompt",
3800
+ ]),
3801
+ ...inspect,
3802
+ ],
3803
+ ...(convBudgetMapping
3804
+ ? budgetFailureRecord(convBudgetMapping, { runDir: paths.root, followUps: inspect })
3741
3805
  : {}),
3806
+ safeMessage: withPoolAttempts(convBudgetMapping?.safeMessage ??
3807
+ (convNeedsDecision
3808
+ ? `review escalated to a human decision after ${attempt} attempt(s)`
3809
+ : facts.reason === "stuck_no_progress"
3810
+ ? (stuckNoProgressReason ?? `stuck_no_progress after ${attempt} attempt(s)`)
3811
+ : `${facts.lifecycle}${facts.reason ? ` (${facts.reason})` : ""} after ${attempt} attempt(s)${lastDiffStable ? "" : " (diff changed after review; review is stale)"}`), convPool),
3812
+ poolAttempts: convPool,
3742
3813
  });
3743
3814
  // D-16 r8/r9: an INTERRUPTED envelope run still gets its diagnostic
3744
3815
  // summary + output.ready (only patch/work_product are withheld) — the
3745
3816
  // ARCHITECTURE event contract guarantees output.ready precedes the
3746
3817
  // terminal in every mode.
3747
3818
  if (!lastRun || (interrupted && input.inPlace !== true && !lastRun.files)) {
3748
- store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${mode})\n\n- Lifecycle: ${facts.lifecycle}${facts.reason ? ` (${facts.reason})` : ""}\n- Attempts: ${attempt}\n`);
3819
+ store.writeText(join(paths.finalDir, "summary.md"), `${withPoolAttempts(`# Run ${runId} (${mode})\n\n- Lifecycle: ${facts.lifecycle}${facts.reason ? ` (${facts.reason})` : ""}\n- Attempts: ${attempt}`, convPool)}\n`);
3749
3820
  log.emit("output.ready", {
3750
3821
  kind: "summary",
3751
3822
  path: "final/summary.md",
@@ -3753,8 +3824,9 @@ export class Orchestrator {
3753
3824
  });
3754
3825
  }
3755
3826
  }
3756
- // work_product.emitted only when a product was actually written (r9).
3757
- if (lastRun && (lastRun.files || !interrupted || input.inPlace === true)) {
3827
+ // work_product.emitted only when a product was actually written (r9); a
3828
+ // capture refusal's live record announces itself.
3829
+ if (lastRun && !lastRun.captureRefusal && (lastRun.files || !interrupted || input.inPlace)) {
3758
3830
  log.emit("work_product.emitted", { winner: lastRun.attemptId });
3759
3831
  }
3760
3832
  if (!convIsFailureTerminal) {
@@ -3764,6 +3836,8 @@ export class Orchestrator {
3764
3836
  facts,
3765
3837
  reason: facts.reason,
3766
3838
  attempts: attempt,
3839
+ // A work-state veto is still a block: it keeps the earlier refusals.
3840
+ ...(convPool.length > 0 ? { pool_attempts: convPool } : {}),
3767
3841
  });
3768
3842
  }
3769
3843
  else if (facts.lifecycle === "succeeded") {
@@ -3773,6 +3847,7 @@ export class Orchestrator {
3773
3847
  facts,
3774
3848
  attempts: attempt,
3775
3849
  phase: "review",
3850
+ pool_attempts: convPool,
3776
3851
  failure_ref: "final/failure.yaml",
3777
3852
  });
3778
3853
  }
@@ -3784,6 +3859,7 @@ export class Orchestrator {
3784
3859
  reason: facts.reason,
3785
3860
  attempts: attempt,
3786
3861
  phase: "convergence",
3862
+ pool_attempts: convPool,
3787
3863
  failure_ref: "final/failure.yaml",
3788
3864
  // QA-041: surface the typed deadline reason on the terminal event so the
3789
3865
  // daemon job result / Control API can distinguish it from a user cancel.
@@ -3806,9 +3882,9 @@ export class Orchestrator {
3806
3882
  ...(facts.lifecycle === "cancelled" && convAbortReason
3807
3883
  ? { cancelReason: convAbortReason }
3808
3884
  : {}),
3809
- summary: converged
3885
+ summary: withPoolAttempts(converged
3810
3886
  ? `converged in ${attempt} attempt(s)`
3811
- : `${facts.lifecycle} after ${attempt} attempt(s)`,
3887
+ : `${facts.lifecycle} after ${attempt} attempt(s)`, convPool),
3812
3888
  candidates: lastRun
3813
3889
  ? [
3814
3890
  {
@@ -3848,9 +3924,9 @@ export class Orchestrator {
3848
3924
  ...sessionFields,
3849
3925
  ...this.harnessSpecKnobs(contract, knobs, args.intent),
3850
3926
  env_inheritance: envInheritance(this.config(input.repoRoot)),
3851
- env: (args.laneRun
3927
+ env: scopedSpecEnv(input.runScopeMarker, (args.laneRun
3852
3928
  ? this.laneHomeEnvFor(input, adapter.id, sessionFields.credential_profile?.profile_id ?? input.credentialProfileId ?? null)
3853
- : null) ?? args.fallbackHome,
3929
+ : null) ?? args.fallbackHome),
3854
3930
  });
3855
3931
  this.credentials.stampProfileSelection(spec, input, adapter.id);
3856
3932
  const plannerAbort = new AbortController();
@@ -3903,9 +3979,9 @@ export class Orchestrator {
3903
3979
  // harnesses run sequentially. Doctor, model inventory, and spawn share each
3904
3980
  // lane's durable HOME; the disposable context remains the non-thread fallback.
3905
3981
  const laneRun = Boolean(input.threadId);
3906
- const roHome = resolveReadOnlyRouteContext(this.execRootOf(input), laneRun
3982
+ const roHome = scopedRouteContext(resolveReadOnlyRouteContext(this.execRootOf(input), laneRun
3907
3983
  ? (id) => this.laneHomeEnvFor(input, id, input.credentialProfileId ?? null)
3908
- : undefined);
3984
+ : undefined), input.runScopeMarker);
3909
3985
  let adapters;
3910
3986
  try {
3911
3987
  adapters = await this.resolveCandidateAdapters({ ...input, n: undefined }, "plan", ledger, log, roHome, runId);
@@ -4190,7 +4266,7 @@ export class Orchestrator {
4190
4266
  */
4191
4267
  deepScanReducerDeps(input, contract, log) {
4192
4268
  return {
4193
- newReadOnlyHome: () => resolveReadOnlyRouteContext(this.execRootOf(input)),
4269
+ newReadOnlyHome: () => scopedRouteContext(resolveReadOnlyRouteContext(this.execRootOf(input)), input.runScopeMarker),
4194
4270
  costEvidence: (harnessId, attemptId, routed) =>
4195
4271
  // The reducer admits under a finite estimate floor (mirror of the n>1
4196
4272
  // scout reserve) so a subscription route is not refused for lacking a
@@ -4213,7 +4289,7 @@ export class Orchestrator {
4213
4289
  resume_session_id: null,
4214
4290
  ...this.harnessSpecKnobs(contract, knobs, "synthesize"),
4215
4291
  env_inheritance: envInheritance(this.config(input.repoRoot)),
4216
- env: homeEnv,
4292
+ env: scopedSpecEnv(input.runScopeMarker, homeEnv),
4217
4293
  });
4218
4294
  this.credentials.stampProfileSelection(spec, input, routed.adapter.id);
4219
4295
  // D-16: compile the WorkReport transport onto the reducer spec (the
@@ -4242,7 +4318,7 @@ export class Orchestrator {
4242
4318
  // recorded user intent is the CALLER's goal.
4243
4319
  const contract = this.buildContract({ ...input, prompt: opts.contractIntent ?? prompt }, taskId, opts.mode);
4244
4320
  const quotaSnapshots = this.quotaSnapshotPreflight();
4245
- const { store, paths, log, ledger } = beginAnnouncedRun({
4321
+ const { store, paths, log, ledger, runContext } = beginAnnouncedRun({
4246
4322
  input,
4247
4323
  contract,
4248
4324
  quotaSnapshots,
@@ -4265,9 +4341,9 @@ export class Orchestrator {
4265
4341
  // and the next lane turn resumes it (INV-034). Its doctor/model/spawn path
4266
4342
  // shares that lane HOME; deep-scan and one-shot asks keep the disposable one.
4267
4343
  const laneRun = Boolean(input.threadId) && opts.mode === "ask" && !opts.deepScan;
4268
- const roHome = resolveReadOnlyRouteContext(this.execRootOf(input), laneRun
4344
+ const roHome = scopedRouteContext(resolveReadOnlyRouteContext(this.execRootOf(input), laneRun
4269
4345
  ? (id) => this.laneHomeEnvFor(input, id, input.credentialProfileId ?? null)
4270
- : undefined);
4346
+ : undefined), input.runScopeMarker);
4271
4347
  let adapters, width;
4272
4348
  try {
4273
4349
  ({ adapters, width } = await resolveReadOnlyCandidates({
@@ -4302,6 +4378,14 @@ export class Orchestrator {
4302
4378
  };
4303
4379
  }
4304
4380
  const attempts = [];
4381
+ // A launched attempt's tries' accounts, owned here until its record joins
4382
+ // `attempts` (as an Agent slot owns them): a throw after its tries settled
4383
+ // still names them on the run's terminal, and never twice.
4384
+ const launched = new Set();
4385
+ runContext.attempts = () => [
4386
+ ...attempts,
4387
+ ...[...launched].map((tried) => ({ poolAttempts: tried.snapshot() })),
4388
+ ];
4305
4389
  const attemptTelemetries = [];
4306
4390
  let fallbackOpen = false;
4307
4391
  let budgetStopped = false;
@@ -4445,9 +4529,9 @@ export class Orchestrator {
4445
4529
  // A thread lane turn spawns in its DURABLE per-lane home so the native
4446
4530
  // session it records is reachable for resume next turn; everything else
4447
4531
  // uses the disposable route-context home.
4448
- env: (laneRun
4532
+ env: scopedSpecEnv(input.runScopeMarker, (laneRun
4449
4533
  ? this.laneHomeEnvFor(input, adapter.id, sessionFields.credential_profile?.profile_id ?? input.credentialProfileId ?? null)
4450
- : null) ?? roHome.env,
4534
+ : null) ?? roHome.env),
4451
4535
  });
4452
4536
  this.credentials.stampProfileSelection(spec, input, adapter.id);
4453
4537
  const reportInteraction = interactionChannelFor(input, log, runId, taskId, attemptId, adapter.id, routed.supportsInteractive);
@@ -4513,12 +4597,15 @@ export class Orchestrator {
4513
4597
  const retryPolicy = transientRetryPolicy(this.config(input.repoRoot));
4514
4598
  let activeSessionId = spec.session_id;
4515
4599
  const live = liveAttempt(input, routed, paths, contract, attemptId, () => activeSessionId);
4600
+ const tried = new PoolAttemptEvidence(adapter.id, attemptId);
4601
+ launched.add(tried);
4516
4602
  let continuity;
4517
4603
  ({ continuity, spec } = await openContinuity(this.continuityDeps(adapter, routed, contract, attemptId, paths, input, log, {
4518
4604
  envelope: null,
4519
4605
  harnessHome: null,
4520
4606
  workOrder: input.prompt,
4521
4607
  firstPrompt: spec.prompt,
4608
+ poolEvidence: tried,
4522
4609
  }), { spec, continuation: input.continuation, store, telemetry }));
4523
4610
  const onAbort = () => void adapter.cancel?.(activeSessionId)?.catch(() => { });
4524
4611
  if (input.signal) {
@@ -4553,9 +4640,8 @@ export class Orchestrator {
4553
4640
  runSpec.extra["abortSignal"] = input.signal
4554
4641
  ? AbortSignal.any([input.signal, tryAbort.signal])
4555
4642
  : tryAbort.signal;
4556
- continuity.beginTry(runSpec, nativeTry, tryAbort);
4557
4643
  // #363: bound before the spawn — a credential change mid-try voids it.
4558
- const refusal = this.credentials.bindPreProgressRefusal(adapter.id, runSpec);
4644
+ const refusal = continuity.beginTry(runSpec, nativeTry, tryAbort);
4559
4645
  const transientStart = telemetry.transientFailures.length;
4560
4646
  const rateLimitStart = telemetry.rateLimits.length;
4561
4647
  log.emit("harness.started", {
@@ -4630,8 +4716,6 @@ export class Orchestrator {
4630
4716
  if (!recordAdapterThrow(telemetry, err))
4631
4717
  break;
4632
4718
  }
4633
- if (streamBudgetDenied)
4634
- break;
4635
4719
  if (telemetry.requestRefusal)
4636
4720
  break;
4637
4721
  const newTransients = telemetry.transientFailures.slice(transientStart);
@@ -4640,10 +4724,14 @@ export class Orchestrator {
4640
4724
  const reportSoFar = answer.text();
4641
4725
  const delivered = !harnessError && reportSoFar.length > 0 && !input.signal?.aborted;
4642
4726
  this.credentials.noteTryServed(refusal, telemetry.outputMarkers, delivered);
4727
+ // A budget stop ends the attempt; typed progress before it still served the account.
4728
+ if (streamBudgetDenied)
4729
+ break;
4643
4730
  const verdict = await continuity.afterTry({
4644
4731
  runSpec,
4645
4732
  nativeTry,
4646
4733
  harnessErrored: harnessError !== null,
4734
+ originalError: harnessError,
4647
4735
  aborted: input.signal?.aborted === true,
4648
4736
  requestRefused: Boolean(telemetry.requestRefusal),
4649
4737
  newTransients,
@@ -4761,6 +4849,7 @@ export class Orchestrator {
4761
4849
  : {}),
4762
4850
  ...continuity.terminalFacts(roResumable),
4763
4851
  });
4852
+ launched.delete(tried);
4764
4853
  if (opts.deepScan) {
4765
4854
  store.writeText(join(paths.findingsDir, `${attemptId}-error.md`), `# Explorer ${attemptId} failed\n\n${harnessError}\n`);
4766
4855
  }
@@ -4786,6 +4875,7 @@ export class Orchestrator {
4786
4875
  ...(scoutInterrupted ? { interrupted: true } : {}),
4787
4876
  ...continuity.terminalFacts(roResumable),
4788
4877
  });
4878
+ launched.delete(tried);
4789
4879
  if (opts.deepScan) {
4790
4880
  const warningNote = toolWarnings(telemetry).length
4791
4881
  ? `\n\n> Tool warnings: ${toolWarnings(telemetry)
@@ -4946,188 +5036,20 @@ export class Orchestrator {
4946
5036
  harnessId: a.harnessId,
4947
5037
  status: a.status,
4948
5038
  }));
4949
- const cancelledTerminal = () => cancelledResult(log, runId, taskId, opts.mode, paths.root, candidateSummaries, () => this.writeRunTelemetry(store, paths, contract, runId, taskId, opts.mode, attemptTelemetries, null), ledger.spend(), input.signal, store, undefined, resumableOf(attempts, "cancelled"));
5039
+ const cancelledTerminal = () => cancelledResult(log, runId, taskId, opts.mode, paths.root, candidateSummaries, () => this.writeRunTelemetry(store, paths, contract, runId, taskId, opts.mode, attemptTelemetries, null), ledger.spend(), input.signal, store, undefined, resumableOf(attempts, "cancelled"), attempts);
4950
5040
  if (input.signal?.aborted)
4951
5041
  return cancelledTerminal();
4952
5042
  const succeededReadonly = attempts.filter((a) => a.status === "success");
4953
- if (!opts.deepScan && succeededReadonly.length === 0) {
4954
- const last = attempts[attempts.length - 1];
4955
- const webBlocked = attempts.some((a) => a.status === "blocked");
4956
- // QA-050: a budget refusal is a BUDGET failure, not a harness one — route
4957
- // it through the shared classifier so phase/category/code/route and the
4958
- // remediation are budget-typed (never auth/setup) across every mode.
4959
- const budgetMapping = budgetStopped && !webBlocked
4960
- ? classifyBudgetFailure({ denial: budgetDenial, terminal: ledger.terminal() })
4961
- : null;
4962
- const singleError = budgetMapping?.safeMessage ??
4963
- last?.error ??
4964
- (budgetStopped ? "budget exhausted before any attempt" : "harness failed");
4965
- if (fallbackOpen || webBlocked) {
4966
- log.emit("route.fallback.exhausted", {
4967
- harness_id: last?.harnessId ?? null,
4968
- attempt_id: last?.attemptId ?? null,
4969
- reason: "web_evidence_unsatisfied",
4970
- error: singleError,
4971
- });
4972
- fallbackOpen = false;
4973
- }
4974
- const partialReport = [...attempts].reverse().find((a) => a.report)?.report ?? "";
4975
- this.writeRunTelemetry(store, paths, contract, runId, taskId, opts.mode, attemptTelemetries, null);
4976
- store.writeText(join(paths.contextDir, "context_error.md"), `# ${budgetMapping ? "Budget Denied" : "Harness Error"}\n\n${singleError}\n`);
4977
- const roEventRefs = attempts.map((a) => `attempts/${a.attemptId}/events.jsonl`);
4978
- if (budgetMapping) {
4979
- writeFailure(store, paths, budgetFailureRecord(budgetMapping, { eventRefs: roEventRefs, runDir: paths.root }));
4980
- }
4981
- else {
4982
- // #31: classify the harness cause across the read-only attempts so auth
4983
- // guidance appears only on a real auth failure.
4984
- const roCategory = dominantHarnessFailureCategory(attemptTelemetries.flatMap((a) => a.telemetry.transientFailures));
4985
- // A5: the terminal already speaks with the LAST attempt's message; its
4986
- // typed refusal rides along machine-readably instead of reducing to it.
4987
- const roDeclared = webBlocked ? undefined : last?.declaredFailure;
4988
- writeFailure(store, paths, {
4989
- phase: "harness",
4990
- category: webBlocked ? "policy" : (roDeclared?.category ?? "harness_error"),
4991
- code: roDeclared?.code ?? null,
4992
- harnessId: last?.harnessId,
4993
- attemptId: last?.attemptId,
4994
- safeMessage: singleError,
4995
- eventRefs: roEventRefs,
4996
- runDir: paths.root,
4997
- resetsAt: roDeclared?.resetsAt ?? null,
4998
- requestRefusal: roDeclared?.requestRefusal,
4999
- vendorFailure: attemptVendorFailure(attemptTelemetries, last?.attemptId),
5000
- nextActions: harnessFailureNextActions(roCategory),
5001
- });
5002
- }
5003
- // QA-036: re-check the DELIVERABLE through the shared finalizer helper —
5004
- // a blocked Ask that produced NO answer can no longer read as a succeeded
5005
- // "Needs review" run (exit 0); it is an honest failure (exit 1).
5006
- const roTerminal = readOnlyNoSuccessTerminal({
5007
- webBlocked,
5008
- hasDeliverable: partialReport.trim().length > 0,
5009
- budgetStopped,
5010
- attemptsCount: attempts.length,
5011
- });
5012
- const terminalFacts = makeOutcomeFacts(roTerminal.lifecycle, {
5013
- ...(roTerminal.review ? { review: roTerminal.review } : {}),
5014
- reason: budgetMapping?.reason ?? roTerminal.reason,
5015
- });
5016
- const terminalHarnessId = budgetMapping?.harnessId ?? last?.harnessId;
5017
- 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`);
5018
- log.emit("output.ready", { kind: "summary", path: "final/summary.md", state: "diagnostic" });
5019
- if (terminalFacts.lifecycle === "succeeded") {
5020
- log.emit("run.blocked", {
5021
- lifecycle: terminalFacts.lifecycle,
5022
- facts: terminalFacts,
5023
- harness_id: terminalHarnessId,
5024
- error: singleError,
5025
- failure_ref: "final/failure.yaml",
5026
- ...resumableTerminal(store, paths, attempts, true),
5027
- });
5028
- }
5029
- else {
5030
- log.emit("run.failed", {
5031
- lifecycle: terminalFacts.lifecycle,
5032
- facts: terminalFacts,
5033
- reason: terminalFacts.reason,
5034
- phase: budgetMapping?.phase,
5035
- harness_id: terminalHarnessId,
5036
- error: singleError,
5037
- failure_ref: "final/failure.yaml",
5038
- ...resumableTerminal(store, paths, attempts, true),
5039
- });
5040
- }
5041
- return {
5042
- spendUsd: ledger.spend(),
5043
- runId,
5044
- taskId,
5045
- mode: opts.mode,
5046
- lifecycle: terminalFacts.lifecycle,
5047
- facts: terminalFacts,
5048
- winner: null,
5049
- runDir: paths.root,
5050
- summary: singleError,
5051
- candidates: attempts.map((a) => ({
5052
- attemptId: a.attemptId,
5053
- harnessId: a.harnessId,
5054
- status: a.status,
5055
- })),
5056
- };
5057
- }
5043
+ const readonlyTerminal = {
5044
+ ...{ store, paths, log, runId, taskId, mode: opts.mode, ledger, attempts },
5045
+ ...{ attemptTelemetries, budgetStopped, budgetDenial },
5046
+ writeTelemetry: () => this.writeRunTelemetry(store, paths, contract, runId, taskId, opts.mode, attemptTelemetries, null),
5047
+ };
5048
+ if (!opts.deepScan && succeededReadonly.length === 0)
5049
+ return readOnlyChainFailureResult({ ...readonlyTerminal, fallbackOpen });
5058
5050
  const succeeded = succeededReadonly;
5059
- if (opts.deepScan && succeeded.length === 0) {
5060
- const blocked = attempts.some((a) => a.status === "blocked");
5061
- // QA-050/QA-019: only a pure-denial scan may claim a budget failure.
5062
- const scanBudgetMapping = budgetStopped && !blocked && attempts.every((a) => a.budgetDenied === true)
5063
- ? classifyBudgetFailure({ denial: budgetDenial, terminal: ledger.terminal() })
5064
- : null;
5065
- // D-16 r8: ALL scouts out of context → the aggregate is interrupted.
5066
- const allInterrupted = attempts.length > 0 && attempts.every((a) => a.interrupted === true);
5067
- const scanFailFacts = allInterrupted
5068
- ? makeOutcomeFacts("interrupted", { reason: "context_capacity_exhausted" })
5069
- : makeOutcomeFacts("failed", {
5070
- reason: scanBudgetMapping ? scanBudgetMapping.reason : "harness_failed",
5071
- });
5072
- const message = scanBudgetMapping
5073
- ? scanBudgetMapping.safeMessage
5074
- : attempts.map((a) => `${a.attemptId}/${a.harnessId}: ${a.error ?? "failed"}`).join("\n");
5075
- this.writeRunTelemetry(store, paths, contract, runId, taskId, opts.mode, attemptTelemetries, null);
5076
- if (scanBudgetMapping) {
5077
- writeFailure(store, paths, budgetFailureRecord(scanBudgetMapping, {
5078
- eventRefs: attempts.map((a) => `attempts/${a.attemptId}/events.jsonl`),
5079
- runDir: paths.root,
5080
- }));
5081
- }
5082
- else {
5083
- // Mixed causes stay generic; only unanimous evidence may name the scan's cause.
5084
- const declared = blocked ? null : unanimousDeclaredFailure(attempts);
5085
- const scanCategory = dominantHarnessFailureCategory(attemptTelemetries.flatMap((a) => a.telemetry.transientFailures));
5086
- writeFailure(store, paths, {
5087
- phase: "harness",
5088
- category: blocked ? "policy" : (declared?.category ?? "harness_error"),
5089
- code: declared?.code,
5090
- resetsAt: declared?.resetsAt,
5091
- requestRefusal: declared?.requestRefusal,
5092
- safeMessage: message || "all explorers failed",
5093
- eventRefs: attempts.map((a) => `attempts/${a.attemptId}/events.jsonl`),
5094
- runDir: paths.root,
5095
- nextActions: [
5096
- ...harnessFailureNextActions(scanCategory).filter((a) => !a.startsWith("Retry")),
5097
- "Reduce explore width",
5098
- "Retry after setup",
5099
- ],
5100
- });
5101
- }
5102
- // QA-036: no successful explorer means no synthesizable deliverable.
5103
- store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${opts.mode})\n\n- Lifecycle: ${scanFailFacts.lifecycle}${scanFailFacts.reason ? ` (${scanFailFacts.reason})` : ""}\n\n${message}\n`);
5104
- log.emit("output.ready", { kind: "summary", path: "final/summary.md", state: "diagnostic" });
5105
- log.emit("run.failed", {
5106
- lifecycle: scanFailFacts.lifecycle,
5107
- facts: scanFailFacts,
5108
- reason: scanFailFacts.reason,
5109
- phase: scanBudgetMapping ? scanBudgetMapping.phase : "harness",
5110
- error: message,
5111
- failure_ref: "final/failure.yaml",
5112
- ...resumableTerminal(store, paths, attempts, true),
5113
- });
5114
- return {
5115
- spendUsd: ledger.spend(),
5116
- runId,
5117
- taskId,
5118
- mode: opts.mode,
5119
- lifecycle: scanFailFacts.lifecycle,
5120
- facts: scanFailFacts,
5121
- winner: null,
5122
- runDir: paths.root,
5123
- summary: message,
5124
- candidates: attempts.map((a) => ({
5125
- attemptId: a.attemptId,
5126
- harnessId: a.harnessId,
5127
- status: a.status,
5128
- })),
5129
- };
5130
- }
5051
+ if (opts.deepScan && succeeded.length === 0)
5052
+ return deepScanFailureResult(readonlyTerminal);
5131
5053
  const unsuccessful = attempts.filter((a) => a.status !== "success");
5132
5054
  // #27 / D-6: a multi-scout deep scan runs ONE bounded synthesis reducer over
5133
5055
  // the raw scout reports so the final artifact is a real merge, not a
@@ -5231,7 +5153,9 @@ export class Orchestrator {
5231
5153
  .map((a) => `${a.attemptId}:${a.harnessId}:${a.status}`)
5232
5154
  .join(", ");
5233
5155
  const secretLike = secretDiff.answerSecretLikeFinding(answerMatches);
5234
- store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${opts.mode})\n\n- Harnesses: ${harnessLabel}\n- Lifecycle: ${terminalFacts.lifecycle}${terminalFacts.reason ? ` (${terminalFacts.reason})` : ""}${secretDiff.summaryDisclosure(secretLike)}\n\n${report}\n`);
5156
+ // A delivered report that still ends unsuccessfully or blocked keeps it and names every failed try.
5157
+ const reportPool = terminalPoolAttempts(terminalFacts, attempts);
5158
+ store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${opts.mode})\n\n- Harnesses: ${harnessLabel}\n- Lifecycle: ${terminalFacts.lifecycle}${terminalFacts.reason ? ` (${terminalFacts.reason})` : ""}${secretDiff.summaryDisclosure(secretLike)}\n\n${withPoolAttempts(report, reportPool)}\n`);
5235
5159
  const reportProducerAttemptId = opts.deepScan &&
5236
5160
  deepScanSynthesis?.status === "succeeded" &&
5237
5161
  deepScanSynthesis.reducer_attempt_id
@@ -5258,7 +5182,8 @@ export class Orchestrator {
5258
5182
  writeFailure(store, paths, {
5259
5183
  phase: "executor",
5260
5184
  category: terminalFacts.reason === "context_capacity_exhausted" ? "harness_error" : "budget",
5261
- safeMessage: `read-only report ended ${terminalFacts.lifecycle}${terminalFacts.reason ? ` (${terminalFacts.reason.replaceAll("_", " ")})` : ""}`,
5185
+ safeMessage: withPoolAttempts(`read-only report ended ${terminalFacts.lifecycle}${terminalFacts.reason ? ` (${terminalFacts.reason.replaceAll("_", " ")})` : ""}`, reportPool),
5186
+ poolAttempts: reportPool,
5262
5187
  runDir: paths.root,
5263
5188
  nextActions: terminalFacts.reason === "context_capacity_exhausted"
5264
5189
  ? ["Inspect the partial report", "Re-run with a narrower scope"]
@@ -5269,6 +5194,7 @@ export class Orchestrator {
5269
5194
  facts: terminalFacts,
5270
5195
  reason: terminalFacts.reason,
5271
5196
  phase: "executor",
5197
+ pool_attempts: reportPool,
5272
5198
  failure_ref: "final/failure.yaml",
5273
5199
  ...resumableTerminal(store, paths, attempts, true),
5274
5200
  });
@@ -5278,6 +5204,7 @@ export class Orchestrator {
5278
5204
  lifecycle: terminalFacts.lifecycle,
5279
5205
  facts: terminalFacts,
5280
5206
  reason: terminalFacts.reason,
5207
+ pool_attempts: reportPool,
5281
5208
  ...resumableTerminal(store, paths, attempts, true),
5282
5209
  });
5283
5210
  }
@@ -5298,7 +5225,7 @@ export class Orchestrator {
5298
5225
  facts: terminalFacts,
5299
5226
  winner: null,
5300
5227
  runDir: paths.root,
5301
- summary: redactSecrets(report).slice(0, 400),
5228
+ summary: withPoolAttempts(redactSecrets(report).slice(0, 400), reportPool),
5302
5229
  candidates: candidateSummaries,
5303
5230
  };
5304
5231
  }