@claudexor/orchestrator 3.1.0 → 3.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.
@@ -12,11 +12,13 @@ import { HarnessRunSpec, ModeKind as ModeKindSchema, SCHEMA_VERSION, TRUST_FULL_
12
12
  import { globalConfigDir, loadConfig, trustConfigPath } from "@claudexor/config";
13
13
  import { AnswerAssembly, CLAUDEXOR_ARTIFACT_DIR, CLAUDEXOR_BROWSER_ARTIFACT_SUBDIR, HarnessUnavailableError, summarizeDiffPaths as diffStats, withInactivityWatchdog, } from "@claudexor/core";
14
14
  import { assertRouteModelsAllowed } from "./modelGovernance.js";
15
+ import { governRouteEffort } from "./effortGovernance.js";
15
16
  import { RequestRequirementsResolver } from "./requestRequirements.js";
17
+ import { buildRevisePrompt } from "./revisePrompt.js";
16
18
  import { cancelledResult, failTerminally, guardAnnouncedRun, writeFailure, } from "./runTerminals.js";
17
19
  import { budgetFailureRecord, classifyBudgetFailure } from "./budgetFailure.js";
18
20
  import { assertOutputSchemaCompiles, finalizeStructuredOutput } from "./structuredOutput.js";
19
- import { transientRetryDelayMs, promptWithEngineConstraints, sleep, redactHarnessEvent, harnessEventPayload, formatFindings, renderSummary, observeBudgetSignals, rotateOnStall, recordCleanAttemptMetrics, envInheritance, transientRetryPolicy, reviewerTimeoutMs, harnessInactivityTimeoutMs, observeAuthSwitch, emitPrimaryDivergence, emitPoolDegraded, deliveryRefusalFailure, writeRaceDeliveryDecision, } from "./runSupport.js";
21
+ import { transientRetryDelayMs, promptWithEngineConstraints, sleep, redactHarnessEvent, harnessEventPayload, safeErrorMessage, renderSummary, observeBudgetSignals, rotateOnStall, recordCleanAttemptMetrics, envInheritance, transientRetryPolicy, reviewerTimeoutMs, harnessInactivityTimeoutMs, observeAuthSwitch, emitPrimaryDivergence, emitPoolDegraded, deliveryRefusalFailure, writeRaceDeliveryDecision, } from "./runSupport.js";
20
22
  import { candidateStatusInRouteContext, resolveReadOnlyRouteContext, } from "./routeContext.js";
21
23
  import { resolveAutoReviewerPanel, resolveExplicitReviewerPanel } from "./reviewerPanel.js";
22
24
  import { buildContinuation, } from "./continuity.js";
@@ -25,7 +27,7 @@ import { runDiffReview } from "./diffReview.js";
25
27
  import { rawScoutBundle, resolveDeepScanSynthesis, } from "./deepScanReducer.js";
26
28
  import { classifyAdapterThrow, createAttemptTelemetry, observeAttemptTelemetry, setAttemptOutcome, telemetrySummary, toolWarnings, unrecoveredToolErrors, webUnsatisfied, } from "./attemptTelemetry.js";
27
29
  import { dominantHarnessFailureCategory, harnessFailureNextActions } from "./harnessFailure.js";
28
- import { finalizeAttempt, readOnlyNoSuccessTerminal, resolveWorkReportEnvelope, unwrapWorkReportEnvelope, } from "./attemptFinalize.js";
30
+ import { finalizeAttempt, readOnlyNoSuccessTerminal, resolveWorkReportEnvelope, unrecoveredToolErrorFailure, unwrapWorkReportEnvelope, webEvidenceFailure, } from "./attemptFinalize.js";
29
31
  import { buildContinuationPacket, decideContinuation, synthesizeContinuationRequest, } from "./continuation.js";
30
32
  import { interactionChannelFor } from "./interaction.js";
31
33
  import { gateSpecsFromContract, renderTestsEvidence, resolveContractGates, } from "./contract-gates.js";
@@ -40,6 +42,7 @@ import { arbitrate } from "@claudexor/arbitration";
40
42
  import { buildSynthesisPlan, decideSynthesis } from "@claudexor/synthesis";
41
43
  import { attemptCostEvidence, attemptUsageCostSettlement, BudgetLedger, isBudgetTerminal, explainRanking, loadHarnessMetrics, promptFingerprint, unknownCostSettlement, rankHarnesses, reviewUsageCostSettlement, } from "@claudexor/budget";
42
44
  import { readTextSafe, appendLine, assertNoInlineSecretValues, containsSecretLikeToken, DELEGATION_ENV, hashJson, newId, noProjectRepoRoot, nowIso, redactSecrets, safeInvoke, sha256, userConfigDir, writeText, } from "@claudexor/util";
45
+ import { assertWriteIsolation } from "./write-isolation.js";
43
46
  /** A routed candidate adapter plus its manifest capabilities and user settings. */
44
47
  /** The two access profiles that map to codex `danger-full-access` / an
45
48
  * unsandboxed lane — the only ones under which a full-access-requiring MCP
@@ -134,13 +137,15 @@ export class Orchestrator {
134
137
  throw new Error(`unknown mode: ${String(resolved.mode)}`);
135
138
  }
136
139
  const mode = parsedMode.data;
137
- // denyPaths is enforced by the post-diff policy gate BEFORE delivery, which
138
- // only exists on envelope/isolated runs — an in-place run mutates the live
139
- // tree directly, so the gate could not contain a violation. Refuse loudly
140
- // rather than accept a knob the engine cannot honor (INV-023).
141
- if ((resolved.denyPaths?.length ?? 0) > 0 && resolved.inPlace === true) {
142
- throw new Error("denyPaths requires an isolated/envelope run: the post-diff policy gate blocks a violating patch before delivery, which an in-place run cannot guarantee; drop --deny-path or run isolated");
143
- }
140
+ const projectProtectedPaths = mode === "agent" ? this.projectConfig(resolved.repoRoot).constraints.protected_paths : [];
141
+ assertWriteIsolation({
142
+ mode,
143
+ protectedPaths: projectProtectedPaths,
144
+ denyPaths: resolved.denyPaths,
145
+ inPlace: resolved.inPlace,
146
+ repoRoot: resolved.repoRoot,
147
+ executionRoot: this.execRootOf(resolved),
148
+ });
144
149
  // outputSchema constrains the run's final ANSWER. It is honored exactly
145
150
  // where a final answer is delivered (agent race incl. synthesis, and ask);
146
151
  // every other strategy refuses loudly rather than carrying a contract the
@@ -204,7 +209,7 @@ export class Orchestrator {
204
209
  // writer ran, which is the leak this closes).
205
210
  (runId) => this.routingRationaleByRun.delete(runId));
206
211
  }
207
- async resolveReviewers(cwd, runAuthPreference) {
212
+ async resolveReviewers(cwd, runAuthPreference, onIgnoredSetting) {
208
213
  if (this.deps.reviewers)
209
214
  return this.deps.reviewers;
210
215
  if (this.deps.reviewerPanel && this.deps.reviewerPanel.length > 0) {
@@ -215,10 +220,8 @@ export class Orchestrator {
215
220
  registry: this.deps.registry,
216
221
  harnessSettings: this.config(cwd)?.global.harnesses ?? {},
217
222
  authPreferenceFor: (id) => this.authPreferenceForHarness(cwd, id, runAuthPreference),
218
- }, {
219
- reviewerModels: this.deps.reviewerModels,
220
- reviewerEfforts: this.deps.reviewerEfforts,
221
- });
223
+ onIgnoredSetting,
224
+ }, { reviewerModels: this.deps.reviewerModels, reviewerEfforts: this.deps.reviewerEfforts });
222
225
  }
223
226
  /**
224
227
  * Resolve reviewers INSIDE a strategy, after run-dir creation: an explicit
@@ -229,7 +232,9 @@ export class Orchestrator {
229
232
  */
230
233
  async resolveReviewersWithArtifacts(input, log, store, paths, runId, taskId, mode) {
231
234
  try {
232
- return { reviewers: await this.resolveReviewers(input.repoRoot, input.authPreference) };
235
+ // Auto-panel dropped knobs (reviewerEfforts) → ignored-settings channel (QA-070):
236
+ const warn = (d) => void log.emit("review.preflight", { ignored_settings: [d] });
237
+ return { reviewers: await this.resolveReviewers(input.repoRoot, input.authPreference, warn) };
233
238
  }
234
239
  catch (err) {
235
240
  const message = safeErrorMessage(err);
@@ -917,13 +922,13 @@ export class Orchestrator {
917
922
  intent,
918
923
  qualityTiers: config.routing.quality_tiers,
919
924
  ledger: routeLedger,
925
+ now: Date.now(), // ONE instant for the sort AND the rationale below
920
926
  };
921
927
  const ranked = rankHarnesses(remaining, routeCtx)
922
928
  .map((candidate) => byId.get(candidate.harnessId))
923
929
  .filter((candidate) => Boolean(candidate));
924
- // QA-034: record the typed rationale ONCE at pool ordering (run evidence,
925
- // not an event). Axis-aligned with rankHarnesses above so the persisted
926
- // reason can never disagree with the order actually taken.
930
+ // QA-034: the rationale is run evidence recorded ONCE at pool ordering,
931
+ // pinned to routeCtx.now so it cannot disagree with the order just taken.
927
932
  if (runId)
928
933
  this.routingRationaleByRun.set(runId, explainRanking(remaining, routeCtx));
929
934
  ordered = ranked;
@@ -1067,7 +1072,7 @@ export class Orchestrator {
1067
1072
  projectCommands: cfg.tests?.commands ?? [],
1068
1073
  });
1069
1074
  const commands = resolvedGates.commands;
1070
- const protectedPaths = [];
1075
+ const protectedPaths = [...new Set(cfg.constraints.protected_paths)];
1071
1076
  const autoProtectedPaths = resolvedGates.autoProtectedPaths;
1072
1077
  const protectedPathApprovals = [
1073
1078
  ...new Map([...(input.protectedPathApprovals ?? [])].map((approval) => [approval.path, approval])).values(),
@@ -1278,17 +1283,16 @@ export class Orchestrator {
1278
1283
  // per-harness map, which beats the per-harness settings default. There is
1279
1284
  // no run-global model.
1280
1285
  const model = overrideModel ?? contract.routing_models[routed.adapter.id] ?? s?.defaultModel ?? null;
1281
- // Effort disclosure (INV-105): a requested effort on a harness with no
1282
- // declared ladder is DISCLOSED as ignored, never silently dropped.
1283
- // Harness-scoped resolution mirrors the model line above: the contract's
1284
- // FROZEN per-lane effort (QA-035) is authoritative so Exact Retry replays it
1285
- // without re-reading settings; a per-attempt `effortHint` (or settings
1286
- // default) applies only to a lane the contract did not freeze.
1287
- let effort = contract.routing_efforts[routed.adapter.id] ?? effortHint ?? s?.effort ?? null;
1288
- if (effort && routed.effortLevels.length === 0) {
1289
- ignored.push(`effort=${effort} (manifest capabilities.effort_levels is empty for ${routed.adapter.id})`);
1290
- effort = null;
1291
- }
1286
+ // Effort disclosure (INV-105) against the harness's advertised ladder. This
1287
+ // gate only DISCLOSES an unplaceable level; the clamp belongs to the adapter,
1288
+ // which resolves against the catalog for the profile env the child runs in
1289
+ // (the manifest here is the DEFAULT account's see effortGovernance.ts). The
1290
+ // contract's FROZEN per-lane effort (QA-035) wins so Exact Retry replays it
1291
+ // without re-reading settings; `effortHint`/settings apply only to an unfrozen lane.
1292
+ const governed = governRouteEffort(contract.routing_efforts[routed.adapter.id] ?? effortHint ?? s?.effort ?? null, { id: routed.adapter.id, ...routed });
1293
+ const effort = governed.effort;
1294
+ if (governed.ignored)
1295
+ ignored.push(governed.ignored);
1292
1296
  return {
1293
1297
  model,
1294
1298
  effort,
@@ -1721,7 +1725,7 @@ export class Orchestrator {
1721
1725
  }
1722
1726
  const attemptStreamEndedMs = Date.now();
1723
1727
  if (webUnsatisfied(telemetry)) {
1724
- errors.push(`web evidence unsatisfied: ${telemetry.web.errorSummary ?? (telemetry.web.attempted ? "web tool failed without verified recovery" : "web evidence required but never attempted")}`);
1728
+ errors.push(webEvidenceFailure(telemetry.web));
1725
1729
  }
1726
1730
  const diff = await wsm.diff(envelope);
1727
1731
  // D-16: un-nest {work_report, output} so answer.md persists the OUTPUT, not the envelope.
@@ -3436,7 +3440,7 @@ export class Orchestrator {
3436
3440
  : "";
3437
3441
  const prompt = attempt === 1
3438
3442
  ? input.prompt
3439
- : `${input.prompt}\n\nThe previous attempt did not converge. Address these review findings (verify each against the code; fix valid ones, rebut invalid ones with evidence):\n${formatFindings(lastFindings)}${runtimeErrors}`;
3443
+ : buildRevisePrompt(input.prompt, lastFindings, runtimeErrors);
3440
3444
  // Loop detection (budget router): the 3rd identical repair prompt means
3441
3445
  // findings/errors are not changing — stop burning paid attempts.
3442
3446
  const fingerprint = promptFingerprint(prompt);
@@ -4208,20 +4212,22 @@ export class Orchestrator {
4208
4212
  input.signal?.removeEventListener("abort", onAbort);
4209
4213
  ledger.settle(lease.lease?.lease_id ?? "", attemptUsageCostSettlement(cost, costEstimated, attemptId, adapter.id, telemetry.authMode, telemetry.usageCost));
4210
4214
  }
4211
- const unrecovered = unrecoveredToolErrors(telemetry);
4212
- const webBlocked = webUnsatisfied(telemetry);
4213
- if (!harnessError && webBlocked) {
4214
- harnessError = `web evidence unsatisfied: ${telemetry.web.errorSummary ?? (telemetry.web.attempted ? "web tool failed without verified recovery" : "web evidence required but never attempted")}`;
4215
- }
4216
- if (!harnessError && unrecovered.length > 0) {
4217
- const first = unrecovered[0];
4218
- harnessError = `${first.tool} failed without recovery: ${first.summary}`;
4219
- }
4220
4215
  // D-16: unwrap and require PLAN TEXT — a plan with no text is not delivered.
4216
+ // The unwrap runs BEFORE the error axes: the deliverable it yields is what
4217
+ // decides whether an unrecovered tool error is fatal (explorer parity).
4221
4218
  const planUnwrapped = unwrapWorkReportEnvelope(answer.machineText() ?? "", planWorkMode, {
4222
4219
  sideToolReport: telemetry.sideToolWorkReport ?? undefined,
4223
4220
  });
4224
4221
  const planText = redactSecrets(planUnwrapped.deliverable).trim();
4222
+ const unrecovered = unrecoveredToolErrors(telemetry);
4223
+ const webBlocked = webUnsatisfied(telemetry);
4224
+ if (!harnessError && webBlocked) {
4225
+ harnessError = webEvidenceFailure(telemetry.web);
4226
+ }
4227
+ // INV-043/INV-044, explorer parity: a DELIVERED plan keeps an unrecovered
4228
+ // non-web tool error as warning evidence instead of discarding the plan (see
4229
+ // the helper). Web keeps its hard gate above; the finalizer outranks both.
4230
+ harnessError ??= unrecoveredToolErrorFailure(unrecovered, planText.length > 0);
4225
4231
  const planFinalized = finalizeAttempt({
4226
4232
  deliverableEvidence: planText.length > 0,
4227
4233
  harnessErrored: harnessError !== null && !webBlocked,
@@ -5112,12 +5118,9 @@ export class Orchestrator {
5112
5118
  const webBlocked = webUnsatisfied(telemetry);
5113
5119
  const reportPresent = report.length > 0;
5114
5120
  if (!harnessError && webBlocked) {
5115
- harnessError = `web evidence unsatisfied: ${telemetry.web.errorSummary ?? (telemetry.web.attempted ? "web tool failed without verified recovery" : "web evidence required but never attempted")}`;
5116
- }
5117
- if (!harnessError && unrecovered.length > 0 && !reportPresent) {
5118
- const first = unrecovered[0];
5119
- harnessError = `${first.tool} failed without recovery: ${first.summary}`;
5121
+ harnessError = webEvidenceFailure(telemetry.web);
5120
5122
  }
5123
+ harnessError ??= unrecoveredToolErrorFailure(unrecovered, reportPresent);
5121
5124
  const roFinalized = finalizeAttempt({
5122
5125
  deliverableEvidence: reportPresent,
5123
5126
  harnessErrored: harnessError !== null && !webBlocked,
@@ -5696,7 +5699,4 @@ function assertNoSecretLikeTokens(label, text) {
5696
5699
  throw new Error(`${label} contains secret-like token; refusing to persist artifact`);
5697
5700
  }
5698
5701
  }
5699
- function safeErrorMessage(err) {
5700
- return redactSecrets(err instanceof Error ? err.message : String(err));
5701
- }
5702
5702
  //# sourceMappingURL=orchestrator.js.map