@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.
- package/dist/autoresearch/sol-run-health-bridge.js +246 -0
- package/dist/cli/adopt-canon.js +16 -5
- package/dist/cli/adopt-cognition.js +23 -8
- package/dist/cli/medic.js +65 -0
- package/dist/cognition/closeout-librarian-types.js +53 -0
- package/dist/cognition/librarian-packet.js +25 -1
- package/dist/cognition/validate.js +85 -3
- package/dist/config/defaults.js +4 -0
- package/dist/config/validator.js +141 -0
- package/dist/finalize/index.js +224 -5
- package/dist/finalize/medic-gate.js +42 -0
- package/dist/loop/adapters/terminal-cli.js +3 -2
- package/dist/loop/parent.js +208 -1
- package/dist/loop/worker-packet.js +19 -1
- package/dist/medic/run-health-consult.js +381 -0
- package/dist/medic/treatment-packets.js +169 -0
- package/dist/qc/providers/coderabbit.js +90 -18
- package/dist/qc/repair-loop.js +108 -28
- package/dist/qc/runner.js +7 -1
- package/dist/run-health/foreman-symptoms.js +100 -0
- package/dist/run-health/index.js +301 -0
- package/dist/run-health/qc-escalation.js +155 -0
- package/dist/run-health/schema.js +123 -0
- package/dist/smartdocs-engine/validate-instructions.js +60 -2
- package/package.json +1 -1
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SOL → run-health bridge.
|
|
4
|
+
*
|
|
5
|
+
* Evaluates SOL score reports against operator-configured thresholds and,
|
|
6
|
+
* when policy enables it, appends run-health symptoms so that Medic can be
|
|
7
|
+
* engaged even when no worker explicitly wrote symptoms.
|
|
8
|
+
*
|
|
9
|
+
* Design rules:
|
|
10
|
+
* - SOL is ADVISORY by default. No symptoms are created unless the operator
|
|
11
|
+
* sets `sol.thresholds.enabled = true` AND at least one policy flag
|
|
12
|
+
* (`createRunHealthReport` or `requireMedic`) is true.
|
|
13
|
+
* - Symptoms reference the SOL evidence file as their source; they do NOT
|
|
14
|
+
* mutate raw metric artifacts.
|
|
15
|
+
* - Each threshold crossing produces exactly one symptom per evaluation.
|
|
16
|
+
* Re-evaluating the same run does not duplicate symptoms because the
|
|
17
|
+
* symptom id encodes the threshold name.
|
|
18
|
+
* - Never throws — threshold evaluation must not block the run.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.detectSolThresholdCrossings = detectSolThresholdCrossings;
|
|
22
|
+
exports.evaluateSolThresholds = evaluateSolThresholds;
|
|
23
|
+
const index_js_1 = require("../run-health/index.js");
|
|
24
|
+
// ── Defaults ─────────────────────────────────────────────────────────────────
|
|
25
|
+
const DEFAULT_LOW_COMPOSITE_SCORE = 0.4;
|
|
26
|
+
const DEFAULT_QC_REPAIR_LOOP_FAILURE_STATUSES = [
|
|
27
|
+
"max-rounds",
|
|
28
|
+
"medic-referral",
|
|
29
|
+
"all-providers-failed",
|
|
30
|
+
];
|
|
31
|
+
const DEFAULT_REPEATED_PROVIDER_FAILURES = 3;
|
|
32
|
+
const DEFAULT_FOREMAN_INTERVENTION_COUNT = 2;
|
|
33
|
+
const DEFAULT_STALE_WRONG_RUN_TELEMETRY = true;
|
|
34
|
+
const DEFAULT_VALIDATION_FAILURES = 2;
|
|
35
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
36
|
+
function makeSolSourceActor() {
|
|
37
|
+
return { role: "sol" };
|
|
38
|
+
}
|
|
39
|
+
function makeSolSymptom(crossing) {
|
|
40
|
+
return {
|
|
41
|
+
// Deterministic id: code only (no UUID) so repeated evaluations of the
|
|
42
|
+
// same run are idempotent — the same threshold crossing produces the
|
|
43
|
+
// same id, and a caller that deduplicates by id will skip the duplicate.
|
|
44
|
+
// ponytail: when run-health supports upsert-by-id, switch to that.
|
|
45
|
+
id: `sol:${crossing.code}`,
|
|
46
|
+
severity: solThresholdSeverity(crossing.code),
|
|
47
|
+
code: crossing.code,
|
|
48
|
+
message: crossing.message,
|
|
49
|
+
source_actor: makeSolSourceActor(),
|
|
50
|
+
evidence_refs: crossing.evidenceRefs,
|
|
51
|
+
occurred_at: new Date().toISOString(),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function solThresholdSeverity(code) {
|
|
55
|
+
switch (code) {
|
|
56
|
+
case "sol-qc-repair-loop-failure":
|
|
57
|
+
case "sol-repeated-provider-failures":
|
|
58
|
+
return "high";
|
|
59
|
+
case "sol-low-composite-score":
|
|
60
|
+
case "sol-high-foreman-intervention":
|
|
61
|
+
case "sol-validation-failures":
|
|
62
|
+
return "medium";
|
|
63
|
+
case "sol-stale-wrong-run-telemetry":
|
|
64
|
+
return "high";
|
|
65
|
+
default:
|
|
66
|
+
return "medium";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function upsertSolSymptom(runId, clusterId, symptom, repoRoot) {
|
|
70
|
+
const existing = (0, index_js_1.readRunHealthReport)(runId, repoRoot);
|
|
71
|
+
if (!existing) {
|
|
72
|
+
(0, index_js_1.createRunHealthReport)({
|
|
73
|
+
runId,
|
|
74
|
+
clusterId,
|
|
75
|
+
firstSymptom: symptom,
|
|
76
|
+
sourceActor: makeSolSourceActor(),
|
|
77
|
+
repoRoot,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
// Deduplicate: skip if a symptom with the same id already exists
|
|
82
|
+
if (existing.symptoms.some((s) => s.id === symptom.id)) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
(0, index_js_1.appendSymptom)(runId, symptom, repoRoot);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// ── Threshold evaluation ──────────────────────────────────────────────────────
|
|
89
|
+
/**
|
|
90
|
+
* Detect which thresholds are crossed given the SOL score report.
|
|
91
|
+
* Returns an array of crossing descriptors without any I/O side effects.
|
|
92
|
+
*/
|
|
93
|
+
function detectSolThresholdCrossings(scoreReport, thresholdsConfig, evidencePaths) {
|
|
94
|
+
const crossings = [];
|
|
95
|
+
const lowCompositeThreshold = thresholdsConfig.low_composite_score ?? DEFAULT_LOW_COMPOSITE_SCORE;
|
|
96
|
+
const qcFailureStatuses = thresholdsConfig.qc_repair_loop_failure_statuses ?? DEFAULT_QC_REPAIR_LOOP_FAILURE_STATUSES;
|
|
97
|
+
const providerFailureThreshold = thresholdsConfig.repeated_provider_failures ?? DEFAULT_REPEATED_PROVIDER_FAILURES;
|
|
98
|
+
const interventionThreshold = thresholdsConfig.foreman_intervention_count ?? DEFAULT_FOREMAN_INTERVENTION_COUNT;
|
|
99
|
+
const checkStaleWrongRun = thresholdsConfig.stale_wrong_run_telemetry ?? DEFAULT_STALE_WRONG_RUN_TELEMETRY;
|
|
100
|
+
const validationFailureThreshold = thresholdsConfig.validation_failures ?? DEFAULT_VALIDATION_FAILURES;
|
|
101
|
+
// 1. Low composite score
|
|
102
|
+
if (scoreReport.run_composite_score !== null &&
|
|
103
|
+
scoreReport.run_composite_score < lowCompositeThreshold) {
|
|
104
|
+
crossings.push({
|
|
105
|
+
code: "sol-low-composite-score",
|
|
106
|
+
message: `SOL run composite score ${scoreReport.run_composite_score.toFixed(4)} is below ` +
|
|
107
|
+
`configured threshold ${lowCompositeThreshold} — run health may be degraded.`,
|
|
108
|
+
evidenceRefs: evidencePaths,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
// 2. QC repair-loop failure state
|
|
112
|
+
const foremanReport = scoreReport.foreman;
|
|
113
|
+
const qcRepairLoopDetail = foremanReport.qc_repair_loop?.detail ?? "";
|
|
114
|
+
for (const status of qcFailureStatuses) {
|
|
115
|
+
if (qcRepairLoopDetail.includes(`status=${status}`)) {
|
|
116
|
+
crossings.push({
|
|
117
|
+
code: "sol-qc-repair-loop-failure",
|
|
118
|
+
message: `SOL evidence shows QC repair loop reached failure status "${status}" — ` +
|
|
119
|
+
`Medic consultation is recommended.`,
|
|
120
|
+
evidenceRefs: evidencePaths,
|
|
121
|
+
});
|
|
122
|
+
break; // One symptom per run for QC repair-loop failures
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Also detect via qc_repair_loop score == 0
|
|
126
|
+
if (foremanReport.qc_repair_loop?.score !== null &&
|
|
127
|
+
foremanReport.qc_repair_loop?.score === 0 &&
|
|
128
|
+
crossings.every((c) => c.code !== "sol-qc-repair-loop-failure")) {
|
|
129
|
+
crossings.push({
|
|
130
|
+
code: "sol-qc-repair-loop-failure",
|
|
131
|
+
message: `SOL evidence shows QC repair loop score 0.0 (worst outcome) — ` +
|
|
132
|
+
`Medic consultation is recommended.`,
|
|
133
|
+
evidenceRefs: evidencePaths,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
// 3. Repeated provider failures — inferred from worker composite scores ≤ 0
|
|
137
|
+
// and workers_failed count (available through foreman scores proxy).
|
|
138
|
+
const failedWorkers = Object.values(scoreReport.workers).filter((w) => w.composite_score !== null && w.composite_score <= 0);
|
|
139
|
+
if (failedWorkers.length >= providerFailureThreshold) {
|
|
140
|
+
crossings.push({
|
|
141
|
+
code: "sol-repeated-provider-failures",
|
|
142
|
+
message: `SOL evidence shows ${failedWorkers.length} worker(s) with zero composite score ` +
|
|
143
|
+
`(threshold: ${providerFailureThreshold}) — possible repeated provider failures.`,
|
|
144
|
+
evidenceRefs: evidencePaths,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
// 4. High Foreman intervention count
|
|
148
|
+
// intervention.score: 0.0 = user_intervened, 0.5 = foreman_intervened
|
|
149
|
+
const interventionScore = foremanReport.intervention?.score;
|
|
150
|
+
if (interventionScore !== null && interventionScore !== undefined) {
|
|
151
|
+
// pre_analysis dimension uses escalation_events as a proxy
|
|
152
|
+
// intervention score < 0.5 means foreman_intervened; 0.0 means user_intervened
|
|
153
|
+
// We use the foreman.pre_analysis detail to extract escalation_events
|
|
154
|
+
const preAnalysisDetail = foremanReport.pre_analysis?.detail ?? "";
|
|
155
|
+
const escalationMatch = /escalation_events=(\d+)/.exec(preAnalysisDetail);
|
|
156
|
+
const escalationEvents = escalationMatch ? parseInt(escalationMatch[1], 10) : 0;
|
|
157
|
+
if (escalationEvents > interventionThreshold) {
|
|
158
|
+
crossings.push({
|
|
159
|
+
code: "sol-high-foreman-intervention",
|
|
160
|
+
message: `SOL evidence shows ${escalationEvents} escalation event(s) ` +
|
|
161
|
+
`(threshold: ${interventionThreshold}) — possible Foreman intervention loop.`,
|
|
162
|
+
evidenceRefs: evidencePaths,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// 5. Stale / wrong-run telemetry
|
|
167
|
+
// Detected via foreman.dispatch and foreman.duration dimension details
|
|
168
|
+
if (checkStaleWrongRun) {
|
|
169
|
+
const dispatchDetail = foremanReport.dispatch?.detail ?? "";
|
|
170
|
+
const durationDetail = foremanReport.duration?.detail ?? "";
|
|
171
|
+
const hasRedispatch = /redispatched=\d+\//.exec(dispatchDetail);
|
|
172
|
+
const redispatchCount = hasRedispatch
|
|
173
|
+
? parseInt(/redispatched=(\d+)/.exec(dispatchDetail)?.[1] ?? "0", 10)
|
|
174
|
+
: 0;
|
|
175
|
+
// dispatch_epoch >> continue_epoch suggests wrong-run telemetry
|
|
176
|
+
const dispatchEpochMatch = /dispatch_epoch=(\d+)/.exec(durationDetail);
|
|
177
|
+
const continueEpochMatch = /continue_epoch=(\d+)/.exec(durationDetail);
|
|
178
|
+
const dispatchEpoch = dispatchEpochMatch ? parseInt(dispatchEpochMatch[1], 10) : 0;
|
|
179
|
+
const continueEpoch = continueEpochMatch ? parseInt(continueEpochMatch[1], 10) : 0;
|
|
180
|
+
const epochOverhead = dispatchEpoch > 0 ? dispatchEpoch - (continueEpoch + 1) : 0;
|
|
181
|
+
if (redispatchCount >= providerFailureThreshold || epochOverhead >= 3) {
|
|
182
|
+
crossings.push({
|
|
183
|
+
code: "sol-stale-wrong-run-telemetry",
|
|
184
|
+
message: `SOL evidence suggests stale or wrong-run telemetry: ` +
|
|
185
|
+
`redispatched=${redispatchCount}, epoch_overhead=${epochOverhead}. ` +
|
|
186
|
+
`This pattern is consistent with the POL-509 wrong-run failure mode.`,
|
|
187
|
+
evidenceRefs: evidencePaths,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// 6. Validation failures
|
|
192
|
+
const validationFailedWorkers = Object.values(scoreReport.workers).filter((w) => w.validation?.score !== null && w.validation?.score === 0);
|
|
193
|
+
if (validationFailedWorkers.length >= validationFailureThreshold) {
|
|
194
|
+
crossings.push({
|
|
195
|
+
code: "sol-validation-failures",
|
|
196
|
+
message: `SOL evidence shows ${validationFailedWorkers.length} worker(s) with validation failures ` +
|
|
197
|
+
`(threshold: ${validationFailureThreshold}).`,
|
|
198
|
+
evidenceRefs: evidencePaths,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return crossings;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Evaluate SOL score thresholds and, when policy enables it, append
|
|
205
|
+
* run-health symptoms for each crossing.
|
|
206
|
+
*
|
|
207
|
+
* Advisory only by default. To activate run-health writes:
|
|
208
|
+
* `sol.thresholds.enabled = true` AND one of:
|
|
209
|
+
* `sol.thresholds.policy.createRunHealthReport = true`
|
|
210
|
+
* `sol.thresholds.policy.requireMedic = true`
|
|
211
|
+
*
|
|
212
|
+
* Never throws — threshold evaluation must not block the run.
|
|
213
|
+
*/
|
|
214
|
+
function evaluateSolThresholds(params) {
|
|
215
|
+
const { runId, clusterId, scoreReport, evidencePaths = [], thresholdsConfig, repoRoot } = params;
|
|
216
|
+
const crossings = detectSolThresholdCrossings(scoreReport, thresholdsConfig, evidencePaths);
|
|
217
|
+
const policyEnabled = thresholdsConfig.enabled === true;
|
|
218
|
+
const createReport = thresholdsConfig.policy?.createRunHealthReport === true;
|
|
219
|
+
const requireMedic = thresholdsConfig.policy?.requireMedic === true;
|
|
220
|
+
const shouldWrite = policyEnabled && (createReport || requireMedic);
|
|
221
|
+
if (!shouldWrite || crossings.length === 0) {
|
|
222
|
+
return { crossings, symptomsAppended: 0, medicRequired: false };
|
|
223
|
+
}
|
|
224
|
+
let symptomsAppended = 0;
|
|
225
|
+
let medicDecisionWritten = false;
|
|
226
|
+
try {
|
|
227
|
+
for (const crossing of crossings) {
|
|
228
|
+
upsertSolSymptom(runId, clusterId, makeSolSymptom(crossing), repoRoot);
|
|
229
|
+
symptomsAppended++;
|
|
230
|
+
}
|
|
231
|
+
// If requireMedic is true, set medic_consult to pending so finalize gate fires.
|
|
232
|
+
if (requireMedic && crossings.length > 0) {
|
|
233
|
+
(0, index_js_1.markMedicDecision)(runId, { status: "pending" }, repoRoot);
|
|
234
|
+
medicDecisionWritten = true;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
// Threshold evaluation must never block the run.
|
|
239
|
+
// ponytail: surface write errors to telemetry in a future pass
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
crossings,
|
|
243
|
+
symptomsAppended,
|
|
244
|
+
medicRequired: medicDecisionWritten,
|
|
245
|
+
};
|
|
246
|
+
}
|
package/dist/cli/adopt-canon.js
CHANGED
|
@@ -148,7 +148,7 @@ function dispatchCanonAgent(options) {
|
|
|
148
148
|
.slice(0, 30)
|
|
149
149
|
.map((d, i) => `${i + 1}. [${d.title}] path: ${d.path}`)
|
|
150
150
|
.join("\n");
|
|
151
|
-
const prompt = `You are a Polaris librarian generating
|
|
151
|
+
const prompt = `You are a Polaris librarian generating route cognition for an agent work area.
|
|
152
152
|
|
|
153
153
|
Route folder: ${routeFolder}
|
|
154
154
|
Repo root: ${repoRoot}
|
|
@@ -156,16 +156,27 @@ Repo root: ${repoRoot}
|
|
|
156
156
|
Available doctrine documents:
|
|
157
157
|
${docList}
|
|
158
158
|
|
|
159
|
-
Generate two outputs for this route area
|
|
159
|
+
Generate two outputs for this route area.
|
|
160
160
|
|
|
161
|
-
1. SUMMARY.md content
|
|
161
|
+
1. SUMMARY.md content (current-state memory / navigation index):
|
|
162
|
+
- Include: 2-4 lines describing what this area covers, its current canonical status, and which doctrine docs are relevant.
|
|
163
|
+
- Avoid: operational procedures, step-by-step agent instructions, duplicated doctrine text, or per-run diary content.
|
|
162
164
|
|
|
163
|
-
2. POLARIS.md content
|
|
165
|
+
2. POLARIS.md content (route operating guidance):
|
|
166
|
+
REQUIRED sections that must be present in polaris_lines:
|
|
167
|
+
- Purpose/boundaries: what this area is responsible for and what it excludes
|
|
168
|
+
- Invariants/safety rules: constraints and what to avoid
|
|
169
|
+
- Commands/workflows: key patterns/conventions agents must follow
|
|
170
|
+
- Canonical links: which doctrine docs to consult for specific concerns
|
|
171
|
+
- Read-before-edit references: upstream/downstream dependencies
|
|
172
|
+
|
|
173
|
+
Avoid: navigation-index content, summary-style history, per-run notes, or duplicating doctrine text instead of linking to it.
|
|
164
174
|
|
|
165
175
|
Respond with ONLY valid JSON on a single line:
|
|
166
176
|
{"relevant_docs":[{"path":"<doc path>","title":"<doc title>"}],"summary_lines":["line1","line2"],"polaris_lines":["line1","line2"]}
|
|
167
177
|
|
|
168
|
-
If no doctrine docs are relevant, return an empty array for relevant_docs
|
|
178
|
+
If no doctrine docs are relevant, return an empty array for relevant_docs.
|
|
179
|
+
The polaris_lines array must contain all required POLARIS contract sections or be empty if the agent cannot generate valid content.`;
|
|
169
180
|
// Same dispatch pattern as dispatchLibrarianReview — pass args straight through,
|
|
170
181
|
// letting the provider config (e.g. env CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=0) handle auth.
|
|
171
182
|
const args = (cfg.args ?? []).map((a) => (a === "{{worker_prompt}}" ? prompt : a));
|
|
@@ -119,7 +119,7 @@ function buildPolarisDraft(folder) {
|
|
|
119
119
|
"<!-- polaris:draft -->",
|
|
120
120
|
`# ${folderName}`,
|
|
121
121
|
"",
|
|
122
|
-
"> Polaris draft —
|
|
122
|
+
"> Polaris draft — route operating guidance. Remove the `<!-- polaris:draft -->` marker to promote.",
|
|
123
123
|
"",
|
|
124
124
|
"## Purpose",
|
|
125
125
|
"",
|
|
@@ -129,13 +129,15 @@ function buildPolarisDraft(folder) {
|
|
|
129
129
|
`**Route:** ${folder}`,
|
|
130
130
|
"**Taskchain:** unknown",
|
|
131
131
|
"",
|
|
132
|
-
"##
|
|
132
|
+
"## Responsibilities",
|
|
133
133
|
"",
|
|
134
|
-
"<!--
|
|
134
|
+
"<!-- What agents entering this route must know, do, and check. -->",
|
|
135
135
|
"",
|
|
136
|
-
"##
|
|
136
|
+
"## Constraints and exclusions",
|
|
137
137
|
"",
|
|
138
|
-
"
|
|
138
|
+
"- Do NOT duplicate doctrine from `smartdocs/doctrine/active/`; link to it instead.",
|
|
139
|
+
"- Do NOT use this file as a run diary; per-run notes belong in the run artifact, not here or in SUMMARY.md.",
|
|
140
|
+
"- Keep guidance operational and directive — this is not a history or index.",
|
|
139
141
|
"",
|
|
140
142
|
].join("\n");
|
|
141
143
|
}
|
|
@@ -145,15 +147,28 @@ function buildSummaryDraft(folder) {
|
|
|
145
147
|
"<!-- polaris:draft -->",
|
|
146
148
|
`# Summary — ${folderName}`,
|
|
147
149
|
"",
|
|
148
|
-
"> Polaris draft —
|
|
150
|
+
"> Polaris draft — current-state memory for this route. Remove the `<!-- polaris:draft -->` marker to promote.",
|
|
149
151
|
"",
|
|
150
152
|
`- Route: \`${folder}\``,
|
|
151
153
|
"- Canon status: draft",
|
|
152
154
|
"- Linked doctrine: `POLARIS.md`",
|
|
153
155
|
"",
|
|
154
|
-
"##
|
|
156
|
+
"## Current state",
|
|
155
157
|
"",
|
|
156
|
-
"<!--
|
|
158
|
+
"<!-- Synthesized current-state context: what's here, ownership, and domain. -->",
|
|
159
|
+
"",
|
|
160
|
+
"## Synthesized recent changes",
|
|
161
|
+
"",
|
|
162
|
+
"<!-- High-level summary of significant changes, not a per-run diary. Run-specific notes belong in run artifacts. -->",
|
|
163
|
+
"",
|
|
164
|
+
"## Caveats and drift",
|
|
165
|
+
"",
|
|
166
|
+
"<!-- Known gaps, stale areas, or planned changes that affect navigation. -->",
|
|
167
|
+
"",
|
|
168
|
+
"## Canonical sources",
|
|
169
|
+
"",
|
|
170
|
+
"- [POLARIS.md](POLARIS.md) — operational guidance for this route",
|
|
171
|
+
"<!-- Link to relevant doctrine from `smartdocs/doctrine/active/` -->",
|
|
157
172
|
"",
|
|
158
173
|
].join("\n");
|
|
159
174
|
}
|
package/dist/cli/medic.js
CHANGED
|
@@ -5,6 +5,10 @@ const commander_1 = require("commander");
|
|
|
5
5
|
const node_path_1 = require("node:path");
|
|
6
6
|
const node_fs_1 = require("node:fs");
|
|
7
7
|
const chart_id_js_1 = require("../medic/chart-id.js");
|
|
8
|
+
const loader_js_1 = require("../config/loader.js");
|
|
9
|
+
const terminal_cli_js_1 = require("../loop/adapters/terminal-cli.js");
|
|
10
|
+
const run_health_consult_js_1 = require("../medic/run-health-consult.js");
|
|
11
|
+
const treatment_packets_js_1 = require("../medic/treatment-packets.js");
|
|
8
12
|
function createMedicCommand(options = {}) {
|
|
9
13
|
const repoRootDefault = options.repoRoot ?? (0, node_path_1.resolve)(process.cwd());
|
|
10
14
|
const medic = new commander_1.Command("medic")
|
|
@@ -34,6 +38,67 @@ function createMedicCommand(options = {}) {
|
|
|
34
38
|
process.exit(1);
|
|
35
39
|
}
|
|
36
40
|
}));
|
|
41
|
+
medic
|
|
42
|
+
.command("run-health-consult")
|
|
43
|
+
.description("Run a Medic run-health consult from a packet file")
|
|
44
|
+
.requiredOption("--packet-file <path>", "Path to MedicRunHealthPacket JSON")
|
|
45
|
+
.option("-r, --repo-root <path>", "Repository root", repoRootDefault)
|
|
46
|
+
.action(async (cmdOptions) => {
|
|
47
|
+
const repoRoot = cmdOptions.repoRoot;
|
|
48
|
+
let packet;
|
|
49
|
+
try {
|
|
50
|
+
packet = JSON.parse((0, node_fs_1.readFileSync)(cmdOptions.packetFile, "utf-8"));
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
process.stderr.write(`medic run-health-consult error: cannot read packet: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (packet.role !== "medic-run-health") {
|
|
58
|
+
process.stderr.write(`medic run-health-consult error: packet role must be \"medic-run-health\", got \"${packet.role}\"\n`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const config = (0, loader_js_1.loadConfig)(repoRoot);
|
|
63
|
+
const adapter = new terminal_cli_js_1.TerminalCliAdapter(config.execution);
|
|
64
|
+
// Use shared provider resolution logic: prefer rotation, fall back to first provider
|
|
65
|
+
const providerName = (config.execution.rotation && config.execution.rotation.length > 0)
|
|
66
|
+
? config.execution.rotation[0]
|
|
67
|
+
: Object.keys(config.execution.providers ?? {})[0] ?? "terminal-cli";
|
|
68
|
+
// Get current branch from git
|
|
69
|
+
let branch = "main";
|
|
70
|
+
try {
|
|
71
|
+
const { execFileSync } = require("node:child_process");
|
|
72
|
+
branch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
73
|
+
cwd: repoRoot,
|
|
74
|
+
encoding: "utf-8",
|
|
75
|
+
}).trim();
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Fall back to main if git fails
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const result = await (0, run_health_consult_js_1.runMedicRunHealthConsult)({
|
|
82
|
+
packet,
|
|
83
|
+
repoRoot,
|
|
84
|
+
stateFile: packet.cluster_state_path,
|
|
85
|
+
telemetryFile: packet.telemetry_path,
|
|
86
|
+
branch,
|
|
87
|
+
dryRun: false,
|
|
88
|
+
dispatchTreatmentWorkerFn: (input) => (0, treatment_packets_js_1.dispatchTreatmentWorker)({
|
|
89
|
+
...input,
|
|
90
|
+
repoRoot,
|
|
91
|
+
dispatch: (workerPacket) => adapter.dispatch(workerPacket, { provider: providerName }),
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
94
|
+
(0, node_fs_1.writeFileSync)(packet.result_path, JSON.stringify(result, null, 2), "utf-8");
|
|
95
|
+
process.stdout.write(`Medic run-health consult result written to ${packet.result_path}\n`);
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
process.stderr.write(`medic run-health-consult error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
37
102
|
return medic;
|
|
38
103
|
}
|
|
39
104
|
function createChart(options) {
|
|
@@ -118,6 +118,59 @@ function validateCloseoutLibrarianResult(value) {
|
|
|
118
118
|
if (!Array.isArray(r["summary_md_updates"])) {
|
|
119
119
|
errors.push("summary_md_updates must be an array");
|
|
120
120
|
}
|
|
121
|
+
if (r["artifact_reconciliation"] !== undefined &&
|
|
122
|
+
!Array.isArray(r["artifact_reconciliation"])) {
|
|
123
|
+
errors.push("artifact_reconciliation must be an array when provided");
|
|
124
|
+
}
|
|
125
|
+
if (Array.isArray(r["artifact_reconciliation"])) {
|
|
126
|
+
const allowed = new Set([
|
|
127
|
+
"polaris-only",
|
|
128
|
+
"summary-only",
|
|
129
|
+
"both",
|
|
130
|
+
"no-change",
|
|
131
|
+
]);
|
|
132
|
+
for (const [index, update] of r["artifact_reconciliation"].entries()) {
|
|
133
|
+
if (typeof update !== "object" || update === null) {
|
|
134
|
+
errors.push(`artifact_reconciliation[${index}] must be an object`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const entry = update;
|
|
138
|
+
if (typeof entry["folder"] !== "string" || !entry["folder"]) {
|
|
139
|
+
errors.push(`artifact_reconciliation[${index}].folder must be a non-empty string`);
|
|
140
|
+
}
|
|
141
|
+
const decision = entry["decision"];
|
|
142
|
+
if (typeof entry["decision"] !== "string" ||
|
|
143
|
+
!allowed.has(decision)) {
|
|
144
|
+
errors.push(`artifact_reconciliation[${index}].decision must be one of: polaris-only, summary-only, both, no-change`);
|
|
145
|
+
}
|
|
146
|
+
if (typeof entry["polaris_md"] !== "string" || !entry["polaris_md"]) {
|
|
147
|
+
errors.push(`artifact_reconciliation[${index}].polaris_md must be a non-empty string`);
|
|
148
|
+
}
|
|
149
|
+
// Validate summary_md based on decision: required for summary-only and both, null for polaris-only and no-change
|
|
150
|
+
if (decision === "summary-only" || decision === "both") {
|
|
151
|
+
if (typeof entry["summary_md"] !== "string" || !entry["summary_md"]) {
|
|
152
|
+
errors.push(`artifact_reconciliation[${index}].summary_md must be a non-empty string for decision "${decision}"`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else if (decision === "polaris-only" || decision === "no-change") {
|
|
156
|
+
if (entry["summary_md"] !== null && (typeof entry["summary_md"] !== "string" || !entry["summary_md"])) {
|
|
157
|
+
errors.push(`artifact_reconciliation[${index}].summary_md must be a non-empty string or null for decision "${decision}"`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
// Fallback for any decision type: reject empty strings
|
|
162
|
+
if (entry["summary_md"] !== null && typeof entry["summary_md"] !== "string") {
|
|
163
|
+
errors.push(`artifact_reconciliation[${index}].summary_md must be a string or null`);
|
|
164
|
+
}
|
|
165
|
+
if (typeof entry["summary_md"] === "string" && !entry["summary_md"]) {
|
|
166
|
+
errors.push(`artifact_reconciliation[${index}].summary_md must not be an empty string when provided`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (typeof entry["reason"] !== "string" || !entry["reason"]) {
|
|
170
|
+
errors.push(`artifact_reconciliation[${index}].reason must be a non-empty string`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
121
174
|
if (!Array.isArray(r["docs_ingested"])) {
|
|
122
175
|
errors.push("docs_ingested must be an array");
|
|
123
176
|
}
|
|
@@ -76,6 +76,24 @@ function generateLibrarianPacket(options) {
|
|
|
76
76
|
cognition_index: node_fs_1.default.existsSync(cognitionIndexAbs)
|
|
77
77
|
? node_path_1.default.relative(repoRoot, cognitionIndexAbs)
|
|
78
78
|
: null,
|
|
79
|
+
artifact_contract: {
|
|
80
|
+
polaris_md: {
|
|
81
|
+
path: node_path_1.default.join(folder, "POLARIS.md"),
|
|
82
|
+
intent: "must-reconcile",
|
|
83
|
+
reason: "Affected folders must reconcile POLARIS.md to keep operational doctrine current.",
|
|
84
|
+
},
|
|
85
|
+
summary_md: node_fs_1.default.existsSync(summaryAbs)
|
|
86
|
+
? {
|
|
87
|
+
path: node_path_1.default.join(folder, "SUMMARY.md"),
|
|
88
|
+
intent: "reconcile-if-present",
|
|
89
|
+
reason: "SUMMARY.md exists for this folder and may be reconciled independently when informational canon changed.",
|
|
90
|
+
}
|
|
91
|
+
: {
|
|
92
|
+
path: null,
|
|
93
|
+
intent: "not-present",
|
|
94
|
+
reason: "SUMMARY.md is missing for this folder; skip SUMMARY reconciliation and evaluate POLARIS.md independently.",
|
|
95
|
+
},
|
|
96
|
+
},
|
|
79
97
|
};
|
|
80
98
|
});
|
|
81
99
|
const cognitionNotes = findPendingCognitionNotes(repoRoot, state.completed_children);
|
|
@@ -103,10 +121,16 @@ function generateLibrarianPacket(options) {
|
|
|
103
121
|
node_path_1.default.join(repoRoot, "scripts"),
|
|
104
122
|
];
|
|
105
123
|
// Allowed: documentation and cognition paths only
|
|
124
|
+
// Build explicit smartdocs paths instead of granting the entire root
|
|
125
|
+
const smartdocsRawDir = node_path_1.default.join(repoRoot, "smartdocs", "raw");
|
|
126
|
+
const smartdocsSpecsActiveDir = node_path_1.default.join(repoRoot, "smartdocs", "specs", "active");
|
|
127
|
+
const smartdocsDoctrineActiveDir = node_path_1.default.join(repoRoot, "smartdocs", "doctrine", "active");
|
|
106
128
|
const allowedWritePaths = [
|
|
107
129
|
...polarisMdPaths.map((p) => node_path_1.default.join(repoRoot, p.polaris_md)),
|
|
108
130
|
...polarisMdPaths.filter((p) => p.summary_md).map((p) => node_path_1.default.join(repoRoot, p.summary_md)),
|
|
109
|
-
|
|
131
|
+
smartdocsRawDir,
|
|
132
|
+
smartdocsSpecsActiveDir,
|
|
133
|
+
smartdocsDoctrineActiveDir,
|
|
110
134
|
node_path_1.default.join(repoRoot, ".polaris", "cognition", "archive"),
|
|
111
135
|
resultPath,
|
|
112
136
|
];
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* - Locality assumptions (route-local only, no repo-wide surface)
|
|
10
10
|
*/
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.computeNormalizedSimilarity = computeNormalizedSimilarity;
|
|
12
13
|
exports.looksLikePolarisChurn = looksLikePolarisChurn;
|
|
13
14
|
exports.validateCognitionSurfaces = validateCognitionSurfaces;
|
|
14
15
|
exports.validateSummaryFile = validateSummaryFile;
|
|
@@ -17,6 +18,48 @@ const node_fs_1 = require("node:fs");
|
|
|
17
18
|
const node_path_1 = require("node:path");
|
|
18
19
|
const route_cognition_delta_js_1 = require("./route-cognition-delta.js");
|
|
19
20
|
const summary_delta_js_1 = require("./summary-delta.js");
|
|
21
|
+
const DEFAULT_PAIRWISE_DRIFT_THRESHOLD = 0.5;
|
|
22
|
+
// ── Pairwise POLARIS.md / SUMMARY.md drift detection ───────────────────────────
|
|
23
|
+
function escapeRegExp(s) {
|
|
24
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Normalize route artifact text so shared boilerplate (headings, links, route
|
|
28
|
+
* names) does not dominate similarity scores.
|
|
29
|
+
*/
|
|
30
|
+
function normalizeRouteArtifact(content, routeName) {
|
|
31
|
+
let s = content.toLowerCase();
|
|
32
|
+
// Strip markdown headings
|
|
33
|
+
s = s.replace(/^#+\s+.*$/gm, " ");
|
|
34
|
+
// Remove link URLs but keep link text
|
|
35
|
+
s = s.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
|
|
36
|
+
// Remove bare URLs
|
|
37
|
+
s = s.replace(/https?:\/\/\S+/g, " ");
|
|
38
|
+
// Remove route name tokens
|
|
39
|
+
if (routeName) {
|
|
40
|
+
for (const token of routeName.split(/[-_\s]+/).filter(Boolean)) {
|
|
41
|
+
s = s.replace(new RegExp(`\\b${escapeRegExp(token)}\\b`, "g"), " ");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Drop non-alphanumeric characters
|
|
45
|
+
s = s.replace(/[^\p{L}\p{N}\s]+/gu, " ");
|
|
46
|
+
// Collapse whitespace
|
|
47
|
+
s = s.replace(/\s+/g, " ").trim();
|
|
48
|
+
return s;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Compute a normalized Jaccard similarity between two route artifacts.
|
|
52
|
+
* Returns a value between 0 and 1.
|
|
53
|
+
*/
|
|
54
|
+
function computeNormalizedSimilarity(a, b, routeName) {
|
|
55
|
+
const tokensA = new Set(normalizeRouteArtifact(a, routeName).split(" ").filter(Boolean));
|
|
56
|
+
const tokensB = new Set(normalizeRouteArtifact(b, routeName).split(" ").filter(Boolean));
|
|
57
|
+
if (tokensA.size === 0 && tokensB.size === 0)
|
|
58
|
+
return 0;
|
|
59
|
+
const intersection = new Set([...tokensA].filter((t) => tokensB.has(t)));
|
|
60
|
+
const union = new Set([...tokensA, ...tokensB]);
|
|
61
|
+
return union.size === 0 ? 0 : intersection.size / union.size;
|
|
62
|
+
}
|
|
20
63
|
// ── POLARIS.md churn detection ────────────────────────────────────────────────
|
|
21
64
|
/**
|
|
22
65
|
* Signals that indicate a POLARIS.md change is likely non-operational churn
|
|
@@ -62,18 +105,21 @@ function* walkForCognitionFiles(dir, repoRoot) {
|
|
|
62
105
|
}
|
|
63
106
|
}
|
|
64
107
|
}
|
|
65
|
-
// ── Main validation entry point ───────────────────────────────────────────────
|
|
66
108
|
/**
|
|
67
109
|
* Validate all route-local cognition surfaces under repoRoot.
|
|
68
110
|
*
|
|
69
111
|
* Checks:
|
|
70
112
|
* 1. SUMMARY.md size guard (≤ SUMMARY_MAX_BYTES)
|
|
71
113
|
* 2. SUMMARY.md doctrine bleed
|
|
72
|
-
* 3.
|
|
114
|
+
* 3. Pairwise POLARIS.md / SUMMARY.md drift (exact duplicate → error,
|
|
115
|
+
* high normalized similarity → warn)
|
|
116
|
+
* 4. Locality: POLARIS.md at root is not flagged (root is special)
|
|
73
117
|
*/
|
|
74
|
-
function validateCognitionSurfaces(repoRoot) {
|
|
118
|
+
function validateCognitionSurfaces(repoRoot, options = {}) {
|
|
75
119
|
const violations = [];
|
|
76
120
|
const warnings = [];
|
|
121
|
+
const threshold = options.similarityThreshold ?? DEFAULT_PAIRWISE_DRIFT_THRESHOLD;
|
|
122
|
+
const pairs = new Map();
|
|
77
123
|
for (const { rel, name } of walkForCognitionFiles((0, node_path_1.resolve)(repoRoot), repoRoot)) {
|
|
78
124
|
// Skip root-level files — root cognition is special (AGENTS.md / CLAUDE.md)
|
|
79
125
|
if (!rel.includes("/"))
|
|
@@ -85,7 +131,16 @@ function validateCognitionSurfaces(repoRoot) {
|
|
|
85
131
|
catch {
|
|
86
132
|
continue;
|
|
87
133
|
}
|
|
134
|
+
const absDir = (0, node_path_1.dirname)((0, node_path_1.resolve)(repoRoot, rel));
|
|
135
|
+
const dirRel = (0, node_path_1.relative)(repoRoot, absDir).replace(/\\/g, "/");
|
|
136
|
+
let entry = pairs.get(absDir);
|
|
137
|
+
if (!entry) {
|
|
138
|
+
entry = { dirRel };
|
|
139
|
+
pairs.set(absDir, entry);
|
|
140
|
+
}
|
|
88
141
|
if (name === "SUMMARY.md") {
|
|
142
|
+
entry.summary = content;
|
|
143
|
+
entry.summaryRel = rel;
|
|
89
144
|
if ((0, summary_delta_js_1.isSummaryOversized)(content)) {
|
|
90
145
|
const bytes = Buffer.byteLength(content, "utf-8");
|
|
91
146
|
violations.push({
|
|
@@ -106,6 +161,33 @@ function validateCognitionSurfaces(repoRoot) {
|
|
|
106
161
|
});
|
|
107
162
|
}
|
|
108
163
|
}
|
|
164
|
+
else if (name === "POLARIS.md") {
|
|
165
|
+
entry.polaris = content;
|
|
166
|
+
entry.polarisRel = rel;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
for (const entry of pairs.values()) {
|
|
170
|
+
if (!entry.polaris || !entry.summary || !entry.polarisRel || !entry.summaryRel)
|
|
171
|
+
continue;
|
|
172
|
+
if (entry.polaris === entry.summary) {
|
|
173
|
+
violations.push({
|
|
174
|
+
type: "polaris-summary-drift",
|
|
175
|
+
file: entry.dirRel,
|
|
176
|
+
detail: `Route ${entry.dirRel}: POLARIS.md and SUMMARY.md are exact duplicates (${entry.polarisRel}, ${entry.summaryRel})`,
|
|
177
|
+
severity: "error",
|
|
178
|
+
});
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const routeName = entry.dirRel === "." ? undefined : (0, node_path_1.basename)(entry.dirRel);
|
|
182
|
+
const similarity = computeNormalizedSimilarity(entry.polaris, entry.summary, routeName);
|
|
183
|
+
if (similarity >= threshold) {
|
|
184
|
+
warnings.push({
|
|
185
|
+
type: "polaris-summary-drift",
|
|
186
|
+
file: entry.dirRel,
|
|
187
|
+
detail: `Route ${entry.dirRel}: POLARIS.md and SUMMARY.md normalized similarity ${similarity.toFixed(2)} exceeds threshold ${threshold} (${entry.polarisRel}, ${entry.summaryRel})`,
|
|
188
|
+
severity: "warn",
|
|
189
|
+
});
|
|
190
|
+
}
|
|
109
191
|
}
|
|
110
192
|
return {
|
|
111
193
|
valid: violations.length === 0,
|