@claudexor/orchestrator 3.1.0 → 3.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/attemptFinalize.d.ts +38 -0
- package/dist/attemptFinalize.d.ts.map +1 -1
- package/dist/attemptFinalize.js +44 -0
- package/dist/attemptFinalize.js.map +1 -1
- package/dist/attemptTelemetry.d.ts +9 -20
- package/dist/attemptTelemetry.d.ts.map +1 -1
- package/dist/attemptTelemetry.js +36 -69
- package/dist/attemptTelemetry.js.map +1 -1
- package/dist/attemptUsageCost.d.ts +31 -0
- package/dist/attemptUsageCost.d.ts.map +1 -0
- package/dist/attemptUsageCost.js +181 -0
- package/dist/attemptUsageCost.js.map +1 -0
- package/dist/budgetFailure.d.ts +3 -1
- package/dist/budgetFailure.d.ts.map +1 -1
- package/dist/budgetFailure.js.map +1 -1
- package/dist/candidateEvidence.d.ts +5 -0
- package/dist/candidateEvidence.d.ts.map +1 -1
- package/dist/candidateEvidence.js +3 -1
- package/dist/candidateEvidence.js.map +1 -1
- package/dist/candidateOutputs.d.ts +19 -0
- package/dist/candidateOutputs.d.ts.map +1 -1
- package/dist/candidateOutputs.js +201 -67
- package/dist/candidateOutputs.js.map +1 -1
- package/dist/decisionBudget.d.ts +43 -0
- package/dist/decisionBudget.d.ts.map +1 -0
- package/dist/decisionBudget.js +84 -0
- package/dist/decisionBudget.js.map +1 -0
- package/dist/decisionTerminalReconciliation.d.ts +18 -0
- package/dist/decisionTerminalReconciliation.d.ts.map +1 -0
- package/dist/decisionTerminalReconciliation.js +26 -0
- package/dist/decisionTerminalReconciliation.js.map +1 -0
- package/dist/deepScanReducer.d.ts.map +1 -1
- package/dist/deepScanReducer.js +1 -5
- package/dist/deepScanReducer.js.map +1 -1
- package/dist/delegation-parent-activation.d.ts +6 -0
- package/dist/delegation-parent-activation.d.ts.map +1 -0
- package/dist/delegation-parent-activation.js +13 -0
- package/dist/delegation-parent-activation.js.map +1 -0
- package/dist/delegationBudgetAuthority.d.ts +30 -0
- package/dist/delegationBudgetAuthority.d.ts.map +1 -0
- package/dist/delegationBudgetAuthority.js +157 -0
- package/dist/delegationBudgetAuthority.js.map +1 -0
- package/dist/delegationFailure.d.ts +58 -0
- package/dist/delegationFailure.d.ts.map +1 -0
- package/dist/delegationFailure.js +184 -0
- package/dist/delegationFailure.js.map +1 -0
- package/dist/delegationToolEvidence.d.ts +20 -0
- package/dist/delegationToolEvidence.d.ts.map +1 -0
- package/dist/delegationToolEvidence.js +46 -0
- package/dist/delegationToolEvidence.js.map +1 -0
- package/dist/effortGovernance.d.ts +49 -0
- package/dist/effortGovernance.d.ts.map +1 -0
- package/dist/effortGovernance.js +35 -0
- package/dist/effortGovernance.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/orchestrator.d.ts +29 -38
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +294 -392
- package/dist/orchestrator.js.map +1 -1
- package/dist/plan-prompt.d.ts +3 -0
- package/dist/plan-prompt.d.ts.map +1 -0
- package/dist/plan-prompt.js +28 -0
- package/dist/plan-prompt.js.map +1 -0
- package/dist/requestRequirements.d.ts +17 -0
- package/dist/requestRequirements.d.ts.map +1 -1
- package/dist/requestRequirements.js +69 -1
- package/dist/requestRequirements.js.map +1 -1
- package/dist/reviewerPanel.d.ts +4 -0
- package/dist/reviewerPanel.d.ts.map +1 -1
- package/dist/reviewerPanel.js +54 -9
- package/dist/reviewerPanel.js.map +1 -1
- package/dist/revisePrompt.d.ts +17 -0
- package/dist/revisePrompt.d.ts.map +1 -0
- package/dist/revisePrompt.js +15 -0
- package/dist/revisePrompt.js.map +1 -0
- package/dist/root-ledger.d.ts +13 -0
- package/dist/root-ledger.d.ts.map +1 -0
- package/dist/root-ledger.js +19 -0
- package/dist/root-ledger.js.map +1 -0
- package/dist/routing-failure.d.ts +6 -0
- package/dist/routing-failure.d.ts.map +1 -0
- package/dist/routing-failure.js +12 -0
- package/dist/routing-failure.js.map +1 -0
- package/dist/run-bounded.d.ts +3 -0
- package/dist/run-bounded.d.ts.map +1 -0
- package/dist/run-bounded.js +17 -0
- package/dist/run-bounded.js.map +1 -0
- package/dist/run-input-resolution.d.ts +10 -0
- package/dist/run-input-resolution.d.ts.map +1 -0
- package/dist/run-input-resolution.js +70 -0
- package/dist/run-input-resolution.js.map +1 -0
- package/dist/runSupport.d.ts +17 -0
- package/dist/runSupport.d.ts.map +1 -1
- package/dist/runSupport.js +26 -0
- package/dist/runSupport.js.map +1 -1
- package/dist/runTelemetryWriter.d.ts +5 -2
- package/dist/runTelemetryWriter.d.ts.map +1 -1
- package/dist/runTelemetryWriter.js +72 -1
- package/dist/runTelemetryWriter.js.map +1 -1
- package/dist/runTerminals.d.ts +34 -7
- package/dist/runTerminals.d.ts.map +1 -1
- package/dist/runTerminals.js +215 -24
- package/dist/runTerminals.js.map +1 -1
- package/dist/secretDiff.d.ts +29 -0
- package/dist/secretDiff.d.ts.map +1 -0
- package/dist/secretDiff.js +133 -0
- package/dist/secretDiff.js.map +1 -0
- package/dist/terminalOutcome.d.ts +22 -0
- package/dist/terminalOutcome.d.ts.map +1 -0
- package/dist/terminalOutcome.js +39 -0
- package/dist/terminalOutcome.js.map +1 -0
- package/dist/write-isolation.d.ts +11 -0
- package/dist/write-isolation.d.ts.map +1 -0
- package/dist/write-isolation.js +13 -0
- package/dist/write-isolation.js.map +1 -0
- package/package.json +17 -17
package/dist/orchestrator.js
CHANGED
|
@@ -3,6 +3,7 @@ import { observeNativeSessionEvent, preflightCredentialProfile, preflightDefault
|
|
|
3
3
|
import { writeRunTelemetryArtifact } from "./runTelemetryWriter.js";
|
|
4
4
|
import { buildFileBackedSynthesisInput, materializeWinnerOutputs, stageFileBackedContext, writeCandidateAttemptArtifacts, } from "./candidateOutputs.js";
|
|
5
5
|
import { processAttemptUsage } from "./attemptUsage.js";
|
|
6
|
+
import * as AC from "./attemptUsageCost.js";
|
|
6
7
|
import { candidateRoster, convergenceOutcomeFacts, isWorkingCandidate, partitionCandidates, toCandidateEvidence, } from "./candidateEvidence.js";
|
|
7
8
|
import { capabilityIntents } from "@claudexor/gateway";
|
|
8
9
|
import { policyFindings } from "./policyFindings.js";
|
|
@@ -12,11 +13,21 @@ import { HarnessRunSpec, ModeKind as ModeKindSchema, SCHEMA_VERSION, TRUST_FULL_
|
|
|
12
13
|
import { globalConfigDir, loadConfig, trustConfigPath } from "@claudexor/config";
|
|
13
14
|
import { AnswerAssembly, CLAUDEXOR_ARTIFACT_DIR, CLAUDEXOR_BROWSER_ARTIFACT_SUBDIR, HarnessUnavailableError, summarizeDiffPaths as diffStats, withInactivityWatchdog, } from "@claudexor/core";
|
|
14
15
|
import { assertRouteModelsAllowed } from "./modelGovernance.js";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
16
|
+
import { governRouteEffort } from "./effortGovernance.js";
|
|
17
|
+
import { isFullAccess, RequestRequirementsResolver } from "./requestRequirements.js";
|
|
18
|
+
import { activateDelegationParent } from "./delegation-parent-activation.js";
|
|
19
|
+
import { routingFailureClassification } from "./routing-failure.js";
|
|
20
|
+
export { routingFailureClassification } from "./routing-failure.js";
|
|
21
|
+
import { runBounded } from "./run-bounded.js";
|
|
22
|
+
import { planPrompt } from "./plan-prompt.js";
|
|
23
|
+
import { resolveRunInputDefaults } from "./run-input-resolution.js";
|
|
24
|
+
import { createRootLedger } from "./root-ledger.js";
|
|
25
|
+
import { arbitrationBudgetOptions, decisionBudgetSummary } from "./decisionBudget.js";
|
|
26
|
+
import { buildRevisePrompt } from "./revisePrompt.js";
|
|
27
|
+
import { announcedRunContext, cancelledResult, failTerminally, guardAnnouncedRun, writeFailure, } from "./runTerminals.js";
|
|
17
28
|
import { budgetFailureRecord, classifyBudgetFailure } from "./budgetFailure.js";
|
|
18
29
|
import { assertOutputSchemaCompiles, finalizeStructuredOutput } from "./structuredOutput.js";
|
|
19
|
-
import { transientRetryDelayMs, promptWithEngineConstraints, sleep, redactHarnessEvent, harnessEventPayload,
|
|
30
|
+
import { transientRetryDelayMs, promptWithEngineConstraints, sleep, redactHarnessEvent, harnessEventPayload, safeErrorMessage, renderSummary, observeBudgetSignals, rotateOnStall, recordCleanAttemptMetrics, envInheritance, transientRetryPolicy, reviewerTimeoutMs, harnessInactivityTimeoutMs, observeAuthSwitch, emitPrimaryDivergence, emitPoolDegraded, deliveryRefusalFailure, writeRaceDeliveryDecision, } from "./runSupport.js";
|
|
20
31
|
import { candidateStatusInRouteContext, resolveReadOnlyRouteContext, } from "./routeContext.js";
|
|
21
32
|
import { resolveAutoReviewerPanel, resolveExplicitReviewerPanel } from "./reviewerPanel.js";
|
|
22
33
|
import { buildContinuation, } from "./continuity.js";
|
|
@@ -24,8 +35,10 @@ import { activePlanPointer, resolveContinuitySummary, workspaceAnchor, } from ".
|
|
|
24
35
|
import { runDiffReview } from "./diffReview.js";
|
|
25
36
|
import { rawScoutBundle, resolveDeepScanSynthesis, } from "./deepScanReducer.js";
|
|
26
37
|
import { classifyAdapterThrow, createAttemptTelemetry, observeAttemptTelemetry, setAttemptOutcome, telemetrySummary, toolWarnings, unrecoveredToolErrors, webUnsatisfied, } from "./attemptTelemetry.js";
|
|
38
|
+
import * as delegateFailure from "./delegationFailure.js";
|
|
39
|
+
import * as secretDiff from "./secretDiff.js";
|
|
27
40
|
import { dominantHarnessFailureCategory, harnessFailureNextActions } from "./harnessFailure.js";
|
|
28
|
-
import { finalizeAttempt, readOnlyNoSuccessTerminal, resolveWorkReportEnvelope, unwrapWorkReportEnvelope, } from "./attemptFinalize.js";
|
|
41
|
+
import { finalizeAttempt, readOnlyNoSuccessTerminal, resolveWorkReportEnvelope, unrecoveredToolErrorFailure, unwrapWorkReportEnvelope, webEvidenceFailure, } from "./attemptFinalize.js";
|
|
29
42
|
import { buildContinuationPacket, decideContinuation, synthesizeContinuationRequest, } from "./continuation.js";
|
|
30
43
|
import { interactionChannelFor } from "./interaction.js";
|
|
31
44
|
import { gateSpecsFromContract, renderTestsEvidence, resolveContractGates, } from "./contract-gates.js";
|
|
@@ -38,56 +51,15 @@ import { HarnessGateway } from "@claudexor/gateway";
|
|
|
38
51
|
import { ReadinessLedger, evaluateConvergence, failureSignature, gatesPassed, reviewCandidate, revalidateFindings, runGates, } from "@claudexor/review";
|
|
39
52
|
import { arbitrate } from "@claudexor/arbitration";
|
|
40
53
|
import { buildSynthesisPlan, decideSynthesis } from "@claudexor/synthesis";
|
|
41
|
-
import { attemptCostEvidence, attemptUsageCostSettlement, BudgetLedger, isBudgetTerminal, explainRanking, loadHarnessMetrics, promptFingerprint,
|
|
54
|
+
import { attemptCostEvidence, attemptUsageCostSettlement, BudgetLedger, isBudgetTerminal, explainRanking, loadHarnessMetrics, promptFingerprint, rankHarnesses, reviewUsageCostSettlement, } from "@claudexor/budget";
|
|
42
55
|
import { readTextSafe, appendLine, assertNoInlineSecretValues, containsSecretLikeToken, DELEGATION_ENV, hashJson, newId, noProjectRepoRoot, nowIso, redactSecrets, safeInvoke, sha256, userConfigDir, writeText, } from "@claudexor/util";
|
|
43
|
-
|
|
44
|
-
/** The two access profiles that map to codex `danger-full-access` / an
|
|
45
|
-
* unsandboxed lane — the only ones under which a full-access-requiring MCP
|
|
46
|
-
* injection (the belt on codex) can reach the daemon. */
|
|
47
|
-
export function isFullAccess(access) {
|
|
48
|
-
return access === "full" || access === "external_sandbox_full";
|
|
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
|
-
}
|
|
56
|
+
import { assertWriteIsolation } from "./write-isolation.js";
|
|
69
57
|
const LABELS = "ABCDEFGHIJ".split("");
|
|
70
58
|
const NO_PROJECT_ROOT = noProjectRepoRoot();
|
|
71
59
|
/** Concurrency cap for parallel candidates/explorers (locked decision: min(n, 4)). */
|
|
72
60
|
const MAX_PARALLEL_CANDIDATES = 4;
|
|
73
61
|
/** Default wait for one interactive answer before a benign decline. */
|
|
74
62
|
const DEFAULT_INTERACTION_TIMEOUT_MS = 900_000;
|
|
75
|
-
/** Run `work` over `items` with bounded concurrency, preserving item order via index. */
|
|
76
|
-
async function runBounded(items, limit, work) {
|
|
77
|
-
if (items.length === 0)
|
|
78
|
-
return;
|
|
79
|
-
const concurrency = Math.max(1, Math.min(limit, items.length));
|
|
80
|
-
let next = 0;
|
|
81
|
-
const workers = Array.from({ length: concurrency }, async () => {
|
|
82
|
-
for (;;) {
|
|
83
|
-
const idx = next++;
|
|
84
|
-
if (idx >= items.length)
|
|
85
|
-
return;
|
|
86
|
-
await work(items[idx], idx);
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
await Promise.all(workers);
|
|
90
|
-
}
|
|
91
63
|
export class Orchestrator {
|
|
92
64
|
deps;
|
|
93
65
|
gateway;
|
|
@@ -115,7 +87,7 @@ export class Orchestrator {
|
|
|
115
87
|
});
|
|
116
88
|
}
|
|
117
89
|
async run(input) {
|
|
118
|
-
|
|
90
|
+
let resolved = this.resolveRunInput(input);
|
|
119
91
|
// INV-062 at the ENGINE boundary: every surface fences prompts already,
|
|
120
92
|
// but a direct embedder (or the daemon-less local REPL fallback) reaches
|
|
121
93
|
// this entry without one. Prompts, per-run instructions, AND outputSchema
|
|
@@ -134,13 +106,30 @@ export class Orchestrator {
|
|
|
134
106
|
throw new Error(`unknown mode: ${String(resolved.mode)}`);
|
|
135
107
|
}
|
|
136
108
|
const mode = parsedMode.data;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
// tree directly, so the gate could not contain a violation. Refuse loudly
|
|
140
|
-
// rather than accept a knob the engine cannot honor (INV-023).
|
|
141
|
-
if ((resolved.denyPaths?.length ?? 0) > 0 && resolved.inPlace === true) {
|
|
142
|
-
throw new Error("denyPaths requires an isolated/envelope run: the post-diff policy gate blocks a violating patch before delivery, which an in-place run cannot guarantee; drop --deny-path or run isolated");
|
|
109
|
+
if (resolved.delegate === true && mode !== "agent") {
|
|
110
|
+
throw new Error(`Delegate is an agent-only strategy (got mode=${mode})`);
|
|
143
111
|
}
|
|
112
|
+
const runId = resolved.runId ?? newId("run");
|
|
113
|
+
resolved = {
|
|
114
|
+
...resolved,
|
|
115
|
+
runId,
|
|
116
|
+
taskId: resolved.taskId ?? newId("task"),
|
|
117
|
+
};
|
|
118
|
+
if (resolved.delegate === true) {
|
|
119
|
+
resolved = {
|
|
120
|
+
...resolved,
|
|
121
|
+
delegationParentRunId: runId,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const projectProtectedPaths = mode === "agent" ? this.projectConfig(resolved.repoRoot).constraints.protected_paths : [];
|
|
125
|
+
assertWriteIsolation({
|
|
126
|
+
mode,
|
|
127
|
+
protectedPaths: projectProtectedPaths,
|
|
128
|
+
denyPaths: resolved.denyPaths,
|
|
129
|
+
inPlace: resolved.inPlace,
|
|
130
|
+
repoRoot: resolved.repoRoot,
|
|
131
|
+
executionRoot: this.execRootOf(resolved),
|
|
132
|
+
});
|
|
144
133
|
// outputSchema constrains the run's final ANSWER. It is honored exactly
|
|
145
134
|
// where a final answer is delivered (agent race incl. synthesis, and ask);
|
|
146
135
|
// every other strategy refuses loudly rather than carrying a contract the
|
|
@@ -198,13 +187,22 @@ export class Orchestrator {
|
|
|
198
187
|
case "plan":
|
|
199
188
|
return this.runPlan(resolved, announce);
|
|
200
189
|
}
|
|
190
|
+
}, async ({ runId }) => {
|
|
191
|
+
const authority = this.deps.delegationBudgetAuthority;
|
|
192
|
+
if (!authority?.hasParent(runId))
|
|
193
|
+
return;
|
|
194
|
+
authority.beginParentClose(runId);
|
|
195
|
+
await authority.waitForChildren(runId);
|
|
201
196
|
},
|
|
202
197
|
// Single per-run terminalization hook: release the routing-rationale map
|
|
203
198
|
// entry on EVERY terminal (incl. a run that died before its telemetry
|
|
204
199
|
// writer ran, which is the leak this closes).
|
|
205
|
-
(runId) =>
|
|
200
|
+
(runId) => {
|
|
201
|
+
this.routingRationaleByRun.delete(runId);
|
|
202
|
+
this.deps.delegationBudgetAuthority?.releaseRun(runId);
|
|
203
|
+
});
|
|
206
204
|
}
|
|
207
|
-
async resolveReviewers(cwd, runAuthPreference) {
|
|
205
|
+
async resolveReviewers(cwd, runAuthPreference, onIgnoredSetting) {
|
|
208
206
|
if (this.deps.reviewers)
|
|
209
207
|
return this.deps.reviewers;
|
|
210
208
|
if (this.deps.reviewerPanel && this.deps.reviewerPanel.length > 0) {
|
|
@@ -215,10 +213,8 @@ export class Orchestrator {
|
|
|
215
213
|
registry: this.deps.registry,
|
|
216
214
|
harnessSettings: this.config(cwd)?.global.harnesses ?? {},
|
|
217
215
|
authPreferenceFor: (id) => this.authPreferenceForHarness(cwd, id, runAuthPreference),
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
reviewerEfforts: this.deps.reviewerEfforts,
|
|
221
|
-
});
|
|
216
|
+
onIgnoredSetting,
|
|
217
|
+
}, { reviewerModels: this.deps.reviewerModels, reviewerEfforts: this.deps.reviewerEfforts });
|
|
222
218
|
}
|
|
223
219
|
/**
|
|
224
220
|
* Resolve reviewers INSIDE a strategy, after run-dir creation: an explicit
|
|
@@ -229,7 +225,9 @@ export class Orchestrator {
|
|
|
229
225
|
*/
|
|
230
226
|
async resolveReviewersWithArtifacts(input, log, store, paths, runId, taskId, mode) {
|
|
231
227
|
try {
|
|
232
|
-
|
|
228
|
+
// Auto-panel dropped knobs (reviewerEfforts) → ignored-settings channel (QA-070):
|
|
229
|
+
const warn = (d) => void log.emit("review.preflight", { ignored_settings: [d] });
|
|
230
|
+
return { reviewers: await this.resolveReviewers(input.repoRoot, input.authPreference, warn) };
|
|
233
231
|
}
|
|
234
232
|
catch (err) {
|
|
235
233
|
const message = safeErrorMessage(err);
|
|
@@ -411,89 +409,11 @@ export class Orchestrator {
|
|
|
411
409
|
* expand to n. Fails loudly if nothing can perform the intent.
|
|
412
410
|
*/
|
|
413
411
|
resolveRunInput(input) {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
const configuredPool = cfg?.global.routing.eligible_harnesses;
|
|
420
|
-
const harnesses = input.harnesses ?? (configuredPool && configuredPool.length > 0 ? configuredPool : undefined);
|
|
421
|
-
// GH #25 precedence: an explicit --primary-harness wins and is validated
|
|
422
|
-
// against the pool; else a single-item explicit pool infers itself as
|
|
423
|
-
// primary (shipped in #34); else the configured default primary applies.
|
|
424
|
-
const explicitPrimary = input.primaryHarness;
|
|
425
|
-
const configPrimary = cfg?.global.routing.primary_harness;
|
|
426
|
-
const primaryHarness = explicitPrimary ??
|
|
427
|
-
(input.harnesses?.length === 1 ? input.harnesses[0] : undefined) ??
|
|
428
|
-
configPrimary ??
|
|
429
|
-
undefined;
|
|
430
|
-
if (primaryHarness &&
|
|
431
|
-
harnesses &&
|
|
432
|
-
harnesses.length > 0 &&
|
|
433
|
-
!harnesses.includes(primaryHarness)) {
|
|
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(", ")}]).`);
|
|
446
|
-
}
|
|
447
|
-
if (input.web && input.externalContextPolicy && input.web !== input.externalContextPolicy) {
|
|
448
|
-
throw new Error(`contradictory web policy: web='${input.web}' vs externalContextPolicy='${input.externalContextPolicy}' (pass one, or equal values)`);
|
|
449
|
-
}
|
|
450
|
-
const web = input.web ?? input.externalContextPolicy ?? "auto";
|
|
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).
|
|
453
|
-
const knownHarnessIds = new Set(this.deps.registry.keys());
|
|
454
|
-
for (const key of Object.keys(input.models ?? {})) {
|
|
455
|
-
if (!knownHarnessIds.has(key)) {
|
|
456
|
-
throw new Error(`models map names unknown harness '${key}' (registered: ${[...knownHarnessIds].sort().join(", ")}); ` +
|
|
457
|
-
`run \`claudexor harness list --all\``);
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
const models = { ...input.models };
|
|
461
|
-
if (input.model) {
|
|
462
|
-
const scalarTarget = primaryHarness ?? (harnesses && harnesses.length === 1 ? harnesses[0] : undefined);
|
|
463
|
-
if (!scalarTarget) {
|
|
464
|
-
throw new Error(`a scalar model ('${input.model}') is ambiguous without a primary harness: ` +
|
|
465
|
-
`the pool is ${harnesses && harnesses.length > 0 ? `[${harnesses.join(", ")}]` : "auto-resolved"} — ` +
|
|
466
|
-
`set a primary harness, pass exactly one --harness, or use a harness-scoped model map`);
|
|
467
|
-
}
|
|
468
|
-
models[scalarTarget] ??= input.model;
|
|
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
|
-
}
|
|
483
|
-
return {
|
|
484
|
-
...input,
|
|
485
|
-
harnesses,
|
|
486
|
-
primaryHarness,
|
|
487
|
-
model: undefined,
|
|
488
|
-
models,
|
|
489
|
-
routingGoal: input.routingGoal ??
|
|
490
|
-
this.deps.routingGoal ??
|
|
491
|
-
cfg?.project.budget?.routing_goal ??
|
|
492
|
-
cfg?.global.routing.goal ??
|
|
493
|
-
"auto",
|
|
494
|
-
web,
|
|
495
|
-
externalContextPolicy: web,
|
|
496
|
-
};
|
|
412
|
+
return resolveRunInputDefaults(input, {
|
|
413
|
+
config: this.config(input.repoRoot),
|
|
414
|
+
registryIds: this.deps.registry.keys(),
|
|
415
|
+
routingGoal: this.deps.routingGoal,
|
|
416
|
+
});
|
|
497
417
|
}
|
|
498
418
|
async resolveCandidateAdapters(input, intent, ledger, log, routeContext,
|
|
499
419
|
/** QA-034: when provided, the pool-ordering rationale is recorded under this
|
|
@@ -712,6 +632,14 @@ export class Orchestrator {
|
|
|
712
632
|
access: requiredAccess,
|
|
713
633
|
}),
|
|
714
634
|
denyRequirement: this.requestRequirements.resolveDenyPaths(id, (input.denyPaths?.length ?? 0) > 0),
|
|
635
|
+
delegationRequirement: this.requestRequirements.resolveDelegation({
|
|
636
|
+
harnessId: id,
|
|
637
|
+
requested: input.delegate === true,
|
|
638
|
+
runtimeAvailable: input.delegationBelt != null,
|
|
639
|
+
manifestCapable: manifest.capability_profile.mcp_injection,
|
|
640
|
+
requiresFullAccess: manifest.capability_profile.mcp_injection_requires_full_access,
|
|
641
|
+
fullAccess: isFullAccess(requiredAccess),
|
|
642
|
+
}),
|
|
715
643
|
effortLevels: manifest.capabilities.effort_levels,
|
|
716
644
|
knownModels: manifest.capabilities.known_models,
|
|
717
645
|
// A selected profile's credential_kind IS the route (round-18 #2);
|
|
@@ -758,6 +686,7 @@ export class Orchestrator {
|
|
|
758
686
|
}
|
|
759
687
|
emitPrimaryDivergence(log, input.primaryHarness, ordered, pool, dropped);
|
|
760
688
|
const n = input.n ?? ordered.length;
|
|
689
|
+
const selectionOrder = ordered;
|
|
761
690
|
const out = [];
|
|
762
691
|
if (droppedLanes.length > 0 && !allowDuplicateFill) {
|
|
763
692
|
// QA-043: lanes were dropped from an AUTO best-of pool (an explicit pool
|
|
@@ -766,15 +695,15 @@ export class Orchestrator {
|
|
|
766
695
|
// masks the omission. Clamp to distinct survivors and disclose below.
|
|
767
696
|
// (Deep-scan sets allowDuplicateFill: its width is scout coverage, not
|
|
768
697
|
// harness diversity, so a dropped lane must not cut the scout count.)
|
|
769
|
-
for (let i = 0; i < Math.min(n,
|
|
770
|
-
out.push(
|
|
698
|
+
for (let i = 0; i < Math.min(n, selectionOrder.length); i++)
|
|
699
|
+
out.push(selectionOrder[i]);
|
|
771
700
|
}
|
|
772
701
|
else {
|
|
773
702
|
// No lane was dropped: a pool smaller than `n` is an intentional
|
|
774
703
|
// best-of-N on the available harness(es) (e.g. explicit `--harness codex
|
|
775
704
|
// -n 3`), so the historical width fill is preserved.
|
|
776
705
|
for (let i = 0; i < n; i++)
|
|
777
|
-
out.push(
|
|
706
|
+
out.push(selectionOrder[i % selectionOrder.length]);
|
|
778
707
|
}
|
|
779
708
|
// Disclose an auto-pool omission / width clamp once, with the
|
|
780
709
|
// requested-vs-effective route receipt (never silent — QA-043).
|
|
@@ -786,25 +715,24 @@ export class Orchestrator {
|
|
|
786
715
|
droppedLanes,
|
|
787
716
|
});
|
|
788
717
|
this.requestRequirements.requireEffectiveBrowser(input.browser === true, out.map((lane) => lane.browserRequirement));
|
|
789
|
-
//
|
|
790
|
-
//
|
|
791
|
-
//
|
|
792
|
-
|
|
793
|
-
const names = [...new Set(out.map((lane) => lane.adapter.id))].join(", ");
|
|
794
|
-
throw new HarnessUnavailableError(`--delegate requires a harness that can host the Claudexor delegation belt (capability_profile.mcp_injection); the routed harness(es) [${names}] cannot inject MCP servers — choose claude or codex, or drop --delegate`);
|
|
795
|
-
}
|
|
796
|
-
// A belt-injecting lane may still be UNABLE to reach the daemon at its
|
|
797
|
-
// access: codex's workspace-write seatbelt cancels the belt's daemon-crossing
|
|
798
|
-
// MCP call, so codex only hosts the belt at FULL access (same as its browser
|
|
799
|
-
// MCP). If EVERY injecting lane requires full access but runs below it, the
|
|
800
|
-
// belt would be injected only to be silently cancelled by the sandbox — the
|
|
801
|
-
// exact non-delegation this guard prevents. Refuse with the real remedy.
|
|
718
|
+
// Owner decision (2026-07-26): known PRE-START belt unavailability does
|
|
719
|
+
// not discard the requested Agent work. Continue without Delegate and emit
|
|
720
|
+
// a durable typed warning. Once a descriptor is injected, typed startup
|
|
721
|
+
// failure stays terminal in attemptTelemetry (no mid-attempt downgrade).
|
|
802
722
|
if (input.delegate === true) {
|
|
803
|
-
const
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
723
|
+
const unavailable = out
|
|
724
|
+
.map((lane) => lane.delegationRequirement)
|
|
725
|
+
.filter((resolution) => !resolution.effective);
|
|
726
|
+
if (unavailable.length > 0) {
|
|
727
|
+
log?.emit("delegation.belt.degraded", {
|
|
728
|
+
requested: true,
|
|
729
|
+
effective: out.some((lane) => lane.delegationRequirement.effective),
|
|
730
|
+
reason: unavailable[0]?.reason ?? "runtime_unavailable",
|
|
731
|
+
lanes: unavailable.map((resolution) => ({
|
|
732
|
+
harness_id: resolution.harness_id,
|
|
733
|
+
reason: resolution.reason,
|
|
734
|
+
})),
|
|
735
|
+
});
|
|
808
736
|
}
|
|
809
737
|
}
|
|
810
738
|
// outputSchema is MANDATORY (Quiz-6a): a selected lane that cannot
|
|
@@ -841,6 +769,8 @@ export class Orchestrator {
|
|
|
841
769
|
*/
|
|
842
770
|
orderPool(pool, input, intent, statusById, ledger, runId) {
|
|
843
771
|
let ordered = pool;
|
|
772
|
+
let rationale = null;
|
|
773
|
+
let selectionReason = null;
|
|
844
774
|
if (pool.length > 0) {
|
|
845
775
|
const routeLedger = ledger ?? new BudgetLedger();
|
|
846
776
|
const config = this.config(input.repoRoot).global;
|
|
@@ -917,21 +847,40 @@ export class Orchestrator {
|
|
|
917
847
|
intent,
|
|
918
848
|
qualityTiers: config.routing.quality_tiers,
|
|
919
849
|
ledger: routeLedger,
|
|
850
|
+
now: Date.now(), // ONE instant for the sort AND the rationale below
|
|
920
851
|
};
|
|
921
852
|
const ranked = rankHarnesses(remaining, routeCtx)
|
|
922
853
|
.map((candidate) => byId.get(candidate.harnessId))
|
|
923
854
|
.filter((candidate) => Boolean(candidate));
|
|
924
|
-
|
|
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));
|
|
855
|
+
rationale = explainRanking(remaining, routeCtx);
|
|
929
856
|
ordered = ranked;
|
|
930
857
|
}
|
|
858
|
+
if (input.delegate === true && input.primaryHarnessExplicit !== true) {
|
|
859
|
+
const delegateFirst = [
|
|
860
|
+
...ordered.filter((lane) => lane.delegationRequirement.effective),
|
|
861
|
+
...ordered.filter((lane) => !lane.delegationRequirement.effective),
|
|
862
|
+
];
|
|
863
|
+
if (delegateFirst.some((lane, index) => lane !== ordered[index])) {
|
|
864
|
+
ordered = delegateFirst;
|
|
865
|
+
selectionReason = "delegate_effective_first";
|
|
866
|
+
}
|
|
867
|
+
}
|
|
931
868
|
if (input.primaryHarness) {
|
|
932
869
|
const primary = ordered.find((r) => r.adapter.id === input.primaryHarness);
|
|
933
|
-
if (primary)
|
|
870
|
+
if (primary && primary !== ordered[0]) {
|
|
934
871
|
ordered = [primary, ...ordered.filter((r) => r !== primary)];
|
|
872
|
+
selectionReason = "explicit_primary";
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
// QA-034: persist the FINAL selected order, including request constraints
|
|
876
|
+
// that intentionally override the underlying cost/quota ranking. This is
|
|
877
|
+
// what keeps route evidence aligned with the lane actually executed.
|
|
878
|
+
if (runId && rationale) {
|
|
879
|
+
this.routingRationaleByRun.set(runId, {
|
|
880
|
+
...rationale,
|
|
881
|
+
order: ordered.map((lane) => lane.adapter.id),
|
|
882
|
+
reason: selectionReason ?? rationale.reason,
|
|
883
|
+
});
|
|
935
884
|
}
|
|
936
885
|
return ordered;
|
|
937
886
|
}
|
|
@@ -1067,7 +1016,7 @@ export class Orchestrator {
|
|
|
1067
1016
|
projectCommands: cfg.tests?.commands ?? [],
|
|
1068
1017
|
});
|
|
1069
1018
|
const commands = resolvedGates.commands;
|
|
1070
|
-
const protectedPaths = [];
|
|
1019
|
+
const protectedPaths = [...new Set(cfg.constraints.protected_paths)];
|
|
1071
1020
|
const autoProtectedPaths = resolvedGates.autoProtectedPaths;
|
|
1072
1021
|
const protectedPathApprovals = [
|
|
1073
1022
|
...new Map([...(input.protectedPathApprovals ?? [])].map((approval) => [approval.path, approval])).values(),
|
|
@@ -1078,6 +1027,11 @@ export class Orchestrator {
|
|
|
1078
1027
|
created_at: nowIso(),
|
|
1079
1028
|
repo: { root: input.repoRoot, base_ref: input.baseRef ?? "HEAD", dirty_policy: "snapshot" },
|
|
1080
1029
|
mode: { kind: mode },
|
|
1030
|
+
delegation_requested: input.delegate === true,
|
|
1031
|
+
run_lineage: {
|
|
1032
|
+
parent_run_id: input.parentRunId ?? null,
|
|
1033
|
+
delegated_from_run_id: input.delegatedFromRunId ?? null,
|
|
1034
|
+
},
|
|
1081
1035
|
user_intent: { raw: redactSecrets(input.prompt) },
|
|
1082
1036
|
// Redacted for symmetry with user_intent.raw — a no-op on fenced input
|
|
1083
1037
|
// (the inline-secret fence already blocked any secret-like value at every
|
|
@@ -1163,13 +1117,15 @@ export class Orchestrator {
|
|
|
1163
1117
|
* a WRITING agent intent (the delegator integrates results in its workspace;
|
|
1164
1118
|
* read lanes and reviewers have nothing to delegate). */
|
|
1165
1119
|
delegationBeltFor(input, intent, routed, resolvedBudget) {
|
|
1166
|
-
if (!input?.delegate ||
|
|
1120
|
+
if (!input?.delegate ||
|
|
1121
|
+
!input.delegationBelt ||
|
|
1122
|
+
!input.delegationParentRunId ||
|
|
1123
|
+
!routed.delegationRequirement.effective)
|
|
1167
1124
|
return [];
|
|
1168
1125
|
// A lane that sandbox-cancels the belt below full access (codex) must NOT
|
|
1169
|
-
// receive a belt it cannot use
|
|
1170
|
-
//
|
|
1171
|
-
//
|
|
1172
|
-
// that cannot host it, so a mixed pool keeps the belt on the lanes that can.
|
|
1126
|
+
// receive a belt it cannot use. Per-lane requirement resolution records the
|
|
1127
|
+
// typed degradation, while a mixed pool keeps the belt on lanes that can
|
|
1128
|
+
// host it.
|
|
1173
1129
|
if (routed.mcpInjectionRequiresFullAccess && !isFullAccess(routed.adapterAccess))
|
|
1174
1130
|
return [];
|
|
1175
1131
|
const writingIntents = ["implement", "create_from_scratch", "repair"];
|
|
@@ -1185,6 +1141,8 @@ export class Orchestrator {
|
|
|
1185
1141
|
...input.delegationBelt,
|
|
1186
1142
|
env: {
|
|
1187
1143
|
...input.delegationBelt.env,
|
|
1144
|
+
[DELEGATION_ENV.parentRunId]: input.delegationParentRunId,
|
|
1145
|
+
[DELEGATION_ENV.repoRoot]: input.repoRoot,
|
|
1188
1146
|
[DELEGATION_ENV.budget]: JSON.stringify(resolvedBudget),
|
|
1189
1147
|
},
|
|
1190
1148
|
},
|
|
@@ -1278,17 +1236,16 @@ export class Orchestrator {
|
|
|
1278
1236
|
// per-harness map, which beats the per-harness settings default. There is
|
|
1279
1237
|
// no run-global model.
|
|
1280
1238
|
const model = overrideModel ?? contract.routing_models[routed.adapter.id] ?? s?.defaultModel ?? null;
|
|
1281
|
-
// Effort disclosure (INV-105)
|
|
1282
|
-
//
|
|
1283
|
-
//
|
|
1284
|
-
//
|
|
1285
|
-
//
|
|
1286
|
-
//
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
}
|
|
1239
|
+
// Effort disclosure (INV-105) against the harness's advertised ladder. This
|
|
1240
|
+
// gate only DISCLOSES an unplaceable level; the clamp belongs to the adapter,
|
|
1241
|
+
// which resolves against the catalog for the profile env the child runs in
|
|
1242
|
+
// (the manifest here is the DEFAULT account's — see effortGovernance.ts). The
|
|
1243
|
+
// contract's FROZEN per-lane effort (QA-035) wins so Exact Retry replays it
|
|
1244
|
+
// without re-reading settings; `effortHint`/settings apply only to an unfrozen lane.
|
|
1245
|
+
const governed = governRouteEffort(contract.routing_efforts[routed.adapter.id] ?? effortHint ?? s?.effort ?? null, { id: routed.adapter.id, ...routed });
|
|
1246
|
+
const effort = governed.effort;
|
|
1247
|
+
if (governed.ignored)
|
|
1248
|
+
ignored.push(governed.ignored);
|
|
1292
1249
|
return {
|
|
1293
1250
|
model,
|
|
1294
1251
|
effort,
|
|
@@ -1489,7 +1446,7 @@ export class Orchestrator {
|
|
|
1489
1446
|
const browserServerName = spec.browser ? "browser" : null;
|
|
1490
1447
|
const telemetry = createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required ||
|
|
1491
1448
|
knobs.webPolicy === "cached" ||
|
|
1492
|
-
knobs.webPolicy === "live", effectiveWebMode ?? knobs.webPolicy, [routed.browserRequirement, routed.denyRequirement], knobs.model, beltServerName, browserServerName);
|
|
1449
|
+
knobs.webPolicy === "live", effectiveWebMode ?? knobs.webPolicy, [routed.browserRequirement, routed.denyRequirement, routed.delegationRequirement], knobs.model, beltServerName, browserServerName);
|
|
1493
1450
|
let activeSessionId = spec.session_id;
|
|
1494
1451
|
const onAbort = () => {
|
|
1495
1452
|
void adapter.cancel?.(activeSessionId)?.catch(() => { });
|
|
@@ -1569,10 +1526,9 @@ export class Orchestrator {
|
|
|
1569
1526
|
observeNativeSessionEvent(runInput, adapter.id, safeEv);
|
|
1570
1527
|
observeAuthSwitch(log, adapter.id, attemptId, safeEv);
|
|
1571
1528
|
observeAttemptTelemetry(telemetry, safeEv);
|
|
1572
|
-
// QA-024: the injected delegation belt's MCP server reported
|
|
1573
|
-
//
|
|
1574
|
-
//
|
|
1575
|
-
// `mcp__<belt>__*` tools and may degrade to its own native subagent.
|
|
1529
|
+
// QA-024: the injected delegation belt's MCP server reported a
|
|
1530
|
+
// terminal startup failure. Disclose it ONCE while live; recoverable
|
|
1531
|
+
// exact tool-result failures are evaluated at attempt finalization.
|
|
1576
1532
|
// The terminal outcome axis (delegationBeltUnavailable) reflects it
|
|
1577
1533
|
// too; this event makes the failure visible while the run is live.
|
|
1578
1534
|
if (telemetry.delegationBelt.requested &&
|
|
@@ -1721,22 +1677,22 @@ export class Orchestrator {
|
|
|
1721
1677
|
}
|
|
1722
1678
|
const attemptStreamEndedMs = Date.now();
|
|
1723
1679
|
if (webUnsatisfied(telemetry)) {
|
|
1724
|
-
errors.push(
|
|
1680
|
+
errors.push(webEvidenceFailure(telemetry.web));
|
|
1725
1681
|
}
|
|
1726
|
-
const diff = await wsm.diff(envelope);
|
|
1727
1682
|
// D-16: un-nest {work_report, output} so answer.md persists the OUTPUT, not the envelope.
|
|
1728
1683
|
const unwrapped = unwrapWorkReportEnvelope(answer.machineText() ?? "", workReportMode, {
|
|
1729
1684
|
sideToolReport: telemetry.sideToolWorkReport ?? undefined,
|
|
1730
1685
|
});
|
|
1731
|
-
// X119: persist the VERBATIM redacted bytes; trim ONLY for the emptiness check.
|
|
1732
1686
|
const redacted = redactSecrets(unwrapped.deliverable);
|
|
1733
|
-
const
|
|
1687
|
+
const candidateAnswer = redacted.trim().length > 0 ? redacted : undefined;
|
|
1688
|
+
const { diff, refusal: secretDiffRefusal } = await secretDiff.quarantineCandidateWorkspace(wsm, envelope, inPlaceEnvelope, candidateAnswer);
|
|
1689
|
+
harnessErrored = secretDiff.recordSecretDiffRefusal(secretDiffRefusal, errors, harnessErrored);
|
|
1690
|
+
const answerText = secretDiffRefusal ? undefined : candidateAnswer;
|
|
1734
1691
|
const deliverableEvidence = diff.trim().length > 0 || Boolean(answerText);
|
|
1735
|
-
// Cancelled attempts skip gates
|
|
1736
|
-
// stop NOW; running a 600s-per-gate suite after the abort delays the ack
|
|
1692
|
+
// Cancelled attempts skip gates: running a 600s-per-gate suite delays the ack
|
|
1737
1693
|
// and burns compute on a result nobody will adopt. Diff/attempt.yaml
|
|
1738
1694
|
// still land, so partial work stays inspectable.
|
|
1739
|
-
const gateSignalAborted = signal?.aborted === true;
|
|
1695
|
+
const gateSignalAborted = signal?.aborted === true || secretDiffRefusal !== undefined;
|
|
1740
1696
|
if (!gateSignalAborted) {
|
|
1741
1697
|
log?.emit("gate.started", {
|
|
1742
1698
|
attempt_id: attemptId,
|
|
@@ -1766,6 +1722,14 @@ export class Orchestrator {
|
|
|
1766
1722
|
});
|
|
1767
1723
|
}
|
|
1768
1724
|
const webBlocked = webUnsatisfied(telemetry);
|
|
1725
|
+
// A descriptor that was injected and then reported failed is past the
|
|
1726
|
+
// pre-start degradation boundary. Hard-fail this attempt; never continue as
|
|
1727
|
+
// ordinary Agent or let a native vendor subagent masquerade as belt work.
|
|
1728
|
+
const delegationError = delegateFailure.delegationFailureError(telemetry);
|
|
1729
|
+
if (delegationError) {
|
|
1730
|
+
harnessErrored = true;
|
|
1731
|
+
errors.push(delegationError);
|
|
1732
|
+
}
|
|
1769
1733
|
// D-16 unified finalizer: fold the WorkReport / context signals into the
|
|
1770
1734
|
// deliverable + work_state. A broken contract on a constrained route
|
|
1771
1735
|
// elevates harnessErrored (never a prose success).
|
|
@@ -1790,14 +1754,6 @@ export class Orchestrator {
|
|
|
1790
1754
|
workState: finalized.workState,
|
|
1791
1755
|
});
|
|
1792
1756
|
const attemptDir = join(paths.attemptsDir, attemptId);
|
|
1793
|
-
try {
|
|
1794
|
-
assertNoSecretLikeTokens("candidate patch diff", diff);
|
|
1795
|
-
}
|
|
1796
|
-
catch (err) {
|
|
1797
|
-
// The stream already settled real spend; a post-stream assertion throw
|
|
1798
|
-
// must carry it so the slot catch settles the TRUE cost, not 0.
|
|
1799
|
-
throw Object.assign(err instanceof Error ? err : new Error(String(err)), { costUsd: cost });
|
|
1800
|
-
}
|
|
1801
1757
|
recordCleanAttemptMetrics(globalConfigDir(), adapter.id, {
|
|
1802
1758
|
costUsd: cost,
|
|
1803
1759
|
streamMs: attemptStreamEndedMs - attemptStartedMs,
|
|
@@ -1805,11 +1761,13 @@ export class Orchestrator {
|
|
|
1805
1761
|
aborted: signal?.aborted === true,
|
|
1806
1762
|
authMode: telemetry.authMode,
|
|
1807
1763
|
});
|
|
1808
|
-
const producedFiles = writeCandidateAttemptArtifacts({
|
|
1764
|
+
const producedFiles = AC.withAttemptFailureCost(() => writeCandidateAttemptArtifacts({
|
|
1809
1765
|
store,
|
|
1810
1766
|
attemptDir,
|
|
1811
1767
|
worktreePath: envelope.worktree_path,
|
|
1812
1768
|
diff,
|
|
1769
|
+
persistPatch: secretDiffRefusal === undefined,
|
|
1770
|
+
persistProducedMedia: secretDiffRefusal === undefined,
|
|
1813
1771
|
answerText,
|
|
1814
1772
|
record: {
|
|
1815
1773
|
attempt_id: attemptId,
|
|
@@ -1821,9 +1779,14 @@ export class Orchestrator {
|
|
|
1821
1779
|
errors: errors.slice(0, 5),
|
|
1822
1780
|
...telemetrySummary(telemetry),
|
|
1823
1781
|
outcome: telemetry.outcome,
|
|
1782
|
+
...(secretDiffRefusal ? { secret_diff_refusal: secretDiffRefusal } : {}),
|
|
1824
1783
|
gates: gates.map((g) => ({ id: g.id, status: g.status })),
|
|
1825
1784
|
branch: envelope.branch_name,
|
|
1826
1785
|
},
|
|
1786
|
+
}), {
|
|
1787
|
+
totalUsd: cost,
|
|
1788
|
+
estimated: costEstimated,
|
|
1789
|
+
settlement: attemptUsageCostSettlement(cost, costEstimated, attemptId, adapter.id, telemetry.authMode, telemetry.usageCost),
|
|
1827
1790
|
});
|
|
1828
1791
|
return {
|
|
1829
1792
|
attemptId,
|
|
@@ -1840,6 +1803,7 @@ export class Orchestrator {
|
|
|
1840
1803
|
costEstimated,
|
|
1841
1804
|
errors: errors.slice(0, 8),
|
|
1842
1805
|
telemetry,
|
|
1806
|
+
...(secretDiffRefusal ? { secretDiffRefusal } : {}),
|
|
1843
1807
|
outcomeClass: finalized.outcomeClass,
|
|
1844
1808
|
};
|
|
1845
1809
|
}
|
|
@@ -1985,16 +1949,7 @@ export class Orchestrator {
|
|
|
1985
1949
|
safeInvoke(input.onRunStart, { runId, taskId, runDir: paths.root });
|
|
1986
1950
|
log.emit("run.created", { mode, prompt: redactSecrets(input.prompt) });
|
|
1987
1951
|
const ledger = this.rootLedger(input, contract, log);
|
|
1988
|
-
announce?.({
|
|
1989
|
-
log,
|
|
1990
|
-
store,
|
|
1991
|
-
paths,
|
|
1992
|
-
runId,
|
|
1993
|
-
taskId,
|
|
1994
|
-
mode,
|
|
1995
|
-
phase: "race",
|
|
1996
|
-
spend: () => ledger.spend(),
|
|
1997
|
-
});
|
|
1952
|
+
announce?.(announcedRunContext({ log, store, paths, runId, taskId, mode, phase: "race" }, ledger, () => this.deps.delegationBudgetAuthority?.hasParent(runId) === true));
|
|
1998
1953
|
store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
|
|
1999
1954
|
log.emit("task.contract.created", { task_contract_hash: hashJson(contract) });
|
|
2000
1955
|
// Write modes need a git boundary for worktree isolation and honest diffs.
|
|
@@ -2083,6 +2038,7 @@ export class Orchestrator {
|
|
|
2083
2038
|
candidates: [],
|
|
2084
2039
|
};
|
|
2085
2040
|
}
|
|
2041
|
+
activateDelegationParent(this.deps.delegationBudgetAuthority, input, runId, ledger, adapters, log);
|
|
2086
2042
|
const reviewersOutcome = await this.resolveReviewersWithArtifacts(input, log, store, paths, runId, taskId, mode);
|
|
2087
2043
|
if ("failed" in reviewersOutcome)
|
|
2088
2044
|
return reviewersOutcome.failed;
|
|
@@ -2307,7 +2263,7 @@ export class Orchestrator {
|
|
|
2307
2263
|
effectiveRun = contRun;
|
|
2308
2264
|
}
|
|
2309
2265
|
catch (err) {
|
|
2310
|
-
ledger.settle(contLeaseId,
|
|
2266
|
+
ledger.settle(contLeaseId, AC.attemptFailureCost(err, "continuation-error", 0).settlement);
|
|
2311
2267
|
log.emit("harness.completed", {
|
|
2312
2268
|
harness_id: adapter.id,
|
|
2313
2269
|
attempt_id: contAttemptId,
|
|
@@ -2331,18 +2287,9 @@ export class Orchestrator {
|
|
|
2331
2287
|
envelope = undefined;
|
|
2332
2288
|
}
|
|
2333
2289
|
catch (err) {
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
// post-stream throw (e.g. the secret-token assertion) carries its
|
|
2337
|
-
// streamed spend on the error — settle the TRUE cost, never launder
|
|
2338
|
-
// real spend down to 0.
|
|
2339
|
-
const carriedCost = typeof err?.costUsd === "number"
|
|
2340
|
-
? err.costUsd
|
|
2341
|
-
: 0;
|
|
2342
|
-
ledger.settle(slot.leaseId, unknownCostSettlement("post-stream-error", carriedCost));
|
|
2290
|
+
const failureCost = AC.attemptFailureCost(err, "post-stream-error", 0);
|
|
2291
|
+
ledger.settle(slot.leaseId, failureCost.settlement);
|
|
2343
2292
|
const message = safeErrorMessage(err);
|
|
2344
|
-
// envelope is still undefined when wsm.create() itself threw — that is
|
|
2345
|
-
// a workspace-phase infrastructure failure, not a harness error.
|
|
2346
2293
|
const infraPhase = envelope === undefined ? "workspace" : "harness";
|
|
2347
2294
|
log.emit("harness.completed", {
|
|
2348
2295
|
harness_id: adapter.id,
|
|
@@ -2351,26 +2298,22 @@ export class Orchestrator {
|
|
|
2351
2298
|
error: message,
|
|
2352
2299
|
phase: infraPhase,
|
|
2353
2300
|
});
|
|
2354
|
-
|
|
2355
|
-
store.writeYaml(join(paths.attemptsDir, slot.attemptId, "attempt.yaml"), {
|
|
2356
|
-
attempt_id: slot.attemptId,
|
|
2357
|
-
harness_id: adapter.id,
|
|
2358
|
-
cost_usd: carriedCost,
|
|
2359
|
-
errored: true,
|
|
2360
|
-
phase: infraPhase,
|
|
2361
|
-
errors: [message],
|
|
2362
|
-
});
|
|
2301
|
+
store.writeYaml(join(paths.attemptsDir, slot.attemptId, "attempt.yaml"), AC.attemptFailureRecord(slot.attemptId, adapter.id, failureCost, infraPhase, message));
|
|
2363
2302
|
runsBySlot[slotIdx] = {
|
|
2364
2303
|
attemptId: slot.attemptId,
|
|
2365
2304
|
harnessId: adapter.id,
|
|
2366
2305
|
label: slot.label,
|
|
2367
2306
|
diff: "",
|
|
2368
2307
|
gates: [],
|
|
2369
|
-
cost:
|
|
2308
|
+
cost: failureCost.totalUsd,
|
|
2370
2309
|
errored: true,
|
|
2371
|
-
costEstimated:
|
|
2310
|
+
costEstimated: failureCost.estimated,
|
|
2372
2311
|
errors: [message],
|
|
2373
|
-
telemetry: createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required, effectiveWeb, [
|
|
2312
|
+
telemetry: createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required, effectiveWeb, [
|
|
2313
|
+
slot.routed.browserRequirement,
|
|
2314
|
+
slot.routed.denyRequirement,
|
|
2315
|
+
slot.routed.delegationRequirement,
|
|
2316
|
+
], knobs.model),
|
|
2374
2317
|
infraPhase,
|
|
2375
2318
|
};
|
|
2376
2319
|
}
|
|
@@ -2393,7 +2336,7 @@ export class Orchestrator {
|
|
|
2393
2336
|
// arbitration (as the race-adoption path does) would fold those user edits
|
|
2394
2337
|
// into the revert target and let a later revert clobber them.
|
|
2395
2338
|
let earlyPostTurnSha = null;
|
|
2396
|
-
if (input.inPlace
|
|
2339
|
+
if (input.inPlace && requestedSingleCandidate && runs.every((run) => !run.secretDiffRefusal)) {
|
|
2397
2340
|
try {
|
|
2398
2341
|
earlyPostTurnSha = await snapshotTree(execRoot);
|
|
2399
2342
|
}
|
|
@@ -2405,6 +2348,20 @@ export class Orchestrator {
|
|
|
2405
2348
|
await disposeReviewEnvelopes();
|
|
2406
2349
|
return cancelledResult(log, runId, taskId, mode, paths.root, cancelledCandidates(), () => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null), ledger.spend(), input.signal, store);
|
|
2407
2350
|
}
|
|
2351
|
+
const failedDelegation = delegateFailure.dominantRaceCandidateFailure(runs);
|
|
2352
|
+
if (failedDelegation) {
|
|
2353
|
+
const failure = delegateFailure.candidateFailureTerminal(failedDelegation, "race");
|
|
2354
|
+
await disposeReviewEnvelopes();
|
|
2355
|
+
await delegateFailure.persistFailedInPlaceWorkProduct({
|
|
2356
|
+
...{ store, log, paths, execRoot, preTurnSha, taskId, mode },
|
|
2357
|
+
live: input.inPlace === true && failedDelegation.reviewCwd === execRoot,
|
|
2358
|
+
run: failedDelegation,
|
|
2359
|
+
postTurnSha: earlyPostTurnSha,
|
|
2360
|
+
kind: input.create === true ? "new_repo" : "patch",
|
|
2361
|
+
});
|
|
2362
|
+
this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null);
|
|
2363
|
+
return failTerminally(log, store, paths, runId, taskId, mode, failure.phase, failure.error, ledger.spend(), failure.metadata);
|
|
2364
|
+
}
|
|
2408
2365
|
if (runs.length === 0) {
|
|
2409
2366
|
const budgetReason = ledger.terminal();
|
|
2410
2367
|
// QA-050: when the zero-candidate cause is a budget refusal, the shared
|
|
@@ -2424,12 +2381,7 @@ export class Orchestrator {
|
|
|
2424
2381
|
why_winner: why,
|
|
2425
2382
|
evidence_facts: ["no candidates were produced"],
|
|
2426
2383
|
apply_recommendation: "continue",
|
|
2427
|
-
budget_summary:
|
|
2428
|
-
spend_usd: ledger.spend(),
|
|
2429
|
-
estimated: false,
|
|
2430
|
-
cash_usd: ledger.spend(),
|
|
2431
|
-
valuation_usd: ledger.valuation(),
|
|
2432
|
-
},
|
|
2384
|
+
budget_summary: decisionBudgetSummary(ledger),
|
|
2433
2385
|
});
|
|
2434
2386
|
store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${mode})\n\n- Lifecycle: ${facts.lifecycle}${facts.reason ? ` (${facts.reason})` : ""}\n- Phase: ${agentBudgetMapping ? "budget" : "executor"}\n\n${why}\n`);
|
|
2435
2387
|
if (agentBudgetMapping) {
|
|
@@ -2476,7 +2428,7 @@ export class Orchestrator {
|
|
|
2476
2428
|
if (workingRuns.length === 0) {
|
|
2477
2429
|
await disposeReviewEnvelopes();
|
|
2478
2430
|
const first = runs[0];
|
|
2479
|
-
const phase = first.infraPhase ?? "harness";
|
|
2431
|
+
const phase = first.secretDiffRefusal ? "artifact_security" : (first.infraPhase ?? "harness");
|
|
2480
2432
|
const { facts, why: rootCause } = partitionCandidates(runs);
|
|
2481
2433
|
store.writeYaml(join(paths.arbitrationDir, "decision.yaml"), {
|
|
2482
2434
|
winner: null,
|
|
@@ -2484,12 +2436,7 @@ export class Orchestrator {
|
|
|
2484
2436
|
why_winner: rootCause,
|
|
2485
2437
|
evidence_facts: runs.map((r) => `${r.attemptId} produced no work: ${r.errors[0] ?? "unknown"}`),
|
|
2486
2438
|
apply_recommendation: "continue",
|
|
2487
|
-
budget_summary:
|
|
2488
|
-
spend_usd: ledger.spend(),
|
|
2489
|
-
estimated: false,
|
|
2490
|
-
cash_usd: ledger.spend(),
|
|
2491
|
-
valuation_usd: ledger.valuation(),
|
|
2492
|
-
},
|
|
2439
|
+
budget_summary: decisionBudgetSummary(ledger),
|
|
2493
2440
|
});
|
|
2494
2441
|
this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null);
|
|
2495
2442
|
store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${mode})\n\n- Lifecycle: ${facts.lifecycle}\n- Phase: ${phase}\n\n${rootCause}\n`);
|
|
@@ -2509,9 +2456,11 @@ export class Orchestrator {
|
|
|
2509
2456
|
rawDetailRef: `attempts/${first.attemptId}/attempt.yaml`,
|
|
2510
2457
|
eventRefs: existingEventRefs,
|
|
2511
2458
|
runDir: paths.root,
|
|
2512
|
-
nextActions:
|
|
2513
|
-
?
|
|
2514
|
-
:
|
|
2459
|
+
nextActions: first.secretDiffRefusal
|
|
2460
|
+
? secretDiff.secretDiffNextActions(first.secretDiffRefusal)
|
|
2461
|
+
: phase === "workspace"
|
|
2462
|
+
? ["Check the project folder", "Open diagnostics", "Retry the run"]
|
|
2463
|
+
: harnessFailureNextActions(harnessCategory),
|
|
2515
2464
|
});
|
|
2516
2465
|
log.emit("output.ready", { kind: "summary", path: "final/summary.md", state: "diagnostic" });
|
|
2517
2466
|
log.emit("run.failed", {
|
|
@@ -2644,7 +2593,7 @@ export class Orchestrator {
|
|
|
2644
2593
|
}
|
|
2645
2594
|
}
|
|
2646
2595
|
catch (err) {
|
|
2647
|
-
ledger.settle(lease.lease?.lease_id ?? "",
|
|
2596
|
+
ledger.settle(lease.lease?.lease_id ?? "", AC.attemptFailureCost(err, "synthesis-error").settlement);
|
|
2648
2597
|
log.emit("harness.completed", {
|
|
2649
2598
|
attempt_id: "synth",
|
|
2650
2599
|
status: "failed",
|
|
@@ -2662,14 +2611,7 @@ export class Orchestrator {
|
|
|
2662
2611
|
}
|
|
2663
2612
|
let result;
|
|
2664
2613
|
try {
|
|
2665
|
-
result = arbitrate(evidences,
|
|
2666
|
-
spendUsd: ledger.spend(),
|
|
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(),
|
|
2672
|
-
});
|
|
2614
|
+
result = arbitrate(evidences, arbitrationBudgetOptions(ledger));
|
|
2673
2615
|
}
|
|
2674
2616
|
catch (err) {
|
|
2675
2617
|
// Arbitration throws end terminally with artifacts, never as an orphan.
|
|
@@ -3082,7 +3024,7 @@ export class Orchestrator {
|
|
|
3082
3024
|
// (so a failing test gate or no_op outcome is unchanged), just unreviewed.
|
|
3083
3025
|
const hasDiff = run.diff.trim().length > 0;
|
|
3084
3026
|
// Reviewer panels spend real money: reserve before, settle the observed cost.
|
|
3085
|
-
const reviewLease = hasDiff
|
|
3027
|
+
const reviewLease = hasDiff && reviewers.length > 0
|
|
3086
3028
|
? ledger?.reserve({
|
|
3087
3029
|
taskId: taskId ?? "task",
|
|
3088
3030
|
attemptId: run.attemptId,
|
|
@@ -3115,11 +3057,16 @@ export class Orchestrator {
|
|
|
3115
3057
|
reviewSpendUsd: 0,
|
|
3116
3058
|
reviewSpendEstimated: false,
|
|
3117
3059
|
reviewCashUsd: 0,
|
|
3060
|
+
reviewCashKnowledge: "unknown",
|
|
3118
3061
|
reviewValuationUsd: 0,
|
|
3062
|
+
reviewValuationKnowledge: "unknown",
|
|
3119
3063
|
reviewUnknownUsd: 0,
|
|
3120
3064
|
};
|
|
3121
3065
|
if (reviewLease?.granted) {
|
|
3122
|
-
ledger?.settle(reviewLease.lease?.lease_id ?? "", reviewUsageCostSettlement(result.reviewCashUsd, result.reviewValuationUsd,
|
|
3066
|
+
ledger?.settle(reviewLease.lease?.lease_id ?? "", reviewUsageCostSettlement(result.reviewCashUsd, result.reviewValuationUsd, {
|
|
3067
|
+
cash: result.reviewCashKnowledge,
|
|
3068
|
+
valuation: result.reviewValuationKnowledge,
|
|
3069
|
+
}, [`attempt:${run.attemptId}`, "review:panel"], result.reviewUnknownUsd));
|
|
3123
3070
|
if ((result.reviewSpendUsd ?? 0) > 0) {
|
|
3124
3071
|
log.emit("budget.observation", {
|
|
3125
3072
|
harness_id: "review-panel",
|
|
@@ -3239,20 +3186,23 @@ export class Orchestrator {
|
|
|
3239
3186
|
const execRoot = this.execRootOf(input);
|
|
3240
3187
|
const wsm = new WorkspaceManager(execRoot);
|
|
3241
3188
|
const readiness = new ReadinessLedger();
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
log,
|
|
3248
|
-
store,
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3189
|
+
let ledger;
|
|
3190
|
+
try {
|
|
3191
|
+
ledger = this.rootLedger(input, contract, log);
|
|
3192
|
+
store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
|
|
3193
|
+
safeInvoke(input.onRunStart, { runId, taskId, runDir: paths.root });
|
|
3194
|
+
log.emit("run.created", { mode, prompt: redactSecrets(input.prompt) });
|
|
3195
|
+
announce?.(announcedRunContext({ log, store, paths, runId, taskId, mode, phase: "convergence" }, ledger, () => this.deps.delegationBudgetAuthority?.hasParent(runId) === true));
|
|
3196
|
+
}
|
|
3197
|
+
catch (error) {
|
|
3198
|
+
// A delegated child attaches its scoped financial view before the run is
|
|
3199
|
+
// announced. If any fallible artifact/start callback in that narrow gap
|
|
3200
|
+
// throws, the terminal net has no run context, so detach here explicitly.
|
|
3201
|
+
if (input.delegatedFromRunId) {
|
|
3202
|
+
this.deps.delegationBudgetAuthority?.releaseRun(runId);
|
|
3203
|
+
}
|
|
3204
|
+
throw error;
|
|
3205
|
+
}
|
|
3256
3206
|
// Live (in-place) isolation deliberately tolerates non-git stateful
|
|
3257
3207
|
// environments; only envelope isolation needs the git boundary.
|
|
3258
3208
|
if (!input.inPlace) {
|
|
@@ -3330,6 +3280,7 @@ export class Orchestrator {
|
|
|
3330
3280
|
candidates: [],
|
|
3331
3281
|
};
|
|
3332
3282
|
}
|
|
3283
|
+
activateDelegationParent(this.deps.delegationBudgetAuthority, input, runId, ledger, adapterPool, log);
|
|
3333
3284
|
// Fail fast on a provably unwinnable predicate instead of burning paid
|
|
3334
3285
|
// rounds: the default convergence predicate requires a clean cross-family
|
|
3335
3286
|
// review, which needs >=2 healthy reviewer provider families.
|
|
@@ -3401,7 +3352,6 @@ export class Orchestrator {
|
|
|
3401
3352
|
const allCooledDown = () => adapterPool.every((a) => ledger.cooldownActive(a.adapter.id));
|
|
3402
3353
|
const attemptTelemetries = [];
|
|
3403
3354
|
let lastDiffStable = true;
|
|
3404
|
-
let reviewSpendEstimated = false;
|
|
3405
3355
|
try {
|
|
3406
3356
|
// The contract's ENGINE-COMPUTED effective profile drives the envelope and
|
|
3407
3357
|
// every attempt spec (parity with runRace); telemetry must never claim an
|
|
@@ -3436,7 +3386,7 @@ export class Orchestrator {
|
|
|
3436
3386
|
: "";
|
|
3437
3387
|
const prompt = attempt === 1
|
|
3438
3388
|
? input.prompt
|
|
3439
|
-
:
|
|
3389
|
+
: buildRevisePrompt(input.prompt, lastFindings, runtimeErrors);
|
|
3440
3390
|
// Loop detection (budget router): the 3rd identical repair prompt means
|
|
3441
3391
|
// findings/errors are not changing — stop burning paid attempts.
|
|
3442
3392
|
const fingerprint = promptFingerprint(prompt);
|
|
@@ -3491,30 +3441,48 @@ export class Orchestrator {
|
|
|
3491
3441
|
});
|
|
3492
3442
|
}
|
|
3493
3443
|
catch (err) {
|
|
3494
|
-
//
|
|
3495
|
-
//
|
|
3496
|
-
|
|
3444
|
+
// Setup failures remain unknown; post-stream persistence failures
|
|
3445
|
+
// carry their route-specific settlement from runCandidateInEnvelope.
|
|
3446
|
+
const failureCost = AC.attemptFailureCost(err, "attempt-error");
|
|
3447
|
+
const message = safeErrorMessage(err);
|
|
3448
|
+
ledger.settle(lease.lease?.lease_id ?? "", failureCost.settlement);
|
|
3497
3449
|
log.emit("harness.completed", {
|
|
3498
3450
|
harness_id: adapter.id,
|
|
3499
3451
|
attempt_id: attemptId,
|
|
3500
3452
|
status: "failed",
|
|
3501
|
-
error:
|
|
3453
|
+
error: message,
|
|
3502
3454
|
});
|
|
3455
|
+
store.writeYaml(join(paths.attemptsDir, attemptId, "attempt.yaml"), AC.attemptFailureRecord(attemptId, adapter.id, failureCost, "harness", message));
|
|
3503
3456
|
run = {
|
|
3504
3457
|
attemptId,
|
|
3505
3458
|
harnessId: adapter.id,
|
|
3506
3459
|
label: `Attempt ${attempt}`,
|
|
3507
3460
|
diff: "",
|
|
3508
3461
|
gates: [],
|
|
3509
|
-
cost:
|
|
3462
|
+
cost: failureCost.totalUsd,
|
|
3510
3463
|
errored: true,
|
|
3511
|
-
costEstimated:
|
|
3512
|
-
errors: [
|
|
3513
|
-
telemetry: createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required, effectiveWeb, [routed.browserRequirement, routed.denyRequirement], knobs.model),
|
|
3464
|
+
costEstimated: failureCost.estimated,
|
|
3465
|
+
errors: [message],
|
|
3466
|
+
telemetry: createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required, effectiveWeb, [routed.browserRequirement, routed.denyRequirement, routed.delegationRequirement], knobs.model),
|
|
3514
3467
|
};
|
|
3515
3468
|
}
|
|
3516
3469
|
lastRun = run;
|
|
3517
3470
|
attemptTelemetries.push({ attemptId, harnessId: adapter.id, telemetry: run.telemetry });
|
|
3471
|
+
// Cancellation/deadline keeps priority over a belt failure finalized concurrently.
|
|
3472
|
+
if (input.signal?.aborted)
|
|
3473
|
+
break;
|
|
3474
|
+
if (delegateFailure.candidateFailureKind(run)) {
|
|
3475
|
+
const failure = delegateFailure.candidateFailureTerminal(run, "convergence");
|
|
3476
|
+
await delegateFailure.persistFailedInPlaceWorkProduct({
|
|
3477
|
+
...{ store, log, paths, execRoot, preTurnSha, taskId, mode },
|
|
3478
|
+
live: input.inPlace === true,
|
|
3479
|
+
run,
|
|
3480
|
+
kind: input.create === true ? "new_repo" : "patch",
|
|
3481
|
+
attempts: attempt,
|
|
3482
|
+
});
|
|
3483
|
+
this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, attemptTelemetries, null);
|
|
3484
|
+
return failTerminally(log, store, paths, runId, taskId, mode, failure.phase, failure.error, ledger.spend(), failure.metadata);
|
|
3485
|
+
}
|
|
3518
3486
|
// D-16 r8: interrupted (errored===false) would CONVERGE a partial diff
|
|
3519
3487
|
// as clean — break BEFORE review; a harness error still gate-retries.
|
|
3520
3488
|
if (run.outcomeClass === "interrupted") {
|
|
@@ -3576,11 +3544,16 @@ export class Orchestrator {
|
|
|
3576
3544
|
reviewSpendUsd: 0,
|
|
3577
3545
|
reviewSpendEstimated: false,
|
|
3578
3546
|
reviewCashUsd: 0,
|
|
3547
|
+
reviewCashKnowledge: "unknown",
|
|
3579
3548
|
reviewValuationUsd: 0,
|
|
3549
|
+
reviewValuationKnowledge: "unknown",
|
|
3580
3550
|
reviewUnknownUsd: 0,
|
|
3581
3551
|
};
|
|
3582
3552
|
if (reviewLease?.granted) {
|
|
3583
|
-
ledger.settle(reviewLease.lease?.lease_id ?? "", reviewUsageCostSettlement(reviewResult.reviewCashUsd, reviewResult.reviewValuationUsd,
|
|
3553
|
+
ledger.settle(reviewLease.lease?.lease_id ?? "", reviewUsageCostSettlement(reviewResult.reviewCashUsd, reviewResult.reviewValuationUsd, {
|
|
3554
|
+
cash: reviewResult.reviewCashKnowledge,
|
|
3555
|
+
valuation: reviewResult.reviewValuationKnowledge,
|
|
3556
|
+
}, [`attempt:${attemptId}`, "review:panel"], reviewResult.reviewUnknownUsd));
|
|
3584
3557
|
if ((reviewResult.reviewSpendUsd ?? 0) > 0) {
|
|
3585
3558
|
log.emit("budget.observation", {
|
|
3586
3559
|
harness_id: "review-panel",
|
|
@@ -3592,8 +3565,6 @@ export class Orchestrator {
|
|
|
3592
3565
|
unknown_usd: reviewResult.reviewUnknownUsd,
|
|
3593
3566
|
estimated: reviewResult.reviewSpendEstimated === true,
|
|
3594
3567
|
});
|
|
3595
|
-
if (reviewResult.reviewSpendEstimated === true)
|
|
3596
|
-
reviewSpendEstimated = true;
|
|
3597
3568
|
}
|
|
3598
3569
|
}
|
|
3599
3570
|
else if (reviewLease && !reviewLease.granted) {
|
|
@@ -3775,13 +3746,7 @@ export class Orchestrator {
|
|
|
3775
3746
|
if (lastRun && !interrupted) {
|
|
3776
3747
|
const arb = arbitrate([
|
|
3777
3748
|
toCandidateEvidence(lastRun, contract, lastFindings, lastFinalReviewClean, actualReviewVerified),
|
|
3778
|
-
],
|
|
3779
|
-
spendUsd: ledger.spend(),
|
|
3780
|
-
estimatedSpend: lastRun.costEstimated || reviewSpendEstimated,
|
|
3781
|
-
// QA-010b: settled cash + valuation (reviewer panel included).
|
|
3782
|
-
cashUsd: ledger.spend(),
|
|
3783
|
-
valuationUsd: ledger.valuation(),
|
|
3784
|
-
});
|
|
3749
|
+
], arbitrationBudgetOptions(ledger));
|
|
3785
3750
|
decision = arb.decision;
|
|
3786
3751
|
store.writeYaml(join(paths.arbitrationDir, "decision.yaml"), decision);
|
|
3787
3752
|
// A converged run adopts the arbitration axes (checks/review); an
|
|
@@ -4013,40 +3978,6 @@ export class Orchestrator {
|
|
|
4013
3978
|
reviewVerified: actualReviewVerified,
|
|
4014
3979
|
};
|
|
4015
3980
|
}
|
|
4016
|
-
/** plan mode: multi-harness planning -> aggregate -> (optional) plan review -> plan. Read-only. */
|
|
4017
|
-
/**
|
|
4018
|
-
* Wrap the user's goal in an explicit "plan, do not implement" instruction.
|
|
4019
|
-
* Without this the raw prompt ("make a racing game") reaches the harness with
|
|
4020
|
-
* only a read-only sandbox, so the model tries to BUILD it and dumps code into
|
|
4021
|
-
* the plan when writes are blocked — the v0.9 "HTML in the plan" bug. The
|
|
4022
|
-
* read-only access still enforces it; this gives the model the right job.
|
|
4023
|
-
*/
|
|
4024
|
-
planPrompt(goal) {
|
|
4025
|
-
return [
|
|
4026
|
-
`You are planning, NOT implementing. Explore the repository read-only and produce a plan another agent will execute later. Do not write files or output full implementations.`,
|
|
4027
|
-
``,
|
|
4028
|
-
`## Goal`,
|
|
4029
|
-
goal,
|
|
4030
|
-
``,
|
|
4031
|
-
`## Required output (markdown)`,
|
|
4032
|
-
`1. Approach — 2-3 sentences on how you'd solve this.`,
|
|
4033
|
-
`2. Steps — a numbered list; each step names the file(s) it touches and what changes.`,
|
|
4034
|
-
`3. Risks & edge cases.`,
|
|
4035
|
-
`4. End your response with a section titled exactly:`,
|
|
4036
|
-
``,
|
|
4037
|
-
`## Open Questions`,
|
|
4038
|
-
``,
|
|
4039
|
-
`List every decision the user must make before implementation, one per bullet, in EXACTLY this format:`,
|
|
4040
|
-
``,
|
|
4041
|
-
`- [single] <question> :: <option A> :: <option B>`,
|
|
4042
|
-
`- [multi] <question> :: <option A> :: <option B>`,
|
|
4043
|
-
`- [text] <question that has no good fixed options>`,
|
|
4044
|
-
``,
|
|
4045
|
-
`Rules: [single] = pick exactly one; [multi] = pick one or more; [text] = free-form (no "::" options). Ground every option in THIS repository. If nothing is ambiguous, write a single bullet: - (none)`,
|
|
4046
|
-
``,
|
|
4047
|
-
`Keep it concise. Reference real paths you found. Do NOT paste large code blocks; describe the change instead.`,
|
|
4048
|
-
].join("\n");
|
|
4049
|
-
}
|
|
4050
3981
|
/** One read-only planner spawn shared by solo fallback, Council drafts, and merge. */
|
|
4051
3982
|
async runPlannerAttempt(args) {
|
|
4052
3983
|
const { input, contract, taskId, runId, log, store, paths, ledger, routed, attemptId } = args;
|
|
@@ -4208,20 +4139,22 @@ export class Orchestrator {
|
|
|
4208
4139
|
input.signal?.removeEventListener("abort", onAbort);
|
|
4209
4140
|
ledger.settle(lease.lease?.lease_id ?? "", attemptUsageCostSettlement(cost, costEstimated, attemptId, adapter.id, telemetry.authMode, telemetry.usageCost));
|
|
4210
4141
|
}
|
|
4211
|
-
const unrecovered = unrecoveredToolErrors(telemetry);
|
|
4212
|
-
const webBlocked = webUnsatisfied(telemetry);
|
|
4213
|
-
if (!harnessError && webBlocked) {
|
|
4214
|
-
harnessError = `web evidence unsatisfied: ${telemetry.web.errorSummary ?? (telemetry.web.attempted ? "web tool failed without verified recovery" : "web evidence required but never attempted")}`;
|
|
4215
|
-
}
|
|
4216
|
-
if (!harnessError && unrecovered.length > 0) {
|
|
4217
|
-
const first = unrecovered[0];
|
|
4218
|
-
harnessError = `${first.tool} failed without recovery: ${first.summary}`;
|
|
4219
|
-
}
|
|
4220
4142
|
// D-16: unwrap and require PLAN TEXT — a plan with no text is not delivered.
|
|
4143
|
+
// The unwrap runs BEFORE the error axes: the deliverable it yields is what
|
|
4144
|
+
// decides whether an unrecovered tool error is fatal (explorer parity).
|
|
4221
4145
|
const planUnwrapped = unwrapWorkReportEnvelope(answer.machineText() ?? "", planWorkMode, {
|
|
4222
4146
|
sideToolReport: telemetry.sideToolWorkReport ?? undefined,
|
|
4223
4147
|
});
|
|
4224
4148
|
const planText = redactSecrets(planUnwrapped.deliverable).trim();
|
|
4149
|
+
const unrecovered = unrecoveredToolErrors(telemetry);
|
|
4150
|
+
const webBlocked = webUnsatisfied(telemetry);
|
|
4151
|
+
if (!harnessError && webBlocked) {
|
|
4152
|
+
harnessError = webEvidenceFailure(telemetry.web);
|
|
4153
|
+
}
|
|
4154
|
+
// INV-043/INV-044, explorer parity: a DELIVERED plan keeps an unrecovered
|
|
4155
|
+
// non-web tool error as warning evidence instead of discarding the plan (see
|
|
4156
|
+
// the helper). Web keeps its hard gate above; the finalizer outranks both.
|
|
4157
|
+
harnessError ??= unrecoveredToolErrorFailure(unrecovered, planText.length > 0);
|
|
4225
4158
|
const planFinalized = finalizeAttempt({
|
|
4226
4159
|
deliverableEvidence: planText.length > 0,
|
|
4227
4160
|
harnessErrored: harnessError !== null && !webBlocked,
|
|
@@ -4303,16 +4236,7 @@ export class Orchestrator {
|
|
|
4303
4236
|
safeInvoke(input.onRunStart, { runId, taskId, runDir: paths.root });
|
|
4304
4237
|
log.emit("run.created", { mode: "plan", prompt: redactSecrets(input.prompt) });
|
|
4305
4238
|
const ledger = this.rootLedger(input, contract, log);
|
|
4306
|
-
announce?.({
|
|
4307
|
-
log,
|
|
4308
|
-
store,
|
|
4309
|
-
paths,
|
|
4310
|
-
runId,
|
|
4311
|
-
taskId,
|
|
4312
|
-
mode: "plan",
|
|
4313
|
-
phase: "plan",
|
|
4314
|
-
spend: () => ledger.spend(),
|
|
4315
|
-
});
|
|
4239
|
+
announce?.(announcedRunContext({ log, store, paths, runId, taskId, mode: "plan", phase: "plan" }, ledger, () => this.deps.delegationBudgetAuthority?.hasParent(runId) === true));
|
|
4316
4240
|
store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
|
|
4317
4241
|
log.emit("task.contract.created", { task_contract_hash: hashJson(contract) });
|
|
4318
4242
|
// W3.3: ONE resolved read-only context — the routing point-probe and every
|
|
@@ -4447,7 +4371,7 @@ export class Orchestrator {
|
|
|
4447
4371
|
attemptId,
|
|
4448
4372
|
laneRun,
|
|
4449
4373
|
fallbackHome: roHome.env,
|
|
4450
|
-
promptBody:
|
|
4374
|
+
promptBody: planPrompt(input.prompt) + contextSection,
|
|
4451
4375
|
intent: "plan",
|
|
4452
4376
|
});
|
|
4453
4377
|
if (outcome.budgetDenied) {
|
|
@@ -4570,7 +4494,7 @@ export class Orchestrator {
|
|
|
4570
4494
|
runPlannerAttempt: (a) => this.runPlannerAttempt(a),
|
|
4571
4495
|
writeRunTelemetry: (store, paths, contract, runId, taskId, mode, attempts, finalAttemptId) => this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, attempts, finalAttemptId),
|
|
4572
4496
|
execRootOf: (input) => this.execRootOf(input),
|
|
4573
|
-
planPrompt
|
|
4497
|
+
planPrompt,
|
|
4574
4498
|
};
|
|
4575
4499
|
}
|
|
4576
4500
|
/** ask: one selected harness answers read-only questions; no patch/apply controls. */
|
|
@@ -4599,21 +4523,14 @@ export class Orchestrator {
|
|
|
4599
4523
|
resolvePaidBudget(inputBudget, cfg) {
|
|
4600
4524
|
return inputBudget ?? this.deps.paidBudget ?? cfg.global.budget.paid_budget_per_run;
|
|
4601
4525
|
}
|
|
4602
|
-
rootLedger(
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
cash_spend_usd: cashSpendUsd,
|
|
4610
|
-
valuation_usd: valuationUsd,
|
|
4611
|
-
}),
|
|
4526
|
+
rootLedger(input, contract, log) {
|
|
4527
|
+
return createRootLedger({
|
|
4528
|
+
input,
|
|
4529
|
+
contract,
|
|
4530
|
+
log,
|
|
4531
|
+
authority: this.deps.delegationBudgetAuthority,
|
|
4532
|
+
quotaSnapshots: this.deps.quotaSnapshots?.() ?? [],
|
|
4612
4533
|
});
|
|
4613
|
-
for (const snapshot of this.deps.quotaSnapshots?.() ?? []) {
|
|
4614
|
-
ledger.observeQuotaSnapshot(snapshot);
|
|
4615
|
-
}
|
|
4616
|
-
return ledger;
|
|
4617
4534
|
}
|
|
4618
4535
|
routeBillingKnowledge(input, harnessId) {
|
|
4619
4536
|
// A selected profile's credential_kind decides billing (round-18 #2).
|
|
@@ -4693,16 +4610,7 @@ export class Orchestrator {
|
|
|
4693
4610
|
safeInvoke(input.onRunStart, { runId, taskId, runDir: paths.root });
|
|
4694
4611
|
log.emit("run.created", { mode: opts.mode, prompt: redactSecrets(prompt) });
|
|
4695
4612
|
const ledger = this.rootLedger(input, contract, log);
|
|
4696
|
-
announce?.({
|
|
4697
|
-
log,
|
|
4698
|
-
store,
|
|
4699
|
-
paths,
|
|
4700
|
-
runId,
|
|
4701
|
-
taskId,
|
|
4702
|
-
mode: opts.mode,
|
|
4703
|
-
phase: "report",
|
|
4704
|
-
spend: () => ledger.spend(),
|
|
4705
|
-
});
|
|
4613
|
+
announce?.(announcedRunContext({ log, store, paths, runId, taskId, mode: opts.mode, phase: "report" }, ledger, () => this.deps.delegationBudgetAuthority?.hasParent(runId) === true));
|
|
4706
4614
|
store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
|
|
4707
4615
|
log.emit("task.contract.created", { task_contract_hash: hashJson(contract) });
|
|
4708
4616
|
// The ask/deep-scan report stays bare (its scouts read the tree themselves);
|
|
@@ -5112,12 +5020,9 @@ export class Orchestrator {
|
|
|
5112
5020
|
const webBlocked = webUnsatisfied(telemetry);
|
|
5113
5021
|
const reportPresent = report.length > 0;
|
|
5114
5022
|
if (!harnessError && webBlocked) {
|
|
5115
|
-
harnessError =
|
|
5116
|
-
}
|
|
5117
|
-
if (!harnessError && unrecovered.length > 0 && !reportPresent) {
|
|
5118
|
-
const first = unrecovered[0];
|
|
5119
|
-
harnessError = `${first.tool} failed without recovery: ${first.summary}`;
|
|
5023
|
+
harnessError = webEvidenceFailure(telemetry.web);
|
|
5120
5024
|
}
|
|
5025
|
+
harnessError ??= unrecoveredToolErrorFailure(unrecovered, reportPresent);
|
|
5121
5026
|
const roFinalized = finalizeAttempt({
|
|
5122
5027
|
deliverableEvidence: reportPresent,
|
|
5123
5028
|
harnessErrored: harnessError !== null && !webBlocked,
|
|
@@ -5696,7 +5601,4 @@ function assertNoSecretLikeTokens(label, text) {
|
|
|
5696
5601
|
throw new Error(`${label} contains secret-like token; refusing to persist artifact`);
|
|
5697
5602
|
}
|
|
5698
5603
|
}
|
|
5699
|
-
function safeErrorMessage(err) {
|
|
5700
|
-
return redactSecrets(err instanceof Error ? err.message : String(err));
|
|
5701
|
-
}
|
|
5702
5604
|
//# sourceMappingURL=orchestrator.js.map
|