@lsctech/polaris 0.5.8 → 0.5.10

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.
@@ -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,16 +921,35 @@ 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;
927
951
  // Build the repair worker dispatcher using the existing adapter + provider.
928
- const repairDispatcher = async (packet, round, manifest) => {
952
+ const repairDispatcher = async (packet, round, manifest, signal) => {
929
953
  const repairPacketId = packet.packetId;
930
954
  const dispatchId = (0, node_crypto_1.randomUUID)();
931
955
  const repairWorkerId = `${state.run_id}:repair-${repairPacketId}:${Date.now()}`;
@@ -955,6 +979,12 @@ async function runParentLoop(options) {
955
979
  timestamp: new Date().toISOString(),
956
980
  });
957
981
  try {
982
+ // TODO: Propagate AbortSignal to adapter.dispatch() once ExecutionAdapter
983
+ // interface supports cancellation. Current limitation: timeout abandons the
984
+ // Promise but doesn't terminate the spawned worker process.
985
+ if (signal?.aborted) {
986
+ throw signal.reason || new Error("Repair dispatch aborted");
987
+ }
958
988
  const dispatchResult = await adapter.dispatch(workerPacket, { provider: providerName, dryRun });
959
989
  const workerSummary = parseWorkerSummary(dispatchResult.summary);
960
990
  const success = workerSummary?.status === "done";
@@ -1021,6 +1051,22 @@ async function runParentLoop(options) {
1021
1051
  summary: repairLoopResult.summary,
1022
1052
  timestamp: new Date().toISOString(),
1023
1053
  });
1054
+ // Append QC escalation symptoms for non-passing outcomes and post-repair findings.
1055
+ (0, qc_escalation_js_1.appendRepairLoopOutcomeSymptom)({
1056
+ runId: state.run_id,
1057
+ clusterId: state.cluster_id,
1058
+ repairResult: repairLoopResult,
1059
+ repoRoot,
1060
+ });
1061
+ if (repairLoopResult.final_qc_results.length > 0 && repairLoopResult.rounds_completed > 0) {
1062
+ (0, qc_escalation_js_1.appendQcEscalationSymptoms)({
1063
+ runId: state.run_id,
1064
+ clusterId: state.cluster_id,
1065
+ qcResults: repairLoopResult.final_qc_results,
1066
+ afterRepair: true,
1067
+ repoRoot,
1068
+ });
1069
+ }
1024
1070
  }
1025
1071
  catch (err) {
1026
1072
  const msg = err instanceof Error ? err.message : String(err);
@@ -1030,11 +1076,90 @@ async function runParentLoop(options) {
1030
1076
  error: msg,
1031
1077
  timestamp: new Date().toISOString(),
1032
1078
  });
1079
+ (0, foreman_symptoms_js_1.appendForemanSymptom)({
1080
+ runId: state.run_id,
1081
+ clusterId: state.cluster_id,
1082
+ code: "foreman-qc-runtime-failure",
1083
+ message: `QC repair loop threw a runtime error: ${msg}`,
1084
+ evidenceRefs: [telemetryFile],
1085
+ repoRoot,
1086
+ config,
1087
+ });
1033
1088
  // Non-fatal: proceed to cluster-complete.
1034
1089
  }
1035
1090
  }
1036
1091
  }
1037
1092
  // ── End QC repair loop ─────────────────────────────────────────────────
1093
+ // ── Run-health Medic consult ────────────────────────────────────────────
1094
+ // If the run recorded health symptoms and no Medic decision exists yet,
1095
+ // dispatch Medic to diagnose and optionally emit bounded treatment packets.
1096
+ if (!dryRun) {
1097
+ const runHealthReport = (0, index_js_4.readRunHealthReport)(state.run_id, repoRoot);
1098
+ if (runHealthReport && !(0, index_js_4.isMedicGateSatisfied)(runHealthReport)) {
1099
+ const maxTreatmentRounds = config.qc?.maxRepairRounds ?? repair_loop_js_1.DEFAULT_MAX_REPAIR_ROUNDS;
1100
+ const medicDispatchId = (0, node_crypto_1.randomUUID)();
1101
+ const medicResultPath = (0, node_path_1.join)(repoRoot, ".polaris", "clusters", state.cluster_id, "results", `medic-run-health-${medicDispatchId}.json`);
1102
+ const medicPacket = {
1103
+ role: "medic-run-health",
1104
+ run_id: state.run_id,
1105
+ dispatch_id: medicDispatchId,
1106
+ cluster_id: state.cluster_id,
1107
+ run_health_report_path: (0, index_js_4.getRunHealthReportPath)(state.run_id, repoRoot),
1108
+ qc_artifact_refs: Array.from(new Set([
1109
+ ...runHealthReport.evidence_refs,
1110
+ ...runHealthReport.symptoms.flatMap((s) => s.evidence_refs),
1111
+ ])),
1112
+ telemetry_path: telemetryFile,
1113
+ cluster_state_path: (0, node_path_1.join)(repoRoot, ".polaris", "clusters", state.cluster_id, "state.json"),
1114
+ policy_limits: { max_treatment_rounds: maxTreatmentRounds },
1115
+ result_path: medicResultPath,
1116
+ allowed_write_paths: [
1117
+ "smartdocs/medic/charts",
1118
+ ".polaris/runs",
1119
+ ".polaris/clusters",
1120
+ ".taskchain_artifacts",
1121
+ ],
1122
+ prohibited_write_paths: [],
1123
+ };
1124
+ try {
1125
+ await (0, run_health_consult_js_1.runMedicRunHealthConsult)({
1126
+ packet: medicPacket,
1127
+ repoRoot,
1128
+ stateFile,
1129
+ telemetryFile,
1130
+ branch: state.branch ?? getCurrentBranch(repoRoot),
1131
+ validationCommands: ["npm run build", "npm test"],
1132
+ maxConcurrentWorkers,
1133
+ dryRun,
1134
+ dispatchTreatmentWorkerFn: (input) => (0, treatment_packets_js_1.dispatchTreatmentWorker)({
1135
+ ...input,
1136
+ repoRoot,
1137
+ dispatch: (workerPacket) => adapter.dispatch(workerPacket, { provider: providerName }),
1138
+ }),
1139
+ });
1140
+ }
1141
+ catch (err) {
1142
+ const msg = err instanceof Error ? err.message : String(err);
1143
+ appendTelemetry(telemetryFile, {
1144
+ event: "medic-run-health-consult-error",
1145
+ run_id: state.run_id,
1146
+ cluster_id: state.cluster_id,
1147
+ error: msg,
1148
+ timestamp: new Date().toISOString(),
1149
+ });
1150
+ (0, foreman_symptoms_js_1.appendForemanSymptom)({
1151
+ runId: state.run_id,
1152
+ clusterId: state.cluster_id,
1153
+ code: "foreman-medic-runtime-failure",
1154
+ message: `Medic run-health consult threw a runtime error: ${msg}`,
1155
+ evidenceRefs: [telemetryFile],
1156
+ repoRoot,
1157
+ config,
1158
+ });
1159
+ }
1160
+ }
1161
+ }
1162
+ // ── End run-health Medic consult ───────────────────────────────────────
1038
1163
  // All children completed — write final state and halt
1039
1164
  if (!dryRun) {
1040
1165
  logStatus(notificationFormat, "COMPLETE");
@@ -1143,6 +1268,15 @@ async function runParentLoop(options) {
1143
1268
  reason: msg,
1144
1269
  timestamp: new Date().toISOString(),
1145
1270
  });
1271
+ (0, foreman_symptoms_js_1.appendForemanSymptom)({
1272
+ runId: state.run_id,
1273
+ clusterId: state.cluster_id,
1274
+ code: "foreman-dispatch-boundary-repair",
1275
+ message: `Dispatch boundary violation: ${msg}`,
1276
+ evidenceRefs: [telemetryFile],
1277
+ repoRoot,
1278
+ config,
1279
+ });
1146
1280
  }
1147
1281
  return {
1148
1282
  haltReason: 'worker-error',
@@ -1502,6 +1636,43 @@ async function runParentLoop(options) {
1502
1636
  sealedResult['status'] = 'failed';
1503
1637
  }
1504
1638
  finalWorkerSummary = sealedResult;
1639
+ // ── Ingest worker-reported run-health symptoms ────────────────────────
1640
+ // Only process when worker reports at least one symptom. Errors are
1641
+ // logged to telemetry but never block the run — symptom reporting is
1642
+ // advisory, not a gate.
1643
+ const rawSymptoms = sealedResult['run_health_symptoms'];
1644
+ if (!dryRun && Array.isArray(rawSymptoms) && rawSymptoms.length > 0) {
1645
+ const dispatchRecord = state.open_children_meta?.[nextChild]?.dispatch_record;
1646
+ try {
1647
+ (0, index_js_4.upsertWorkerSymptoms)({
1648
+ runId: state.run_id,
1649
+ clusterId: state.cluster_id,
1650
+ childId: nextChild,
1651
+ workerId: dispatchRecord?.worker_id,
1652
+ provider: dispatchRecord?.provider,
1653
+ symptoms: rawSymptoms,
1654
+ repoRoot,
1655
+ });
1656
+ appendTelemetry(telemetryFile, {
1657
+ event: "run-health-symptoms-ingested",
1658
+ run_id: state.run_id,
1659
+ child_id: nextChild,
1660
+ symptom_count: rawSymptoms.length,
1661
+ timestamp: new Date().toISOString(),
1662
+ });
1663
+ }
1664
+ catch (symptomErr) {
1665
+ const symptomMsg = symptomErr instanceof Error ? symptomErr.message : String(symptomErr);
1666
+ appendTelemetry(telemetryFile, {
1667
+ event: "run-health-ingest-error",
1668
+ run_id: state.run_id,
1669
+ child_id: nextChild,
1670
+ error: symptomMsg,
1671
+ timestamp: new Date().toISOString(),
1672
+ });
1673
+ // ponytail: consider surfacing ingest errors to operator via logStatus in a future pass
1674
+ }
1675
+ }
1505
1676
  }
1506
1677
  catch (error) {
1507
1678
  const msg = error instanceof Error ? error.message : String(error);
@@ -1514,6 +1685,15 @@ async function runParentLoop(options) {
1514
1685
  result_file: packet.result_file_contract.result_file,
1515
1686
  timestamp: new Date().toISOString(),
1516
1687
  });
1688
+ (0, foreman_symptoms_js_1.appendForemanSymptom)({
1689
+ runId: state.run_id,
1690
+ clusterId: state.cluster_id,
1691
+ code: "foreman-packet-repair",
1692
+ message: `Failed to read sealed result file for ${nextChild}: ${msg}`,
1693
+ evidenceRefs: [telemetryFile],
1694
+ repoRoot,
1695
+ config,
1696
+ });
1517
1697
  }
1518
1698
  return {
1519
1699
  haltReason: 'worker-error',
@@ -1540,6 +1720,15 @@ async function runParentLoop(options) {
1540
1720
  error: errMsg,
1541
1721
  timestamp: new Date().toISOString(),
1542
1722
  });
1723
+ (0, foreman_symptoms_js_1.appendForemanSymptom)({
1724
+ runId: state.run_id,
1725
+ clusterId: state.cluster_id,
1726
+ code: "foreman-wrong-run-telemetry",
1727
+ message: errMsg,
1728
+ evidenceRefs: [telemetryFile],
1729
+ repoRoot,
1730
+ config,
1731
+ });
1543
1732
  }
1544
1733
  return {
1545
1734
  haltReason: 'worker-error',
@@ -1709,6 +1898,15 @@ async function runParentLoop(options) {
1709
1898
  error: errMsg,
1710
1899
  timestamp: new Date().toISOString(),
1711
1900
  });
1901
+ (0, foreman_symptoms_js_1.appendForemanSymptom)({
1902
+ runId: state.run_id,
1903
+ clusterId: state.cluster_id,
1904
+ code: "foreman-state-repair",
1905
+ message: `State file validation failed after worker execution; using pre-dispatch fallback: ${errMsg}`,
1906
+ evidenceRefs: [telemetryFile],
1907
+ repoRoot,
1908
+ config,
1909
+ });
1712
1910
  state = stateBeforeDispatch;
1713
1911
  }
1714
1912
  else if (!dryRun) {
@@ -1747,6 +1945,15 @@ async function runParentLoop(options) {
1747
1945
  error: errMsg,
1748
1946
  timestamp: new Date().toISOString(),
1749
1947
  });
1948
+ (0, foreman_symptoms_js_1.appendForemanSymptom)({
1949
+ runId: state.run_id,
1950
+ clusterId: state.cluster_id,
1951
+ code: "foreman-state-repair",
1952
+ message: `State reload threw after worker execution; using pre-dispatch fallback: ${errMsg}`,
1953
+ evidenceRefs: [telemetryFile],
1954
+ repoRoot,
1955
+ config,
1956
+ });
1750
1957
  state = stateBeforeDispatch;
1751
1958
  }
1752
1959
  else if (!dryRun) {
@@ -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
  ];