@lsctech/polaris 0.5.6 → 0.5.8
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/index.js +30 -1
- package/dist/autoresearch/proposal.js +5 -0
- package/dist/autoresearch/score.js +174 -2
- package/dist/autoresearch/sol-evaluation-writer.js +135 -0
- package/dist/autoresearch/sol-evidence-loader.js +40 -3
- package/dist/autoresearch/sol-evidence-normalizer.js +337 -0
- package/dist/autoresearch/sol-recommendations.js +212 -0
- package/dist/autoresearch/sol-report-renderer.js +282 -0
- package/dist/autoresearch/sol-report.js +44 -0
- package/dist/autoresearch/sol-scorecard-calculator.js +865 -0
- package/dist/autoresearch/sol-scorer.js +45 -1
- package/dist/cli/autoresearch.js +77 -0
- package/dist/cluster-state/store.js +56 -0
- package/dist/config/defaults.js +1 -0
- package/dist/config/validator.js +157 -0
- package/dist/finalize/artifact-policy.js +2 -0
- package/dist/finalize/github.js +13 -9
- package/dist/finalize/index.js +198 -4
- package/dist/finalize/linear.js +8 -4
- package/dist/finalize/steps/08-create-pr.js +2 -2
- package/dist/finalize/steps/11-update-linear.js +2 -2
- package/dist/loop/parent.js +189 -33
- package/dist/loop/worker-packet.js +75 -1
- package/dist/qc/artifacts.js +27 -0
- package/dist/qc/fixtures/repair-packets.js +170 -0
- package/dist/qc/index.js +2 -0
- package/dist/qc/orchestration.js +5 -2
- package/dist/qc/policy.js +30 -3
- package/dist/qc/providers/coderabbit.js +152 -17
- package/dist/qc/registry.js +36 -3
- package/dist/qc/repair-loop.js +458 -0
- package/dist/qc/repair-packets.js +420 -0
- package/dist/qc/runner.js +328 -37
- package/dist/qc/schemas.js +79 -1
- package/dist/types/sol-metrics.js +108 -0
- package/dist/types/sol-scorecard.js +148 -0
- package/package.json +1 -1
package/dist/finalize/index.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateStateFileAuthority = validateStateFileAuthority;
|
|
4
|
+
exports.validateQcRepairLoopGate = validateQcRepairLoopGate;
|
|
5
|
+
exports.warnOnMissingQcArtifacts = warnOnMissingQcArtifacts;
|
|
6
|
+
exports.validateAuthoritativeChildState = validateAuthoritativeChildState;
|
|
3
7
|
exports.runFinalize = runFinalize;
|
|
4
8
|
exports.createFinalizeCommand = createFinalizeCommand;
|
|
5
9
|
const commander_1 = require("commander");
|
|
@@ -12,6 +16,7 @@ const checkpoint_js_1 = require("../loop/checkpoint.js");
|
|
|
12
16
|
const artifact_policy_js_1 = require("./artifact-policy.js");
|
|
13
17
|
const git_custody_js_1 = require("../loop/git-custody.js");
|
|
14
18
|
const store_js_1 = require("../cluster-state/store.js");
|
|
19
|
+
const artifacts_js_1 = require("../qc/artifacts.js");
|
|
15
20
|
const canon_check_js_1 = require("../smartdocs-engine/canon-check.js");
|
|
16
21
|
const _01_map_update_js_1 = require("./steps/01-map-update.js");
|
|
17
22
|
const _02_map_validate_js_1 = require("./steps/02-map-validate.js");
|
|
@@ -85,6 +90,60 @@ function validateStateBranchMatchesGitBranch(stateBranch, branch) {
|
|
|
85
90
|
process.exit(1);
|
|
86
91
|
}
|
|
87
92
|
}
|
|
93
|
+
const CLUSTER_STATE_FILE_PATTERN = /[\\/]\.polaris[\\/]clusters[\\/][^\\/]+[\\/]state\.json$/;
|
|
94
|
+
function isClusterStateSnapshotFile(filePath) {
|
|
95
|
+
return CLUSTER_STATE_FILE_PATTERN.test(filePath.replace(/\\/g, "/"));
|
|
96
|
+
}
|
|
97
|
+
function computeRunStateFreshness(state) {
|
|
98
|
+
const s = state;
|
|
99
|
+
let score = 0;
|
|
100
|
+
score += (s.completed_children?.length ?? 0) * 10;
|
|
101
|
+
if (s.status === "complete")
|
|
102
|
+
score += 50;
|
|
103
|
+
if (s.pr_url)
|
|
104
|
+
score += 30;
|
|
105
|
+
if (s.qc_repair_loop?.terminal_outcome)
|
|
106
|
+
score += 40;
|
|
107
|
+
score += (s.dispatch_boundary?.dispatch_epoch ?? 0) * 5;
|
|
108
|
+
score += (s.context_budget?.children_completed ?? 0) * 2;
|
|
109
|
+
return score;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Refuse to use a promoted cluster state snapshot when the authoritative
|
|
113
|
+
* taskchain current-state.json is newer. Cluster snapshots under
|
|
114
|
+
* `.polaris/clusters/<id>/state.json` are secondary; the taskchain state
|
|
115
|
+
* owned by the parent loop is authoritative.
|
|
116
|
+
*/
|
|
117
|
+
function validateStateFileAuthority(stateFile, state, repoRoot) {
|
|
118
|
+
if (!isClusterStateSnapshotFile(stateFile)) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const taskchainStateFile = (0, node_path_1.join)(repoRoot, ".taskchain_artifacts", "polaris-run", "current-state.json");
|
|
122
|
+
if (!(0, node_fs_1.existsSync)(taskchainStateFile)) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
let taskchainState;
|
|
126
|
+
try {
|
|
127
|
+
taskchainState = (0, checkpoint_js_1.readState)(taskchainStateFile);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (taskchainState.run_id !== state.run_id ||
|
|
133
|
+
taskchainState.cluster_id !== state.cluster_id) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const taskchainFreshness = computeRunStateFreshness(taskchainState);
|
|
137
|
+
const snapshotFreshness = computeRunStateFreshness(state);
|
|
138
|
+
if (taskchainFreshness > snapshotFreshness) {
|
|
139
|
+
process.stderr.write(`finalize aborted: cluster state snapshot is stale.\n` +
|
|
140
|
+
` Snapshot: ${stateFile}\n` +
|
|
141
|
+
` Authoritative taskchain state: ${taskchainStateFile}\n` +
|
|
142
|
+
`The taskchain current-state.json is newer; use it instead, or ` +
|
|
143
|
+
`update the cluster snapshot before finalizing.\n`);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
88
147
|
/**
|
|
89
148
|
* Check that the Closeout Librarian has run and its result passes the gate.
|
|
90
149
|
* Returns null if finalize may proceed; returns a human-readable blocker string if not.
|
|
@@ -183,9 +242,107 @@ function resolveQcTelemetryFile(state, repoRoot) {
|
|
|
183
242
|
const artifactDir = state.artifact_dir ?? (0, node_path_1.join)(repoRoot, ".taskchain_artifacts", "polaris-run");
|
|
184
243
|
return (0, node_path_1.join)(artifactDir, "runs", state.run_id, "telemetry.jsonl");
|
|
185
244
|
}
|
|
245
|
+
// ── QC repair-loop terminal state gate ────────────────────────────────────────
|
|
246
|
+
/**
|
|
247
|
+
* Trusted QC repair-loop terminal outcomes that allow finalize to proceed.
|
|
248
|
+
* "pass" = QC rerun passed; "qc-disabled" = QC was off; "no-repairable" = no
|
|
249
|
+
* findings routable to repair workers (follow-up-only or already resolved).
|
|
250
|
+
*/
|
|
251
|
+
const TRUSTED_QC_REPAIR_OUTCOMES = new Set(["pass", "qc-disabled", "no-repairable"]);
|
|
252
|
+
/**
|
|
253
|
+
* Validate QC repair-loop terminal state when QC is enabled and repair routing
|
|
254
|
+
* is active. Returns null when finalize may proceed; returns a human-readable
|
|
255
|
+
* blocker string otherwise.
|
|
256
|
+
*/
|
|
257
|
+
function validateQcRepairLoopGate(state, config) {
|
|
258
|
+
// Gate only applies when QC is enabled
|
|
259
|
+
if (!config.enabled)
|
|
260
|
+
return null;
|
|
261
|
+
// Gate only applies when repair routing is active (not "log" or "block")
|
|
262
|
+
const repairRouting = config.repairRouting ?? "route";
|
|
263
|
+
if (repairRouting !== "route" && repairRouting !== "follow-up")
|
|
264
|
+
return null;
|
|
265
|
+
const repairLoop = state.qc_repair_loop;
|
|
266
|
+
// No repair-loop state at all — the completed-cluster QC trigger ran but
|
|
267
|
+
// never entered the repair loop. This is a gap: finalize should not proceed.
|
|
268
|
+
if (!repairLoop) {
|
|
269
|
+
return ("QC is enabled with repair routing active, but no qc_repair_loop state " +
|
|
270
|
+
"was found in the run state. The parent loop must run the QC repair loop " +
|
|
271
|
+
"before finalize can proceed.");
|
|
272
|
+
}
|
|
273
|
+
const outcome = repairLoop.terminal_outcome;
|
|
274
|
+
if (outcome === null || outcome === undefined) {
|
|
275
|
+
return ("QC repair loop is still in-flight (terminal_outcome is null). " +
|
|
276
|
+
"Finalize cannot proceed until the repair loop reaches a terminal state.");
|
|
277
|
+
}
|
|
278
|
+
if (TRUSTED_QC_REPAIR_OUTCOMES.has(outcome))
|
|
279
|
+
return null;
|
|
280
|
+
return (`QC repair loop terminated with untrusted outcome: "${outcome}". ` +
|
|
281
|
+
`Only ${Array.from(TRUSTED_QC_REPAIR_OUTCOMES).join(", ")} outcomes allow finalize to proceed. ` +
|
|
282
|
+
`Resolve the repair loop before re-running finalize.`);
|
|
283
|
+
}
|
|
284
|
+
// ── Authoritative completed-child state cross-check ───────────────────────────
|
|
285
|
+
function warnOnMissingQcArtifacts(clusterState, repoRoot) {
|
|
286
|
+
if (!clusterState)
|
|
287
|
+
return;
|
|
288
|
+
const validation = (0, artifacts_js_1.validateQcArtifactPointers)(clusterState.qc_runs);
|
|
289
|
+
if (validation.ok)
|
|
290
|
+
return;
|
|
291
|
+
for (const missing of validation.missing) {
|
|
292
|
+
console.warn(`finalize warning: cluster-state QC pointer references missing artifact: ${missing}`);
|
|
293
|
+
}
|
|
294
|
+
for (const unavailable of validation.unavailable) {
|
|
295
|
+
console.warn(`finalize warning: QC audit artifact unavailable: ${unavailable}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Cross-check completed children in the loop state against the cluster-state
|
|
300
|
+
* child_states. Returns a diagnostic result — when `ok` is false, finalize
|
|
301
|
+
* should refuse PR creation.
|
|
302
|
+
*/
|
|
303
|
+
function validateAuthoritativeChildState(state, repoRoot) {
|
|
304
|
+
const clusterState = (0, store_js_1.readClusterStateSync)(state.cluster_id, repoRoot);
|
|
305
|
+
const stateCount = state.completed_children.length;
|
|
306
|
+
// No cluster state available — backward-compat: trust the loop state.
|
|
307
|
+
if (!clusterState) {
|
|
308
|
+
return { ok: true, authoritativeCount: stateCount, stateCount };
|
|
309
|
+
}
|
|
310
|
+
const childStates = clusterState.child_states;
|
|
311
|
+
// No child_states array in cluster state — backward-compat: trust the loop state.
|
|
312
|
+
// This covers legacy cluster-state files that don't track individual child lifecycle.
|
|
313
|
+
if (!childStates || childStates.length === 0) {
|
|
314
|
+
return { ok: true, authoritativeCount: stateCount, stateCount };
|
|
315
|
+
}
|
|
316
|
+
const doneChildren = childStates.filter((c) => c.status === "done" || c.status === "reviewed" || c.status === "finalized");
|
|
317
|
+
const authoritativeCount = doneChildren.length;
|
|
318
|
+
// When cluster-state has tracked children but zero are done while loop state
|
|
319
|
+
// claims completions, the cluster state file is stale or was never updated.
|
|
320
|
+
if (authoritativeCount === 0 && stateCount > 0) {
|
|
321
|
+
return {
|
|
322
|
+
ok: false,
|
|
323
|
+
authoritativeCount,
|
|
324
|
+
stateCount,
|
|
325
|
+
reason: `Stale cluster state: cluster-state.json has 0 done children but ` +
|
|
326
|
+
`loop state claims ${stateCount} completed. ` +
|
|
327
|
+
`The cluster state file was never updated by worker completions.`,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
// Significant mismatch: cluster-state has fewer done children than loop state
|
|
331
|
+
if (authoritativeCount < stateCount) {
|
|
332
|
+
return {
|
|
333
|
+
ok: false,
|
|
334
|
+
authoritativeCount,
|
|
335
|
+
stateCount,
|
|
336
|
+
reason: `Completed-child count mismatch: cluster-state has ${authoritativeCount} done ` +
|
|
337
|
+
`children but loop state has ${stateCount}. ` +
|
|
338
|
+
`The cluster state may be stale or child completions were not recorded properly.`,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
return { ok: true, authoritativeCount, stateCount };
|
|
342
|
+
}
|
|
186
343
|
async function runQcGate(options) {
|
|
187
|
-
const { config, state, repoRoot, branch, trigger, prUrl, stepLabel } = options;
|
|
188
|
-
const registry = (0, index_js_2.
|
|
344
|
+
const { config, state, repoRoot, branch, baseRef, trigger, prUrl, stepLabel } = options;
|
|
345
|
+
const registry = (0, index_js_2.createQcRegistry)(config);
|
|
189
346
|
const result = await (0, index_js_2.runQcAtTrigger)({
|
|
190
347
|
config,
|
|
191
348
|
registry,
|
|
@@ -195,6 +352,7 @@ async function runQcGate(options) {
|
|
|
195
352
|
runId: state.run_id,
|
|
196
353
|
clusterId: state.cluster_id,
|
|
197
354
|
branch,
|
|
355
|
+
baseRef,
|
|
198
356
|
telemetryFile: resolveQcTelemetryFile(state, repoRoot),
|
|
199
357
|
state,
|
|
200
358
|
});
|
|
@@ -236,6 +394,7 @@ async function runFinalize(options) {
|
|
|
236
394
|
validateStateFilePath(stateFile);
|
|
237
395
|
validateClusterIdMatchesBranch(state.cluster_id, branch);
|
|
238
396
|
validateStateBranchMatchesGitBranch(state.branch, branch);
|
|
397
|
+
validateStateFileAuthority(stateFile, state, repoRoot);
|
|
239
398
|
// Step 4: Run configured checks
|
|
240
399
|
const checks = config.finalize?.runChecks ?? [];
|
|
241
400
|
if (checks.length > 0) {
|
|
@@ -322,6 +481,7 @@ async function runFinalize(options) {
|
|
|
322
481
|
// loop.allowBranchDivergence is true (direct-main mode).
|
|
323
482
|
{
|
|
324
483
|
const clusterState = (0, store_js_1.readClusterStateSync)(state.cluster_id, repoRoot);
|
|
484
|
+
warnOnMissingQcArtifacts(clusterState, repoRoot);
|
|
325
485
|
const baseBranch = clusterState?.base_branch;
|
|
326
486
|
const deliveryBranch = clusterState?.delivery_branch;
|
|
327
487
|
const directMainMode = config.loop?.allowBranchDivergence === true;
|
|
@@ -389,14 +549,37 @@ async function runFinalize(options) {
|
|
|
389
549
|
}
|
|
390
550
|
// Step 5.8: Completed-cluster QC trigger (when configured)
|
|
391
551
|
// Runs after all authoritative gates and before final commit/delivery.
|
|
552
|
+
const clusterStateForQc = (0, store_js_1.readClusterStateSync)(state.cluster_id, repoRoot);
|
|
553
|
+
const qcBaseRef = clusterStateForQc?.base_branch ??
|
|
554
|
+
config.finalize?.targetBranch ??
|
|
555
|
+
"main";
|
|
392
556
|
await runQcGate({
|
|
393
557
|
config: config.qc,
|
|
394
558
|
state,
|
|
395
559
|
repoRoot,
|
|
396
560
|
branch,
|
|
561
|
+
baseRef: qcBaseRef,
|
|
397
562
|
trigger: "completed-cluster",
|
|
398
563
|
stepLabel: "[5.8/14]",
|
|
399
564
|
});
|
|
565
|
+
// Step 5.9: QC repair-loop terminal state gate
|
|
566
|
+
// When QC is enabled and repair routing is active, require a trusted
|
|
567
|
+
// terminal outcome from the QC repair loop before allowing PR creation.
|
|
568
|
+
{
|
|
569
|
+
const repairLoopBlocker = validateQcRepairLoopGate(state, config.qc);
|
|
570
|
+
if (repairLoopBlocker) {
|
|
571
|
+
process.stderr.write(`finalize aborted: QC repair-loop gate failed.\n${repairLoopBlocker}\n`);
|
|
572
|
+
process.exit(1);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
// Step 5.10: Authoritative completed-child state cross-check
|
|
576
|
+
// Verify that the completed children claimed by the loop state match
|
|
577
|
+
// the cluster-state child_states. Blocks on stale or inconsistent state.
|
|
578
|
+
const authChildResult = validateAuthoritativeChildState(state, repoRoot);
|
|
579
|
+
if (!authChildResult.ok) {
|
|
580
|
+
process.stderr.write(`finalize aborted: authoritative child state mismatch.\n${authChildResult.reason}\n`);
|
|
581
|
+
process.exit(1);
|
|
582
|
+
}
|
|
400
583
|
// Step 6: Tracker Reconciliation
|
|
401
584
|
// LinearAdapter is sync-in only; only McpBridgeAdapter supports full reconciliation.
|
|
402
585
|
const trackerType = config.tracker?.adapter;
|
|
@@ -465,7 +648,7 @@ async function runFinalize(options) {
|
|
|
465
648
|
// Step 10: Create draft PR
|
|
466
649
|
const prDraft = config.finalize?.prDraft ?? true;
|
|
467
650
|
console.log("[10/14] Creating draft PR...");
|
|
468
|
-
const prUrl = (0, _08_create_pr_js_1.stepCreatePr)(repoRoot, branch, state, prDraft);
|
|
651
|
+
const prUrl = (0, _08_create_pr_js_1.stepCreatePr)(repoRoot, branch, state, prDraft, authChildResult.authoritativeCount);
|
|
469
652
|
// Step 10.5: PR-required QC trigger (when configured)
|
|
470
653
|
// Providers that require a PR URL run here after the PR is created.
|
|
471
654
|
await runQcGate({
|
|
@@ -480,6 +663,17 @@ async function runFinalize(options) {
|
|
|
480
663
|
// Step 11: Write PR URL to current-state.json
|
|
481
664
|
console.log("[11/14] Writing PR URL to state...");
|
|
482
665
|
state = (0, _09_update_state_js_1.stepUpdateState)(resolvedStateFile, state, prUrl);
|
|
666
|
+
// Promote the authoritative run state into the cluster snapshot so that
|
|
667
|
+
// `.polaris/clusters/<id>/state.json` preserves completed children, the
|
|
668
|
+
// dispatch boundary, QC repair-loop terminal state, and the PR URL.
|
|
669
|
+
try {
|
|
670
|
+
const clusterStateSnapshotPath = (0, node_path_1.join)(repoRoot, ".polaris", "clusters", state.cluster_id, "state.json");
|
|
671
|
+
(0, checkpoint_js_1.writeStateAtomic)(clusterStateSnapshotPath, state);
|
|
672
|
+
}
|
|
673
|
+
catch (snapshotError) {
|
|
674
|
+
process.stderr.write(`[11/14] WARNING: Failed to write cluster state snapshot: ${snapshotError instanceof Error ? snapshotError.message : String(snapshotError)}\n`);
|
|
675
|
+
process.stderr.write(`Delivery will proceed, but cluster state snapshot at .polaris/clusters/${state.cluster_id}/state.json may be incomplete.\n`);
|
|
676
|
+
}
|
|
483
677
|
// Step 12: Append JSONL events
|
|
484
678
|
console.log("[12/14] Appending JSONL events...");
|
|
485
679
|
const artifactDir = state.artifact_dir ?? (0, node_path_1.join)(repoRoot, ".taskchain_artifacts", "polaris-run");
|
|
@@ -489,7 +683,7 @@ async function runFinalize(options) {
|
|
|
489
683
|
console.log("[13/14] Updating Linear...");
|
|
490
684
|
const linearEnabled = config.tracker?.linear?.enabled ?? false;
|
|
491
685
|
const lifecyclePolicy = config.tracker?.lifecyclePolicy;
|
|
492
|
-
await (0, _11_update_linear_js_1.stepUpdateLinear)(state, branch, prUrl, true, linearEnabled, state.cluster_id, lifecyclePolicy);
|
|
686
|
+
await (0, _11_update_linear_js_1.stepUpdateLinear)(state, branch, prUrl, true, linearEnabled, state.cluster_id, lifecyclePolicy, authChildResult.authoritativeCount);
|
|
493
687
|
// Step 14: Archive run snapshot
|
|
494
688
|
console.log("[14/14] Archiving run snapshot...");
|
|
495
689
|
(0, _12_archive_js_1.stepArchive)(repoRoot, state, resolvedStateFile, reportPath);
|
package/dist/finalize/linear.js
CHANGED
|
@@ -97,6 +97,7 @@ async function findReviewState(issueId, apiKey) {
|
|
|
97
97
|
// Comment body builder
|
|
98
98
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
99
99
|
function buildCommentBody(opts) {
|
|
100
|
+
const childCount = opts.authoritativeChildCount ?? opts.state.completed_children.length;
|
|
100
101
|
const lines = [
|
|
101
102
|
`**polaris finalize complete** — run \`${opts.state.run_id}\``,
|
|
102
103
|
``,
|
|
@@ -104,7 +105,7 @@ function buildCommentBody(opts) {
|
|
|
104
105
|
`|---|---|`,
|
|
105
106
|
`| Branch | \`${opts.branch}\` |`,
|
|
106
107
|
`| PR | ${opts.prUrl} |`,
|
|
107
|
-
`| Children completed | ${
|
|
108
|
+
`| Children completed | ${childCount} |`,
|
|
108
109
|
`| Map validation | ${opts.validationPassed ? "✓ passed" : "✗ failed"} |`,
|
|
109
110
|
];
|
|
110
111
|
if (opts.reviewStateMissing) {
|
|
@@ -117,8 +118,8 @@ function buildCommentBody(opts) {
|
|
|
117
118
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
118
119
|
/** Posts a finalize-complete comment without attempting a state transition. */
|
|
119
120
|
async function postLinearComment(options) {
|
|
120
|
-
const { issueId, state, branch, prUrl, validationPassed, apiKey } = options;
|
|
121
|
-
const body = buildCommentBody({ state, branch, prUrl, validationPassed, reviewStateMissing: false });
|
|
121
|
+
const { issueId, state, branch, prUrl, validationPassed, apiKey, authoritativeChildCount } = options;
|
|
122
|
+
const body = buildCommentBody({ state, branch, prUrl, validationPassed, reviewStateMissing: false, authoritativeChildCount });
|
|
122
123
|
const data = await linearGraphQL(`mutation CreateComment($issueId: String!, $body: String!) {
|
|
123
124
|
commentCreate(input: { issueId: $issueId, body: $body }) { success }
|
|
124
125
|
}`, { issueId, body }, apiKey);
|
|
@@ -140,7 +141,7 @@ async function postLinearComment(options) {
|
|
|
140
141
|
* state ID. The assertNotDoneState guard enforces this at runtime.
|
|
141
142
|
*/
|
|
142
143
|
async function updateLinearIssueAfterFinalize(options) {
|
|
143
|
-
const { issueId, state, branch, prUrl, validationPassed, apiKey, lifecyclePolicy } = options;
|
|
144
|
+
const { issueId, state, branch, prUrl, validationPassed, apiKey, lifecyclePolicy, authoritativeChildCount } = options;
|
|
144
145
|
// Resolve lifecycle transition from policy
|
|
145
146
|
const lifecycleTransition = (0, lifecycle_policy_js_1.resolveLifecycleTransition)("parent-all-children-complete", lifecyclePolicy);
|
|
146
147
|
// If policy says skip, skip the state transition entirely
|
|
@@ -152,6 +153,7 @@ async function updateLinearIssueAfterFinalize(options) {
|
|
|
152
153
|
prUrl,
|
|
153
154
|
validationPassed,
|
|
154
155
|
reviewStateMissing: false,
|
|
156
|
+
authoritativeChildCount,
|
|
155
157
|
});
|
|
156
158
|
const commentData = await linearGraphQL(`mutation CreateComment($issueId: String!, $body: String!) {
|
|
157
159
|
commentCreate(input: { issueId: $issueId, body: $body }) { success }
|
|
@@ -172,6 +174,7 @@ async function updateLinearIssueAfterFinalize(options) {
|
|
|
172
174
|
prUrl,
|
|
173
175
|
validationPassed,
|
|
174
176
|
reviewStateMissing: false,
|
|
177
|
+
authoritativeChildCount,
|
|
175
178
|
});
|
|
176
179
|
const commentData = await linearGraphQL(`mutation CreateComment($issueId: String!, $body: String!) {
|
|
177
180
|
commentCreate(input: { issueId: $issueId, body: $body }) { success }
|
|
@@ -205,6 +208,7 @@ async function updateLinearIssueAfterFinalize(options) {
|
|
|
205
208
|
prUrl,
|
|
206
209
|
validationPassed,
|
|
207
210
|
reviewStateMissing: reviewState === null,
|
|
211
|
+
authoritativeChildCount,
|
|
208
212
|
});
|
|
209
213
|
const commentData = await linearGraphQL(`mutation CreateComment($issueId: String!, $body: String!) {
|
|
210
214
|
commentCreate(input: { issueId: $issueId, body: $body }) { success }
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.stepCreatePr = stepCreatePr;
|
|
4
4
|
const github_js_1 = require("../github.js");
|
|
5
|
-
function stepCreatePr(repoRoot, branch, state, draft) {
|
|
6
|
-
const prUrl = (0, github_js_1.createDraftPr)({ repoRoot, branch, state, draft });
|
|
5
|
+
function stepCreatePr(repoRoot, branch, state, draft, authoritativeChildCount) {
|
|
6
|
+
const prUrl = (0, github_js_1.createDraftPr)({ repoRoot, branch, state, draft, authoritativeChildCount });
|
|
7
7
|
console.log(`PR created: ${prUrl}`);
|
|
8
8
|
return prUrl;
|
|
9
9
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.stepUpdateLinear = stepUpdateLinear;
|
|
4
4
|
const linear_js_1 = require("../linear.js");
|
|
5
|
-
async function stepUpdateLinear(state, branch, prUrl, validationPassed, linearEnabled, parentIssueId, lifecyclePolicy) {
|
|
5
|
+
async function stepUpdateLinear(state, branch, prUrl, validationPassed, linearEnabled, parentIssueId, lifecyclePolicy, authoritativeChildCount) {
|
|
6
6
|
if (!linearEnabled) {
|
|
7
7
|
console.log("[11/12] Linear integration disabled — skipping.");
|
|
8
8
|
return;
|
|
@@ -17,6 +17,6 @@ async function stepUpdateLinear(state, branch, prUrl, validationPassed, linearEn
|
|
|
17
17
|
process.stderr.write("Warning: no Linear parent issue ID — skipping Linear update.\n");
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
|
-
await (0, linear_js_1.updateLinearIssueAfterFinalize)({ issueId, state, branch, prUrl, validationPassed, apiKey, lifecyclePolicy });
|
|
20
|
+
await (0, linear_js_1.updateLinearIssueAfterFinalize)({ issueId, state, branch, prUrl, validationPassed, apiKey, lifecyclePolicy, authoritativeChildCount });
|
|
21
21
|
console.log(`Linear parent ${issueId} updated.`);
|
|
22
22
|
}
|
package/dist/loop/parent.js
CHANGED
|
@@ -37,12 +37,15 @@ 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
39
|
const triggers_js_1 = require("../qc/triggers.js");
|
|
40
|
+
const index_js_1 = require("../qc/index.js");
|
|
41
|
+
const repair_loop_js_1 = require("../qc/repair-loop.js");
|
|
42
|
+
const worker_packet_js_2 = require("./worker-packet.js");
|
|
40
43
|
const run_bootstrap_js_1 = require("./run-bootstrap.js");
|
|
41
44
|
const ledger_js_1 = require("./ledger.js");
|
|
42
45
|
const dispatch_js_1 = require("./dispatch.js");
|
|
43
|
-
const
|
|
46
|
+
const index_js_2 = require("./router/index.js");
|
|
44
47
|
const child_selector_js_1 = require("../runtime/scheduling/child-selector.js");
|
|
45
|
-
const
|
|
48
|
+
const index_js_3 = require("../tracker/index.js");
|
|
46
49
|
const lifecycle_transition_js_1 = require("../tracker/lifecycle-transition.js");
|
|
47
50
|
const local_graph_js_1 = require("../tracker/local-graph.js");
|
|
48
51
|
const CLAIM_TTL_MS = 30 * 60 * 1000;
|
|
@@ -813,7 +816,7 @@ async function runParentLoop(options) {
|
|
|
813
816
|
if (openChildrenNeedingScope.length > 0) {
|
|
814
817
|
process.stderr.write(`[polaris] ${openChildrenNeedingScope.length} children missing scope — attempting tracker sync-in...\n`);
|
|
815
818
|
try {
|
|
816
|
-
await (0,
|
|
819
|
+
await (0, index_js_3.loadTrackerGraph)(config, state.cluster_id);
|
|
817
820
|
process.stderr.write(`[polaris] sync-in complete.\n`);
|
|
818
821
|
}
|
|
819
822
|
catch (syncErr) {
|
|
@@ -844,7 +847,7 @@ async function runParentLoop(options) {
|
|
|
844
847
|
max_concurrent: maxConcurrentWorkers,
|
|
845
848
|
claim_ttl_ms: CLAIM_TTL_MS,
|
|
846
849
|
get_dependencies: (childId) => localGraph?.getDependencies(childId) ?? [],
|
|
847
|
-
decide_route: ({ activeSlotsByProvider }) => (0,
|
|
850
|
+
decide_route: ({ activeSlotsByProvider }) => (0, index_js_2.decideWorkerRoute)({
|
|
848
851
|
role: "worker",
|
|
849
852
|
taskType: "impl",
|
|
850
853
|
adapter: adapterName,
|
|
@@ -884,6 +887,154 @@ async function runParentLoop(options) {
|
|
|
884
887
|
};
|
|
885
888
|
}
|
|
886
889
|
const autoFinalizeRequested = orchestrationMode === "auto" && config.orchestration?.auto_finalize === true;
|
|
890
|
+
// ── QC repair loop (post-completion gate) ─────────────────────────────
|
|
891
|
+
// When QC is enabled, run the completed-cluster QC trigger and, if
|
|
892
|
+
// findings are produced, run the bounded repair loop before halting.
|
|
893
|
+
// The repair loop is Foreman-owned: it dispatches repair workers via the
|
|
894
|
+
// same adapter and NEVER implements repairs inline.
|
|
895
|
+
if (!dryRun && config.qc?.enabled) {
|
|
896
|
+
const qcRegistry = (0, index_js_1.createQcRegistry)(config.qc);
|
|
897
|
+
let initialQcResult;
|
|
898
|
+
try {
|
|
899
|
+
initialQcResult = await (0, index_js_1.runQcAtTrigger)({
|
|
900
|
+
config: config.qc,
|
|
901
|
+
registry: qcRegistry,
|
|
902
|
+
trigger: "completed-cluster",
|
|
903
|
+
repoRoot,
|
|
904
|
+
runId: state.run_id,
|
|
905
|
+
clusterId: state.cluster_id,
|
|
906
|
+
branch: state.branch ?? getCurrentBranch(repoRoot),
|
|
907
|
+
telemetryFile,
|
|
908
|
+
state,
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
catch (err) {
|
|
912
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
913
|
+
appendTelemetry(telemetryFile, {
|
|
914
|
+
event: "qc-repair-loop-qc-run-error",
|
|
915
|
+
run_id: state.run_id,
|
|
916
|
+
error: msg,
|
|
917
|
+
timestamp: new Date().toISOString(),
|
|
918
|
+
});
|
|
919
|
+
// Non-fatal: proceed to cluster-complete without repair loop.
|
|
920
|
+
initialQcResult = null;
|
|
921
|
+
}
|
|
922
|
+
const hasFindings = initialQcResult !== null &&
|
|
923
|
+
initialQcResult.results.some((r) => r.findings.length > 0 && r.status !== "passed" && r.status !== "skipped");
|
|
924
|
+
if (initialQcResult !== null && hasFindings) {
|
|
925
|
+
const maxRepairRounds = config.qc.maxRepairRounds ?? repair_loop_js_1.DEFAULT_MAX_REPAIR_ROUNDS;
|
|
926
|
+
const priorLoopState = state.qc_repair_loop ?? null;
|
|
927
|
+
// Build the repair worker dispatcher using the existing adapter + provider.
|
|
928
|
+
const repairDispatcher = async (packet, round, manifest) => {
|
|
929
|
+
const repairPacketId = packet.packetId;
|
|
930
|
+
const dispatchId = (0, node_crypto_1.randomUUID)();
|
|
931
|
+
const repairWorkerId = `${state.run_id}:repair-${repairPacketId}:${Date.now()}`;
|
|
932
|
+
const repairResultPath = (0, node_path_1.join)(repoRoot, ".polaris", "clusters", state.cluster_id, "results", `repair-${repairPacketId}-${dispatchId}.json`);
|
|
933
|
+
const workerPacket = (0, worker_packet_js_2.compileRepairWorkerPacket)({
|
|
934
|
+
runId: state.run_id,
|
|
935
|
+
clusterId: state.cluster_id,
|
|
936
|
+
packetId: repairPacketId,
|
|
937
|
+
branch: state.branch ?? getCurrentBranch(repoRoot),
|
|
938
|
+
stateFile,
|
|
939
|
+
telemetryFile,
|
|
940
|
+
round,
|
|
941
|
+
allowedScope: packet.allowedScope,
|
|
942
|
+
prohibitedScope: packet.prohibitedScope,
|
|
943
|
+
validationCommands: packet.validationCommands,
|
|
944
|
+
rootCauseHint: packet.rootCauseHint,
|
|
945
|
+
resultFile: repairResultPath,
|
|
946
|
+
maxConcurrentWorkers,
|
|
947
|
+
});
|
|
948
|
+
appendTelemetry(telemetryFile, {
|
|
949
|
+
event: "repair-worker-dispatched",
|
|
950
|
+
run_id: state.run_id,
|
|
951
|
+
cluster_id: state.cluster_id,
|
|
952
|
+
packet_id: repairPacketId,
|
|
953
|
+
worker_id: repairWorkerId,
|
|
954
|
+
round,
|
|
955
|
+
timestamp: new Date().toISOString(),
|
|
956
|
+
});
|
|
957
|
+
try {
|
|
958
|
+
const dispatchResult = await adapter.dispatch(workerPacket, { provider: providerName, dryRun });
|
|
959
|
+
const workerSummary = parseWorkerSummary(dispatchResult.summary);
|
|
960
|
+
const success = workerSummary?.status === "done";
|
|
961
|
+
appendTelemetry(telemetryFile, {
|
|
962
|
+
event: "repair-worker-completed",
|
|
963
|
+
run_id: state.run_id,
|
|
964
|
+
packet_id: repairPacketId,
|
|
965
|
+
worker_id: repairWorkerId,
|
|
966
|
+
round,
|
|
967
|
+
status: success ? "success" : "failure",
|
|
968
|
+
timestamp: new Date().toISOString(),
|
|
969
|
+
});
|
|
970
|
+
return {
|
|
971
|
+
packetId: repairPacketId,
|
|
972
|
+
status: success ? "success" : "failure",
|
|
973
|
+
commitSha: workerSummary?.commit,
|
|
974
|
+
errorMessage: success ? undefined : String(workerSummary?.error_message ?? "repair worker failed"),
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
catch (err) {
|
|
978
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
979
|
+
appendTelemetry(telemetryFile, {
|
|
980
|
+
event: "repair-worker-error",
|
|
981
|
+
run_id: state.run_id,
|
|
982
|
+
packet_id: repairPacketId,
|
|
983
|
+
worker_id: repairWorkerId,
|
|
984
|
+
round,
|
|
985
|
+
error: msg,
|
|
986
|
+
timestamp: new Date().toISOString(),
|
|
987
|
+
});
|
|
988
|
+
return {
|
|
989
|
+
packetId: repairPacketId,
|
|
990
|
+
status: "failure",
|
|
991
|
+
errorMessage: msg,
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
try {
|
|
996
|
+
const repairLoopResult = await (0, repair_loop_js_1.runQcRepairLoop)({
|
|
997
|
+
clusterId: state.cluster_id,
|
|
998
|
+
runId: state.run_id,
|
|
999
|
+
branch: state.branch ?? getCurrentBranch(repoRoot),
|
|
1000
|
+
repoRoot,
|
|
1001
|
+
telemetryFile,
|
|
1002
|
+
config: config.qc,
|
|
1003
|
+
registry: qcRegistry,
|
|
1004
|
+
initialQcResults: initialQcResult.results,
|
|
1005
|
+
dispatchRepairWorker: repairDispatcher,
|
|
1006
|
+
maxRounds: maxRepairRounds,
|
|
1007
|
+
priorLoopState,
|
|
1008
|
+
onStateUpdate: (loopState) => {
|
|
1009
|
+
// Persist loop state to the state file on each mutation.
|
|
1010
|
+
const stateWithLoop = { ...state, qc_repair_loop: loopState };
|
|
1011
|
+
(0, checkpoint_js_1.writeStateAtomic)(stateFile, stateWithLoop);
|
|
1012
|
+
},
|
|
1013
|
+
});
|
|
1014
|
+
state = { ...state, qc_repair_loop: repairLoopResult.loop_state };
|
|
1015
|
+
(0, checkpoint_js_1.writeStateAtomic)(stateFile, state);
|
|
1016
|
+
appendTelemetry(telemetryFile, {
|
|
1017
|
+
event: "qc-repair-loop-finished",
|
|
1018
|
+
run_id: state.run_id,
|
|
1019
|
+
outcome: repairLoopResult.outcome,
|
|
1020
|
+
rounds_completed: repairLoopResult.rounds_completed,
|
|
1021
|
+
summary: repairLoopResult.summary,
|
|
1022
|
+
timestamp: new Date().toISOString(),
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
catch (err) {
|
|
1026
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1027
|
+
appendTelemetry(telemetryFile, {
|
|
1028
|
+
event: "qc-repair-loop-error",
|
|
1029
|
+
run_id: state.run_id,
|
|
1030
|
+
error: msg,
|
|
1031
|
+
timestamp: new Date().toISOString(),
|
|
1032
|
+
});
|
|
1033
|
+
// Non-fatal: proceed to cluster-complete.
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
// ── End QC repair loop ─────────────────────────────────────────────────
|
|
887
1038
|
// All children completed — write final state and halt
|
|
888
1039
|
if (!dryRun) {
|
|
889
1040
|
logStatus(notificationFormat, "COMPLETE");
|
|
@@ -1427,7 +1578,7 @@ async function runParentLoop(options) {
|
|
|
1427
1578
|
// Errors are logged to telemetry but do not fail the halt.
|
|
1428
1579
|
let adapter;
|
|
1429
1580
|
try {
|
|
1430
|
-
adapter = (0,
|
|
1581
|
+
adapter = (0, index_js_3.loadTrackerAdapter)(config);
|
|
1431
1582
|
}
|
|
1432
1583
|
catch (err) {
|
|
1433
1584
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -1880,35 +2031,40 @@ async function runParentLoop(options) {
|
|
|
1880
2031
|
};
|
|
1881
2032
|
}
|
|
1882
2033
|
// ── Step 05 (post-dispatch): Re-check budget before next iteration ───
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
2034
|
+
// Skip when open_children is empty: the cluster is complete and the
|
|
2035
|
+
// top-of-loop nextChild === null path handles cluster-complete and QC
|
|
2036
|
+
// repair-loop. Halting here would bypass QC repair on final-child completion.
|
|
2037
|
+
if (state.open_children.length > 0) {
|
|
2038
|
+
const postBudgetCheck = (0, budget_js_1.checkBudget)({
|
|
2039
|
+
childrenCompleted: state.context_budget.children_completed,
|
|
2040
|
+
lastChildStatus: workerStatus,
|
|
2041
|
+
policy: budgetPolicy,
|
|
2042
|
+
});
|
|
2043
|
+
if (postBudgetCheck.status === 'exhausted') {
|
|
2044
|
+
const nextPending = state.open_children[0] ?? null;
|
|
2045
|
+
if (!dryRun) {
|
|
2046
|
+
(0, checkpoint_js_1.writeStateAtomic)(stateFile, {
|
|
2047
|
+
...state,
|
|
2048
|
+
status: "budget-exhausted",
|
|
2049
|
+
step_cursor: "budget-check",
|
|
2050
|
+
next_open_child: nextPending,
|
|
2051
|
+
});
|
|
2052
|
+
appendTelemetry(telemetryFile, {
|
|
2053
|
+
event: "budget-exhausted",
|
|
2054
|
+
run_id: state.run_id,
|
|
2055
|
+
children_completed: state.context_budget.children_completed,
|
|
2056
|
+
next_child: nextPending,
|
|
2057
|
+
reason: postBudgetCheck.reason,
|
|
2058
|
+
timestamp: new Date().toISOString(),
|
|
2059
|
+
});
|
|
2060
|
+
}
|
|
2061
|
+
return {
|
|
2062
|
+
haltReason: 'budget-exhausted',
|
|
2063
|
+
childrenDispatched,
|
|
2064
|
+
haltingChild: nextPending ?? undefined,
|
|
2065
|
+
message: postBudgetCheck.reason,
|
|
2066
|
+
};
|
|
1905
2067
|
}
|
|
1906
|
-
return {
|
|
1907
|
-
haltReason: 'budget-exhausted',
|
|
1908
|
-
childrenDispatched,
|
|
1909
|
-
haltingChild: nextPending ?? undefined,
|
|
1910
|
-
message: postBudgetCheck.reason,
|
|
1911
|
-
};
|
|
1912
2068
|
}
|
|
1913
2069
|
// ── Step 06: CONTINUE (back to step 02) ─────────────────────────────
|
|
1914
2070
|
}
|