@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.
- package/dist/cli/adopt-command.js +2 -1
- package/dist/cli/adopt-genesis.js +17 -7
- package/dist/cli/adopt-smartdocs.js +56 -32
- package/dist/cognition/librarian-packet.js +7 -7
- package/dist/loop/continue.js +71 -0
- package/dist/smartdocs-engine/index.js +70 -30
- package/dist/smartdocs-engine/seed-instructions.js +4 -0
- package/dist/tracker/adapters/linear/index.js +106 -11
- package/dist/workspace/POLARIS.md +40 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
162
|
+
if (!dryRun)
|
|
163
|
+
effectivePlan.steps[index] = normalizeStatus(step, "skipped", now);
|
|
151
164
|
continue;
|
|
152
165
|
}
|
|
153
166
|
if (isExcludedSourcePath(sourcePath)) {
|
|
154
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
// Check
|
|
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
|
|
164
|
-
return
|
|
163
|
+
// Return per-cluster state path as default
|
|
164
|
+
return legacyState;
|
|
165
165
|
}
|
|
166
166
|
function findResultFile(resultsDir, childId) {
|
|
167
167
|
try {
|
package/dist/loop/continue.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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. -->
|