@lsctech/polaris 0.5.11 → 0.5.13

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 (41) hide show
  1. package/dist/autoresearch/proposal.js +61 -0
  2. package/dist/autoresearch/score.js +124 -0
  3. package/dist/cli/adopt-canon.js +22 -8
  4. package/dist/cli/index.js +4 -0
  5. package/dist/cli/qc.js +97 -0
  6. package/dist/config/validator.js +16 -6
  7. package/dist/finalize/artifact-policy.js +12 -3
  8. package/dist/finalize/index.js +36 -21
  9. package/dist/finalize/run-report.js +189 -19
  10. package/dist/finalize/steps/05-generate-report.js +5 -1
  11. package/dist/finalize/steps/12-archive.js +6 -0
  12. package/dist/loop/adapters/terminal-cli.js +159 -25
  13. package/dist/loop/body-parser.js +73 -6
  14. package/dist/loop/continue.js +22 -4
  15. package/dist/loop/dispatch.js +103 -68
  16. package/dist/loop/orphan-recovery.js +2 -1
  17. package/dist/loop/parent.js +50 -15
  18. package/dist/loop/router/engine.js +1 -0
  19. package/dist/loop/run-bootstrap.js +3 -1
  20. package/dist/loop/run-preflight.js +4 -1
  21. package/dist/loop/worker-packet.js +81 -2
  22. package/dist/map/inference.js +4 -1
  23. package/dist/medic/routing-signals.js +60 -0
  24. package/dist/medic/run-health-consult.js +5 -4
  25. package/dist/medic/treatment-packets.js +5 -1
  26. package/dist/qc/policy.js +2 -0
  27. package/dist/qc/providers/coderabbit.js +140 -10
  28. package/dist/qc/repair-loop.js +147 -1
  29. package/dist/qc/repair-packets.js +16 -3
  30. package/dist/qc/routing.js +6 -0
  31. package/dist/qc/types.js +3 -0
  32. package/dist/run-health/index.js +12 -0
  33. package/dist/skill-packet/generator.js +234 -3
  34. package/dist/smartdocs-engine/canon-check.js +5 -5
  35. package/dist/smartdocs-engine/index.js +54 -0
  36. package/dist/smartdocs-engine/seed-instructions.js +159 -4
  37. package/dist/smartdocs-engine/validate-instructions.js +46 -4
  38. package/dist/tracker/local-graph.js +106 -3
  39. package/dist/workspace/POLARIS.md +4 -0
  40. package/dist/workspace/SUMMARY.md +56 -0
  41. package/package.json +1 -1
@@ -78,6 +78,16 @@ function subsystemFromFilePath(filePath) {
78
78
  const lastSlash = filePath.lastIndexOf("/");
79
79
  return lastSlash === -1 ? "root" : filePath.slice(0, lastSlash);
80
80
  }
81
+ function deriveSourceQcRunId(finding, fallbackRunId) {
82
+ // Some findingId formats encode the source QC run as `<provider>-<index>-<runId>`
83
+ // (e.g. coderabbit-1-1783818427443). Prefer that provenance over the result
84
+ // run ID so packet sourceQcRunIds match the runs that emitted the findings.
85
+ const match = finding.findingId.match(/^(.+?)-(\d+)-(\d+)$/);
86
+ if (match) {
87
+ return `${match[1]}-${match[3]}`;
88
+ }
89
+ return fallbackRunId;
90
+ }
81
91
  function enrichFinding(finding, config, sourceQcRunId) {
82
92
  return {
83
93
  ...finding,
@@ -85,7 +95,7 @@ function enrichFinding(finding, config, sourceQcRunId) {
85
95
  risk: riskLevel(finding),
86
96
  confidenceBand: confidenceBand(finding),
87
97
  subsystem: subsystemFromFilePath(finding.filePath),
88
- sourceQcRunId,
98
+ sourceQcRunId: deriveSourceQcRunId(finding, sourceQcRunId),
89
99
  };
90
100
  }
91
101
  /**
@@ -213,6 +223,8 @@ function buildPacket(group, index, clusterId, round, allRunIds, validationComman
213
223
  const packetId = `pkt-${clusterId}-r${round}-${String(index).padStart(3, "0")}`;
214
224
  const findingIds = group.map((f) => f.findingId).sort();
215
225
  const sourceRunIds = [...new Set(group.map((f) => f.sourceQcRunId))].sort();
226
+ // Preserve the artifact run ID while also capturing the finding-level source run.
227
+ const combinedSourceRunIds = [...new Set([...allRunIds, ...sourceRunIds])].sort();
216
228
  const severityFloor = group.reduce((acc, f) => (0, severity_js_1.maxSeverity)(acc, f.severity), "info");
217
229
  const categories = [...new Set(group.map((f) => f.category).filter(Boolean))].sort();
218
230
  const filePaths = group.map((f) => f.filePath).filter((f) => Boolean(f));
@@ -230,7 +242,7 @@ function buildPacket(group, index, clusterId, round, allRunIds, validationComman
230
242
  packetId,
231
243
  round,
232
244
  clusterId,
233
- sourceQcRunIds: sourceRunIds.length > 0 ? sourceRunIds : allRunIds,
245
+ sourceQcRunIds: combinedSourceRunIds,
234
246
  findingIds,
235
247
  severityFloor,
236
248
  rootCauseHint,
@@ -346,12 +358,13 @@ function compileRepairPackets(input) {
346
358
  packets.push(buildPacket(group, packets.length, clusterId, round, allRunIds, validationCommands, compiledAt));
347
359
  }
348
360
  const finalPackets = assignParallelGroups(packets);
361
+ const manifestSourceRunIds = [...new Set(finalPackets.flatMap((p) => p.sourceQcRunIds))].sort();
349
362
  const manifest = {
350
363
  schemaVersion: "1.0",
351
364
  clusterId,
352
365
  round,
353
366
  compiledAt,
354
- sourceQcRunIds: allRunIds,
367
+ sourceQcRunIds: manifestSourceRunIds.length > 0 ? manifestSourceRunIds : allRunIds,
355
368
  packets: finalPackets,
356
369
  };
357
370
  const manifestPath = getRepairPacketManifestPath(clusterId, round, repoRoot);
@@ -12,6 +12,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.decideRepairRouting = decideRepairRouting;
13
13
  const security_category_js_1 = require("./security-category.js");
14
14
  const severity_js_1 = require("./severity.js");
15
+ function isStateRepairCategory(category) {
16
+ return category === "state-repair";
17
+ }
15
18
  function getBlockThreshold(config, context) {
16
19
  const route = context.routeName ? config.routes?.[context.routeName] : undefined;
17
20
  return route?.blockThreshold ?? config.severityThresholds?.block ?? "high";
@@ -34,6 +37,9 @@ function decideRepairRouting(finding, config, autofixEligible, context = {}) {
34
37
  if ((0, security_category_js_1.isSecurityCategory)(finding.category)) {
35
38
  return "operator-review";
36
39
  }
40
+ if (isStateRepairCategory(finding.category)) {
41
+ return "operator-review";
42
+ }
37
43
  const attribution = finding.attribution;
38
44
  const clearAttribution = attribution.confidence === "high" || attribution.confidence === "medium";
39
45
  if (autofixEligible && clearAttribution && attribution.childId) {
package/dist/qc/types.js CHANGED
@@ -7,3 +7,6 @@
7
7
  * shapes so that Polaris can support multiple QC backends without vendor lock-in.
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.QC_RESOLUTION_OUTCOMES = void 0;
11
+ /** Allowed outcomes for an operator resolution artifact. */
12
+ exports.QC_RESOLUTION_OUTCOMES = ["pass", "no-repairable"];
@@ -150,6 +150,18 @@ function appendSymptom(runId, symptom, repoRoot) {
150
150
  symptoms: [...existing.symptoms, symptom],
151
151
  updated_at: new Date().toISOString(),
152
152
  };
153
+ // A later high/critical symptom means a previously resolved Medic consult is
154
+ // no longer satisfied; reopen it so closeout/medic gates do not pass on a
155
+ // stale resolved decision.
156
+ if (existing.medic_consult?.status === "resolved" &&
157
+ (symptom.severity === "critical" || symptom.severity === "high")) {
158
+ updated.medic_consult = {
159
+ ...existing.medic_consult,
160
+ status: "in-progress",
161
+ resolved_at: undefined,
162
+ resolution_notes: undefined,
163
+ };
164
+ }
153
165
  const validation = (0, schema_js_1.validateRunHealthReport)(updated);
154
166
  if (!validation.valid) {
155
167
  throw new Error(`Appended symptom produces invalid report:\n${validation.errors.join("\n")}`);
@@ -1,9 +1,15 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.SUPPORTED_SKILLS = exports.SKILL_ROLE_MAP = void 0;
4
7
  exports.generateSetupBootstrapPacket = generateSetupBootstrapPacket;
5
8
  exports.generateSkillPacket = generateSkillPacket;
6
9
  const node_crypto_1 = require("node:crypto");
10
+ const node_child_process_1 = require("node:child_process");
11
+ const node_fs_1 = __importDefault(require("node:fs"));
12
+ const node_path_1 = __importDefault(require("node:path"));
7
13
  exports.SKILL_ROLE_MAP = {
8
14
  analyze: "Analyst",
9
15
  run: "Foreman",
@@ -362,7 +368,37 @@ function buildCatalogPacket() {
362
368
  ],
363
369
  };
364
370
  }
365
- function buildReconcilePacket() {
371
+ function buildReconcilePacket(_config, options) {
372
+ const repoRoot = node_path_1.default.resolve(options?.repoRoot ?? process.cwd());
373
+ const issueId = resolveIssueId(repoRoot, options?.issueId);
374
+ const runId = buildReconcileRunId(issueId);
375
+ const changedFiles = getReconcileChangedFiles(repoRoot);
376
+ const affectedFolders = resolveAffectedFolders(repoRoot, changedFiles);
377
+ if (affectedFolders.length === 0) {
378
+ return buildBlockedReconcilePacket(runId, issueId, repoRoot);
379
+ }
380
+ const allowedWritePaths = affectedFolders.flatMap((folder) => {
381
+ const paths = [node_path_1.default.join(repoRoot, folder, "POLARIS.md")];
382
+ const summaryPath = node_path_1.default.join(repoRoot, folder, "SUMMARY.md");
383
+ if (node_fs_1.default.existsSync(summaryPath)) {
384
+ paths.push(summaryPath);
385
+ }
386
+ return paths;
387
+ });
388
+ const workInventory = {
389
+ affected_folders: affectedFolders,
390
+ all_changed_files: changedFiles,
391
+ child_summaries: [],
392
+ pending_cognition_notes: [],
393
+ polaris_md_files: Object.fromEntries(affectedFolders.map((folder) => [
394
+ folder,
395
+ readFileOrNull(node_path_1.default.join(repoRoot, folder, "POLARIS.md")),
396
+ ])),
397
+ summary_md_files: Object.fromEntries(affectedFolders.map((folder) => [
398
+ folder,
399
+ readFileOrNull(node_path_1.default.join(repoRoot, folder, "SUMMARY.md")),
400
+ ])),
401
+ };
366
402
  return {
367
403
  authority_boundaries: [
368
404
  "Read packet-scoped folders and their POLARIS.md and SUMMARY.md files",
@@ -389,9 +425,204 @@ function buildReconcilePacket() {
389
425
  "A requested write falls outside packet-allowed paths",
390
426
  "Work evidence is missing or contradictory",
391
427
  ],
428
+ packet_kind: "reconcile",
429
+ run_id: runId,
430
+ issue_id: issueId,
431
+ affected_folders: affectedFolders,
432
+ work_inventory: workInventory,
433
+ allowed_write_paths: allowedWritePaths,
434
+ prohibited_write_paths: [repoRoot],
435
+ constraints: {
436
+ max_summary_addition_lines: 50,
437
+ },
392
438
  };
393
439
  }
394
- function generateSkillPacket(skillName, config) {
440
+ function buildBlockedReconcilePacket(runId, issueId, repoRoot) {
441
+ return {
442
+ authority_boundaries: [
443
+ "Read packet-scoped folders and their POLARIS.md and SUMMARY.md files",
444
+ "Update cognition files only within packet-allowed write paths",
445
+ "Create one sealed local cognition commit",
446
+ ],
447
+ prohibited_actions: [
448
+ "Modify implementation source code, tests, or configuration",
449
+ "Move, ingest, classify, or promote documents",
450
+ "Write outside packet-allowed paths",
451
+ "Call polaris loop continue or polaris finalize",
452
+ "Git push or create a pull request",
453
+ "Fabricate affected folders or work inventory when no git diff is available",
454
+ ],
455
+ allowed_outputs: [
456
+ "Updated POLARIS.md and SUMMARY.md files in packet-allowed paths",
457
+ "A sealed local cognition commit",
458
+ ],
459
+ deliverables: [
460
+ "Packet-scoped cognition reconciled with completed work",
461
+ "All cognition changes recorded in one sealed local commit",
462
+ ],
463
+ stop_conditions: [
464
+ "All packet-scoped cognition reconciled",
465
+ "A requested write falls outside packet-allowed paths",
466
+ "Work evidence is missing or contradictory",
467
+ "No git diff is available to scope reconciliation",
468
+ ],
469
+ packet_kind: "reconcile",
470
+ run_id: runId,
471
+ issue_id: issueId,
472
+ affected_folders: [],
473
+ work_inventory: {
474
+ affected_folders: [],
475
+ all_changed_files: [],
476
+ child_summaries: [],
477
+ pending_cognition_notes: [],
478
+ polaris_md_files: {},
479
+ summary_md_files: {},
480
+ },
481
+ allowed_write_paths: [],
482
+ prohibited_write_paths: [repoRoot],
483
+ constraints: {
484
+ max_summary_addition_lines: 50,
485
+ },
486
+ };
487
+ }
488
+ // ── Reconcile helpers ─────────────────────────────────────────────────────────
489
+ function resolveIssueId(repoRoot, explicitIssueId) {
490
+ if (explicitIssueId)
491
+ return explicitIssueId;
492
+ const stateCandidates = [
493
+ node_path_1.default.join(repoRoot, ".taskchain_artifacts", "polaris-run", "current-state.json"),
494
+ node_path_1.default.join(repoRoot, ".polaris", "runs", "current-state.json"),
495
+ ];
496
+ for (const candidate of stateCandidates) {
497
+ try {
498
+ const raw = JSON.parse(node_fs_1.default.readFileSync(candidate, "utf-8"));
499
+ const activeChild = raw["active_child"];
500
+ if (typeof activeChild === "string" && activeChild)
501
+ return activeChild;
502
+ const clusterId = raw["cluster_id"];
503
+ if (typeof clusterId === "string" && clusterId)
504
+ return clusterId;
505
+ }
506
+ catch {
507
+ // ignore missing or malformed state
508
+ }
509
+ }
510
+ try {
511
+ const branch = execGit(repoRoot, ["branch", "--show-current"]).join("").trim();
512
+ const match = /^pol-?(\d+)(?:-.*)?$/i.exec(branch);
513
+ if (match && match[1]) {
514
+ return `POL-${match[1]}`;
515
+ }
516
+ }
517
+ catch {
518
+ // ignore
519
+ }
520
+ return "unknown";
521
+ }
522
+ function buildReconcileRunId(issueId) {
523
+ const slug = issueId.replace(/\s+/g, "-");
524
+ const date = new Date().toISOString().slice(0, 10);
525
+ // Minimal sequence: 001 for now; each call is a distinct run by packet_id.
526
+ const seq = "001";
527
+ return `polaris-reconcile-${slug}-${date}-${seq}`;
528
+ }
529
+ function getReconcileChangedFiles(repoRoot) {
530
+ const files = new Set();
531
+ try {
532
+ for (const file of execGit(repoRoot, ["diff", "--name-only", "HEAD"])) {
533
+ if (file)
534
+ files.add(file);
535
+ }
536
+ }
537
+ catch {
538
+ // ignore
539
+ }
540
+ try {
541
+ const base = resolveReconcileBase(repoRoot);
542
+ if (base) {
543
+ for (const file of execGit(repoRoot, ["diff", "--name-only", `${base}..HEAD`])) {
544
+ if (file)
545
+ files.add(file);
546
+ }
547
+ }
548
+ }
549
+ catch {
550
+ // ignore
551
+ }
552
+ return [...files].sort();
553
+ }
554
+ function resolveReconcileBase(repoRoot) {
555
+ const candidates = [
556
+ resolveRemoteDefaultBranch(repoRoot),
557
+ "origin/main",
558
+ "origin/master",
559
+ "main",
560
+ "master",
561
+ ];
562
+ for (const candidate of candidates) {
563
+ if (!candidate)
564
+ continue;
565
+ try {
566
+ const base = execGit(repoRoot, ["merge-base", "HEAD", candidate]).join("").trim();
567
+ if (base)
568
+ return base;
569
+ }
570
+ catch {
571
+ // try next candidate
572
+ }
573
+ }
574
+ return null;
575
+ }
576
+ function resolveRemoteDefaultBranch(repoRoot) {
577
+ try {
578
+ const ref = execGit(repoRoot, ["rev-parse", "--abbrev-ref", "refs/remotes/origin/HEAD"]).join("").trim();
579
+ return ref || null;
580
+ }
581
+ catch {
582
+ return null;
583
+ }
584
+ }
585
+ function resolveAffectedFolders(repoRoot, changedFiles) {
586
+ const routeMap = loadFileRouteMap(repoRoot);
587
+ const folders = new Set();
588
+ for (const file of changedFiles) {
589
+ const entry = routeMap[file];
590
+ if (entry && typeof entry.route === "string") {
591
+ folders.add(entry.route.replace(/\/+$/, "") + "/");
592
+ }
593
+ }
594
+ return [...folders].sort();
595
+ }
596
+ function loadFileRouteMap(repoRoot) {
597
+ const mapPath = node_path_1.default.join(repoRoot, ".polaris", "map", "file-routes.json");
598
+ try {
599
+ const raw = JSON.parse(node_fs_1.default.readFileSync(mapPath, "utf-8"));
600
+ const normalized = {};
601
+ for (const [key, value] of Object.entries(raw)) {
602
+ if (value && typeof value === "object") {
603
+ const entry = value;
604
+ normalized[key] = { route: typeof entry.route === "string" ? entry.route : undefined };
605
+ }
606
+ }
607
+ return normalized;
608
+ }
609
+ catch {
610
+ return {};
611
+ }
612
+ }
613
+ function readFileOrNull(filePath) {
614
+ try {
615
+ return node_fs_1.default.readFileSync(filePath, "utf-8");
616
+ }
617
+ catch {
618
+ return null;
619
+ }
620
+ }
621
+ function execGit(repoRoot, args) {
622
+ const output = (0, node_child_process_1.execFileSync)("git", args, { cwd: repoRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
623
+ return output.split("\n").filter(Boolean);
624
+ }
625
+ function generateSkillPacket(skillName, config, options) {
395
626
  const active_role = exports.SKILL_ROLE_MAP[skillName];
396
627
  const role_summary = ROLE_SUMMARIES[active_role];
397
628
  const generated_at = new Date().toISOString();
@@ -425,7 +656,7 @@ function generateSkillPacket(skillName, config) {
425
656
  body = buildCatalogPacket();
426
657
  break;
427
658
  case "reconcile":
428
- body = buildReconcilePacket();
659
+ body = buildReconcilePacket(config, options);
429
660
  break;
430
661
  }
431
662
  return {
@@ -43,7 +43,7 @@ function locateCanonFiles(changedFiles, repoRoot) {
43
43
  canon.add(polarisMd);
44
44
  }
45
45
  // Active doctrine
46
- const doctrineDir = (0, node_path_1.join)(repoRoot, "docs", "doctrine", "active");
46
+ const doctrineDir = (0, node_path_1.join)(repoRoot, "smartdocs", "doctrine", "active");
47
47
  if ((0, node_fs_1.existsSync)(doctrineDir)) {
48
48
  for (const f of (0, node_fs_1.readdirSync)(doctrineDir)) {
49
49
  if (f.endsWith(".md") && (0, node_path_1.basename)(f).toLowerCase() !== "summary.md")
@@ -61,7 +61,7 @@ function locateCanonFiles(changedFiles, repoRoot) {
61
61
  }
62
62
  }
63
63
  for (const specSubdir of ["active", "implemented"]) {
64
- const specDir = (0, node_path_1.join)(repoRoot, "docs", "specs", specSubdir);
64
+ const specDir = (0, node_path_1.join)(repoRoot, "smartdocs", "specs", specSubdir);
65
65
  if (!(0, node_fs_1.existsSync)(specDir))
66
66
  continue;
67
67
  for (const f of (0, node_fs_1.readdirSync)(specDir)) {
@@ -124,7 +124,7 @@ function checkPolarisMd(canonFile, changedFiles, repoRoot) {
124
124
  // Determine conflict type based on rule text and changed file
125
125
  let conflictType = "stale-implementation";
126
126
  const ext = changedFile.split(".").pop()?.toLowerCase() ?? "";
127
- const isDocFile = ext === "md" || changedFile.startsWith("docs/");
127
+ const isDocFile = ext === "md" || changedFile.startsWith("smartdocs/");
128
128
  const hasDocKeywords = /\b(doc|documentation|readme|guide|spec)\b/i.test(lower);
129
129
  const hasCandidateKeywords = /\b(candidate|divergence|proposal)\b/i.test(lower);
130
130
  if (isDocFile || hasDocKeywords) {
@@ -282,7 +282,7 @@ function runCanonCheck(options) {
282
282
  return result;
283
283
  }
284
284
  function writeStaleDraftDocs(conflicts, repoRoot, childId) {
285
- const rawDir = (0, node_path_1.join)(repoRoot, "docs", "raw");
285
+ const rawDir = (0, node_path_1.join)(repoRoot, "smartdocs", "raw");
286
286
  try {
287
287
  (0, node_fs_1.mkdirSync)(rawDir, { recursive: true });
288
288
  for (const conflict of conflicts) {
@@ -309,7 +309,7 @@ function writeStaleDraftDocs(conflicts, repoRoot, childId) {
309
309
  }
310
310
  }
311
311
  function writeCandidateDraftDocs(conflicts, repoRoot, childId) {
312
- const candidateDir = (0, node_path_1.join)(repoRoot, "docs", "doctrine", "candidate");
312
+ const candidateDir = (0, node_path_1.join)(repoRoot, "smartdocs", "doctrine", "candidate");
313
313
  try {
314
314
  (0, node_fs_1.mkdirSync)(candidateDir, { recursive: true });
315
315
  for (const conflict of conflicts) {
@@ -38,6 +38,32 @@ function printSeedAllResult(filename, dryRun, { written, skippedExists, skippedD
38
38
  }
39
39
  console.log(`${leadingBlankLine ? "\n" : ""}Done. ${written.length} written, ${skippedExists.length} skipped (exists), ${skippedDraft.length} skipped (draft)${extraSummary}.`);
40
40
  }
41
+ function printReconcileReport(report, dryRun) {
42
+ for (const path of report.created) {
43
+ console.log(`${dryRun ? "[dry-run] would create" : "created"}: ${path}`);
44
+ }
45
+ for (const path of report.regenerated) {
46
+ console.log(`${dryRun ? "[dry-run] would regenerate" : "regenerated"}: ${path}`);
47
+ }
48
+ for (const path of report.regeneratedRegion) {
49
+ console.log(`${dryRun ? "[dry-run] would regenerate (region)" : "regenerated (region)"}: ${path}`);
50
+ }
51
+ for (const path of report.skipped) {
52
+ console.log(`skipped (up-to-date): ${path}`);
53
+ }
54
+ for (const path of report.blocked) {
55
+ console.log(`blocked (manual migration): ${path}`);
56
+ }
57
+ if (report.skippedRoot) {
58
+ console.log(`skipped (root): ${report.skippedRoot.path}/ (${report.skippedRoot.reason})`);
59
+ }
60
+ if (report.skippedIneligible.length > 0) {
61
+ for (const entry of report.skippedIneligible) {
62
+ console.log(`skipped (ineligible): ${entry.path}/ (${entry.reason})`);
63
+ }
64
+ }
65
+ console.log(`\nDone. ${report.created.length} created, ${report.regenerated.length} regenerated, ${report.regeneratedRegion.length} regenerated (region), ${report.skipped.length} skipped, ${report.blocked.length} blocked.`);
66
+ }
41
67
  /**
42
68
  * Build and return the top-level "docs" Commander command group for Polaris docs lifecycle workflows.
43
69
  *
@@ -434,6 +460,34 @@ function createDocsCommand(options = {}) {
434
460
  console.log(`skipped (draft exists): ${targetPath}/index.md`);
435
461
  }
436
462
  });
463
+ docs
464
+ .command("reconcile")
465
+ .description("Reconcile POLARIS.md and SUMMARY.md files across all eligible route folders")
466
+ .option("--all", "Reconcile all eligible directories")
467
+ .option("--dry-run", "Print what would be changed without writing files")
468
+ .option("--include-agent-folders", "Include .codex, .claude, .agents folders (default: skip)")
469
+ .option("--include-hidden", "Include all hidden directories starting with . (default: skip)")
470
+ .option("--include-root", "Include root directory (default: skip)")
471
+ .option("-r, --repo-root <path>", "Repository root", defaultRepoRoot)
472
+ .action((options) => {
473
+ if (!options.all) {
474
+ console.error("Error: provide --all to reconcile all eligible directories");
475
+ process.exit(1);
476
+ }
477
+ try {
478
+ const report = (0, seed_instructions_js_1.reconcileAll)(options.repoRoot, {
479
+ dryRun: options.dryRun,
480
+ includeAgentFolders: options.includeAgentFolders,
481
+ includeHidden: options.includeHidden,
482
+ includeRoot: options.includeRoot,
483
+ });
484
+ printReconcileReport(report, options.dryRun);
485
+ }
486
+ catch (err) {
487
+ console.error(`polaris docs reconcile: ${err instanceof Error ? err.message : String(err)}`);
488
+ process.exit(1);
489
+ }
490
+ });
437
491
  docs
438
492
  .command("backfill-type")
439
493
  .description("Add OKF-conformant `type` frontmatter to existing smartdocs/ files that are missing it, " +