@claudexor/orchestrator 3.1.2 → 3.3.7-rc.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 +14 -11
- package/dist/attemptFinalize.d.ts.map +1 -1
- package/dist/attemptFinalize.js +41 -14
- package/dist/attemptFinalize.js.map +1 -1
- package/dist/attemptTelemetry.d.ts +7 -0
- package/dist/attemptTelemetry.d.ts.map +1 -1
- package/dist/attemptTelemetry.js +2 -1
- package/dist/attemptTelemetry.js.map +1 -1
- package/dist/attemptUsageCost.d.ts +24 -2
- package/dist/attemptUsageCost.d.ts.map +1 -1
- package/dist/attemptUsageCost.js +18 -2
- package/dist/attemptUsageCost.js.map +1 -1
- package/dist/budgetFailure.d.ts +1 -1
- package/dist/budgetFailure.d.ts.map +1 -1
- package/dist/budgetFailure.js.map +1 -1
- package/dist/candidateEvidence.d.ts +24 -0
- package/dist/candidateEvidence.d.ts.map +1 -1
- package/dist/candidateEvidence.js +26 -0
- package/dist/candidateEvidence.js.map +1 -1
- package/dist/credential-profile-rotation.d.ts +145 -0
- package/dist/credential-profile-rotation.d.ts.map +1 -0
- package/dist/credential-profile-rotation.js +335 -0
- package/dist/credential-profile-rotation.js.map +1 -0
- package/dist/credential-profiles.d.ts +90 -130
- package/dist/credential-profiles.d.ts.map +1 -1
- package/dist/credential-profiles.js +156 -290
- package/dist/credential-profiles.js.map +1 -1
- package/dist/decisionBudget.d.ts +2 -2
- package/dist/decisionBudget.d.ts.map +1 -1
- package/dist/deepScanReducer.d.ts +14 -44
- package/dist/deepScanReducer.d.ts.map +1 -1
- package/dist/deepScanReducer.js +240 -128
- package/dist/deepScanReducer.js.map +1 -1
- package/dist/delegatedHome.d.ts +156 -0
- package/dist/delegatedHome.d.ts.map +1 -0
- package/dist/delegatedHome.js +179 -0
- package/dist/delegatedHome.js.map +1 -0
- package/dist/delegationBudgetAuthority.d.ts.map +1 -1
- package/dist/delegationBudgetAuthority.js.map +1 -1
- package/dist/delegationFailure.d.ts.map +1 -1
- package/dist/git-precondition.d.ts +11 -0
- package/dist/git-precondition.d.ts.map +1 -0
- package/dist/git-precondition.js +69 -0
- package/dist/git-precondition.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/interaction.d.ts +14 -6
- package/dist/interaction.d.ts.map +1 -1
- package/dist/interaction.js +106 -19
- package/dist/interaction.js.map +1 -1
- package/dist/orchestrator.d.ts +64 -83
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +578 -691
- package/dist/orchestrator.js.map +1 -1
- package/dist/planBrief.d.ts +27 -0
- package/dist/planBrief.d.ts.map +1 -0
- package/dist/planBrief.js +42 -0
- package/dist/planBrief.js.map +1 -0
- package/dist/planQuestions.js +4 -1
- package/dist/planQuestions.js.map +1 -1
- package/dist/planRun.d.ts +4 -3
- package/dist/planRun.d.ts.map +1 -1
- package/dist/planRun.js +33 -33
- package/dist/planRun.js.map +1 -1
- package/dist/plannerAttempt.d.ts +64 -0
- package/dist/plannerAttempt.d.ts.map +1 -0
- package/dist/plannerAttempt.js +235 -0
- package/dist/plannerAttempt.js.map +1 -0
- package/dist/policyFindings.js.map +1 -1
- package/dist/requestRequirements.d.ts +43 -3
- package/dist/requestRequirements.d.ts.map +1 -1
- package/dist/requestRequirements.js +106 -13
- package/dist/requestRequirements.js.map +1 -1
- package/dist/reviewerPanel.js.map +1 -1
- package/dist/root-ledger.d.ts.map +1 -1
- package/dist/root-ledger.js +2 -0
- package/dist/root-ledger.js.map +1 -1
- package/dist/run-bounded.d.ts.map +1 -1
- package/dist/run-bounded.js +7 -1
- package/dist/run-bounded.js.map +1 -1
- package/dist/run-input-resolution.d.ts.map +1 -1
- package/dist/run-input-resolution.js +26 -8
- package/dist/run-input-resolution.js.map +1 -1
- package/dist/runEventLog.d.ts +48 -0
- package/dist/runEventLog.d.ts.map +1 -0
- package/dist/runEventLog.js +63 -0
- package/dist/runEventLog.js.map +1 -0
- package/dist/runFacts.d.ts +23 -0
- package/dist/runFacts.d.ts.map +1 -0
- package/dist/runFacts.js +179 -0
- package/dist/runFacts.js.map +1 -0
- package/dist/runFactsBuilder.d.ts +5 -0
- package/dist/runFactsBuilder.d.ts.map +1 -0
- package/dist/runFactsBuilder.js +412 -0
- package/dist/runFactsBuilder.js.map +1 -0
- package/dist/runFactsDeliverable.d.ts +13 -0
- package/dist/runFactsDeliverable.d.ts.map +1 -0
- package/dist/runFactsDeliverable.js +94 -0
- package/dist/runFactsDeliverable.js.map +1 -0
- package/dist/runFactsPresentation.d.ts +7 -0
- package/dist/runFactsPresentation.d.ts.map +1 -0
- package/dist/runFactsPresentation.js +113 -0
- package/dist/runFactsPresentation.js.map +1 -0
- package/dist/runFactsReview.d.ts +7 -0
- package/dist/runFactsReview.d.ts.map +1 -0
- package/dist/runFactsReview.js +63 -0
- package/dist/runFactsReview.js.map +1 -0
- package/dist/runSupport.d.ts +17 -15
- package/dist/runSupport.d.ts.map +1 -1
- package/dist/runSupport.js +16 -0
- package/dist/runSupport.js.map +1 -1
- package/dist/runTerminalContext.d.ts +31 -0
- package/dist/runTerminalContext.d.ts.map +1 -0
- package/dist/runTerminalContext.js +12 -0
- package/dist/runTerminalContext.js.map +1 -0
- package/dist/runTerminalResults.d.ts +80 -0
- package/dist/runTerminalResults.d.ts.map +1 -0
- package/dist/runTerminalResults.js +164 -0
- package/dist/runTerminalResults.js.map +1 -0
- package/dist/runTerminals.d.ts +4 -80
- package/dist/runTerminals.d.ts.map +1 -1
- package/dist/runTerminals.js +357 -260
- package/dist/runTerminals.js.map +1 -1
- package/dist/secretDiff.d.ts +3 -0
- package/dist/secretDiff.d.ts.map +1 -1
- package/dist/secretDiff.js +7 -0
- package/dist/secretDiff.js.map +1 -1
- package/dist/structuredOutput.d.ts.map +1 -1
- package/dist/structuredOutput.js.map +1 -1
- package/dist/task-contract-builder.d.ts +35 -0
- package/dist/task-contract-builder.d.ts.map +1 -0
- package/dist/task-contract-builder.js +114 -0
- package/dist/task-contract-builder.js.map +1 -0
- package/dist/transientClassify.js.map +1 -1
- package/package.json +17 -17
package/dist/orchestrator.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { observeNativeSessionEvent, preflightCredentialProfile, preflightDefaultSubject, resolveCredentialProfile, resumeSessionForProfile, rotateSpecOnTypedLimit, selectedProfileAvailability, } from "./credential-profiles.js";
|
|
2
|
+
import { effectiveAuthPreference, observeNativeSessionEvent, preflightCredentialProfile, preflightDefaultSubject, probeCredentialProfileStatus, profileHeadroomBreach, profileStatusAdmits, resolveCredentialProfile, resumeSessionForProfile, rotateSpecOnTypedLimit, selectedProfileAvailability, staticRotationCandidates, vendorVerifiedProfileStatus, } from "./credential-profiles.js";
|
|
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 { appliedAttemptFacts, assertDelegatedEvidence, confinementNotice, externallyConfinedLane, scopedHarnessHome, } from "./delegatedHome.js";
|
|
6
7
|
import * as AC from "./attemptUsageCost.js";
|
|
7
|
-
import { candidateRoster, convergenceOutcomeFacts, isWorkingCandidate, partitionCandidates, toCandidateEvidence, } from "./candidateEvidence.js";
|
|
8
|
+
import { candidateRoster, convergenceOutcomeFacts, isWorkingCandidate, partitionCandidates, toCandidateEvidence, unanimousDeclaredFailure, } from "./candidateEvidence.js";
|
|
8
9
|
import { capabilityIntents } from "@claudexor/gateway";
|
|
9
10
|
import { policyFindings } from "./policyFindings.js";
|
|
10
11
|
import { join } from "node:path";
|
|
11
12
|
import { finalizePlanRun, runCouncilPlan, writePlanHarnessFailure, } from "./planRun.js";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
13
|
+
import { runPlannerAttempt as executePlannerAttempt, } from "./plannerAttempt.js";
|
|
14
|
+
import { HarnessRunSpec, ModeKind as ModeKindSchema, QuotaSnapshot as QuotaSnapshotSchema, isBlocking, makeOutcomeFacts, normalizeUserOutputSchema, strictifyOutputSchema, estimateEffectiveAuthRoute, } from "@claudexor/schema";
|
|
15
|
+
import { globalConfigDir, loadConfig } from "@claudexor/config";
|
|
16
|
+
import { AnswerAssembly, CLAUDEXOR_ARTIFACT_DIR, CLAUDEXOR_BROWSER_ARTIFACT_SUBDIR, countsAsAgentProgress, HarnessUnavailableError, summarizeDiffPaths as diffStats, withInactivityWatchdog, } from "@claudexor/core";
|
|
15
17
|
import { assertRouteModelsAllowed } from "./modelGovernance.js";
|
|
16
18
|
import { governRouteEffort } from "./effortGovernance.js";
|
|
17
19
|
import { isFullAccess, RequestRequirementsResolver } from "./requestRequirements.js";
|
|
@@ -20,16 +22,18 @@ import { routingFailureClassification } from "./routing-failure.js";
|
|
|
20
22
|
export { routingFailureClassification } from "./routing-failure.js";
|
|
21
23
|
import { runBounded } from "./run-bounded.js";
|
|
22
24
|
import { planPrompt } from "./plan-prompt.js";
|
|
25
|
+
import { verifiedPlanBrief, withPlanBrief } from "./planBrief.js";
|
|
23
26
|
import { resolveRunInputDefaults } from "./run-input-resolution.js";
|
|
24
|
-
import {
|
|
27
|
+
import { beginAnnouncedRun } from "./runEventLog.js";
|
|
25
28
|
import { arbitrationBudgetOptions, decisionBudgetSummary } from "./decisionBudget.js";
|
|
26
29
|
import { buildRevisePrompt } from "./revisePrompt.js";
|
|
27
|
-
import {
|
|
30
|
+
import { cancelledResult, declaredFailure, failTerminally, guardAnnouncedRun, writeFailure, } from "./runTerminals.js";
|
|
28
31
|
import { budgetFailureRecord, classifyBudgetFailure } from "./budgetFailure.js";
|
|
29
32
|
import { assertOutputSchemaCompiles, finalizeStructuredOutput } from "./structuredOutput.js";
|
|
30
|
-
import { transientRetryDelayMs, promptWithEngineConstraints, sleep, redactHarnessEvent, harnessEventPayload, safeErrorMessage, renderSummary, observeBudgetSignals, rotateOnStall, recordCleanAttemptMetrics, envInheritance, transientRetryPolicy, reviewerTimeoutMs, harnessInactivityTimeoutMs, observeAuthSwitch, emitPrimaryDivergence, emitPoolDegraded, deliveryRefusalFailure, writeRaceDeliveryDecision, } from "./runSupport.js";
|
|
33
|
+
import { transientRetryDelayMs, promptWithEngineConstraints, sleep, redactHarnessEvent, harnessEventPayload, safeErrorMessage, renderSummary, observeBudgetSignals, rotateOnStall, recordCleanAttemptMetrics, envInheritance, transientRetryPolicy, reviewerTimeoutMs, harnessInactivityTimeoutMs, observeAuthSwitch, emitPrimaryDivergence, emitPoolDegraded, deliveryRefusalFailure, winnerNeedsHuman, writeRaceDeliveryDecision, } from "./runSupport.js";
|
|
31
34
|
import { candidateStatusInRouteContext, resolveReadOnlyRouteContext, } from "./routeContext.js";
|
|
32
35
|
import { resolveAutoReviewerPanel, resolveExplicitReviewerPanel } from "./reviewerPanel.js";
|
|
36
|
+
import { ensureWriteModeGitBoundary } from "./git-precondition.js";
|
|
33
37
|
import { buildContinuation, } from "./continuity.js";
|
|
34
38
|
import { activePlanPointer, resolveContinuitySummary, workspaceAnchor, } from "./continuity-facts.js";
|
|
35
39
|
import { runDiffReview } from "./diffReview.js";
|
|
@@ -41,18 +45,18 @@ import { dominantHarnessFailureCategory, harnessFailureNextActions } from "./har
|
|
|
41
45
|
import { finalizeAttempt, readOnlyNoSuccessTerminal, resolveWorkReportEnvelope, unrecoveredToolErrorFailure, unwrapWorkReportEnvelope, webEvidenceFailure, } from "./attemptFinalize.js";
|
|
42
46
|
import { buildContinuationPacket, decideContinuation, synthesizeContinuationRequest, } from "./continuation.js";
|
|
43
47
|
import { interactionChannelFor } from "./interaction.js";
|
|
44
|
-
import { gateSpecsFromContract, renderTestsEvidence
|
|
48
|
+
import { gateSpecsFromContract, renderTestsEvidence } from "./contract-gates.js";
|
|
49
|
+
import { buildTaskContract } from "./task-contract-builder.js";
|
|
45
50
|
import { ArtifactStore } from "@claudexor/artifact-store";
|
|
46
|
-
import { EventLog } from "@claudexor/event-log";
|
|
47
51
|
import { assertMandatoryContext, buildContextPack, rawContextForEnvelope, preflightEvidence, writeEvidencePacket, } from "@claudexor/context";
|
|
48
|
-
import { WorkspaceManager, captureRawPatchEnvelope, createRevertAnchorFromPatchOrNull, createRevertAnchorOrNull, ensureClaudeBridge,
|
|
52
|
+
import { WorkspaceManager, captureRawPatchEnvelope, createRevertAnchorFromPatchOrNull, createRevertAnchorOrNull, ensureClaudeBridge, consumeRawPatchEnvelope, snapshotTree, } from "@claudexor/workspace";
|
|
49
53
|
import { blockedDecisionOverride, finalVerifyBlocks, finalVerifyPatch, verifyAndDeliver, } from "@claudexor/delivery";
|
|
50
54
|
import { HarnessGateway } from "@claudexor/gateway";
|
|
51
55
|
import { ReadinessLedger, evaluateConvergence, failureSignature, gatesPassed, reviewCandidate, revalidateFindings, runGates, } from "@claudexor/review";
|
|
52
56
|
import { arbitrate } from "@claudexor/arbitration";
|
|
53
57
|
import { buildSynthesisPlan, decideSynthesis } from "@claudexor/synthesis";
|
|
54
58
|
import { attemptCostEvidence, attemptUsageCostSettlement, BudgetLedger, isBudgetTerminal, explainRanking, loadHarnessMetrics, promptFingerprint, rankHarnesses, reviewUsageCostSettlement, } from "@claudexor/budget";
|
|
55
|
-
import { readTextSafe, appendLine, assertNoInlineSecretValues,
|
|
59
|
+
import { readTextSafe, appendLine, assertNoInlineSecretValues, DELEGATION_ENV, hashJson, newId, noProjectRepoRoot, redactSecrets, safeInvoke, sha256, userConfigDir, writeText, } from "@claudexor/util";
|
|
56
60
|
import { assertWriteIsolation } from "./write-isolation.js";
|
|
57
61
|
const LABELS = "ABCDEFGHIJ".split("");
|
|
58
62
|
const NO_PROJECT_ROOT = noProjectRepoRoot();
|
|
@@ -110,10 +114,14 @@ export class Orchestrator {
|
|
|
110
114
|
throw new Error(`Delegate is an agent-only strategy (got mode=${mode})`);
|
|
111
115
|
}
|
|
112
116
|
const runId = resolved.runId ?? newId("run");
|
|
117
|
+
let delegatedLedgerAttached = false;
|
|
113
118
|
resolved = {
|
|
114
119
|
...resolved,
|
|
115
120
|
runId,
|
|
116
121
|
taskId: resolved.taskId ?? newId("task"),
|
|
122
|
+
onDelegatedLedgerAttached: () => {
|
|
123
|
+
delegatedLedgerAttached = true;
|
|
124
|
+
},
|
|
117
125
|
};
|
|
118
126
|
if (resolved.delegate === true) {
|
|
119
127
|
resolved = {
|
|
@@ -157,50 +165,63 @@ export class Orchestrator {
|
|
|
157
165
|
if (resolved.repoRoot !== NO_PROJECT_ROOT) {
|
|
158
166
|
assertMandatoryContext(resolved.repoRoot, this.projectConfig(resolved.repoRoot).context.mandatory_files);
|
|
159
167
|
}
|
|
160
|
-
// Reviewer panels are validated only inside
|
|
161
|
-
// review (race/convergence
|
|
168
|
+
// Reviewer panels are validated only inside Agent strategies that actually
|
|
169
|
+
// review (race/convergence; Plan Council is the plan critique path) — AFTER run-dir
|
|
162
170
|
// creation, so a doomed explicit panel yields typed failure ARTIFACTS
|
|
163
171
|
// (failure.yaml naming the refusal) instead of a bare pre-run throw.
|
|
164
|
-
//
|
|
172
|
+
// Ask and Plan never spawn code reviewers, so a panel there never spends doctor/
|
|
165
173
|
// model probes and never fails a run that would not use it.
|
|
166
174
|
// Whole-strategy terminal net: once a strategy ANNOUNCES its
|
|
167
175
|
// run, any escaped throw still stamps failure.yaml + summary + run.failed
|
|
168
176
|
// instead of orphaning events.jsonl.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
177
|
+
const releaseRunState = (settledRunId) => {
|
|
178
|
+
this.routingRationaleByRun.delete(settledRunId);
|
|
179
|
+
this.deps.delegationBudgetAuthority?.releaseRun(settledRunId);
|
|
180
|
+
};
|
|
181
|
+
try {
|
|
182
|
+
return await guardAnnouncedRun(resolved.signal, (announce) => {
|
|
183
|
+
switch (mode) {
|
|
184
|
+
case "ask":
|
|
185
|
+
// `--deep-scan` widens the answer into the bounded multi-scout
|
|
186
|
+
// research sweep with synthesis (the old `audit --swarm`/`explore`).
|
|
187
|
+
return resolved.deepScan
|
|
188
|
+
? this.runDeepScan(resolved, announce)
|
|
189
|
+
: this.runAsk(resolved, announce);
|
|
190
|
+
case "agent":
|
|
191
|
+
// Engine strategies are FLAGS on agent (v0.9 collapse): `--until-clean`
|
|
192
|
+
// and `--attempts` select the convergence loop; `--n` selects the race
|
|
193
|
+
// width; `--create` switches the candidate intent to create_from_scratch.
|
|
194
|
+
if (resolved.untilClean)
|
|
195
|
+
return this.runConvergence(resolved, mode, null, announce);
|
|
196
|
+
if (resolved.attempts !== undefined && resolved.attempts !== null) {
|
|
197
|
+
return this.runConvergence(resolved, mode, resolved.attempts, announce);
|
|
198
|
+
}
|
|
199
|
+
return this.runRace({ ...resolved, n: resolved.n ?? 1 }, mode, announce);
|
|
200
|
+
case "plan":
|
|
201
|
+
return this.runPlan(resolved, announce);
|
|
202
|
+
}
|
|
203
|
+
}, async ({ runId }) => {
|
|
204
|
+
const authority = this.deps.delegationBudgetAuthority;
|
|
205
|
+
if (!authority?.hasParent(runId))
|
|
206
|
+
return;
|
|
207
|
+
authority.beginParentClose(runId);
|
|
208
|
+
await authority.waitForChildren(runId);
|
|
209
|
+
},
|
|
210
|
+
// Single per-run terminalization hook: release the routing-rationale map
|
|
211
|
+
// entry on EVERY terminal (incl. a run that died before its telemetry
|
|
212
|
+
// writer ran, which is the leak this closes).
|
|
213
|
+
releaseRunState);
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
// A durable startup sink may refuse `run.created` before the strategy can
|
|
217
|
+
// announce its context. Release only a delegated child ledger acquired by
|
|
218
|
+
// THIS invocation; a caller-supplied run-id collision must never release
|
|
219
|
+
// another live parent/child authority or its routing state.
|
|
220
|
+
if (delegatedLedgerAttached) {
|
|
221
|
+
this.deps.delegationBudgetAuthority?.releaseRun(runId);
|
|
189
222
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (!authority?.hasParent(runId))
|
|
193
|
-
return;
|
|
194
|
-
authority.beginParentClose(runId);
|
|
195
|
-
await authority.waitForChildren(runId);
|
|
196
|
-
},
|
|
197
|
-
// Single per-run terminalization hook: release the routing-rationale map
|
|
198
|
-
// entry on EVERY terminal (incl. a run that died before its telemetry
|
|
199
|
-
// writer ran, which is the leak this closes).
|
|
200
|
-
(runId) => {
|
|
201
|
-
this.routingRationaleByRun.delete(runId);
|
|
202
|
-
this.deps.delegationBudgetAuthority?.releaseRun(runId);
|
|
203
|
-
});
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
204
225
|
}
|
|
205
226
|
async resolveReviewers(cwd, runAuthPreference, onIgnoredSetting) {
|
|
206
227
|
if (this.deps.reviewers)
|
|
@@ -273,11 +294,7 @@ export class Orchestrator {
|
|
|
273
294
|
}
|
|
274
295
|
authPreferenceForHarness(repoRoot, harnessId, runAuthPreference) {
|
|
275
296
|
const cfg = this.config(repoRoot)?.global;
|
|
276
|
-
|
|
277
|
-
return (explicit(runAuthPreference) ??
|
|
278
|
-
explicit(cfg?.harnesses?.[harnessId]?.auth_preference) ??
|
|
279
|
-
explicit(cfg?.routing?.auth_preference) ??
|
|
280
|
-
"auto");
|
|
297
|
+
return effectiveAuthPreference(runAuthPreference, cfg?.harnesses?.[harnessId]?.auth_preference, cfg?.routing?.auth_preference);
|
|
281
298
|
}
|
|
282
299
|
artifactStore(input) {
|
|
283
300
|
if (input.mode === "ask" && input.contextMode === "off" && input.repoRoot === NO_PROJECT_ROOT) {
|
|
@@ -300,20 +317,23 @@ export class Orchestrator {
|
|
|
300
317
|
estimateUsdFloor(repoRoot) {
|
|
301
318
|
return this.config(repoRoot)?.global.budget.estimate_usd_floor ?? 0.05;
|
|
302
319
|
}
|
|
320
|
+
/** Delegate children overlap their still-running parent, so child-side paid
|
|
321
|
+
* units use the same bounded floor as later slots in a parallel wave. */
|
|
322
|
+
reservationEstimateUsd(input, parallel = false) {
|
|
323
|
+
return parallel || Boolean(input.delegatedFromRunId)
|
|
324
|
+
? this.estimateUsdFloor(input.repoRoot)
|
|
325
|
+
: undefined;
|
|
326
|
+
}
|
|
303
327
|
execRootOf(input) {
|
|
304
328
|
return input.executionRoot ?? input.repoRoot;
|
|
305
329
|
}
|
|
306
|
-
sessionSpecFields(input, harnessId, log) {
|
|
330
|
+
async sessionSpecFields(input, harnessId, log, defaultRoute = null) {
|
|
307
331
|
const cfg = this.config(input.repoRoot)?.global;
|
|
308
|
-
const profile = this.preflightProfile(input, harnessId, log);
|
|
309
|
-
const explicit = (v) => (v && v !== "auto" ? v : undefined);
|
|
332
|
+
const profile = await this.preflightProfile(input, harnessId, log, defaultRoute);
|
|
310
333
|
return {
|
|
311
334
|
// "auto" at ANY level falls through (thread turns send the thread default
|
|
312
335
|
// "auto" as a per-run value; it must not shadow a configured preference).
|
|
313
|
-
auth_preference:
|
|
314
|
-
explicit(cfg?.harnesses?.[harnessId]?.auth_preference) ??
|
|
315
|
-
explicit(cfg?.routing?.auth_preference) ??
|
|
316
|
-
"auto",
|
|
336
|
+
auth_preference: effectiveAuthPreference(input.authPreference, cfg?.harnesses?.[harnessId]?.auth_preference, cfg?.routing?.auth_preference),
|
|
317
337
|
resume_session_id: resumeSessionForProfile(input.resumeSessions?.[harnessId], profile),
|
|
318
338
|
credential_profile: profile,
|
|
319
339
|
};
|
|
@@ -388,19 +408,74 @@ export class Orchestrator {
|
|
|
388
408
|
const policy = this.config(repoRoot)?.global.harnesses?.[harnessId]?.profile_policy;
|
|
389
409
|
return policy ?? { limit_action: "fail", rotation_eligible: [], headroom_threshold: 0.9 };
|
|
390
410
|
}
|
|
391
|
-
|
|
411
|
+
/** The quota poller's authenticated vendor evidence for THIS decision epoch,
|
|
412
|
+
* read once so every profile in the epoch is judged against one observation
|
|
413
|
+
* set. This is what makes profile readiness at admission mean the same thing
|
|
414
|
+
* it means on the Accounts card (INV-135 honesty). */
|
|
415
|
+
vendorQuotaObservations() {
|
|
416
|
+
return {
|
|
417
|
+
snapshots: this.deps.quotaSnapshots?.() ?? [],
|
|
418
|
+
absences: this.deps.quotaAbsences?.() ?? [],
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
/** Fresh profile readiness for one rotation decision epoch. Accounts uses
|
|
422
|
+
* the same probe wrapper + vendor overlay + admission predicate when
|
|
423
|
+
* projecting next_up. */
|
|
424
|
+
async readyProfileIdsForRotation(input, harnessId, current, excluded = new Set()) {
|
|
425
|
+
const profiles = staticRotationCandidates({
|
|
426
|
+
registry: this.config(input.repoRoot)?.global.credential_profiles ?? [],
|
|
427
|
+
harnessId,
|
|
428
|
+
policy: this.profilePolicy(input.repoRoot, harnessId),
|
|
429
|
+
current,
|
|
430
|
+
excluded,
|
|
431
|
+
});
|
|
432
|
+
const adapter = this.deps.registry.get(harnessId);
|
|
433
|
+
const quota = this.vendorQuotaObservations();
|
|
434
|
+
const entries = await Promise.all(profiles.map(async (profile) => ({
|
|
435
|
+
profile,
|
|
436
|
+
// Rotating INTO a profile the vendor has already rejected would spend a
|
|
437
|
+
// whole attempt to rediscover the 401 the poller reported a minute ago.
|
|
438
|
+
status: vendorVerifiedProfileStatus(await probeCredentialProfileStatus(profile, adapter?.probeCredentialProfile?.bind(adapter)), quota),
|
|
439
|
+
})));
|
|
440
|
+
return new Set(entries
|
|
441
|
+
.filter(({ profile, status }) => profileStatusAdmits(profile, status))
|
|
442
|
+
.map(({ profile }) => profile.profile_id));
|
|
443
|
+
}
|
|
444
|
+
async preflightProfile(input, harnessId, log, defaultRoute) {
|
|
392
445
|
const profile = this.resolveCredentialProfile(input, harnessId);
|
|
393
446
|
const policy = this.profilePolicy(input.repoRoot, harnessId);
|
|
394
447
|
const registry = this.config(input.repoRoot)?.global.credential_profiles ?? [];
|
|
395
448
|
const snapshots = this.deps.quotaSnapshots?.() ?? [];
|
|
396
449
|
const emit = (type, payload) => log?.emit(type, payload);
|
|
450
|
+
const breach = profileHeadroomBreach(snapshots, harnessId, profile?.profile_id ?? null, policy.headroom_threshold);
|
|
451
|
+
const readyProfileIds = policy.limit_action === "rotate" &&
|
|
452
|
+
breach !== null &&
|
|
453
|
+
(profile !== null || defaultRoute === "local_session")
|
|
454
|
+
? await this.readyProfileIdsForRotation(input, harnessId, profile)
|
|
455
|
+
: new Set();
|
|
397
456
|
if (!profile) {
|
|
398
457
|
// Unpinned runs (INV-135 auto-balance): under `rotate`, a fresh
|
|
399
458
|
// default-subject headroom breach starts on the next eligible
|
|
400
459
|
// subscription profile instead; `fail`/`ask` change nothing.
|
|
401
|
-
return preflightDefaultSubject({
|
|
460
|
+
return preflightDefaultSubject({
|
|
461
|
+
harnessId,
|
|
462
|
+
policy,
|
|
463
|
+
registry,
|
|
464
|
+
snapshots,
|
|
465
|
+
readyProfileIds,
|
|
466
|
+
defaultRoute,
|
|
467
|
+
emit,
|
|
468
|
+
});
|
|
402
469
|
}
|
|
403
|
-
return preflightCredentialProfile({
|
|
470
|
+
return preflightCredentialProfile({
|
|
471
|
+
profile,
|
|
472
|
+
harnessId,
|
|
473
|
+
policy,
|
|
474
|
+
registry,
|
|
475
|
+
snapshots,
|
|
476
|
+
readyProfileIds,
|
|
477
|
+
emit,
|
|
478
|
+
});
|
|
404
479
|
}
|
|
405
480
|
/**
|
|
406
481
|
* Resolve candidate adapters: explicit `--harness`, else available real harnesses, then
|
|
@@ -477,6 +552,27 @@ export class Orchestrator {
|
|
|
477
552
|
}
|
|
478
553
|
}
|
|
479
554
|
}
|
|
555
|
+
const attachments = input.attachments ?? [];
|
|
556
|
+
// Resolve the complete attachment pool once from manifest truth. If any
|
|
557
|
+
// lane still lacks usable discovery truth, preserve the existing per-lane
|
|
558
|
+
// discovery/doctor precedence and resolve that lane inside the route loop.
|
|
559
|
+
const canResolveAttachmentPool = attachments.length > 0 &&
|
|
560
|
+
ids.every((id) => {
|
|
561
|
+
const status = statusById.get(id);
|
|
562
|
+
return status?.manifest != null && status.status !== "unavailable";
|
|
563
|
+
});
|
|
564
|
+
const attachmentPoolAdmission = canResolveAttachmentPool
|
|
565
|
+
? this.requestRequirements.resolveAttachmentPool(explicitPool ? "explicit" : "auto", attachments, ids.map((id) => ({
|
|
566
|
+
harnessId: id,
|
|
567
|
+
declarations: statusById.get(id)?.manifest?.capability_profile.attachment_inputs ?? null,
|
|
568
|
+
available: true,
|
|
569
|
+
})))
|
|
570
|
+
: null;
|
|
571
|
+
if (attachmentPoolAdmission?.outcome === "refused") {
|
|
572
|
+
throw new HarnessUnavailableError(attachmentPoolAdmission.message ??
|
|
573
|
+
"no available harness lane can receive the selected attachments");
|
|
574
|
+
}
|
|
575
|
+
const attachmentRejectionById = new Map(attachmentPoolAdmission?.rejected.map((admission) => [admission.harnessId, admission]) ?? []);
|
|
480
576
|
const policy = input.web ?? input.externalContextPolicy ?? "auto";
|
|
481
577
|
const pool = [];
|
|
482
578
|
const dropped = [];
|
|
@@ -495,6 +591,9 @@ export class Orchestrator {
|
|
|
495
591
|
dropped.push(detail);
|
|
496
592
|
droppedLanes.push({ harnessId, stage, detail });
|
|
497
593
|
};
|
|
594
|
+
// One observation set for the whole admission pass, so two lanes cannot be
|
|
595
|
+
// judged against different vendor epochs.
|
|
596
|
+
const vendorQuota = this.vendorQuotaObservations();
|
|
498
597
|
for (const id of ids) {
|
|
499
598
|
const adapter = this.deps.registry.get(id);
|
|
500
599
|
if (!adapter) {
|
|
@@ -558,6 +657,11 @@ export class Orchestrator {
|
|
|
558
657
|
profileId: this.effectiveProfileId(input, id),
|
|
559
658
|
harnessId: id,
|
|
560
659
|
probe: profileAdapter?.probeCredentialProfile?.bind(profileAdapter),
|
|
660
|
+
// `verification: passed` from the local store only means a login file
|
|
661
|
+
// is present. The poller's authenticated vendor call is the only
|
|
662
|
+
// liveness evidence we have, and admission is the surface that must
|
|
663
|
+
// act on it — otherwise the run dispatches into a revoked token.
|
|
664
|
+
quota: vendorQuota,
|
|
561
665
|
});
|
|
562
666
|
if (profileVerdict !== null) {
|
|
563
667
|
if (profileVerdict === "available") {
|
|
@@ -590,7 +694,7 @@ export class Orchestrator {
|
|
|
590
694
|
const readOnlyIntent = intent === "plan" || intent === "spec" || intent === "explain" || intent === "audit";
|
|
591
695
|
const requiredAccess = this.requestRequirements.adapterAccess(intent, manifest.capabilities.implementation_transport, readOnlyIntent
|
|
592
696
|
? "readonly"
|
|
593
|
-
: (input.access ?? this.config(input.repoRoot).trust.access_default));
|
|
697
|
+
: (input.access ?? this.config(input.repoRoot).trust.access_default), externallyConfinedLane(input.delegated === true));
|
|
594
698
|
const accessSupported = !requiredAccess || manifest.access_profiles_supported.includes(requiredAccess);
|
|
595
699
|
const webSupport = manifest.capabilities.web_policy;
|
|
596
700
|
// Match routeSpecKnobs: a per-harness web default upgrades run-level auto,
|
|
@@ -610,9 +714,10 @@ export class Orchestrator {
|
|
|
610
714
|
dropLane(id, "web", why);
|
|
611
715
|
continue;
|
|
612
716
|
}
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
|
|
717
|
+
const attachmentAdmission = attachmentRejectionById.get(id) ??
|
|
718
|
+
this.requestRequirements.resolveAttachmentLane(id, attachments, manifest.capability_profile.attachment_inputs);
|
|
719
|
+
if (!attachmentAdmission.admitted) {
|
|
720
|
+
dropLane(id, "attachment", attachmentAdmission.message ?? `${id} rejects attachments`);
|
|
616
721
|
continue;
|
|
617
722
|
}
|
|
618
723
|
const reason = status.reasons.length > 0 ? `: ${status.reasons.join("; ")}` : "";
|
|
@@ -981,119 +1086,9 @@ export class Orchestrator {
|
|
|
981
1086
|
return this.config(repoRoot).project;
|
|
982
1087
|
}
|
|
983
1088
|
buildContract(input, taskId, mode) {
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
const requestedAccess = input.access ?? (readOnlyMode ? "readonly" : resolvedCfg.trust.access_default);
|
|
988
|
-
// Effective access is COMPUTED by the engine, never echoed from a client:
|
|
989
|
-
// read-only modes clamp to readonly regardless of the request.
|
|
990
|
-
const effectiveAccess = readOnlyMode ? "readonly" : requestedAccess;
|
|
991
|
-
// TrustConfig is USER-LEVEL only (versioned repo config must never
|
|
992
|
-
// self-grant sensitive powers): unsandboxed full access requires an
|
|
993
|
-
// explicit allow in ~/.claudexor trust settings — loud error, no downgrade.
|
|
994
|
-
// The gate applies to the EFFECTIVE profile: a read-only run clamped to
|
|
995
|
-
// readonly never runs unsandboxed and needs no trust allow.
|
|
996
|
-
if (effectiveAccess === "full" && !resolvedCfg.trust.allow_full_access) {
|
|
997
|
-
// Typed refusal: the `code` rides the daemon job record onto the thread
|
|
998
|
-
// turn (TurnEnqueueError.code), so surfaces key remedies on the CODE —
|
|
999
|
-
// never on substring-matching this human message.
|
|
1000
|
-
throw Object.assign(new Error(`access profile 'full' requires allow_full_access: true in the user-level trust file for this repo ` +
|
|
1001
|
-
`(${trustConfigPath(input.repoRoot)}); enable it with \`claudexor trust --allow-full-access\` — refusing to run unsandboxed`),
|
|
1002
|
-
// Refusal semantics are born at the throw (W24): the one-time grant is
|
|
1003
|
-
// a 403, and the daemon persists this status onto the job record.
|
|
1004
|
-
{ code: TRUST_FULL_ACCESS_CODE, status: 403 });
|
|
1005
|
-
}
|
|
1006
|
-
const externalContextPolicy = input.web ?? input.externalContextPolicy ?? "auto";
|
|
1007
|
-
// Deterministic gate commands come from explicit run input, then versioned
|
|
1008
|
-
// project config. Without these, gateSpecs is empty and convergence is
|
|
1009
|
-
// review-only; with them, convergence is test-driven.
|
|
1010
|
-
const resolvedGates = resolveContractGates({
|
|
1011
|
-
repoRoot: input.repoRoot,
|
|
1012
|
-
effectiveAccess,
|
|
1013
|
-
config: cfg,
|
|
1014
|
-
trustGrants: resolvedCfg.trust.test_command_grants,
|
|
1015
|
-
operatorCommands: input.tests ?? [],
|
|
1016
|
-
projectCommands: cfg.tests?.commands ?? [],
|
|
1017
|
-
});
|
|
1018
|
-
const commands = resolvedGates.commands;
|
|
1019
|
-
const protectedPaths = [...new Set(cfg.constraints.protected_paths)];
|
|
1020
|
-
const autoProtectedPaths = resolvedGates.autoProtectedPaths;
|
|
1021
|
-
const protectedPathApprovals = [
|
|
1022
|
-
...new Map([...(input.protectedPathApprovals ?? [])].map((approval) => [approval.path, approval])).values(),
|
|
1023
|
-
];
|
|
1024
|
-
return TaskContractSchema.parse({
|
|
1025
|
-
schema_version: SCHEMA_VERSION,
|
|
1026
|
-
task_id: taskId,
|
|
1027
|
-
created_at: nowIso(),
|
|
1028
|
-
repo: { root: input.repoRoot, base_ref: input.baseRef ?? "HEAD", dirty_policy: "snapshot" },
|
|
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
|
-
},
|
|
1035
|
-
user_intent: { raw: redactSecrets(input.prompt) },
|
|
1036
|
-
// Redacted for symmetry with user_intent.raw — a no-op on fenced input
|
|
1037
|
-
// (the inline-secret fence already blocked any secret-like value at every
|
|
1038
|
-
// ingress incl. this engine boundary), so task-producing lanes read back
|
|
1039
|
-
// the real instructions via harnessSpecKnobs().
|
|
1040
|
-
instructions: input.instructions === undefined ? undefined : redactSecrets(input.instructions),
|
|
1041
|
-
// Already normalized/strictified at the engine boundary (run() refuses
|
|
1042
|
-
// unsupported shapes before any run dir exists).
|
|
1043
|
-
output_schema: input.outputSchema ?? null,
|
|
1044
|
-
auth_preference: input.authPreference ?? "auto",
|
|
1045
|
-
credential_profile_id: input.credentialProfileId ?? null,
|
|
1046
|
-
max_turns: input.maxTurns ?? null,
|
|
1047
|
-
constraints: {
|
|
1048
|
-
protected_paths: protectedPaths,
|
|
1049
|
-
deny_paths: [...new Set(input.denyPaths ?? [])],
|
|
1050
|
-
auto_protected_paths: autoProtectedPaths,
|
|
1051
|
-
protected_path_approvals: protectedPathApprovals,
|
|
1052
|
-
},
|
|
1053
|
-
tests: { commands },
|
|
1054
|
-
access: {
|
|
1055
|
-
requested_profile: requestedAccess,
|
|
1056
|
-
effective_profile: effectiveAccess,
|
|
1057
|
-
},
|
|
1058
|
-
external_context: {
|
|
1059
|
-
policy: externalContextPolicy,
|
|
1060
|
-
web_required: externalContextPolicy === "cached" || externalContextPolicy === "live",
|
|
1061
|
-
// Per-route upgrades (e.g. claude cached->live) are disclosed in events
|
|
1062
|
-
// and telemetry.yaml; the immutable contract records the requested policy.
|
|
1063
|
-
effective_mode: externalContextPolicy,
|
|
1064
|
-
},
|
|
1065
|
-
// Harness-native tool names are adapter knowledge; the neutral contract
|
|
1066
|
-
// carries only the policy plus user-configured allow/deny lists (wired
|
|
1067
|
-
// from per-harness settings).
|
|
1068
|
-
tool_permission_policy: {
|
|
1069
|
-
web: externalContextPolicy,
|
|
1070
|
-
allow: [],
|
|
1071
|
-
deny: [],
|
|
1072
|
-
},
|
|
1073
|
-
budget: {
|
|
1074
|
-
routing_goal: input.routingGoal ?? this.deps.routingGoal ?? cfg?.budget?.routing_goal ?? "auto",
|
|
1075
|
-
paid_budget: this.resolvePaidBudget(input.paidBudget, resolvedCfg),
|
|
1076
|
-
},
|
|
1077
|
-
// The resolved harness-scoped model map (scalar already expanded to the
|
|
1078
|
-
// primary by resolveRunInput). The contract is what route spec building
|
|
1079
|
-
// reads — there is no run-global model (INV-103).
|
|
1080
|
-
routing_models: input.models ?? {},
|
|
1081
|
-
// QA-035: freeze the RESOLVED reasoning-effort per known lane so Exact
|
|
1082
|
-
// Retry replays it instead of re-resolving current settings. Precedence
|
|
1083
|
-
// (specific beats general): the harness-scoped `efforts` map entry, then a
|
|
1084
|
-
// per-turn scalar `input.effort`, then the harness settings default — the
|
|
1085
|
-
// same map that Exact Retry replays so a NON-PRIMARY lane keeps its own
|
|
1086
|
-
// frozen effort (QA-035 completeness). Only known-pool lanes are frozen
|
|
1087
|
-
// here (a pure auto pool's lanes resolve later — documented seam).
|
|
1088
|
-
routing_efforts: Object.fromEntries([...new Set([...(input.harnesses ?? []), ...Object.keys(input.efforts ?? {})])]
|
|
1089
|
-
.map((hid) => [
|
|
1090
|
-
hid,
|
|
1091
|
-
input.efforts?.[hid] ??
|
|
1092
|
-
input.effort ??
|
|
1093
|
-
resolvedCfg.global.harnesses?.[hid]?.effort ??
|
|
1094
|
-
null,
|
|
1095
|
-
])
|
|
1096
|
-
.filter((entry) => entry[1] !== null)),
|
|
1089
|
+
return buildTaskContract(input, taskId, mode, {
|
|
1090
|
+
paidBudget: this.deps.paidBudget,
|
|
1091
|
+
routingGoal: this.deps.routingGoal,
|
|
1097
1092
|
});
|
|
1098
1093
|
}
|
|
1099
1094
|
/**
|
|
@@ -1172,9 +1167,9 @@ export class Orchestrator {
|
|
|
1172
1167
|
/**
|
|
1173
1168
|
* D-16: the WorkReport transport envelope for one route. Called at every
|
|
1174
1169
|
* task-producing spec-build site AFTER harnessSpecKnobs so it OVERRIDES the
|
|
1175
|
-
* plain caller-schema transport with the
|
|
1176
|
-
*
|
|
1177
|
-
*
|
|
1170
|
+
* plain caller-schema transport with the resolved WorkReport channel on
|
|
1171
|
+
* capable routes. The returned `mode` is retained by the caller and handed
|
|
1172
|
+
* to `unwrapWorkReportEnvelope` when the answer is finalized.
|
|
1178
1173
|
*/
|
|
1179
1174
|
workReportEnvelopeFor(routed, contract, interactive) {
|
|
1180
1175
|
return resolveWorkReportEnvelope({
|
|
@@ -1188,7 +1183,7 @@ export class Orchestrator {
|
|
|
1188
1183
|
/**
|
|
1189
1184
|
* D-16: apply the resolved WorkReport transport to a built spec — set the
|
|
1190
1185
|
* envelope output_schema (constrained/side_tool routes) and APPEND the fenced
|
|
1191
|
-
*
|
|
1186
|
+
* metadata instruction (validated routes, e.g. cursor). Mutates the spec in
|
|
1192
1187
|
* place and returns the mode the answer unwrap consumes. Called at every
|
|
1193
1188
|
* task-producing spec-build site so the transport is never wired one-off.
|
|
1194
1189
|
*/
|
|
@@ -1265,7 +1260,7 @@ export class Orchestrator {
|
|
|
1265
1260
|
* Returns null (no packet, no pointer) for native resume, a fresh thread, or
|
|
1266
1261
|
* a non-thread run. Never throws: continuity failure degrades to no packet.
|
|
1267
1262
|
*/
|
|
1268
|
-
async resolveContinuity(runInput, harnessId, resolvedProfileId, nativeResumeAvailable, store, paths, repoRoot, log) {
|
|
1263
|
+
async resolveContinuity(runInput, harnessId, resolvedProfileId, nativeResumeAvailable, sessionFields, store, paths, repoRoot, log) {
|
|
1269
1264
|
const ctx = runInput.threadContinuity;
|
|
1270
1265
|
if (!runInput.threadId || !ctx)
|
|
1271
1266
|
return null;
|
|
@@ -1303,7 +1298,6 @@ export class Orchestrator {
|
|
|
1303
1298
|
// summary. Same credential route + scoped lane home a real read-only
|
|
1304
1299
|
// thread turn uses (INV-034/135). Best-effort in its OWN guard — a summary
|
|
1305
1300
|
// failure keeps the full mechanical packet, never drops it.
|
|
1306
|
-
const sessionFields = this.sessionSpecFields(runInput, harnessId);
|
|
1307
1301
|
req.cachedSummary = await resolveContinuitySummary({
|
|
1308
1302
|
req,
|
|
1309
1303
|
threadId: runInput.threadId,
|
|
@@ -1361,30 +1355,51 @@ export class Orchestrator {
|
|
|
1361
1355
|
return { pointerLine: null };
|
|
1362
1356
|
}
|
|
1363
1357
|
}
|
|
1364
|
-
|
|
1358
|
+
/**
|
|
1359
|
+
* The ONE place an attempt's HOME and OS boundary are decided.
|
|
1360
|
+
*
|
|
1361
|
+
* Called by every caller of `runCandidateInEnvelope` INSIDE that caller's
|
|
1362
|
+
* try, so a delegated refusal is caught by the same catch that writes the
|
|
1363
|
+
* attempt record, and so the decided home is in scope there.
|
|
1364
|
+
*/
|
|
1365
|
+
harnessHomeFor(wsm, envelope, routed, runInput) {
|
|
1366
|
+
// Refuses (never silently degrades) when a delegated run cannot be confined.
|
|
1367
|
+
return scopedHarnessHome(wsm, envelope, envelope.worktree_path === envelope.repo_root, runInput?.delegated === true, routed.adapterAccess);
|
|
1368
|
+
}
|
|
1369
|
+
async runCandidateInEnvelope(routed, envelope, attemptId, label, contract, prompt, store, paths, wsm, ledger, access, onHarnessEvent, signal, modelHint, effortHint, intent, log, effectiveWebMode, interaction, budgetGuard, runInput, streamDeltas, fileBackedContext,
|
|
1365
1370
|
/** D-16d: when set, the mechanical continuation checkpoint pointer for a
|
|
1366
1371
|
* one-shot fresh-session continuation — appended to the prompt so the model
|
|
1367
1372
|
* (and the offline fake) re-grounds in the exhausted attempt's partial work. */
|
|
1368
|
-
continuationPointer
|
|
1373
|
+
continuationPointer,
|
|
1374
|
+
/** Decided by `harnessHomeFor` in the CALLER, so the per-attempt applied
|
|
1375
|
+
* facts exist in the caller's catch too: an attempt that ran and then threw
|
|
1376
|
+
* must record what it ran under, not just why it stopped. REQUIRED (no
|
|
1377
|
+
* default): a silent fallback here would spawn a delegated child on the
|
|
1378
|
+
* operator's real home while the record still claimed confinement. */
|
|
1379
|
+
harnessHome) {
|
|
1369
1380
|
const adapter = routed.adapter;
|
|
1370
1381
|
const knobs = this.routeSpecKnobs(routed, contract, modelHint, effortHint);
|
|
1371
1382
|
// Isolated scoped-home sessions are never retained after disposal.
|
|
1372
1383
|
const inPlaceEnvelope = envelope.worktree_path === envelope.repo_root;
|
|
1373
1384
|
const rawContextPacket = await rawContextForEnvelope(routed.implementationTransport, envelope);
|
|
1374
|
-
const sessionFields = runInput
|
|
1385
|
+
const sessionFields = runInput
|
|
1386
|
+
? await this.sessionSpecFields(runInput, adapter.id, log, routed.authRouteEstimate)
|
|
1387
|
+
: undefined;
|
|
1375
1388
|
// Continuity (INV-137): once the lane (harness + resolved profile) is known,
|
|
1376
1389
|
// build the continuation packet, materialize context/THREAD.md, and point
|
|
1377
1390
|
// the prompt at it — never embed the packet body in the prompt. Replaces the
|
|
1378
1391
|
// old static session.rebound "not_portable" phrase with a real disclosure.
|
|
1379
1392
|
const laneContinuity = runInput
|
|
1380
|
-
? 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)
|
|
1393
|
+
? await this.resolveContinuity(runInput, adapter.id, sessionFields?.credential_profile?.profile_id ?? runInput.credentialProfileId ?? null, inPlaceEnvelope && !!sessionFields?.resume_session_id, sessionFields, store, paths, envelope.repo_root, log)
|
|
1381
1394
|
: null;
|
|
1382
1395
|
let spec = HarnessRunSpec.parse({
|
|
1383
1396
|
session_id: newId("ses"),
|
|
1384
1397
|
intent,
|
|
1385
1398
|
// Engine-derived read-only prompt constraints: protected/auto-protected
|
|
1386
1399
|
// paths PLUS the exact typed gate argv the run will execute (QA-022 FIX B).
|
|
1387
|
-
prompt: promptWithEngineConstraints(
|
|
1400
|
+
prompt: promptWithEngineConstraints(
|
|
1401
|
+
// A child running without an OS boundary is TOLD so (disclosure 2 of 3).
|
|
1402
|
+
[prompt, confinementNotice(harnessHome), laneContinuity?.pointerLine, continuationPointer]
|
|
1388
1403
|
.filter((s) => typeof s === "string" && s.length > 0)
|
|
1389
1404
|
.join("\n\n"), contract.constraints.protected_paths, contract.constraints.auto_protected_paths, contract.constraints.protected_path_approvals, contract.tests.commands),
|
|
1390
1405
|
attachments: runInput?.attachments ?? [],
|
|
@@ -1406,9 +1421,14 @@ export class Orchestrator {
|
|
|
1406
1421
|
...(inPlaceEnvelope && sessionFields?.resume_session_id
|
|
1407
1422
|
? { resume_session_id: sessionFields.resume_session_id }
|
|
1408
1423
|
: {}),
|
|
1409
|
-
// Scoped harness home
|
|
1410
|
-
// native environment so the resumed
|
|
1411
|
-
|
|
1424
|
+
// Scoped harness home for isolated envelopes AND for every delegated run;
|
|
1425
|
+
// an ordinary in-place run keeps the native environment so the resumed
|
|
1426
|
+
// vendor session is reachable. See scopedHarnessHome for the cost a
|
|
1427
|
+
// delegated in-place attempt pays for that confinement.
|
|
1428
|
+
...(harnessHome.env ? { env: harnessHome.env } : {}),
|
|
1429
|
+
// The OS boundary itself. The shared CLI run loop wraps the argv with it;
|
|
1430
|
+
// no adapter opts in, so no adapter can forget.
|
|
1431
|
+
confinement: harnessHome.confinement,
|
|
1412
1432
|
raw_context_packet: rawContextPacket,
|
|
1413
1433
|
stream_deltas: streamDeltas,
|
|
1414
1434
|
});
|
|
@@ -1419,6 +1439,9 @@ export class Orchestrator {
|
|
|
1419
1439
|
const workEnvelope = this.workReportEnvelopeFor(routed, contract, Boolean(interaction));
|
|
1420
1440
|
const workReportMode = this.applyWorkEnvelope(spec, workEnvelope);
|
|
1421
1441
|
const inactivityMs = harnessInactivityTimeoutMs(this.config(contract.repo.root));
|
|
1442
|
+
// Named once: the attempt record, the CandidateRun and the terminal gate all
|
|
1443
|
+
// read the SAME applied facts rather than three re-derivations.
|
|
1444
|
+
const applied = appliedAttemptFacts(harnessHome, spec.access, sessionFields?.credential_profile?.profile_id ?? runInput?.credentialProfileId ?? null);
|
|
1422
1445
|
const attemptStartedMs = Date.now();
|
|
1423
1446
|
const budgetSignalState = { quotaPressureDisclosed: false };
|
|
1424
1447
|
const triedProfiles = new Set(); // W5.4 failover: each profile at most once
|
|
@@ -1480,13 +1503,16 @@ export class Orchestrator {
|
|
|
1480
1503
|
try {
|
|
1481
1504
|
const watched = withInactivityWatchdog(adapter.run(runSpec), {
|
|
1482
1505
|
timeoutMs: inactivityMs,
|
|
1506
|
+
countsAsProgress: countsAsAgentProgress,
|
|
1483
1507
|
onTimeout: () => {
|
|
1484
1508
|
attemptAbort.abort();
|
|
1485
1509
|
void adapter.cancel?.(activeSessionId)?.catch(() => { });
|
|
1486
1510
|
},
|
|
1487
|
-
// Waiting on the USER
|
|
1488
|
-
//
|
|
1511
|
+
// Waiting on the USER is legitimate silence. The interaction policy
|
|
1512
|
+
// either enforces a finite deadline or waits until answer/cancel/
|
|
1513
|
+
// terminal/restart when the timeout is disabled.
|
|
1489
1514
|
isSuspended: () => (interaction?.pendingCount?.() ?? 0) > 0,
|
|
1515
|
+
suspensionVersion: () => interaction?.suspensionVersion?.() ?? 0,
|
|
1490
1516
|
});
|
|
1491
1517
|
for await (const ev of watched) {
|
|
1492
1518
|
if (signal?.aborted)
|
|
@@ -1616,13 +1642,18 @@ export class Orchestrator {
|
|
|
1616
1642
|
// W5.4 failover: a typed-limit hit rebuilds the spec on a NEW vendor
|
|
1617
1643
|
// session under the next profile with provenance (vendor_limit_rejected).
|
|
1618
1644
|
if (harnessErrored && runInput && !signal?.aborted) {
|
|
1645
|
+
const rotationPolicy = this.profilePolicy(contract.repo.root, adapter.id);
|
|
1646
|
+
const readyProfileIds = sawTypedLimit && deliverableEmpty && rotationPolicy.limit_action === "rotate"
|
|
1647
|
+
? await this.readyProfileIdsForRotation(runInput, adapter.id, spec.credential_profile ?? null, triedProfiles)
|
|
1648
|
+
: new Set();
|
|
1619
1649
|
const rotated = rotateSpecOnTypedLimit({
|
|
1620
1650
|
spec,
|
|
1621
1651
|
harnessId: adapter.id,
|
|
1622
1652
|
attemptId,
|
|
1623
|
-
policy:
|
|
1653
|
+
policy: rotationPolicy,
|
|
1624
1654
|
registry: this.config(contract.repo.root)?.global.credential_profiles ?? [],
|
|
1625
1655
|
snapshots: this.deps.quotaSnapshots?.() ?? [],
|
|
1656
|
+
readyProfileIds,
|
|
1626
1657
|
triedProfiles,
|
|
1627
1658
|
sawTypedLimit,
|
|
1628
1659
|
deliverableEmpty,
|
|
@@ -1679,7 +1710,7 @@ export class Orchestrator {
|
|
|
1679
1710
|
if (webUnsatisfied(telemetry)) {
|
|
1680
1711
|
errors.push(webEvidenceFailure(telemetry.web));
|
|
1681
1712
|
}
|
|
1682
|
-
// D-16:
|
|
1713
|
+
// D-16: remove the WorkReport transport so answer.md persists only the deliverable.
|
|
1683
1714
|
const unwrapped = unwrapWorkReportEnvelope(answer.machineText() ?? "", workReportMode, {
|
|
1684
1715
|
sideToolReport: telemetry.sideToolWorkReport ?? undefined,
|
|
1685
1716
|
});
|
|
@@ -1782,6 +1813,10 @@ export class Orchestrator {
|
|
|
1782
1813
|
...(secretDiffRefusal ? { secret_diff_refusal: secretDiffRefusal } : {}),
|
|
1783
1814
|
gates: gates.map((g) => ({ id: g.id, status: g.status })),
|
|
1784
1815
|
branch: envelope.branch_name,
|
|
1816
|
+
// Applied facts, not promises: the caller of a delegated run
|
|
1817
|
+
// verifies the confinement it asked for instead of trusting it.
|
|
1818
|
+
// Built by the SAME shape the failure path writes.
|
|
1819
|
+
...applied,
|
|
1785
1820
|
},
|
|
1786
1821
|
}), {
|
|
1787
1822
|
totalUsd: cost,
|
|
@@ -1805,6 +1840,7 @@ export class Orchestrator {
|
|
|
1805
1840
|
telemetry,
|
|
1806
1841
|
...(secretDiffRefusal ? { secretDiffRefusal } : {}),
|
|
1807
1842
|
outcomeClass: finalized.outcomeClass,
|
|
1843
|
+
applied,
|
|
1808
1844
|
};
|
|
1809
1845
|
}
|
|
1810
1846
|
interactionChannelFor(input, log, runId, taskId, attemptId, harnessId,
|
|
@@ -1814,66 +1850,17 @@ export class Orchestrator {
|
|
|
1814
1850
|
// Thin delegate — the channel mechanics live in interaction.ts.
|
|
1815
1851
|
return interactionChannelFor(input, log, runId, taskId, attemptId, harnessId, supportsInteractive, DEFAULT_INTERACTION_TIMEOUT_MS);
|
|
1816
1852
|
}
|
|
1817
|
-
/**
|
|
1818
|
-
* Guarantee a git boundary for write-mode runs. Non-git project folders are
|
|
1819
|
-
* initialized in place (`git init`, deterministic baseline commit) without
|
|
1820
|
-
* creating or editing `.gitignore`, and the action is announced via a
|
|
1821
|
-
* `project.git.initialized` event. Returns the failure message when the
|
|
1822
|
-
* boundary cannot be established (the terminal failure events are already
|
|
1823
|
-
* emitted); null on success.
|
|
1824
|
-
*/
|
|
1825
|
-
async ensureWriteModeGitBoundary(repoRoot, log, store, paths, runId, mode) {
|
|
1826
|
-
if (repoRoot === NO_PROJECT_ROOT)
|
|
1827
|
-
return null;
|
|
1828
|
-
try {
|
|
1829
|
-
const result = await ensureGitRepository(repoRoot);
|
|
1830
|
-
if (result.initialized || result.baselineCommitted) {
|
|
1831
|
-
log.emit("project.git.initialized", {
|
|
1832
|
-
repo_root: repoRoot,
|
|
1833
|
-
initialized: result.initialized,
|
|
1834
|
-
baseline_committed: result.baselineCommitted,
|
|
1835
|
-
gitignore_seeded: result.gitignoreSeeded,
|
|
1836
|
-
head_sha: result.headSha,
|
|
1837
|
-
});
|
|
1838
|
-
}
|
|
1839
|
-
return null;
|
|
1840
|
-
}
|
|
1841
|
-
catch (err) {
|
|
1842
|
-
const message = safeErrorMessage(err);
|
|
1843
|
-
writeFailure(store, paths, {
|
|
1844
|
-
phase: "workspace",
|
|
1845
|
-
category: "project",
|
|
1846
|
-
safeMessage: message,
|
|
1847
|
-
runDir: paths.root,
|
|
1848
|
-
nextActions: [
|
|
1849
|
-
"Check the project folder permissions",
|
|
1850
|
-
"Initialize git manually (git init)",
|
|
1851
|
-
"Retry the run",
|
|
1852
|
-
],
|
|
1853
|
-
});
|
|
1854
|
-
store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${mode})\n\n- Lifecycle: failed\n- Phase: workspace\n\n${message}\n`);
|
|
1855
|
-
log.emit("output.ready", { kind: "summary", path: "final/summary.md", state: "diagnostic" });
|
|
1856
|
-
log.emit("run.failed", {
|
|
1857
|
-
lifecycle: "failed",
|
|
1858
|
-
facts: makeOutcomeFacts("failed", { reason: "harness_failed" }),
|
|
1859
|
-
reason: "harness_failed",
|
|
1860
|
-
phase: "workspace",
|
|
1861
|
-
error: message,
|
|
1862
|
-
failure_ref: "final/failure.yaml",
|
|
1863
|
-
});
|
|
1864
|
-
return message;
|
|
1865
|
-
}
|
|
1866
|
-
}
|
|
1867
1853
|
/**
|
|
1868
1854
|
* D-14 layer 3 (AGENTS.md unification, INV-113): the ONE new live-tree write.
|
|
1869
1855
|
* When the PROJECT root has `AGENTS.md` and no `CLAUDE.md`, drop a thin
|
|
1870
1856
|
* `CLAUDE.md` (`@AGENTS.md` import + Claudexor ownership marker) so a Claude
|
|
1871
1857
|
* Code route reads the same instruction file codex/cursor read natively.
|
|
1872
1858
|
*
|
|
1873
|
-
*
|
|
1874
|
-
*
|
|
1875
|
-
* untouched
|
|
1876
|
-
*
|
|
1859
|
+
* The project-root bridge has its own narrower fence: read-only modes never
|
|
1860
|
+
* reach this run-prep stage and `--in-place` stateful targets are left
|
|
1861
|
+
* untouched. Git admission is independently owned by `runStartRequiresGit`.
|
|
1862
|
+
* The write targets the PROJECT root (`repoRoot`), never a worktree envelope.
|
|
1863
|
+
* The workspace helper adds exclusive-create + no-follow +
|
|
1877
1864
|
* idempotency, so a hand-written or symlinked `CLAUDE.md` is never overwritten
|
|
1878
1865
|
* and a concurrent/second prep is a no-op. Announced via a typed
|
|
1879
1866
|
* `project.claude_bridge.created` event on an actual create only — the git-init
|
|
@@ -1898,37 +1885,6 @@ export class Orchestrator {
|
|
|
1898
1885
|
});
|
|
1899
1886
|
}
|
|
1900
1887
|
}
|
|
1901
|
-
/**
|
|
1902
|
-
* Freeze-on-implement delivery (D17/D27): verify the frozen plan's hash and
|
|
1903
|
-
* materialize it as context/PLAN.md in the run artifact tree — OUTSIDE every
|
|
1904
|
-
* worktree, so it can never dirty a diff — then point the prompt at the
|
|
1905
|
-
* absolute path. A mismatched or unreadable plan fails LOUDLY before any
|
|
1906
|
-
* harness spawns (the tamper fence; retry replays planRef verbatim, so a
|
|
1907
|
-
* retried implement can never silently run without its plan).
|
|
1908
|
-
*/
|
|
1909
|
-
withPlanBrief(input, store, paths, log) {
|
|
1910
|
-
if (!input.planRef)
|
|
1911
|
-
return input;
|
|
1912
|
-
const text = readTextSafe(input.planRef.path);
|
|
1913
|
-
if (!text || !text.trim()) {
|
|
1914
|
-
throw new Error(`implement plan: the frozen plan at ${input.planRef.path} is missing or unreadable`);
|
|
1915
|
-
}
|
|
1916
|
-
const digest = sha256(text).replace(/^sha256:/, "");
|
|
1917
|
-
if (digest !== input.planRef.sha256) {
|
|
1918
|
-
throw new Error(`implement plan: plan hash mismatch (expected ${input.planRef.sha256}, got ${digest}) — the plan was modified after freeze; re-run Implement from the plan turn`);
|
|
1919
|
-
}
|
|
1920
|
-
const briefPath = join(paths.contextDir, "PLAN.md");
|
|
1921
|
-
store.writeText(briefPath, text);
|
|
1922
|
-
log.emit("plan.brief.materialized", {
|
|
1923
|
-
plan_run_id: input.planRef.runId,
|
|
1924
|
-
sha256: input.planRef.sha256,
|
|
1925
|
-
path: "context/PLAN.md",
|
|
1926
|
-
});
|
|
1927
|
-
return {
|
|
1928
|
-
...input,
|
|
1929
|
-
prompt: `${input.prompt}\n\nThe approved plan is at: ${briefPath} — read it before starting and re-read it as needed.`,
|
|
1930
|
-
};
|
|
1931
|
-
}
|
|
1932
1888
|
async runRace(input, mode, announce) {
|
|
1933
1889
|
const taskId = input.taskId ?? newId("task");
|
|
1934
1890
|
const runId = input.runId ?? newId("run");
|
|
@@ -1936,20 +1892,28 @@ export class Orchestrator {
|
|
|
1936
1892
|
// announced: a refused run must fail the request loudly, not 200 a runId
|
|
1937
1893
|
// and leave an orphaned run dir without a terminal event.
|
|
1938
1894
|
const contract = this.buildContract(input, taskId, mode);
|
|
1939
|
-
const
|
|
1940
|
-
const
|
|
1941
|
-
const
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1895
|
+
const planBrief = verifiedPlanBrief(input);
|
|
1896
|
+
const quotaSnapshots = this.quotaSnapshotPreflight();
|
|
1897
|
+
const { store, paths, log, ledger } = beginAnnouncedRun({
|
|
1898
|
+
input,
|
|
1899
|
+
contract,
|
|
1900
|
+
quotaSnapshots,
|
|
1901
|
+
store: this.artifactStore(input),
|
|
1902
|
+
authority: this.deps.delegationBudgetAuthority,
|
|
1903
|
+
runId,
|
|
1904
|
+
taskId,
|
|
1905
|
+
mode,
|
|
1906
|
+
phase: "race",
|
|
1907
|
+
prompt: input.prompt,
|
|
1908
|
+
}, announce);
|
|
1909
|
+
input = withPlanBrief(input, store, paths, log, planBrief);
|
|
1910
|
+
// The execution root is the tree the harness mutates: the project itself
|
|
1911
|
+
// for in-place threads/ordinary runs, or the thread's persistent worktree
|
|
1912
|
+
// for an isolated thread. Config/artifacts/contract stay anchored to
|
|
1913
|
+
// repoRoot. Both the WorkspaceManager and the git boundary resolve against
|
|
1914
|
+
// this SINGLE root.
|
|
1947
1915
|
const execRoot = this.execRootOf(input);
|
|
1948
1916
|
const wsm = new WorkspaceManager(execRoot);
|
|
1949
|
-
safeInvoke(input.onRunStart, { runId, taskId, runDir: paths.root });
|
|
1950
|
-
log.emit("run.created", { mode, prompt: redactSecrets(input.prompt) });
|
|
1951
|
-
const ledger = this.rootLedger(input, contract, log);
|
|
1952
|
-
announce?.(announcedRunContext({ log, store, paths, runId, taskId, mode, phase: "race" }, ledger, () => this.deps.delegationBudgetAuthority?.hasParent(runId) === true));
|
|
1953
1917
|
store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
|
|
1954
1918
|
log.emit("task.contract.created", { task_contract_hash: hashJson(contract) });
|
|
1955
1919
|
// Write modes need a git boundary for worktree isolation and honest diffs.
|
|
@@ -1958,17 +1922,17 @@ export class Orchestrator {
|
|
|
1958
1922
|
// silent mutation (user-locked decision, comparator: Codex requires git).
|
|
1959
1923
|
// For an isolated thread the execution root is already a git worktree, so
|
|
1960
1924
|
// this is a no-op there; for in-place it ensures the live project is git.
|
|
1961
|
-
const gitPreconditionError = await
|
|
1925
|
+
const gitPreconditionError = await ensureWriteModeGitBoundary(execRoot, log, store, paths, runId, mode);
|
|
1962
1926
|
if (gitPreconditionError) {
|
|
1963
1927
|
return {
|
|
1964
1928
|
runId,
|
|
1965
1929
|
taskId,
|
|
1966
1930
|
mode,
|
|
1967
1931
|
lifecycle: "failed",
|
|
1968
|
-
facts: makeOutcomeFacts("failed", { reason:
|
|
1932
|
+
facts: makeOutcomeFacts("failed", { reason: gitPreconditionError.reason }),
|
|
1969
1933
|
winner: null,
|
|
1970
1934
|
runDir: paths.root,
|
|
1971
|
-
summary: gitPreconditionError,
|
|
1935
|
+
summary: gitPreconditionError.message,
|
|
1972
1936
|
candidates: [],
|
|
1973
1937
|
};
|
|
1974
1938
|
}
|
|
@@ -2066,7 +2030,7 @@ export class Orchestrator {
|
|
|
2066
2030
|
attemptId,
|
|
2067
2031
|
intent: this.candidateIntent(input),
|
|
2068
2032
|
harnessId: routed.adapter.id,
|
|
2069
|
-
cost: attemptCostEvidence(routed.adapter.id, attemptId, i > 0
|
|
2033
|
+
cost: attemptCostEvidence(routed.adapter.id, attemptId, this.reservationEstimateUsd(input, i > 0), this.routeBillingKnowledge(input, routed.adapter.id)),
|
|
2070
2034
|
});
|
|
2071
2035
|
log.emit("budget.lease.created", {
|
|
2072
2036
|
granted: lease.granted,
|
|
@@ -2146,6 +2110,9 @@ export class Orchestrator {
|
|
|
2146
2110
|
const knobs = this.routeSpecKnobs(slot.routed, contract, downgradeModel ?? undefined, input.effort);
|
|
2147
2111
|
const effectiveWeb = this.discloseWebUpgrade(log, slot.routed, knobs.webPolicy, slot.attemptId);
|
|
2148
2112
|
let envelope;
|
|
2113
|
+
// Declared OUTSIDE the try so the catch below can state what the attempt
|
|
2114
|
+
// actually ran under, not merely that it stopped.
|
|
2115
|
+
let harnessHome;
|
|
2149
2116
|
try {
|
|
2150
2117
|
log.emit("harness.started", {
|
|
2151
2118
|
harness_id: adapter.id,
|
|
@@ -2165,6 +2132,7 @@ export class Orchestrator {
|
|
|
2165
2132
|
requestedSingleCandidate &&
|
|
2166
2133
|
slot.routed.implementationTransport !== "git_patch_envelope",
|
|
2167
2134
|
});
|
|
2135
|
+
harnessHome = this.harnessHomeFor(wsm, envelope, slot.routed, input);
|
|
2168
2136
|
const run = await this.runCandidateInEnvelope(slot.routed, envelope, slot.attemptId, slot.label, contract, input.prompt, store, paths, wsm, ledger, candidateAccess, (ev) => {
|
|
2169
2137
|
const safeEv = redactHarnessEvent(ev);
|
|
2170
2138
|
safeInvoke(input.onHarnessEvent, safeEv);
|
|
@@ -2175,7 +2143,8 @@ export class Orchestrator {
|
|
|
2175
2143
|
return false;
|
|
2176
2144
|
budgetStopped = true;
|
|
2177
2145
|
return true;
|
|
2178
|
-
}, input, requestedSingleCandidate)
|
|
2146
|
+
}, input, requestedSingleCandidate, // W-C4 deltas: single-candidate chat lane only (racing = noise x N)
|
|
2147
|
+
undefined, undefined, harnessHome);
|
|
2179
2148
|
ledger.settle(slot.leaseId, attemptUsageCostSettlement(run.cost, run.costEstimated, run.attemptId, run.harnessId, run.telemetry.authMode, run.telemetry.usageCost));
|
|
2180
2149
|
log.emit("harness.completed", {
|
|
2181
2150
|
harness_id: adapter.id,
|
|
@@ -2232,6 +2201,13 @@ export class Orchestrator {
|
|
|
2232
2201
|
continuation_count: candidateContinuationCount,
|
|
2233
2202
|
packet_turns: packet.continuity.disclosure.packetTurns,
|
|
2234
2203
|
});
|
|
2204
|
+
log.emit("harness.started", {
|
|
2205
|
+
harness_id: adapter.id,
|
|
2206
|
+
attempt_id: contAttemptId,
|
|
2207
|
+
external_context_policy: knobs.webPolicy,
|
|
2208
|
+
...(knobs.ignored.length > 0 ? { ignored_settings: knobs.ignored } : {}),
|
|
2209
|
+
continuation_of: run.attemptId,
|
|
2210
|
+
});
|
|
2235
2211
|
const contLeaseId = contLease.lease?.lease_id ?? "";
|
|
2236
2212
|
try {
|
|
2237
2213
|
const contRun = await this.runCandidateInEnvelope(slot.routed, envelope, contAttemptId, slot.label, contract, input.prompt, store, paths, wsm, ledger, candidateAccess, (ev) => {
|
|
@@ -2244,7 +2220,7 @@ export class Orchestrator {
|
|
|
2244
2220
|
return false;
|
|
2245
2221
|
budgetStopped = true;
|
|
2246
2222
|
return true;
|
|
2247
|
-
}, input, requestedSingleCandidate, undefined, packet.pointerLine ?? undefined);
|
|
2223
|
+
}, input, requestedSingleCandidate, undefined, packet.pointerLine ?? undefined, harnessHome);
|
|
2248
2224
|
ledger.settle(contLeaseId, attemptUsageCostSettlement(contRun.cost, contRun.costEstimated, contRun.attemptId, contRun.harnessId, contRun.telemetry.authMode, contRun.telemetry.usageCost));
|
|
2249
2225
|
log.emit("harness.completed", {
|
|
2250
2226
|
harness_id: adapter.id,
|
|
@@ -2290,6 +2266,7 @@ export class Orchestrator {
|
|
|
2290
2266
|
const failureCost = AC.attemptFailureCost(err, "post-stream-error", 0);
|
|
2291
2267
|
ledger.settle(slot.leaseId, failureCost.settlement);
|
|
2292
2268
|
const message = safeErrorMessage(err);
|
|
2269
|
+
const declared = declaredFailure(err);
|
|
2293
2270
|
const infraPhase = envelope === undefined ? "workspace" : "harness";
|
|
2294
2271
|
log.emit("harness.completed", {
|
|
2295
2272
|
harness_id: adapter.id,
|
|
@@ -2298,7 +2275,7 @@ export class Orchestrator {
|
|
|
2298
2275
|
error: message,
|
|
2299
2276
|
phase: infraPhase,
|
|
2300
2277
|
});
|
|
2301
|
-
store.writeYaml(join(paths.attemptsDir, slot.attemptId, "attempt.yaml"), AC.attemptFailureRecord(slot.attemptId, adapter.id, failureCost, infraPhase, message));
|
|
2278
|
+
store.writeYaml(join(paths.attemptsDir, slot.attemptId, "attempt.yaml"), AC.attemptFailureRecord(slot.attemptId, adapter.id, failureCost, infraPhase, message, appliedAttemptFacts(harnessHome, slot.routed.adapterAccess, this.effectiveProfileId(input, adapter.id))));
|
|
2302
2279
|
runsBySlot[slotIdx] = {
|
|
2303
2280
|
attemptId: slot.attemptId,
|
|
2304
2281
|
harnessId: adapter.id,
|
|
@@ -2315,6 +2292,11 @@ export class Orchestrator {
|
|
|
2315
2292
|
slot.routed.delegationRequirement,
|
|
2316
2293
|
], knobs.model),
|
|
2317
2294
|
infraPhase,
|
|
2295
|
+
// Keep a TYPED pre-spawn refusal (spent quota window + its reset)
|
|
2296
|
+
// alive past this catch; `message` alone would force the terminal to
|
|
2297
|
+
// read prose back out.
|
|
2298
|
+
...(declared.code ? { declaredFailure: declared } : {}),
|
|
2299
|
+
applied: appliedAttemptFacts(harnessHome, slot.routed.adapterAccess, this.effectiveProfileId(input, adapter.id)),
|
|
2318
2300
|
};
|
|
2319
2301
|
}
|
|
2320
2302
|
finally {
|
|
@@ -2324,6 +2306,9 @@ export class Orchestrator {
|
|
|
2324
2306
|
};
|
|
2325
2307
|
await runBounded(slots, Math.min(slots.length, MAX_PARALLEL_CANDIDATES), runSlot);
|
|
2326
2308
|
const runs = runsBySlot.filter((r) => r !== undefined);
|
|
2309
|
+
// Fail-closed terminal: a delegated mutating run whose attempts cannot state
|
|
2310
|
+
// their applied confinement refuses instead of passing.
|
|
2311
|
+
assertDelegatedEvidence(input.delegated === true, candidateAccess, runs);
|
|
2327
2312
|
const cancelledCandidates = () => runs.map((r) => ({
|
|
2328
2313
|
attemptId: r.attemptId,
|
|
2329
2314
|
harnessId: r.harnessId,
|
|
@@ -2447,15 +2432,21 @@ export class Orchestrator {
|
|
|
2447
2432
|
// harness cause (timeout, rate limit, crash, config) gets remediation that
|
|
2448
2433
|
// fits it, instead of a doomed "Check harness authentication".
|
|
2449
2434
|
const harnessCategory = dominantHarnessFailureCategory(first.telemetry.transientFailures);
|
|
2435
|
+
// A run speaks with a candidate's TYPED refusal only when EVERY candidate
|
|
2436
|
+
// died of the same one (candidateEvidence owns that rule); mixed causes
|
|
2437
|
+
// keep the honest harness terminal.
|
|
2438
|
+
const unanimous = unanimousDeclaredFailure(runs);
|
|
2450
2439
|
writeFailure(store, paths, {
|
|
2451
2440
|
phase,
|
|
2452
|
-
category: phase === "workspace" ? "project" : "harness_error",
|
|
2441
|
+
category: unanimous?.category ?? (phase === "workspace" ? "project" : "harness_error"),
|
|
2442
|
+
code: unanimous?.code ?? null,
|
|
2453
2443
|
harnessId: first.harnessId,
|
|
2454
2444
|
attemptId: first.attemptId,
|
|
2455
2445
|
safeMessage: rootCause,
|
|
2456
2446
|
rawDetailRef: `attempts/${first.attemptId}/attempt.yaml`,
|
|
2457
2447
|
eventRefs: existingEventRefs,
|
|
2458
2448
|
runDir: paths.root,
|
|
2449
|
+
resetsAt: unanimous?.resetsAt ?? null,
|
|
2459
2450
|
nextActions: first.secretDiffRefusal
|
|
2460
2451
|
? secretDiff.secretDiffNextActions(first.secretDiffRefusal)
|
|
2461
2452
|
: phase === "workspace"
|
|
@@ -2519,7 +2510,7 @@ export class Orchestrator {
|
|
|
2519
2510
|
// candidates (a trivial greeting in agent mode no longer burns two reviewers on
|
|
2520
2511
|
// "(empty diff)"). Candidates still flow through arbitration/gates so the
|
|
2521
2512
|
// no_op/answer outcome and gate failures are unchanged.
|
|
2522
|
-
evidences = await this.reviewRuns(workingRuns, reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal);
|
|
2513
|
+
evidences = await this.reviewRuns(workingRuns, reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal, this.reservationEstimateUsd(input));
|
|
2523
2514
|
}
|
|
2524
2515
|
catch (err) {
|
|
2525
2516
|
// Review preflight/evidence failures end TERMINALLY with artifacts —
|
|
@@ -2544,7 +2535,7 @@ export class Orchestrator {
|
|
|
2544
2535
|
attemptId: "synth",
|
|
2545
2536
|
intent: "synthesize",
|
|
2546
2537
|
harnessId: synthRouted.adapter.id,
|
|
2547
|
-
cost: attemptCostEvidence(synthRouted.adapter.id, "synth",
|
|
2538
|
+
cost: attemptCostEvidence(synthRouted.adapter.id, "synth", this.reservationEstimateUsd(input), this.routeBillingKnowledge(input, synthRouted.adapter.id)),
|
|
2548
2539
|
});
|
|
2549
2540
|
if (lease.granted) {
|
|
2550
2541
|
let envelope;
|
|
@@ -2567,11 +2558,12 @@ export class Orchestrator {
|
|
|
2567
2558
|
dirtyPolicy: "snapshot",
|
|
2568
2559
|
accessProfile: candidateAccess,
|
|
2569
2560
|
});
|
|
2561
|
+
const synthHome = this.harnessHomeFor(wsm, envelope, synthRouted, input);
|
|
2570
2562
|
const run = await this.runCandidateInEnvelope(synthRouted, envelope, "synth", "Synthesis", contract, synthesisInput.prompt, store, paths, wsm, ledger, candidateAccess, (ev) => {
|
|
2571
2563
|
const safeEv = redactHarnessEvent(ev);
|
|
2572
2564
|
safeInvoke(input.onHarnessEvent, safeEv);
|
|
2573
2565
|
log.emit("harness.event", harnessEventPayload(synthAdapter.id, "synth", safeEv));
|
|
2574
|
-
}, input.signal, undefined, input.effort, "synthesize", log, effectiveWeb, this.interactionChannelFor(input, log, runId, taskId, "synth", synthAdapter.id, synthRouted.supportsInteractive), undefined, input, false, synthesisInput.content);
|
|
2566
|
+
}, input.signal, undefined, input.effort, "synthesize", log, effectiveWeb, this.interactionChannelFor(input, log, runId, taskId, "synth", synthAdapter.id, synthRouted.supportsInteractive), undefined, input, false, synthesisInput.content, undefined, synthHome);
|
|
2575
2567
|
ledger.settle(lease.lease?.lease_id ?? "", attemptUsageCostSettlement(run.cost, run.costEstimated, run.attemptId, run.harnessId, run.telemetry.authMode, run.telemetry.usageCost));
|
|
2576
2568
|
reviewEnvelopes.push(envelope);
|
|
2577
2569
|
envelope = undefined;
|
|
@@ -2580,7 +2572,7 @@ export class Orchestrator {
|
|
|
2580
2572
|
runs.push(run);
|
|
2581
2573
|
try {
|
|
2582
2574
|
if (isWorkingCandidate(run)) {
|
|
2583
|
-
const synthEvidence = await this.reviewRuns([run], reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal);
|
|
2575
|
+
const synthEvidence = await this.reviewRuns([run], reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal, this.reservationEstimateUsd(input));
|
|
2584
2576
|
evidences.push(...synthEvidence);
|
|
2585
2577
|
workingRuns.push(run);
|
|
2586
2578
|
}
|
|
@@ -2629,8 +2621,12 @@ export class Orchestrator {
|
|
|
2629
2621
|
// Winner can only be a candidate that actually produced work; corpses are
|
|
2630
2622
|
// excluded from arbitration upstream and from the fallback here.
|
|
2631
2623
|
const winnerRun = workingRuns.find((r) => r.attemptId === result.decision.winner) ?? workingRuns[0];
|
|
2632
|
-
|
|
2633
|
-
|
|
2624
|
+
const winnerEvidence = winnerRun
|
|
2625
|
+
? evidences.find((e) => e.attemptId === winnerRun.attemptId)
|
|
2626
|
+
: undefined;
|
|
2627
|
+
// D9 winner-only NEEDS_HUMAN gate, fail-closed on a winner with no review
|
|
2628
|
+
// evidence record (see winnerNeedsHuman).
|
|
2629
|
+
const needsHuman = winnerNeedsHuman(winnerRun?.attemptId ?? null, evidences);
|
|
2634
2630
|
// Run-level review_verified is the WINNER's verification: an
|
|
2635
2631
|
// empty-diff loser's unverified route must not drag the shipped result's
|
|
2636
2632
|
// flag false. No winner -> fall back to the all-candidates view.
|
|
@@ -2693,12 +2689,11 @@ export class Orchestrator {
|
|
|
2693
2689
|
})) {
|
|
2694
2690
|
log.emit("output.ready", { kind: "artifact", path });
|
|
2695
2691
|
}
|
|
2696
|
-
assertNoSecretLikeTokens("final patch diff", winnerRun.diff);
|
|
2692
|
+
secretDiff.assertNoSecretLikeTokens("final patch diff", winnerRun.diff);
|
|
2697
2693
|
const patchSha256 = sha256(winnerRun.diff);
|
|
2698
2694
|
store.writeText(join(paths.finalDir, "patch.diff"), winnerRun.diff);
|
|
2699
2695
|
const wstats = diffStats(winnerRun.diff);
|
|
2700
2696
|
const hasDiff = winnerRun.diff.trim().length > 0;
|
|
2701
|
-
const winnerEvidence = evidences.find((e) => e.attemptId === winnerRun.attemptId);
|
|
2702
2697
|
const blockers = winnerEvidence
|
|
2703
2698
|
? winnerEvidence.findings.filter((f) => isBlocking(f)).length
|
|
2704
2699
|
: 0;
|
|
@@ -3011,7 +3006,7 @@ export class Orchestrator {
|
|
|
3011
3006
|
env: reviewHome.env,
|
|
3012
3007
|
}).finally(() => reviewHome.dispose());
|
|
3013
3008
|
}
|
|
3014
|
-
async reviewRuns(runs, reviewers, reviewVerified, reviewDir, cwd, contract, store, paths, log, ledger, taskId, signal) {
|
|
3009
|
+
async reviewRuns(runs, reviewers, reviewVerified, reviewDir, cwd, contract, store, paths, log, ledger, taskId, signal, reservationEstimateUsd) {
|
|
3015
3010
|
const evidences = [];
|
|
3016
3011
|
for (const run of runs) {
|
|
3017
3012
|
const candidateCwd = run.reviewCwd ?? cwd;
|
|
@@ -3030,7 +3025,7 @@ export class Orchestrator {
|
|
|
3030
3025
|
attemptId: run.attemptId,
|
|
3031
3026
|
intent: "review",
|
|
3032
3027
|
harnessId: "review-panel",
|
|
3033
|
-
cost: attemptCostEvidence("review-panel", run.attemptId),
|
|
3028
|
+
cost: attemptCostEvidence("review-panel", run.attemptId, reservationEstimateUsd),
|
|
3034
3029
|
})
|
|
3035
3030
|
: undefined;
|
|
3036
3031
|
const result = hasDiff && reviewers.length > 0 && (reviewLease?.granted ?? true)
|
|
@@ -3174,10 +3169,21 @@ export class Orchestrator {
|
|
|
3174
3169
|
const runId = input.runId ?? newId("run");
|
|
3175
3170
|
// Contract validation BEFORE the run is announced (see runRace).
|
|
3176
3171
|
const contract = this.buildContract(input, taskId, mode);
|
|
3177
|
-
const
|
|
3178
|
-
const
|
|
3179
|
-
const
|
|
3180
|
-
|
|
3172
|
+
const planBrief = verifiedPlanBrief(input);
|
|
3173
|
+
const quotaSnapshots = this.quotaSnapshotPreflight();
|
|
3174
|
+
const { store, paths, log, ledger } = beginAnnouncedRun({
|
|
3175
|
+
input,
|
|
3176
|
+
contract,
|
|
3177
|
+
quotaSnapshots,
|
|
3178
|
+
store: this.artifactStore(input),
|
|
3179
|
+
authority: this.deps.delegationBudgetAuthority,
|
|
3180
|
+
runId,
|
|
3181
|
+
taskId,
|
|
3182
|
+
mode,
|
|
3183
|
+
phase: "convergence",
|
|
3184
|
+
prompt: input.prompt,
|
|
3185
|
+
}, announce);
|
|
3186
|
+
input = withPlanBrief(input, store, paths, log, planBrief);
|
|
3181
3187
|
// The execution root is the tree the harness mutates (thread worktree for an
|
|
3182
3188
|
// isolated thread, else the project). The WorkspaceManager AND the git
|
|
3183
3189
|
// boundary must resolve against the SAME root — the race path does so via the
|
|
@@ -3186,27 +3192,11 @@ export class Orchestrator {
|
|
|
3186
3192
|
const execRoot = this.execRootOf(input);
|
|
3187
3193
|
const wsm = new WorkspaceManager(execRoot);
|
|
3188
3194
|
const readiness = new ReadinessLedger();
|
|
3189
|
-
|
|
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
|
-
}
|
|
3195
|
+
store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
|
|
3206
3196
|
// Live (in-place) isolation deliberately tolerates non-git stateful
|
|
3207
3197
|
// environments; only envelope isolation needs the git boundary.
|
|
3208
3198
|
if (!input.inPlace) {
|
|
3209
|
-
const gitPreconditionError = await
|
|
3199
|
+
const gitPreconditionError = await ensureWriteModeGitBoundary(execRoot, log, store, paths, runId, mode);
|
|
3210
3200
|
if (gitPreconditionError) {
|
|
3211
3201
|
return {
|
|
3212
3202
|
spendUsd: ledger.spend(),
|
|
@@ -3214,10 +3204,10 @@ export class Orchestrator {
|
|
|
3214
3204
|
taskId,
|
|
3215
3205
|
mode,
|
|
3216
3206
|
lifecycle: "failed",
|
|
3217
|
-
facts: makeOutcomeFacts("failed", { reason:
|
|
3207
|
+
facts: makeOutcomeFacts("failed", { reason: gitPreconditionError.reason }),
|
|
3218
3208
|
winner: null,
|
|
3219
3209
|
runDir: paths.root,
|
|
3220
|
-
summary: gitPreconditionError,
|
|
3210
|
+
summary: gitPreconditionError.message,
|
|
3221
3211
|
candidates: [],
|
|
3222
3212
|
};
|
|
3223
3213
|
}
|
|
@@ -3406,7 +3396,7 @@ export class Orchestrator {
|
|
|
3406
3396
|
attemptId,
|
|
3407
3397
|
intent: "repair",
|
|
3408
3398
|
harnessId: adapter.id,
|
|
3409
|
-
cost: attemptCostEvidence(adapter.id, attemptId,
|
|
3399
|
+
cost: attemptCostEvidence(adapter.id, attemptId, this.reservationEstimateUsd(input), this.routeBillingKnowledge(input, adapter.id)),
|
|
3410
3400
|
});
|
|
3411
3401
|
if (!lease.granted) {
|
|
3412
3402
|
exhausted = true;
|
|
@@ -3415,7 +3405,9 @@ export class Orchestrator {
|
|
|
3415
3405
|
const knobs = this.routeSpecKnobs(routed, contract, undefined, input.effort);
|
|
3416
3406
|
const effectiveWeb = this.discloseWebUpgrade(log, routed, knobs.webPolicy, attemptId);
|
|
3417
3407
|
let run;
|
|
3408
|
+
let harnessHome;
|
|
3418
3409
|
try {
|
|
3410
|
+
harnessHome = this.harnessHomeFor(wsm, envelope, routed, input);
|
|
3419
3411
|
log.emit("harness.started", {
|
|
3420
3412
|
harness_id: adapter.id,
|
|
3421
3413
|
attempt_id: attemptId,
|
|
@@ -3429,7 +3421,8 @@ export class Orchestrator {
|
|
|
3429
3421
|
}, input.signal, undefined, input.effort, "repair", log, effectiveWeb, this.interactionChannelFor(input, log, runId, taskId, attemptId, adapter.id, routed.supportsInteractive), (streamedUsd) => {
|
|
3430
3422
|
ledger.updateHold(lease.lease?.lease_id ?? "", streamedUsd);
|
|
3431
3423
|
return ledger.tier() === "hard";
|
|
3432
|
-
}, input, true)
|
|
3424
|
+
}, input, true, // convergence runs one candidate: live deltas on (W-C4)
|
|
3425
|
+
undefined, undefined, harnessHome);
|
|
3433
3426
|
ledger.settle(lease.lease?.lease_id ?? "", attemptUsageCostSettlement(run.cost, run.costEstimated, run.attemptId, run.harnessId, run.telemetry.authMode, run.telemetry.usageCost));
|
|
3434
3427
|
log.emit("harness.completed", {
|
|
3435
3428
|
harness_id: adapter.id,
|
|
@@ -3452,8 +3445,9 @@ export class Orchestrator {
|
|
|
3452
3445
|
status: "failed",
|
|
3453
3446
|
error: message,
|
|
3454
3447
|
});
|
|
3455
|
-
store.writeYaml(join(paths.attemptsDir, attemptId, "attempt.yaml"), AC.attemptFailureRecord(attemptId, adapter.id, failureCost, "harness", message));
|
|
3448
|
+
store.writeYaml(join(paths.attemptsDir, attemptId, "attempt.yaml"), AC.attemptFailureRecord(attemptId, adapter.id, failureCost, "harness", message, appliedAttemptFacts(harnessHome, routed.adapterAccess, this.effectiveProfileId(input, adapter.id))));
|
|
3456
3449
|
run = {
|
|
3450
|
+
applied: appliedAttemptFacts(harnessHome, routed.adapterAccess, this.effectiveProfileId(input, adapter.id)),
|
|
3457
3451
|
attemptId,
|
|
3458
3452
|
harnessId: adapter.id,
|
|
3459
3453
|
label: `Attempt ${attempt}`,
|
|
@@ -3467,6 +3461,9 @@ export class Orchestrator {
|
|
|
3467
3461
|
};
|
|
3468
3462
|
}
|
|
3469
3463
|
lastRun = run;
|
|
3464
|
+
// Fail-closed twin of the candidate lane's gate: this loop terminalizes
|
|
3465
|
+
// per attempt, so the proof is spent per attempt.
|
|
3466
|
+
assertDelegatedEvidence(input.delegated === true, convergenceAccess, [run]);
|
|
3470
3467
|
attemptTelemetries.push({ attemptId, harnessId: adapter.id, telemetry: run.telemetry });
|
|
3471
3468
|
// Cancellation/deadline keeps priority over a belt failure finalized concurrently.
|
|
3472
3469
|
if (input.signal?.aborted)
|
|
@@ -3518,7 +3515,7 @@ export class Orchestrator {
|
|
|
3518
3515
|
attemptId,
|
|
3519
3516
|
intent: "review",
|
|
3520
3517
|
harnessId: "review-panel",
|
|
3521
|
-
cost: attemptCostEvidence("review-panel", attemptId),
|
|
3518
|
+
cost: attemptCostEvidence("review-panel", attemptId, this.reservationEstimateUsd(input)),
|
|
3522
3519
|
})
|
|
3523
3520
|
: null;
|
|
3524
3521
|
const reviewResult = reviewers.length > 0 && (reviewLease?.granted ?? false)
|
|
@@ -3808,7 +3805,7 @@ export class Orchestrator {
|
|
|
3808
3805
|
// work_product (its partial patch.diff stays diagnostic via attempts/);
|
|
3809
3806
|
// in-place keeps the product so the honest Revert offer survives.
|
|
3810
3807
|
if (lastRun && (!interrupted || input.inPlace === true)) {
|
|
3811
|
-
assertNoSecretLikeTokens("final patch diff", lastRun.diff);
|
|
3808
|
+
secretDiff.assertNoSecretLikeTokens("final patch diff", lastRun.diff);
|
|
3812
3809
|
const patchSha256 = sha256(lastRun.diff);
|
|
3813
3810
|
store.writeText(join(paths.finalDir, "patch.diff"), lastRun.diff);
|
|
3814
3811
|
// Honest apply-state (parity with runRace single-candidate in-place): a
|
|
@@ -3978,265 +3975,77 @@ export class Orchestrator {
|
|
|
3978
3975
|
reviewVerified: actualReviewVerified,
|
|
3979
3976
|
};
|
|
3980
3977
|
}
|
|
3981
|
-
/**
|
|
3982
|
-
|
|
3983
|
-
const { input, contract, taskId, runId, log, store, paths, ledger, routed, attemptId } = args;
|
|
3984
|
-
const adapter = routed.adapter;
|
|
3985
|
-
const lease = ledger.reserve({
|
|
3986
|
-
taskId,
|
|
3987
|
-
attemptId,
|
|
3988
|
-
intent: args.intent,
|
|
3989
|
-
harnessId: adapter.id,
|
|
3990
|
-
cost: attemptCostEvidence(adapter.id, attemptId, undefined, this.routeBillingKnowledge(input, adapter.id)),
|
|
3991
|
-
});
|
|
3992
|
-
if (!lease.granted) {
|
|
3993
|
-
log.emit("budget.lease.created", {
|
|
3994
|
-
granted: false,
|
|
3995
|
-
reason: lease.reason,
|
|
3996
|
-
denied: lease.denied,
|
|
3997
|
-
attempt_id: attemptId,
|
|
3998
|
-
harness_id: adapter.id,
|
|
3999
|
-
});
|
|
4000
|
-
return {
|
|
4001
|
-
attemptId,
|
|
4002
|
-
harnessId: adapter.id,
|
|
4003
|
-
status: "failed",
|
|
4004
|
-
outcomeClass: "clean", // never spawned: refused pre-flight by the budget gate
|
|
4005
|
-
error: lease.reason ?? "budget lease denied",
|
|
4006
|
-
text: null,
|
|
4007
|
-
telemetry: null,
|
|
4008
|
-
budgetDenied: true,
|
|
4009
|
-
budgetDenial: {
|
|
4010
|
-
code: lease.denied ?? "hard_cap",
|
|
4011
|
-
reason: lease.reason ?? "budget lease denied",
|
|
4012
|
-
harnessId: adapter.id,
|
|
4013
|
-
attemptId,
|
|
4014
|
-
},
|
|
4015
|
-
};
|
|
4016
|
-
}
|
|
4017
|
-
const knobs = this.routeSpecKnobs(routed, contract, undefined, input.effort);
|
|
4018
|
-
const effectiveWeb = this.discloseWebUpgrade(log, routed, knobs.webPolicy, attemptId);
|
|
4019
|
-
const planSessionFields = this.sessionSpecFields(input, adapter.id, log);
|
|
4020
|
-
// Continuity (INV-137): a thread PLAN turn is a chat turn — hydrate a
|
|
4021
|
-
// lane switch/gap with a packet and disclose it.
|
|
4022
|
-
const laneContinuity = args.laneRun
|
|
4023
|
-
? await this.resolveContinuity(input, adapter.id, planSessionFields.credential_profile?.profile_id ?? input.credentialProfileId ?? null, planSessionFields.resume_session_id !== null, store, paths, this.execRootOf(input), log)
|
|
4024
|
-
: null;
|
|
4025
|
-
const spec = HarnessRunSpec.parse({
|
|
4026
|
-
session_id: newId("ses"),
|
|
4027
|
-
intent: args.intent,
|
|
4028
|
-
prompt: laneContinuity?.pointerLine
|
|
4029
|
-
? `${args.promptBody}\n\n${laneContinuity.pointerLine}`
|
|
4030
|
-
: args.promptBody,
|
|
4031
|
-
cwd: this.execRootOf(input),
|
|
4032
|
-
access: "readonly",
|
|
4033
|
-
// Planners must SEE any image/file the user attached (e.g. "plan a fix for
|
|
4034
|
-
// what's in this screenshot"), not just agent/race runs.
|
|
4035
|
-
attachments: input.attachments ?? [],
|
|
4036
|
-
...planSessionFields,
|
|
4037
|
-
...this.harnessSpecKnobs(contract, knobs, args.intent),
|
|
4038
|
-
env_inheritance: envInheritance(this.config(input.repoRoot)),
|
|
4039
|
-
// A thread plan turn spawns in its DURABLE per-lane home so its native
|
|
4040
|
-
// session is reachable for resume next turn (INV-034); a non-thread
|
|
4041
|
-
// plan keeps the disposable route-context home.
|
|
4042
|
-
env: (args.laneRun ? this.laneHomeEnvFor(input, adapter.id) : null) ?? args.fallbackHome,
|
|
4043
|
-
});
|
|
4044
|
-
const plannerAbort = new AbortController();
|
|
4045
|
-
spec.extra["abortSignal"] = input.signal
|
|
4046
|
-
? AbortSignal.any([input.signal, plannerAbort.signal])
|
|
4047
|
-
: plannerAbort.signal;
|
|
4048
|
-
const planInteraction = this.interactionChannelFor(input, log, runId, taskId, attemptId, adapter.id, routed.supportsInteractive);
|
|
4049
|
-
if (planInteraction)
|
|
4050
|
-
spec.extra["interactionChannel"] = planInteraction;
|
|
4051
|
-
// D-16: compile the WorkReport envelope for the plan lane (require plan text
|
|
4052
|
-
// below folds the deliverable; the veto rides work_state).
|
|
4053
|
-
const planWorkEnvelope = this.workReportEnvelopeFor(routed, contract, Boolean(planInteraction));
|
|
4054
|
-
const planWorkMode = this.applyWorkEnvelope(spec, planWorkEnvelope);
|
|
4055
|
-
const attemptEventsPath = join(paths.attemptsDir, attemptId, "events.jsonl");
|
|
4056
|
-
const answer = new AnswerAssembly();
|
|
4057
|
-
const telemetry = createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required ||
|
|
4058
|
-
knobs.webPolicy === "cached" ||
|
|
4059
|
-
knobs.webPolicy === "live", effectiveWeb, [],
|
|
4060
|
-
// Requested-model capture: a plan lane silently downgraded to another
|
|
4061
|
-
// model surfaces the mismatch in its route receipt, just like agent.
|
|
4062
|
-
knobs.model);
|
|
4063
|
-
const onAbort = () => {
|
|
4064
|
-
void adapter.cancel?.(spec.session_id)?.catch(() => { });
|
|
4065
|
-
};
|
|
4066
|
-
if (input.signal) {
|
|
4067
|
-
if (input.signal.aborted)
|
|
4068
|
-
onAbort();
|
|
4069
|
-
else
|
|
4070
|
-
input.signal.addEventListener("abort", onAbort, { once: true });
|
|
4071
|
-
}
|
|
4072
|
-
let cost = 0;
|
|
4073
|
-
let costEstimated = false;
|
|
4074
|
-
let harnessError = null;
|
|
4075
|
-
const budgetSignalState = { quotaPressureDisclosed: false };
|
|
4076
|
-
try {
|
|
4077
|
-
log.emit("harness.started", {
|
|
4078
|
-
harness_id: adapter.id,
|
|
4079
|
-
attempt_id: attemptId,
|
|
4080
|
-
external_context_policy: knobs.webPolicy,
|
|
4081
|
-
...(knobs.ignored.length > 0 ? { ignored_settings: knobs.ignored } : {}),
|
|
4082
|
-
});
|
|
4083
|
-
if (!input.signal?.aborted) {
|
|
4084
|
-
const watchedPlan = withInactivityWatchdog(adapter.run(spec), {
|
|
4085
|
-
timeoutMs: harnessInactivityTimeoutMs(this.config(input.repoRoot)),
|
|
4086
|
-
onTimeout: () => {
|
|
4087
|
-
plannerAbort.abort();
|
|
4088
|
-
void adapter.cancel?.(spec.session_id)?.catch(() => { });
|
|
4089
|
-
},
|
|
4090
|
-
isSuspended: () => (planInteraction?.pendingCount?.() ?? 0) > 0,
|
|
4091
|
-
});
|
|
4092
|
-
for await (const ev of watchedPlan) {
|
|
4093
|
-
if (input.signal?.aborted)
|
|
4094
|
-
break;
|
|
4095
|
-
const safeEv = redactHarnessEvent(ev);
|
|
4096
|
-
safeInvoke(input.onHarnessEvent, safeEv);
|
|
4097
|
-
// A thread PLAN turn IS a chat turn now (INV-034): its native
|
|
4098
|
-
// session lives in the DURABLE per-lane home, so record it for the
|
|
4099
|
-
// next lane turn's resume. Council members are distinct lanes.
|
|
4100
|
-
if (args.laneRun)
|
|
4101
|
-
observeNativeSessionEvent(input, adapter.id, safeEv);
|
|
4102
|
-
observeAuthSwitch(log, adapter.id, attemptId, safeEv);
|
|
4103
|
-
log.emit("harness.event", harnessEventPayload(adapter.id, attemptId, safeEv));
|
|
4104
|
-
appendLine(attemptEventsPath, JSON.stringify(safeEv));
|
|
4105
|
-
observeAttemptTelemetry(telemetry, safeEv);
|
|
4106
|
-
if (safeEv.plan_progress) {
|
|
4107
|
-
log.emit("plan.progress", {
|
|
4108
|
-
attempt_id: attemptId,
|
|
4109
|
-
harness_id: adapter.id,
|
|
4110
|
-
items: safeEv.plan_progress.items,
|
|
4111
|
-
});
|
|
4112
|
-
}
|
|
4113
|
-
// read-only routes burn quota too — same single owner as the agent loop.
|
|
4114
|
-
observeBudgetSignals(ledger, log, adapter.id, attemptId, safeEv, budgetSignalState);
|
|
4115
|
-
this.deps.quotaEventSink?.(adapter.id, safeEv);
|
|
4116
|
-
if (safeEv.type === "usage" && safeEv.usage?.cost_usd) {
|
|
4117
|
-
cost += safeEv.usage.cost_usd;
|
|
4118
|
-
if (safeEv.usage.estimated)
|
|
4119
|
-
costEstimated = true;
|
|
4120
|
-
log.emit("budget.observation", {
|
|
4121
|
-
harness_id: adapter.id,
|
|
4122
|
-
attempt_id: attemptId,
|
|
4123
|
-
kind: "spend",
|
|
4124
|
-
usd: safeEv.usage.cost_usd,
|
|
4125
|
-
estimated: safeEv.usage.estimated === true,
|
|
4126
|
-
});
|
|
4127
|
-
}
|
|
4128
|
-
// A TYPED final message wins verbatim over joined narration.
|
|
4129
|
-
answer.observe(safeEv);
|
|
4130
|
-
if (safeEv.type === "error")
|
|
4131
|
-
harnessError = safeEv.error ? redactSecrets(safeEv.error) : "harness emitted an error";
|
|
4132
|
-
}
|
|
4133
|
-
}
|
|
4134
|
-
}
|
|
4135
|
-
catch (err) {
|
|
4136
|
-
harnessError = safeErrorMessage(err);
|
|
4137
|
-
}
|
|
4138
|
-
finally {
|
|
4139
|
-
input.signal?.removeEventListener("abort", onAbort);
|
|
4140
|
-
ledger.settle(lease.lease?.lease_id ?? "", attemptUsageCostSettlement(cost, costEstimated, attemptId, adapter.id, telemetry.authMode, telemetry.usageCost));
|
|
4141
|
-
}
|
|
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).
|
|
4145
|
-
const planUnwrapped = unwrapWorkReportEnvelope(answer.machineText() ?? "", planWorkMode, {
|
|
4146
|
-
sideToolReport: telemetry.sideToolWorkReport ?? undefined,
|
|
4147
|
-
});
|
|
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);
|
|
4158
|
-
const planFinalized = finalizeAttempt({
|
|
4159
|
-
deliverableEvidence: planText.length > 0,
|
|
4160
|
-
harnessErrored: harnessError !== null && !webBlocked,
|
|
4161
|
-
workReport: planUnwrapped.workReport,
|
|
4162
|
-
workReportSource: planUnwrapped.source,
|
|
4163
|
-
workReportViolation: planUnwrapped.contractViolation,
|
|
4164
|
-
contextTerminalExhausted: telemetry.contextExhausted,
|
|
4165
|
-
});
|
|
4166
|
-
// A broken WorkReport contract is a hard failure only when the finalizer
|
|
4167
|
-
// ranked it so (a terminal context exhaustion outranks it).
|
|
4168
|
-
if (!harnessError && planFinalized.outcomeClass === "contract_failure") {
|
|
4169
|
-
harnessError = `work_report contract: ${planUnwrapped.contractViolation}`;
|
|
4170
|
-
}
|
|
4171
|
-
// D-16 r9: an interrupted (context-exhausted) planner is NEVER a clean
|
|
4172
|
-
// plan — partial text must not become final/plan.md as success. A VETO
|
|
4173
|
-
// (needs_input/incomplete work_state) is DIFFERENT by the sealed D-16
|
|
4174
|
-
// contract (X35, INV-116 canaries): the plan still delivers, lifecycle
|
|
4175
|
-
// succeeded, and the work_state veto rides the OUTCOME (non-zero exit) —
|
|
4176
|
-
// it must not be laundered into a harness failure either direction.
|
|
4177
|
-
if (!harnessError && planFinalized.outcomeClass === "interrupted") {
|
|
4178
|
-
harnessError = "context capacity exhausted before the plan completed";
|
|
4179
|
-
}
|
|
4180
|
-
const attemptError = harnessError ??
|
|
4181
|
-
(planFinalized.deliverablePresent ? null : "planner produced no plan text") ??
|
|
4182
|
-
(input.signal?.aborted ? "planner cancelled" : null);
|
|
4183
|
-
setAttemptOutcome(telemetry, {
|
|
4184
|
-
deliverablePresent: planFinalized.deliverablePresent,
|
|
4185
|
-
gatesPassed: null,
|
|
4186
|
-
harnessErrored: (harnessError !== null && !webBlocked) || planFinalized.harnessErrored,
|
|
4187
|
-
webRequiredUnsatisfied: webBlocked,
|
|
4188
|
-
workState: planFinalized.workState,
|
|
4189
|
-
});
|
|
4190
|
-
if (attemptError) {
|
|
4191
|
-
log.emit("harness.completed", {
|
|
4192
|
-
harness_id: adapter.id,
|
|
4193
|
-
attempt_id: attemptId,
|
|
4194
|
-
status: webBlocked ? "blocked" : "failed",
|
|
4195
|
-
error: attemptError,
|
|
4196
|
-
...telemetrySummary(telemetry),
|
|
4197
|
-
});
|
|
4198
|
-
return {
|
|
4199
|
-
attemptId,
|
|
4200
|
-
harnessId: adapter.id,
|
|
4201
|
-
status: webBlocked ? "blocked" : "failed",
|
|
4202
|
-
outcomeClass: planFinalized.outcomeClass,
|
|
4203
|
-
error: attemptError,
|
|
4204
|
-
text: null,
|
|
4205
|
-
telemetry,
|
|
4206
|
-
budgetDenied: false,
|
|
4207
|
-
};
|
|
4208
|
-
}
|
|
4209
|
-
const text = planText || "(no output)";
|
|
4210
|
-
log.emit("harness.completed", {
|
|
4211
|
-
harness_id: adapter.id,
|
|
4212
|
-
attempt_id: attemptId,
|
|
4213
|
-
status: "success",
|
|
4214
|
-
...telemetrySummary(telemetry),
|
|
4215
|
-
});
|
|
3978
|
+
/** Bind private route/session preparation to the planner-attempt owner. */
|
|
3979
|
+
plannerAttemptDeps() {
|
|
4216
3980
|
return {
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
3981
|
+
billingKnowledge: (input, harnessId) => this.routeBillingKnowledge(input, harnessId),
|
|
3982
|
+
inactivityTimeoutMs: (repoRoot) => harnessInactivityTimeoutMs(this.config(repoRoot)),
|
|
3983
|
+
quotaEventSink: this.deps.quotaEventSink,
|
|
3984
|
+
prepare: async (args) => {
|
|
3985
|
+
const { input, contract, taskId, runId, log, store, paths, routed, attemptId } = args;
|
|
3986
|
+
const adapter = routed.adapter;
|
|
3987
|
+
const knobs = this.routeSpecKnobs(routed, contract, undefined, input.effort);
|
|
3988
|
+
const effectiveWeb = this.discloseWebUpgrade(log, routed, knobs.webPolicy, attemptId);
|
|
3989
|
+
const sessionFields = await this.sessionSpecFields(input, adapter.id, log, routed.authRouteEstimate);
|
|
3990
|
+
const laneContinuity = args.laneRun
|
|
3991
|
+
? await this.resolveContinuity(input, adapter.id, sessionFields.credential_profile?.profile_id ?? input.credentialProfileId ?? null, sessionFields.resume_session_id !== null, sessionFields, store, paths, this.execRootOf(input), log)
|
|
3992
|
+
: null;
|
|
3993
|
+
const spec = HarnessRunSpec.parse({
|
|
3994
|
+
session_id: newId("ses"),
|
|
3995
|
+
intent: args.intent,
|
|
3996
|
+
prompt: laneContinuity?.pointerLine
|
|
3997
|
+
? `${args.promptBody}\n\n${laneContinuity.pointerLine}`
|
|
3998
|
+
: args.promptBody,
|
|
3999
|
+
cwd: this.execRootOf(input),
|
|
4000
|
+
access: "readonly",
|
|
4001
|
+
attachments: input.attachments ?? [],
|
|
4002
|
+
...sessionFields,
|
|
4003
|
+
...this.harnessSpecKnobs(contract, knobs, args.intent),
|
|
4004
|
+
env_inheritance: envInheritance(this.config(input.repoRoot)),
|
|
4005
|
+
env: (args.laneRun ? this.laneHomeEnvFor(input, adapter.id) : null) ?? args.fallbackHome,
|
|
4006
|
+
});
|
|
4007
|
+
const plannerAbort = new AbortController();
|
|
4008
|
+
spec.extra["abortSignal"] = input.signal
|
|
4009
|
+
? AbortSignal.any([input.signal, plannerAbort.signal])
|
|
4010
|
+
: plannerAbort.signal;
|
|
4011
|
+
const planInteraction = this.interactionChannelFor(input, log, runId, taskId, attemptId, adapter.id, routed.supportsInteractive);
|
|
4012
|
+
if (planInteraction)
|
|
4013
|
+
spec.extra["interactionChannel"] = planInteraction;
|
|
4014
|
+
const planWorkMode = this.applyWorkEnvelope(spec, this.workReportEnvelopeFor(routed, contract, Boolean(planInteraction)));
|
|
4015
|
+
return {
|
|
4016
|
+
knobs,
|
|
4017
|
+
effectiveWeb,
|
|
4018
|
+
spec,
|
|
4019
|
+
plannerAbort,
|
|
4020
|
+
planInteraction,
|
|
4021
|
+
planWorkMode,
|
|
4022
|
+
};
|
|
4023
|
+
},
|
|
4225
4024
|
};
|
|
4226
4025
|
}
|
|
4026
|
+
/** One read-only planner spawn shared by solo fallback, Council drafts, and merge. */
|
|
4027
|
+
async runPlannerAttempt(args) {
|
|
4028
|
+
return executePlannerAttempt(this.plannerAttemptDeps(), args);
|
|
4029
|
+
}
|
|
4227
4030
|
async runPlan(input, announce) {
|
|
4228
4031
|
const taskId = input.taskId ?? newId("task");
|
|
4229
4032
|
const runId = input.runId ?? newId("run");
|
|
4230
4033
|
// Plan runs get the same immutable contract truth as every other mode;
|
|
4231
4034
|
// contract validation runs BEFORE the run is announced (see runRace).
|
|
4232
4035
|
const contract = this.buildContract(input, taskId, "plan");
|
|
4233
|
-
const
|
|
4234
|
-
const paths =
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4036
|
+
const quotaSnapshots = this.quotaSnapshotPreflight();
|
|
4037
|
+
const { store, paths, log, ledger } = beginAnnouncedRun({
|
|
4038
|
+
input,
|
|
4039
|
+
contract,
|
|
4040
|
+
quotaSnapshots,
|
|
4041
|
+
store: this.artifactStore(input),
|
|
4042
|
+
authority: this.deps.delegationBudgetAuthority,
|
|
4043
|
+
runId,
|
|
4044
|
+
taskId,
|
|
4045
|
+
mode: "plan",
|
|
4046
|
+
phase: "plan",
|
|
4047
|
+
prompt: input.prompt,
|
|
4048
|
+
}, announce);
|
|
4240
4049
|
store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
|
|
4241
4050
|
log.emit("task.contract.created", { task_contract_hash: hashJson(contract) });
|
|
4242
4051
|
// W3.3: ONE resolved read-only context — the routing point-probe and every
|
|
@@ -4344,6 +4153,7 @@ export class Orchestrator {
|
|
|
4344
4153
|
roHome,
|
|
4345
4154
|
contextSection,
|
|
4346
4155
|
laneRun,
|
|
4156
|
+
estimateUsdFloor: this.estimateUsdFloor(input.repoRoot),
|
|
4347
4157
|
});
|
|
4348
4158
|
}
|
|
4349
4159
|
const plans = [];
|
|
@@ -4373,6 +4183,7 @@ export class Orchestrator {
|
|
|
4373
4183
|
fallbackHome: roHome.env,
|
|
4374
4184
|
promptBody: planPrompt(input.prompt) + contextSection,
|
|
4375
4185
|
intent: "plan",
|
|
4186
|
+
reservationEstimateUsd: this.reservationEstimateUsd(input),
|
|
4376
4187
|
});
|
|
4377
4188
|
if (outcome.budgetDenied) {
|
|
4378
4189
|
// QA-050: retain the denied planner slot before breaking so the
|
|
@@ -4451,7 +4262,6 @@ export class Orchestrator {
|
|
|
4451
4262
|
}
|
|
4452
4263
|
if (plans.length === 0) {
|
|
4453
4264
|
return writePlanHarnessFailure(this.planRunDeps(), {
|
|
4454
|
-
input,
|
|
4455
4265
|
contract,
|
|
4456
4266
|
taskId,
|
|
4457
4267
|
runId,
|
|
@@ -4520,17 +4330,8 @@ export class Orchestrator {
|
|
|
4520
4330
|
defaultPrompt: "Explore this repository and synthesize evidence-cited findings, omissions, and follow-up questions.",
|
|
4521
4331
|
}, announce);
|
|
4522
4332
|
}
|
|
4523
|
-
|
|
4524
|
-
return
|
|
4525
|
-
}
|
|
4526
|
-
rootLedger(input, contract, log) {
|
|
4527
|
-
return createRootLedger({
|
|
4528
|
-
input,
|
|
4529
|
-
contract,
|
|
4530
|
-
log,
|
|
4531
|
-
authority: this.deps.delegationBudgetAuthority,
|
|
4532
|
-
quotaSnapshots: this.deps.quotaSnapshots?.() ?? [],
|
|
4533
|
-
});
|
|
4333
|
+
quotaSnapshotPreflight() {
|
|
4334
|
+
return [...(this.deps.quotaSnapshots?.() ?? [])].map((snapshot) => QuotaSnapshotSchema.parse(snapshot));
|
|
4534
4335
|
}
|
|
4535
4336
|
routeBillingKnowledge(input, harnessId) {
|
|
4536
4337
|
// A selected profile's credential_kind decides billing (round-18 #2).
|
|
@@ -4560,10 +4361,10 @@ export class Orchestrator {
|
|
|
4560
4361
|
// scout reserve) so a subscription route is not refused for lacking a
|
|
4561
4362
|
// cash quote.
|
|
4562
4363
|
attemptCostEvidence(harnessId, attemptId, this.estimateUsdFloor(input.repoRoot), this.routeBillingKnowledge(input, harnessId)),
|
|
4563
|
-
buildSpec: (routed, homeEnv, prompt, attemptId) => {
|
|
4364
|
+
buildSpec: async (routed, homeEnv, prompt, attemptId) => {
|
|
4564
4365
|
const knobs = this.routeSpecKnobs(routed, contract, undefined, input.effort);
|
|
4565
4366
|
const effectiveWeb = this.discloseWebUpgrade(log, routed, knobs.webPolicy, attemptId);
|
|
4566
|
-
const sessionFields = this.sessionSpecFields(input, routed.adapter.id, log);
|
|
4367
|
+
const sessionFields = await this.sessionSpecFields(input, routed.adapter.id, log, routed.authRouteEstimate);
|
|
4567
4368
|
const spec = HarnessRunSpec.parse({
|
|
4568
4369
|
session_id: newId("ses"),
|
|
4569
4370
|
intent: "synthesize",
|
|
@@ -4604,13 +4405,19 @@ export class Orchestrator {
|
|
|
4604
4405
|
// Contract validation BEFORE the run is announced (see runRace). The
|
|
4605
4406
|
// recorded user intent is the CALLER's goal.
|
|
4606
4407
|
const contract = this.buildContract({ ...input, prompt: opts.contractIntent ?? prompt }, taskId, opts.mode);
|
|
4607
|
-
const
|
|
4608
|
-
const paths =
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4408
|
+
const quotaSnapshots = this.quotaSnapshotPreflight();
|
|
4409
|
+
const { store, paths, log, ledger } = beginAnnouncedRun({
|
|
4410
|
+
input,
|
|
4411
|
+
contract,
|
|
4412
|
+
quotaSnapshots,
|
|
4413
|
+
store: this.artifactStore(input),
|
|
4414
|
+
authority: this.deps.delegationBudgetAuthority,
|
|
4415
|
+
runId,
|
|
4416
|
+
taskId,
|
|
4417
|
+
mode: opts.mode,
|
|
4418
|
+
phase: "report",
|
|
4419
|
+
prompt,
|
|
4420
|
+
}, announce);
|
|
4614
4421
|
store.writeYaml(join(paths.contextDir, "task.yaml"), contract);
|
|
4615
4422
|
log.emit("task.contract.created", { task_contract_hash: hashJson(contract) });
|
|
4616
4423
|
// The ask/deep-scan report stays bare (its scouts read the tree themselves);
|
|
@@ -4752,10 +4559,10 @@ export class Orchestrator {
|
|
|
4752
4559
|
intent: opts.intent,
|
|
4753
4560
|
harnessId: adapter.id,
|
|
4754
4561
|
// QA-019: an n>1 deep-scan scout admits under a FINITE estimate floor
|
|
4755
|
-
// (mirror of the candidate loop): the first scout reserves
|
|
4756
|
-
// floor
|
|
4757
|
-
//
|
|
4758
|
-
cost: attemptCostEvidence(adapter.id, attemptId, opts.deepScan && idx > 0
|
|
4562
|
+
// (mirror of the candidate loop): the first top-level scout reserves
|
|
4563
|
+
// without a floor; later scouts and every real Delegate child pass the
|
|
4564
|
+
// repo floor because they overlap an existing family unit.
|
|
4565
|
+
cost: attemptCostEvidence(adapter.id, attemptId, this.reservationEstimateUsd(input, opts.deepScan && idx > 0), this.routeBillingKnowledge(input, adapter.id)),
|
|
4759
4566
|
});
|
|
4760
4567
|
if (!lease.granted) {
|
|
4761
4568
|
log.emit("budget.lease.created", {
|
|
@@ -4784,67 +4591,120 @@ export class Orchestrator {
|
|
|
4784
4591
|
}
|
|
4785
4592
|
return { status: "budget_denied", reason: lease.reason ?? "budget lease denied" };
|
|
4786
4593
|
}
|
|
4787
|
-
//
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
const
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4594
|
+
// As with planners, the granted lease owns profile/continuity/spec
|
|
4595
|
+
// preparation. Contain a pre-stream rejection as this attempt's failure;
|
|
4596
|
+
// parallel siblings can then finish before the shared HOME is disposed.
|
|
4597
|
+
const preparation = await (async () => {
|
|
4598
|
+
// Lease granted: the attempt is now committed to run — disclose the launch.
|
|
4599
|
+
onLaunch?.();
|
|
4600
|
+
const knobs = this.routeSpecKnobs(routed, contract, modelOverride, input.effort);
|
|
4601
|
+
const effectiveWeb = this.discloseWebUpgrade(log, routed, knobs.webPolicy, attemptId);
|
|
4602
|
+
const explorerPrompt = (opts.deepScan
|
|
4603
|
+
? `${prompt}\n\nExplorer ${idx + 1}/${adapters.length}: focus on a distinct slice. Emit evidence-cited findings, explicit unknowns/omissions, and follow-up questions. Do not edit files.`
|
|
4604
|
+
: prompt) + contextSection;
|
|
4605
|
+
const sessionFields = await this.sessionSpecFields(input, adapter.id, log, routed.authRouteEstimate);
|
|
4606
|
+
const grantResume = sessionFields.resume_session_id !== null && !resumeGranted.has(adapter.id);
|
|
4607
|
+
if (grantResume)
|
|
4608
|
+
resumeGranted.add(adapter.id);
|
|
4609
|
+
// Continuity (INV-137): a thread ASK turn is a chat turn — hydrate a lane
|
|
4610
|
+
// switch/gap with a packet and disclose it. Gated on laneRun (deep-scan
|
|
4611
|
+
// scouts are excluded from laneRun); native resume is available only when
|
|
4612
|
+
// this slot was granted the lane's recorded session.
|
|
4613
|
+
const laneContinuity = laneRun
|
|
4614
|
+
? await this.resolveContinuity(input, adapter.id, sessionFields.credential_profile?.profile_id ?? input.credentialProfileId ?? null, grantResume, sessionFields, store, paths, this.execRootOf(input), log)
|
|
4615
|
+
: null;
|
|
4616
|
+
// D-16d: the continuation packet pointer rides after the lane pointer so
|
|
4617
|
+
// the fresh session is re-grounded in the exhausted attempt's work.
|
|
4618
|
+
const promptWithPointers = [
|
|
4619
|
+
explorerPrompt,
|
|
4620
|
+
laneContinuity?.pointerLine,
|
|
4621
|
+
continuationPointer,
|
|
4622
|
+
]
|
|
4623
|
+
.filter((p) => Boolean(p))
|
|
4624
|
+
.join("\n\n");
|
|
4625
|
+
const spec = HarnessRunSpec.parse({
|
|
4626
|
+
session_id: newId("ses"),
|
|
4627
|
+
intent: opts.intent,
|
|
4628
|
+
prompt: promptWithPointers,
|
|
4629
|
+
cwd: this.execRootOf(input),
|
|
4630
|
+
access: "readonly",
|
|
4631
|
+
// ASK/EXPLORE/AUDIT read-only runs must forward the user's attachments —
|
|
4632
|
+
// a live "describe this image" turn sent an image that was being dropped here, so
|
|
4633
|
+
// the model honestly reported it saw nothing (the v0.13 attachment bug).
|
|
4634
|
+
attachments: input.attachments ?? [],
|
|
4635
|
+
auth_preference: sessionFields.auth_preference,
|
|
4636
|
+
credential_profile: sessionFields.credential_profile,
|
|
4637
|
+
resume_session_id: grantResume ? sessionFields.resume_session_id : null,
|
|
4638
|
+
...this.harnessSpecKnobs(contract, knobs, opts.intent),
|
|
4639
|
+
env_inheritance: envInheritance(this.config(input.repoRoot)),
|
|
4640
|
+
// A thread lane turn spawns in its DURABLE per-lane home so the native
|
|
4641
|
+
// session it records is reachable for resume next turn; everything else
|
|
4642
|
+
// uses the disposable route-context home.
|
|
4643
|
+
env: (laneRun ? this.laneHomeEnvFor(input, adapter.id) : null) ?? roHome.env,
|
|
4644
|
+
});
|
|
4645
|
+
const reportAbort = new AbortController();
|
|
4646
|
+
spec.extra["abortSignal"] = input.signal
|
|
4647
|
+
? AbortSignal.any([input.signal, reportAbort.signal])
|
|
4648
|
+
: reportAbort.signal;
|
|
4649
|
+
const reportInteraction = this.interactionChannelFor(input, log, runId, taskId, attemptId, adapter.id, routed.supportsInteractive);
|
|
4650
|
+
if (reportInteraction)
|
|
4651
|
+
spec.extra["interactionChannel"] = reportInteraction;
|
|
4652
|
+
// D-16: compile the WorkReport envelope for the read-only lane.
|
|
4653
|
+
const readonlyWorkEnvelope = this.workReportEnvelopeFor(routed, contract, Boolean(reportInteraction));
|
|
4654
|
+
const readonlyWorkMode = this.applyWorkEnvelope(spec, readonlyWorkEnvelope);
|
|
4655
|
+
const attemptEventsPath = join(paths.attemptsDir, attemptId, "events.jsonl");
|
|
4656
|
+
const answer = new AnswerAssembly();
|
|
4657
|
+
const telemetry = createAttemptTelemetry(knobs.webPolicy, contract.external_context.web_required ||
|
|
4658
|
+
knobs.webPolicy === "cached" ||
|
|
4659
|
+
knobs.webPolicy === "live", effectiveWeb, [],
|
|
4660
|
+
// Requested-model capture so ask/audit route receipts detect a silent
|
|
4661
|
+
// model downgrade (typed model_mismatch), not just agent runs.
|
|
4662
|
+
knobs.model);
|
|
4663
|
+
return {
|
|
4664
|
+
knobs,
|
|
4665
|
+
spec,
|
|
4666
|
+
reportAbort,
|
|
4667
|
+
reportInteraction,
|
|
4668
|
+
readonlyWorkMode,
|
|
4669
|
+
attemptEventsPath,
|
|
4670
|
+
answer,
|
|
4671
|
+
telemetry,
|
|
4672
|
+
};
|
|
4673
|
+
})().then((value) => ({ ok: true, value }), (error) => ({ ok: false, error }));
|
|
4674
|
+
if (!preparation.ok) {
|
|
4675
|
+
const message = `read-only attempt setup failed: ${safeErrorMessage(preparation.error)}`;
|
|
4676
|
+
AC.settleGrantedAttemptLease({
|
|
4677
|
+
ledger,
|
|
4678
|
+
leaseId: lease.lease?.lease_id ?? "",
|
|
4679
|
+
attemptId,
|
|
4680
|
+
harnessId: adapter.id,
|
|
4681
|
+
costUsd: 0,
|
|
4682
|
+
costEstimated: false,
|
|
4683
|
+
preStreamFailureSource: "readonly-pre-stream",
|
|
4684
|
+
});
|
|
4685
|
+
const telemetry = createAttemptTelemetry(contract.external_context.policy, contract.external_context.web_required, contract.external_context.effective_mode);
|
|
4686
|
+
setAttemptOutcome(telemetry, {
|
|
4687
|
+
deliverablePresent: false,
|
|
4688
|
+
gatesPassed: null,
|
|
4689
|
+
harnessErrored: true,
|
|
4690
|
+
webRequiredUnsatisfied: false,
|
|
4691
|
+
});
|
|
4692
|
+
attempts.push({
|
|
4693
|
+
attemptId,
|
|
4694
|
+
harnessId: adapter.id,
|
|
4695
|
+
status: "failed",
|
|
4696
|
+
report: "",
|
|
4697
|
+
error: message,
|
|
4698
|
+
telemetry,
|
|
4699
|
+
});
|
|
4700
|
+
attemptTelemetries.push({ attemptId, harnessId: adapter.id, telemetry });
|
|
4701
|
+
if (opts.deepScan) {
|
|
4702
|
+
store.writeText(join(paths.findingsDir, `${attemptId}-error.md`), `# Explorer ${attemptId} failed\n\n${message}\n`);
|
|
4703
|
+
}
|
|
4704
|
+
return { status: "launched" };
|
|
4705
|
+
}
|
|
4706
|
+
const { knobs, spec: preparedSpec, reportAbort, reportInteraction, readonlyWorkMode, attemptEventsPath, answer, telemetry, } = preparation.value;
|
|
4707
|
+
let spec = preparedSpec;
|
|
4848
4708
|
const retryPolicy = transientRetryPolicy(this.config(input.repoRoot));
|
|
4849
4709
|
let activeSessionId = spec.session_id;
|
|
4850
4710
|
const onAbort = () => {
|
|
@@ -4884,11 +4744,13 @@ export class Orchestrator {
|
|
|
4884
4744
|
try {
|
|
4885
4745
|
const watchedReport = withInactivityWatchdog(adapter.run(runSpec), {
|
|
4886
4746
|
timeoutMs: harnessInactivityTimeoutMs(this.config(input.repoRoot)),
|
|
4747
|
+
countsAsProgress: countsAsAgentProgress,
|
|
4887
4748
|
onTimeout: () => {
|
|
4888
4749
|
reportAbort.abort();
|
|
4889
4750
|
void adapter.cancel?.(activeSessionId)?.catch(() => { });
|
|
4890
4751
|
},
|
|
4891
4752
|
isSuspended: () => (reportInteraction?.pendingCount?.() ?? 0) > 0,
|
|
4753
|
+
suspensionVersion: () => reportInteraction?.suspensionVersion?.() ?? 0,
|
|
4892
4754
|
});
|
|
4893
4755
|
for await (const ev of watchedReport) {
|
|
4894
4756
|
if (input.signal?.aborted)
|
|
@@ -4952,13 +4814,18 @@ export class Orchestrator {
|
|
|
4952
4814
|
// W5.4 reactive failover, READ-ONLY lane (same contract as the
|
|
4953
4815
|
// candidate lane; typed limits only, never plain transients).
|
|
4954
4816
|
if (harnessError && !input.signal?.aborted) {
|
|
4817
|
+
const rotationPolicy = this.profilePolicy(input.repoRoot, adapter.id);
|
|
4818
|
+
const readyProfileIds = sawTypedLimit && reportSoFar.length === 0 && rotationPolicy.limit_action === "rotate"
|
|
4819
|
+
? await this.readyProfileIdsForRotation(input, adapter.id, spec.credential_profile ?? null, triedProfiles)
|
|
4820
|
+
: new Set();
|
|
4955
4821
|
const rotated = rotateSpecOnTypedLimit({
|
|
4956
4822
|
spec,
|
|
4957
4823
|
harnessId: adapter.id,
|
|
4958
4824
|
attemptId,
|
|
4959
|
-
policy:
|
|
4825
|
+
policy: rotationPolicy,
|
|
4960
4826
|
registry: this.config(input.repoRoot)?.global.credential_profiles ?? [],
|
|
4961
4827
|
snapshots: this.deps.quotaSnapshots?.() ?? [],
|
|
4828
|
+
readyProfileIds,
|
|
4962
4829
|
triedProfiles,
|
|
4963
4830
|
sawTypedLimit,
|
|
4964
4831
|
deliverableEmpty: reportSoFar.length === 0,
|
|
@@ -5000,7 +4867,17 @@ export class Orchestrator {
|
|
|
5000
4867
|
}
|
|
5001
4868
|
finally {
|
|
5002
4869
|
input.signal?.removeEventListener("abort", onAbort);
|
|
5003
|
-
|
|
4870
|
+
AC.settleGrantedAttemptLease({
|
|
4871
|
+
ledger,
|
|
4872
|
+
leaseId: lease.lease?.lease_id ?? "",
|
|
4873
|
+
attemptId,
|
|
4874
|
+
harnessId: adapter.id,
|
|
4875
|
+
costUsd: cost,
|
|
4876
|
+
costEstimated,
|
|
4877
|
+
authMode: telemetry.authMode,
|
|
4878
|
+
usageCost: telemetry.usageCost,
|
|
4879
|
+
preStreamFailureSource: "readonly-pre-stream",
|
|
4880
|
+
});
|
|
5004
4881
|
}
|
|
5005
4882
|
if (harnessError && telemetry.transientFailures.length > 0) {
|
|
5006
4883
|
log.emit("route.transient.exhausted", {
|
|
@@ -5011,11 +4888,14 @@ export class Orchestrator {
|
|
|
5011
4888
|
});
|
|
5012
4889
|
}
|
|
5013
4890
|
attemptTelemetries.push({ attemptId, harnessId: adapter.id, telemetry });
|
|
5014
|
-
// D-16:
|
|
4891
|
+
// D-16: remove the WorkReport transport; the deliverable is the report.
|
|
5015
4892
|
const roUnwrapped = unwrapWorkReportEnvelope(answer.machineText() ?? "", readonlyWorkMode, {
|
|
5016
4893
|
sideToolReport: telemetry.sideToolWorkReport ?? undefined,
|
|
5017
4894
|
});
|
|
5018
|
-
|
|
4895
|
+
// Trim symmetrically with the plan path: a whitespace-only answer is not
|
|
4896
|
+
// a delivered report (the final-artifact wrapper heading would otherwise
|
|
4897
|
+
// make it read as present content by construction).
|
|
4898
|
+
const report = redactSecrets(roUnwrapped.deliverable).trim();
|
|
5019
4899
|
const unrecovered = unrecoveredToolErrors(telemetry);
|
|
5020
4900
|
const webBlocked = webUnsatisfied(telemetry);
|
|
5021
4901
|
const reportPresent = report.length > 0;
|
|
@@ -5043,6 +4923,13 @@ export class Orchestrator {
|
|
|
5043
4923
|
harnessErrored: harnessError !== null && !webBlocked,
|
|
5044
4924
|
webRequiredUnsatisfied: webBlocked,
|
|
5045
4925
|
workState: roFinalized.workState,
|
|
4926
|
+
// A read-only attempt that completed CLEANLY with an honestly empty
|
|
4927
|
+
// answer is a success with deliverable_present=false ("(no output)"),
|
|
4928
|
+
// never a fake contract failure — the trim above must not convert a
|
|
4929
|
+
// phantom deliverable into a phantom harness failure. Only the clean
|
|
4930
|
+
// finalizer class qualifies: contract failures, vetoes, and context
|
|
4931
|
+
// interruptions keep the strict deliverable requirement.
|
|
4932
|
+
emptyDeliverableAllowed: roFinalized.outcomeClass === "clean",
|
|
5046
4933
|
});
|
|
5047
4934
|
if (harnessError) {
|
|
5048
4935
|
log.emit("harness.completed", {
|
|
@@ -5473,7 +5360,7 @@ export class Orchestrator {
|
|
|
5473
5360
|
}
|
|
5474
5361
|
this.writeRunTelemetry(store, paths, contract, runId, taskId, opts.mode, attemptTelemetries, opts.deepScan ? null : (succeeded[0]?.attemptId ?? null), deepScanSynthesis);
|
|
5475
5362
|
log.emit("output.ready", {
|
|
5476
|
-
kind: opts.
|
|
5363
|
+
kind: opts.deepScan ? "report" : "answer",
|
|
5477
5364
|
path: `final/${opts.artifactName}`,
|
|
5478
5365
|
});
|
|
5479
5366
|
if (opts.deepScan) {
|
|
@@ -5531,11 +5418,16 @@ export class Orchestrator {
|
|
|
5531
5418
|
.map((a) => `${a.attemptId}:${a.harnessId}:${a.status}`)
|
|
5532
5419
|
.join(", ");
|
|
5533
5420
|
store.writeText(join(paths.finalDir, "summary.md"), `# Run ${runId} (${opts.mode})\n\n- Harnesses: ${harnessLabel}\n- Lifecycle: ${terminalFacts.lifecycle}${terminalFacts.reason ? ` (${terminalFacts.reason})` : ""}\n\n${report}\n`);
|
|
5421
|
+
const reportProducerAttemptId = opts.deepScan &&
|
|
5422
|
+
deepScanSynthesis?.status === "succeeded" &&
|
|
5423
|
+
deepScanSynthesis.reducer_attempt_id
|
|
5424
|
+
? deepScanSynthesis.reducer_attempt_id
|
|
5425
|
+
: (succeeded[0]?.attemptId ?? "a01");
|
|
5534
5426
|
store.writeYaml(join(paths.finalDir, "work_product.yaml"), {
|
|
5535
5427
|
id: newId("wp"),
|
|
5536
5428
|
kind: "report",
|
|
5537
5429
|
source_task_id: taskId,
|
|
5538
|
-
producer_attempt_id:
|
|
5430
|
+
producer_attempt_id: reportProducerAttemptId,
|
|
5539
5431
|
files: Object.fromEntries([[opts.artifactName, join(paths.finalDir, opts.artifactName)]]),
|
|
5540
5432
|
meta: {
|
|
5541
5433
|
harnesses: attempts.map((a) => a.harnessId),
|
|
@@ -5544,7 +5436,7 @@ export class Orchestrator {
|
|
|
5544
5436
|
read_only: true,
|
|
5545
5437
|
},
|
|
5546
5438
|
});
|
|
5547
|
-
log.emit("work_product.emitted", { kind: "report", winner:
|
|
5439
|
+
log.emit("work_product.emitted", { kind: "report", winner: reportProducerAttemptId });
|
|
5548
5440
|
const workVetoed = terminalFacts.work_state?.state === "needs_input" ||
|
|
5549
5441
|
terminalFacts.work_state?.state === "incomplete";
|
|
5550
5442
|
if (terminalFacts.lifecycle !== "succeeded") {
|
|
@@ -5596,9 +5488,4 @@ export class Orchestrator {
|
|
|
5596
5488
|
};
|
|
5597
5489
|
}
|
|
5598
5490
|
}
|
|
5599
|
-
function assertNoSecretLikeTokens(label, text) {
|
|
5600
|
-
if (containsSecretLikeToken(text)) {
|
|
5601
|
-
throw new Error(`${label} contains secret-like token; refusing to persist artifact`);
|
|
5602
|
-
}
|
|
5603
|
-
}
|
|
5604
5491
|
//# sourceMappingURL=orchestrator.js.map
|