@lsctech/polaris 0.5.7 → 0.5.9

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 (36) hide show
  1. package/dist/autoresearch/index.js +30 -1
  2. package/dist/autoresearch/sol-evaluation-writer.js +135 -0
  3. package/dist/autoresearch/sol-evidence-normalizer.js +337 -0
  4. package/dist/autoresearch/sol-recommendations.js +130 -0
  5. package/dist/autoresearch/sol-report-renderer.js +282 -0
  6. package/dist/autoresearch/sol-run-health-bridge.js +246 -0
  7. package/dist/autoresearch/sol-scorecard-calculator.js +865 -0
  8. package/dist/cli/autoresearch.js +77 -0
  9. package/dist/cli/medic.js +65 -0
  10. package/dist/cluster-state/store.js +56 -0
  11. package/dist/config/defaults.js +3 -0
  12. package/dist/config/validator.js +141 -0
  13. package/dist/finalize/artifact-policy.js +2 -0
  14. package/dist/finalize/github.js +13 -9
  15. package/dist/finalize/index.js +231 -5
  16. package/dist/finalize/linear.js +8 -4
  17. package/dist/finalize/medic-gate.js +42 -0
  18. package/dist/finalize/steps/08-create-pr.js +2 -2
  19. package/dist/finalize/steps/11-update-linear.js +2 -2
  20. package/dist/loop/parent.js +234 -28
  21. package/dist/loop/worker-packet.js +19 -1
  22. package/dist/medic/run-health-consult.js +381 -0
  23. package/dist/medic/treatment-packets.js +169 -0
  24. package/dist/qc/artifacts.js +27 -0
  25. package/dist/qc/orchestration.js +3 -2
  26. package/dist/qc/providers/coderabbit.js +114 -11
  27. package/dist/qc/repair-loop.js +49 -14
  28. package/dist/qc/runner.js +10 -2
  29. package/dist/qc/schemas.js +1 -0
  30. package/dist/run-health/foreman-symptoms.js +100 -0
  31. package/dist/run-health/index.js +301 -0
  32. package/dist/run-health/qc-escalation.js +155 -0
  33. package/dist/run-health/schema.js +123 -0
  34. package/dist/types/sol-metrics.js +108 -0
  35. package/dist/types/sol-scorecard.js +148 -0
  36. package/package.json +1 -1
@@ -48,6 +48,11 @@ const child_selector_js_1 = require("../runtime/scheduling/child-selector.js");
48
48
  const index_js_3 = require("../tracker/index.js");
49
49
  const lifecycle_transition_js_1 = require("../tracker/lifecycle-transition.js");
50
50
  const local_graph_js_1 = require("../tracker/local-graph.js");
51
+ const index_js_4 = require("../run-health/index.js");
52
+ const foreman_symptoms_js_1 = require("../run-health/foreman-symptoms.js");
53
+ const qc_escalation_js_1 = require("../run-health/qc-escalation.js");
54
+ const run_health_consult_js_1 = require("../medic/run-health-consult.js");
55
+ const treatment_packets_js_1 = require("../medic/treatment-packets.js");
51
56
  const CLAIM_TTL_MS = 30 * 60 * 1000;
52
57
  /**
53
58
  * Returns the list of files touched by a git commit, or null when the commit
@@ -916,11 +921,30 @@ async function runParentLoop(options) {
916
921
  error: msg,
917
922
  timestamp: new Date().toISOString(),
918
923
  });
924
+ (0, foreman_symptoms_js_1.appendForemanSymptom)({
925
+ runId: state.run_id,
926
+ clusterId: state.cluster_id,
927
+ code: "foreman-qc-runtime-failure",
928
+ message: `QC run at completed-cluster trigger threw a runtime error: ${msg}`,
929
+ evidenceRefs: [telemetryFile],
930
+ repoRoot,
931
+ config,
932
+ });
919
933
  // Non-fatal: proceed to cluster-complete without repair loop.
920
934
  initialQcResult = null;
921
935
  }
922
936
  const hasFindings = initialQcResult !== null &&
923
937
  initialQcResult.results.some((r) => r.findings.length > 0 && r.status !== "passed" && r.status !== "skipped");
938
+ // Append QC escalation symptoms for initial findings (blocking / parse failures / etc.)
939
+ if (initialQcResult !== null && initialQcResult.results.length > 0) {
940
+ (0, qc_escalation_js_1.appendQcEscalationSymptoms)({
941
+ runId: state.run_id,
942
+ clusterId: state.cluster_id,
943
+ qcResults: initialQcResult.results,
944
+ afterRepair: false,
945
+ repoRoot,
946
+ });
947
+ }
924
948
  if (initialQcResult !== null && hasFindings) {
925
949
  const maxRepairRounds = config.qc.maxRepairRounds ?? repair_loop_js_1.DEFAULT_MAX_REPAIR_ROUNDS;
926
950
  const priorLoopState = state.qc_repair_loop ?? null;
@@ -1021,6 +1045,22 @@ async function runParentLoop(options) {
1021
1045
  summary: repairLoopResult.summary,
1022
1046
  timestamp: new Date().toISOString(),
1023
1047
  });
1048
+ // Append QC escalation symptoms for non-passing outcomes and post-repair findings.
1049
+ (0, qc_escalation_js_1.appendRepairLoopOutcomeSymptom)({
1050
+ runId: state.run_id,
1051
+ clusterId: state.cluster_id,
1052
+ repairResult: repairLoopResult,
1053
+ repoRoot,
1054
+ });
1055
+ if (repairLoopResult.final_qc_results.length > 0 && repairLoopResult.rounds_completed > 0) {
1056
+ (0, qc_escalation_js_1.appendQcEscalationSymptoms)({
1057
+ runId: state.run_id,
1058
+ clusterId: state.cluster_id,
1059
+ qcResults: repairLoopResult.final_qc_results,
1060
+ afterRepair: true,
1061
+ repoRoot,
1062
+ });
1063
+ }
1024
1064
  }
1025
1065
  catch (err) {
1026
1066
  const msg = err instanceof Error ? err.message : String(err);
@@ -1030,11 +1070,90 @@ async function runParentLoop(options) {
1030
1070
  error: msg,
1031
1071
  timestamp: new Date().toISOString(),
1032
1072
  });
1073
+ (0, foreman_symptoms_js_1.appendForemanSymptom)({
1074
+ runId: state.run_id,
1075
+ clusterId: state.cluster_id,
1076
+ code: "foreman-qc-runtime-failure",
1077
+ message: `QC repair loop threw a runtime error: ${msg}`,
1078
+ evidenceRefs: [telemetryFile],
1079
+ repoRoot,
1080
+ config,
1081
+ });
1033
1082
  // Non-fatal: proceed to cluster-complete.
1034
1083
  }
1035
1084
  }
1036
1085
  }
1037
1086
  // ── End QC repair loop ─────────────────────────────────────────────────
1087
+ // ── Run-health Medic consult ────────────────────────────────────────────
1088
+ // If the run recorded health symptoms and no Medic decision exists yet,
1089
+ // dispatch Medic to diagnose and optionally emit bounded treatment packets.
1090
+ if (!dryRun) {
1091
+ const runHealthReport = (0, index_js_4.readRunHealthReport)(state.run_id, repoRoot);
1092
+ if (runHealthReport && !(0, index_js_4.isMedicGateSatisfied)(runHealthReport)) {
1093
+ const maxTreatmentRounds = config.qc?.maxRepairRounds ?? repair_loop_js_1.DEFAULT_MAX_REPAIR_ROUNDS;
1094
+ const medicDispatchId = (0, node_crypto_1.randomUUID)();
1095
+ const medicResultPath = (0, node_path_1.join)(repoRoot, ".polaris", "clusters", state.cluster_id, "results", `medic-run-health-${medicDispatchId}.json`);
1096
+ const medicPacket = {
1097
+ role: "medic-run-health",
1098
+ run_id: state.run_id,
1099
+ dispatch_id: medicDispatchId,
1100
+ cluster_id: state.cluster_id,
1101
+ run_health_report_path: (0, index_js_4.getRunHealthReportPath)(state.run_id, repoRoot),
1102
+ qc_artifact_refs: Array.from(new Set([
1103
+ ...runHealthReport.evidence_refs,
1104
+ ...runHealthReport.symptoms.flatMap((s) => s.evidence_refs),
1105
+ ])),
1106
+ telemetry_path: telemetryFile,
1107
+ cluster_state_path: (0, node_path_1.join)(repoRoot, ".polaris", "clusters", state.cluster_id, "state.json"),
1108
+ policy_limits: { max_treatment_rounds: maxTreatmentRounds },
1109
+ result_path: medicResultPath,
1110
+ allowed_write_paths: [
1111
+ "smartdocs/medic/charts",
1112
+ ".polaris/runs",
1113
+ ".polaris/clusters",
1114
+ ".taskchain_artifacts",
1115
+ ],
1116
+ prohibited_write_paths: [],
1117
+ };
1118
+ try {
1119
+ await (0, run_health_consult_js_1.runMedicRunHealthConsult)({
1120
+ packet: medicPacket,
1121
+ repoRoot,
1122
+ stateFile,
1123
+ telemetryFile,
1124
+ branch: state.branch ?? getCurrentBranch(repoRoot),
1125
+ validationCommands: ["npm run build", "npm test"],
1126
+ maxConcurrentWorkers,
1127
+ dryRun,
1128
+ dispatchTreatmentWorkerFn: (input) => (0, treatment_packets_js_1.dispatchTreatmentWorker)({
1129
+ ...input,
1130
+ repoRoot,
1131
+ dispatch: (workerPacket) => adapter.dispatch(workerPacket, { provider: providerName }),
1132
+ }),
1133
+ });
1134
+ }
1135
+ catch (err) {
1136
+ const msg = err instanceof Error ? err.message : String(err);
1137
+ appendTelemetry(telemetryFile, {
1138
+ event: "medic-run-health-consult-error",
1139
+ run_id: state.run_id,
1140
+ cluster_id: state.cluster_id,
1141
+ error: msg,
1142
+ timestamp: new Date().toISOString(),
1143
+ });
1144
+ (0, foreman_symptoms_js_1.appendForemanSymptom)({
1145
+ runId: state.run_id,
1146
+ clusterId: state.cluster_id,
1147
+ code: "foreman-medic-runtime-failure",
1148
+ message: `Medic run-health consult threw a runtime error: ${msg}`,
1149
+ evidenceRefs: [telemetryFile],
1150
+ repoRoot,
1151
+ config,
1152
+ });
1153
+ }
1154
+ }
1155
+ }
1156
+ // ── End run-health Medic consult ───────────────────────────────────────
1038
1157
  // All children completed — write final state and halt
1039
1158
  if (!dryRun) {
1040
1159
  logStatus(notificationFormat, "COMPLETE");
@@ -1143,6 +1262,15 @@ async function runParentLoop(options) {
1143
1262
  reason: msg,
1144
1263
  timestamp: new Date().toISOString(),
1145
1264
  });
1265
+ (0, foreman_symptoms_js_1.appendForemanSymptom)({
1266
+ runId: state.run_id,
1267
+ clusterId: state.cluster_id,
1268
+ code: "foreman-dispatch-boundary-repair",
1269
+ message: `Dispatch boundary violation: ${msg}`,
1270
+ evidenceRefs: [telemetryFile],
1271
+ repoRoot,
1272
+ config,
1273
+ });
1146
1274
  }
1147
1275
  return {
1148
1276
  haltReason: 'worker-error',
@@ -1502,6 +1630,43 @@ async function runParentLoop(options) {
1502
1630
  sealedResult['status'] = 'failed';
1503
1631
  }
1504
1632
  finalWorkerSummary = sealedResult;
1633
+ // ── Ingest worker-reported run-health symptoms ────────────────────────
1634
+ // Only process when worker reports at least one symptom. Errors are
1635
+ // logged to telemetry but never block the run — symptom reporting is
1636
+ // advisory, not a gate.
1637
+ const rawSymptoms = sealedResult['run_health_symptoms'];
1638
+ if (!dryRun && Array.isArray(rawSymptoms) && rawSymptoms.length > 0) {
1639
+ const dispatchRecord = state.open_children_meta?.[nextChild]?.dispatch_record;
1640
+ try {
1641
+ (0, index_js_4.upsertWorkerSymptoms)({
1642
+ runId: state.run_id,
1643
+ clusterId: state.cluster_id,
1644
+ childId: nextChild,
1645
+ workerId: dispatchRecord?.worker_id,
1646
+ provider: dispatchRecord?.provider,
1647
+ symptoms: rawSymptoms,
1648
+ repoRoot,
1649
+ });
1650
+ appendTelemetry(telemetryFile, {
1651
+ event: "run-health-symptoms-ingested",
1652
+ run_id: state.run_id,
1653
+ child_id: nextChild,
1654
+ symptom_count: rawSymptoms.length,
1655
+ timestamp: new Date().toISOString(),
1656
+ });
1657
+ }
1658
+ catch (symptomErr) {
1659
+ const symptomMsg = symptomErr instanceof Error ? symptomErr.message : String(symptomErr);
1660
+ appendTelemetry(telemetryFile, {
1661
+ event: "run-health-ingest-error",
1662
+ run_id: state.run_id,
1663
+ child_id: nextChild,
1664
+ error: symptomMsg,
1665
+ timestamp: new Date().toISOString(),
1666
+ });
1667
+ // ponytail: consider surfacing ingest errors to operator via logStatus in a future pass
1668
+ }
1669
+ }
1505
1670
  }
1506
1671
  catch (error) {
1507
1672
  const msg = error instanceof Error ? error.message : String(error);
@@ -1514,6 +1679,15 @@ async function runParentLoop(options) {
1514
1679
  result_file: packet.result_file_contract.result_file,
1515
1680
  timestamp: new Date().toISOString(),
1516
1681
  });
1682
+ (0, foreman_symptoms_js_1.appendForemanSymptom)({
1683
+ runId: state.run_id,
1684
+ clusterId: state.cluster_id,
1685
+ code: "foreman-packet-repair",
1686
+ message: `Failed to read sealed result file for ${nextChild}: ${msg}`,
1687
+ evidenceRefs: [telemetryFile],
1688
+ repoRoot,
1689
+ config,
1690
+ });
1517
1691
  }
1518
1692
  return {
1519
1693
  haltReason: 'worker-error',
@@ -1540,6 +1714,15 @@ async function runParentLoop(options) {
1540
1714
  error: errMsg,
1541
1715
  timestamp: new Date().toISOString(),
1542
1716
  });
1717
+ (0, foreman_symptoms_js_1.appendForemanSymptom)({
1718
+ runId: state.run_id,
1719
+ clusterId: state.cluster_id,
1720
+ code: "foreman-wrong-run-telemetry",
1721
+ message: errMsg,
1722
+ evidenceRefs: [telemetryFile],
1723
+ repoRoot,
1724
+ config,
1725
+ });
1543
1726
  }
1544
1727
  return {
1545
1728
  haltReason: 'worker-error',
@@ -1709,6 +1892,15 @@ async function runParentLoop(options) {
1709
1892
  error: errMsg,
1710
1893
  timestamp: new Date().toISOString(),
1711
1894
  });
1895
+ (0, foreman_symptoms_js_1.appendForemanSymptom)({
1896
+ runId: state.run_id,
1897
+ clusterId: state.cluster_id,
1898
+ code: "foreman-state-repair",
1899
+ message: `State file validation failed after worker execution; using pre-dispatch fallback: ${errMsg}`,
1900
+ evidenceRefs: [telemetryFile],
1901
+ repoRoot,
1902
+ config,
1903
+ });
1712
1904
  state = stateBeforeDispatch;
1713
1905
  }
1714
1906
  else if (!dryRun) {
@@ -1747,6 +1939,15 @@ async function runParentLoop(options) {
1747
1939
  error: errMsg,
1748
1940
  timestamp: new Date().toISOString(),
1749
1941
  });
1942
+ (0, foreman_symptoms_js_1.appendForemanSymptom)({
1943
+ runId: state.run_id,
1944
+ clusterId: state.cluster_id,
1945
+ code: "foreman-state-repair",
1946
+ message: `State reload threw after worker execution; using pre-dispatch fallback: ${errMsg}`,
1947
+ evidenceRefs: [telemetryFile],
1948
+ repoRoot,
1949
+ config,
1950
+ });
1750
1951
  state = stateBeforeDispatch;
1751
1952
  }
1752
1953
  else if (!dryRun) {
@@ -2031,35 +2232,40 @@ async function runParentLoop(options) {
2031
2232
  };
2032
2233
  }
2033
2234
  // ── Step 05 (post-dispatch): Re-check budget before next iteration ───
2034
- const postBudgetCheck = (0, budget_js_1.checkBudget)({
2035
- childrenCompleted: state.context_budget.children_completed,
2036
- lastChildStatus: workerStatus,
2037
- policy: budgetPolicy,
2038
- });
2039
- if (postBudgetCheck.status === 'exhausted') {
2040
- const nextPending = state.open_children[0] ?? null;
2041
- if (!dryRun) {
2042
- (0, checkpoint_js_1.writeStateAtomic)(stateFile, {
2043
- ...state,
2044
- status: "budget-exhausted",
2045
- step_cursor: "budget-check",
2046
- next_open_child: nextPending,
2047
- });
2048
- appendTelemetry(telemetryFile, {
2049
- event: "budget-exhausted",
2050
- run_id: state.run_id,
2051
- children_completed: state.context_budget.children_completed,
2052
- next_child: nextPending,
2053
- reason: postBudgetCheck.reason,
2054
- timestamp: new Date().toISOString(),
2055
- });
2235
+ // Skip when open_children is empty: the cluster is complete and the
2236
+ // top-of-loop nextChild === null path handles cluster-complete and QC
2237
+ // repair-loop. Halting here would bypass QC repair on final-child completion.
2238
+ if (state.open_children.length > 0) {
2239
+ const postBudgetCheck = (0, budget_js_1.checkBudget)({
2240
+ childrenCompleted: state.context_budget.children_completed,
2241
+ lastChildStatus: workerStatus,
2242
+ policy: budgetPolicy,
2243
+ });
2244
+ if (postBudgetCheck.status === 'exhausted') {
2245
+ const nextPending = state.open_children[0] ?? null;
2246
+ if (!dryRun) {
2247
+ (0, checkpoint_js_1.writeStateAtomic)(stateFile, {
2248
+ ...state,
2249
+ status: "budget-exhausted",
2250
+ step_cursor: "budget-check",
2251
+ next_open_child: nextPending,
2252
+ });
2253
+ appendTelemetry(telemetryFile, {
2254
+ event: "budget-exhausted",
2255
+ run_id: state.run_id,
2256
+ children_completed: state.context_budget.children_completed,
2257
+ next_child: nextPending,
2258
+ reason: postBudgetCheck.reason,
2259
+ timestamp: new Date().toISOString(),
2260
+ });
2261
+ }
2262
+ return {
2263
+ haltReason: 'budget-exhausted',
2264
+ childrenDispatched,
2265
+ haltingChild: nextPending ?? undefined,
2266
+ message: postBudgetCheck.reason,
2267
+ };
2056
2268
  }
2057
- return {
2058
- haltReason: 'budget-exhausted',
2059
- childrenDispatched,
2060
- haltingChild: nextPending ?? undefined,
2061
- message: postBudgetCheck.reason,
2062
- };
2063
2269
  }
2064
2270
  // ── Step 06: CONTINUE (back to step 02) ─────────────────────────────
2065
2271
  }
@@ -11,7 +11,7 @@
11
11
  * accept BootstrapPacket transparently accept WorkerPacket.
12
12
  */
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
- exports.REPAIR_RETURN_CONTRACT = exports.STARTUP_RETURN_CONTRACT = exports.PREFLIGHT_RETURN_CONTRACT = exports.WORKER_PROHIBITED_WRITE_PATHS = exports.FINALIZE_RETURN_CONTRACT = exports.IMPL_RETURN_CONTRACT = void 0;
14
+ exports.REPAIR_RETURN_CONTRACT = exports.STARTUP_RETURN_CONTRACT = exports.PREFLIGHT_RETURN_CONTRACT = exports.WORKER_SYMPTOM_CATEGORIES = exports.WORKER_PROHIBITED_WRITE_PATHS = exports.FINALIZE_RETURN_CONTRACT = exports.IMPL_RETURN_CONTRACT = void 0;
15
15
  exports.roleContextForWorkerRole = roleContextForWorkerRole;
16
16
  exports.isWorkerPacket = isWorkerPacket;
17
17
  exports.getWorkerCommitPolicy = getWorkerCommitPolicy;
@@ -130,6 +130,21 @@ exports.WORKER_PROHIBITED_WRITE_PATHS = [
130
130
  ".polaris/runs/",
131
131
  "**/telemetry.jsonl",
132
132
  ];
133
+ // ── Worker symptom reporting ──────────────────────────────────────────────────
134
+ /**
135
+ * Canonical symptom categories workers may use in their sealed result.
136
+ * Workers observe and report — they do NOT diagnose root cause.
137
+ *
138
+ * Include run_health_symptoms in the sealed result only when at least one
139
+ * symptom occurred. When no symptoms occurred, omit the field entirely.
140
+ */
141
+ exports.WORKER_SYMPTOM_CATEGORIES = [
142
+ "worker-blocked", // Could not proceed (missing info, approval needed, out-of-scope)
143
+ "validation-failed", // Build / test / lint validation commands failed
144
+ "repeated-rework", // Same fix attempted multiple times without success
145
+ "unclear-requirements", // Requirements are contradictory or too ambiguous to act on
146
+ "unusual-assumption", // Had to assume something outside normal scope to proceed
147
+ ];
133
148
  function getWorkerCommitPolicy(packet) {
134
149
  return {
135
150
  allowedScope: [...packet.instructions.allowed_scope],
@@ -255,6 +270,9 @@ function compileImplPacket(input) {
255
270
  `Create exactly ONE git commit: [${input.childId}] ${childTitle}.`,
256
271
  `Do NOT modify open_children or completed_children in ${input.stateFile}. The parent runtime (loop continue) owns that state transition.`,
257
272
  `Append a telemetry event to ${input.telemetryFile}.`,
273
+ `If you encountered any struggle signals during execution, include a "run_health_symptoms" array in your sealed result. ` +
274
+ `Use one of these categories: ${exports.WORKER_SYMPTOM_CATEGORIES.join(', ')}. ` +
275
+ `Report what you observed — do NOT diagnose root cause. Omit the field if no symptoms occurred.`,
258
276
  `Write compact return JSON to stdout (fields: ${exports.IMPL_RETURN_CONTRACT.join(', ')}). next_recommended_action MUST be exactly "continue" on success, "stop" on unresolvable blocker, or "investigate" if manual review is needed.`,
259
277
  `TERMINATE SESSION IMMEDIATELY. Do not select or execute the next child.`,
260
278
  ];