@lsctech/polaris 0.4.9 → 0.5.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 (42) hide show
  1. package/dist/autoresearch/gates.js +47 -0
  2. package/dist/autoresearch/proposal.js +183 -1
  3. package/dist/autoresearch/score.js +276 -0
  4. package/dist/cli/init.js +17 -0
  5. package/dist/cluster-state/store.js +70 -1
  6. package/dist/config/defaults.js +25 -0
  7. package/dist/config/validator.js +390 -0
  8. package/dist/finalize/artifact-policy.js +3 -1
  9. package/dist/finalize/index.js +73 -17
  10. package/dist/finalize/linear.js +45 -0
  11. package/dist/finalize/run-report.js +68 -12
  12. package/dist/loop/adapters/agent-subtask.js +19 -0
  13. package/dist/loop/adapters/cli-subtask-bridge.js +0 -1
  14. package/dist/loop/adapters/terminal-cli.js +194 -14
  15. package/dist/loop/checkpoint.js +40 -0
  16. package/dist/loop/dispatch-boundary.js +29 -0
  17. package/dist/loop/dispatch.js +193 -22
  18. package/dist/loop/orphan-recovery.js +56 -0
  19. package/dist/loop/parent.js +197 -22
  20. package/dist/loop/router/engine.js +229 -0
  21. package/dist/loop/router/index.js +5 -0
  22. package/dist/loop/router/types.js +2 -0
  23. package/dist/loop/worker-packet.js +16 -0
  24. package/dist/qc/artifacts.js +117 -0
  25. package/dist/qc/attribution.js +266 -0
  26. package/dist/qc/autofix.js +77 -0
  27. package/dist/qc/index.js +30 -0
  28. package/dist/qc/orchestration.js +150 -0
  29. package/dist/qc/policy.js +70 -0
  30. package/dist/qc/provider.js +28 -0
  31. package/dist/qc/providers/coderabbit.js +214 -0
  32. package/dist/qc/providers/index.js +5 -0
  33. package/dist/qc/registry.js +16 -0
  34. package/dist/qc/routing.js +55 -0
  35. package/dist/qc/runner.js +137 -0
  36. package/dist/qc/schemas.js +110 -0
  37. package/dist/qc/security-category.js +11 -0
  38. package/dist/qc/severity.js +78 -0
  39. package/dist/qc/triggers.js +92 -0
  40. package/dist/qc/types.js +9 -0
  41. package/dist/runtime/scheduling/child-selector.js +81 -0
  42. package/package.json +1 -1
@@ -36,11 +36,15 @@ const body_parser_js_1 = require("./body-parser.js");
36
36
  const node_child_process_1 = require("node:child_process");
37
37
  const artifact_policy_js_1 = require("../finalize/artifact-policy.js");
38
38
  const dispatch_boundary_js_1 = require("./dispatch-boundary.js");
39
+ const triggers_js_1 = require("../qc/triggers.js");
39
40
  const run_bootstrap_js_1 = require("./run-bootstrap.js");
40
41
  const ledger_js_1 = require("./ledger.js");
41
42
  const dispatch_js_1 = require("./dispatch.js");
42
- const index_js_1 = require("../tracker/index.js");
43
+ const index_js_1 = require("./router/index.js");
44
+ const child_selector_js_1 = require("../runtime/scheduling/child-selector.js");
45
+ const index_js_2 = require("../tracker/index.js");
43
46
  const lifecycle_transition_js_1 = require("../tracker/lifecycle-transition.js");
47
+ const local_graph_js_1 = require("../tracker/local-graph.js");
44
48
  const CLAIM_TTL_MS = 30 * 60 * 1000;
45
49
  /**
46
50
  * Returns the list of files touched by a git commit, or null when the commit
@@ -130,13 +134,6 @@ function emitBootstrapContextSize(telemetryFile, runId, childId, stateFile, pack
130
134
  timestamp: new Date().toISOString(),
131
135
  });
132
136
  }
133
- /**
134
- * Select the next open child that is not Done/blocked.
135
- * Returns null when all children are completed.
136
- */
137
- function selectNextChild(state) {
138
- return state.open_children[0] ?? null;
139
- }
140
137
  /**
141
138
  * Returns true when the given child is detected as an analyze issue.
142
139
  * Detection is intentionally conservative: title prefix or label match only.
@@ -212,7 +209,7 @@ function appendRunStartedLedgerEvent(repoRoot, state) {
212
209
  timestamp: new Date().toISOString(),
213
210
  });
214
211
  }
215
- function appendChildCompletedLedgerEvent(repoRoot, state, completedChild, lastCommit, validation) {
212
+ function appendChildCompletedLedgerEvent(repoRoot, state, completedChild, lastCommit, validation, completion) {
216
213
  const writer = new ledger_js_1.LedgerWriter((0, node_path_1.join)(repoRoot, ledger_js_1.DEFAULT_LEDGER_PATH));
217
214
  const base = {
218
215
  schema_version: 1,
@@ -237,6 +234,18 @@ function appendChildCompletedLedgerEvent(repoRoot, state, completedChild, lastCo
237
234
  validation: typeof validation === "object" && validation !== null
238
235
  ? { status: "complete", ...validation }
239
236
  : { status: "complete" },
237
+ ...(completion?.dispatchId ? { dispatch_id: completion.dispatchId } : {}),
238
+ provider: completion?.provider ?? null,
239
+ model: completion?.model ?? null,
240
+ ...(completion?.elapsedSeconds !== undefined ? { elapsed_seconds: completion.elapsedSeconds } : {}),
241
+ ...(completion?.commitFiles !== undefined ? { commit_files: completion.commitFiles } : {}),
242
+ ...(completion?.completionStatus ? { completion_status: completion.completionStatus } : {}),
243
+ ...(completion?.routerSelectionReason
244
+ ? { router_selection_reason: completion.routerSelectionReason }
245
+ : {}),
246
+ ...(completion?.providersTried?.length
247
+ ? { providers_tried: completion.providersTried }
248
+ : {}),
240
249
  });
241
250
  }
242
251
  function appendClusterCompletedLedgerEvent(repoRoot, state) {
@@ -275,7 +284,7 @@ function appendClusterCompletedLedgerEvent(repoRoot, state) {
275
284
  * @param resultFile - Optional override path where the worker should write its result
276
285
  * @returns The compiled WorkerPacket configured for `activeChild`
277
286
  */
278
- function buildPacket(state, activeChild, stateFile, telemetryFile, repoRoot, resultFile) {
287
+ function buildPacket(state, activeChild, stateFile, telemetryFile, repoRoot, resultFile, maxConcurrentWorkers) {
279
288
  const branch = state.branch ?? getCurrentBranch(repoRoot);
280
289
  const childMeta = state.open_children_meta?.[activeChild];
281
290
  // Hydrate body from cluster snapshot when runtime state lacks it.
@@ -312,7 +321,7 @@ function buildPacket(state, activeChild, stateFile, telemetryFile, repoRoot, res
312
321
  telemetryFile,
313
322
  issueContext,
314
323
  allowedScope: resolvedScope.length > 0 ? resolvedScope : undefined,
315
- maxConcurrentWorkers: 1,
324
+ maxConcurrentWorkers,
316
325
  promptMode,
317
326
  resultFile,
318
327
  });
@@ -416,6 +425,8 @@ function buildParentDispatchRecord(args) {
416
425
  packet_path: args.packetPath,
417
426
  expected_result_path: args.resultPath,
418
427
  provider: args.provider,
428
+ provider_selection_reason: args.providerSelectionReason,
429
+ providers_tried: args.providersTried,
419
430
  dispatched_at: args.dispatchedAt,
420
431
  status: "dispatched",
421
432
  dispatch_mode: "direct-worker",
@@ -487,6 +498,26 @@ function advanceState(state, completedChild, lastCommit) {
487
498
  },
488
499
  };
489
500
  }
501
+ function withWorkerPoolState(state, maxConcurrentWorkers, slotClaims) {
502
+ return {
503
+ ...state,
504
+ worker_pool_state: {
505
+ max_concurrent: maxConcurrentWorkers,
506
+ slot_claims: slotClaims,
507
+ },
508
+ };
509
+ }
510
+ function pruneWorkerPoolClaimsForChild(state, childId) {
511
+ if (!state.worker_pool_state)
512
+ return state;
513
+ return {
514
+ ...state,
515
+ worker_pool_state: {
516
+ ...state.worker_pool_state,
517
+ slot_claims: state.worker_pool_state.slot_claims.filter((claim) => claim.child_id !== childId),
518
+ },
519
+ };
520
+ }
490
521
  async function syncClusterCompletion(args) {
491
522
  const clusterState = await (0, store_js_1.readClusterState)(args.clusterId, args.repoRoot);
492
523
  if (!clusterState) {
@@ -651,8 +682,12 @@ async function runParentLoop(options) {
651
682
  const notificationFormat = config.orchestration?.notification_format ?? (orchestrationMode === 'auto' ? 'terse' : 'verbose');
652
683
  const adapterName = legacyEphemeralMode ? "agent-subtask" : (options.adapter ?? config.execution?.adapter ?? "terminal-cli");
653
684
  let providerName;
685
+ let providerSelectionReason;
686
+ let providersTried;
654
687
  if (adapterName === "agent-subtask") {
655
688
  providerName = "agent-subtask";
689
+ providerSelectionReason = "agent-subtask-adapter";
690
+ providersTried = ["agent-subtask"];
656
691
  }
657
692
  else {
658
693
  let evidence;
@@ -668,6 +703,8 @@ async function runParentLoop(options) {
668
703
  };
669
704
  }
670
705
  providerName = evidence.provider ?? "default";
706
+ providerSelectionReason = evidence.selectionReason;
707
+ providersTried = evidence.providersTried;
671
708
  }
672
709
  const executionConfig = adapterName === "agent-subtask"
673
710
  ? { ...(config.execution ?? { providers: {} }), adapter: "agent-subtask" }
@@ -675,6 +712,7 @@ async function runParentLoop(options) {
675
712
  const adapter = (0, registry_js_1.createAdapter)(adapterName, executionConfig);
676
713
  const budgetPolicy = (0, budget_js_1.policyFromConfig)(state.context_budget, config.budget);
677
714
  const allowAnalyzeChildren = allowAnalyzeChildrenFlag || (config.budget?.allow_analyze_children === true);
715
+ const maxConcurrentWorkers = config.execution?.routerPolicy?.defaultWorkerPool?.maxActiveWorkers ?? 1;
678
716
  const telemetryFile = resolveTelemetryFile(state, repoRoot);
679
717
  // Enforce provider policy for explicit --provider flag before entering the loop.
680
718
  // resolveProviderAndMode handles policy-filtered rotation; this gate blocks an
@@ -725,18 +763,80 @@ async function runParentLoop(options) {
725
763
  if (!dryRun) {
726
764
  flushBodiesToClusterSnapshot(state, repoRoot);
727
765
  }
766
+ let localGraph = null;
767
+ try {
768
+ localGraph = await local_graph_js_1.LocalGraph.load(state.cluster_id, repoRoot);
769
+ }
770
+ catch {
771
+ localGraph = null;
772
+ }
728
773
  // ── Lifecycle manager: enforce one-active-worker policy ─────────────────
729
774
  // forceReleaseAll() clears any orphaned registrations from a previous
730
775
  // crashed session. Registrations are session-memory only; they are not
731
776
  // persisted to disk, so a fresh loop start always begins clean.
732
- const lifecycle = new lifecycle_js_1.WorkerLifecycleManager(1);
777
+ const lifecycle = new lifecycle_js_1.WorkerLifecycleManager(maxConcurrentWorkers);
733
778
  lifecycle.forceReleaseAll();
734
779
  // ── Main dispatch loop ───────────────────────────────────────────────────
735
780
  // eslint-disable-next-line no-constant-condition
736
781
  while (true) {
737
782
  // ── Step 02: Select next open child ─────────────────────────────────
738
- const nextChild = selectNextChild(state);
783
+ const existingSlotClaims = state.worker_pool_state?.slot_claims ?? [];
784
+ if (!dryRun) {
785
+ const clusterState = await (0, store_js_1.readClusterState)(state.cluster_id, repoRoot);
786
+ if (clusterState) {
787
+ const pruned = (0, store_js_1.pruneExpiredClaims)(clusterState);
788
+ if (pruned.expiredChildIds.length > 0) {
789
+ await (0, store_js_1.writeClusterState)(state.cluster_id, pruned.state, repoRoot);
790
+ }
791
+ }
792
+ }
793
+ const selection = (0, child_selector_js_1.selectChildSlotClaims)({
794
+ open_children: state.open_children,
795
+ completed_children: state.completed_children,
796
+ active_child: state.active_child || null,
797
+ existing_claims: existingSlotClaims,
798
+ max_concurrent: maxConcurrentWorkers,
799
+ claim_ttl_ms: CLAIM_TTL_MS,
800
+ get_dependencies: (childId) => localGraph?.getDependencies(childId) ?? [],
801
+ decide_route: ({ activeSlotsByProvider }) => (0, index_js_1.decideWorkerRoute)({
802
+ role: "worker",
803
+ taskType: "impl",
804
+ adapter: adapterName,
805
+ providerOverride: options.provider,
806
+ providers: Object.keys(config.execution?.providers ?? {}),
807
+ rotation: config.execution?.rotation ?? [],
808
+ rolePolicy: config.execution?.providerPolicy?.worker,
809
+ roleConfiguredProvider: config.execution?.roles?.worker?.provider,
810
+ routerPolicy: config.execution?.routerPolicy,
811
+ constraints: {
812
+ requiredCapabilities: ["implementation"],
813
+ },
814
+ runtime: {
815
+ activeSlotsByProvider,
816
+ },
817
+ compatibilityMode: false,
818
+ }),
819
+ });
820
+ const nextSlotClaims = selection.slot_claims;
821
+ const nextChild = selection.selected_child;
739
822
  if (nextChild === null) {
823
+ if (state.open_children.length > 0) {
824
+ if (!dryRun) {
825
+ appendTelemetry(telemetryFile, {
826
+ event: "scheduler-no-eligible-child",
827
+ run_id: state.run_id,
828
+ open_children: state.open_children,
829
+ rejected_children: selection.rejected_children,
830
+ slot_claims: nextSlotClaims,
831
+ timestamp: new Date().toISOString(),
832
+ });
833
+ }
834
+ return {
835
+ haltReason: "blocked",
836
+ childrenDispatched,
837
+ message: "No schedulable child matched dependency and router slot constraints.",
838
+ };
839
+ }
740
840
  const autoFinalizeRequested = orchestrationMode === "auto" && config.orchestration?.auto_finalize === true;
741
841
  // All children completed — write final state and halt
742
842
  if (!dryRun) {
@@ -979,10 +1079,57 @@ async function runParentLoop(options) {
979
1079
  (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(resultFile), { recursive: true });
980
1080
  const statePreDispatch = state;
981
1081
  let stateBeforeDispatch = state;
982
- const packet = buildPacket(state, nextChild, stateFile, telemetryFile, repoRoot, resultFile);
1082
+ const packet = buildPacket(state, nextChild, stateFile, telemetryFile, repoRoot, resultFile, maxConcurrentWorkers);
1083
+ // ── Child-level QC selection ─────────────────────────────────────────────
1084
+ // Only opt-in conditions may select child-level QC. The dispatch boundary
1085
+ // below enforces that any child-level marker on the packet is valid.
1086
+ let packetWithQc = packet;
1087
+ let stateWithQcMeta = state;
1088
+ const childQcTrigger = (0, triggers_js_1.selectChildQcTrigger)(config.qc, nextChild, packet.instructions.allowed_scope, state.open_children_meta?.[nextChild]?.labels);
1089
+ if (childQcTrigger) {
1090
+ packetWithQc = { ...packet, qc_trigger: childQcTrigger };
1091
+ stateWithQcMeta = {
1092
+ ...state,
1093
+ open_children_meta: {
1094
+ ...state.open_children_meta,
1095
+ [nextChild]: {
1096
+ ...(state.open_children_meta?.[nextChild] ?? {}),
1097
+ qc_trigger: childQcTrigger,
1098
+ },
1099
+ },
1100
+ };
1101
+ if (!dryRun) {
1102
+ appendTelemetry(telemetryFile, {
1103
+ event: "qc-child-trigger-selected",
1104
+ run_id: state.run_id,
1105
+ child_id: nextChild,
1106
+ qc_trigger: childQcTrigger,
1107
+ timestamp: new Date().toISOString(),
1108
+ });
1109
+ }
1110
+ }
983
1111
  if (!dryRun) {
984
1112
  try {
985
- writePacketArtifact(packetPath, packet);
1113
+ (0, dispatch_boundary_js_1.assertChildQcSelectionAllowed)(stateWithQcMeta, nextChild, config.qc, packet.instructions.allowed_scope, state.open_children_meta?.[nextChild]?.labels, telemetryFile);
1114
+ }
1115
+ catch (err) {
1116
+ const msg = err instanceof Error ? err.message : String(err);
1117
+ appendTelemetry(telemetryFile, {
1118
+ event: "qc-child-selection-rejected",
1119
+ run_id: state.run_id,
1120
+ child_id: nextChild,
1121
+ error: msg,
1122
+ timestamp: new Date().toISOString(),
1123
+ });
1124
+ return {
1125
+ haltReason: 'worker-error',
1126
+ childrenDispatched,
1127
+ haltingChild: nextChild,
1128
+ message: msg,
1129
+ };
1130
+ }
1131
+ try {
1132
+ writePacketArtifact(packetPath, packetWithQc);
986
1133
  }
987
1134
  catch (err) {
988
1135
  const msg = err instanceof Error ? err.message : String(err);
@@ -1009,11 +1156,13 @@ async function runParentLoop(options) {
1009
1156
  packetPath,
1010
1157
  resultPath: resultFile,
1011
1158
  provider: providerName,
1159
+ providerSelectionReason,
1160
+ providersTried,
1012
1161
  workerId,
1013
1162
  dispatchedAt,
1014
1163
  });
1015
1164
  const stateWithDispatch = {
1016
- ...withChildDispatchMetadata(state, nextChild, resultFile, dispatchRecord),
1165
+ ...withWorkerPoolState(withChildDispatchMetadata(stateWithQcMeta, nextChild, resultFile, dispatchRecord), maxConcurrentWorkers, nextSlotClaims),
1017
1166
  active_child: nextChild,
1018
1167
  step_cursor: "dispatch",
1019
1168
  dispatch_boundary: (0, dispatch_boundary_js_1.advanceDispatchEpoch)(state.dispatch_boundary, nextChild),
@@ -1066,6 +1215,7 @@ async function runParentLoop(options) {
1066
1215
  if (!dryRun) {
1067
1216
  const totalChildren = state.open_children.length + state.completed_children.length;
1068
1217
  const childIndex = state.completed_children.length + 1;
1218
+ const selectedSlotClaim = nextSlotClaims.find((claim) => claim.child_id === nextChild);
1069
1219
  logStatus(notificationFormat, `RUNNING ${nextChild} (${childIndex}/${totalChildren})`);
1070
1220
  appendTelemetry(telemetryFile, {
1071
1221
  event: "child-dispatched",
@@ -1080,6 +1230,8 @@ async function runParentLoop(options) {
1080
1230
  packet_path: packetPath,
1081
1231
  expected_result_path: resultFile,
1082
1232
  dry_run: dryRun,
1233
+ selected_slot_claim: selectedSlotClaim ?? null,
1234
+ slot_claims: nextSlotClaims,
1083
1235
  timestamp: new Date().toISOString(),
1084
1236
  });
1085
1237
  emitBootstrapContextSize(telemetryFile, state.run_id, nextChild, stateFile, packet);
@@ -1229,7 +1381,7 @@ async function runParentLoop(options) {
1229
1381
  // Errors are logged to telemetry but do not fail the halt.
1230
1382
  let adapter;
1231
1383
  try {
1232
- adapter = (0, index_js_1.loadTrackerAdapter)(config);
1384
+ adapter = (0, index_js_2.loadTrackerAdapter)(config);
1233
1385
  }
1234
1386
  catch (err) {
1235
1387
  const errorMsg = err instanceof Error ? err.message : String(err);
@@ -1423,6 +1575,14 @@ async function runParentLoop(options) {
1423
1575
  finalWorkerSummary?.['commit_hash'];
1424
1576
  const validationSummary = finalWorkerSummary?.['validation'] ??
1425
1577
  finalWorkerSummary?.['validation_summary'];
1578
+ const dispatchRecord = state.open_children_meta?.[nextChild]?.dispatch_record;
1579
+ const providerUsed = finalWorkerSummary?.['provider'] ??
1580
+ finalWorkerSummary?.['provider_used'] ??
1581
+ dispatchRecord?.provider ??
1582
+ null;
1583
+ const modelUsed = finalWorkerSummary?.['model'] ??
1584
+ finalWorkerSummary?.['model_used'] ??
1585
+ null;
1426
1586
  if (!dryRun && workerStatus === 'done' && (!lastCommit || lastCommit.trim().length === 0)) {
1427
1587
  const errMsg = `Worker reported done for ${nextChild} without commit evidence`;
1428
1588
  appendTelemetry(telemetryFile, {
@@ -1561,14 +1721,14 @@ async function runParentLoop(options) {
1561
1721
  }
1562
1722
  // Advance state, including continue_epoch to match the consumed dispatch
1563
1723
  const advanced = advanceState(state, nextChild, lastCommit);
1564
- state = {
1724
+ state = pruneWorkerPoolClaimsForChild({
1565
1725
  ...advanced,
1566
1726
  dispatch_boundary: (0, dispatch_boundary_js_1.advanceContinueEpoch)(state.dispatch_boundary),
1567
1727
  completed_children_results: {
1568
1728
  ...advanced.completed_children_results,
1569
1729
  [nextChild]: workerResult,
1570
1730
  },
1571
- };
1731
+ }, nextChild);
1572
1732
  childrenDispatched += 1;
1573
1733
  // Worker did not write its own completion — orchestrator fills the gap.
1574
1734
  if (!dryRun) {
@@ -1578,14 +1738,14 @@ async function runParentLoop(options) {
1578
1738
  else {
1579
1739
  // Worker wrote its own completion — advance continue_epoch to stay in sync.
1580
1740
  // The worker does not manage dispatch_boundary, so the parent must do it.
1581
- state = {
1741
+ state = pruneWorkerPoolClaimsForChild({
1582
1742
  ...state,
1583
1743
  dispatch_boundary: (0, dispatch_boundary_js_1.advanceContinueEpoch)(state.dispatch_boundary),
1584
1744
  completed_children_results: {
1585
1745
  ...state.completed_children_results,
1586
1746
  [nextChild]: workerResult,
1587
1747
  },
1588
- };
1748
+ }, nextChild);
1589
1749
  childrenDispatched += 1;
1590
1750
  if (!dryRun) {
1591
1751
  (0, checkpoint_js_1.writeStateAtomic)(stateFile, state);
@@ -1641,15 +1801,30 @@ async function runParentLoop(options) {
1641
1801
  child_id: nextChild,
1642
1802
  children_completed: state.context_budget.children_completed,
1643
1803
  validation_summary: validationSummary,
1804
+ completion_status: workerStatus,
1644
1805
  commit_hash: lastCommit,
1645
1806
  commit_files: commitFiles,
1807
+ dispatch_id: dispatchRecord?.dispatch_id,
1808
+ provider: providerUsed,
1809
+ model: modelUsed,
1810
+ router_selection_reason: dispatchRecord?.provider_selection_reason,
1811
+ providers_tried: dispatchRecord?.providers_tried,
1646
1812
  timestamp: new Date().toISOString(),
1647
1813
  };
1648
1814
  if (elapsedSeconds !== undefined) {
1649
1815
  telemetryEvent.elapsed_seconds = elapsedSeconds;
1650
1816
  }
1651
1817
  appendTelemetry(telemetryFile, telemetryEvent);
1652
- appendChildCompletedLedgerEvent(repoRoot, state, nextChild, lastCommit ?? null, validationSummary);
1818
+ appendChildCompletedLedgerEvent(repoRoot, state, nextChild, lastCommit ?? null, validationSummary, {
1819
+ dispatchId: dispatchRecord?.dispatch_id,
1820
+ provider: providerUsed,
1821
+ model: modelUsed,
1822
+ elapsedSeconds,
1823
+ commitFiles,
1824
+ completionStatus: workerStatus,
1825
+ routerSelectionReason: dispatchRecord?.provider_selection_reason,
1826
+ providersTried: dispatchRecord?.providers_tried,
1827
+ });
1653
1828
  }
1654
1829
  if (orchestrationMode === 'supervised') {
1655
1830
  return {
@@ -0,0 +1,229 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.decideWorkerRoute = decideWorkerRoute;
4
+ const TRUST_RANK = {
5
+ sandbox: 0,
6
+ standard: 1,
7
+ trusted: 2,
8
+ };
9
+ const COST_RANK = {
10
+ low: 0,
11
+ medium: 1,
12
+ high: 2,
13
+ };
14
+ function defaultCapabilitiesForTask(taskType) {
15
+ switch (taskType) {
16
+ case "startup":
17
+ return ["orchestration"];
18
+ case "analyze":
19
+ return ["analysis"];
20
+ case "impl":
21
+ return ["implementation"];
22
+ case "repair":
23
+ return ["repair"];
24
+ case "docs":
25
+ return ["docs"];
26
+ case "finalize":
27
+ return ["finalization"];
28
+ default:
29
+ return [];
30
+ }
31
+ }
32
+ function orderedProviders(input) {
33
+ if (input.providerOverride) {
34
+ return [input.providerOverride];
35
+ }
36
+ const roleProviders = input.rolePolicy?.providers ?? [];
37
+ const rotation = input.rotation ?? [];
38
+ if (input.compatibilityMode !== false) {
39
+ if (roleProviders.length > 0) {
40
+ const filteredRotation = rotation.filter((provider) => roleProviders.includes(provider));
41
+ if (filteredRotation.length > 0) {
42
+ return filteredRotation;
43
+ }
44
+ const configuredProviders = new Set(input.providers);
45
+ return roleProviders.filter((provider) => configuredProviders.has(provider));
46
+ }
47
+ if (input.roleConfiguredProvider) {
48
+ return [input.roleConfiguredProvider];
49
+ }
50
+ if (rotation.length > 0) {
51
+ return rotation;
52
+ }
53
+ return input.providers;
54
+ }
55
+ const preferred = rotation.length > 0 ? rotation : input.providers;
56
+ if (roleProviders.length === 0) {
57
+ return preferred;
58
+ }
59
+ const configuredAndAllowed = new Set(roleProviders.filter((provider) => input.providers.includes(provider)));
60
+ const ordered = preferred.filter((provider) => configuredAndAllowed.has(provider));
61
+ if (ordered.length > 0) {
62
+ return ordered;
63
+ }
64
+ return roleProviders.filter((provider) => input.providers.includes(provider));
65
+ }
66
+ function compatibilitySelectionReason(input, selectedProvider) {
67
+ if (input.providerOverride)
68
+ return "cli-provider-override";
69
+ if (!selectedProvider)
70
+ return "delegated-no-provider";
71
+ const roleProviders = input.rolePolicy?.providers ?? [];
72
+ if (roleProviders.length > 0) {
73
+ const rotation = input.rotation ?? [];
74
+ if (rotation.filter((provider) => roleProviders.includes(provider))[0] === selectedProvider) {
75
+ return "policy-filtered-rotation";
76
+ }
77
+ return "role-policy";
78
+ }
79
+ if (input.roleConfiguredProvider === selectedProvider)
80
+ return "role-config";
81
+ if ((input.rotation ?? [])[0] === selectedProvider)
82
+ return "config-rotation";
83
+ return "config-first-provider";
84
+ }
85
+ function compatibilityExhaustedReason(input) {
86
+ const roleProviders = input.rolePolicy?.providers ?? [];
87
+ if (roleProviders.length > 0)
88
+ return "role-policy-no-configured-provider";
89
+ return "no-configured-provider";
90
+ }
91
+ function topRejectionReason(candidates) {
92
+ const orderedReasons = [
93
+ "role-disabled",
94
+ "not-in-policy",
95
+ "quota-exhausted",
96
+ "trust-too-low",
97
+ "capability-mismatch",
98
+ "cost-policy",
99
+ "no-slot",
100
+ ];
101
+ for (const reason of orderedReasons) {
102
+ if (candidates.some((candidate) => candidate.rejectionReasons.includes(reason))) {
103
+ return reason;
104
+ }
105
+ }
106
+ return undefined;
107
+ }
108
+ function decideWorkerRoute(input) {
109
+ const compatibilityMode = input.compatibilityMode !== false;
110
+ const providerRegistry = input.routerPolicy?.providerRegistry ?? {};
111
+ const roleProviders = input.rolePolicy?.providers ?? [];
112
+ const requiredCapabilities = input.constraints?.requiredCapabilities ?? defaultCapabilitiesForTask(input.taskType);
113
+ const ordered = orderedProviders(input);
114
+ const activeSlotsByProvider = input.runtime?.activeSlotsByProvider ?? {};
115
+ const quotaAvailableByProvider = input.runtime?.quotaAvailableByProvider ?? {};
116
+ const globalSlotLimit = input.routerPolicy?.defaultWorkerPool?.maxActiveSlots;
117
+ const candidates = [];
118
+ if (roleProviders.length === 0 && input.rolePolicy) {
119
+ return {
120
+ selectedProvider: undefined,
121
+ selectedWorker: { role: input.role, taskType: input.taskType },
122
+ mode: "delegated",
123
+ selectionReason: compatibilityMode ? "delegated-no-provider" : "router-role-disabled",
124
+ exhaustedReason: "role-disabled",
125
+ compatibilityMode,
126
+ providersTried: [],
127
+ candidates: [],
128
+ };
129
+ }
130
+ for (const [orderIndex, provider] of ordered.entries()) {
131
+ const policy = providerRegistry[provider];
132
+ const rejectionReasons = [];
133
+ const eligibleRoleSet = new Set(policy?.eligibleRoles ?? []);
134
+ const policyMatched = !policy || eligibleRoleSet.size === 0 || eligibleRoleSet.has(input.role);
135
+ if (!policyMatched) {
136
+ rejectionReasons.push("role-disabled");
137
+ }
138
+ if (roleProviders.length > 0 && !roleProviders.includes(provider)) {
139
+ rejectionReasons.push("not-in-policy");
140
+ }
141
+ const capabilitySet = new Set(policy?.capabilities ?? []);
142
+ const allowedTaskSet = new Set(policy?.taskTypes ?? []);
143
+ if ((capabilitySet.size > 0 && requiredCapabilities.some((capability) => !capabilitySet.has(capability))) ||
144
+ (allowedTaskSet.size > 0 && !allowedTaskSet.has(input.taskType))) {
145
+ rejectionReasons.push("capability-mismatch");
146
+ }
147
+ const providerTrust = policy?.trustTier;
148
+ const minTrust = input.constraints?.minTrustTier;
149
+ if (providerTrust && minTrust && TRUST_RANK[providerTrust] < TRUST_RANK[minTrust]) {
150
+ rejectionReasons.push("trust-too-low");
151
+ }
152
+ const providerCost = policy?.costTier;
153
+ const maxCost = input.constraints?.maxCostTier;
154
+ if (providerCost && maxCost && COST_RANK[providerCost] > COST_RANK[maxCost]) {
155
+ rejectionReasons.push("cost-policy");
156
+ }
157
+ const disallowedQuota = new Set(input.constraints?.disallowedQuotaPolicies ?? []);
158
+ if (policy?.quotaPolicy && disallowedQuota.has(policy.quotaPolicy)) {
159
+ rejectionReasons.push("cost-policy");
160
+ }
161
+ if (policy?.quotaPolicy && quotaAvailableByProvider[provider] === false) {
162
+ rejectionReasons.push("quota-exhausted");
163
+ }
164
+ const activeSlots = activeSlotsByProvider[provider] ?? 0;
165
+ const providerSlotLimit = policy?.maxActiveSlots;
166
+ // Provider-specific maxActiveSlots overrides the global default when present.
167
+ const effectiveSlotLimit = providerSlotLimit ?? globalSlotLimit;
168
+ if (effectiveSlotLimit !== undefined && activeSlots >= effectiveSlotLimit) {
169
+ rejectionReasons.push("no-slot");
170
+ }
171
+ const score = {
172
+ orderScore: Math.max(0, 100 - orderIndex),
173
+ trustScore: providerTrust ? TRUST_RANK[providerTrust] * 1_000 : 0,
174
+ costScore: providerCost ? (3 - COST_RANK[providerCost]) * 10 : 0,
175
+ total: 0,
176
+ };
177
+ score.total = score.orderScore + score.trustScore + score.costScore;
178
+ candidates.push({
179
+ provider,
180
+ eligible: rejectionReasons.length === 0,
181
+ score,
182
+ rejectionReasons,
183
+ evidence: {
184
+ orderIndex,
185
+ trustTier: providerTrust,
186
+ costTier: providerCost,
187
+ quotaPolicy: policy?.quotaPolicy,
188
+ activeSlots,
189
+ slotLimit: providerSlotLimit ?? globalSlotLimit,
190
+ policyMatched,
191
+ },
192
+ });
193
+ }
194
+ const eligibleCandidates = candidates.filter((candidate) => candidate.eligible);
195
+ const selected = eligibleCandidates
196
+ .slice()
197
+ .sort((a, b) => {
198
+ if (a.score.total !== b.score.total)
199
+ return b.score.total - a.score.total;
200
+ if (a.evidence.orderIndex !== b.evidence.orderIndex)
201
+ return a.evidence.orderIndex - b.evidence.orderIndex;
202
+ return a.provider.localeCompare(b.provider);
203
+ })[0];
204
+ const selectedProvider = selected?.provider;
205
+ if (!selectedProvider) {
206
+ const rejection = topRejectionReason(candidates);
207
+ const exhaustedReason = rejection ?? (compatibilityMode ? compatibilityExhaustedReason(input) : "no-eligible-provider");
208
+ const selectionReason = compatibilityMode ? "delegated-no-provider" : "router-no-eligible-provider";
209
+ return {
210
+ selectedProvider: undefined,
211
+ selectedWorker: { role: input.role, taskType: input.taskType },
212
+ mode: "delegated",
213
+ selectionReason,
214
+ exhaustedReason,
215
+ compatibilityMode,
216
+ providersTried: ordered,
217
+ candidates,
218
+ };
219
+ }
220
+ return {
221
+ selectedProvider,
222
+ selectedWorker: { role: input.role, taskType: input.taskType },
223
+ mode: "direct-worker",
224
+ selectionReason: compatibilityMode ? compatibilitySelectionReason(input, selectedProvider) : "policy-router",
225
+ compatibilityMode,
226
+ providersTried: ordered,
227
+ candidates,
228
+ };
229
+ }
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.decideWorkerRoute = void 0;
4
+ var engine_js_1 = require("./engine.js");
5
+ Object.defineProperty(exports, "decideWorkerRoute", { enumerable: true, get: function () { return engine_js_1.decideWorkerRoute; } });
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });