@claudexor/orchestrator 3.0.4 → 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.
- package/dist/attemptFinalize.d.ts +162 -0
- package/dist/attemptFinalize.d.ts.map +1 -0
- package/dist/attemptFinalize.js +368 -0
- package/dist/attemptFinalize.js.map +1 -0
- package/dist/attemptTelemetry.d.ts +88 -7
- package/dist/attemptTelemetry.d.ts.map +1 -1
- package/dist/attemptTelemetry.js +276 -14
- package/dist/attemptTelemetry.js.map +1 -1
- package/dist/budgetFailure.d.ts +70 -0
- package/dist/budgetFailure.d.ts.map +1 -0
- package/dist/budgetFailure.js +114 -0
- package/dist/budgetFailure.js.map +1 -0
- package/dist/candidateEvidence.d.ts +55 -1
- package/dist/candidateEvidence.d.ts.map +1 -1
- package/dist/candidateEvidence.js +72 -0
- package/dist/candidateEvidence.js.map +1 -1
- package/dist/continuation.d.ts +77 -0
- package/dist/continuation.d.ts.map +1 -0
- package/dist/continuation.js +92 -0
- package/dist/continuation.js.map +1 -0
- package/dist/deepScanReducer.d.ts +148 -0
- package/dist/deepScanReducer.d.ts.map +1 -0
- package/dist/deepScanReducer.js +352 -0
- package/dist/deepScanReducer.js.map +1 -0
- package/dist/harnessFailure.d.ts +27 -0
- package/dist/harnessFailure.d.ts.map +1 -0
- package/dist/harnessFailure.js +61 -0
- package/dist/harnessFailure.js.map +1 -0
- package/dist/orchestrator.d.ts +97 -10
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +1213 -295
- package/dist/orchestrator.js.map +1 -1
- package/dist/planQuestions.d.ts.map +1 -1
- package/dist/planQuestions.js +105 -50
- package/dist/planQuestions.js.map +1 -1
- package/dist/planRun.d.ts +15 -7
- package/dist/planRun.d.ts.map +1 -1
- package/dist/planRun.js +88 -42
- package/dist/planRun.js.map +1 -1
- package/dist/planTerminal.d.ts +36 -0
- package/dist/planTerminal.d.ts.map +1 -0
- package/dist/planTerminal.js +63 -0
- package/dist/planTerminal.js.map +1 -0
- package/dist/runSupport.d.ts +50 -2
- package/dist/runSupport.d.ts.map +1 -1
- package/dist/runSupport.js +60 -6
- package/dist/runSupport.js.map +1 -1
- package/dist/runTelemetryWriter.d.ts +6 -1
- package/dist/runTelemetryWriter.d.ts.map +1 -1
- package/dist/runTelemetryWriter.js +2 -0
- package/dist/runTelemetryWriter.js.map +1 -1
- package/dist/runTerminals.d.ts +15 -4
- package/dist/runTerminals.d.ts.map +1 -1
- package/dist/runTerminals.js +16 -1
- package/dist/runTerminals.js.map +1 -1
- package/dist/structuredOutput.d.ts +27 -0
- package/dist/structuredOutput.d.ts.map +1 -1
- package/dist/structuredOutput.js +65 -7
- package/dist/structuredOutput.js.map +1 -1
- package/dist/transientClassify.d.ts +59 -0
- package/dist/transientClassify.d.ts.map +1 -0
- package/dist/transientClassify.js +109 -0
- package/dist/transientClassify.js.map +1 -0
- package/package.json +17 -17
package/dist/orchestrator.js
CHANGED
|
@@ -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,
|
|
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 {
|
|
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);
|
|
132
|
-
// transport-only
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
400
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
678
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1404
|
-
const
|
|
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
|
-
!
|
|
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
|
-
|
|
1475
|
-
const
|
|
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
|
|
1734
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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:
|
|
2417
|
+
reason: agentBudgetMapping?.reason ?? (budgetStopped ? "budget_exhausted" : "harness_failed"),
|
|
1985
2418
|
noChanges: true,
|
|
1986
2419
|
});
|
|
1987
|
-
const why =
|
|
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: {
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
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
|
|
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: {
|
|
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
|
|
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
|
-
:
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
2166
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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:
|
|
2469
|
-
category:
|
|
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
|
-
:
|
|
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:
|
|
2964
|
+
nextActions: decisionBlock
|
|
2486
2965
|
? [
|
|
2487
|
-
"
|
|
2488
|
-
"
|
|
2489
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
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
|
-
|
|
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
|
|
3319
|
-
|
|
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
|
-
?
|
|
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
|
-
"
|
|
3393
|
-
"
|
|
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
|
-
|
|
3409
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
4124
|
-
? `a${String(idx + 1).padStart(2, "0")}-
|
|
4125
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
4294
|
-
const
|
|
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
|
-
!
|
|
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
|
-
|
|
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
|
|
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 && !
|
|
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
|
-
|
|
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
|
|
4421
|
-
|
|
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
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
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
|
|
4544
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
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
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
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
|
-
|
|
4626
|
-
|
|
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
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
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
|
|
4674
|
-
//
|
|
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
|
-
|
|
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:
|
|
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:
|
|
4769
|
-
attemptId: a.attemptId,
|
|
4770
|
-
harnessId: a.harnessId,
|
|
4771
|
-
status: a.status,
|
|
4772
|
-
})),
|
|
5690
|
+
candidates: candidateSummaries,
|
|
4773
5691
|
};
|
|
4774
5692
|
}
|
|
4775
5693
|
}
|