@claudexor/orchestrator 3.8.0 → 3.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/account-pool.d.ts +1 -0
  2. package/dist/account-pool.d.ts.map +1 -1
  3. package/dist/account-pool.js +2 -0
  4. package/dist/account-pool.js.map +1 -1
  5. package/dist/account-resolution.d.ts +2 -0
  6. package/dist/account-resolution.d.ts.map +1 -1
  7. package/dist/account-resolution.js +32 -12
  8. package/dist/account-resolution.js.map +1 -1
  9. package/dist/candidateEvidence.d.ts +3 -3
  10. package/dist/candidateEvidence.d.ts.map +1 -1
  11. package/dist/candidateOutputs.d.ts +2 -2
  12. package/dist/candidateOutputs.d.ts.map +1 -1
  13. package/dist/candidateOutputs.js +11 -6
  14. package/dist/candidateOutputs.js.map +1 -1
  15. package/dist/contract-gates.d.ts +4 -4
  16. package/dist/contract-gates.d.ts.map +1 -1
  17. package/dist/contract-gates.js +2 -0
  18. package/dist/contract-gates.js.map +1 -1
  19. package/dist/credential-profiles.d.ts +12 -1
  20. package/dist/credential-profiles.d.ts.map +1 -1
  21. package/dist/credential-profiles.js +13 -3
  22. package/dist/credential-profiles.js.map +1 -1
  23. package/dist/delegatedHome.d.ts +21 -69
  24. package/dist/delegatedHome.d.ts.map +1 -1
  25. package/dist/delegatedHome.js +29 -85
  26. package/dist/delegatedHome.js.map +1 -1
  27. package/dist/diffReview.d.ts +2 -1
  28. package/dist/diffReview.d.ts.map +1 -1
  29. package/dist/diffReview.js +14 -2
  30. package/dist/diffReview.js.map +1 -1
  31. package/dist/orchestrator-credentials.d.ts +13 -2
  32. package/dist/orchestrator-credentials.d.ts.map +1 -1
  33. package/dist/orchestrator-credentials.js +64 -3
  34. package/dist/orchestrator-credentials.js.map +1 -1
  35. package/dist/orchestrator.d.ts +5 -6
  36. package/dist/orchestrator.d.ts.map +1 -1
  37. package/dist/orchestrator.js +204 -211
  38. package/dist/orchestrator.js.map +1 -1
  39. package/dist/planRun.d.ts +5 -5
  40. package/dist/planRun.d.ts.map +1 -1
  41. package/dist/planRun.js.map +1 -1
  42. package/dist/plannerAttempt.d.ts +2 -2
  43. package/dist/plannerAttempt.d.ts.map +1 -1
  44. package/dist/requestRequirements.d.ts +3 -15
  45. package/dist/requestRequirements.d.ts.map +1 -1
  46. package/dist/requestRequirements.js +11 -21
  47. package/dist/requestRequirements.js.map +1 -1
  48. package/dist/reviewerPanel.d.ts +9 -1
  49. package/dist/reviewerPanel.d.ts.map +1 -1
  50. package/dist/reviewerPanel.js +276 -133
  51. package/dist/reviewerPanel.js.map +1 -1
  52. package/dist/runAdmission.d.ts +29 -0
  53. package/dist/runAdmission.d.ts.map +1 -0
  54. package/dist/runAdmission.js +63 -0
  55. package/dist/runAdmission.js.map +1 -0
  56. package/dist/task-contract-builder.d.ts +2 -2
  57. package/dist/task-contract-builder.d.ts.map +1 -1
  58. package/dist/task-contract-builder.js +5 -6
  59. package/dist/task-contract-builder.js.map +1 -1
  60. package/package.json +17 -17
@@ -1,11 +1,11 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { effectiveAuthPreference, observeNativeSessionEvent, resumeSessionForProfile, rotateSpecOnTypedLimit, selectedProfileAvailability, } from "./credential-profiles.js";
3
- import { OrchestratorCredentials, rotatedSpecInLaneHome } from "./orchestrator-credentials.js";
3
+ import { OrchestratorCredentials, reviewerProfileResolver, rotatedSpecInLaneHome, } from "./orchestrator-credentials.js";
4
4
  import { accountPoolRows } from "./account-pool.js";
5
5
  import { writeRunTelemetryArtifact } from "./runTelemetryWriter.js";
6
6
  import { buildFileBackedSynthesisInput, materializeWinnerOutputs, stageFileBackedContext, writeCandidateAttemptArtifacts, } from "./candidateOutputs.js";
7
7
  import { processAttemptUsage } from "./attemptUsage.js";
8
- import { appliedAttemptFacts, assertDelegatedEvidence, confinementNotice, externallyConfinedLane, scopedHarnessHome, } from "./delegatedHome.js";
8
+ import { appliedAttemptFacts, assertDelegatedEvidence, isMutatingAccess, outerBoundaryNotice, scopedHarnessHome, } from "./delegatedHome.js";
9
9
  import * as AC from "./attemptUsageCost.js";
10
10
  import { candidateRoster, convergenceOutcomeFacts, isWorkingCandidate, partitionCandidates, toCandidateEvidence, unanimousDeclaredFailure, } from "./candidateEvidence.js";
11
11
  import { capabilityIntents } from "@claudexor/gateway";
@@ -13,9 +13,9 @@ import { policyFindings } from "./policyFindings.js";
13
13
  import { join } from "node:path";
14
14
  import { finalizePlanRun, runCouncilPlan, writePlanHarnessFailure, } from "./planRun.js";
15
15
  import { runPlannerAttempt as executePlannerAttempt, } from "./plannerAttempt.js";
16
- import { HarnessRunSpec, ModeKind as ModeKindSchema, QuotaSnapshot as QuotaSnapshotSchema, isBlocking, makeOutcomeFacts, normalizeUserOutputSchema, strictifyOutputSchema, estimateEffectiveAuthRoute, } from "@claudexor/schema";
16
+ import { HarnessRunSpec, ModeKind as ModeKindSchema, QuotaSnapshot as QuotaSnapshotSchema, isBlocking, makeOutcomeFacts, strictifyOutputSchema, estimateEffectiveAuthRoute, } from "@claudexor/schema";
17
17
  import { globalConfigDir, loadConfig } from "@claudexor/config";
18
- import { acceptedTryOutput, AnswerAssembly, CLAUDEXOR_BROWSER_ARTIFACT_SUBDIR, countsAsAgentProgress, HarnessUnavailableError, summarizeDiffPaths as diffStats, withInactivityWatchdog, } from "@claudexor/core";
18
+ import { acceptedTryOutput, AccessProfileIncompatibleError, AnswerAssembly, CLAUDEXOR_BROWSER_ARTIFACT_SUBDIR, countsAsAgentProgress, HarnessUnavailableError, summarizeDiffPaths as diffStats, withInactivityWatchdog, } from "@claudexor/core";
19
19
  import { assertRouteModelsAllowed, runModelGovernedRoute } from "./modelGovernance.js";
20
20
  import { authModeForCredentialRoute, authModeForPreference } from "./auth-route-classification.js";
21
21
  import { governRouteEffort } from "./effortGovernance.js";
@@ -32,7 +32,8 @@ import { arbitrationBudgetOptions, decisionBudgetSummary } from "./decisionBudge
32
32
  import { buildRevisePrompt } from "./revisePrompt.js";
33
33
  import { cancelledResult, declaredFailure, failTerminally, guardAnnouncedRun, writeFailure, } from "./runTerminals.js";
34
34
  import { budgetFailureRecord, classifyBudgetFailure } from "./budgetFailure.js";
35
- import { assertOutputSchemaCompiles, finalizeStructuredOutput } from "./structuredOutput.js";
35
+ import { finalizeStructuredOutput } from "./structuredOutput.js";
36
+ import { admitRun } from "./runAdmission.js";
36
37
  import { dropDeltaPastBudget, emitPlanProgress, emitTransientExhausted, emitTransientRetryPlan, observeReadonlySpend, } from "./laneStreamEvents.js";
37
38
  import { promptWithEngineConstraints, sleep, redactHarnessEvent, harnessEventPayload, safeErrorMessage, renderSummary, observeBudgetSignals, rotateOnStall, recordCleanAttemptMetrics, envInheritance, transientRetryPolicy, reviewerTimeoutMs, harnessInactivityTimeoutMs, observeAuthSwitch, emitPrimaryDivergence, emitPoolDegraded, deliveryRefusalFailure, winnerNeedsHuman, writeRaceDeliveryDecision, } from "./runSupport.js";
38
39
  import { candidateStatusInRouteContext, resolveReadOnlyRouteContext, } from "./routeContext.js";
@@ -53,7 +54,7 @@ import { interactionChannelFor } from "./interaction.js";
53
54
  import { gateSpecsFromContract, renderTestsEvidence } from "./contract-gates.js";
54
55
  import { buildTaskContract } from "./task-contract-builder.js";
55
56
  import { ArtifactStore } from "@claudexor/artifact-store";
56
- import { assertMandatoryContext, buildContextPack, rawContextForEnvelope, preflightEvidence, writeEvidencePacket, } from "@claudexor/context";
57
+ import { buildContextPack, rawContextForEnvelope, preflightEvidence, writeEvidencePacket, } from "@claudexor/context";
57
58
  import { WorkspaceManager, captureRawPatchEnvelope, createRevertAnchorFromPatchOrNull, createRevertAnchorOrNull, ensureClaudeBridge, consumeRawPatchEnvelope, snapshotTree, } from "@claudexor/workspace";
58
59
  import { blockedDecisionOverride, finalVerifyBlocks, finalVerifyPatch, verifyAndDeliver, } from "@claudexor/delivery";
59
60
  import { HarnessGateway } from "@claudexor/gateway";
@@ -62,7 +63,6 @@ import { arbitrate } from "@claudexor/arbitration";
62
63
  import { buildSynthesisPlan, decideSynthesis } from "@claudexor/synthesis";
63
64
  import { attemptCostEvidence, attemptUsageCostSettlement, BudgetLedger, isBudgetTerminal, explainRanking, loadHarnessMetrics, promptFingerprint, rankHarnesses, reviewUsageCostSettlement, } from "@claudexor/budget";
64
65
  import { readTextSafe, appendLine, assertNoInlineSecretValues, DELEGATION_ENV, hashJson, newId, noProjectRepoRoot, redactSecrets, safeInvoke, sha256, userConfigDir, writeText, } from "@claudexor/util";
65
- import { assertWriteIsolation } from "./write-isolation.js";
66
66
  const LABELS = "ABCDEFGHIJ".split("");
67
67
  const NO_PROJECT_ROOT = noProjectRepoRoot();
68
68
  /** Concurrency cap for parallel candidates/explorers (locked decision: min(n, 4)). */
@@ -89,7 +89,7 @@ export class Orchestrator {
89
89
  /** Scoped DIFF review — thin delegate; mechanics live in diffReview.ts. */
90
90
  async reviewDiff(input) {
91
91
  return runDiffReview(input, {
92
- resolveReviewers: (root, pref) => this.resolveReviewers(root, pref),
92
+ resolveReviewers: this.resolveReviewers.bind(this),
93
93
  reviewScoped: (i) => this.reviewScoped(i),
94
94
  execRootOf: (root) => this.execRootOf({ repoRoot: root }),
95
95
  envInheritance: (root) => envInheritance(this.config(root)),
@@ -134,42 +134,11 @@ export class Orchestrator {
134
134
  delegationParentRunId: runId,
135
135
  };
136
136
  }
137
- const projectProtectedPaths = mode === "agent" ? this.projectConfig(resolved.repoRoot).constraints.protected_paths : [];
138
- assertWriteIsolation({
139
- mode,
140
- protectedPaths: projectProtectedPaths,
141
- denyPaths: resolved.denyPaths,
142
- inPlace: resolved.inPlace,
143
- repoRoot: resolved.repoRoot,
144
- executionRoot: this.execRootOf(resolved),
137
+ resolved.outputSchema = admitRun(resolved, mode, {
138
+ accessDefault: this.config(resolved.repoRoot).trust.access_default,
139
+ projectProtectedPaths: () => this.projectConfig(resolved.repoRoot).constraints.protected_paths,
140
+ mandatoryFiles: () => this.projectConfig(resolved.repoRoot).context.mandatory_files,
145
141
  });
146
- // outputSchema constrains the run's final ANSWER. It is honored exactly
147
- // where a final answer is delivered (agent race incl. synthesis, and ask);
148
- // every other strategy refuses loudly rather than carrying a contract the
149
- // engine would not validate (INV-023). The schema itself is normalized for
150
- // the native structured-output routes here at the boundary — unsupported
151
- // shapes (external/cyclic $ref, non-object root) are a typed refusal, not a mid-run 400.
152
- if (resolved.outputSchema !== undefined && resolved.outputSchema !== null) {
153
- if (mode !== "agent" && mode !== "ask") {
154
- throw new Error(`outputSchema constrains the final answer and applies to agent/ask runs (got mode=${mode}); drop the schema or switch modes`);
155
- }
156
- if (resolved.untilClean || (resolved.attempts !== undefined && resolved.attempts !== null)) {
157
- throw new Error("outputSchema is not supported with convergence flags (--until-clean/--attempts): convergence delivers a gated patch, not a structured answer; drop the schema or the convergence flags");
158
- }
159
- // Shape-refuse unsupported schemas, then PROVE it compiles under the same
160
- // ajv the engine validator uses — a malformed schema is a preflight
161
- // refusal here (before any run dir), never a mid-run validator crash. The
162
- // contract keeps the ORIGINAL (conformance authority); local-ref inlining
163
- // and strictification are transport-only transforms in harnessSpecKnobs.
164
- resolved.outputSchema = normalizeUserOutputSchema(resolved.outputSchema);
165
- assertOutputSchemaCompiles(resolved.outputSchema);
166
- }
167
- // P1: a versioned `mandatory_files` contract is enforced UNIFORMLY here, for
168
- // every mode, so the same repo state can't pass `run`/`ask` while failing
169
- // `audit`. No-op when the list is empty (the default) or for no-project runs.
170
- if (resolved.repoRoot !== NO_PROJECT_ROOT) {
171
- assertMandatoryContext(resolved.repoRoot, this.projectConfig(resolved.repoRoot).context.mandatory_files);
172
- }
173
142
  // Reviewer panels are validated only inside Agent strategies that actually
174
143
  // review (race/convergence; Plan Council is the plan critique path) — AFTER run-dir
175
144
  // creation, so a doomed explicit panel yields typed failure ARTIFACTS
@@ -239,6 +208,7 @@ export class Orchestrator {
239
208
  registry: this.deps.registry,
240
209
  harnessSettings: this.config(cwd)?.global.harnesses ?? {},
241
210
  authPreferenceFor: (id) => this.authPreferenceForHarness(cwd, id, runAuthPreference),
211
+ resolveReviewerProfile: reviewerProfileResolver(this.credentials, cwd),
242
212
  onIgnoredSetting,
243
213
  }, { reviewerModels: this.deps.reviewerModels, reviewerEfforts: this.deps.reviewerEfforts });
244
214
  }
@@ -295,6 +265,7 @@ export class Orchestrator {
295
265
  registry: this.deps.registry,
296
266
  harnessSettings: this.config(cwd)?.global.harnesses ?? {},
297
267
  authPreferenceFor: (id) => this.authPreferenceForHarness(cwd, id, runAuthPreference),
268
+ resolveReviewerProfile: reviewerProfileResolver(this.credentials, cwd),
298
269
  }, panel);
299
270
  }
300
271
  authPreferenceForHarness(repoRoot, harnessId, runAuthPreference) {
@@ -449,9 +420,9 @@ export class Orchestrator {
449
420
  : `credential profile "${input.credentialProfileId}" belongs to unavailable harness(es): ${registered.map((p) => p.harness_id).join(", ")}`);
450
421
  }
451
422
  else {
452
- // Auto-pools take doctor-OK harnesses (BIBLE §2: doctor decides
453
- // readiness; a key string or degraded route is visible but not
454
- // routable) PLUS harnesses with enabled account rows: the default
423
+ // Auto-pools take doctor-OK default routes (BIBLE §2: doctor decides
424
+ // default-route readiness; a key string or degraded route is visible
425
+ // but not routable) PLUS harnesses with enabled account rows: the default
455
426
  // doctor only sees the default store, and a signed-in registry row is
456
427
  // invisible to it (cursor after the host-Keychain retirement, or a
457
428
  // named-rows-only claude/codex install). Row-bearing lanes still pass
@@ -463,7 +434,7 @@ export class Orchestrator {
463
434
  accountPoolRows(registry, s.id).length > 0))
464
435
  .map((s) => s.id);
465
436
  if (ids.length === 0) {
466
- throw new HarnessUnavailableError("no doctor-ok harness for this mode; install/login codex/claude/cursor/opencode (see `claudexor doctor`), or pass --harness explicitly");
437
+ throw new HarnessUnavailableError("no doctor-OK default route or enabled account row for this mode; inspect `claudexor doctor` and `claudexor accounts`, or pass --harness explicitly");
467
438
  }
468
439
  }
469
440
  }
@@ -491,8 +462,7 @@ export class Orchestrator {
491
462
  const policy = input.web ?? input.externalContextPolicy ?? "auto";
492
463
  const pool = [];
493
464
  const dropped = [];
494
- // Structured requested-vs-effective route receipt (QA-043): every auto-pool
495
- // drop is recorded with its typed STAGE so the disclosure preserves the
465
+ // Every auto-pool drop is recorded with its typed STAGE so the disclosure preserves the
496
466
  // real cause instead of collapsing to one reason.
497
467
  const droppedLanes = [];
498
468
  // The ONE explicit-lane admission gate shared by every drop site (QA-043 /
@@ -501,14 +471,18 @@ export class Orchestrator {
501
471
  // substitution or self-race duplication; an AUTO lane is dropped with a
502
472
  // typed omission recorded for the degradation receipt.
503
473
  const dropLane = (harnessId, stage, detail) => {
504
- if (explicitPool)
474
+ if (explicitPool) {
475
+ if (stage === "access")
476
+ throw new AccessProfileIncompatibleError(detail);
505
477
  throw new HarnessUnavailableError(detail);
478
+ }
506
479
  dropped.push(detail);
507
480
  droppedLanes.push({ harnessId, stage, detail });
508
481
  };
509
482
  // One observation set for the whole admission pass, so two lanes cannot be
510
483
  // judged against different vendor epochs.
511
484
  const vendorQuota = this.credentials.vendorQuotaObservations();
485
+ const liveUnusable = this.deps.credentialUnusable?.() ?? [];
512
486
  for (const id of ids) {
513
487
  const adapter = this.deps.registry.get(id);
514
488
  if (!adapter) {
@@ -577,14 +551,8 @@ export class Orchestrator {
577
551
  const profileAdapter = this.deps.registry.get(id);
578
552
  const profileProbe = profileAdapter?.probeCredentialProfile?.bind(profileAdapter);
579
553
  const explicitPin = this.credentials.effectiveProfileId(input, id);
580
- // The account candidates whose readiness may overlay the default-store
581
- // verdict: the explicit pin when set; otherwise row-aware UNPINNED
582
- // admission — the thread-bound row followed by the harness's enabled
583
- // pool rows, consulted only when the default doctor did not already
584
- // admit the lane. A signed-in registry row is invisible to the default
585
- // doctor (cursor after the host-Keychain retirement, or a named-rows-
586
- // only claude/codex install), so an unpinned run must be admitted on
587
- // row readiness exactly the way explicit pins already are.
554
+ const model = input.models?.[id] ?? cfgEntry?.default_model ?? null;
555
+ // Pins and bound/pool rows are checked against their own readiness.
588
556
  const rowCandidateIds = [];
589
557
  if (explicitPin) {
590
558
  rowCandidateIds.push(explicitPin);
@@ -603,9 +571,12 @@ export class Orchestrator {
603
571
  probe: profileProbe,
604
572
  // `verification: passed` from the local store only means a login file
605
573
  // is present. The poller's authenticated vendor call is the only
606
- // liveness evidence we have, and admission is the surface that must
607
- // act on it — otherwise the run dispatches into a revoked token.
574
+ // liveness evidence; admission must act on it or dispatch may use a revoked token.
608
575
  quota: vendorQuota,
576
+ unusable: liveUnusable,
577
+ model,
578
+ // Only an explicit/bound route may consume bounded stale LKG evidence.
579
+ allowStale: explicitPin !== null || input.threadAccountBindings?.[id] === candidateId,
609
580
  });
610
581
  if (verdict === "available") {
611
582
  profileAdmitted = true;
@@ -645,7 +616,7 @@ export class Orchestrator {
645
616
  const readOnlyIntent = intent === "plan" || intent === "spec" || intent === "explain" || intent === "audit";
646
617
  const requiredAccess = this.requestRequirements.adapterAccess(intent, manifest.capabilities.implementation_transport, readOnlyIntent
647
618
  ? "readonly"
648
- : (input.access ?? this.config(input.repoRoot).trust.access_default), externallyConfinedLane(input.delegated === true));
619
+ : (input.access ?? this.config(input.repoRoot).trust.access_default));
649
620
  const accessSupported = !requiredAccess || manifest.access_profiles_supported.includes(requiredAccess);
650
621
  const webSupport = manifest.capabilities.web_policy;
651
622
  // Match routeSpecKnobs: a per-harness web default upgrades run-level auto,
@@ -679,6 +650,7 @@ export class Orchestrator {
679
650
  harnessId: id,
680
651
  requested: input.browser === true,
681
652
  manifestCapable: manifest.capabilities.browser_tool,
653
+ requiresFullAccess: manifest.capability_profile.mcp_injection_requires_full_access,
682
654
  webPolicy: routePolicy,
683
655
  access: requiredAccess,
684
656
  }),
@@ -730,6 +702,9 @@ export class Orchestrator {
730
702
  }
731
703
  }
732
704
  if (pool.length === 0) {
705
+ if (droppedLanes.length > 0 && droppedLanes.every((lane) => lane.stage === "access")) {
706
+ throw new AccessProfileIncompatibleError(`no harness can enforce the requested access profile (${droppedLanes.map((lane) => lane.detail).join(", ")})`);
707
+ }
733
708
  throw new HarnessUnavailableError(`no harness can perform '${intent}' for this mode${dropped.length ? ` (skipped: ${dropped.join(", ")})` : ""}`);
734
709
  }
735
710
  // Quota admission must use the account that will actually spawn. In
@@ -1382,8 +1357,8 @@ export class Orchestrator {
1382
1357
  /** Decided by `harnessHomeFor` in the CALLER, so the per-attempt applied
1383
1358
  * facts exist in the caller's catch too: an attempt that ran and then threw
1384
1359
  * must record what it ran under, not just why it stopped. REQUIRED (no
1385
- * default): a silent fallback here would spawn a delegated child on the
1386
- * operator's real home while the record still claimed confinement. */
1360
+ * default): a silent fallback here would spawn a delegated attempt on the
1361
+ * operator's real home while the record still claimed scoped state. */
1387
1362
  harnessHome) {
1388
1363
  const adapter = routed.adapter;
1389
1364
  const knobs = this.routeSpecKnobs(routed, contract, modelHint, effortHint);
@@ -1412,8 +1387,8 @@ export class Orchestrator {
1412
1387
  // Engine-derived read-only prompt constraints: protected/auto-protected
1413
1388
  // paths PLUS the exact typed gate argv the run will execute (QA-022 FIX B).
1414
1389
  prompt: promptWithEngineConstraints(
1415
- // A child running without an OS boundary is TOLD so (disclosure 2 of 3).
1416
- [prompt, confinementNotice(harnessHome), laneContinuity?.pointerLine, continuationPointer]
1390
+ // The child is told that Claudexor adds no outer OS boundary (disclosure 2 of 3).
1391
+ [prompt, outerBoundaryNotice(harnessHome), laneContinuity?.pointerLine, continuationPointer]
1417
1392
  .filter((s) => typeof s === "string" && s.length > 0)
1418
1393
  .join("\n\n"), contract.constraints.protected_paths, contract.constraints.auto_protected_paths, contract.constraints.protected_path_approvals, contract.tests.commands),
1419
1394
  attachments: runInput?.attachments ?? [],
@@ -1440,11 +1415,8 @@ export class Orchestrator {
1440
1415
  // Scoped harness home for isolated envelopes AND for every delegated run;
1441
1416
  // an ordinary in-place run keeps the native environment so the resumed
1442
1417
  // vendor session is reachable. See scopedHarnessHome for the cost a
1443
- // delegated in-place attempt pays for that confinement.
1418
+ // delegated in-place attempt pays for that scoped state.
1444
1419
  ...(harnessHome.env ? { env: harnessHome.env } : {}),
1445
- // The OS boundary itself. The shared CLI run loop wraps the argv with it;
1446
- // no adapter opts in, so no adapter can forget.
1447
- confinement: harnessHome.confinement,
1448
1420
  raw_context_packet: rawContextPacket,
1449
1421
  stream_deltas: streamDeltas,
1450
1422
  });
@@ -1791,7 +1763,7 @@ export class Orchestrator {
1791
1763
  worktreePath: envelope.worktree_path,
1792
1764
  artifactRelativeDir,
1793
1765
  diff,
1794
- persistPatch: secretDiffRefusal === undefined,
1766
+ persistPatch: secretDiffRefusal === undefined && isMutatingAccess(access),
1795
1767
  persistProducedMedia: secretDiffRefusal === undefined,
1796
1768
  answerText,
1797
1769
  record: {
@@ -1807,8 +1779,8 @@ export class Orchestrator {
1807
1779
  ...(secretDiffRefusal ? { secret_diff_refusal: secretDiffRefusal } : {}),
1808
1780
  gates: gates.map((g) => ({ id: g.id, status: g.status })),
1809
1781
  branch: envelope.branch_name,
1810
- // Applied facts, not promises: the caller of a delegated run
1811
- // verifies the confinement it asked for instead of trusting it.
1782
+ // Applied facts, not promises: historical proofs stay readable and
1783
+ // current delegated runs state deliberate outer-boundary absence.
1812
1784
  // Built by the SAME shape the failure path writes.
1813
1785
  ...applied,
1814
1786
  },
@@ -1888,6 +1860,7 @@ export class Orchestrator {
1888
1860
  // announced: a refused run must fail the request loudly, not 200 a runId
1889
1861
  // and leave an orphaned run dir without a terminal event.
1890
1862
  const contract = this.buildContract(input, taskId, mode);
1863
+ const mutatingRun = isMutatingAccess(contract.access.effective_profile);
1891
1864
  const planBrief = verifiedPlanBrief(input);
1892
1865
  const quotaSnapshots = this.quotaSnapshotPreflight();
1893
1866
  const { store, paths, log, ledger } = beginAnnouncedRun({
@@ -1918,28 +1891,32 @@ export class Orchestrator {
1918
1891
  // directory, a filesystem root, or one that cannot be classified), which
1919
1892
  // gets a typed refusal BEFORE any mutation (INV-075). For an isolated
1920
1893
  // thread the execution root is already a git worktree: a no-op there.
1921
- const gitPreconditionError = await ensureWriteModeGitBoundary(execRoot, log, store, paths, runId, mode);
1922
- if (gitPreconditionError) {
1923
- return {
1924
- runId,
1925
- taskId,
1926
- mode,
1927
- lifecycle: "failed",
1928
- facts: makeOutcomeFacts("failed", { reason: gitPreconditionError.reason }),
1929
- winner: null,
1930
- runDir: paths.root,
1931
- summary: gitPreconditionError.message,
1932
- candidates: [],
1933
- };
1894
+ if (mutatingRun) {
1895
+ const gitPreconditionError = await ensureWriteModeGitBoundary(execRoot, log, store, paths, runId, mode);
1896
+ if (gitPreconditionError) {
1897
+ return {
1898
+ runId,
1899
+ taskId,
1900
+ mode,
1901
+ lifecycle: "failed",
1902
+ facts: makeOutcomeFacts("failed", { reason: gitPreconditionError.reason }),
1903
+ winner: null,
1904
+ runDir: paths.root,
1905
+ summary: gitPreconditionError.message,
1906
+ candidates: [],
1907
+ };
1908
+ }
1934
1909
  }
1935
1910
  // Same run-prep stage as the git boundary: if the PROJECT root uses AGENTS.md
1936
1911
  // with no CLAUDE.md, bridge it so a Claude Code candidate reads it (INV-113).
1937
- this.ensureClaudeBridgeForRun(input.repoRoot, input.inPlace === true, log);
1912
+ if (mutatingRun) {
1913
+ this.ensureClaudeBridgeForRun(input.repoRoot, input.inPlace === true, log);
1914
+ }
1938
1915
  // Pre-turn snapshot of the live tree for in-place runs: the revert restore
1939
1916
  // target (server-owned revertInPlace). A snapshot failure must never fail the
1940
1917
  // run — revert is simply unavailable then.
1941
1918
  let preTurnSha = null;
1942
- if (input.inPlace === true) {
1919
+ if (mutatingRun && input.inPlace === true) {
1943
1920
  try {
1944
1921
  preTurnSha = await snapshotTree(execRoot);
1945
1922
  }
@@ -2109,9 +2086,10 @@ export class Orchestrator {
2109
2086
  accessProfile: candidateAccess,
2110
2087
  // Direct-workspace singletons run in place. Races and patch-envelope
2111
2088
  // transports stay isolated and adopt through the delivery service.
2112
- inPlace: input.inPlace === true &&
2113
- requestedSingleCandidate &&
2114
- slot.routed.implementationTransport !== "git_patch_envelope",
2089
+ inPlace: !mutatingRun ||
2090
+ (input.inPlace === true &&
2091
+ requestedSingleCandidate &&
2092
+ slot.routed.implementationTransport !== "git_patch_envelope"),
2115
2093
  });
2116
2094
  harnessHome = this.harnessHomeFor(wsm, envelope, slot.routed, input);
2117
2095
  const run = await this.runCandidateInEnvelope(slot.routed, envelope, slot.attemptId, slot.label, contract, input.prompt, store, paths, wsm, ledger, candidateAccess, (ev) => {
@@ -2287,8 +2265,8 @@ export class Orchestrator {
2287
2265
  };
2288
2266
  await runBounded(slots, Math.min(slots.length, MAX_PARALLEL_CANDIDATES), runSlot);
2289
2267
  const runs = runsBySlot.filter((r) => r !== undefined);
2290
- // Fail-closed terminal: a delegated mutating run whose attempts cannot state
2291
- // their applied confinement refuses instead of passing.
2268
+ // Fail-closed terminal: a delegated mutating run whose attempts state
2269
+ // neither historical proof nor deliberate absence refuses instead of passing.
2292
2270
  assertDelegatedEvidence(input.delegated === true, candidateAccess, runs);
2293
2271
  const cancelledCandidates = () => runs.map((r) => ({
2294
2272
  attemptId: r.attemptId,
@@ -2306,7 +2284,10 @@ export class Orchestrator {
2306
2284
  // arbitration (as the race-adoption path does) would fold those user edits
2307
2285
  // into the revert target and let a later revert clobber them.
2308
2286
  let earlyPostTurnSha = null;
2309
- if (input.inPlace && requestedSingleCandidate && runs.every((run) => !run.secretDiffRefusal)) {
2287
+ if (mutatingRun &&
2288
+ input.inPlace &&
2289
+ requestedSingleCandidate &&
2290
+ runs.every((run) => !run.secretDiffRefusal)) {
2310
2291
  try {
2311
2292
  earlyPostTurnSha = await snapshotTree(execRoot);
2312
2293
  }
@@ -2322,13 +2303,15 @@ export class Orchestrator {
2322
2303
  if (failedDelegation) {
2323
2304
  const failure = delegateFailure.candidateFailureTerminal(failedDelegation, "race");
2324
2305
  await disposeReviewEnvelopes();
2325
- await delegateFailure.persistFailedInPlaceWorkProduct({
2326
- ...{ store, log, paths, execRoot, preTurnSha, taskId, mode },
2327
- live: input.inPlace === true && failedDelegation.reviewCwd === execRoot,
2328
- run: failedDelegation,
2329
- postTurnSha: earlyPostTurnSha,
2330
- kind: input.create === true ? "new_repo" : "patch",
2331
- });
2306
+ if (mutatingRun) {
2307
+ await delegateFailure.persistFailedInPlaceWorkProduct({
2308
+ ...{ store, log, paths, execRoot, preTurnSha, taskId, mode },
2309
+ live: input.inPlace === true && failedDelegation.reviewCwd === execRoot,
2310
+ run: failedDelegation,
2311
+ postTurnSha: earlyPostTurnSha,
2312
+ kind: input.create === true ? "new_repo" : "patch",
2313
+ });
2314
+ }
2332
2315
  this.writeRunTelemetry(store, paths, contract, runId, taskId, mode, candidateRoster(runs), null);
2333
2316
  return failTerminally(log, store, paths, runId, taskId, mode, failure.phase, failure.error, ledger.spend(), failure.metadata);
2334
2317
  }
@@ -2510,7 +2493,13 @@ export class Orchestrator {
2510
2493
  return cancelledRaceResult();
2511
2494
  }
2512
2495
  // Synthesis: if worthwhile, run a synthesizer as a NEW, re-checked candidate.
2513
- const synth = decideSynthesis(evidences, input.synthesis ?? "auto");
2496
+ const synth = mutatingRun
2497
+ ? decideSynthesis(evidences, input.synthesis ?? "auto")
2498
+ : {
2499
+ synthesize: false,
2500
+ reason: "readonly access has no write-backed synthesis lifecycle",
2501
+ sources: evidences.map((evidence) => evidence.attemptId),
2502
+ };
2514
2503
  store.writeYaml(join(paths.arbitrationDir, "synthesis.yaml"), synth);
2515
2504
  log.emit("synthesis.started", { synthesize: synth.synthesize, reason: synth.reason });
2516
2505
  if (synth.synthesize && !budgetStopped) {
@@ -2639,7 +2628,8 @@ export class Orchestrator {
2639
2628
  // instead defers verification until immediately before delivery.
2640
2629
  const inPlaceWinner = winnerRun?.reviewCwd === execRoot;
2641
2630
  const deferredRaceVerify = input.inPlace === true && !inPlaceWinner;
2642
- if (winnerRun &&
2631
+ if (mutatingRun &&
2632
+ winnerRun &&
2643
2633
  !inPlaceWinner &&
2644
2634
  !deferredRaceVerify &&
2645
2635
  winnerRun.diff.trim().length > 0 &&
@@ -2674,17 +2664,7 @@ export class Orchestrator {
2674
2664
  })) {
2675
2665
  log.emit("output.ready", { kind: "artifact", path });
2676
2666
  }
2677
- secretDiff.assertNoSecretLikeTokens("final patch diff", winnerRun.diff);
2678
- const patchSha256 = sha256(winnerRun.diff);
2679
- store.writeText(join(paths.finalDir, "patch.diff"), winnerRun.diff);
2680
- const wstats = diffStats(winnerRun.diff);
2681
- const hasDiff = winnerRun.diff.trim().length > 0;
2682
- const blockers = winnerEvidence
2683
- ? winnerEvidence.findings.filter((f) => isBlocking(f)).length
2684
- : 0;
2685
- // Prose from an empty-diff winner is an answer, never a patch.
2686
2667
  const winnerAnswer = winnerRun.answerText ?? "";
2687
- const resultKind = hasDiff ? "patch" : winnerAnswer.length > 0 ? "answer" : "none";
2688
2668
  // The winner's final MESSAGE is the human-facing answer and materializes
2689
2669
  // for diff-ful runs too: the chat renders final/answer.md (the projection
2690
2670
  // prefers it), never the arbitration summary — "Run … Winner: a01 …" is
@@ -2705,107 +2685,120 @@ export class Orchestrator {
2705
2685
  answerText: winnerAnswer,
2706
2686
  });
2707
2687
  }
2708
- // Only a fully verified, applyable success may auto-adopt; a not-verified
2709
- // or needs-decision terminal remains an inspectable artifact.
2710
- const adoptable = facts.lifecycle === "succeeded" && facts.review === "approved" && facts.checks !== "failed";
2711
- let adopted = null;
2712
- let applyState = "not_applied";
2713
- let postTurnSha = null;
2714
- let revertAnchorId = null;
2715
- if (input.inPlace === true && hasDiff) {
2716
- if (inPlaceWinner) {
2717
- // Already live: the candidate ran in-place and wrote the tree itself.
2718
- adopted = true;
2719
- applyState = adoptable ? "applied" : "applied_review_blocked";
2720
- // The pre-review fence excludes later user edits from the target.
2721
- postTurnSha = earlyPostTurnSha;
2722
- }
2723
- else if (adoptable) {
2724
- // Protected apply preserves the live tree or reports tree_mutated.
2725
- const applied = await verifyAndDeliver(execRoot, winnerRun.diff, { mode: "apply", protectedApply: true }, gateSpecsFromContract(contract), (freshVerify) => {
2726
- finalVerify = freshVerify;
2727
- return finalVerifyBlocks(freshVerify)
2728
- ? (freshVerify.reason ?? "final verify failed before race adoption")
2729
- : null;
2730
- }, log);
2731
- raceDeliveryReceipt = applied;
2732
- store.writeYaml(join(paths.finalDir, "delivery_receipt.yaml"), applied);
2733
- finalVerify = applied.finalVerify;
2734
- if (applied.applied) {
2688
+ if (mutatingRun) {
2689
+ secretDiff.assertNoSecretLikeTokens("final patch diff", winnerRun.diff);
2690
+ const patchSha256 = sha256(winnerRun.diff);
2691
+ store.writeText(join(paths.finalDir, "patch.diff"), winnerRun.diff);
2692
+ const wstats = diffStats(winnerRun.diff);
2693
+ const hasDiff = winnerRun.diff.trim().length > 0;
2694
+ const blockers = winnerEvidence
2695
+ ? winnerEvidence.findings.filter((f) => isBlocking(f)).length
2696
+ : 0;
2697
+ const resultKind = hasDiff ? "patch" : winnerAnswer.length > 0 ? "answer" : "none";
2698
+ // Only a fully verified, applyable success may auto-adopt; a not-verified
2699
+ // or needs-decision terminal remains an inspectable artifact.
2700
+ const adoptable = facts.lifecycle === "succeeded" &&
2701
+ facts.review === "approved" &&
2702
+ facts.checks !== "failed";
2703
+ let adopted = null;
2704
+ let applyState = "not_applied";
2705
+ let postTurnSha = null;
2706
+ let revertAnchorId = null;
2707
+ if (input.inPlace === true && hasDiff) {
2708
+ if (inPlaceWinner) {
2709
+ // Already live: the candidate ran in-place and wrote the tree itself.
2735
2710
  adopted = true;
2736
- applyState = "applied";
2737
- log.emit("work_product.adopted", {
2738
- applied: true,
2739
- patch_sha256: patchSha256,
2740
- winner: winnerRun.attemptId,
2741
- });
2742
- try {
2743
- postTurnSha = await snapshotTree(execRoot);
2711
+ applyState = adoptable ? "applied" : "applied_review_blocked";
2712
+ // The pre-review fence excludes later user edits from the target.
2713
+ postTurnSha = earlyPostTurnSha;
2714
+ }
2715
+ else if (adoptable) {
2716
+ // Protected apply preserves the live tree or reports tree_mutated.
2717
+ const applied = await verifyAndDeliver(execRoot, winnerRun.diff, { mode: "apply", protectedApply: true }, gateSpecsFromContract(contract), (freshVerify) => {
2718
+ finalVerify = freshVerify;
2719
+ return finalVerifyBlocks(freshVerify)
2720
+ ? (freshVerify.reason ?? "final verify failed before race adoption")
2721
+ : null;
2722
+ }, log);
2723
+ raceDeliveryReceipt = applied;
2724
+ store.writeYaml(join(paths.finalDir, "delivery_receipt.yaml"), applied);
2725
+ finalVerify = applied.finalVerify;
2726
+ if (applied.applied) {
2727
+ adopted = true;
2728
+ applyState = "applied";
2729
+ log.emit("work_product.adopted", {
2730
+ applied: true,
2731
+ patch_sha256: patchSha256,
2732
+ winner: winnerRun.attemptId,
2733
+ });
2734
+ try {
2735
+ postTurnSha = await snapshotTree(execRoot);
2736
+ }
2737
+ catch {
2738
+ postTurnSha = null;
2739
+ }
2740
+ revertAnchorId = createRevertAnchorFromPatchOrNull(execRoot, winnerRun.diff);
2744
2741
  }
2745
- catch {
2746
- postTurnSha = null;
2742
+ else {
2743
+ adopted = false;
2744
+ applyState = "not_applied";
2745
+ deliveryFailureReason = applied.detail ?? "race adoption delivery was refused";
2746
+ facts = { ...facts, checks: "failed", reason: "checks_failed" };
2747
+ if (finalVerifyBlocks(finalVerify))
2748
+ finalVerifyFailed = true;
2749
+ log.emit("work_product.adopted", {
2750
+ applied: false,
2751
+ patch_sha256: patchSha256,
2752
+ detail: redactSecrets(applied.detail ?? "apply failed"),
2753
+ tree_mutated: applied.treeMutated,
2754
+ });
2747
2755
  }
2748
- revertAnchorId = createRevertAnchorFromPatchOrNull(execRoot, winnerRun.diff);
2749
- }
2750
- else {
2751
- adopted = false;
2752
- applyState = "not_applied";
2753
- deliveryFailureReason = applied.detail ?? "race adoption delivery was refused";
2754
- facts = { ...facts, checks: "failed", reason: "checks_failed" };
2755
- if (finalVerifyBlocks(finalVerify))
2756
- finalVerifyFailed = true;
2757
- log.emit("work_product.adopted", {
2758
- applied: false,
2759
- patch_sha256: patchSha256,
2760
- detail: redactSecrets(applied.detail ?? "apply failed"),
2761
- tree_mutated: applied.treeMutated,
2762
- });
2763
2756
  }
2764
2757
  }
2765
- }
2766
- writeRaceDeliveryDecision(store, decisionPath, {
2767
- decision: result.decision,
2768
- facts,
2769
- reviewVerified: actualReviewVerified,
2770
- finalVerify,
2771
- deliveryFailureReason,
2772
- deliveryReceiptPath: raceDeliveryReceipt ? "final/delivery_receipt.yaml" : null,
2773
- });
2774
- if (inPlaceWinner && requestedSingleCandidate && adopted === true) {
2775
- revertAnchorId = await createRevertAnchorOrNull(execRoot, preTurnSha, postTurnSha);
2776
- }
2777
- store.writeYaml(join(paths.finalDir, "work_product.yaml"), {
2778
- id: newId("wp"),
2779
- kind: input.create === true ? "new_repo" : "patch",
2780
- source_task_id: taskId,
2781
- producer_attempt_id: winnerRun.attemptId,
2782
- ...(raceDeliveryReceipt
2783
- ? { files: { delivery_receipt: "final/delivery_receipt.yaml" } }
2784
- : {}),
2785
- meta: {
2786
- harness_id: winnerRun.harnessId,
2787
- synthesis: synth,
2788
- mode,
2789
- // Artifact-only apply reads the same terminal axes as the daemon (D8).
2790
- lifecycle: facts.lifecycle,
2791
- outcome_facts: facts,
2792
- review_verified: actualReviewVerified,
2793
- budget_stopped: budgetStopped,
2794
- patch_sha256: patchSha256,
2795
- result_kind: resultKind,
2796
- diffstat: {
2797
- files: wstats.paths.length,
2798
- additions: wstats.additions,
2799
- deletions: wstats.deletions,
2758
+ writeRaceDeliveryDecision(store, decisionPath, {
2759
+ decision: result.decision,
2760
+ facts,
2761
+ reviewVerified: actualReviewVerified,
2762
+ finalVerify,
2763
+ deliveryFailureReason,
2764
+ deliveryReceiptPath: raceDeliveryReceipt ? "final/delivery_receipt.yaml" : null,
2765
+ });
2766
+ if (inPlaceWinner && requestedSingleCandidate && adopted === true) {
2767
+ revertAnchorId = await createRevertAnchorOrNull(execRoot, preTurnSha, postTurnSha);
2768
+ }
2769
+ store.writeYaml(join(paths.finalDir, "work_product.yaml"), {
2770
+ id: newId("wp"),
2771
+ kind: input.create === true ? "new_repo" : "patch",
2772
+ source_task_id: taskId,
2773
+ producer_attempt_id: winnerRun.attemptId,
2774
+ ...(raceDeliveryReceipt
2775
+ ? { files: { delivery_receipt: "final/delivery_receipt.yaml" } }
2776
+ : {}),
2777
+ meta: {
2778
+ harness_id: winnerRun.harnessId,
2779
+ synthesis: synth,
2780
+ mode,
2781
+ // Artifact-only apply reads the same terminal axes as the daemon (D8).
2782
+ lifecycle: facts.lifecycle,
2783
+ outcome_facts: facts,
2784
+ review_verified: actualReviewVerified,
2785
+ budget_stopped: budgetStopped,
2786
+ patch_sha256: patchSha256,
2787
+ result_kind: resultKind,
2788
+ diffstat: {
2789
+ files: wstats.paths.length,
2790
+ additions: wstats.additions,
2791
+ deletions: wstats.deletions,
2792
+ },
2793
+ blockers,
2794
+ adopted,
2795
+ apply_state: applyState,
2796
+ pre_turn_sha: preTurnSha,
2797
+ post_turn_sha: postTurnSha,
2798
+ revert_anchor_id: revertAnchorId,
2800
2799
  },
2801
- blockers,
2802
- adopted,
2803
- apply_state: applyState,
2804
- pre_turn_sha: preTurnSha,
2805
- post_turn_sha: postTurnSha,
2806
- revert_anchor_id: revertAnchorId,
2807
- },
2808
- });
2800
+ });
2801
+ }
2809
2802
  store.writeText(join(paths.finalDir, "summary.md"), renderSummary(runId, mode, { ...result.decision, facts }, evidences, synth.reason, actualReviewVerified));
2810
2803
  // summary.md is a DIAGNOSTIC artifact only (V8/PLAN addendum 2): it no
2811
2804
  // longer carries primary-output authority. A clean applyable success or a
@@ -2906,7 +2899,9 @@ export class Orchestrator {
2906
2899
  });
2907
2900
  }
2908
2901
  }
2909
- log.emit("work_product.emitted", { winner: result.decision.winner });
2902
+ if (mutatingRun && winnerRun) {
2903
+ log.emit("work_product.emitted", { winner: result.decision.winner });
2904
+ }
2910
2905
  if (!isFailureTerminal) {
2911
2906
  log.emit("run.completed", { lifecycle: facts.lifecycle, facts, reason: facts.reason });
2912
2907
  }
@@ -2983,13 +2978,11 @@ export class Orchestrator {
2983
2978
  * panel settles (resolve OR reject).
2984
2979
  */
2985
2980
  reviewScoped(input) {
2986
- const reviewHome = new WorkspaceManager(input.cwd).readOnlyHomeEnv();
2987
2981
  return reviewCandidate({
2988
2982
  ...input,
2989
2983
  reviewerTimeoutMs: input.reviewerTimeoutMs ?? reviewerTimeoutMs(this.config(input.cwd)),
2990
2984
  transientRetryPolicy: input.transientRetryPolicy ?? transientRetryPolicy(this.config(input.cwd)),
2991
- env: reviewHome.env,
2992
- }).finally(() => reviewHome.dispose());
2985
+ });
2993
2986
  }
2994
2987
  async reviewRuns(runs, reviewers, reviewVerified, reviewDir, cwd, contract, store, paths, log, ledger, taskId, signal, reservationEstimateUsd) {
2995
2988
  const evidences = [];