@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.
- package/dist/autoresearch/gates.js +47 -0
- package/dist/autoresearch/proposal.js +183 -1
- package/dist/autoresearch/score.js +276 -0
- package/dist/cli/init.js +17 -0
- package/dist/cluster-state/store.js +70 -1
- package/dist/config/defaults.js +25 -0
- package/dist/config/validator.js +390 -0
- package/dist/finalize/artifact-policy.js +3 -1
- package/dist/finalize/index.js +73 -17
- package/dist/finalize/linear.js +45 -0
- package/dist/finalize/run-report.js +68 -12
- package/dist/loop/adapters/agent-subtask.js +19 -0
- package/dist/loop/adapters/cli-subtask-bridge.js +0 -1
- package/dist/loop/adapters/terminal-cli.js +194 -14
- package/dist/loop/checkpoint.js +40 -0
- package/dist/loop/dispatch-boundary.js +29 -0
- package/dist/loop/dispatch.js +193 -22
- package/dist/loop/orphan-recovery.js +56 -0
- package/dist/loop/parent.js +197 -22
- package/dist/loop/router/engine.js +229 -0
- package/dist/loop/router/index.js +5 -0
- package/dist/loop/router/types.js +2 -0
- package/dist/loop/worker-packet.js +16 -0
- package/dist/qc/artifacts.js +117 -0
- package/dist/qc/attribution.js +266 -0
- package/dist/qc/autofix.js +77 -0
- package/dist/qc/index.js +30 -0
- package/dist/qc/orchestration.js +150 -0
- package/dist/qc/policy.js +70 -0
- package/dist/qc/provider.js +28 -0
- package/dist/qc/providers/coderabbit.js +214 -0
- package/dist/qc/providers/index.js +5 -0
- package/dist/qc/registry.js +16 -0
- package/dist/qc/routing.js +55 -0
- package/dist/qc/runner.js +137 -0
- package/dist/qc/schemas.js +110 -0
- package/dist/qc/security-category.js +11 -0
- package/dist/qc/severity.js +78 -0
- package/dist/qc/triggers.js +92 -0
- package/dist/qc/types.js +9 -0
- package/dist/runtime/scheduling/child-selector.js +81 -0
- package/package.json +1 -1
package/dist/loop/parent.js
CHANGED
|
@@ -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("
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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; } });
|