@claudexor/orchestrator 3.9.7 → 3.10.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.
Files changed (38) hide show
  1. package/dist/account-resolution.d.ts +1 -1
  2. package/dist/account-resolution.d.ts.map +1 -1
  3. package/dist/account-resolution.js +8 -3
  4. package/dist/account-resolution.js.map +1 -1
  5. package/dist/candidateReview.d.ts +41 -0
  6. package/dist/candidateReview.d.ts.map +1 -0
  7. package/dist/candidateReview.js +200 -0
  8. package/dist/candidateReview.js.map +1 -0
  9. package/dist/credential-cooldown.d.ts.map +1 -1
  10. package/dist/credential-cooldown.js +28 -3
  11. package/dist/credential-cooldown.js.map +1 -1
  12. package/dist/index.d.ts +3 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +3 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/orchestrator.d.ts +2 -0
  17. package/dist/orchestrator.d.ts.map +1 -1
  18. package/dist/orchestrator.js +74 -140
  19. package/dist/orchestrator.js.map +1 -1
  20. package/dist/policyFindings.d.ts +1 -1
  21. package/dist/policyFindings.d.ts.map +1 -1
  22. package/dist/policyFindings.js +2 -2
  23. package/dist/policyFindings.js.map +1 -1
  24. package/dist/runFactsBuilder.d.ts.map +1 -1
  25. package/dist/runFactsBuilder.js +3 -0
  26. package/dist/runFactsBuilder.js.map +1 -1
  27. package/dist/runFactsReview.d.ts.map +1 -1
  28. package/dist/runFactsReview.js +5 -1
  29. package/dist/runFactsReview.js.map +1 -1
  30. package/dist/runSupport.d.ts +2 -1
  31. package/dist/runSupport.d.ts.map +1 -1
  32. package/dist/runSupport.js +3 -3
  33. package/dist/runSupport.js.map +1 -1
  34. package/dist/task-contract-builder.d.ts +1 -0
  35. package/dist/task-contract-builder.d.ts.map +1 -1
  36. package/dist/task-contract-builder.js +10 -0
  37. package/dist/task-contract-builder.js.map +1 -1
  38. package/package.json +17 -17
@@ -10,10 +10,11 @@ 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";
12
12
  import { policyFindings } from "./policyFindings.js";
13
+ import { reviewCandidateRuns, resolveEngineReview, unreviewedCandidateEvidence, evaluateUnreviewedConvergence, } from "./candidateReview.js";
13
14
  import { join } from "node:path";
14
15
  import { finalizePlanRun, runCouncilPlan, writePlanHarnessFailure, } from "./planRun.js";
15
16
  import { runPlannerAttempt as executePlannerAttempt, } from "./plannerAttempt.js";
16
- import { HarnessRunSpec, ModeKind as ModeKindSchema, QuotaSnapshot as QuotaSnapshotSchema, isBlocking, makeOutcomeFacts, strictifyOutputSchema, estimateEffectiveAuthRoute, } from "@claudexor/schema";
17
+ import { HarnessRunSpec, ModeKind as ModeKindSchema, QuotaSnapshot as QuotaSnapshotSchema, isBlocking, makeOutcomeFacts, reviewAllowsApply, workStateVetoes, strictifyOutputSchema, estimateEffectiveAuthRoute, } from "@claudexor/schema";
17
18
  import { globalConfigDir, loadConfig } from "@claudexor/config";
18
19
  import { acceptedTryOutput, AccessProfileIncompatibleError, AnswerAssembly, CLAUDEXOR_BROWSER_ARTIFACT_SUBDIR, countsAsAgentProgress, HarnessUnavailableError, summarizeDiffPaths as diffStats, withInactivityWatchdog, } from "@claudexor/core";
19
20
  import { assertRouteModelsAllowed, runModelGovernedRoute } from "./modelGovernance.js";
@@ -115,6 +116,8 @@ export class Orchestrator {
115
116
  throw new Error(`unknown mode: ${String(resolved.mode)}`);
116
117
  }
117
118
  const mode = parsedMode.data;
119
+ if (mode === "agent")
120
+ resolved.review = resolveEngineReview(resolved, this.deps);
118
121
  if (resolved.delegate === true && mode !== "agent") {
119
122
  throw new Error(`Delegate is an agent-only strategy (got mode=${mode})`);
120
123
  }
@@ -220,6 +223,8 @@ export class Orchestrator {
220
223
  * pre-announce throw with no artifacts (artifact clause).
221
224
  */
222
225
  async resolveReviewersWithArtifacts(input, log, store, paths, runId, taskId, mode) {
226
+ if (input.review === false)
227
+ return { reviewers: [] };
223
228
  try {
224
229
  // Auto-panel dropped knobs (reviewerEfforts) → ignored-settings channel (QA-070):
225
230
  const warn = (d) => void log.emit("review.preflight", { ignored_settings: [d] });
@@ -1917,11 +1922,12 @@ export class Orchestrator {
1917
1922
  // themselves inside their envelopes; only the read-only report modes
1918
1923
  // (explore/plan/readonly_audit) build and attach the compact atlas.
1919
1924
  const reviewDir = join(paths.root, "review-evidence");
1920
- writeEvidencePacket(reviewDir, {
1921
- userIntent: redactSecrets(input.prompt),
1922
- diff: "(per-candidate diffs are supplied to reviewers individually)\n",
1923
- tests: renderTestsEvidence(contract),
1924
- });
1925
+ if (contract.review_requested !== false)
1926
+ writeEvidencePacket(reviewDir, {
1927
+ userIntent: redactSecrets(input.prompt),
1928
+ diff: "(per-candidate diffs are supplied to reviewers individually)\n",
1929
+ tests: renderTestsEvidence(contract),
1930
+ });
1925
1931
  let adapters;
1926
1932
  try {
1927
1933
  // Best-of races the whole pool. The `log` is passed so an AUTO pool that
@@ -2450,7 +2456,11 @@ export class Orchestrator {
2450
2456
  const configuredFamilies = new Set(reviewers.map((r) => r.providerFamily)).size;
2451
2457
  if (reviewableRuns.length === 0 || reviewers.length === 0) {
2452
2458
  log.emit("review.skipped", {
2453
- reason: reviewers.length === 0 ? "no_reviewers" : "no_changes",
2459
+ reason: contract.review_requested === false
2460
+ ? "not_requested"
2461
+ : reviewers.length === 0
2462
+ ? "no_reviewers"
2463
+ : "no_changes",
2454
2464
  reviewable_candidates: reviewableRuns.length,
2455
2465
  configured_reviewers: reviewers.length,
2456
2466
  configured_provider_families: configuredFamilies,
@@ -2569,7 +2579,10 @@ export class Orchestrator {
2569
2579
  }
2570
2580
  let result;
2571
2581
  try {
2572
- result = arbitrate(evidences, arbitrationBudgetOptions(ledger));
2582
+ result = arbitrate(evidences, {
2583
+ ...arbitrationBudgetOptions(ledger),
2584
+ reviewRequested: contract.review_requested,
2585
+ });
2573
2586
  }
2574
2587
  catch (err) {
2575
2588
  // Arbitration throws end terminally with artifacts, never as an orphan.
@@ -2609,7 +2622,11 @@ export class Orchestrator {
2609
2622
  // budget reason IS a RunReason.
2610
2623
  const budgetTerminal = ledger.terminal();
2611
2624
  if (facts.lifecycle === "succeeded" && budgetTerminal) {
2612
- facts = makeOutcomeFacts("failed", { reason: budgetTerminal, noChanges: facts.noChanges });
2625
+ facts = makeOutcomeFacts("failed", {
2626
+ reason: budgetTerminal,
2627
+ noChanges: facts.noChanges,
2628
+ review_requested: contract.review_requested,
2629
+ });
2613
2630
  }
2614
2631
  // FinalVerifier blocks adoption until the patch and gates pass on a fresh base.
2615
2632
  let finalVerify = null;
@@ -2687,11 +2704,12 @@ export class Orchestrator {
2687
2704
  ? winnerEvidence.findings.filter((f) => isBlocking(f)).length
2688
2705
  : 0;
2689
2706
  const resultKind = hasDiff ? "patch" : winnerAnswer.length > 0 ? "answer" : "none";
2690
- // Only a fully verified, applyable success may auto-adopt; a not-verified
2691
- // or needs-decision terminal remains an inspectable artifact.
2707
+ // Adoption honors the frozen review policy and all independent checks.
2708
+ // Unfinished work or a needs-decision terminal remains inspectable.
2692
2709
  const adoptable = facts.lifecycle === "succeeded" &&
2693
- facts.review === "approved" &&
2694
- facts.checks !== "failed";
2710
+ reviewAllowsApply(facts) &&
2711
+ facts.checks !== "failed" &&
2712
+ !workStateVetoes(facts);
2695
2713
  let adopted = null;
2696
2714
  let applyState = "not_applied";
2697
2715
  let postTurnSha = null;
@@ -2800,8 +2818,9 @@ export class Orchestrator {
2800
2818
  kind: "summary",
2801
2819
  path: "final/summary.md",
2802
2820
  state: (facts.lifecycle === "succeeded" &&
2803
- facts.review === "approved" &&
2804
- facts.checks !== "failed") ||
2821
+ reviewAllowsApply(facts) &&
2822
+ facts.checks !== "failed" &&
2823
+ !workStateVetoes(facts)) ||
2805
2824
  winnerAnswer.length > 0
2806
2825
  ? "ready"
2807
2826
  : "diagnostic",
@@ -2980,124 +2999,26 @@ export class Orchestrator {
2980
2999
  });
2981
3000
  }
2982
3001
  async reviewRuns(runs, reviewers, reviewVerified, reviewDir, cwd, contract, store, paths, log, ledger, taskId, signal, reservationEstimateUsd) {
2983
- const evidences = [];
2984
- for (const run of runs) {
2985
- const candidateCwd = run.reviewCwd ?? cwd;
2986
- const candidateEvidenceDir = this.prepareReviewEvidenceDir(reviewDir, candidateCwd);
2987
- try {
2988
- writeText(join(candidateEvidenceDir, "TESTS.txt"), renderTestsEvidence(contract, run.gates).trim() + "\n");
2989
- // a candidate that changed NO files has nothing to review — never
2990
- // spend a reviewer panel on "(empty diff)" (a trivial greeting in agent mode used to
2991
- // cost two reviewers). It still flows through policy gates and arbitration
2992
- // (so a failing test gate or no_op outcome is unchanged), just unreviewed.
2993
- const hasDiff = run.diff.trim().length > 0;
2994
- // Reviewer panels spend real money: reserve before, settle the observed cost.
2995
- const reviewLease = hasDiff && reviewers.length > 0
2996
- ? ledger?.reserve({
2997
- taskId: taskId ?? "task",
2998
- attemptId: run.attemptId,
2999
- intent: "review",
3000
- harnessId: "review-panel",
3001
- cost: attemptCostEvidence("review-panel", run.attemptId, reservationEstimateUsd),
3002
- })
3003
- : undefined;
3004
- const result = hasDiff && reviewers.length > 0 && (reviewLease?.granted ?? true)
3005
- ? await this.reviewScoped({
3006
- candidateLabel: run.label,
3007
- diff: run.diff,
3008
- evidenceDir: candidateEvidenceDir,
3009
- artifactsDir: join(paths.reviewsDir, `${run.attemptId}-reviewers`),
3010
- cwd: candidateCwd,
3011
- reviewers,
3012
- reviewerTimeoutMs: reviewerTimeoutMs(this.config(contract.repo.root)),
3013
- envInheritance: envInheritance(this.config(cwd)),
3014
- signal,
3015
- onReviewerEvent: (event) => log.emit(event.type, { ...event }),
3016
- })
3017
- : {
3018
- findings: [],
3019
- routeProofs: [],
3020
- reviewerRequests: [],
3021
- crossFamilyHealthy: false,
3022
- healthyProviders: [],
3023
- crossFamilyVerified: false,
3024
- distinctProviders: [],
3025
- reviewSpendUsd: 0,
3026
- reviewSpendEstimated: false,
3027
- reviewCashUsd: 0,
3028
- reviewCashKnowledge: "unknown",
3029
- reviewValuationUsd: 0,
3030
- reviewValuationKnowledge: "unknown",
3031
- reviewUnknownUsd: 0,
3032
- };
3033
- if (reviewLease?.granted) {
3034
- ledger?.settle(reviewLease.lease?.lease_id ?? "", reviewUsageCostSettlement(result.reviewCashUsd, result.reviewValuationUsd, {
3035
- cash: result.reviewCashKnowledge,
3036
- valuation: result.reviewValuationKnowledge,
3037
- }, [`attempt:${run.attemptId}`, "review:panel"], result.reviewUnknownUsd));
3038
- if ((result.reviewSpendUsd ?? 0) > 0) {
3039
- log.emit("budget.observation", {
3040
- harness_id: "review-panel",
3041
- attempt_id: run.attemptId,
3042
- kind: "spend",
3043
- usd: result.reviewSpendUsd,
3044
- cash_usd: result.reviewCashUsd,
3045
- valuation_usd: result.reviewValuationUsd,
3046
- unknown_usd: result.reviewUnknownUsd,
3047
- estimated: result.reviewSpendEstimated === true,
3048
- });
3049
- }
3050
- }
3051
- else if (reviewLease && !reviewLease.granted) {
3052
- log.emit("budget.lease.created", {
3053
- granted: false,
3054
- reason: reviewLease.reason,
3055
- attempt_id: run.attemptId,
3056
- harness_id: "review-panel",
3057
- });
3058
- }
3059
- const revalidated = await revalidateFindings(result.findings, {
3060
- candidateRoot: candidateCwd,
3061
- evidenceDir: candidateEvidenceDir,
3062
- });
3063
- // The high-risk human gate must key off the ACTUAL cross-family verification
3064
- // (stream-observed route proofs), not the preliminary routeVerified (families
3065
- // merely configured). Otherwise a high-risk diff skips its NEEDS_HUMAN gate
3066
- // when two families were configured but their route proofs went unverified.
3067
- // Mirrors the convergence path (actualReviewVerified).
3068
- const candidateReviewVerified = reviewVerified && result.crossFamilyHealthy && result.crossFamilyVerified;
3069
- // Typed policy gate (risk + protected paths) merges with reviewer findings.
3070
- const policy = policyFindings(run, candidateReviewVerified, contract.constraints.protected_paths, contract.constraints.auto_protected_paths, contract.constraints.protected_path_approvals, contract.constraints.deny_paths);
3071
- const allFindings = [...policy.findings, ...revalidated];
3072
- const inconclusive = allFindings.some((f) => f.severity === "INSUFFICIENT_EVIDENCE" || f.status === "insufficient_evidence");
3073
- const noBlockers = !allFindings.some((f) => isBlocking(f));
3074
- const reviewClean = result.crossFamilyHealthy && result.crossFamilyVerified && noBlockers && !inconclusive;
3075
- store.writeYaml(join(paths.reviewsDir, `${run.attemptId}.yaml`), {
3076
- attempt_id: run.attemptId,
3077
- review_verified: candidateReviewVerified,
3078
- final_review_clean: reviewClean,
3079
- cross_family_healthy: result.crossFamilyHealthy,
3080
- cross_family_verified: result.crossFamilyVerified,
3081
- healthy_providers: result.healthyProviders,
3082
- verified_providers: result.distinctProviders,
3083
- reviewer_requests: result.reviewerRequests,
3084
- risk: policy.risk,
3085
- findings: allFindings,
3086
- route_proofs: result.routeProofs,
3087
- });
3088
- for (const f of allFindings)
3089
- log.emit("finding.revalidated", {
3090
- attempt_id: run.attemptId,
3091
- severity: f.severity,
3092
- status: f.status,
3093
- });
3094
- evidences.push(toCandidateEvidence(run, contract, allFindings, reviewClean, candidateReviewVerified));
3095
- }
3096
- finally {
3097
- this.recordReviewEvidenceCleanup(store, join(paths.reviewsDir, `${run.attemptId}-evidence-cleanup.yaml`), run.attemptId, candidateEvidenceDir, candidateCwd);
3098
- }
3099
- }
3100
- return evidences;
3002
+ return reviewCandidateRuns({
3003
+ runs,
3004
+ reviewers,
3005
+ reviewVerified,
3006
+ reviewDir,
3007
+ cwd,
3008
+ contract,
3009
+ store,
3010
+ paths,
3011
+ log,
3012
+ ledger,
3013
+ taskId,
3014
+ signal,
3015
+ reservationEstimateUsd,
3016
+ }, {
3017
+ prepareReviewEvidenceDir: this.prepareReviewEvidenceDir.bind(this),
3018
+ recordReviewEvidenceCleanup: this.recordReviewEvidenceCleanup.bind(this),
3019
+ reviewScoped: this.reviewScoped.bind(this),
3020
+ config: this.config.bind(this),
3021
+ });
3101
3022
  }
3102
3023
  prepareReviewEvidenceDir(sourceDir, _candidateCwd) {
3103
3024
  // Evidence is an external runtime artifact. ReviewEngine builds a separate
@@ -3191,11 +3112,12 @@ export class Orchestrator {
3191
3112
  this.ensureClaudeBridgeForRun(input.repoRoot, false, log);
3192
3113
  }
3193
3114
  const reviewDir = join(paths.root, "review-evidence");
3194
- writeEvidencePacket(reviewDir, {
3195
- userIntent: redactSecrets(input.prompt),
3196
- diff: "(per-attempt)\n",
3197
- tests: renderTestsEvidence(contract),
3198
- });
3115
+ if (contract.review_requested !== false)
3116
+ writeEvidencePacket(reviewDir, {
3117
+ userIntent: redactSecrets(input.prompt),
3118
+ diff: "(per-attempt)\n",
3119
+ tests: renderTestsEvidence(contract),
3120
+ });
3199
3121
  const reviewersOutcome = await this.resolveReviewersWithArtifacts(input, log, store, paths, runId, taskId, mode);
3200
3122
  if ("failed" in reviewersOutcome)
3201
3123
  return reviewersOutcome.failed;
@@ -3464,6 +3386,14 @@ export class Orchestrator {
3464
3386
  let conv;
3465
3387
  try {
3466
3388
  conv = await (async () => {
3389
+ if (contract.review_requested === false) {
3390
+ const evidence = unreviewedCandidateEvidence(run, contract, store, paths, log);
3391
+ lastFindings = evidence.findings;
3392
+ lastFinalReviewClean = false;
3393
+ actualReviewVerified = false;
3394
+ log.emit("review.skipped", { reason: "not_requested", attempt_id: attemptId });
3395
+ return evaluateUnreviewedConvergence(evidence, contract);
3396
+ }
3467
3397
  const candidateReviewCwd = run.reviewCwd ?? input.repoRoot;
3468
3398
  const candidateReviewEvidenceDir = this.prepareReviewEvidenceDir(reviewDir, candidateReviewCwd);
3469
3399
  try {
@@ -3705,7 +3635,7 @@ export class Orchestrator {
3705
3635
  if (lastRun && !interrupted) {
3706
3636
  const arb = arbitrate([
3707
3637
  toCandidateEvidence(lastRun, contract, lastFindings, lastFinalReviewClean, actualReviewVerified),
3708
- ], arbitrationBudgetOptions(ledger));
3638
+ ], { ...arbitrationBudgetOptions(ledger), reviewRequested: contract.review_requested });
3709
3639
  decision = arb.decision;
3710
3640
  store.writeYaml(join(paths.arbitrationDir, "decision.yaml"), decision);
3711
3641
  // A converged run adopts the arbitration axes (checks/review); an
@@ -3724,6 +3654,7 @@ export class Orchestrator {
3724
3654
  facts = makeOutcomeFacts("failed", {
3725
3655
  reason: convBudgetTerminal,
3726
3656
  noChanges: facts.noChanges,
3657
+ review_requested: contract.review_requested,
3727
3658
  });
3728
3659
  }
3729
3660
  // A reviewer escalation to a human forces the REVIEW axis to blocked.
@@ -3774,7 +3705,10 @@ export class Orchestrator {
3774
3705
  // convergence run with inPlace mutated the live tree directly across its
3775
3706
  // attempts, so it is "applied" even when review blocked (Revert offered).
3776
3707
  const convHasDiff = lastRun.diff.trim().length > 0;
3777
- const convAdoptable = facts.lifecycle === "succeeded" && facts.review === "approved" && facts.checks !== "failed";
3708
+ const convAdoptable = facts.lifecycle === "succeeded" &&
3709
+ reviewAllowsApply(facts) &&
3710
+ facts.checks !== "failed" &&
3711
+ !workStateVetoes(facts);
3778
3712
  const convAdopted = input.inPlace === true && convHasDiff ? true : null;
3779
3713
  const convApplyState = convAdopted === true
3780
3714
  ? convAdoptable