@claudexor/orchestrator 4.1.0 → 4.1.1

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.
@@ -1,9 +1,9 @@
1
1
  import { applyWorkEnvelope, promptWithPointers as renderPromptWithPointers, threadContextPointer, } from "./prompt-framing.js";
2
2
  import { requestRefusalFailure } from "./runTerminalResults.js";
3
+ import { openRunScopeCustody, scopedRouteContext, scopedSpecEnv, } from "./runScope.js";
3
4
  import { delegationBeltFor } from "./delegationBelt.js";
4
5
  import { bindProcessingAdmission, processingAttemptCostEvidence, processingAdmissionForLease, updateProcessingStreamHold, ProcessingBudgetAdmissionError, } from "./processing-dispatch.js";
5
6
  import { cancelledCandidatesResult, emptyCandidateResult, failedCandidatesResult, failAfterCandidates, } from "./candidateTerminals.js";
6
- import { existsSync } from "node:fs";
7
7
  import { effectiveAuthPreference, observeNativeSessionEvent, resumeSessionForProfile, } from "./credential-profiles.js";
8
8
  import { AdmissionProfileProbes, OrchestratorCredentials, reviewerProfileResolver, } from "./orchestrator-credentials.js";
9
9
  import { continuityWorkspace, resumableOf, resumableTerminal, } from "./continuity-terminal.js";
@@ -62,12 +62,13 @@ import { resolveWorkReportEnvelope, unwrapWorkReportEnvelope, } from "./workRepo
62
62
  import { buildContinuationPacket, decideContinuation, synthesizeContinuationRequest, } from "./continuation.js";
63
63
  import { interactionChannelFor } from "./interaction.js";
64
64
  import { liveAttempt } from "./live-input.js";
65
- import { gateSpecsFromContract, renderTestsEvidence } from "./contract-gates.js";
65
+ import { finalVerifyRun, gateSpecsFromContract, renderTestsEvidence } from "./contract-gates.js";
66
66
  import { buildTaskContract } from "./task-contract-builder.js";
67
+ import { prepareReviewEvidenceDir, recordReviewEvidenceCleanup } from "./reviewEvidence.js";
67
68
  import { ArtifactStore } from "@claudexor/artifact-store";
68
- import { buildContextPack, rawContextForEnvelope, preflightEvidence, writeEvidencePacket, } from "@claudexor/context";
69
+ import { buildContextPack, rawContextForEnvelope, writeEvidencePacket } from "@claudexor/context";
69
70
  import { WorkspaceManager, captureRawPatchEnvelope, createRevertAnchorFromPatchOrNull, createRevertAnchorOrNull, consumeRawPatchEnvelope, snapshotTree, } from "@claudexor/workspace";
70
- import { blockedDecisionOverride, finalVerifyBlocks, finalVerifyPatch, finalVerifyFiles, verifyAndDeliverFiles, verifyAndDeliver, } from "@claudexor/delivery";
71
+ import { blockedDecisionOverride, finalVerifyBlocks, verifyAndDeliverFiles, verifyAndDeliver, } from "@claudexor/delivery";
71
72
  import { HarnessGateway } from "@claudexor/gateway";
72
73
  import { ReadinessLedger, evaluateConvergence, failureSignature, gatesPassed, reviewCandidate, runGates, } from "@claudexor/review";
73
74
  import { arbitrate } from "@claudexor/arbitration";
@@ -162,39 +163,54 @@ export class Orchestrator {
162
163
  this.routingRationaleByRun.delete(settledRunId);
163
164
  this.deps.delegationBudgetAuthority?.releaseRun(settledRunId);
164
165
  };
166
+ // Process custody (owner 1A, #449; `runScope.ts`): the marker is minted and
167
+ // recorded BEFORE the first launch, rides every spec env (`scopedSpecEnv`),
168
+ // and every announced run HOLDS its terminal until the barrier has ended
169
+ // what still carries it. The reap never changes the verdict.
170
+ const custody = await openRunScopeCustody(this.deps.runScopes, runId);
171
+ resolved = { ...resolved, runScopeMarker: custody.marker };
165
172
  try {
166
173
  return await guardAnnouncedRun(resolved.signal, (announce) => {
174
+ const announced = custody.announced(announce);
167
175
  switch (mode) {
168
176
  case "ask":
169
177
  // `--deep-scan` widens the answer into the bounded multi-scout
170
178
  // research sweep with synthesis (the old `audit --swarm`/`explore`).
171
179
  return resolved.deepScan
172
- ? this.runDeepScan(resolved, announce)
173
- : this.runAsk(resolved, announce);
180
+ ? this.runDeepScan(resolved, announced)
181
+ : this.runAsk(resolved, announced);
174
182
  case "agent":
175
183
  // Engine strategies are FLAGS on agent (v0.9 collapse): `--until-clean`
176
184
  // and `--attempts` select the convergence loop; `--n` selects the race
177
185
  // width; `--create` switches the candidate intent to create_from_scratch.
178
186
  if (resolved.untilClean)
179
- return this.runConvergence(resolved, mode, null, announce);
187
+ return this.runConvergence(resolved, mode, null, announced);
180
188
  if (resolved.attempts !== undefined && resolved.attempts !== null) {
181
- return this.runConvergence(resolved, mode, resolved.attempts, announce);
189
+ return this.runConvergence(resolved, mode, resolved.attempts, announced);
182
190
  }
183
- return this.runRace({ ...resolved, n: resolved.n ?? 1 }, mode, announce);
191
+ return this.runRace({ ...resolved, n: resolved.n ?? 1 }, mode, announced);
184
192
  case "plan":
185
- return this.runPlan(resolved, announce);
193
+ return this.runPlan(resolved, announced);
186
194
  }
187
- }, async ({ runId }) => {
195
+ },
196
+ // The whole-run barrier: drain delegated children, then end the run's
197
+ // process scope and record its outcome before ANY terminal is flushed
198
+ // (a child-drain rejection keeps its error). Bounded by the reap deadline.
199
+ ({ runId }) => custody.barrier(async () => {
188
200
  const authority = this.deps.delegationBudgetAuthority;
189
201
  if (!authority?.hasParent(runId))
190
202
  return;
191
203
  authority.beginParentClose(runId);
192
204
  await authority.waitForChildren(runId);
193
- },
194
- // Single per-run terminalization hook: release the routing-rationale map
195
- // entry on EVERY terminal (incl. a run that died before its telemetry
196
- // writer ran, which is the leak this closes).
197
- releaseRunState);
205
+ }),
206
+ // Single per-run terminalization hook: the custody fallback for a path
207
+ // that never reached the barrier, then release per-run engine state
208
+ // (the delegated-child drain authority is released only AFTER
209
+ // settlement) on EVERY terminal.
210
+ async (settledRunId) => {
211
+ await custody.record();
212
+ releaseRunState(settledRunId);
213
+ });
198
214
  }
199
215
  catch (error) {
200
216
  // A durable startup sink may refuse `run.created` before the strategy can
@@ -206,6 +222,9 @@ export class Orchestrator {
206
222
  }
207
223
  throw error;
208
224
  }
225
+ finally {
226
+ await custody.settleUnannounced();
227
+ }
209
228
  }
210
229
  async resolveReviewers(cwd, runAuthPreference, onIgnoredSetting) {
211
230
  if (this.deps.reviewers)
@@ -1217,7 +1236,7 @@ export class Orchestrator {
1217
1236
  adapter: this.deps.registry.get(harnessId),
1218
1237
  credentialProfile: sessionFields.credential_profile,
1219
1238
  authPreference: sessionFields.auth_preference ?? "auto",
1220
- laneEnv: this.laneHomeEnvFor(runInput, harnessId, profileId) ?? {},
1239
+ laneEnv: scopedSpecEnv(runInput.runScopeMarker, this.laneHomeEnvFor(runInput, harnessId, profileId) ?? {}) ?? {},
1221
1240
  envInheritance: envInheritance(this.config(runInput.repoRoot)),
1222
1241
  signal: runInput.signal,
1223
1242
  processing,
@@ -1328,6 +1347,9 @@ export class Orchestrator {
1328
1347
  if (routed.browserRequirement.effective && artifactRelativeDir === null) {
1329
1348
  throw new Error("browser artifact ownership marker was not persisted");
1330
1349
  }
1350
+ // The run's scope marker rides every spec env (plus the host facts when the
1351
+ // HOME is scoped), added AFTER the lane/profile home so that patch is kept.
1352
+ const scopedEnv = scopedSpecEnv(runInput?.runScopeMarker, harnessHome.env);
1331
1353
  let spec = HarnessRunSpec.parse({
1332
1354
  session_id: newId("ses"),
1333
1355
  intent,
@@ -1359,7 +1381,7 @@ export class Orchestrator {
1359
1381
  ...(inPlaceEnvelope && sessionFields?.resume_session_id
1360
1382
  ? { resume_session_id: sessionFields.resume_session_id }
1361
1383
  : {}),
1362
- ...(harnessHome.env ? { env: harnessHome.env } : {}),
1384
+ ...(scopedEnv ? { env: scopedEnv } : {}),
1363
1385
  raw_context_packet: rawContextPacket,
1364
1386
  stream_deltas: streamDeltas,
1365
1387
  });
@@ -1650,7 +1672,7 @@ export class Orchestrator {
1650
1672
  }
1651
1673
  const gates = gateSignalAborted
1652
1674
  ? []
1653
- : await runGates(gateSpecsFromContract(contract), {
1675
+ : await runGates(gateSpecsFromContract(contract, runInput?.runScopeMarker), {
1654
1676
  cwd: envelope.worktree_path,
1655
1677
  env: wsm.envFor(envelope),
1656
1678
  signal,
@@ -2396,7 +2418,7 @@ export class Orchestrator {
2396
2418
  // candidates (a trivial greeting in agent mode no longer burns two reviewers on
2397
2419
  // "(empty diff)"). Candidates still flow through arbitration/gates so the
2398
2420
  // no_op/answer outcome and gate failures are unchanged.
2399
- evidences = await this.reviewRuns(workingRuns, reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal, this.reservationEstimateUsd(input));
2421
+ evidences = await this.reviewRuns(workingRuns, reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal, this.reservationEstimateUsd(input), false, input.runScopeMarker);
2400
2422
  }
2401
2423
  catch (err) {
2402
2424
  // Review preflight/evidence failures end TERMINALLY with artifacts —
@@ -2468,7 +2490,7 @@ export class Orchestrator {
2468
2490
  runs.push(run);
2469
2491
  try {
2470
2492
  if (isWorkingCandidate(run)) {
2471
- const synthEvidence = await this.reviewRuns([run], reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal, this.reservationEstimateUsd(input));
2493
+ const synthEvidence = await this.reviewRuns([run], reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal, this.reservationEstimateUsd(input), false, input.runScopeMarker);
2472
2494
  evidences.push(...synthEvidence);
2473
2495
  workingRuns.push(run);
2474
2496
  }
@@ -2570,9 +2592,7 @@ export class Orchestrator {
2570
2592
  facts.lifecycle === "succeeded" &&
2571
2593
  facts.review !== "blocked" &&
2572
2594
  !input.signal?.aborted) {
2573
- finalVerify = winnerRun.files
2574
- ? await finalVerifyFiles(winnerRun.files, undefined, gateSpecsFromContract(contract), log)
2575
- : await finalVerifyPatch(execRoot, winnerRun, gateSpecsFromContract(contract), log);
2595
+ finalVerify = await finalVerifyRun(execRoot, winnerRun, contract, log, input);
2576
2596
  finalVerifyFailed = finalVerifyBlocks(finalVerify);
2577
2597
  if (finalVerifyFailed)
2578
2598
  facts = { ...facts, checks: "failed", reason: "checks_failed" };
@@ -2617,7 +2637,7 @@ export class Orchestrator {
2617
2637
  facts.checks !== "failed" &&
2618
2638
  !workStateVetoes(facts);
2619
2639
  if (input.inPlace === true && !inPlaceWinner && directoryAdoptable) {
2620
- const delivered = await verifyAndDeliverFiles(execRoot, winnerRun.files, {}, gateSpecsFromContract(contract), (freshVerify) => finalVerifyBlocks(freshVerify)
2640
+ const delivered = await verifyAndDeliverFiles(execRoot, winnerRun.files, {}, gateSpecsFromContract(contract, input.runScopeMarker), (freshVerify) => finalVerifyBlocks(freshVerify)
2621
2641
  ? (freshVerify.reason ?? "final verify failed before directory race adoption")
2622
2642
  : null, log);
2623
2643
  store.writeYaml(join(paths.finalDir, "delivery_receipt.yaml"), delivered);
@@ -2680,7 +2700,7 @@ export class Orchestrator {
2680
2700
  }
2681
2701
  else if (adoptable) {
2682
2702
  // Protected apply preserves the live tree or reports tree_mutated.
2683
- const applied = await verifyAndDeliver(execRoot, winnerRun.diff, { mode: "apply", protectedApply: true }, gateSpecsFromContract(contract), (freshVerify) => {
2703
+ const applied = await verifyAndDeliver(execRoot, winnerRun.diff, { mode: "apply", protectedApply: true }, gateSpecsFromContract(contract, input.runScopeMarker), (freshVerify) => {
2684
2704
  finalVerify = freshVerify;
2685
2705
  return finalVerifyBlocks(freshVerify)
2686
2706
  ? (freshVerify.reason ?? "final verify failed before race adoption")
@@ -2954,7 +2974,7 @@ export class Orchestrator {
2954
2974
  transientRetryPolicy: input.transientRetryPolicy ?? transientRetryPolicy(this.config(input.cwd)),
2955
2975
  });
2956
2976
  }
2957
- async reviewRuns(runs, reviewers, reviewVerified, reviewDir, cwd, contract, store, paths, log, ledger, taskId, signal, reservationEstimateUsd, reviewUnchanged = false) {
2977
+ async reviewRuns(runs, reviewers, reviewVerified, reviewDir, cwd, contract, store, paths, log, ledger, taskId, signal, reservationEstimateUsd, reviewUnchanged = false, runScopeMarker) {
2958
2978
  return reviewCandidateRuns({
2959
2979
  runs,
2960
2980
  reviewers,
@@ -2970,50 +2990,17 @@ export class Orchestrator {
2970
2990
  signal,
2971
2991
  reservationEstimateUsd,
2972
2992
  reviewUnchanged,
2993
+ runScopeMarker,
2973
2994
  }, {
2974
2995
  prepareReviewEvidenceDir: this.prepareReviewEvidenceDir.bind(this),
2975
- recordReviewEvidenceCleanup: this.recordReviewEvidenceCleanup.bind(this),
2996
+ recordReviewEvidenceCleanup,
2976
2997
  reviewScoped: this.reviewScoped.bind(this),
2977
2998
  config: this.config.bind(this),
2978
2999
  });
2979
3000
  }
2980
- prepareReviewEvidenceDir(sourceDir, _candidateCwd) {
2981
- // Evidence is an external runtime artifact. ReviewEngine builds a separate
2982
- // reviewer workspace and copies the packet there; writing/copying it into
2983
- // the candidate tree would contaminate the Git diff and, worse, overwrite a
2984
- // user-owned path with the same name.
2985
- if (!existsSync(sourceDir)) {
2986
- throw new Error(`review evidence preflight failed for ${sourceDir}: source packet missing`);
2987
- }
2988
- return this.requireReviewEvidence(sourceDir);
2989
- }
2990
- requireReviewEvidence(dir) {
2991
- const result = preflightEvidence(dir);
2992
- if (result.ok)
2993
- return dir;
2994
- const missing = result.missing.length ? `missing=${result.missing.join(",")}` : "";
2995
- const empty = result.empty.length ? `empty=${result.empty.join(",")}` : "";
2996
- throw new Error(`review evidence preflight failed for ${dir}: ${[missing, empty].filter(Boolean).join(" ")}`);
2997
- }
2998
- cleanupReviewEvidenceDir(_candidateEvidenceDir, _candidateCwd) {
2999
- // No candidate-tree packet exists in v2; external runtime retention is
3000
- // governed by the artifact/journal lifecycle rather than best-effort rm.
3001
- return null;
3002
- }
3003
- recordReviewEvidenceCleanup(store, metadataPath, attemptId, candidateEvidenceDir, candidateCwd) {
3004
- const cleanupMetadata = this.cleanupReviewEvidenceDir(candidateEvidenceDir, candidateCwd);
3005
- if (!cleanupMetadata)
3006
- return;
3007
- try {
3008
- store.writeYaml(metadataPath, {
3009
- ...cleanupMetadata,
3010
- attempt_id: attemptId,
3011
- });
3012
- }
3013
- catch {
3014
- // Cleanup telemetry must not mask the review/revalidation failure that
3015
- // triggered best-effort cleanup.
3016
- }
3001
+ /** The instance method is the seam tests stub to make evidence setup fail. */
3002
+ prepareReviewEvidenceDir(sourceDir, candidateCwd) {
3003
+ return prepareReviewEvidenceDir(sourceDir, candidateCwd);
3017
3004
  }
3018
3005
  async runConvergence(input, mode, maxAttempts, announce) {
3019
3006
  const taskId = input.taskId ?? newId("task");
@@ -3398,7 +3385,7 @@ export class Orchestrator {
3398
3385
  log.emit("review.skipped", { reason: "not_requested", attempt_id: attemptId });
3399
3386
  return evaluateUnreviewedConvergence(evidence, contract);
3400
3387
  }
3401
- const [evidence] = await this.reviewRuns([run], reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal, this.reservationEstimateUsd(input), true);
3388
+ const [evidence] = await this.reviewRuns([run], reviewers, reviewVerified, reviewDir, input.repoRoot, contract, store, paths, log, ledger, taskId, input.signal, this.reservationEstimateUsd(input), true, input.runScopeMarker);
3402
3389
  if (!evidence)
3403
3390
  throw new Error("Convergence review produced no evidence");
3404
3391
  lastFindings = evidence.findings;
@@ -3587,9 +3574,7 @@ export class Orchestrator {
3587
3574
  facts.lifecycle === "succeeded" &&
3588
3575
  facts.review !== "blocked" &&
3589
3576
  !input.signal?.aborted) {
3590
- convFinalVerify = lastRun.files
3591
- ? await finalVerifyFiles(lastRun.files, undefined, gateSpecsFromContract(contract), log)
3592
- : await finalVerifyPatch(execRoot, lastRun, gateSpecsFromContract(contract), log);
3577
+ convFinalVerify = await finalVerifyRun(execRoot, lastRun, contract, log, input);
3593
3578
  if (finalVerifyBlocks(convFinalVerify))
3594
3579
  facts = { ...facts, checks: "failed", reason: "checks_failed" };
3595
3580
  }
@@ -3848,9 +3833,9 @@ export class Orchestrator {
3848
3833
  ...sessionFields,
3849
3834
  ...this.harnessSpecKnobs(contract, knobs, args.intent),
3850
3835
  env_inheritance: envInheritance(this.config(input.repoRoot)),
3851
- env: (args.laneRun
3836
+ env: scopedSpecEnv(input.runScopeMarker, (args.laneRun
3852
3837
  ? this.laneHomeEnvFor(input, adapter.id, sessionFields.credential_profile?.profile_id ?? input.credentialProfileId ?? null)
3853
- : null) ?? args.fallbackHome,
3838
+ : null) ?? args.fallbackHome),
3854
3839
  });
3855
3840
  this.credentials.stampProfileSelection(spec, input, adapter.id);
3856
3841
  const plannerAbort = new AbortController();
@@ -3903,9 +3888,9 @@ export class Orchestrator {
3903
3888
  // harnesses run sequentially. Doctor, model inventory, and spawn share each
3904
3889
  // lane's durable HOME; the disposable context remains the non-thread fallback.
3905
3890
  const laneRun = Boolean(input.threadId);
3906
- const roHome = resolveReadOnlyRouteContext(this.execRootOf(input), laneRun
3891
+ const roHome = scopedRouteContext(resolveReadOnlyRouteContext(this.execRootOf(input), laneRun
3907
3892
  ? (id) => this.laneHomeEnvFor(input, id, input.credentialProfileId ?? null)
3908
- : undefined);
3893
+ : undefined), input.runScopeMarker);
3909
3894
  let adapters;
3910
3895
  try {
3911
3896
  adapters = await this.resolveCandidateAdapters({ ...input, n: undefined }, "plan", ledger, log, roHome, runId);
@@ -4190,7 +4175,7 @@ export class Orchestrator {
4190
4175
  */
4191
4176
  deepScanReducerDeps(input, contract, log) {
4192
4177
  return {
4193
- newReadOnlyHome: () => resolveReadOnlyRouteContext(this.execRootOf(input)),
4178
+ newReadOnlyHome: () => scopedRouteContext(resolveReadOnlyRouteContext(this.execRootOf(input)), input.runScopeMarker),
4194
4179
  costEvidence: (harnessId, attemptId, routed) =>
4195
4180
  // The reducer admits under a finite estimate floor (mirror of the n>1
4196
4181
  // scout reserve) so a subscription route is not refused for lacking a
@@ -4213,7 +4198,7 @@ export class Orchestrator {
4213
4198
  resume_session_id: null,
4214
4199
  ...this.harnessSpecKnobs(contract, knobs, "synthesize"),
4215
4200
  env_inheritance: envInheritance(this.config(input.repoRoot)),
4216
- env: homeEnv,
4201
+ env: scopedSpecEnv(input.runScopeMarker, homeEnv),
4217
4202
  });
4218
4203
  this.credentials.stampProfileSelection(spec, input, routed.adapter.id);
4219
4204
  // D-16: compile the WorkReport transport onto the reducer spec (the
@@ -4265,9 +4250,9 @@ export class Orchestrator {
4265
4250
  // and the next lane turn resumes it (INV-034). Its doctor/model/spawn path
4266
4251
  // shares that lane HOME; deep-scan and one-shot asks keep the disposable one.
4267
4252
  const laneRun = Boolean(input.threadId) && opts.mode === "ask" && !opts.deepScan;
4268
- const roHome = resolveReadOnlyRouteContext(this.execRootOf(input), laneRun
4253
+ const roHome = scopedRouteContext(resolveReadOnlyRouteContext(this.execRootOf(input), laneRun
4269
4254
  ? (id) => this.laneHomeEnvFor(input, id, input.credentialProfileId ?? null)
4270
- : undefined);
4255
+ : undefined), input.runScopeMarker);
4271
4256
  let adapters, width;
4272
4257
  try {
4273
4258
  ({ adapters, width } = await resolveReadOnlyCandidates({
@@ -4445,9 +4430,9 @@ export class Orchestrator {
4445
4430
  // A thread lane turn spawns in its DURABLE per-lane home so the native
4446
4431
  // session it records is reachable for resume next turn; everything else
4447
4432
  // uses the disposable route-context home.
4448
- env: (laneRun
4433
+ env: scopedSpecEnv(input.runScopeMarker, (laneRun
4449
4434
  ? this.laneHomeEnvFor(input, adapter.id, sessionFields.credential_profile?.profile_id ?? input.credentialProfileId ?? null)
4450
- : null) ?? roHome.env,
4435
+ : null) ?? roHome.env),
4451
4436
  });
4452
4437
  this.credentials.stampProfileSelection(spec, input, adapter.id);
4453
4438
  const reportInteraction = interactionChannelFor(input, log, runId, taskId, attemptId, adapter.id, routed.supportsInteractive);