@claudexor/orchestrator 3.0.3 → 3.1.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 (65) hide show
  1. package/LICENSE +1 -1
  2. package/dist/attemptFinalize.d.ts +162 -0
  3. package/dist/attemptFinalize.d.ts.map +1 -0
  4. package/dist/attemptFinalize.js +368 -0
  5. package/dist/attemptFinalize.js.map +1 -0
  6. package/dist/attemptTelemetry.d.ts +88 -7
  7. package/dist/attemptTelemetry.d.ts.map +1 -1
  8. package/dist/attemptTelemetry.js +276 -14
  9. package/dist/attemptTelemetry.js.map +1 -1
  10. package/dist/budgetFailure.d.ts +70 -0
  11. package/dist/budgetFailure.d.ts.map +1 -0
  12. package/dist/budgetFailure.js +114 -0
  13. package/dist/budgetFailure.js.map +1 -0
  14. package/dist/candidateEvidence.d.ts +55 -1
  15. package/dist/candidateEvidence.d.ts.map +1 -1
  16. package/dist/candidateEvidence.js +72 -0
  17. package/dist/candidateEvidence.js.map +1 -1
  18. package/dist/continuation.d.ts +77 -0
  19. package/dist/continuation.d.ts.map +1 -0
  20. package/dist/continuation.js +92 -0
  21. package/dist/continuation.js.map +1 -0
  22. package/dist/deepScanReducer.d.ts +148 -0
  23. package/dist/deepScanReducer.d.ts.map +1 -0
  24. package/dist/deepScanReducer.js +352 -0
  25. package/dist/deepScanReducer.js.map +1 -0
  26. package/dist/harnessFailure.d.ts +27 -0
  27. package/dist/harnessFailure.d.ts.map +1 -0
  28. package/dist/harnessFailure.js +61 -0
  29. package/dist/harnessFailure.js.map +1 -0
  30. package/dist/orchestrator.d.ts +97 -10
  31. package/dist/orchestrator.d.ts.map +1 -1
  32. package/dist/orchestrator.js +1213 -295
  33. package/dist/orchestrator.js.map +1 -1
  34. package/dist/planQuestions.d.ts.map +1 -1
  35. package/dist/planQuestions.js +105 -50
  36. package/dist/planQuestions.js.map +1 -1
  37. package/dist/planRun.d.ts +15 -7
  38. package/dist/planRun.d.ts.map +1 -1
  39. package/dist/planRun.js +88 -42
  40. package/dist/planRun.js.map +1 -1
  41. package/dist/planTerminal.d.ts +36 -0
  42. package/dist/planTerminal.d.ts.map +1 -0
  43. package/dist/planTerminal.js +63 -0
  44. package/dist/planTerminal.js.map +1 -0
  45. package/dist/runSupport.d.ts +50 -2
  46. package/dist/runSupport.d.ts.map +1 -1
  47. package/dist/runSupport.js +60 -6
  48. package/dist/runSupport.js.map +1 -1
  49. package/dist/runTelemetryWriter.d.ts +6 -1
  50. package/dist/runTelemetryWriter.d.ts.map +1 -1
  51. package/dist/runTelemetryWriter.js +2 -0
  52. package/dist/runTelemetryWriter.js.map +1 -1
  53. package/dist/runTerminals.d.ts +15 -4
  54. package/dist/runTerminals.d.ts.map +1 -1
  55. package/dist/runTerminals.js +16 -1
  56. package/dist/runTerminals.js.map +1 -1
  57. package/dist/structuredOutput.d.ts +27 -0
  58. package/dist/structuredOutput.d.ts.map +1 -1
  59. package/dist/structuredOutput.js +65 -7
  60. package/dist/structuredOutput.js.map +1 -1
  61. package/dist/transientClassify.d.ts +59 -0
  62. package/dist/transientClassify.d.ts.map +1 -0
  63. package/dist/transientClassify.js +109 -0
  64. package/dist/transientClassify.js.map +1 -0
  65. package/package.json +17 -17
@@ -3,7 +3,7 @@ import { observeNativeSessionEvent, preflightCredentialProfile, preflightDefault
3
3
  import { writeRunTelemetryArtifact } from "./runTelemetryWriter.js";
4
4
  import { buildFileBackedSynthesisInput, materializeWinnerOutputs, stageFileBackedContext, writeCandidateAttemptArtifacts, } from "./candidateOutputs.js";
5
5
  import { processAttemptUsage } from "./attemptUsage.js";
6
- import { toCandidateEvidence } from "./candidateEvidence.js";
6
+ import { candidateRoster, convergenceOutcomeFacts, isWorkingCandidate, partitionCandidates, toCandidateEvidence, } from "./candidateEvidence.js";
7
7
  import { capabilityIntents } from "@claudexor/gateway";
8
8
  import { policyFindings } from "./policyFindings.js";
9
9
  import { join } from "node:path";
@@ -14,26 +14,31 @@ import { AnswerAssembly, CLAUDEXOR_ARTIFACT_DIR, CLAUDEXOR_BROWSER_ARTIFACT_SUBD
14
14
  import { assertRouteModelsAllowed } from "./modelGovernance.js";
15
15
  import { RequestRequirementsResolver } from "./requestRequirements.js";
16
16
  import { cancelledResult, failTerminally, guardAnnouncedRun, writeFailure, } from "./runTerminals.js";
17
+ import { budgetFailureRecord, classifyBudgetFailure } from "./budgetFailure.js";
17
18
  import { assertOutputSchemaCompiles, finalizeStructuredOutput } from "./structuredOutput.js";
18
- import { transientRetryDelayMs, promptWithProtectedPathConstraint, sleep, redactHarnessEvent, harnessEventPayload, formatFindings, renderSummary, observeBudgetSignals, rotateOnStall, recordCleanAttemptMetrics, envInheritance, transientRetryPolicy, reviewerTimeoutMs, harnessInactivityTimeoutMs, observeAuthSwitch, emitPrimaryDivergence, deliveryRefusalFailure, writeRaceDeliveryDecision, } from "./runSupport.js";
19
+ import { transientRetryDelayMs, promptWithEngineConstraints, sleep, redactHarnessEvent, harnessEventPayload, formatFindings, renderSummary, observeBudgetSignals, rotateOnStall, recordCleanAttemptMetrics, envInheritance, transientRetryPolicy, reviewerTimeoutMs, harnessInactivityTimeoutMs, observeAuthSwitch, emitPrimaryDivergence, emitPoolDegraded, deliveryRefusalFailure, writeRaceDeliveryDecision, } from "./runSupport.js";
19
20
  import { candidateStatusInRouteContext, resolveReadOnlyRouteContext, } from "./routeContext.js";
20
21
  import { resolveAutoReviewerPanel, resolveExplicitReviewerPanel } from "./reviewerPanel.js";
21
22
  import { buildContinuation, } from "./continuity.js";
22
23
  import { activePlanPointer, resolveContinuitySummary, workspaceAnchor, } from "./continuity-facts.js";
23
24
  import { runDiffReview } from "./diffReview.js";
24
- import { createAttemptTelemetry, observeAttemptTelemetry, setAttemptOutcome, telemetrySummary, toolWarnings, unrecoveredToolErrors, webUnsatisfied, } from "./attemptTelemetry.js";
25
+ import { rawScoutBundle, resolveDeepScanSynthesis, } from "./deepScanReducer.js";
26
+ import { classifyAdapterThrow, createAttemptTelemetry, observeAttemptTelemetry, setAttemptOutcome, telemetrySummary, toolWarnings, unrecoveredToolErrors, webUnsatisfied, } from "./attemptTelemetry.js";
27
+ import { dominantHarnessFailureCategory, harnessFailureNextActions } from "./harnessFailure.js";
28
+ import { finalizeAttempt, readOnlyNoSuccessTerminal, resolveWorkReportEnvelope, unwrapWorkReportEnvelope, } from "./attemptFinalize.js";
29
+ import { buildContinuationPacket, decideContinuation, synthesizeContinuationRequest, } from "./continuation.js";
25
30
  import { interactionChannelFor } from "./interaction.js";
26
31
  import { gateSpecsFromContract, renderTestsEvidence, resolveContractGates, } from "./contract-gates.js";
27
32
  import { ArtifactStore } from "@claudexor/artifact-store";
28
33
  import { EventLog } from "@claudexor/event-log";
29
34
  import { assertMandatoryContext, buildContextPack, rawContextForEnvelope, preflightEvidence, writeEvidencePacket, } from "@claudexor/context";
30
- import { WorkspaceManager, captureRawPatchEnvelope, createRevertAnchorFromPatchOrNull, createRevertAnchorOrNull, ensureGitRepository, consumeRawPatchEnvelope, snapshotTree, } from "@claudexor/workspace";
35
+ import { WorkspaceManager, captureRawPatchEnvelope, createRevertAnchorFromPatchOrNull, createRevertAnchorOrNull, ensureClaudeBridge, ensureGitRepository, consumeRawPatchEnvelope, snapshotTree, } from "@claudexor/workspace";
31
36
  import { blockedDecisionOverride, finalVerifyBlocks, finalVerifyPatch, verifyAndDeliver, } from "@claudexor/delivery";
32
37
  import { HarnessGateway } from "@claudexor/gateway";
33
38
  import { ReadinessLedger, evaluateConvergence, failureSignature, gatesPassed, reviewCandidate, revalidateFindings, runGates, } from "@claudexor/review";
34
39
  import { arbitrate } from "@claudexor/arbitration";
35
40
  import { buildSynthesisPlan, decideSynthesis } from "@claudexor/synthesis";
36
- import { attemptCostEvidence, attemptUsageCostSettlement, BudgetLedger, isBudgetTerminal, loadHarnessMetrics, promptFingerprint, unknownCostSettlement, rankHarnesses, reviewUsageCostSettlement, } from "@claudexor/budget";
41
+ import { attemptCostEvidence, attemptUsageCostSettlement, BudgetLedger, isBudgetTerminal, explainRanking, loadHarnessMetrics, promptFingerprint, unknownCostSettlement, rankHarnesses, reviewUsageCostSettlement, } from "@claudexor/budget";
37
42
  import { readTextSafe, appendLine, assertNoInlineSecretValues, containsSecretLikeToken, DELEGATION_ENV, hashJson, newId, noProjectRepoRoot, nowIso, redactSecrets, safeInvoke, sha256, userConfigDir, writeText, } from "@claudexor/util";
38
43
  /** A routed candidate adapter plus its manifest capabilities and user settings. */
39
44
  /** The two access profiles that map to codex `danger-full-access` / an
@@ -42,6 +47,25 @@ import { readTextSafe, appendLine, assertNoInlineSecretValues, containsSecretLik
42
47
  export function isFullAccess(access) {
43
48
  return access === "full" || access === "external_sandbox_full";
44
49
  }
50
+ /**
51
+ * A routing preflight refusal (`RoutingPreflightError`: quality routing with no
52
+ * comparable user-declared tier for the intent) is a CONFIGURATION error, not a
53
+ * harness-availability problem (A-1/D-9/#22). Classifying it as
54
+ * `harness_unavailable` sent the operator to re-auth or wait for a harness; the
55
+ * real fix is to configure a tier or change the routing goal. Detected by the
56
+ * typed `code` (robust across duplicate `@claudexor/budget` package copies) so
57
+ * EVERY strategy's routing catch (ask/agent/plan/deep-scan/council) classifies
58
+ * it identically. Returns the failure category + matching remediation.
59
+ */
60
+ export function routingFailureClassification(err) {
61
+ const isPreflightRefusal = !!err &&
62
+ typeof err === "object" &&
63
+ err.code === "routing_preflight_refused";
64
+ if (isPreflightRefusal) {
65
+ return { category: "config_error", nextActions: harnessFailureNextActions("config_error") };
66
+ }
67
+ return { category: "harness_unavailable" };
68
+ }
45
69
  const LABELS = "ABCDEFGHIJ".split("");
46
70
  const NO_PROJECT_ROOT = noProjectRepoRoot();
47
71
  /** Concurrency cap for parallel candidates/explorers (locked decision: min(n, 4)). */
@@ -68,6 +92,11 @@ export class Orchestrator {
68
92
  deps;
69
93
  gateway;
70
94
  requestRequirements = new RequestRequirementsResolver();
95
+ /** QA-034: the typed routing rationale computed ONCE at pool ordering, keyed
96
+ * by run id so the terminal telemetry writer can record it as run evidence
97
+ * (RunTelemetry.routing_rationale). Cleared when the run's telemetry is
98
+ * written. Absent for runs with an explicit single-harness pool (no ranking). */
99
+ routingRationaleByRun = new Map();
71
100
  /** Per-attempt cap on forwarded live delta chunks (W-C4 flood guard, sol
72
101
  * #10): past this the deltas are dropped and the cutoff is disclosed once;
73
102
  * the complete message always still lands. */
@@ -117,7 +146,7 @@ export class Orchestrator {
117
146
  // every other strategy refuses loudly rather than carrying a contract the
118
147
  // engine would not validate (INV-023). The schema itself is normalized for
119
148
  // the native structured-output routes here at the boundary — unsupported
120
- // shapes ($ref, non-object root) are a typed refusal, not a mid-run 400.
149
+ // shapes (external/cyclic $ref, non-object root) are a typed refusal, not a mid-run 400.
121
150
  if (resolved.outputSchema !== undefined && resolved.outputSchema !== null) {
122
151
  if (mode !== "agent" && mode !== "ask") {
123
152
  throw new Error(`outputSchema constrains the final answer and applies to agent/ask runs (got mode=${mode}); drop the schema or switch modes`);
@@ -128,8 +157,8 @@ export class Orchestrator {
128
157
  // Shape-refuse unsupported schemas, then PROVE it compiles under the same
129
158
  // ajv the engine validator uses — a malformed schema is a preflight
130
159
  // refusal here (before any run dir), never a mid-run validator crash. The
131
- // contract keeps the ORIGINAL (conformance authority); strictify is a
132
- // transport-only transform applied per-lane in harnessSpecKnobs.
160
+ // contract keeps the ORIGINAL (conformance authority); local-ref inlining
161
+ // and strictification are transport-only transforms in harnessSpecKnobs.
133
162
  resolved.outputSchema = normalizeUserOutputSchema(resolved.outputSchema);
134
163
  assertOutputSchemaCompiles(resolved.outputSchema);
135
164
  }
@@ -169,7 +198,11 @@ export class Orchestrator {
169
198
  case "plan":
170
199
  return this.runPlan(resolved, announce);
171
200
  }
172
- });
201
+ },
202
+ // Single per-run terminalization hook: release the routing-rationale map
203
+ // entry on EVERY terminal (incl. a run that died before its telemetry
204
+ // writer ran, which is the leak this closes).
205
+ (runId) => this.routingRationaleByRun.delete(runId));
173
206
  }
174
207
  async resolveReviewers(cwd, runAuthPreference) {
175
208
  if (this.deps.reviewers)
@@ -385,25 +418,38 @@ export class Orchestrator {
385
418
  const cfg = this.config(input.repoRoot);
386
419
  const configuredPool = cfg?.global.routing.eligible_harnesses;
387
420
  const harnesses = input.harnesses ?? (configuredPool && configuredPool.length > 0 ? configuredPool : undefined);
388
- const primaryHarness = input.primaryHarness ?? cfg?.global.routing.primary_harness ?? undefined;
421
+ // GH #25 precedence: an explicit --primary-harness wins and is validated
422
+ // against the pool; else a single-item explicit pool infers itself as
423
+ // primary (shipped in #34); else the configured default primary applies.
424
+ const explicitPrimary = input.primaryHarness;
425
+ const configPrimary = cfg?.global.routing.primary_harness;
426
+ const primaryHarness = explicitPrimary ??
427
+ (input.harnesses?.length === 1 ? input.harnesses[0] : undefined) ??
428
+ configPrimary ??
429
+ undefined;
389
430
  if (primaryHarness &&
390
431
  harnesses &&
391
432
  harnesses.length > 0 &&
392
433
  !harnesses.includes(primaryHarness)) {
393
- throw new Error(`primary harness '${primaryHarness}' is not in the eligible harness pool (${harnesses.join(", ")})`);
434
+ if (explicitPrimary) {
435
+ // An explicit primary must be a member of the eligible pool (authoritative).
436
+ throw new Error(`primary harness '${explicitPrimary}' is not in the eligible harness pool (${harnesses.join(", ")}); ` +
437
+ `pass --primary-harness as one of [${harnesses.join(", ")}], or add '${explicitPrimary}' to --harness`);
438
+ }
439
+ // GH #25 remainder: a MULTI-harness pool whose CONFIGURED default primary
440
+ // is absent, with no --primary-harness pinned, is ambiguous — the engine
441
+ // must not silently reroute. Refuse with a structured, copy-pasteable fix
442
+ // naming the pool, the missing primary, and the exact flag to add.
443
+ throw new HarnessUnavailableError(`ambiguous primary harness: the configured default primary '${primaryHarness}' is not in the selected pool [${harnesses.join(", ")}], ` +
444
+ `and no --primary-harness was given. Pin one explicitly, e.g. \`--primary-harness ${harnesses[0]}\` ` +
445
+ `(or another of [${harnesses.join(", ")}]).`);
394
446
  }
395
447
  if (input.web && input.externalContextPolicy && input.web !== input.externalContextPolicy) {
396
448
  throw new Error(`contradictory web policy: web='${input.web}' vs externalContextPolicy='${input.externalContextPolicy}' (pass one, or equal values)`);
397
449
  }
398
450
  const web = input.web ?? input.externalContextPolicy ?? "auto";
399
- // INV-103: model choice is harness-scoped end-to-end. The scalar
400
- // `model` is a convenience that expands to the RESOLVED PRIMARY only —
401
- // never the whole pool (the old global fallback poisoned every pool
402
- // member with one vendor's model id). Specific beats general: an explicit
403
- // per-harness map entry wins over the scalar.
404
- //
405
- // Map KEYS fail loudly (INV-021): a typo'd harness id ("claud") must
406
- // never silently no-op into "the run used defaults and nothing said why".
451
+ // INV-103: scalar `model` expands only to the resolved primary, never the pool;
452
+ // an explicit per-harness map wins. Unknown map keys fail loudly (INV-021).
407
453
  const knownHarnessIds = new Set(this.deps.registry.keys());
408
454
  for (const key of Object.keys(input.models ?? {})) {
409
455
  if (!knownHarnessIds.has(key)) {
@@ -421,6 +467,19 @@ export class Orchestrator {
421
467
  }
422
468
  models[scalarTarget] ??= input.model;
423
469
  }
470
+ // QA-035: FREEZE the config-derived per-harness default_model into the
471
+ // resolved model map at initial normalization, exactly like an explicit
472
+ // input. Without this the TaskContract records `routing_models: {}` and an
473
+ // Exact Retry re-resolves the model against CURRENT settings — silently
474
+ // changing the route after a settings edit. A per-turn/scalar value already
475
+ // set wins (??=). Only a known resolved pool can be frozen here; a pure
476
+ // auto pool's lanes are not yet known (documented seam).
477
+ const harnessCfg = cfg?.global.harnesses ?? {};
478
+ for (const hid of harnesses ?? []) {
479
+ const def = harnessCfg[hid]?.default_model;
480
+ if (def)
481
+ models[hid] ??= def;
482
+ }
424
483
  return {
425
484
  ...input,
426
485
  harnesses,
@@ -436,7 +495,16 @@ export class Orchestrator {
436
495
  externalContextPolicy: web,
437
496
  };
438
497
  }
439
- async resolveCandidateAdapters(input, intent, ledger, log, routeContext) {
498
+ async resolveCandidateAdapters(input, intent, ledger, log, routeContext,
499
+ /** QA-034: when provided, the pool-ordering rationale is recorded under this
500
+ * run id so the terminal telemetry writer can persist it. */
501
+ runId,
502
+ /** Deep-scan opts in: multi-scout coverage repeats a surviving harness to
503
+ * reach the requested width (distinct SLICES, not distinct harnesses), so a
504
+ * dropped lane must not clamp the scout count. Best-of leaves this false —
505
+ * its width is distinct-harness diversity, so a dropped lane clamps rather
506
+ * than self-races (QA-043). */
507
+ allowDuplicateFill = false) {
440
508
  let ids = input.harnesses;
441
509
  const explicitPool = Boolean(ids && ids.length > 0);
442
510
  const harnessSettings = this.config(input.repoRoot)?.global.harnesses ?? {};
@@ -492,6 +560,21 @@ export class Orchestrator {
492
560
  const policy = input.web ?? input.externalContextPolicy ?? "auto";
493
561
  const pool = [];
494
562
  const dropped = [];
563
+ // Structured requested-vs-effective route receipt (QA-043): every auto-pool
564
+ // drop is recorded with its typed STAGE so the disclosure preserves the
565
+ // real cause instead of collapsing to one reason.
566
+ const droppedLanes = [];
567
+ // The ONE explicit-lane admission gate shared by every drop site (QA-043 /
568
+ // QA-047 meta-move): an EXPLICITLY selected lane that becomes ineligible is
569
+ // a loud typed refusal naming the lane + reason — never a silent
570
+ // substitution or self-race duplication; an AUTO lane is dropped with a
571
+ // typed omission recorded for the degradation receipt.
572
+ const dropLane = (harnessId, stage, detail) => {
573
+ if (explicitPool)
574
+ throw new HarnessUnavailableError(detail);
575
+ dropped.push(detail);
576
+ droppedLanes.push({ harnessId, stage, detail });
577
+ };
495
578
  for (const id of ids) {
496
579
  const adapter = this.deps.registry.get(id);
497
580
  if (!adapter) {
@@ -502,7 +585,7 @@ export class Orchestrator {
502
585
  const known = [...this.deps.registry.keys()].sort().join(", ");
503
586
  throw new HarnessUnavailableError(`unknown harness '${id}' (registered: ${known}); run \`claudexor harness list --all\``);
504
587
  }
505
- dropped.push(`${id} (not registered)`);
588
+ dropLane(id, "discovery", `${id} (not registered)`);
506
589
  continue;
507
590
  }
508
591
  // Per-harness settings: a user-disabled harness never routes. Explicit
@@ -511,9 +594,7 @@ export class Orchestrator {
511
594
  const cfgEntry = harnessSettings[id];
512
595
  if (cfgEntry && cfgEntry.enabled === false) {
513
596
  const why = `${id} is disabled in settings (harnesses.${id}.enabled=false)`;
514
- if (explicitPool)
515
- throw new HarnessUnavailableError(why);
516
- dropped.push(why);
597
+ dropLane(id, "settings", why);
517
598
  continue;
518
599
  }
519
600
  // INV-135 accounts authority: with the native/CLI login excluded and no
@@ -523,9 +604,7 @@ export class Orchestrator {
523
604
  if (this.effectiveProfileId(input, id) === null &&
524
605
  this.nativeCredentialsDisabled(input.repoRoot, id)) {
525
606
  const why = `${id} has no routable credential: the CLI login is disabled (harnesses.${id}.native_credentials_enabled=false) and no account is pinned (--profile)`;
526
- if (explicitPool)
527
- throw new HarnessUnavailableError(why);
528
- dropped.push(why);
607
+ dropLane(id, "credential", why);
529
608
  continue;
530
609
  }
531
610
  // W3.3 (TZ-1 §B): a route is admitted on readiness truth from the SAME
@@ -533,7 +612,12 @@ export class Orchestrator {
533
612
  let status = await candidateStatusInRouteContext(this.gateway, routeContext, id, this.authPreferenceForHarness(input.repoRoot, id, input.authPreference), statusById);
534
613
  const manifest = status?.manifest ?? null;
535
614
  if (!status || !manifest) {
536
- dropped.push(`${id} (unavailable)`);
615
+ // QA-047: an explicit member with no doctor manifest (absent binary /
616
+ // unconfigured provider) is unavailable — it must fail LOUDLY for an
617
+ // explicit pool (naming the real doctor reasons), not vanish before the
618
+ // later explicit-status guard because a healthier lane survived.
619
+ const reasons = status?.reasons?.length ? `: ${status.reasons.join("; ")}` : "";
620
+ dropLane(id, "doctor", `${id} is unavailable${reasons || " (no manifest / not ready)"}`);
537
621
  continue;
538
622
  }
539
623
  // Doctor status is the readiness truth. A DEGRADED harness (e.g. key present but
@@ -570,21 +654,17 @@ export class Orchestrator {
570
654
  }
571
655
  else {
572
656
  const why = `${id} credential profile is not ready: ${profileVerdict}`;
573
- if (explicitPool)
574
- throw new HarnessUnavailableError(why);
575
- dropped.push(why);
657
+ dropLane(id, "credential", why);
576
658
  continue;
577
659
  }
578
660
  }
579
661
  if (status.status === "unavailable" && !profileAdmitted) {
580
662
  const why = `${id} is unavailable${status.reasons.length ? `: ${status.reasons.join("; ")}` : ""}`;
581
- if (explicitPool)
582
- throw new HarnessUnavailableError(why);
583
- dropped.push(why);
663
+ dropLane(id, "doctor", why);
584
664
  continue;
585
665
  }
586
666
  if (status.status !== "ok" && !explicitPool && !profileAdmitted) {
587
- dropped.push(`${id} is ${status.status}${status.reasons.length ? `: ${status.reasons.join("; ")}` : ""}`);
667
+ dropLane(id, "doctor", `${id} is ${status.status}${status.reasons.length ? `: ${status.reasons.join("; ")}` : ""}`);
588
668
  continue;
589
669
  }
590
670
  const readOnlyIntent = intent === "plan" || intent === "spec" || intent === "explain" || intent === "audit";
@@ -607,16 +687,12 @@ export class Orchestrator {
607
687
  (routeWebRequired && (webSupport === "none" || webSupport === "uncontrolled"));
608
688
  if (webIncompatible) {
609
689
  const why = `${id} cannot enforce web policy '${routePolicy}' (manifest web_policy=${webSupport}); choose a web-capable/enforceable harness or change --web to a compatible policy`;
610
- if (explicitPool)
611
- throw new HarnessUnavailableError(why);
612
- dropped.push(why);
690
+ dropLane(id, "web", why);
613
691
  continue;
614
692
  }
615
693
  const attachmentRefusal = this.requestRequirements.attachmentRefusal(id, input.attachments ?? [], manifest.capability_profile.attachment_inputs);
616
694
  if (attachmentRefusal) {
617
- if (explicitPool)
618
- throw new HarnessUnavailableError(attachmentRefusal);
619
- dropped.push(attachmentRefusal);
695
+ dropLane(id, "attachment", attachmentRefusal);
620
696
  continue;
621
697
  }
622
698
  const reason = status.reasons.length > 0 ? `: ${status.reasons.join("; ")}` : "";
@@ -642,8 +718,11 @@ export class Orchestrator {
642
718
  // the default store's sources apply only to profile-less runs.
643
719
  authRouteEstimate: this.profileAuthRoute(input, id) ??
644
720
  estimateEffectiveAuthRoute(this.authPreferenceForHarness(input.repoRoot, id, input.authPreference), status.authSources),
721
+ supportsSynthesize: manifest.capabilities.synthesize,
645
722
  supportsInteractive: manifest.capabilities.interactive,
646
723
  supportsJsonSchemaOutput: manifest.capabilities.json_schema_output,
724
+ workReportTransport: manifest.capabilities.work_report_transport,
725
+ structuredOutputChannel: manifest.capabilities.structured_output_channel,
647
726
  supportsMcpInjection: manifest.capability_profile.mcp_injection,
648
727
  mcpInjectionRequiresFullAccess: manifest.capability_profile.mcp_injection_requires_full_access,
649
728
  implementationTransport: manifest.capabilities.implementation_transport,
@@ -661,21 +740,51 @@ export class Orchestrator {
661
740
  : null,
662
741
  });
663
742
  }
664
- else
665
- dropped.push(`${id} (${accessSupported ? `cannot ${intent}${reason}` : `cannot enforce ${requiredAccess}`})`);
743
+ else {
744
+ // QA-043: an intent- or access-incompatible lane. For an EXPLICIT pool
745
+ // this is a loud refusal (dropLane throws) naming the lane and the
746
+ // exact capability gap — never a silent omission that a surviving lane
747
+ // then masks by modulo self-duplication. The typed stage distinguishes
748
+ // an access refusal from a capability one so the disclosure is honest.
749
+ dropLane(id, accessSupported ? "capability" : "access", `${id} (${accessSupported ? `cannot ${intent}${reason}` : `cannot enforce ${requiredAccess}`})`);
750
+ }
666
751
  }
667
752
  if (pool.length === 0) {
668
753
  throw new HarnessUnavailableError(`no harness can perform '${intent}' for this mode${dropped.length ? ` (skipped: ${dropped.join(", ")})` : ""}`);
669
754
  }
670
- const ordered = this.orderPool(pool, input, intent, statusById, ledger);
755
+ const ordered = this.orderPool(pool, input, intent, statusById, ledger, runId);
671
756
  if (ordered.length === 0) {
672
757
  throw new HarnessUnavailableError(`no harness remains eligible for '${intent}' after budget and quota routing`);
673
758
  }
674
759
  emitPrimaryDivergence(log, input.primaryHarness, ordered, pool, dropped);
675
760
  const n = input.n ?? ordered.length;
676
761
  const out = [];
677
- for (let i = 0; i < n; i++)
678
- out.push(ordered[i % ordered.length]);
762
+ if (droppedLanes.length > 0 && !allowDuplicateFill) {
763
+ // QA-043: lanes were dropped from an AUTO best-of pool (an explicit pool
764
+ // would have thrown at the drop). NEVER refill a dropped lane's slot by
765
+ // duplicating a surviving harness — that manufactures a self-race that
766
+ // masks the omission. Clamp to distinct survivors and disclose below.
767
+ // (Deep-scan sets allowDuplicateFill: its width is scout coverage, not
768
+ // harness diversity, so a dropped lane must not cut the scout count.)
769
+ for (let i = 0; i < Math.min(n, ordered.length); i++)
770
+ out.push(ordered[i]);
771
+ }
772
+ else {
773
+ // No lane was dropped: a pool smaller than `n` is an intentional
774
+ // best-of-N on the available harness(es) (e.g. explicit `--harness codex
775
+ // -n 3`), so the historical width fill is preserved.
776
+ for (let i = 0; i < n; i++)
777
+ out.push(ordered[i % ordered.length]);
778
+ }
779
+ // Disclose an auto-pool omission / width clamp once, with the
780
+ // requested-vs-effective route receipt (never silent — QA-043).
781
+ emitPoolDegraded(log, {
782
+ requestedHarnesses: ids,
783
+ effectiveHarnesses: [...new Set(out.map((lane) => lane.adapter.id))],
784
+ requestedN: n,
785
+ effectiveN: out.length,
786
+ droppedLanes,
787
+ });
679
788
  this.requestRequirements.requireEffectiveBrowser(input.browser === true, out.map((lane) => lane.browserRequirement));
680
789
  // Delegation belt (D32): agent-only, and only on a lane whose adapter can
681
790
  // inject MCP servers. A requested delegate with NO injecting lane is a typed
@@ -730,7 +839,7 @@ export class Orchestrator {
730
839
  * explicit user pool keeps the user's order; an explicit primary harness is
731
840
  * always pinned first. Cross-family diversity is encouraged for later slots.
732
841
  */
733
- orderPool(pool, input, intent, statusById, ledger) {
842
+ orderPool(pool, input, intent, statusById, ledger, runId) {
734
843
  let ordered = pool;
735
844
  if (pool.length > 0) {
736
845
  const routeLedger = ledger ?? new BudgetLedger();
@@ -740,7 +849,8 @@ export class Orchestrator {
740
849
  // Settled cost is evidence for economy routing, never a provider quality prior.
741
850
  const metrics = loadHarnessMetrics(globalConfigDir());
742
851
  const remaining = pool.map((r) => {
743
- const authModes = statusById.get(r.adapter.id)?.manifest?.auth_modes ?? [];
852
+ const status = statusById.get(r.adapter.id);
853
+ const authModes = status?.manifest?.auth_modes ?? [];
744
854
  const metric = metrics[r.adapter.id];
745
855
  // Auth mode for routing: prefer the ROUTE EVIDENCE from the
746
856
  // last settled attempt (adapter-disclosed, persisted in metrics) over
@@ -773,13 +883,23 @@ export class Orchestrator {
773
883
  catch {
774
884
  credentialSubjectId = undefined;
775
885
  }
886
+ // QA-034: the typed auth-route evidence (doctor source verification x the
887
+ // resolved route) is AUTHORITATIVE for billing knowledge in the router —
888
+ // a VERIFIED native route proves subscription_entitlement, so it survives
889
+ // paid_fallback:never and ranks with a real economy tuple instead of
890
+ // reading as unknown/paid. Absent (unknown route) falls back to the
891
+ // metric-derived billingKnowledge below.
892
+ const authRoute = this.authRouteEvidenceFor(authMode, status?.authSources ?? []);
776
893
  return {
777
894
  harnessId: r.adapter.id,
778
895
  available: true,
779
896
  model: input.models?.[r.adapter.id] ??
780
897
  config.harnesses[r.adapter.id]?.default_model ??
781
898
  undefined,
782
- effort: input.effort ?? config.harnesses[r.adapter.id]?.effort ?? undefined,
899
+ effort: input.efforts?.[r.adapter.id] ??
900
+ input.effort ??
901
+ config.harnesses[r.adapter.id]?.effort ??
902
+ undefined,
783
903
  billingKnowledge: authMode === "api_key" ? "metered" : "unknown",
784
904
  incrementalCostUsd: authMode === "api_key" ? (metric?.avg_cost_usd ?? null) : null,
785
905
  credentialRoute: authMode === "api_key"
@@ -787,18 +907,25 @@ export class Orchestrator {
787
907
  : authMode === "local_session"
788
908
  ? "vendor_native"
789
909
  : undefined,
910
+ ...(authRoute ? { authRoute } : {}),
790
911
  credentialSubjectId,
791
912
  };
792
913
  });
793
- const ranked = rankHarnesses(remaining, {
914
+ const routeCtx = {
794
915
  goal,
795
916
  paidFallback: config.routing.paid_fallback,
796
917
  intent,
797
918
  qualityTiers: config.routing.quality_tiers,
798
919
  ledger: routeLedger,
799
- })
920
+ };
921
+ const ranked = rankHarnesses(remaining, routeCtx)
800
922
  .map((candidate) => byId.get(candidate.harnessId))
801
923
  .filter((candidate) => Boolean(candidate));
924
+ // QA-034: record the typed rationale ONCE at pool ordering (run evidence,
925
+ // not an event). Axis-aligned with rankHarnesses above so the persisted
926
+ // reason can never disagree with the order actually taken.
927
+ if (runId)
928
+ this.routingRationaleByRun.set(runId, explainRanking(remaining, routeCtx));
802
929
  ordered = ranked;
803
930
  }
804
931
  if (input.primaryHarness) {
@@ -808,6 +935,30 @@ export class Orchestrator {
808
935
  }
809
936
  return ordered;
810
937
  }
938
+ /**
939
+ * Typed auth-route evidence for one candidate (QA-034): the concrete
940
+ * credential route the resolved auth mode maps to, plus the doctor's
941
+ * verification for the source that route runs under. `local_session` →
942
+ * vendor_native + the native/OAuth source verification; `api_key` →
943
+ * managed_api_key + the key source verification. Unknown route → no evidence
944
+ * (the router keeps its conservative metric-derived billing). Verification is
945
+ * the source's typed verdict — never inferred from mere availability.
946
+ */
947
+ authRouteEvidenceFor(authMode, sources) {
948
+ const usable = (s) => s.availability === "available" && s.verification !== "failed";
949
+ if (authMode === "local_session") {
950
+ const native = sources.find((s) => usable(s) && (s.source === "native_session" || s.source === "oauth_token_env"));
951
+ return { route: "vendor_native", verification: native?.verification ?? "not_run" };
952
+ }
953
+ if (authMode === "api_key") {
954
+ const key = sources.find((s) => usable(s) &&
955
+ (s.source === "api_key_env" ||
956
+ s.source === "api_key_flag" ||
957
+ s.source === "provider_auth_file"));
958
+ return { route: "managed_api_key", verification: key?.verification ?? "not_run" };
959
+ }
960
+ return undefined;
961
+ }
811
962
  /**
812
963
  * Lazy ContextPack: built ONLY for the read-only report modes
813
964
  * (explore/plan/readonly_audit) that consume it. Persisted to
@@ -973,6 +1124,22 @@ export class Orchestrator {
973
1124
  // primary by resolveRunInput). The contract is what route spec building
974
1125
  // reads — there is no run-global model (INV-103).
975
1126
  routing_models: input.models ?? {},
1127
+ // QA-035: freeze the RESOLVED reasoning-effort per known lane so Exact
1128
+ // Retry replays it instead of re-resolving current settings. Precedence
1129
+ // (specific beats general): the harness-scoped `efforts` map entry, then a
1130
+ // per-turn scalar `input.effort`, then the harness settings default — the
1131
+ // same map that Exact Retry replays so a NON-PRIMARY lane keeps its own
1132
+ // frozen effort (QA-035 completeness). Only known-pool lanes are frozen
1133
+ // here (a pure auto pool's lanes resolve later — documented seam).
1134
+ routing_efforts: Object.fromEntries([...new Set([...(input.harnesses ?? []), ...Object.keys(input.efforts ?? {})])]
1135
+ .map((hid) => [
1136
+ hid,
1137
+ input.efforts?.[hid] ??
1138
+ input.effort ??
1139
+ resolvedCfg.global.harnesses?.[hid]?.effort ??
1140
+ null,
1141
+ ])
1142
+ .filter((entry) => entry[1] !== null)),
976
1143
  });
977
1144
  }
978
1145
  /**
@@ -1044,6 +1211,41 @@ export class Orchestrator {
1044
1211
  : {}),
1045
1212
  };
1046
1213
  }
1214
+ /**
1215
+ * D-16: the WorkReport transport envelope for one route. Called at every
1216
+ * task-producing spec-build site AFTER harnessSpecKnobs so it OVERRIDES the
1217
+ * plain caller-schema transport with the compiled `{work_report, output}`
1218
+ * envelope on capable routes. The returned `mode` is retained by the caller
1219
+ * and handed to `unwrapWorkReportEnvelope` when the answer is finalized.
1220
+ */
1221
+ workReportEnvelopeFor(routed, contract, interactive) {
1222
+ return resolveWorkReportEnvelope({
1223
+ transport: routed.workReportTransport,
1224
+ channel: routed.structuredOutputChannel,
1225
+ supportsJsonSchemaOutput: routed.supportsJsonSchemaOutput,
1226
+ interactive,
1227
+ callerSchema: contract.output_schema ?? null,
1228
+ });
1229
+ }
1230
+ /**
1231
+ * D-16: apply the resolved WorkReport transport to a built spec — set the
1232
+ * envelope output_schema (constrained/side_tool routes) and APPEND the fenced
1233
+ * envelope instruction (validated routes, e.g. cursor). Mutates the spec in
1234
+ * place and returns the mode the answer unwrap consumes. Called at every
1235
+ * task-producing spec-build site so the transport is never wired one-off.
1236
+ */
1237
+ applyWorkEnvelope(spec, workEnvelope) {
1238
+ if (workEnvelope.outputSchema !== undefined)
1239
+ spec.output_schema = workEnvelope.outputSchema;
1240
+ const instruction = workEnvelope.mode.instruction;
1241
+ if (instruction) {
1242
+ spec.instructions =
1243
+ spec.instructions && spec.instructions.trim()
1244
+ ? `${spec.instructions}\n\n${instruction}`
1245
+ : instruction;
1246
+ }
1247
+ return workEnvelope.mode;
1248
+ }
1047
1249
  routeSpecKnobs(routed, contract, overrideModel, effortHint) {
1048
1250
  const s = routed.settings;
1049
1251
  const contractPolicy = contract.external_context.policy;
@@ -1078,7 +1280,11 @@ export class Orchestrator {
1078
1280
  const model = overrideModel ?? contract.routing_models[routed.adapter.id] ?? s?.defaultModel ?? null;
1079
1281
  // Effort disclosure (INV-105): a requested effort on a harness with no
1080
1282
  // declared ladder is DISCLOSED as ignored, never silently dropped.
1081
- let effort = effortHint ?? s?.effort ?? null;
1283
+ // Harness-scoped resolution mirrors the model line above: the contract's
1284
+ // FROZEN per-lane effort (QA-035) is authoritative so Exact Retry replays it
1285
+ // without re-reading settings; a per-attempt `effortHint` (or settings
1286
+ // default) applies only to a lane the contract did not freeze.
1287
+ let effort = contract.routing_efforts[routed.adapter.id] ?? effortHint ?? s?.effort ?? null;
1082
1288
  if (effort && routed.effortLevels.length === 0) {
1083
1289
  ignored.push(`effort=${effort} (manifest capabilities.effort_levels is empty for ${routed.adapter.id})`);
1084
1290
  effort = null;
@@ -1198,7 +1404,11 @@ export class Orchestrator {
1198
1404
  return { pointerLine: null };
1199
1405
  }
1200
1406
  }
1201
- async runCandidateInEnvelope(routed, envelope, attemptId, label, contract, prompt, store, paths, wsm, ledger, access = "workspace_write", onHarnessEvent, signal, modelHint, effortHint, intent = "implement", log, effectiveWebMode, interaction, budgetGuard, runInput, streamDeltas = false, fileBackedContext) {
1407
+ async runCandidateInEnvelope(routed, envelope, attemptId, label, contract, prompt, store, paths, wsm, ledger, access = "workspace_write", onHarnessEvent, signal, modelHint, effortHint, intent = "implement", log, effectiveWebMode, interaction, budgetGuard, runInput, streamDeltas = false, fileBackedContext,
1408
+ /** D-16d: when set, the mechanical continuation checkpoint pointer for a
1409
+ * one-shot fresh-session continuation — appended to the prompt so the model
1410
+ * (and the offline fake) re-grounds in the exhausted attempt's partial work. */
1411
+ continuationPointer) {
1202
1412
  const adapter = routed.adapter;
1203
1413
  const knobs = this.routeSpecKnobs(routed, contract, modelHint, effortHint);
1204
1414
  // Isolated scoped-home sessions are never retained after disposal.
@@ -1212,13 +1422,14 @@ export class Orchestrator {
1212
1422
  const laneContinuity = runInput
1213
1423
  ? await this.resolveContinuity(runInput, adapter.id, sessionFields?.credential_profile?.profile_id ?? runInput.credentialProfileId ?? null, inPlaceEnvelope && !!sessionFields?.resume_session_id, store, paths, envelope.repo_root, log)
1214
1424
  : null;
1215
- const promptWithContinuity = laneContinuity?.pointerLine
1216
- ? `${prompt}\n\n${laneContinuity.pointerLine}`
1217
- : prompt;
1218
1425
  let spec = HarnessRunSpec.parse({
1219
1426
  session_id: newId("ses"),
1220
1427
  intent,
1221
- prompt: promptWithProtectedPathConstraint(promptWithContinuity, contract.constraints.protected_paths, contract.constraints.auto_protected_paths, contract.constraints.protected_path_approvals),
1428
+ // Engine-derived read-only prompt constraints: protected/auto-protected
1429
+ // paths PLUS the exact typed gate argv the run will execute (QA-022 FIX B).
1430
+ prompt: promptWithEngineConstraints([prompt, laneContinuity?.pointerLine, continuationPointer]
1431
+ .filter((s) => typeof s === "string" && s.length > 0)
1432
+ .join("\n\n"), contract.constraints.protected_paths, contract.constraints.auto_protected_paths, contract.constraints.protected_path_approvals, contract.tests.commands),
1222
1433
  attachments: runInput?.attachments ?? [],
1223
1434
  browser: this.requestRequirements.browserSpec(routed.browserRequirement,
1224
1435
  // F4: browser-MCP screenshots land in the claudexor-owned
@@ -1246,6 +1457,10 @@ export class Orchestrator {
1246
1457
  });
1247
1458
  if (interaction)
1248
1459
  spec.extra["interactionChannel"] = interaction;
1460
+ // D-16: compile the WorkReport envelope onto the spec (overriding the plain
1461
+ // caller-schema transport) and keep the mode for the answer unwrap.
1462
+ const workEnvelope = this.workReportEnvelopeFor(routed, contract, Boolean(interaction));
1463
+ const workReportMode = this.applyWorkEnvelope(spec, workEnvelope);
1249
1464
  const inactivityMs = harnessInactivityTimeoutMs(this.config(contract.repo.root));
1250
1465
  const attemptStartedMs = Date.now();
1251
1466
  const budgetSignalState = { quotaPressureDisclosed: false };
@@ -1256,12 +1471,25 @@ export class Orchestrator {
1256
1471
  // W-C4 delta flood budget (per attempt): counts forwarded delta chunks.
1257
1472
  let deltaCount = 0;
1258
1473
  let deltaCutoffDisclosed = false;
1474
+ // QA-024: emit the belt-failure disclosure event at most once per attempt.
1475
+ let beltFailureDisclosed = false;
1259
1476
  const errors = [];
1260
1477
  const answer = new AnswerAssembly();
1261
1478
  const retryPolicy = transientRetryPolicy(this.config(contract.repo.root));
1479
+ // QA-024: the delegation belt is the ONLY engine-owned extra MCP server
1480
+ // injected into an agent lane (the browser MCP rides its own field), so its
1481
+ // presence in the spec marks the belt requested-and-injected for THIS
1482
+ // attempt. A mixed pool leaves it off lanes that cannot host it, so this is
1483
+ // per-attempt truth, not the run-wide --delegate flag.
1484
+ const beltServerName = spec.extra_mcp_servers?.[0]?.name ?? null;
1485
+ // QA-040: the browser MCP is injected under the fixed `browser` namespace
1486
+ // (codex `mcp_servers.browser.*`, claude `mcp__browser__*`). Its presence in
1487
+ // the spec marks the browser armed for THIS attempt — the telemetry fold
1488
+ // then recognizes browser tool calls as trusted live-web evidence.
1489
+ const browserServerName = spec.browser ? "browser" : null;
1262
1490
  const telemetry = createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required ||
1263
1491
  knobs.webPolicy === "cached" ||
1264
- knobs.webPolicy === "live", effectiveWebMode ?? knobs.webPolicy, [routed.browserRequirement, routed.denyRequirement], knobs.model);
1492
+ knobs.webPolicy === "live", effectiveWebMode ?? knobs.webPolicy, [routed.browserRequirement, routed.denyRequirement], knobs.model, beltServerName, browserServerName);
1265
1493
  let activeSessionId = spec.session_id;
1266
1494
  const onAbort = () => {
1267
1495
  void adapter.cancel?.(activeSessionId)?.catch(() => { });
@@ -1341,6 +1569,23 @@ export class Orchestrator {
1341
1569
  observeNativeSessionEvent(runInput, adapter.id, safeEv);
1342
1570
  observeAuthSwitch(log, adapter.id, attemptId, safeEv);
1343
1571
  observeAttemptTelemetry(telemetry, safeEv);
1572
+ // QA-024: the injected delegation belt's MCP server reported `failed`
1573
+ // to start. Disclose it ONCE as a typed run event the moment the
1574
+ // `started` frame reveals it — the harness is about to run without
1575
+ // `mcp__<belt>__*` tools and may degrade to its own native subagent.
1576
+ // The terminal outcome axis (delegationBeltUnavailable) reflects it
1577
+ // too; this event makes the failure visible while the run is live.
1578
+ if (telemetry.delegationBelt.requested &&
1579
+ telemetry.delegationBelt.failed &&
1580
+ !beltFailureDisclosed) {
1581
+ beltFailureDisclosed = true;
1582
+ log?.emit("delegation.belt.unavailable", {
1583
+ attempt_id: attemptId,
1584
+ harness_id: adapter.id,
1585
+ server_name: telemetry.delegationBelt.serverName,
1586
+ reason: "mcp_server_failed_to_start",
1587
+ });
1588
+ }
1344
1589
  // Live plan checklist: forward the adapter's typed plan
1345
1590
  // progress as a run event (LAST WINS; the UI renders the latest).
1346
1591
  if (safeEv.plan_progress) {
@@ -1393,15 +1638,21 @@ export class Orchestrator {
1393
1638
  }
1394
1639
  catch (err) {
1395
1640
  // A throwing adapter must not lose the cost already streamed: record the
1396
- // error here and let the caller settle the REAL accumulated spend.
1641
+ // error here and let the caller settle the REAL accumulated spend. #31:
1642
+ // classify the throw (watchdog timeout vs process crash) so the retry
1643
+ // gate and required-actions read a typed category, not a bare boolean.
1397
1644
  harnessErrored = true;
1398
1645
  errors.push(safeErrorMessage(err));
1646
+ telemetry.transientFailures.push(classifyAdapterThrow({ errorName: err instanceof Error ? err.name : null }));
1399
1647
  }
1400
1648
  finally {
1401
1649
  clearFileBackedContext();
1402
1650
  }
1403
- const transient = telemetry.transientFailures.at(-1) ?? null;
1404
- const sawTransient = telemetry.transientFailures.length > transientStart;
1651
+ const newTransients = telemetry.transientFailures.slice(transientStart);
1652
+ const transient = newTransients.at(-1) ?? null;
1653
+ // #31: the centralized retry gate reads the classified `retryable`, not a
1654
+ // bare "saw any transient" boolean.
1655
+ const sawRetryable = newTransients.some((f) => f.retryable);
1405
1656
  const sawTypedLimit = telemetry.rateLimits.length > rateLimitStart;
1406
1657
  const currentDiff = await wsm.diff(envelope);
1407
1658
  const currentAnswer = answer.text();
@@ -1432,7 +1683,7 @@ export class Orchestrator {
1432
1683
  }
1433
1684
  }
1434
1685
  if (!harnessErrored ||
1435
- !sawTransient ||
1686
+ !sawRetryable ||
1436
1687
  !deliverableEmpty ||
1437
1688
  nativeTry >= retryPolicy.maxRetries ||
1438
1689
  signal?.aborted)
@@ -1443,6 +1694,7 @@ export class Orchestrator {
1443
1694
  harness_id: adapter.id,
1444
1695
  attempt_id: attemptId,
1445
1696
  kind: transient?.kind ?? "unknown",
1697
+ category: transient?.category ?? "unknown_harness_error",
1446
1698
  native_try: nativeTry + 1,
1447
1699
  });
1448
1700
  log?.emit("route.transient.retry_scheduled", {
@@ -1463,6 +1715,7 @@ export class Orchestrator {
1463
1715
  log?.emit("route.transient.exhausted", {
1464
1716
  harness_id: adapter.id,
1465
1717
  attempt_id: attemptId,
1718
+ category: telemetry.transientFailures.at(-1)?.category ?? "unknown_harness_error",
1466
1719
  retries: retryPolicy.maxRetries,
1467
1720
  });
1468
1721
  }
@@ -1471,8 +1724,14 @@ export class Orchestrator {
1471
1724
  errors.push(`web evidence unsatisfied: ${telemetry.web.errorSummary ?? (telemetry.web.attempted ? "web tool failed without verified recovery" : "web evidence required but never attempted")}`);
1472
1725
  }
1473
1726
  const diff = await wsm.diff(envelope);
1474
- const answerText = answer.text() || undefined;
1475
- const deliverablePresent = diff.trim().length > 0 || Boolean(answerText);
1727
+ // D-16: un-nest {work_report, output} so answer.md persists the OUTPUT, not the envelope.
1728
+ const unwrapped = unwrapWorkReportEnvelope(answer.machineText() ?? "", workReportMode, {
1729
+ sideToolReport: telemetry.sideToolWorkReport ?? undefined,
1730
+ });
1731
+ // X119: persist the VERBATIM redacted bytes; trim ONLY for the emptiness check.
1732
+ const redacted = redactSecrets(unwrapped.deliverable);
1733
+ const answerText = redacted.trim().length > 0 ? redacted : undefined;
1734
+ const deliverableEvidence = diff.trim().length > 0 || Boolean(answerText);
1476
1735
  // Cancelled attempts skip gates entirely: the operator asked to
1477
1736
  // stop NOW; running a 600s-per-gate suite after the abort delays the ack
1478
1737
  // and burns compute on a result nobody will adopt. Diff/attempt.yaml
@@ -1507,12 +1766,28 @@ export class Orchestrator {
1507
1766
  });
1508
1767
  }
1509
1768
  const webBlocked = webUnsatisfied(telemetry);
1769
+ // D-16 unified finalizer: fold the WorkReport / context signals into the
1770
+ // deliverable + work_state. A broken contract on a constrained route
1771
+ // elevates harnessErrored (never a prose success).
1772
+ const finalized = finalizeAttempt({
1773
+ deliverableEvidence,
1774
+ harnessErrored,
1775
+ workReport: unwrapped.workReport,
1776
+ workReportSource: unwrapped.source,
1777
+ workReportViolation: unwrapped.contractViolation,
1778
+ contextTerminalExhausted: telemetry.contextExhausted,
1779
+ });
1780
+ harnessErrored = finalized.harnessErrored;
1781
+ if (finalized.outcomeClass === "contract_failure" && unwrapped.contractViolation)
1782
+ errors.push(`work_report contract: ${unwrapped.contractViolation}`);
1783
+ const deliverablePresent = finalized.deliverablePresent;
1510
1784
  const errored = harnessErrored || webBlocked;
1511
1785
  setAttemptOutcome(telemetry, {
1512
1786
  deliverablePresent,
1513
1787
  gatesPassed: gates.length > 0 ? gatesPassed(gates) : null,
1514
1788
  harnessErrored,
1515
1789
  webRequiredUnsatisfied: webBlocked,
1790
+ workState: finalized.workState,
1516
1791
  });
1517
1792
  const attemptDir = join(paths.attemptsDir, attemptId);
1518
1793
  try {
@@ -1565,6 +1840,7 @@ export class Orchestrator {
1565
1840
  costEstimated,
1566
1841
  errors: errors.slice(0, 8),
1567
1842
  telemetry,
1843
+ outcomeClass: finalized.outcomeClass,
1568
1844
  };
1569
1845
  }
1570
1846
  interactionChannelFor(input, log, runId, taskId, attemptId, harnessId,
@@ -1624,6 +1900,40 @@ export class Orchestrator {
1624
1900
  return message;
1625
1901
  }
1626
1902
  }
1903
+ /**
1904
+ * D-14 layer 3 (AGENTS.md unification, INV-113): the ONE new live-tree write.
1905
+ * When the PROJECT root has `AGENTS.md` and no `CLAUDE.md`, drop a thin
1906
+ * `CLAUDE.md` (`@AGENTS.md` import + Claudexor ownership marker) so a Claude
1907
+ * Code route reads the same instruction file codex/cursor read natively.
1908
+ *
1909
+ * Fenced exactly where the automatic git-init boundary is: read-only modes
1910
+ * never reach this run-prep stage; `--in-place` stateful targets are left
1911
+ * untouched; the write targets the PROJECT root (`repoRoot`), never a worktree
1912
+ * envelope. The workspace helper adds exclusive-create + no-follow +
1913
+ * idempotency, so a hand-written or symlinked `CLAUDE.md` is never overwritten
1914
+ * and a concurrent/second prep is a no-op. Announced via a typed
1915
+ * `project.claude_bridge.created` event on an actual create only — the git-init
1916
+ * pattern. A bridge is a convenience, not a precondition: any failure is
1917
+ * swallowed so it can never fail an otherwise-valid write run.
1918
+ */
1919
+ ensureClaudeBridgeForRun(repoRoot, inPlace, log) {
1920
+ if (repoRoot === NO_PROJECT_ROOT || inPlace)
1921
+ return;
1922
+ let result;
1923
+ try {
1924
+ result = ensureClaudeBridge(repoRoot);
1925
+ }
1926
+ catch {
1927
+ return;
1928
+ }
1929
+ if (result.created) {
1930
+ log.emit("project.claude_bridge.created", {
1931
+ project_root: repoRoot,
1932
+ path: "CLAUDE.md",
1933
+ source: "AGENTS.md",
1934
+ });
1935
+ }
1936
+ }
1627
1937
  /**
1628
1938
  * Freeze-on-implement delivery (D17/D27): verify the frozen plan's hash and
1629
1939
  * materialize it as context/PLAN.md in the run artifact tree — OUTSIDE every
@@ -1707,6 +2017,9 @@ export class Orchestrator {
1707
2017
  candidates: [],
1708
2018
  };
1709
2019
  }
2020
+ // Same run-prep stage as the git boundary: if the PROJECT root uses AGENTS.md
2021
+ // with no CLAUDE.md, bridge it so a Claude Code candidate reads it (INV-113).
2022
+ this.ensureClaudeBridgeForRun(input.repoRoot, input.inPlace === true, log);
1710
2023
  // Pre-turn snapshot of the live tree for in-place runs: the revert restore
1711
2024
  // target (server-owned revertInPlace). A snapshot failure must never fail the
1712
2025
  // run — revert is simply unavailable then.
@@ -1730,17 +2043,23 @@ export class Orchestrator {
1730
2043
  });
1731
2044
  let adapters;
1732
2045
  try {
1733
- // Best-of races the whole pool (no divergence `log`: the pool still runs).
1734
- adapters = await this.resolveCandidateAdapters(input, this.candidateIntent(input), ledger);
2046
+ // Best-of races the whole pool. The `log` is passed so an AUTO pool that
2047
+ // drops a lane / clamps width discloses `route.pool.degraded` (QA-043)
2048
+ // the resolver never refills a dropped slot with a duplicate harness.
2049
+ adapters = await this.resolveCandidateAdapters(input, this.candidateIntent(input), ledger, log, undefined, runId);
1735
2050
  }
1736
2051
  catch (err) {
1737
2052
  const message = safeErrorMessage(err);
1738
2053
  store.writeText(join(paths.contextDir, "context_error.md"), `# Routing Error\n\n${message}\n`);
2054
+ // A routing preflight refusal is a config_error, not harness_unavailable
2055
+ // (A-1/#22): classify identically across every strategy's routing catch.
2056
+ const routingFailure = routingFailureClassification(err);
1739
2057
  writeFailure(store, paths, {
1740
2058
  phase: "routing",
1741
- category: "harness_unavailable",
2059
+ category: routingFailure.category,
1742
2060
  safeMessage: message,
1743
2061
  runDir: paths.root,
2062
+ ...(routingFailure.nextActions ? { nextActions: routingFailure.nextActions } : {}),
1744
2063
  });
1745
2064
  store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${mode})\n\n- Lifecycle: failed\n- Phase: routing\n\n${message}\n`);
1746
2065
  log.emit("output.ready", { kind: "summary", path: "final/summary.md", state: "diagnostic" });
@@ -1777,6 +2096,9 @@ export class Orchestrator {
1777
2096
  };
1778
2097
  const candidateAccess = contract.access.effective_profile;
1779
2098
  let budgetStopped = false;
2099
+ // QA-050: keep the ledger's typed denial so the zero-candidate terminal
2100
+ // emits budget remediation (not an empty/auth action list).
2101
+ let budgetDenial = null;
1780
2102
  let softWarned = false;
1781
2103
  const requestedSingleCandidate = adapters.length === 1;
1782
2104
  const slots = [];
@@ -1801,6 +2123,12 @@ export class Orchestrator {
1801
2123
  // already granted; only a tripped hard cap stops everything.
1802
2124
  if (lease.denied !== "estimate_headroom")
1803
2125
  budgetStopped = true;
2126
+ budgetDenial ??= {
2127
+ code: lease.denied ?? "hard_cap",
2128
+ reason: lease.reason ?? "budget lease denied",
2129
+ harnessId: routed.adapter.id,
2130
+ attemptId,
2131
+ };
1804
2132
  break; // do not spawn more paid work
1805
2133
  }
1806
2134
  slots.push({
@@ -1811,6 +2139,11 @@ export class Orchestrator {
1811
2139
  });
1812
2140
  }
1813
2141
  const runsBySlot = new Array(slots.length);
2142
+ // D-16d: one-shot continuation budget shared across the concurrent candidate
2143
+ // slots (parity with the read-only chain's single counter). Claimed
2144
+ // synchronously (check-then-increment with no await between), so two slots
2145
+ // that both exhaust cannot both consume the single continuation.
2146
+ let candidateContinuationCount = 0;
1814
2147
  const runSlot = async (slot, slotIdx) => {
1815
2148
  if (input.signal?.aborted) {
1816
2149
  ledger.cancel(slot.leaseId);
@@ -1891,11 +2224,109 @@ export class Orchestrator {
1891
2224
  log.emit("harness.completed", {
1892
2225
  harness_id: adapter.id,
1893
2226
  attempt_id: slot.attemptId,
1894
- status: run.errored ? "failed" : "success",
2227
+ // QA-027: never claim `success` over an attempt the operator/deadline
2228
+ // cut short. An abort makes the run non-successful; the top-level
2229
+ // status axis must say `cancelled` (the nested outcome axis already
2230
+ // rides in telemetrySummary), not launder a torn-off stream as clean.
2231
+ status: input.signal?.aborted ? "cancelled" : run.errored ? "failed" : "success",
1895
2232
  cost_usd: run.cost,
1896
2233
  ...telemetrySummary(run.telemetry),
1897
2234
  });
1898
- runsBySlot[slotIdx] = run;
2235
+ // D-16d one-shot continuation for an ENVELOPED candidate (parity with the
2236
+ // read-only loop, which had the ONLY continuation wiring). An eligible
2237
+ // terminal context exhaustion (repeated_refill, no completed report) gets
2238
+ // ONE fresh-session re-run in the SAME envelope, re-grounded by a
2239
+ // mechanical checkpoint packet; the exhausted candidate is superseded ONLY
2240
+ // after the continuation completes. In-place candidates are excluded (a
2241
+ // fresh session cannot safely resume mutation of the live tree).
2242
+ let effectiveRun = run;
2243
+ const envInPlace = envelope.worktree_path === envelope.repo_root;
2244
+ if (!run.errored && !input.signal?.aborted && candidateContinuationCount === 0) {
2245
+ const contDecision = decideContinuation({
2246
+ contextExhausted: run.telemetry.contextExhausted,
2247
+ contextExhaustedCause: run.telemetry.contextExhaustedCause,
2248
+ workStateCompleted: run.telemetry.outcome?.workState?.state === "completed",
2249
+ continuationCount: candidateContinuationCount,
2250
+ runKind: envInPlace ? "in_place" : "enveloped",
2251
+ });
2252
+ if (contDecision.eligible) {
2253
+ const contAttemptId = `${slot.attemptId}c`;
2254
+ const packet = buildContinuationPacket(synthesizeContinuationRequest({
2255
+ harness: adapter.id,
2256
+ profileId: input.credentialProfileId ?? null,
2257
+ priorPrompt: input.prompt,
2258
+ priorOutput: run.answerText ?? run.diff ?? "",
2259
+ }));
2260
+ // Reserve the continuation lease BEFORE any disclosure: a denied lease
2261
+ // must never emit run.continuation (which claims a continuation
2262
+ // launched) and must not consume the one-shot with no attempt. Grant ->
2263
+ // claim + disclose + run; refusal -> typed run.continuation.denied.
2264
+ const contLease = ledger.reserve({
2265
+ taskId,
2266
+ attemptId: contAttemptId,
2267
+ intent: this.candidateIntent(input),
2268
+ harnessId: adapter.id,
2269
+ cost: attemptCostEvidence(adapter.id, contAttemptId, this.estimateUsdFloor(input.repoRoot), this.routeBillingKnowledge(input, adapter.id)),
2270
+ });
2271
+ if (contLease.granted) {
2272
+ candidateContinuationCount += 1; // claim the one-shot only once it launches
2273
+ log.emit("run.continuation", {
2274
+ from_attempt: run.attemptId,
2275
+ cause: run.telemetry.contextExhaustedCause,
2276
+ continuation_count: candidateContinuationCount,
2277
+ packet_turns: packet.continuity.disclosure.packetTurns,
2278
+ });
2279
+ const contLeaseId = contLease.lease?.lease_id ?? "";
2280
+ try {
2281
+ const contRun = await this.runCandidateInEnvelope(slot.routed, envelope, contAttemptId, slot.label, contract, input.prompt, store, paths, wsm, ledger, candidateAccess, (ev) => {
2282
+ const safeEv = redactHarnessEvent(ev);
2283
+ safeInvoke(input.onHarnessEvent, safeEv);
2284
+ log.emit("harness.event", harnessEventPayload(adapter.id, contAttemptId, safeEv));
2285
+ }, input.signal, downgradeModel ?? undefined, input.effort, this.candidateIntent(input), log, effectiveWeb, this.interactionChannelFor(input, log, runId, taskId, contAttemptId, adapter.id, slot.routed.supportsInteractive), (streamedUsd) => {
2286
+ ledger.updateHold(contLeaseId, streamedUsd);
2287
+ if (ledger.tier() !== "hard")
2288
+ return false;
2289
+ budgetStopped = true;
2290
+ return true;
2291
+ }, input, requestedSingleCandidate, undefined, packet.pointerLine ?? undefined);
2292
+ ledger.settle(contLeaseId, attemptUsageCostSettlement(contRun.cost, contRun.costEstimated, contRun.attemptId, contRun.harnessId, contRun.telemetry.authMode, contRun.telemetry.usageCost));
2293
+ log.emit("harness.completed", {
2294
+ harness_id: adapter.id,
2295
+ attempt_id: contAttemptId,
2296
+ status: input.signal?.aborted
2297
+ ? "cancelled"
2298
+ : contRun.errored
2299
+ ? "failed"
2300
+ : "success",
2301
+ cost_usd: contRun.cost,
2302
+ ...telemetrySummary(contRun.telemetry),
2303
+ });
2304
+ // Supersede the exhausted candidate ONLY after the continuation
2305
+ // actually completes cleanly (never over a torn-off/aborted stream).
2306
+ if (!contRun.errored && !input.signal?.aborted)
2307
+ effectiveRun = contRun;
2308
+ }
2309
+ catch (err) {
2310
+ ledger.settle(contLeaseId, unknownCostSettlement("continuation-error", 0));
2311
+ log.emit("harness.completed", {
2312
+ harness_id: adapter.id,
2313
+ attempt_id: contAttemptId,
2314
+ status: "failed",
2315
+ error: safeErrorMessage(err),
2316
+ });
2317
+ }
2318
+ }
2319
+ else {
2320
+ ledger.cancel(contLease.lease?.lease_id ?? "");
2321
+ log.emit("run.continuation.denied", {
2322
+ from_attempt: run.attemptId,
2323
+ cause: run.telemetry.contextExhaustedCause,
2324
+ reason: contLease.reason ?? contLease.denied ?? "budget lease denied",
2325
+ });
2326
+ }
2327
+ }
2328
+ }
2329
+ runsBySlot[slotIdx] = effectiveRun;
1899
2330
  reviewEnvelopes.push(envelope);
1900
2331
  envelope = undefined;
1901
2332
  }
@@ -1972,42 +2403,54 @@ export class Orchestrator {
1972
2403
  }
1973
2404
  if (input.signal?.aborted) {
1974
2405
  await disposeReviewEnvelopes();
1975
- return cancelledResult(log, runId, taskId, mode, paths.root, cancelledCandidates(), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, runs.map((r) => ({
1976
- attemptId: r.attemptId,
1977
- harnessId: r.harnessId,
1978
- telemetry: r.telemetry,
1979
- })), null), ledger.spend(), input.signal, store);
2406
+ return cancelledResult(log, runId, taskId, mode, paths.root, cancelledCandidates(), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null), ledger.spend(), input.signal, store);
1980
2407
  }
1981
2408
  if (runs.length === 0) {
1982
2409
  const budgetReason = ledger.terminal();
2410
+ // QA-050: when the zero-candidate cause is a budget refusal, the shared
2411
+ // classifier owns the typed code, the refused route/slot, and actionable
2412
+ // budget remediation (previously an empty nextActions array).
2413
+ const agentBudgetMapping = budgetStopped || budgetReason
2414
+ ? classifyBudgetFailure({ denial: budgetDenial, terminal: budgetReason })
2415
+ : null;
1983
2416
  const facts = makeOutcomeFacts("failed", {
1984
- reason: budgetReason ?? (budgetStopped ? "budget_exhausted" : "harness_failed"),
2417
+ reason: agentBudgetMapping?.reason ?? (budgetStopped ? "budget_exhausted" : "harness_failed"),
1985
2418
  noChanges: true,
1986
2419
  });
1987
- const why = budgetStopped
1988
- ? "budget exhausted before any candidate run"
1989
- : "no candidates produced";
2420
+ const why = agentBudgetMapping?.safeMessage ?? "no candidates produced";
1990
2421
  store.writeYaml(join(paths.arbitrationDir, "decision.yaml"), {
1991
2422
  winner: null,
1992
2423
  facts,
1993
2424
  why_winner: why,
1994
2425
  evidence_facts: ["no candidates were produced"],
1995
2426
  apply_recommendation: "continue",
1996
- budget_summary: { spend_usd: ledger.spend(), estimated: false },
1997
- });
1998
- store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${mode})\n\n- Lifecycle: ${facts.lifecycle}${facts.reason ? ` (${facts.reason})` : ""}\n- Phase: budget\n\n${why}\n`);
1999
- writeFailure(store, paths, {
2000
- phase: "budget",
2001
- category: isBudgetTerminal(facts.reason) ? "budget" : "internal",
2002
- safeMessage: why,
2003
- runDir: paths.root,
2427
+ budget_summary: {
2428
+ spend_usd: ledger.spend(),
2429
+ estimated: false,
2430
+ cash_usd: ledger.spend(),
2431
+ valuation_usd: ledger.valuation(),
2432
+ },
2004
2433
  });
2434
+ store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${mode})\n\n- Lifecycle: ${facts.lifecycle}${facts.reason ? ` (${facts.reason})` : ""}\n- Phase: ${agentBudgetMapping ? "budget" : "executor"}\n\n${why}\n`);
2435
+ if (agentBudgetMapping) {
2436
+ writeFailure(store, paths, budgetFailureRecord(agentBudgetMapping, { runDir: paths.root }));
2437
+ }
2438
+ else {
2439
+ writeFailure(store, paths, {
2440
+ phase: "executor",
2441
+ category: "internal",
2442
+ safeMessage: why,
2443
+ runDir: paths.root,
2444
+ nextActions: ["Open diagnostics", "Retry the run"],
2445
+ });
2446
+ }
2005
2447
  log.emit("output.ready", { kind: "summary", path: "final/summary.md", state: "diagnostic" });
2006
2448
  log.emit("run.failed", {
2007
2449
  lifecycle: facts.lifecycle,
2008
2450
  facts,
2009
2451
  reason: facts.reason,
2010
- phase: "budget",
2452
+ phase: agentBudgetMapping ? "budget" : "executor",
2453
+ ...(agentBudgetMapping?.harnessId ? { harness_id: agentBudgetMapping.harnessId } : {}),
2011
2454
  error: why,
2012
2455
  failure_ref: "final/failure.yaml",
2013
2456
  });
@@ -2029,32 +2472,34 @@ export class Orchestrator {
2029
2472
  // producing anything are corpses: reviewing "(empty diff)" spends real
2030
2473
  // reviewer money on nothing and buries the root cause behind an
2031
2474
  // arbitration scoring string.
2032
- const workingRuns = runs.filter((r) => !r.errored || r.diff.length > 0);
2475
+ const workingRuns = partitionCandidates(runs).working;
2033
2476
  if (workingRuns.length === 0) {
2034
2477
  await disposeReviewEnvelopes();
2035
2478
  const first = runs[0];
2036
2479
  const phase = first.infraPhase ?? "harness";
2037
- const rootCause = runs
2038
- .map((r) => `${r.attemptId}/${r.harnessId}: ${r.errors[0] ?? "failed before producing work"}`)
2039
- .join("; ");
2040
- const facts = makeOutcomeFacts("failed", { reason: "harness_failed", noChanges: true });
2480
+ const { facts, why: rootCause } = partitionCandidates(runs);
2041
2481
  store.writeYaml(join(paths.arbitrationDir, "decision.yaml"), {
2042
2482
  winner: null,
2043
2483
  facts,
2044
2484
  why_winner: rootCause,
2045
2485
  evidence_facts: runs.map((r) => `${r.attemptId} produced no work: ${r.errors[0] ?? "unknown"}`),
2046
2486
  apply_recommendation: "continue",
2047
- budget_summary: { spend_usd: ledger.spend(), estimated: false },
2487
+ budget_summary: {
2488
+ spend_usd: ledger.spend(),
2489
+ estimated: false,
2490
+ cash_usd: ledger.spend(),
2491
+ valuation_usd: ledger.valuation(),
2492
+ },
2048
2493
  });
2049
- this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, runs.map((r) => ({
2050
- attemptId: r.attemptId,
2051
- harnessId: r.harnessId,
2052
- telemetry: r.telemetry,
2053
- })), null);
2494
+ this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null);
2054
2495
  store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${mode})\n\n- Lifecycle: ${facts.lifecycle}\n- Phase: ${phase}\n\n${rootCause}\n`);
2055
2496
  const existingEventRefs = runs
2056
2497
  .map((r) => `attempts/${r.attemptId}/events.jsonl`)
2057
2498
  .filter((rel) => existsSync(join(paths.root, rel)));
2499
+ // #31: auth guidance only on a classified auth failure; every other
2500
+ // harness cause (timeout, rate limit, crash, config) gets remediation that
2501
+ // fits it, instead of a doomed "Check harness authentication".
2502
+ const harnessCategory = dominantHarnessFailureCategory(first.telemetry.transientFailures);
2058
2503
  writeFailure(store, paths, {
2059
2504
  phase,
2060
2505
  category: phase === "workspace" ? "project" : "harness_error",
@@ -2066,7 +2511,7 @@ export class Orchestrator {
2066
2511
  runDir: paths.root,
2067
2512
  nextActions: phase === "workspace"
2068
2513
  ? ["Check the project folder", "Open diagnostics", "Retry the run"]
2069
- : ["Open diagnostics", "Check harness authentication", "Retry the run"],
2514
+ : harnessFailureNextActions(harnessCategory),
2070
2515
  });
2071
2516
  log.emit("output.ready", { kind: "summary", path: "final/summary.md", state: "diagnostic" });
2072
2517
  log.emit("run.failed", {
@@ -2094,7 +2539,31 @@ export class Orchestrator {
2094
2539
  spendUsd: ledger.spend(),
2095
2540
  };
2096
2541
  }
2097
- log.emit("review.started", { reviewers: reviewers.length, review_verified: reviewVerified });
2542
+ // QA-025: only announce that review STARTED when the panel will actually
2543
+ // run. A candidate that changed no files is skipped inside reviewRuns; a
2544
+ // start event before that check falsely claims a paid review began (and its
2545
+ // `review_verified` payload was the PRELIMINARY route-family count, not any
2546
+ // real verification). Compute the reviewable set first and emit a typed
2547
+ // `review.skipped` when nothing is reviewable, so every start has a matching
2548
+ // terminal and the no-diff path records `not_run` consistently.
2549
+ const reviewableRuns = workingRuns.filter((r) => r.diff.trim().length > 0);
2550
+ const configuredFamilies = new Set(reviewers.map((r) => r.providerFamily)).size;
2551
+ if (reviewableRuns.length === 0 || reviewers.length === 0) {
2552
+ log.emit("review.skipped", {
2553
+ reason: reviewers.length === 0 ? "no_reviewers" : "no_changes",
2554
+ reviewable_candidates: reviewableRuns.length,
2555
+ configured_reviewers: reviewers.length,
2556
+ configured_provider_families: configuredFamilies,
2557
+ });
2558
+ }
2559
+ else {
2560
+ log.emit("review.started", {
2561
+ reviewers: reviewers.length,
2562
+ reviewable_candidates: reviewableRuns.length,
2563
+ configured_provider_families: configuredFamilies,
2564
+ cross_family_route_eligible: reviewVerified,
2565
+ });
2566
+ }
2098
2567
  let evidences;
2099
2568
  try {
2100
2569
  // reviewRuns internally SKIPS the paid reviewer call for empty-diff
@@ -2113,11 +2582,7 @@ export class Orchestrator {
2113
2582
  await disposeReviewEnvelopes();
2114
2583
  }
2115
2584
  if (input.signal?.aborted) {
2116
- return cancelledResult(log, runId, taskId, mode, paths.root, cancelledCandidates(), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, runs.map((r) => ({
2117
- attemptId: r.attemptId,
2118
- harnessId: r.harnessId,
2119
- telemetry: r.telemetry,
2120
- })), null), ledger.spend(), input.signal, store);
2585
+ return cancelledResult(log, runId, taskId, mode, paths.root, cancelledCandidates(), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null), ledger.spend(), input.signal, store);
2121
2586
  }
2122
2587
  // Synthesis: if worthwhile, run a synthesizer as a NEW, re-checked candidate.
2123
2588
  const synth = decideSynthesis(evidences, input.synthesis ?? "auto");
@@ -2161,22 +2626,22 @@ export class Orchestrator {
2161
2626
  ledger.settle(lease.lease?.lease_id ?? "", attemptUsageCostSettlement(run.cost, run.costEstimated, run.attemptId, run.harnessId, run.telemetry.authMode, run.telemetry.usageCost));
2162
2627
  reviewEnvelopes.push(envelope);
2163
2628
  envelope = undefined;
2629
+ // D-16 r8: only a WORKING synth is reviewed/adopted (same veto owner
2630
+ // as the race lane); `runs` still records it for telemetry.
2631
+ runs.push(run);
2164
2632
  try {
2165
- const synthEvidence = await this.reviewRuns([run], reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal);
2166
- evidences.push(...synthEvidence);
2633
+ if (isWorkingCandidate(run)) {
2634
+ const synthEvidence = await this.reviewRuns([run], reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal);
2635
+ evidences.push(...synthEvidence);
2636
+ workingRuns.push(run);
2637
+ }
2167
2638
  if (input.signal?.aborted) {
2168
- return cancelledResult(log, runId, taskId, mode, paths.root, cancelledCandidates(), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, runs.map((r) => ({
2169
- attemptId: r.attemptId,
2170
- harnessId: r.harnessId,
2171
- telemetry: r.telemetry,
2172
- })), null), ledger.spend(), input.signal, store);
2639
+ return cancelledResult(log, runId, taskId, mode, paths.root, cancelledCandidates(), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null), ledger.spend(), input.signal, store);
2173
2640
  }
2174
2641
  }
2175
2642
  finally {
2176
2643
  await disposeReviewEnvelopes();
2177
2644
  }
2178
- runs.push(run);
2179
- workingRuns.push(run);
2180
2645
  }
2181
2646
  catch (err) {
2182
2647
  ledger.settle(lease.lease?.lease_id ?? "", unknownCostSettlement("synthesis-error"));
@@ -2193,17 +2658,17 @@ export class Orchestrator {
2193
2658
  }
2194
2659
  }
2195
2660
  if (input.signal?.aborted) {
2196
- return cancelledResult(log, runId, taskId, mode, paths.root, cancelledCandidates(), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, runs.map((r) => ({
2197
- attemptId: r.attemptId,
2198
- harnessId: r.harnessId,
2199
- telemetry: r.telemetry,
2200
- })), null), ledger.spend(), input.signal, store);
2661
+ return cancelledResult(log, runId, taskId, mode, paths.root, cancelledCandidates(), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null), ledger.spend(), input.signal, store);
2201
2662
  }
2202
2663
  let result;
2203
2664
  try {
2204
2665
  result = arbitrate(evidences, {
2205
2666
  spendUsd: ledger.spend(),
2206
2667
  estimatedSpend: runs.some((r) => r.costEstimated),
2668
+ // QA-010b: carry the settled cash + subscription-valuation totals
2669
+ // (reviewer panel included) onto the decision record.
2670
+ cashUsd: ledger.spend(),
2671
+ valuationUsd: ledger.valuation(),
2207
2672
  });
2208
2673
  }
2209
2674
  catch (err) {
@@ -2213,6 +2678,11 @@ export class Orchestrator {
2213
2678
  log.emit("arbitration.completed", {
2214
2679
  winner: result.decision.winner,
2215
2680
  lifecycle: result.decision.facts.lifecycle,
2681
+ // QA-028: surface the axis that actually separated the winner from the
2682
+ // runner-up (null on an exact tie) so live surfaces can explain the pick.
2683
+ ...(result.decision.decisive_axis
2684
+ ? { decisive_axis: result.decision.decisive_axis.key }
2685
+ : {}),
2216
2686
  });
2217
2687
  // Winner can only be a candidate that actually produced work; corpses are
2218
2688
  // excluded from arbitration upstream and from the fallback here.
@@ -2266,7 +2736,7 @@ export class Orchestrator {
2266
2736
  store.writeYaml(join(paths.arbitrationDir, "decision.yaml"), {
2267
2737
  ...result.decision,
2268
2738
  ...(needsDec
2269
- ? blockedDecisionOverride(result.decision.evidence_facts, finalVerify)
2739
+ ? blockedDecisionOverride(result.decision.evidence_facts, facts, finalVerify)
2270
2740
  : { facts }),
2271
2741
  review_verified: actualReviewVerified,
2272
2742
  final_verify: finalVerify,
@@ -2291,7 +2761,7 @@ export class Orchestrator {
2291
2761
  ? winnerEvidence.findings.filter((f) => isBlocking(f)).length
2292
2762
  : 0;
2293
2763
  // Prose from an empty-diff winner is an answer, never a patch.
2294
- const winnerAnswer = winnerRun.answerText?.trim() ?? "";
2764
+ const winnerAnswer = winnerRun.answerText ?? "";
2295
2765
  const resultKind = hasDiff ? "patch" : winnerAnswer.length > 0 ? "answer" : "none";
2296
2766
  // The winner's final MESSAGE is the human-facing answer and materializes
2297
2767
  // for diff-ful runs too: the chat renders final/answer.md (the projection
@@ -2430,7 +2900,7 @@ export class Orchestrator {
2430
2900
  : "diagnostic",
2431
2901
  });
2432
2902
  }
2433
- this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, runs.map((r) => ({ attemptId: r.attemptId, harnessId: r.harnessId, telemetry: r.telemetry })), result.decision.facts.lifecycle === "succeeded"
2903
+ this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), result.decision.facts.lifecycle === "succeeded"
2434
2904
  ? result.decision.winner
2435
2905
  : (winnerRun?.attemptId ?? null));
2436
2906
  // A needs-decision terminal (review blocked or checks failed) OR a
@@ -2464,9 +2934,16 @@ export class Orchestrator {
2464
2934
  });
2465
2935
  }
2466
2936
  else if (isFailureTerminal) {
2937
+ // QA-010c: a reviewer-accepted BLOCK/FIX_FIRST (or a failed deterministic
2938
+ // gate) that BLOCKS the run is an operator-decision terminal, not an
2939
+ // internal engine error. Only a genuinely unexpected non-decision terminal
2940
+ // stays `internal`. A review/checks block is `policy` (the acceptance
2941
+ // path), phase `review`, and its remedy names the decision, not "retry
2942
+ // with a different harness".
2943
+ const decisionBlock = needsHuman || needsDecisionTerminal;
2467
2944
  writeFailure(store, paths, {
2468
- phase: needsHuman ? "review" : "arbitration",
2469
- category: needsHuman
2945
+ phase: decisionBlock ? "review" : "arbitration",
2946
+ category: decisionBlock
2470
2947
  ? "policy"
2471
2948
  : winnerRun?.errored
2472
2949
  ? "harness_error"
@@ -2477,16 +2954,20 @@ export class Orchestrator {
2477
2954
  attemptId: winnerRun?.errored ? winnerRun.attemptId : undefined,
2478
2955
  safeMessage: needsHuman
2479
2956
  ? `review escalated to a human decision: ${result.decision.why_winner}`
2480
- : result.decision.why_winner,
2957
+ : needsDecisionTerminal
2958
+ ? `review blocked before apply: ${result.decision.why_winner}`
2959
+ : result.decision.why_winner,
2481
2960
  rawDetailRef: winnerRun?.errored
2482
2961
  ? `attempts/${winnerRun.attemptId}/attempt.yaml`
2483
2962
  : undefined,
2484
2963
  runDir: paths.root,
2485
- nextActions: needsHuman
2964
+ nextActions: decisionBlock
2486
2965
  ? [
2487
- "Open the review queue",
2488
- "Decide the NEEDS_HUMAN findings",
2489
- "Re-run after the decision",
2966
+ "Review the blocking findings on the run's turn",
2967
+ "Accept the risk to apply this exact patch, or discard the change",
2968
+ ...(facts.checks === "failed"
2969
+ ? ["Configure/approve the deterministic test command, then re-run"]
2970
+ : []),
2490
2971
  ]
2491
2972
  : [
2492
2973
  "Open diagnostics",
@@ -2550,7 +3031,11 @@ export class Orchestrator {
2550
3031
  };
2551
3032
  }
2552
3033
  /** Single-owner telemetry artifact (final/telemetry.yaml); surfaces project it, never recompute. */
2553
- writeRunTelemetry(store, paths, contract, runId, taskId, mode, attempts, finalAttemptId) {
3034
+ writeRunTelemetry(store, paths, contract, runId, taskId, mode, attempts, finalAttemptId, deepScanSynthesis) {
3035
+ // QA-034: attach the routing rationale recorded at pool ordering (if this
3036
+ // run computed one), then clear it — telemetry is written once at terminal.
3037
+ const routingRationale = this.routingRationaleByRun.get(runId) ?? null;
3038
+ this.routingRationaleByRun.delete(runId);
2554
3039
  writeRunTelemetryArtifact({
2555
3040
  store,
2556
3041
  finalDir: paths.finalDir,
@@ -2560,6 +3045,8 @@ export class Orchestrator {
2560
3045
  mode,
2561
3046
  attempts,
2562
3047
  finalAttemptId,
3048
+ routingRationale,
3049
+ deepScanSynthesis: deepScanSynthesis ?? null,
2563
3050
  resolveAuthPreference: (harnessId) => this.authPreferenceForHarness(contract.repo.root, harnessId, contract.auth_preference),
2564
3051
  });
2565
3052
  }
@@ -2784,6 +3271,11 @@ export class Orchestrator {
2784
3271
  candidates: [],
2785
3272
  };
2786
3273
  }
3274
+ // Same run-prep stage as the git boundary (and the same `!inPlace`
3275
+ // exclusion — we are inside that branch, so inPlace is false here): bridge
3276
+ // an AGENTS.md-only PROJECT root so a Claude Code convergence attempt reads
3277
+ // it (INV-113).
3278
+ this.ensureClaudeBridgeForRun(input.repoRoot, false, log);
2787
3279
  }
2788
3280
  const reviewDir = join(paths.root, "review-evidence");
2789
3281
  writeEvidencePacket(reviewDir, {
@@ -2799,17 +3291,21 @@ export class Orchestrator {
2799
3291
  // One envelope carried forward across attempts so the harness can repair its own work.
2800
3292
  let adapterPool;
2801
3293
  try {
2802
- adapterPool = await this.resolveCandidateAdapters({ ...input, n: undefined }, this.candidateIntent(input), ledger, log);
3294
+ adapterPool = await this.resolveCandidateAdapters({ ...input, n: undefined }, this.candidateIntent(input), ledger, log, undefined, runId);
2803
3295
  this.requestRequirements.assertConvergenceWorkspace(input.inPlace === true, adapterPool);
2804
3296
  }
2805
3297
  catch (err) {
2806
3298
  const message = safeErrorMessage(err);
2807
3299
  store.writeText(join(paths.contextDir, "context_error.md"), `# Routing Error\n\n${message}\n`);
3300
+ // A routing preflight refusal is a config_error, not harness_unavailable
3301
+ // (A-1/#22): classify identically across every strategy's routing catch.
3302
+ const routingFailure = routingFailureClassification(err);
2808
3303
  writeFailure(store, paths, {
2809
3304
  phase: "routing",
2810
- category: "harness_unavailable",
3305
+ category: routingFailure.category,
2811
3306
  safeMessage: message,
2812
3307
  runDir: paths.root,
3308
+ ...(routingFailure.nextActions ? { nextActions: routingFailure.nextActions } : {}),
2813
3309
  });
2814
3310
  store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${mode})\n\n- Lifecycle: failed\n- Phase: routing\n\n${message}\n`);
2815
3311
  log.emit("output.ready", { kind: "summary", path: "final/summary.md", state: "diagnostic" });
@@ -2881,6 +3377,7 @@ export class Orchestrator {
2881
3377
  let attempt = 0;
2882
3378
  let converged = false;
2883
3379
  let exhausted = false;
3380
+ let interrupted = false; // D-16 r8: terminalizes the run interrupted
2884
3381
  let lastFindings = [];
2885
3382
  let lastRun = null;
2886
3383
  let actualReviewVerified = false;
@@ -2987,7 +3484,8 @@ export class Orchestrator {
2987
3484
  log.emit("harness.completed", {
2988
3485
  harness_id: adapter.id,
2989
3486
  attempt_id: attemptId,
2990
- status: run.errored ? "failed" : "success",
3487
+ // QA-027: an aborted attempt is `cancelled`, never a clean `success`.
3488
+ status: input.signal?.aborted ? "cancelled" : run.errored ? "failed" : "success",
2991
3489
  cost_usd: run.cost,
2992
3490
  ...telemetrySummary(run.telemetry),
2993
3491
  });
@@ -3017,6 +3515,12 @@ export class Orchestrator {
3017
3515
  }
3018
3516
  lastRun = run;
3019
3517
  attemptTelemetries.push({ attemptId, harnessId: adapter.id, telemetry: run.telemetry });
3518
+ // D-16 r8: interrupted (errored===false) would CONVERGE a partial diff
3519
+ // as clean — break BEFORE review; a harness error still gate-retries.
3520
+ if (run.outcomeClass === "interrupted") {
3521
+ interrupted = true;
3522
+ break;
3523
+ }
3020
3524
  // Post-mutation fence for in-place: snapshot the live tree NOW (after the
3021
3525
  // harness mutated it, before this attempt's review). The last attempt's
3022
3526
  // value is the revert target persisted into work_product.yaml.
@@ -3242,22 +3746,41 @@ export class Orchestrator {
3242
3746
  // Base terminal AXES (D8) from the convergence loop outcome. Attempts-cap
3243
3747
  // exhaustion maps to budget_exhausted (an attempt budget); the give-up
3244
3748
  // states map to their matching RunReason.
3245
- let facts = input.signal?.aborted
3246
- ? makeOutcomeFacts("cancelled", { reason: "user_cancelled" })
3247
- : converged
3248
- ? makeOutcomeFacts("succeeded")
3249
- : stuckNoProgress
3250
- ? makeOutcomeFacts("failed", { reason: "stuck_no_progress" })
3251
- : exhausted
3252
- ? makeOutcomeFacts("failed", { reason: "budget_exhausted" })
3253
- : makeOutcomeFacts("failed", { reason: "not_converged" });
3749
+ //
3750
+ // QA-041 terminal-causality precedence: convergence used to hard-code EVERY
3751
+ // aborted signal to `user_cancelled`, which (a) fabricated an operator action
3752
+ // that never happened when the maxSeconds wall-clock deadline fired, and
3753
+ // (b) discarded an already-proven `stuck_no_progress` terminal. The typed
3754
+ // abort reason (`wall_clock_exceeded` from the deadline controller, carried
3755
+ // on `input.signal.reason` via AbortSignal.any) is now read at the source,
3756
+ // and an established semantic terminal (stuck_no_progress) wins over the
3757
+ // abort so the deadline that only ended a redundant post-proof panel does
3758
+ // not overwrite the actionable no-progress reason. `user_cancelled` is
3759
+ // emitted ONLY for a real control cancel (no typed deadline reason).
3760
+ const convAbortReason = typeof input.signal?.reason === "string" && input.signal.reason
3761
+ ? input.signal.reason
3762
+ : undefined;
3763
+ const convCancelFacts = () => makeOutcomeFacts("cancelled", {
3764
+ reason: convAbortReason === "wall_clock_exceeded" ? "wall_clock_exceeded" : "user_cancelled",
3765
+ });
3766
+ let facts = convergenceOutcomeFacts({
3767
+ converged,
3768
+ interrupted,
3769
+ stuckNoProgress,
3770
+ aborted: Boolean(input.signal?.aborted),
3771
+ exhausted,
3772
+ }, convCancelFacts);
3254
3773
  let decision = null;
3255
- if (lastRun) {
3774
+ // D-16 r8: interrupted is never arbitrated; the partial stays diagnostic.
3775
+ if (lastRun && !interrupted) {
3256
3776
  const arb = arbitrate([
3257
3777
  toCandidateEvidence(lastRun, contract, lastFindings, lastFinalReviewClean, actualReviewVerified),
3258
3778
  ], {
3259
3779
  spendUsd: ledger.spend(),
3260
3780
  estimatedSpend: lastRun.costEstimated || reviewSpendEstimated,
3781
+ // QA-010b: settled cash + valuation (reviewer panel included).
3782
+ cashUsd: ledger.spend(),
3783
+ valuationUsd: ledger.valuation(),
3261
3784
  });
3262
3785
  decision = arb.decision;
3263
3786
  store.writeYaml(join(paths.arbitrationDir, "decision.yaml"), decision);
@@ -3308,15 +3831,18 @@ export class Orchestrator {
3308
3831
  decision = {
3309
3832
  ...decision,
3310
3833
  ...(convNeedsDecision
3311
- ? blockedDecisionOverride(decision.evidence_facts, convFinalVerify)
3834
+ ? blockedDecisionOverride(decision.evidence_facts, facts, convFinalVerify)
3312
3835
  : { facts }),
3313
3836
  final_verify: convFinalVerify,
3314
3837
  };
3315
3838
  store.writeYaml(join(paths.arbitrationDir, "decision.yaml"), decision);
3316
3839
  }
3317
3840
  this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, attemptTelemetries, lastRun?.attemptId ?? null);
3318
- // Deliver the converged/last work to final/ so `apply` and `inspect` can use it.
3319
- if (lastRun) {
3841
+ // Deliver the converged/last work to final/ so `apply` and `inspect` can
3842
+ // use it. D-16 r8: an INTERRUPTED envelope run delivers no applyable
3843
+ // work_product (its partial patch.diff stays diagnostic via attempts/);
3844
+ // in-place keeps the product so the honest Revert offer survives.
3845
+ if (lastRun && (!interrupted || input.inPlace === true)) {
3320
3846
  assertNoSecretLikeTokens("final patch diff", lastRun.diff);
3321
3847
  const patchSha256 = sha256(lastRun.diff);
3322
3848
  store.writeText(join(paths.finalDir, "patch.diff"), lastRun.diff);
@@ -3386,12 +3912,16 @@ export class Orchestrator {
3386
3912
  attemptId: lastRun?.attemptId,
3387
3913
  runDir: paths.root,
3388
3914
  nextActions: facts.lifecycle === "cancelled"
3389
- ? ["Retry if cancellation was accidental"]
3915
+ ? facts.reason === "wall_clock_exceeded"
3916
+ ? [
3917
+ "Inspect the partial work kept from before the deadline",
3918
+ "Increase --max-seconds or narrow the scope, then re-run",
3919
+ ]
3920
+ : ["Retry if cancellation was accidental"]
3390
3921
  : convNeedsDecision
3391
3922
  ? [
3392
- "Open the review queue",
3393
- "Decide the NEEDS_HUMAN findings",
3394
- "Re-run after the decision",
3923
+ "Review the blocking findings on the run's turn",
3924
+ "Accept the risk to apply this exact patch, or discard the change",
3395
3925
  ]
3396
3926
  : facts.reason === "stuck_no_progress"
3397
3927
  ? [
@@ -3405,8 +3935,12 @@ export class Orchestrator {
3405
3935
  "Retry with more attempts or a narrower prompt",
3406
3936
  ],
3407
3937
  });
3408
- if (!lastRun) {
3409
- store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${mode})\n\n- Lifecycle: ${facts.lifecycle}\n- Attempts: ${attempt}\n`);
3938
+ // D-16 r8/r9: an INTERRUPTED envelope run still gets its diagnostic
3939
+ // summary + output.ready (only patch/work_product are withheld) the
3940
+ // ARCHITECTURE event contract guarantees output.ready precedes the
3941
+ // terminal in every mode.
3942
+ if (!lastRun || (interrupted && input.inPlace !== true)) {
3943
+ store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${mode})\n\n- Lifecycle: ${facts.lifecycle}${facts.reason ? ` (${facts.reason})` : ""}\n- Attempts: ${attempt}\n`);
3410
3944
  log.emit("output.ready", {
3411
3945
  kind: "summary",
3412
3946
  path: "final/summary.md",
@@ -3414,7 +3948,10 @@ export class Orchestrator {
3414
3948
  });
3415
3949
  }
3416
3950
  }
3417
- log.emit("work_product.emitted", { winner: lastRun?.attemptId ?? null });
3951
+ // work_product.emitted only when a product was actually written (r9).
3952
+ if (lastRun && (!interrupted || input.inPlace === true)) {
3953
+ log.emit("work_product.emitted", { winner: lastRun.attemptId });
3954
+ }
3418
3955
  if (!convIsFailureTerminal) {
3419
3956
  log.emit("run.completed", {
3420
3957
  lifecycle: facts.lifecycle,
@@ -3440,6 +3977,11 @@ export class Orchestrator {
3440
3977
  attempts: attempt,
3441
3978
  phase: "convergence",
3442
3979
  failure_ref: "final/failure.yaml",
3980
+ // QA-041: surface the typed deadline reason on the terminal event so the
3981
+ // daemon job result / Control API can distinguish it from a user cancel.
3982
+ ...(facts.lifecycle === "cancelled" && convAbortReason
3983
+ ? { cancel_reason: convAbortReason }
3984
+ : {}),
3443
3985
  });
3444
3986
  }
3445
3987
  return {
@@ -3451,6 +3993,11 @@ export class Orchestrator {
3451
3993
  facts,
3452
3994
  winner: lastRun?.attemptId ?? null,
3453
3995
  runDir: paths.root,
3996
+ // QA-041: carry the typed deadline reason on the result so daemon/Control
3997
+ // API/CLI never falsely attribute a maxSeconds deadline to the user.
3998
+ ...(facts.lifecycle === "cancelled" && convAbortReason
3999
+ ? { cancelReason: convAbortReason }
4000
+ : {}),
3454
4001
  summary: converged
3455
4002
  ? `converged in ${attempt} attempt(s)`
3456
4003
  : `${facts.lifecycle} after ${attempt} attempt(s)`,
@@ -3500,15 +4047,7 @@ export class Orchestrator {
3500
4047
  `Keep it concise. Reference real paths you found. Do NOT paste large code blocks; describe the change instead.`,
3501
4048
  ].join("\n");
3502
4049
  }
3503
- /**
3504
- * Run ONE planner spawn (native plan mode, read-only) end to end: budget
3505
- * lease, spec build, continuity hydration, event streaming, telemetry, and
3506
- * settle. Shared by the solo plan loop (each pool member is a sequential
3507
- * fallback) and the Council strategy (each member is a parallel draft, then
3508
- * one merge iteration on the primary — same machinery, different prompt +
3509
- * intent). The caller owns bookkeeping that differs per path: which artifact
3510
- * a success writes to, fallback disclosure, and accumulation.
3511
- */
4050
+ /** One read-only planner spawn shared by solo fallback, Council drafts, and merge. */
3512
4051
  async runPlannerAttempt(args) {
3513
4052
  const { input, contract, taskId, runId, log, store, paths, ledger, routed, attemptId } = args;
3514
4053
  const adapter = routed.adapter;
@@ -3523,6 +4062,7 @@ export class Orchestrator {
3523
4062
  log.emit("budget.lease.created", {
3524
4063
  granted: false,
3525
4064
  reason: lease.reason,
4065
+ denied: lease.denied,
3526
4066
  attempt_id: attemptId,
3527
4067
  harness_id: adapter.id,
3528
4068
  });
@@ -3530,10 +4070,17 @@ export class Orchestrator {
3530
4070
  attemptId,
3531
4071
  harnessId: adapter.id,
3532
4072
  status: "failed",
4073
+ outcomeClass: "clean", // never spawned: refused pre-flight by the budget gate
3533
4074
  error: lease.reason ?? "budget lease denied",
3534
4075
  text: null,
3535
4076
  telemetry: null,
3536
4077
  budgetDenied: true,
4078
+ budgetDenial: {
4079
+ code: lease.denied ?? "hard_cap",
4080
+ reason: lease.reason ?? "budget lease denied",
4081
+ harnessId: adapter.id,
4082
+ attemptId,
4083
+ },
3537
4084
  };
3538
4085
  }
3539
4086
  const knobs = this.routeSpecKnobs(routed, contract, undefined, input.effort);
@@ -3570,6 +4117,10 @@ export class Orchestrator {
3570
4117
  const planInteraction = this.interactionChannelFor(input, log, runId, taskId, attemptId, adapter.id, routed.supportsInteractive);
3571
4118
  if (planInteraction)
3572
4119
  spec.extra["interactionChannel"] = planInteraction;
4120
+ // D-16: compile the WorkReport envelope for the plan lane (require plan text
4121
+ // below folds the deliverable; the veto rides work_state).
4122
+ const planWorkEnvelope = this.workReportEnvelopeFor(routed, contract, Boolean(planInteraction));
4123
+ const planWorkMode = this.applyWorkEnvelope(spec, planWorkEnvelope);
3573
4124
  const attemptEventsPath = join(paths.attemptsDir, attemptId, "events.jsonl");
3574
4125
  const answer = new AnswerAssembly();
3575
4126
  const telemetry = createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required ||
@@ -3666,25 +4217,63 @@ export class Orchestrator {
3666
4217
  const first = unrecovered[0];
3667
4218
  harnessError = `${first.tool} failed without recovery: ${first.summary}`;
3668
4219
  }
3669
- if (harnessError) {
4220
+ // D-16: unwrap and require PLAN TEXT — a plan with no text is not delivered.
4221
+ const planUnwrapped = unwrapWorkReportEnvelope(answer.machineText() ?? "", planWorkMode, {
4222
+ sideToolReport: telemetry.sideToolWorkReport ?? undefined,
4223
+ });
4224
+ const planText = redactSecrets(planUnwrapped.deliverable).trim();
4225
+ const planFinalized = finalizeAttempt({
4226
+ deliverableEvidence: planText.length > 0,
4227
+ harnessErrored: harnessError !== null && !webBlocked,
4228
+ workReport: planUnwrapped.workReport,
4229
+ workReportSource: planUnwrapped.source,
4230
+ workReportViolation: planUnwrapped.contractViolation,
4231
+ contextTerminalExhausted: telemetry.contextExhausted,
4232
+ });
4233
+ // A broken WorkReport contract is a hard failure only when the finalizer
4234
+ // ranked it so (a terminal context exhaustion outranks it).
4235
+ if (!harnessError && planFinalized.outcomeClass === "contract_failure") {
4236
+ harnessError = `work_report contract: ${planUnwrapped.contractViolation}`;
4237
+ }
4238
+ // D-16 r9: an interrupted (context-exhausted) planner is NEVER a clean
4239
+ // plan — partial text must not become final/plan.md as success. A VETO
4240
+ // (needs_input/incomplete work_state) is DIFFERENT by the sealed D-16
4241
+ // contract (X35, INV-116 canaries): the plan still delivers, lifecycle
4242
+ // succeeded, and the work_state veto rides the OUTCOME (non-zero exit) —
4243
+ // it must not be laundered into a harness failure either direction.
4244
+ if (!harnessError && planFinalized.outcomeClass === "interrupted") {
4245
+ harnessError = "context capacity exhausted before the plan completed";
4246
+ }
4247
+ const attemptError = harnessError ??
4248
+ (planFinalized.deliverablePresent ? null : "planner produced no plan text") ??
4249
+ (input.signal?.aborted ? "planner cancelled" : null);
4250
+ setAttemptOutcome(telemetry, {
4251
+ deliverablePresent: planFinalized.deliverablePresent,
4252
+ gatesPassed: null,
4253
+ harnessErrored: (harnessError !== null && !webBlocked) || planFinalized.harnessErrored,
4254
+ webRequiredUnsatisfied: webBlocked,
4255
+ workState: planFinalized.workState,
4256
+ });
4257
+ if (attemptError) {
3670
4258
  log.emit("harness.completed", {
3671
4259
  harness_id: adapter.id,
3672
4260
  attempt_id: attemptId,
3673
4261
  status: webBlocked ? "blocked" : "failed",
3674
- error: harnessError,
4262
+ error: attemptError,
3675
4263
  ...telemetrySummary(telemetry),
3676
4264
  });
3677
4265
  return {
3678
4266
  attemptId,
3679
4267
  harnessId: adapter.id,
3680
4268
  status: webBlocked ? "blocked" : "failed",
3681
- error: harnessError,
4269
+ outcomeClass: planFinalized.outcomeClass,
4270
+ error: attemptError,
3682
4271
  text: null,
3683
4272
  telemetry,
3684
4273
  budgetDenied: false,
3685
4274
  };
3686
4275
  }
3687
- const text = answer.text() || "(no output)";
4276
+ const text = planText || "(no output)";
3688
4277
  log.emit("harness.completed", {
3689
4278
  harness_id: adapter.id,
3690
4279
  attempt_id: attemptId,
@@ -3695,6 +4284,7 @@ export class Orchestrator {
3695
4284
  attemptId,
3696
4285
  harnessId: adapter.id,
3697
4286
  status: "success",
4287
+ outcomeClass: planFinalized.outcomeClass,
3698
4288
  error: null,
3699
4289
  text,
3700
4290
  telemetry,
@@ -3737,17 +4327,21 @@ export class Orchestrator {
3737
4327
  const laneRun = Boolean(input.threadId);
3738
4328
  let adapters;
3739
4329
  try {
3740
- adapters = await this.resolveCandidateAdapters({ ...input, n: undefined }, "plan", ledger, log, roHome);
4330
+ adapters = await this.resolveCandidateAdapters({ ...input, n: undefined }, "plan", ledger, log, roHome, runId);
3741
4331
  }
3742
4332
  catch (err) {
3743
4333
  roHome.dispose();
3744
4334
  const message = safeErrorMessage(err);
3745
4335
  store.writeText(join(paths.contextDir, "context_error.md"), `# Routing Error\n\n${message}\n`);
4336
+ // A routing preflight refusal is a config_error, not harness_unavailable
4337
+ // (A-1/#22): classify identically across every strategy's routing catch.
4338
+ const routingFailure = routingFailureClassification(err);
3746
4339
  writeFailure(store, paths, {
3747
4340
  phase: "routing",
3748
- category: "harness_unavailable",
4341
+ category: routingFailure.category,
3749
4342
  safeMessage: message,
3750
4343
  runDir: paths.root,
4344
+ ...(routingFailure.nextActions ? { nextActions: routingFailure.nextActions } : {}),
3751
4345
  });
3752
4346
  store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (plan)\n\n- Lifecycle: failed\n- Phase: routing\n\n${message}\n`);
3753
4347
  log.emit("output.ready", { kind: "summary", path: "final/summary.md", state: "diagnostic" });
@@ -3832,6 +4426,9 @@ export class Orchestrator {
3832
4426
  let fallbackFrom = null;
3833
4427
  const planAttempts = [];
3834
4428
  const attemptTelemetries = [];
4429
+ // QA-050: the ledger's typed denial when a planner slot was refused
4430
+ // pre-spawn, so the terminal is a budget failure (not "all planners failed").
4431
+ let planBudgetDenial = null;
3835
4432
  try {
3836
4433
  for (const [idx, routed] of adapters.entries()) {
3837
4434
  if (input.signal?.aborted)
@@ -3853,8 +4450,20 @@ export class Orchestrator {
3853
4450
  promptBody: this.planPrompt(input.prompt) + contextSection,
3854
4451
  intent: "plan",
3855
4452
  });
3856
- if (outcome.budgetDenied)
4453
+ if (outcome.budgetDenied) {
4454
+ // QA-050: retain the denied planner slot before breaking so the
4455
+ // terminal names the refused route and does not read "all planners
4456
+ // failed"; capture the typed denial for the budget classifier.
4457
+ planBudgetDenial ??= outcome.budgetDenial ?? null;
4458
+ planAttempts.push({
4459
+ attemptId,
4460
+ harnessId: outcome.harnessId,
4461
+ status: outcome.status,
4462
+ outcomeClass: outcome.outcomeClass,
4463
+ error: outcome.error,
4464
+ });
3857
4465
  break;
4466
+ }
3858
4467
  if (outcome.telemetry)
3859
4468
  attemptTelemetries.push({
3860
4469
  attemptId,
@@ -3865,6 +4474,7 @@ export class Orchestrator {
3865
4474
  attemptId,
3866
4475
  harnessId: outcome.harnessId,
3867
4476
  status: outcome.status,
4477
+ outcomeClass: outcome.outcomeClass,
3868
4478
  error: outcome.error,
3869
4479
  });
3870
4480
  if (outcome.status !== "success") {
@@ -3878,7 +4488,7 @@ export class Orchestrator {
3878
4488
  reason: "planner_failed",
3879
4489
  });
3880
4490
  }
3881
- else if (fallbackFrom || next === undefined) {
4491
+ else if (!input.signal?.aborted && (fallbackFrom || next === undefined)) {
3882
4492
  log.emit("route.fallback.exhausted", {
3883
4493
  harness_id: outcome.harnessId,
3884
4494
  attempt_id: attemptId,
@@ -3927,6 +4537,7 @@ export class Orchestrator {
3927
4537
  ledger,
3928
4538
  planAttempts,
3929
4539
  attemptTelemetries,
4540
+ budgetDenial: planBudgetDenial,
3930
4541
  }, "all planners failed");
3931
4542
  }
3932
4543
  if (input.signal?.aborted) {
@@ -4017,6 +4628,58 @@ export class Orchestrator {
4017
4628
  ? "metered"
4018
4629
  : "unknown";
4019
4630
  }
4631
+ /**
4632
+ * #27 / D-6: build the engine-side deps closure for the deep-scan bounded
4633
+ * synthesis reducer (packages/orchestrator/src/deepScanReducer.ts owns the
4634
+ * spawn/stream/settle machinery). The closure keeps the private
4635
+ * route/session/knob machinery HERE and hands the module only finished public
4636
+ * types (a `HarnessRunSpec`, cost evidence, a disposable home).
4637
+ */
4638
+ deepScanReducerDeps(input, contract, log) {
4639
+ return {
4640
+ newReadOnlyHome: () => resolveReadOnlyRouteContext(this.execRootOf(input)),
4641
+ costEvidence: (harnessId, attemptId) =>
4642
+ // The reducer admits under a finite estimate floor (mirror of the n>1
4643
+ // scout reserve) so a subscription route is not refused for lacking a
4644
+ // cash quote.
4645
+ attemptCostEvidence(harnessId, attemptId, this.estimateUsdFloor(input.repoRoot), this.routeBillingKnowledge(input, harnessId)),
4646
+ buildSpec: (routed, homeEnv, prompt, attemptId) => {
4647
+ const knobs = this.routeSpecKnobs(routed, contract, undefined, input.effort);
4648
+ const effectiveWeb = this.discloseWebUpgrade(log, routed, knobs.webPolicy, attemptId);
4649
+ const sessionFields = this.sessionSpecFields(input, routed.adapter.id, log);
4650
+ const spec = HarnessRunSpec.parse({
4651
+ session_id: newId("ses"),
4652
+ intent: "synthesize",
4653
+ prompt,
4654
+ cwd: this.execRootOf(input),
4655
+ access: "readonly",
4656
+ attachments: [],
4657
+ auth_preference: sessionFields.auth_preference,
4658
+ credential_profile: sessionFields.credential_profile,
4659
+ // A FRESH session — the reducer never resumes a scout's conversation.
4660
+ resume_session_id: null,
4661
+ ...this.harnessSpecKnobs(contract, knobs, "synthesize"),
4662
+ env_inheritance: envInheritance(this.config(input.repoRoot)),
4663
+ env: homeEnv,
4664
+ });
4665
+ // D-16: compile the WorkReport transport onto the reducer spec (the
4666
+ // reducer is non-interactive) so its output is unwrapped + finalized
4667
+ // through the shared attempt contract, not a fourth deliverable predicate.
4668
+ const workReportMode = this.applyWorkEnvelope(spec, this.workReportEnvelopeFor(routed, contract, false));
4669
+ return {
4670
+ spec,
4671
+ webPolicy: knobs.webPolicy,
4672
+ effectiveWeb,
4673
+ model: knobs.model,
4674
+ workReportMode,
4675
+ };
4676
+ },
4677
+ hardTimeoutMs: reviewerTimeoutMs(this.config(input.repoRoot)),
4678
+ inactivityTimeoutMs: harnessInactivityTimeoutMs(this.config(input.repoRoot)),
4679
+ webRequired: contract.external_context.web_required,
4680
+ quotaEventSink: this.deps.quotaEventSink,
4681
+ };
4682
+ }
4020
4683
  async runReadOnlyReport(input, opts, announce) {
4021
4684
  const taskId = input.taskId ?? newId("task");
4022
4685
  const runId = input.runId ?? newId("run");
@@ -4066,7 +4729,10 @@ export class Orchestrator {
4066
4729
  const laneRun = Boolean(input.threadId) && opts.mode === "ask" && !opts.deepScan;
4067
4730
  let adapters;
4068
4731
  try {
4069
- adapters = await this.resolveCandidateAdapters({ ...input, prompt, n: width }, opts.intent, ledger, log, roHome);
4732
+ adapters = await this.resolveCandidateAdapters({ ...input, prompt, n: width }, opts.intent, ledger, log, roHome, runId,
4733
+ // Deep-scan repeats a surviving harness to reach scout width; a dropped
4734
+ // lane must not clamp coverage (QA-043 clamp is best-of-only).
4735
+ opts.deepScan === true);
4070
4736
  if (!opts.deepScan) {
4071
4737
  const seen = new Set();
4072
4738
  adapters = adapters.filter((routed) => {
@@ -4081,11 +4747,15 @@ export class Orchestrator {
4081
4747
  roHome.dispose();
4082
4748
  const message = safeErrorMessage(err);
4083
4749
  store.writeText(join(paths.contextDir, "context_error.md"), `# Routing Error\n\n${message}\n`);
4750
+ // A routing preflight refusal is a config_error, not harness_unavailable
4751
+ // (A-1/#22): classify identically across every strategy's routing catch.
4752
+ const routingFailure = routingFailureClassification(err);
4084
4753
  writeFailure(store, paths, {
4085
4754
  phase: "routing",
4086
- category: "harness_unavailable",
4755
+ category: routingFailure.category,
4087
4756
  safeMessage: message,
4088
4757
  runDir: paths.root,
4758
+ ...(routingFailure.nextActions ? { nextActions: routingFailure.nextActions } : {}),
4089
4759
  });
4090
4760
  store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${opts.mode})\n\n- Lifecycle: failed\n- Phase: routing\n\n${message}\n`);
4091
4761
  log.emit("output.ready", { kind: "summary", path: "final/summary.md", state: "diagnostic" });
@@ -4113,34 +4783,101 @@ export class Orchestrator {
4113
4783
  const attemptTelemetries = [];
4114
4784
  let fallbackOpen = false;
4115
4785
  let budgetStopped = false;
4786
+ // QA-050: keep the ledger's TYPED denial (not just a boolean) so the
4787
+ // terminal names the budget sub-code, the refused route/slot, and budget
4788
+ // remediation instead of a harness auth/setup template. First denial wins —
4789
+ // it is the decisive pre-spawn refusal.
4790
+ let budgetDenial = null;
4116
4791
  // in a swarm the same harness appears in several slots; resuming the
4117
4792
  // ONE native session id from all of them races the vendor's session store
4118
4793
  // (and is semantically wrong — N explorers continuing one conversation).
4119
4794
  // Grant resume to the first slot of each harness only; the rest run fresh.
4120
4795
  const resumeGranted = new Set();
4121
- const runReadonlyAttempt = async (routed, idx, modelOverride) => {
4796
+ // QA-019 disclosure: a scout the budget gate refused before spawn is
4797
+ // recorded as a failed attempt with a placeholder telemetry and a
4798
+ // budget_denied marker. It enters the denominator (honest 1/2), omissions,
4799
+ // and telemetry.yaml, and the marker lets the all-denied terminal still
4800
+ // route through the QA-050 budget classifier (never harness_error).
4801
+ const recordBudgetDeniedScout = (harnessId, attemptId, reason) => {
4802
+ const telemetry = createAttemptTelemetry(contract.external_context.policy, contract.external_context.web_required, contract.external_context.effective_mode);
4803
+ const error = `budget denied before spawn: ${reason}`;
4804
+ setAttemptOutcome(telemetry, {
4805
+ deliverablePresent: false,
4806
+ gatesPassed: null,
4807
+ harnessErrored: false,
4808
+ webRequiredUnsatisfied: false,
4809
+ });
4810
+ attempts.push({
4811
+ attemptId,
4812
+ harnessId,
4813
+ status: "failed",
4814
+ report: "",
4815
+ error,
4816
+ telemetry,
4817
+ budgetDenied: true,
4818
+ });
4819
+ attemptTelemetries.push({ attemptId, harnessId, telemetry });
4820
+ if (opts.deepScan) {
4821
+ store.writeText(join(paths.findingsDir, `${attemptId}-budget-denied.md`), `# Explorer ${attemptId} not started\n\n${error}\n`);
4822
+ }
4823
+ };
4824
+ const runReadonlyAttempt = async (routed, idx, modelOverride,
4825
+ // D-16d: a one-shot continuation re-run injects its checkpoint packet
4826
+ // pointer here; the attempt runs a FRESH session (resume is never granted
4827
+ // to a same-adapter follow-up slot) and is tagged `-cont`.
4828
+ continuationPointer,
4829
+ // D-16d: fired exactly once AFTER the budget lease is granted and BEFORE the
4830
+ // attempt streams — the continuation caller emits run.continuation here so
4831
+ // the disclosure never precedes (or outlives) a denied lease. The result
4832
+ // carries the denial reason so a refusal discloses run.continuation.denied.
4833
+ onLaunch) => {
4122
4834
  const adapter = routed.adapter;
4123
- const attemptId = modelOverride
4124
- ? `a${String(idx + 1).padStart(2, "0")}-fb`
4125
- : `a${String(idx + 1).padStart(2, "0")}`;
4835
+ const attemptId = continuationPointer
4836
+ ? `a${String(idx + 1).padStart(2, "0")}-cont`
4837
+ : modelOverride
4838
+ ? `a${String(idx + 1).padStart(2, "0")}-fb`
4839
+ : `a${String(idx + 1).padStart(2, "0")}`;
4126
4840
  const budgetSignalState = { quotaPressureDisclosed: false };
4127
4841
  const lease = ledger.reserve({
4128
4842
  taskId,
4129
4843
  attemptId,
4130
4844
  intent: opts.intent,
4131
4845
  harnessId: adapter.id,
4132
- cost: attemptCostEvidence(adapter.id, attemptId, undefined, this.routeBillingKnowledge(input, adapter.id)),
4846
+ // QA-019: an n>1 deep-scan scout admits under a FINITE estimate floor
4847
+ // (mirror of the candidate loop): the first scout reserves without a
4848
+ // floor, but later scouts pass the repo's usd floor so a subscription
4849
+ // swarm is not refused for lacking a per-attempt cash quote under a cap.
4850
+ cost: attemptCostEvidence(adapter.id, attemptId, opts.deepScan && idx > 0 ? this.estimateUsdFloor(input.repoRoot) : undefined, this.routeBillingKnowledge(input, adapter.id)),
4133
4851
  });
4134
4852
  if (!lease.granted) {
4135
4853
  log.emit("budget.lease.created", {
4136
4854
  granted: false,
4137
4855
  reason: lease.reason,
4856
+ denied: lease.denied,
4138
4857
  attempt_id: attemptId,
4139
4858
  harness_id: adapter.id,
4140
4859
  });
4141
4860
  budgetStopped = true;
4142
- return;
4861
+ budgetDenial ??= {
4862
+ code: lease.denied ?? "hard_cap",
4863
+ reason: lease.reason ?? "budget lease denied",
4864
+ harnessId: adapter.id,
4865
+ attemptId,
4866
+ };
4867
+ // QA-019 disclosure: a still-denied deep-scan scout must not vanish from
4868
+ // the denominator. Record a placeholder failed attempt with a
4869
+ // budget_denied marker so the explore-findings map counts it (1/2, not
4870
+ // 1/1), omissions and telemetry record the denial, and the all-denied
4871
+ // terminal still routes through the QA-050 budget classifier. The
4872
+ // sequential ask/audit path has no denominator — a denial there stays a
4873
+ // pure budget stop (no phantom failed attempt), preserving its terminal.
4874
+ if (opts.deepScan) {
4875
+ recordBudgetDeniedScout(adapter.id, attemptId, lease.reason ?? "budget lease denied");
4876
+ }
4877
+ return { status: "budget_denied", reason: lease.reason ?? "budget lease denied" };
4143
4878
  }
4879
+ // Lease granted: the attempt is now committed to run — disclose the launch.
4880
+ onLaunch?.();
4144
4881
  const knobs = this.routeSpecKnobs(routed, contract, modelOverride, input.effort);
4145
4882
  const effectiveWeb = this.discloseWebUpgrade(log, routed, knobs.webPolicy, attemptId);
4146
4883
  const explorerPrompt = (opts.deepScan
@@ -4157,12 +4894,15 @@ export class Orchestrator {
4157
4894
  const laneContinuity = laneRun
4158
4895
  ? await this.resolveContinuity(input, adapter.id, sessionFields.credential_profile?.profile_id ?? input.credentialProfileId ?? null, grantResume, store, paths, this.execRootOf(input), log)
4159
4896
  : null;
4897
+ // D-16d: the continuation packet pointer rides after the lane pointer so
4898
+ // the fresh session is re-grounded in the exhausted attempt's work.
4899
+ const promptWithPointers = [explorerPrompt, laneContinuity?.pointerLine, continuationPointer]
4900
+ .filter((p) => Boolean(p))
4901
+ .join("\n\n");
4160
4902
  let spec = HarnessRunSpec.parse({
4161
4903
  session_id: newId("ses"),
4162
4904
  intent: opts.intent,
4163
- prompt: laneContinuity?.pointerLine
4164
- ? `${explorerPrompt}\n\n${laneContinuity.pointerLine}`
4165
- : explorerPrompt,
4905
+ prompt: promptWithPointers,
4166
4906
  cwd: this.execRootOf(input),
4167
4907
  access: "readonly",
4168
4908
  // ASK/EXPLORE/AUDIT read-only runs must forward the user's attachments —
@@ -4186,6 +4926,9 @@ export class Orchestrator {
4186
4926
  const reportInteraction = this.interactionChannelFor(input, log, runId, taskId, attemptId, adapter.id, routed.supportsInteractive);
4187
4927
  if (reportInteraction)
4188
4928
  spec.extra["interactionChannel"] = reportInteraction;
4929
+ // D-16: compile the WorkReport envelope for the read-only lane.
4930
+ const readonlyWorkEnvelope = this.workReportEnvelopeFor(routed, contract, Boolean(reportInteraction));
4931
+ const readonlyWorkMode = this.applyWorkEnvelope(spec, readonlyWorkEnvelope);
4189
4932
  const attemptEventsPath = join(paths.attemptsDir, attemptId, "events.jsonl");
4190
4933
  const answer = new AnswerAssembly();
4191
4934
  const telemetry = createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required ||
@@ -4289,9 +5032,13 @@ export class Orchestrator {
4289
5032
  }
4290
5033
  catch (err) {
4291
5034
  harnessError = safeErrorMessage(err);
5035
+ // #31: classify the throw so the retry gate and required-actions read
5036
+ // a typed category (watchdog timeout vs process crash).
5037
+ telemetry.transientFailures.push(classifyAdapterThrow({ errorName: err instanceof Error ? err.name : null }));
4292
5038
  }
4293
- const transient = telemetry.transientFailures.at(-1) ?? null;
4294
- const sawTransient = telemetry.transientFailures.length > transientStart;
5039
+ const newTransients = telemetry.transientFailures.slice(transientStart);
5040
+ const transient = newTransients.at(-1) ?? null;
5041
+ const sawRetryable = newTransients.some((f) => f.retryable);
4295
5042
  const sawTypedLimit = telemetry.rateLimits.length > rateLimitStart;
4296
5043
  const reportSoFar = answer.text();
4297
5044
  // W5.4 reactive failover, READ-ONLY lane (same contract as the
@@ -4319,7 +5066,7 @@ export class Orchestrator {
4319
5066
  }
4320
5067
  }
4321
5068
  if (!harnessError ||
4322
- !sawTransient ||
5069
+ !sawRetryable ||
4323
5070
  reportSoFar.length > 0 ||
4324
5071
  nativeTry >= retryPolicy.maxRetries ||
4325
5072
  input.signal?.aborted)
@@ -4330,6 +5077,7 @@ export class Orchestrator {
4330
5077
  harness_id: adapter.id,
4331
5078
  attempt_id: attemptId,
4332
5079
  kind: transient?.kind ?? "unknown",
5080
+ category: transient?.category ?? "unknown_harness_error",
4333
5081
  native_try: nativeTry + 1,
4334
5082
  });
4335
5083
  log.emit("route.transient.retry_scheduled", {
@@ -4350,26 +5098,46 @@ export class Orchestrator {
4350
5098
  log.emit("route.transient.exhausted", {
4351
5099
  harness_id: adapter.id,
4352
5100
  attempt_id: attemptId,
5101
+ category: telemetry.transientFailures.at(-1)?.category ?? "unknown_harness_error",
4353
5102
  retries: retryPolicy.maxRetries,
4354
5103
  });
4355
5104
  }
4356
5105
  attemptTelemetries.push({ attemptId, harnessId: adapter.id, telemetry });
4357
- const report = redactSecrets(answer.text());
5106
+ // D-16: un-nest the {work_report, output} envelope; the OUTPUT is the report.
5107
+ const roUnwrapped = unwrapWorkReportEnvelope(answer.machineText() ?? "", readonlyWorkMode, {
5108
+ sideToolReport: telemetry.sideToolWorkReport ?? undefined,
5109
+ });
5110
+ const report = redactSecrets(roUnwrapped.deliverable);
4358
5111
  const unrecovered = unrecoveredToolErrors(telemetry);
4359
5112
  const webBlocked = webUnsatisfied(telemetry);
4360
- const deliverablePresent = report.length > 0;
5113
+ const reportPresent = report.length > 0;
4361
5114
  if (!harnessError && webBlocked) {
4362
5115
  harnessError = `web evidence unsatisfied: ${telemetry.web.errorSummary ?? (telemetry.web.attempted ? "web tool failed without verified recovery" : "web evidence required but never attempted")}`;
4363
5116
  }
4364
- if (!harnessError && unrecovered.length > 0 && !deliverablePresent) {
5117
+ if (!harnessError && unrecovered.length > 0 && !reportPresent) {
4365
5118
  const first = unrecovered[0];
4366
5119
  harnessError = `${first.tool} failed without recovery: ${first.summary}`;
4367
5120
  }
5121
+ const roFinalized = finalizeAttempt({
5122
+ deliverableEvidence: reportPresent,
5123
+ harnessErrored: harnessError !== null && !webBlocked,
5124
+ workReport: roUnwrapped.workReport,
5125
+ workReportSource: roUnwrapped.source,
5126
+ workReportViolation: roUnwrapped.contractViolation,
5127
+ contextTerminalExhausted: telemetry.contextExhausted,
5128
+ });
5129
+ // A broken WorkReport contract is a hard failure ONLY when the finalizer
5130
+ // ranked it so — a concurrent terminal context exhaustion outranks it
5131
+ // (interrupted, not a contract failure). Let the finalizer own precedence.
5132
+ if (!harnessError && roFinalized.outcomeClass === "contract_failure") {
5133
+ harnessError = `work_report contract: ${roUnwrapped.contractViolation}`;
5134
+ }
4368
5135
  setAttemptOutcome(telemetry, {
4369
- deliverablePresent,
5136
+ deliverablePresent: roFinalized.deliverablePresent,
4370
5137
  gatesPassed: null,
4371
5138
  harnessErrored: harnessError !== null && !webBlocked,
4372
5139
  webRequiredUnsatisfied: webBlocked,
5140
+ workState: roFinalized.workState,
4373
5141
  });
4374
5142
  if (harnessError) {
4375
5143
  log.emit("harness.completed", {
@@ -4390,21 +5158,25 @@ export class Orchestrator {
4390
5158
  if (opts.deepScan) {
4391
5159
  store.writeText(join(paths.findingsDir, `${attemptId}-error.md`), `# Explorer ${attemptId} failed\n\n${harnessError}\n`);
4392
5160
  }
4393
- return;
5161
+ return { status: "launched" };
4394
5162
  }
5163
+ // D-16 r8: an interrupted scout is a FAILED omission, never reducer input;
5164
+ // the sequential ask/audit winner folds at its own terminal (stays success).
5165
+ const scoutInterrupted = opts.deepScan && roFinalized.outcomeClass === "interrupted";
4395
5166
  log.emit("harness.completed", {
4396
5167
  harness_id: adapter.id,
4397
5168
  attempt_id: attemptId,
4398
- status: "success",
5169
+ status: scoutInterrupted ? "interrupted" : "success",
4399
5170
  ...telemetrySummary(telemetry),
4400
5171
  });
4401
5172
  attempts.push({
4402
5173
  attemptId,
4403
5174
  harnessId: adapter.id,
4404
- status: "success",
5175
+ status: scoutInterrupted ? "failed" : "success",
4405
5176
  report: report || "(no output)",
4406
- error: null,
5177
+ error: scoutInterrupted ? "context capacity exhausted before the scout completed" : null,
4407
5178
  telemetry,
5179
+ ...(scoutInterrupted ? { interrupted: true } : {}),
4408
5180
  });
4409
5181
  if (opts.deepScan) {
4410
5182
  const warningNote = toolWarnings(telemetry).length
@@ -4412,22 +5184,82 @@ export class Orchestrator {
4412
5184
  .map((e) => `${e.tool}: ${e.summary}`)
4413
5185
  .join("; ")}\n`
4414
5186
  : "";
4415
- store.writeText(join(paths.findingsDir, `${attemptId}.md`), `# Explorer ${attemptId} (${adapter.id})\n\n${report || "(no output)"}${warningNote}\n`);
5187
+ const scoutTag = scoutInterrupted
5188
+ ? " interrupted (context capacity exhausted)"
5189
+ : ` (${adapter.id})`;
5190
+ store.writeText(join(paths.findingsDir, `${attemptId}${scoutInterrupted ? "-interrupted" : ""}.md`), `# Explorer ${attemptId}${scoutTag}\n\n${report || "(no output)"}${warningNote}\n`);
4416
5191
  }
5192
+ return { status: "launched" };
4417
5193
  };
4418
5194
  try {
4419
5195
  if (opts.deepScan) {
4420
- // Explorer swarm runs in parallel (bounded), mirroring parallel candidates.
4421
- await runBounded(adapters, Math.min(adapters.length, MAX_PARALLEL_CANDIDATES), runReadonlyAttempt);
5196
+ // Explorer swarm runs in parallel (bounded), mirroring parallel
5197
+ // candidates. The swarm has no continuation lane, so the launched/denied
5198
+ // return is unused here.
5199
+ await runBounded(adapters, Math.min(adapters.length, MAX_PARALLEL_CANDIDATES), async (routed, idx) => {
5200
+ await runReadonlyAttempt(routed, idx);
5201
+ });
4422
5202
  }
4423
5203
  else {
4424
5204
  // ask/audit: sequential fallback chain — first success wins; a blocked
4425
5205
  // attempt opens a fallback arc to the next eligible harness.
5206
+ let continuationCount = 0; // D-16d: one-shot budget across the chain
4426
5207
  for (const [idx, routed] of adapters.entries()) {
4427
5208
  if (input.signal?.aborted)
4428
5209
  break;
4429
5210
  await runReadonlyAttempt(routed, idx);
4430
5211
  let last = attempts[attempts.length - 1];
5212
+ // D-16d one-shot continuation: an ELIGIBLE terminal context exhaustion
5213
+ // (repeated_refill, no completed report) gets ONE fresh-session re-run,
5214
+ // re-grounded by a mechanical checkpoint packet. On completion the
5215
+ // exhausted attempt is superseded so the continuation wins the terminal.
5216
+ if (last?.status === "success" && continuationCount === 0 && !budgetStopped) {
5217
+ const decision = decideContinuation({
5218
+ contextExhausted: last.telemetry.contextExhausted,
5219
+ contextExhaustedCause: last.telemetry.contextExhaustedCause,
5220
+ workStateCompleted: last.telemetry.outcome?.workState?.state === "completed",
5221
+ continuationCount,
5222
+ runKind: "read_only",
5223
+ });
5224
+ if (decision.eligible) {
5225
+ const exhausted = last;
5226
+ const packet = buildContinuationPacket(synthesizeContinuationRequest({
5227
+ harness: exhausted.harnessId,
5228
+ profileId: input.credentialProfileId ?? null,
5229
+ priorPrompt: prompt,
5230
+ priorOutput: exhausted.report,
5231
+ }));
5232
+ // The continuation lease is reserved INSIDE runReadonlyAttempt; emit
5233
+ // run.continuation via onLaunch (fires only AFTER the grant, before the
5234
+ // stream) so a denied lease never leaves a false "launched" disclosure
5235
+ // nor consumes the one-shot. A refusal emits run.continuation.denied.
5236
+ const outcome = await runReadonlyAttempt(routed, idx, undefined, packet.pointerLine ?? undefined, () => {
5237
+ continuationCount += 1;
5238
+ log.emit("run.continuation", {
5239
+ from_attempt: exhausted.attemptId,
5240
+ cause: last.telemetry.contextExhaustedCause,
5241
+ continuation_count: continuationCount,
5242
+ packet_turns: packet.continuity.disclosure.packetTurns,
5243
+ });
5244
+ });
5245
+ if (outcome.status === "budget_denied") {
5246
+ log.emit("run.continuation.denied", {
5247
+ from_attempt: exhausted.attemptId,
5248
+ cause: last.telemetry.contextExhaustedCause,
5249
+ reason: outcome.reason,
5250
+ });
5251
+ }
5252
+ else {
5253
+ const cont = attempts[attempts.length - 1];
5254
+ if (cont && cont !== exhausted && cont.status === "success") {
5255
+ exhausted.status = "failed";
5256
+ exhausted.error =
5257
+ exhausted.error ?? "superseded by one-shot continuation (context exhausted)";
5258
+ last = cont;
5259
+ }
5260
+ }
5261
+ }
5262
+ }
4431
5263
  // Per-harness fallback_model: one same-harness retry on FAILURE (not
4432
5264
  // policy blocks) before falling through to the next harness.
4433
5265
  const fallbackModel = routed.settings?.fallbackModel;
@@ -4498,18 +5330,27 @@ export class Orchestrator {
4498
5330
  // injected API-route state. Vendor-owned native credentials were not copied.
4499
5331
  roHome.dispose();
4500
5332
  }
4501
- if (input.signal?.aborted) {
4502
- return cancelledResult(log, runId, taskId, opts.mode, paths.root, attempts.map((a) => ({
4503
- attemptId: a.attemptId,
4504
- harnessId: a.harnessId,
4505
- status: a.status,
4506
- })), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, opts.mode, attemptTelemetries, null), ledger.spend(), input.signal, store);
4507
- }
5333
+ const candidateSummaries = attempts.map((a) => ({
5334
+ attemptId: a.attemptId,
5335
+ harnessId: a.harnessId,
5336
+ status: a.status,
5337
+ }));
5338
+ 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);
5339
+ if (input.signal?.aborted)
5340
+ return cancelledTerminal();
4508
5341
  const succeededReadonly = attempts.filter((a) => a.status === "success");
4509
5342
  if (!opts.deepScan && succeededReadonly.length === 0) {
4510
5343
  const last = attempts[attempts.length - 1];
4511
5344
  const webBlocked = attempts.some((a) => a.status === "blocked");
4512
- const singleError = last?.error ?? (budgetStopped ? "budget exhausted before any attempt" : "harness failed");
5345
+ // QA-050: a budget refusal is a BUDGET failure, not a harness one route
5346
+ // it through the shared classifier so phase/category/code/route and the
5347
+ // remediation are budget-typed (never auth/setup) across every mode.
5348
+ const budgetMapping = budgetStopped && !webBlocked
5349
+ ? classifyBudgetFailure({ denial: budgetDenial, terminal: ledger.terminal() })
5350
+ : null;
5351
+ const singleError = budgetMapping?.safeMessage ??
5352
+ last?.error ??
5353
+ (budgetStopped ? "budget exhausted before any attempt" : "harness failed");
4513
5354
  if (fallbackOpen || webBlocked) {
4514
5355
  log.emit("route.fallback.exhausted", {
4515
5356
  harness_id: last?.harnessId ?? null,
@@ -4529,29 +5370,47 @@ export class Orchestrator {
4529
5370
  });
4530
5371
  }
4531
5372
  this.writeRunTelemetry(store, paths, contract, runId, taskId, opts.mode, attemptTelemetries, null);
4532
- store.writeText(join(paths.contextDir, "context_error.md"), `# Harness Error\n\n${singleError}\n`);
4533
- writeFailure(store, paths, {
4534
- phase: "harness",
4535
- category: webBlocked ? "policy" : budgetStopped ? "budget" : "harness_error",
4536
- harnessId: last?.harnessId,
4537
- attemptId: last?.attemptId,
4538
- safeMessage: singleError,
4539
- eventRefs: attempts.map((a) => `attempts/${a.attemptId}/events.jsonl`),
4540
- runDir: paths.root,
4541
- nextActions: ["Open diagnostics", "Check harness authentication", "Retry after setup"],
5373
+ store.writeText(join(paths.contextDir, "context_error.md"), `# ${budgetMapping ? "Budget Denied" : "Harness Error"}\n\n${singleError}\n`);
5374
+ const roEventRefs = attempts.map((a) => `attempts/${a.attemptId}/events.jsonl`);
5375
+ if (budgetMapping) {
5376
+ writeFailure(store, paths, budgetFailureRecord(budgetMapping, { eventRefs: roEventRefs, runDir: paths.root }));
5377
+ }
5378
+ else {
5379
+ // #31: classify the harness cause across the read-only attempts so auth
5380
+ // guidance appears only on a real auth failure.
5381
+ const roCategory = dominantHarnessFailureCategory(attemptTelemetries.flatMap((a) => a.telemetry.transientFailures));
5382
+ writeFailure(store, paths, {
5383
+ phase: "harness",
5384
+ category: webBlocked ? "policy" : "harness_error",
5385
+ harnessId: last?.harnessId,
5386
+ attemptId: last?.attemptId,
5387
+ safeMessage: singleError,
5388
+ eventRefs: roEventRefs,
5389
+ runDir: paths.root,
5390
+ nextActions: harnessFailureNextActions(roCategory),
5391
+ });
5392
+ }
5393
+ // QA-036: re-check the DELIVERABLE through the shared finalizer helper —
5394
+ // a blocked Ask that produced NO answer can no longer read as a succeeded
5395
+ // "Needs review" run (exit 0); it is an honest failure (exit 1).
5396
+ const roTerminal = readOnlyNoSuccessTerminal({
5397
+ webBlocked,
5398
+ hasDeliverable: partialReport.trim().length > 0,
5399
+ budgetStopped,
5400
+ attemptsCount: attempts.length,
5401
+ });
5402
+ const terminalFacts = makeOutcomeFacts(roTerminal.lifecycle, {
5403
+ ...(roTerminal.review ? { review: roTerminal.review } : {}),
5404
+ reason: roTerminal.reason,
4542
5405
  });
4543
- const terminalFacts = webBlocked
4544
- ? makeOutcomeFacts("succeeded", { review: "blocked", reason: "review_blocked" })
4545
- : budgetStopped && attempts.length === 0
4546
- ? makeOutcomeFacts("failed", { reason: "budget_exhausted" })
4547
- : makeOutcomeFacts("failed", { reason: "harness_failed" });
4548
- store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${opts.mode})\n\n- Harness: ${last?.harnessId ?? "none"}\n- Lifecycle: ${terminalFacts.lifecycle}${terminalFacts.reason ? ` (${terminalFacts.reason})` : ""}\n\n${singleError}\n`);
5406
+ const terminalHarnessId = budgetMapping?.harnessId ?? last?.harnessId;
5407
+ 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`);
4549
5408
  log.emit("output.ready", { kind: "summary", path: "final/summary.md", state: "diagnostic" });
4550
5409
  if (terminalFacts.lifecycle === "succeeded") {
4551
5410
  log.emit("run.blocked", {
4552
5411
  lifecycle: terminalFacts.lifecycle,
4553
5412
  facts: terminalFacts,
4554
- harness_id: last?.harnessId,
5413
+ harness_id: terminalHarnessId,
4555
5414
  error: singleError,
4556
5415
  failure_ref: "final/failure.yaml",
4557
5416
  });
@@ -4561,7 +5420,8 @@ export class Orchestrator {
4561
5420
  lifecycle: terminalFacts.lifecycle,
4562
5421
  facts: terminalFacts,
4563
5422
  reason: terminalFacts.reason,
4564
- harness_id: last?.harnessId,
5423
+ phase: budgetMapping?.phase,
5424
+ harness_id: terminalHarnessId,
4565
5425
  error: singleError,
4566
5426
  failure_ref: "final/failure.yaml",
4567
5427
  });
@@ -4585,46 +5445,62 @@ export class Orchestrator {
4585
5445
  }
4586
5446
  const succeeded = succeededReadonly;
4587
5447
  if (opts.deepScan && succeeded.length === 0) {
4588
- const message = attempts
4589
- .map((a) => `${a.attemptId}/${a.harnessId}: ${a.error ?? "failed"}`)
4590
- .join("\n");
4591
5448
  const blocked = attempts.some((a) => a.status === "blocked");
4592
- this.writeRunTelemetry(store, paths, contract, runId, taskId, opts.mode, attemptTelemetries, null);
4593
- writeFailure(store, paths, {
4594
- phase: "harness",
4595
- category: blocked ? "policy" : "harness_error",
4596
- safeMessage: message || "all explorers failed",
4597
- eventRefs: attempts.map((a) => `attempts/${a.attemptId}/events.jsonl`),
4598
- runDir: paths.root,
4599
- nextActions: [
4600
- "Open diagnostics",
4601
- "Check harness authentication",
4602
- "Reduce explore width",
4603
- "Retry after setup",
4604
- ],
4605
- });
4606
- store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${opts.mode})\n\n- Lifecycle: ${blocked ? "succeeded (needs review)" : "failed"}\n\n${message}\n`);
4607
- log.emit("output.ready", { kind: "summary", path: "final/summary.md", state: "diagnostic" });
4608
- const scanFailFacts = blocked
4609
- ? makeOutcomeFacts("succeeded", { review: "blocked", reason: "review_blocked" })
4610
- : makeOutcomeFacts("failed", { reason: "harness_failed" });
4611
- if (blocked)
4612
- log.emit("run.blocked", {
4613
- lifecycle: scanFailFacts.lifecycle,
4614
- facts: scanFailFacts,
4615
- phase: "harness",
4616
- error: message,
4617
- failure_ref: "final/failure.yaml",
5449
+ // QA-050/QA-019: an all-denied scan (finite-zero, or every scout refused
5450
+ // before spawn) is a BUDGET failure, not harness_error — route it through
5451
+ // the shared classifier. Only a pure-denial scan (no scout actually errored
5452
+ // in the harness) qualifies, so a real explorer failure is never masked.
5453
+ const scanBudgetMapping = budgetStopped && !blocked && attempts.every((a) => a.budgetDenied === true)
5454
+ ? classifyBudgetFailure({ denial: budgetDenial, terminal: ledger.terminal() })
5455
+ : null;
5456
+ // D-16 r8: ALL scouts out of context → the aggregate is interrupted.
5457
+ const allInterrupted = attempts.length > 0 && attempts.every((a) => a.interrupted === true);
5458
+ const scanFailFacts = allInterrupted
5459
+ ? makeOutcomeFacts("interrupted", { reason: "context_capacity_exhausted" })
5460
+ : makeOutcomeFacts("failed", {
5461
+ reason: scanBudgetMapping ? scanBudgetMapping.reason : "harness_failed",
4618
5462
  });
4619
- else
4620
- log.emit("run.failed", {
4621
- lifecycle: scanFailFacts.lifecycle,
4622
- facts: scanFailFacts,
4623
- reason: scanFailFacts.reason,
5463
+ const message = scanBudgetMapping
5464
+ ? scanBudgetMapping.safeMessage
5465
+ : attempts.map((a) => `${a.attemptId}/${a.harnessId}: ${a.error ?? "failed"}`).join("\n");
5466
+ this.writeRunTelemetry(store, paths, contract, runId, taskId, opts.mode, attemptTelemetries, null);
5467
+ if (scanBudgetMapping) {
5468
+ writeFailure(store, paths, budgetFailureRecord(scanBudgetMapping, {
5469
+ eventRefs: attempts.map((a) => `attempts/${a.attemptId}/events.jsonl`),
5470
+ runDir: paths.root,
5471
+ }));
5472
+ }
5473
+ else {
5474
+ // #31: classify the scout failures; keep the scan-specific width hint but
5475
+ // drop the unconditional auth line unless the cause was a real auth failure.
5476
+ const scanCategory = dominantHarnessFailureCategory(attemptTelemetries.flatMap((a) => a.telemetry.transientFailures));
5477
+ writeFailure(store, paths, {
4624
5478
  phase: "harness",
4625
- error: message,
4626
- failure_ref: "final/failure.yaml",
5479
+ category: blocked ? "policy" : "harness_error",
5480
+ safeMessage: message || "all explorers failed",
5481
+ eventRefs: attempts.map((a) => `attempts/${a.attemptId}/events.jsonl`),
5482
+ runDir: paths.root,
5483
+ nextActions: [
5484
+ ...harnessFailureNextActions(scanCategory).filter((a) => !a.startsWith("Retry")),
5485
+ "Reduce explore width",
5486
+ "Retry after setup",
5487
+ ],
4627
5488
  });
5489
+ }
5490
+ // QA-036: with ZERO successful explorers there is no synthesizable
5491
+ // deliverable, so a blocked scan can no longer read as a succeeded
5492
+ // "needs review" run (exit 0). An empty scan is a failure whether the
5493
+ // explorers were blocked or errored.
5494
+ store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${opts.mode})\n\n- Lifecycle: ${scanFailFacts.lifecycle}${scanFailFacts.reason ? ` (${scanFailFacts.reason})` : ""}\n\n${message}\n`);
5495
+ log.emit("output.ready", { kind: "summary", path: "final/summary.md", state: "diagnostic" });
5496
+ log.emit("run.failed", {
5497
+ lifecycle: scanFailFacts.lifecycle,
5498
+ facts: scanFailFacts,
5499
+ reason: scanFailFacts.reason,
5500
+ phase: scanBudgetMapping ? scanBudgetMapping.phase : "harness",
5501
+ error: message,
5502
+ failure_ref: "final/failure.yaml",
5503
+ });
4628
5504
  return {
4629
5505
  spendUsd: ledger.spend(),
4630
5506
  runId,
@@ -4643,45 +5519,54 @@ export class Orchestrator {
4643
5519
  };
4644
5520
  }
4645
5521
  const unsuccessful = attempts.filter((a) => a.status !== "success");
4646
- const report = opts.deepScan
4647
- ? [
4648
- `Explorers succeeded: ${succeeded.length}/${attempts.length}.`,
4649
- "",
4650
- "## Synthesis",
4651
- ...succeeded.map((a) => {
4652
- const warnings = toolWarnings(a.telemetry);
4653
- const warningText = warnings.length
4654
- ? `\n\n> Tool warnings: ${warnings.map((e) => `${e.tool}: ${e.summary}`).join("; ")}`
4655
- : "";
4656
- return `\n### ${a.attemptId} / ${a.harnessId}\n\n${a.report}${warningText}`;
4657
- }),
4658
- "",
4659
- "## Omissions / Uncertainty",
4660
- ...(unsuccessful.length
4661
- ? unsuccessful.map((a) => `- ${a.attemptId} / ${a.harnessId} ${a.status}: ${a.error}`)
4662
- : [
4663
- "- No explorer failures recorded. Claims still need evidence review before edit execution.",
4664
- ]),
4665
- "",
4666
- "## Follow-up Questions",
4667
- "- Which findings should become a frozen implementation spec?",
4668
- "- Should web research be allowed for a second Explore pass?",
4669
- ].join("\n")
4670
- : (succeeded[0]?.report ?? "(no output)");
5522
+ // #27 / D-6: a multi-scout deep scan runs ONE bounded synthesis reducer over
5523
+ // the raw scout reports so the final artifact is a real merge, not a
5524
+ // concatenation. A single report (width-1) needs no merge; a failed/denied
5525
+ // reducer degrades to an HONEST raw scout bundle, never a fake synthesis. The
5526
+ // whole decision + reducer spawn lives in deepScanReducer.ts (its owner).
5527
+ let deepScanSynthesis = null;
5528
+ let reducedReport = null;
5529
+ if (opts.deepScan) {
5530
+ ({ deepScanSynthesis, reducedReport } = await resolveDeepScanSynthesis(this.deepScanReducerDeps(input, contract, log), {
5531
+ succeeded,
5532
+ adapters,
5533
+ budgetStopped,
5534
+ aborted: Boolean(input.signal?.aborted),
5535
+ taskId,
5536
+ goal: prompt,
5537
+ findingsDir: paths.findingsDir,
5538
+ ledger,
5539
+ log,
5540
+ paths,
5541
+ signal: input.signal,
5542
+ onHarnessEvent: input.onHarnessEvent,
5543
+ attemptTelemetries,
5544
+ }));
5545
+ }
5546
+ // INV-116: a cancel that landed WHILE the bounded reducer ran (scouts done,
5547
+ // synthesis in flight) is a cancelled terminal — never a laundered success.
5548
+ if (input.signal?.aborted)
5549
+ return cancelledTerminal();
5550
+ const report = !opts.deepScan
5551
+ ? (succeeded[0]?.report ?? "(no output)")
5552
+ : reducedReport !== null
5553
+ ? reducedReport
5554
+ : rawScoutBundle({ succeeded, unsuccessful, status: deepScanSynthesis });
4671
5555
  store.writeText(join(paths.finalDir, opts.artifactName), `# ${opts.title}\n\n${report}\n`);
4672
5556
  // ask is the only read-only strategy that can carry a structured-output
4673
- // contract (the boundary refuses the rest); validate the RAW answer text,
4674
- // not the titled artifact wrapper.
5557
+ // contract (the boundary refuses the rest); validate the FINAL aggregate
5558
+ // (the reduced synthesis, or the honest bundle for a degraded scan) — never
5559
+ // the first scout's raw report — and never the titled artifact wrapper.
4675
5560
  if (opts.mode === "ask" && contract.output_schema) {
4676
5561
  finalizeStructuredOutput({
4677
5562
  store,
4678
5563
  finalDir: paths.finalDir,
4679
5564
  log,
4680
5565
  schema: contract.output_schema,
4681
- answerText: succeeded[0]?.report ?? "",
5566
+ answerText: opts.deepScan ? report : (succeeded[0]?.report ?? ""),
4682
5567
  });
4683
5568
  }
4684
- this.writeRunTelemetry(store, paths, contract, runId, taskId, opts.mode, attemptTelemetries, opts.deepScan ? null : (succeeded[0]?.attemptId ?? null));
5569
+ this.writeRunTelemetry(store, paths, contract, runId, taskId, opts.mode, attemptTelemetries, opts.deepScan ? null : (succeeded[0]?.attemptId ?? null), deepScanSynthesis);
4685
5570
  log.emit("output.ready", {
4686
5571
  kind: opts.mode === "ask" ? "answer" : "report",
4687
5572
  path: `final/${opts.artifactName}`,
@@ -4712,8 +5597,31 @@ export class Orchestrator {
4712
5597
  // non-clean terminal is an aggregate paid-budget stop.
4713
5598
  let terminalFacts = makeOutcomeFacts("succeeded");
4714
5599
  const reportBudgetTerminal = ledger.terminal();
4715
- if (reportBudgetTerminal)
5600
+ if (reportBudgetTerminal) {
4716
5601
  terminalFacts = makeOutcomeFacts("failed", { reason: reportBudgetTerminal });
5602
+ }
5603
+ else if (!opts.deepScan) {
5604
+ // D-16: fold the winning read-only attempt's work_state into the terminal.
5605
+ // A terminal context exhaustion with no completed report ⇒ interrupted;
5606
+ // a needs_input/incomplete report ⇒ a succeeded run whose work_state
5607
+ // vetoes applyability and a clean exit (INV-116). answer.md was already
5608
+ // persisted from the unwrapped OUTPUT.
5609
+ const winnerTelemetry = succeeded[0]?.telemetry;
5610
+ const winnerWorkState = winnerTelemetry?.outcome?.workState;
5611
+ if (winnerTelemetry?.contextExhausted && winnerWorkState?.state !== "completed") {
5612
+ terminalFacts = makeOutcomeFacts("interrupted", { reason: "context_capacity_exhausted" });
5613
+ }
5614
+ else if (winnerWorkState?.state === "needs_input" ||
5615
+ winnerWorkState?.state === "incomplete") {
5616
+ terminalFacts = makeOutcomeFacts("succeeded", {
5617
+ reason: winnerWorkState.state === "needs_input" ? "input_required" : "work_incomplete",
5618
+ work_state: winnerWorkState,
5619
+ });
5620
+ }
5621
+ else if (winnerWorkState) {
5622
+ terminalFacts = makeOutcomeFacts("succeeded", { work_state: winnerWorkState });
5623
+ }
5624
+ }
4717
5625
  const harnessLabel = attempts
4718
5626
  .map((a) => `${a.attemptId}:${a.harnessId}:${a.status}`)
4719
5627
  .join(", ");
@@ -4732,13 +5640,17 @@ export class Orchestrator {
4732
5640
  },
4733
5641
  });
4734
5642
  log.emit("work_product.emitted", { kind: "report", winner: succeeded[0]?.attemptId ?? null });
4735
- if (terminalFacts.lifecycle === "failed") {
5643
+ const workVetoed = terminalFacts.work_state?.state === "needs_input" ||
5644
+ terminalFacts.work_state?.state === "incomplete";
5645
+ if (terminalFacts.lifecycle !== "succeeded") {
4736
5646
  writeFailure(store, paths, {
4737
5647
  phase: "executor",
4738
- category: "budget",
5648
+ category: terminalFacts.reason === "context_capacity_exhausted" ? "harness_error" : "budget",
4739
5649
  safeMessage: `read-only report ended ${terminalFacts.lifecycle}${terminalFacts.reason ? ` (${terminalFacts.reason.replaceAll("_", " ")})` : ""}`,
4740
5650
  runDir: paths.root,
4741
- nextActions: ["Inspect the report artifacts", "Adjust the budget and retry"],
5651
+ nextActions: terminalFacts.reason === "context_capacity_exhausted"
5652
+ ? ["Inspect the partial report", "Re-run with a narrower scope"]
5653
+ : ["Inspect the report artifacts", "Adjust the budget and retry"],
4742
5654
  });
4743
5655
  log.emit("run.failed", {
4744
5656
  lifecycle: terminalFacts.lifecycle,
@@ -4748,6 +5660,16 @@ export class Orchestrator {
4748
5660
  failure_ref: "final/failure.yaml",
4749
5661
  });
4750
5662
  }
5663
+ else if (workVetoed) {
5664
+ // D-16: a succeeded lifecycle whose work_state vetoes is a needs-me
5665
+ // terminal — run.blocked (not run.completed); the outcome-aware exit
5666
+ // projection returns non-zero from the same facts.
5667
+ log.emit("run.blocked", {
5668
+ lifecycle: terminalFacts.lifecycle,
5669
+ facts: terminalFacts,
5670
+ reason: terminalFacts.reason,
5671
+ });
5672
+ }
4751
5673
  else {
4752
5674
  log.emit("run.completed", {
4753
5675
  lifecycle: terminalFacts.lifecycle,
@@ -4765,11 +5687,7 @@ export class Orchestrator {
4765
5687
  winner: null,
4766
5688
  runDir: paths.root,
4767
5689
  summary: redactSecrets(report).slice(0, 400),
4768
- candidates: attempts.map((a) => ({
4769
- attemptId: a.attemptId,
4770
- harnessId: a.harnessId,
4771
- status: a.status,
4772
- })),
5690
+ candidates: candidateSummaries,
4773
5691
  };
4774
5692
  }
4775
5693
  }