@lsctech/polaris 0.4.5 → 0.4.7

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.
@@ -140,8 +140,9 @@ async function runAdoptPhase(phase, repoRoot, options = {}) {
140
140
  if (!options.inventory || !options.plan) {
141
141
  throw new Error("consolidate requires inventory and plan");
142
142
  }
143
+ const dryRun = options.plan.dry_run ?? options.dryRun ?? false;
143
144
  await (0, adopt_smartdocs_js_1.migrateSmartDocs)(options.plan, repoRoot);
144
- await (0, adopt_genesis_js_1.reconcileAgentFiles)(repoRoot);
145
+ await (0, adopt_genesis_js_1.reconcileAgentFiles)(repoRoot, { dryRun });
145
146
  break;
146
147
  }
147
148
  case "map": {
@@ -25,7 +25,9 @@ function isAlreadyPointer(content) {
25
25
  });
26
26
  return meaningfulLines.length <= 3 && meaningfulLines.some((line) => line.includes("POLARIS_RULES.md"));
27
27
  }
28
- function appendGenesisProvenance(repoRoot, records) {
28
+ function appendGenesisProvenance(repoRoot, records, dryRun = false) {
29
+ if (dryRun)
30
+ return;
29
31
  if (records.length === 0)
30
32
  return;
31
33
  const provenancePath = (0, node_path_1.join)(repoRoot, ".polaris", "adoption-provenance.json");
@@ -57,6 +59,7 @@ async function reconcileAgentFiles(repoRoot, options) {
57
59
  const provenanceRecords = [];
58
60
  const now = options?.now ?? new Date();
59
61
  const timestamp = now.toISOString();
62
+ const dryRun = options?.dryRun ?? false;
60
63
  for (const file of AGENT_FILES) {
61
64
  const filePath = (0, node_path_1.join)(repoRoot, file);
62
65
  if (!(0, node_fs_1.existsSync)(filePath)) {
@@ -74,6 +77,11 @@ async function reconcileAgentFiles(repoRoot, options) {
74
77
  });
75
78
  continue;
76
79
  }
80
+ if (dryRun) {
81
+ process.stdout.write(`[dry-run] would prompt to compress and archive ${file} as genesis doctrine\n`);
82
+ results.push({ file, outcome: "skipped" });
83
+ continue;
84
+ }
77
85
  const rl = (0, promises_1.createInterface)({ input: process.stdin, output: process.stdout });
78
86
  let answer;
79
87
  try {
@@ -146,11 +154,13 @@ async function reconcileAgentFiles(repoRoot, options) {
146
154
  const dateStr = formatDate(now);
147
155
  const genesisPath = `smartdocs/doctrine/active/${dateStr}-genesis-agent-doctrine.md`;
148
156
  const genesisFullPath = (0, node_path_1.join)(repoRoot, genesisPath);
149
- (0, node_fs_1.mkdirSync)((0, node_path_1.join)(repoRoot, "smartdocs/doctrine/active"), { recursive: true });
150
- (0, node_fs_1.writeFileSync)(genesisFullPath, distilled, "utf8");
151
- const thinPointer = `# Agent Instructions\n\nRead [POLARIS_RULES.md](POLARIS_RULES.md) before beginning any work.\n\n` +
152
- `<!-- genesis doctrine archived: ${genesisPath} -->\n`;
153
- (0, node_fs_1.writeFileSync)(filePath, thinPointer, "utf8");
157
+ if (!dryRun) {
158
+ (0, node_fs_1.mkdirSync)((0, node_path_1.join)(repoRoot, "smartdocs/doctrine/active"), { recursive: true });
159
+ (0, node_fs_1.writeFileSync)(genesisFullPath, distilled, "utf8");
160
+ const thinPointer = `# Agent Instructions\n\nRead [POLARIS_RULES.md](POLARIS_RULES.md) before beginning any work.\n\n` +
161
+ `<!-- genesis doctrine archived: ${genesisPath} -->\n`;
162
+ (0, node_fs_1.writeFileSync)(filePath, thinPointer, "utf8");
163
+ }
154
164
  results.push({ file, outcome: "compressed", genesisPath });
155
165
  provenanceRecords.push({
156
166
  source_path: file,
@@ -160,6 +170,6 @@ async function reconcileAgentFiles(repoRoot, options) {
160
170
  migration_outcome: "compressed",
161
171
  });
162
172
  }
163
- appendGenesisProvenance(repoRoot, provenanceRecords);
173
+ appendGenesisProvenance(repoRoot, provenanceRecords, dryRun);
164
174
  return results;
165
175
  }
@@ -113,8 +113,17 @@ function normalizeStatus(step, status, completedAt) {
113
113
  }
114
114
  const SMARTDOCS_BUNDLE_INDEX_CONTENT = `---\nokf_version: "0.1"\n---\n\n# SmartDocs — Polaris Cognition Bundle\n\n# Governance\n\n- [Docs authority model](specs/active/docs-authority-model.md)\n- [Doctrine](doctrine/active/)\n\n# Routes\n\n<!-- Routes placeholder: dynamic file-routes.json generation deferred. -->\n`;
115
115
  const SMARTDOCS_BUNDLE_LOG_CONTENT = "# SmartDocs — Change Log\n";
116
- function scaffoldBundleRoot(repoRoot) {
116
+ function scaffoldBundleRoot(repoRoot, dryRun = false) {
117
117
  const smartdocsDir = (0, node_path_1.join)(repoRoot, "smartdocs");
118
+ if (dryRun) {
119
+ if (!(0, node_fs_1.existsSync)((0, node_path_1.join)(smartdocsDir, "index.md"))) {
120
+ process.stdout.write(`[dry-run] would create smartdocs/index.md\n`);
121
+ }
122
+ if (!(0, node_fs_1.existsSync)((0, node_path_1.join)(smartdocsDir, "log.md"))) {
123
+ process.stdout.write(`[dry-run] would create smartdocs/log.md\n`);
124
+ }
125
+ return;
126
+ }
118
127
  (0, node_fs_1.mkdirSync)(smartdocsDir, { recursive: true });
119
128
  const indexPath = (0, node_path_1.join)(smartdocsDir, "index.md");
120
129
  if (!(0, node_fs_1.existsSync)(indexPath)) {
@@ -130,59 +139,72 @@ function migrateSmartDocs(plan, repoRoot = (0, node_path_1.resolve)(process.cwd(
130
139
  const provenanceRecords = [];
131
140
  const seenRecords = existingRecordKeys(repoRoot);
132
141
  const now = new Date().toISOString();
142
+ const dryRun = effectivePlan.dry_run ?? false;
133
143
  for (let index = 0; index < effectivePlan.steps.length; index += 1) {
134
144
  const step = effectivePlan.steps[index];
135
145
  if (step.category !== "smartdocs-migrate" || COMPLETE_STATUSES.has(step.status)) {
136
146
  continue;
137
147
  }
138
148
  if (step.routing !== undefined && step.routing !== "candidate") {
139
- effectivePlan.steps[index] = {
140
- ...step,
141
- status: "skipped",
142
- completed_at: now,
143
- error: `routing not candidate: ${step.routing}`,
144
- };
149
+ if (!dryRun) {
150
+ effectivePlan.steps[index] = {
151
+ ...step,
152
+ status: "skipped",
153
+ completed_at: now,
154
+ error: `routing not candidate: ${step.routing}`,
155
+ };
156
+ }
145
157
  continue;
146
158
  }
147
159
  const sourcePath = step.source_path ?? "";
148
160
  const destPath = step.dest_path ?? `smartdocs/raw/${(0, node_path_1.basename)(sourcePath || `step-${step.order}.md`)}`;
149
161
  if (!sourcePath) {
150
- effectivePlan.steps[index] = normalizeStatus(step, "skipped", now);
162
+ if (!dryRun)
163
+ effectivePlan.steps[index] = normalizeStatus(step, "skipped", now);
151
164
  continue;
152
165
  }
153
166
  if (isExcludedSourcePath(sourcePath)) {
154
- effectivePlan.steps[index] = normalizeStatus(step, "skipped", now);
167
+ if (!dryRun)
168
+ effectivePlan.steps[index] = normalizeStatus(step, "skipped", now);
155
169
  continue;
156
170
  }
157
171
  const sourceAbs = (0, node_path_1.join)(repoRoot, sourcePath);
158
172
  const destAbs = (0, node_path_1.join)(repoRoot, destPath);
159
- (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(destAbs), { recursive: true });
160
173
  if (!(0, node_fs_1.existsSync)(sourceAbs)) {
161
174
  if ((0, node_fs_1.existsSync)(destAbs)) {
162
- effectivePlan.steps[index] = normalizeStatus(step, "completed", step.completed_at ?? now);
163
- const record = {
164
- step_id: step.step_id,
165
- source_path: sourcePath,
166
- dest_path: destPath,
167
- migrated_at: step.completed_at ?? now,
168
- migration_run_id: effectivePlan.plan_id,
169
- transport: "reconciled",
170
- };
171
- if (!seenRecords.has(recordKey(record))) {
172
- provenanceRecords.push(record);
173
- seenRecords.add(recordKey(record));
175
+ if (!dryRun) {
176
+ effectivePlan.steps[index] = normalizeStatus(step, "completed", step.completed_at ?? now);
177
+ const record = {
178
+ step_id: step.step_id,
179
+ source_path: sourcePath,
180
+ dest_path: destPath,
181
+ migrated_at: step.completed_at ?? now,
182
+ migration_run_id: effectivePlan.plan_id,
183
+ transport: "reconciled",
184
+ };
185
+ if (!seenRecords.has(recordKey(record))) {
186
+ provenanceRecords.push(record);
187
+ seenRecords.add(recordKey(record));
188
+ }
174
189
  }
175
190
  }
176
191
  else {
177
- effectivePlan.steps[index] = {
178
- ...step,
179
- status: "skipped",
180
- completed_at: now,
181
- error: `source missing: ${sourcePath}`,
182
- };
192
+ if (!dryRun) {
193
+ effectivePlan.steps[index] = {
194
+ ...step,
195
+ status: "skipped",
196
+ completed_at: now,
197
+ error: `source missing: ${sourcePath}`,
198
+ };
199
+ }
183
200
  }
184
201
  continue;
185
202
  }
203
+ if (dryRun) {
204
+ process.stdout.write(`[dry-run] would move ${sourcePath} → ${destPath}\n`);
205
+ continue;
206
+ }
207
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(destAbs), { recursive: true });
186
208
  let transport = "git mv";
187
209
  try {
188
210
  (0, node_child_process_1.execFileSync)("git", ["mv", "--", sourcePath, destPath], {
@@ -209,9 +231,11 @@ function migrateSmartDocs(plan, repoRoot = (0, node_path_1.resolve)(process.cwd(
209
231
  seenRecords.add(recordKey(record));
210
232
  }
211
233
  }
212
- scaffoldBundleRoot(repoRoot);
213
- savePlan(repoRoot, effectivePlan);
214
- saveProvenance(repoRoot, provenanceRecords);
215
- Object.assign(plan, effectivePlan);
234
+ scaffoldBundleRoot(repoRoot, dryRun);
235
+ if (!dryRun) {
236
+ savePlan(repoRoot, effectivePlan);
237
+ saveProvenance(repoRoot, provenanceRecords);
238
+ Object.assign(plan, effectivePlan);
239
+ }
216
240
  return Promise.resolve();
217
241
  }
@@ -144,11 +144,11 @@ function generateLibrarianPacket(options) {
144
144
  }
145
145
  // ── Helpers ───────────────────────────────────────────────────────────────────
146
146
  function resolveStateFile(repoRoot, clusterId) {
147
- // Check current convention: cluster-state.json
148
- const clusterState = node_path_1.default.join(repoRoot, ".polaris", "clusters", clusterId, "cluster-state.json");
149
- if (node_fs_1.default.existsSync(clusterState))
150
- return clusterState;
151
- // Check legacy convention: state.json
147
+ // Note: .polaris/clusters/<id>/cluster-state.json is intentionally not a candidate here.
148
+ // It holds the ClusterState schema (child_states, packet_pointers, ...), not the LoopState
149
+ // schema (completed_children, run_id, open_children_meta, ...) that readState()/this
150
+ // function require.
151
+ // Check per-cluster run-state convention: state.json
152
152
  const legacyState = node_path_1.default.join(repoRoot, ".polaris", "clusters", clusterId, "state.json");
153
153
  if (node_fs_1.default.existsSync(legacyState))
154
154
  return legacyState;
@@ -160,8 +160,8 @@ function resolveStateFile(repoRoot, clusterId) {
160
160
  const legacyRuns = node_path_1.default.join(repoRoot, ".polaris", "runs", "current-state.json");
161
161
  if (node_fs_1.default.existsSync(legacyRuns))
162
162
  return legacyRuns;
163
- // Return current convention path as default
164
- return clusterState;
163
+ // Return per-cluster state path as default
164
+ return legacyState;
165
165
  }
166
166
  function findResultFile(resultsDir, childId) {
167
167
  try {
@@ -14,6 +14,8 @@ const canon_check_js_1 = require("../smartdocs-engine/canon-check.js");
14
14
  const dispatch_boundary_js_1 = require("./dispatch-boundary.js");
15
15
  const ledger_js_1 = require("./ledger.js");
16
16
  const librarian_dispatch_js_1 = require("../cognition/librarian-dispatch.js");
17
+ const index_js_1 = require("../tracker/index.js");
18
+ const lifecycle_transition_js_1 = require("../tracker/lifecycle-transition.js");
17
19
  /** Resolves the expected sealed result file path for a child from its dispatch metadata. */
18
20
  function resolveResultFileForChild(state, childId) {
19
21
  const meta = state.open_children_meta?.[childId];
@@ -206,6 +208,10 @@ function countLoopAbortedEvents(telemetryFile, runId) {
206
208
  .filter((event) => event.event === "loop-aborted" && event.run_id === runId)
207
209
  .length;
208
210
  }
211
+ function appendTelemetryEvent(telemetryFile, event) {
212
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(telemetryFile), { recursive: true });
213
+ (0, node_fs_1.appendFileSync)(telemetryFile, JSON.stringify(event) + "\n", "utf-8");
214
+ }
209
215
  function resolveTelemetryFilePath(state, repoRoot) {
210
216
  const artifactDir = state.artifact_dir ?? (0, node_path_1.join)(repoRoot, ".taskchain_artifacts", "polaris-run");
211
217
  return (0, node_path_1.join)(artifactDir, "runs", state.run_id, "telemetry.jsonl");
@@ -430,6 +436,71 @@ function runLoopContinue(options) {
430
436
  process.exit(1);
431
437
  }
432
438
  }
439
+ // ── Apply lifecycle transition for child-validation-passed event ───────
440
+ // Fire-and-forget: tracker mutations must not block state checkpointing.
441
+ // Policy default targets "in_review" (not "done") — "done" is reserved for
442
+ // the child-merged event, applied once the delivering PR actually merges.
443
+ let transitionAdapter;
444
+ let transitionConfig;
445
+ const transitionTelemetryFile = resolveTelemetryFilePath(state, repoRoot);
446
+ try {
447
+ transitionConfig = (0, loader_js_1.loadConfig)(repoRoot);
448
+ transitionAdapter = (0, index_js_1.loadTrackerAdapter)(transitionConfig);
449
+ }
450
+ catch (err) {
451
+ appendTelemetryEvent(transitionTelemetryFile, {
452
+ event: "lifecycle-transition-error",
453
+ run_id: state.run_id,
454
+ child_id: completedChild,
455
+ transition_event: "child-validation-passed",
456
+ error: `Failed to load config or tracker adapter: ${err instanceof Error ? err.message : String(err)}`,
457
+ timestamp: new Date().toISOString(),
458
+ });
459
+ transitionAdapter = null;
460
+ transitionConfig = null;
461
+ }
462
+ // Note: transitionAdapter may legitimately be null (no tracker adapter configured) —
463
+ // that's not an error, and applyTransitionSafe/applyTransition handle a null adapter
464
+ // by returning a skip result. Only skip the attempt entirely when config loading itself
465
+ // failed (transitionConfig is null).
466
+ if (transitionConfig) {
467
+ new lifecycle_transition_js_1.LifecycleTransitionService()
468
+ .applyTransitionSafe({
469
+ adapter: transitionAdapter,
470
+ policy: transitionConfig.tracker?.lifecyclePolicy,
471
+ taskId: completedChild,
472
+ event: "child-validation-passed",
473
+ evidence: {
474
+ commit: completionCommit,
475
+ validationResults: completionValidation,
476
+ },
477
+ timestamp: new Date().toISOString(),
478
+ })
479
+ .then((result) => {
480
+ appendTelemetryEvent(transitionTelemetryFile, {
481
+ event: "lifecycle-transition-attempt",
482
+ run_id: state.run_id,
483
+ child_id: completedChild,
484
+ transition_event: result.event,
485
+ target_state: result.targetState,
486
+ applied: result.applied,
487
+ skipped: result.skipped,
488
+ skip_reason: result.skipReason,
489
+ error: result.error,
490
+ timestamp: result.timestamp,
491
+ });
492
+ })
493
+ .catch((err) => {
494
+ appendTelemetryEvent(transitionTelemetryFile, {
495
+ event: "lifecycle-transition-error",
496
+ run_id: state.run_id,
497
+ child_id: completedChild,
498
+ transition_event: "child-validation-passed",
499
+ error: err instanceof Error ? err.message : String(err),
500
+ timestamp: new Date().toISOString(),
501
+ });
502
+ });
503
+ }
433
504
  }
434
505
  if (completedChild && completionResultFile) {
435
506
  const telemetryFile = resolveTelemetryFilePath(state, repoRoot);
@@ -15,6 +15,29 @@ const audit_js_1 = require("./audit.js");
15
15
  const triage_js_1 = require("./triage.js");
16
16
  const adapter_js_1 = require("../graph/store/adapter.js");
17
17
  const index_js_1 = require("../graph/query/index.js");
18
+ /**
19
+ * Prints the shared written/skipped-exists/skipped-draft report and summary line used by
20
+ * every `seed-*-all` command (seed-index, seed-instructions, seed-summary, reformat-okf).
21
+ *
22
+ * @param filename - The generated filename (e.g. "index.md") to report against each directory.
23
+ * @param dryRun - Whether this was a dry-run pass (controls the "would write" vs "written" label).
24
+ * @param result - The written/skippedExists/skippedDraft arrays from a seed-*-all call.
25
+ * @param options.leadingBlankLine - Whether to prefix the summary line with a blank line (default true).
26
+ * @param options.extraSummary - Extra summary text appended after "skipped (draft)" (e.g. root/ineligible counts).
27
+ */
28
+ function printSeedAllResult(filename, dryRun, { written, skippedExists, skippedDraft }, options = {}) {
29
+ const { leadingBlankLine = true, extraSummary = "" } = options;
30
+ for (const dir of written) {
31
+ console.log(`${dryRun ? "[dry-run] would write" : "written"}: ${dir}/${filename}`);
32
+ }
33
+ for (const dir of skippedExists) {
34
+ console.log(`skipped (human-edited): ${dir}/${filename}`);
35
+ }
36
+ for (const dir of skippedDraft) {
37
+ console.log(`skipped (draft exists): ${dir}/${filename}`);
38
+ }
39
+ console.log(`${leadingBlankLine ? "\n" : ""}Done. ${written.length} written, ${skippedExists.length} skipped (exists), ${skippedDraft.length} skipped (draft)${extraSummary}.`);
40
+ }
18
41
  /**
19
42
  * Build and return the top-level "docs" Commander command group for Polaris docs lifecycle workflows.
20
43
  *
@@ -283,15 +306,6 @@ function createDocsCommand(options = {}) {
283
306
  includeHidden: options.includeHidden,
284
307
  includeRoot: options.includeRoot,
285
308
  });
286
- for (const dir of written) {
287
- console.log(`${options.dryRun ? "[dry-run] would write" : "written"}: ${dir}/POLARIS.md`);
288
- }
289
- for (const dir of skippedExists) {
290
- console.log(`skipped (human-edited): ${dir}/POLARIS.md`);
291
- }
292
- for (const dir of skippedDraft) {
293
- console.log(`skipped (draft exists): ${dir}/POLARIS.md`);
294
- }
295
309
  if (options.dryRun) {
296
310
  if (skippedRoot) {
297
311
  console.log(`\nSkipped (root):`);
@@ -315,7 +329,7 @@ function createDocsCommand(options = {}) {
315
329
  }
316
330
  }
317
331
  const rootCount = skippedRoot ? 1 : 0;
318
- console.log(`\nDone. ${written.length} written, ${skippedExists.length} skipped (exists), ${skippedDraft.length} skipped (draft), ${rootCount} skipped (root), ${skippedIneligible.length} skipped (ineligible).`);
332
+ printSeedAllResult("POLARIS.md", options.dryRun, { written, skippedExists, skippedDraft }, { extraSummary: `, ${rootCount} skipped (root), ${skippedIneligible.length} skipped (ineligible)` });
319
333
  return;
320
334
  }
321
335
  if (!pathArg) {
@@ -351,15 +365,6 @@ function createDocsCommand(options = {}) {
351
365
  includeHidden: options.includeHidden,
352
366
  includeRoot: options.includeRoot,
353
367
  });
354
- for (const dir of written) {
355
- console.log(`${options.dryRun ? "[dry-run] would write" : "written"}: ${dir}/SUMMARY.md`);
356
- }
357
- for (const dir of skippedExists) {
358
- console.log(`skipped (human-edited): ${dir}/SUMMARY.md`);
359
- }
360
- for (const dir of skippedDraft) {
361
- console.log(`skipped (draft exists): ${dir}/SUMMARY.md`);
362
- }
363
368
  if (options.dryRun) {
364
369
  if (skippedRoot) {
365
370
  console.log(`\nSkipped (root):`);
@@ -383,7 +388,7 @@ function createDocsCommand(options = {}) {
383
388
  }
384
389
  }
385
390
  const rootCount = skippedRoot ? 1 : 0;
386
- console.log(`\nDone. ${written.length} written, ${skippedExists.length} skipped (exists), ${skippedDraft.length} skipped (draft), ${rootCount} skipped (root), ${skippedIneligible.length} skipped (ineligible).`);
391
+ printSeedAllResult("SUMMARY.md", options.dryRun, { written, skippedExists, skippedDraft }, { extraSummary: `, ${rootCount} skipped (root), ${skippedIneligible.length} skipped (ineligible)` });
387
392
  return;
388
393
  }
389
394
  if (!pathArg) {
@@ -413,16 +418,7 @@ function createDocsCommand(options = {}) {
413
418
  const { written, skippedExists, skippedDraft, } = (0, seed_instructions_js_1.seedIndexAll)(options.repoRoot, {
414
419
  dryRun: options.dryRun,
415
420
  });
416
- for (const dir of written) {
417
- console.log(`${options.dryRun ? "[dry-run] would write" : "written"}: ${dir}/index.md`);
418
- }
419
- for (const dir of skippedExists) {
420
- console.log(`skipped (human-edited): ${dir}/index.md`);
421
- }
422
- for (const dir of skippedDraft) {
423
- console.log(`skipped (draft exists): ${dir}/index.md`);
424
- }
425
- console.log(`\nDone. ${written.length} written, ${skippedExists.length} skipped (exists), ${skippedDraft.length} skipped (draft).`);
421
+ printSeedAllResult("index.md", options.dryRun, { written, skippedExists, skippedDraft });
426
422
  return;
427
423
  }
428
424
  const targetPath = pathArg || "smartdocs";
@@ -438,6 +434,50 @@ function createDocsCommand(options = {}) {
438
434
  console.log(`skipped (draft exists): ${targetPath}/index.md`);
439
435
  }
440
436
  });
437
+ docs
438
+ .command("reformat-okf")
439
+ .description("Migrate existing smartdocs to OKF structure in one step: runs migrate → seed-index --all → seed-instructions --all. " +
440
+ "Agent instruction files (CLAUDE.md, AGENTS.md, etc.) are never touched. " +
441
+ "Use --dry-run to preview changes before writing.")
442
+ .option("--dry-run", "Preview what would change without writing any files")
443
+ .option("-r, --repo-root <path>", "Repository root", defaultRepoRoot)
444
+ .action((options) => {
445
+ const dryRun = options.dryRun;
446
+ const repoRoot = options.repoRoot;
447
+ const label = dryRun ? "[dry-run]" : "";
448
+ // Step 1: migrate (moves scattered markdown to smartdocs/raw/)
449
+ console.log(`${label ? label + " " : ""}Step 1/3: migrate`);
450
+ try {
451
+ const migrateResult = (0, migrate_js_1.migrateDocs)({ repoRoot, dryRun });
452
+ (0, migrate_js_1.printMigrateResults)(migrateResult);
453
+ }
454
+ catch (err) {
455
+ console.error(`reformat-okf: migrate failed — ${err instanceof Error ? err.message : String(err)}`);
456
+ process.exit(1);
457
+ }
458
+ // Step 2: seed-index --all (ensures index.md with okf_version frontmatter in every smartdocs dir)
459
+ console.log(`\n${label ? label + " " : ""}Step 2/3: seed-index --all`);
460
+ try {
461
+ const { written, skippedExists, skippedDraft } = (0, seed_instructions_js_1.seedIndexAll)(repoRoot, { dryRun });
462
+ printSeedAllResult("index.md", dryRun, { written, skippedExists, skippedDraft }, { leadingBlankLine: false });
463
+ }
464
+ catch (err) {
465
+ console.error(`reformat-okf: seed-index failed — ${err instanceof Error ? err.message : String(err)}`);
466
+ process.exit(1);
467
+ }
468
+ // Step 3: seed-instructions --all (ensures POLARIS.md drafts; never touches CLAUDE.md/AGENTS.md)
469
+ console.log(`\n${label ? label + " " : ""}Step 3/3: seed-instructions --all`);
470
+ try {
471
+ const { written, skippedExists, skippedDraft, skippedIneligible, skippedRoot } = (0, seed_instructions_js_1.seedInstructionsAll)(repoRoot, { dryRun });
472
+ const rootCount = skippedRoot ? 1 : 0;
473
+ printSeedAllResult("POLARIS.md", dryRun, { written, skippedExists, skippedDraft }, { leadingBlankLine: false, extraSummary: `, ${rootCount} skipped (root), ${skippedIneligible.length} skipped (ineligible)` });
474
+ }
475
+ catch (err) {
476
+ console.error(`reformat-okf: seed-instructions failed — ${err instanceof Error ? err.message : String(err)}`);
477
+ process.exit(1);
478
+ }
479
+ console.log(`\nreformat-okf complete.${dryRun ? " (dry-run — no files written)" : ""}`);
480
+ });
441
481
  docs
442
482
  .command("audit")
443
483
  .description("Scan repo for files at risk of recursive ingestion")
@@ -287,6 +287,10 @@ function generateDirectoryIndex(targetDir, repoRoot) {
287
287
  const dirLabel = (0, node_path_1.basename)(absDir) || "SmartDocs";
288
288
  const files = listConceptFiles(absDir);
289
289
  const lines = [
290
+ "---",
291
+ "okf_version: \"0.1\"",
292
+ "---",
293
+ "",
290
294
  exports.DRAFT_MARKER,
291
295
  `# ${dirLabel}`,
292
296
  "",
@@ -190,6 +190,36 @@ class LinearGraphqlClient {
190
190
  `, { id });
191
191
  return data.issue;
192
192
  }
193
+ async getIssueStateOptions(id) {
194
+ const data = await this.graphql(`
195
+ query PolarisLinearIssueStateOptions($id: String!) {
196
+ issue(id: $id) {
197
+ state { id }
198
+ team {
199
+ states(first: 250) {
200
+ nodes { id name type }
201
+ }
202
+ }
203
+ }
204
+ }
205
+ `, { id });
206
+ if (!data.issue)
207
+ return null;
208
+ return {
209
+ currentStateId: data.issue.state?.id ?? null,
210
+ states: data.issue.team?.states?.nodes ?? [],
211
+ };
212
+ }
213
+ async updateIssueState(id, stateId) {
214
+ const data = await this.graphql(`
215
+ mutation PolarisLinearIssueUpdateState($id: String!, $stateId: String!) {
216
+ issueUpdate(id: $id, input: { stateId: $stateId }) {
217
+ success
218
+ }
219
+ }
220
+ `, { id, stateId });
221
+ return data.issueUpdate.success;
222
+ }
193
223
  }
194
224
  /**
195
225
  * Implements the Linear direct adapter for synchronizing issues.
@@ -530,7 +560,7 @@ class LinearAdapter {
530
560
  * @param evidence - Optional evidence for the transition (e.g., commit hash).
531
561
  * @returns A lifecycle transition result indicating success, skip, or failure.
532
562
  */
533
- async transitionLifecycleState(taskId, lifecycleState, evidence) {
563
+ async transitionLifecycleState(taskId, lifecycleState, _evidence) {
534
564
  if (lifecycleState === "no_status_change") {
535
565
  return {
536
566
  applied: false,
@@ -538,17 +568,82 @@ class LinearAdapter {
538
568
  skipReason: "Lifecycle state is 'no_status_change', skipping transition",
539
569
  };
540
570
  }
541
- // Linear requires team-specific state IDs for state transitions.
542
- // This implementation is a placeholder - full implementation would need:
543
- // 1. Fetch available states for the issue's team
544
- // 2. Map normalized lifecycle state to a specific Linear state ID
545
- // 3. Use GraphQL mutation to update the issue state
546
- console.warn(`LinearAdapter: transitionLifecycleState not fully implemented. Would transition ${taskId} to ${lifecycleState}.`);
547
- return {
548
- applied: false,
549
- skipped: true,
550
- skipReason: "Lifecycle state transitions are not yet implemented for Linear adapter",
571
+ let stateOptions;
572
+ try {
573
+ stateOptions = await this.linearClient.getIssueStateOptions(taskId);
574
+ }
575
+ catch (err) {
576
+ return {
577
+ applied: false,
578
+ skipped: false,
579
+ error: `Failed to fetch Linear workflow states for ${taskId}: ${err instanceof Error ? err.message : String(err)}`,
580
+ };
581
+ }
582
+ if (!stateOptions) {
583
+ return {
584
+ applied: false,
585
+ skipped: true,
586
+ skipReason: `Linear issue '${taskId}' not found`,
587
+ };
588
+ }
589
+ const targetState = this.resolveWorkflowState(stateOptions.states, lifecycleState);
590
+ if (!targetState) {
591
+ return {
592
+ applied: false,
593
+ skipped: true,
594
+ skipReason: `No Linear workflow state on this issue's team maps to lifecycle state '${lifecycleState}'`,
595
+ };
596
+ }
597
+ if (targetState.id === stateOptions.currentStateId) {
598
+ return {
599
+ applied: false,
600
+ skipped: true,
601
+ skipReason: `Issue is already in target state '${targetState.name}'`,
602
+ };
603
+ }
604
+ try {
605
+ const success = await this.linearClient.updateIssueState(taskId, targetState.id);
606
+ if (!success) {
607
+ return {
608
+ applied: false,
609
+ skipped: false,
610
+ error: `Linear issueUpdate mutation for ${taskId} returned success: false`,
611
+ };
612
+ }
613
+ return { applied: true, skipped: false };
614
+ }
615
+ catch (err) {
616
+ return {
617
+ applied: false,
618
+ skipped: false,
619
+ error: err instanceof Error ? err.message : String(err),
620
+ };
621
+ }
622
+ }
623
+ /**
624
+ * Picks the Linear workflow state that best matches a normalized lifecycle state.
625
+ *
626
+ * Prefers exact `type` alignment (Linear's own backlog/unstarted/started/completed/canceled
627
+ * classification) so custom state names don't need to match `mapNativeStatus`'s heuristics;
628
+ * falls back to the first state whose name heuristically maps to the target state.
629
+ */
630
+ resolveWorkflowState(states, lifecycleState) {
631
+ const typeCandidates = {
632
+ backlog: ["backlog", "unstarted"],
633
+ in_progress: ["started"],
634
+ in_review: ["started"],
635
+ done: ["completed"],
636
+ blocked: ["started"],
637
+ cancelled: ["canceled", "cancelled"],
551
638
  };
639
+ const byHeuristic = states.filter((s) => this.mapNativeStatus(s.name).lifecycleState === lifecycleState);
640
+ if (byHeuristic.length === 1)
641
+ return byHeuristic[0];
642
+ const wantedTypes = typeCandidates[lifecycleState] ?? [];
643
+ const byType = (byHeuristic.length > 0 ? byHeuristic : states).filter((s) => wantedTypes.includes(s.type));
644
+ if (byType.length > 0)
645
+ return byType[0];
646
+ return byHeuristic[0] ?? null;
552
647
  }
553
648
  /**
554
649
  * Adds a comment to a Linear issue.
@@ -0,0 +1,40 @@
1
+ <!-- polaris:draft -->
2
+ # workspace
3
+
4
+ > Polaris draft — review and remove the `<!-- polaris:draft -->` marker to promote.
5
+
6
+ ## Purpose
7
+
8
+ <!-- One paragraph describing what this folder does. -->
9
+
10
+ **Domain:** workspace
11
+ **Route:** src/workspace
12
+ **Taskchain:** polaris-workspace
13
+
14
+ ## What belongs here
15
+
16
+ - `POLARIS_RULES.md` — src/workspace (workspace)
17
+
18
+ ## What does not belong here
19
+
20
+ <!-- Explicit exclusions of files or responsibilities. -->
21
+
22
+ ## Editing rules
23
+
24
+ <!-- Behavioral constraints for agents and humans. -->
25
+
26
+ ## Architecture assumptions
27
+
28
+ <!-- What the code assumes about the world. -->
29
+
30
+ ## Read before editing
31
+
32
+ - [analyst.md](src/workspace/.polaris/roles/analyst.md)
33
+ - [closeout-librarian.md](src/workspace/.polaris/roles/closeout-librarian.md)
34
+ - [cognition-librarian.md](src/workspace/.polaris/roles/cognition-librarian.md)
35
+ - [finalizer.md](src/workspace/.polaris/roles/finalizer.md)
36
+ - [foreman.md](src/workspace/.polaris/roles/foreman.md)
37
+
38
+ ## Related routes
39
+
40
+ <!-- Atlas route pointer to sibling or parent folders. -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lsctech/polaris",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "description": "Polaris — standalone taskchain orchestration framework for governed AI agent workflows",
5
5
  "bin": {
6
6
  "polaris": "dist/cli/index.js",