@lsctech/polaris 0.3.29 → 0.4.1

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 (40) hide show
  1. package/dist/agent-plugin/args.js +77 -0
  2. package/dist/agent-plugin/claude-generator.js +158 -0
  3. package/dist/agent-plugin/commands.js +73 -0
  4. package/dist/agent-plugin/help.js +69 -0
  5. package/dist/agent-plugin/sync.js +108 -0
  6. package/dist/autoresearch/dev-gate.js +51 -0
  7. package/dist/autoresearch/gates.js +310 -0
  8. package/dist/autoresearch/index.js +20 -0
  9. package/dist/autoresearch/proposal.js +121 -0
  10. package/dist/autoresearch/routing.js +180 -0
  11. package/dist/autoresearch/score.js +213 -0
  12. package/dist/cli/adopt-approve.js +124 -4
  13. package/dist/cli/adopt-assets.js +9 -1
  14. package/dist/cli/adopt-command.js +123 -11
  15. package/dist/cli/adopt-instructions.js +17 -2
  16. package/dist/cli/adopt-smartdocs.js +9 -0
  17. package/dist/cli/adoption-context.js +46 -0
  18. package/dist/cli/adoption-plan.js +105 -12
  19. package/dist/cli/agent-setup.js +47 -0
  20. package/dist/cli/autoresearch.js +96 -0
  21. package/dist/cli/index.js +2 -0
  22. package/dist/cli/init.js +69 -1
  23. package/dist/cli/setup-interview/generate.js +380 -0
  24. package/dist/cli/setup-interview/report.js +138 -0
  25. package/dist/cli/setup-interview/runner.js +105 -0
  26. package/dist/cli/setup-interview/schema.js +16 -0
  27. package/dist/cli/setup-interview/store.js +73 -0
  28. package/dist/cli/worker.js +18 -8
  29. package/dist/finalize/index.js +5 -4
  30. package/dist/loop/adapters/foreman-dispatch.js +65 -0
  31. package/dist/loop/adapters/index.js +3 -1
  32. package/dist/loop/bootstrap-packet.js +1 -1
  33. package/dist/loop/checkpoint.js +120 -1
  34. package/dist/loop/continue.js +13 -5
  35. package/dist/loop/parent.js +129 -1
  36. package/dist/loop/resume.js +14 -8
  37. package/dist/loop/worker-packet.js +1 -1
  38. package/dist/skill-packet/generator.js +149 -1
  39. package/dist/workspace/.polaris/skills/polaris-tools/tools.js +31 -22
  40. package/package.json +2 -2
@@ -0,0 +1,213 @@
1
+ "use strict";
2
+ /**
3
+ * Autoresearch scoring engine.
4
+ *
5
+ * Reads run artifacts, evaluates binary gates, and produces a structured
6
+ * diagnosis report:
7
+ *
8
+ * {
9
+ * run_id, cluster_id, evaluated_at,
10
+ * gate_results: GateResult[],
11
+ * failed_gates: string[],
12
+ * score: number (0.0–1.0),
13
+ * diagnosis_hints: { gate, fix_zone, hint }[]
14
+ * }
15
+ *
16
+ * Score = passed_gates / evaluable_gates (skipped gates are not counted).
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.loadRunArtifacts = loadRunArtifacts;
20
+ exports.computeScore = computeScore;
21
+ exports.buildDiagnosisHints = buildDiagnosisHints;
22
+ exports.scoreRun = scoreRun;
23
+ const node_fs_1 = require("node:fs");
24
+ const node_path_1 = require("node:path");
25
+ const gates_js_1 = require("./gates.js");
26
+ // ──────────────────────────────────────────────
27
+ // Diagnosis hints table
28
+ // ──────────────────────────────────────────────
29
+ const HINTS = {
30
+ "user-intervened": {
31
+ fix_zone: "worker-prompt / packet-scope",
32
+ hint: "User pushed commits after polaris-finalize. Review the worker packet scope and instructions for ambiguity.",
33
+ },
34
+ "foreman-resent-packet": {
35
+ fix_zone: "foreman-dispatch / dispatch-boundary",
36
+ hint: "Foreman dispatched the same child more than once. Check for state-machine bugs in dispatch-boundary.ts or loop continue logic.",
37
+ },
38
+ "foreman-fixed-worker-output": {
39
+ fix_zone: "worker-prompt / scope-contract",
40
+ hint: "Foreman made corrective commits after child-complete. Tighten the worker packet's allowed_scope and acceptance criteria.",
41
+ },
42
+ "worker-output-required-fixing": {
43
+ fix_zone: "worker-prompt / validation-commands",
44
+ hint: "Post-finalize commits indicate worker output was insufficient. Add stricter validation commands to the worker packet.",
45
+ },
46
+ "validation-failed": {
47
+ fix_zone: "worker-validation / build-setup",
48
+ hint: "One or more children failed validation. Check the validation_commands in the worker packet and the worker's implementation.",
49
+ },
50
+ "worker-went-out-of-scope": {
51
+ fix_zone: "worker-packet / scope-contract",
52
+ hint: "Worker emitted out-of-scope blocks or returned blocked status. Narrow allowed_scope in the worker packet.",
53
+ },
54
+ "foreman-token-burn-over-budget": {
55
+ fix_zone: "bootstrap-context / prompt-size",
56
+ hint: "Foreman bootstrap context exceeds token budget. Reduce current-state.json or worker packet size before dispatch.",
57
+ },
58
+ "state-repair-required": {
59
+ fix_zone: "medic / cluster-state",
60
+ hint: "Medic artifacts detected — state repair was required during this run. Review the medic chart and root cause.",
61
+ },
62
+ };
63
+ // ──────────────────────────────────────────────
64
+ // Artifact loader
65
+ // ──────────────────────────────────────────────
66
+ function findRunDir(repoRoot, runId) {
67
+ // Standard taskchain location
68
+ const taskchainPath = (0, node_path_1.join)(repoRoot, ".taskchain_artifacts", "polaris-run", "runs", runId);
69
+ if ((0, node_fs_1.existsSync)(taskchainPath))
70
+ return taskchainPath;
71
+ // Legacy .polaris/runs location
72
+ const polarisPath = (0, node_path_1.join)(repoRoot, ".polaris", "runs", runId);
73
+ if ((0, node_fs_1.existsSync)(polarisPath))
74
+ return polarisPath;
75
+ return null;
76
+ }
77
+ function findClusterDir(repoRoot, clusterId) {
78
+ const path = (0, node_path_1.join)(repoRoot, ".polaris", "clusters", clusterId);
79
+ return (0, node_fs_1.existsSync)(path) ? path : null;
80
+ }
81
+ function readJson(filePath) {
82
+ try {
83
+ return JSON.parse((0, node_fs_1.readFileSync)(filePath, "utf-8"));
84
+ }
85
+ catch {
86
+ return undefined;
87
+ }
88
+ }
89
+ function safeReaddir(dir) {
90
+ try {
91
+ return (0, node_fs_1.readdirSync)(dir);
92
+ }
93
+ catch {
94
+ return [];
95
+ }
96
+ }
97
+ function readResultPackets(clusterDir) {
98
+ if (!clusterDir)
99
+ return [];
100
+ const resultsDir = (0, node_path_1.join)(clusterDir, "results");
101
+ if (!(0, node_fs_1.existsSync)(resultsDir))
102
+ return [];
103
+ return safeReaddir(resultsDir)
104
+ .filter((f) => f.endsWith(".json"))
105
+ .map((f) => readJson((0, node_path_1.join)(resultsDir, f)))
106
+ .filter((v) => v !== undefined);
107
+ }
108
+ function extractWorkerResultContracts(resultPackets) {
109
+ const contracts = [];
110
+ for (const packet of resultPackets) {
111
+ const rec = packet;
112
+ // WorkerResultContract has packet_hash, worker_id, role as distinguishing fields
113
+ if (rec && typeof rec["packet_hash"] === "string" && typeof rec["worker_id"] === "string") {
114
+ contracts.push(rec);
115
+ }
116
+ }
117
+ return contracts;
118
+ }
119
+ function loadRunArtifacts(repoRoot, runId) {
120
+ const runDir = findRunDir(repoRoot, runId);
121
+ // Derive cluster from current-state.json in the run dir, or fallback to the default
122
+ let clusterId = null;
123
+ let currentState = null;
124
+ // Try run dir's current-state.json first
125
+ if (runDir) {
126
+ const runStatePath = (0, node_path_1.join)(runDir, "current-state.json");
127
+ if ((0, node_fs_1.existsSync)(runStatePath)) {
128
+ currentState = readJson(runStatePath);
129
+ }
130
+ }
131
+ // Fall back to the active current-state.json
132
+ if (!currentState) {
133
+ const taskchainState = (0, node_path_1.join)(repoRoot, ".taskchain_artifacts", "polaris-run", "current-state.json");
134
+ const polarisState = (0, node_path_1.join)(repoRoot, ".polaris", "runs", "current-state.json");
135
+ const statePath = (0, node_fs_1.existsSync)(taskchainState) ? taskchainState : polarisState;
136
+ if ((0, node_fs_1.existsSync)(statePath)) {
137
+ currentState = readJson(statePath);
138
+ }
139
+ }
140
+ if (currentState && typeof currentState === "object" && currentState !== null) {
141
+ const rec = currentState;
142
+ if (typeof rec["cluster_id"] === "string")
143
+ clusterId = rec["cluster_id"];
144
+ if (typeof rec["run_id"] === "string" && rec["run_id"] !== runId) {
145
+ // State belongs to a different run — don't use it for cluster_id derivation
146
+ clusterId = null;
147
+ }
148
+ }
149
+ const clusterDir = clusterId ? findClusterDir(repoRoot, clusterId) : null;
150
+ // Ledger
151
+ const ledgerPath = (0, node_path_1.join)(repoRoot, ".polaris", "runs", "ledger.jsonl");
152
+ const ledgerEvents = (0, gates_js_1.readJsonLines)(ledgerPath).filter((e) => {
153
+ const rec = e;
154
+ return rec?.["run_id"] === runId;
155
+ });
156
+ // Telemetry — prefer the run dir; some older runs have it in .polaris/runs/<runId>/
157
+ const telemetryPath = runDir ? (0, node_path_1.join)(runDir, "telemetry.jsonl") : null;
158
+ const telemetryEvents = telemetryPath ? (0, gates_js_1.readJsonLines)(telemetryPath) : [];
159
+ // Result packets from cluster results dir
160
+ const resultPackets = readResultPackets(clusterDir);
161
+ // Also check for WorkerResultContracts inside result packets
162
+ const workerResultContracts = extractWorkerResultContracts(resultPackets);
163
+ return {
164
+ runId,
165
+ runDir,
166
+ clusterDir,
167
+ currentState,
168
+ ledgerEvents,
169
+ resultPackets,
170
+ workerResultContracts,
171
+ telemetryEvents,
172
+ };
173
+ }
174
+ // ──────────────────────────────────────────────
175
+ // Scoring
176
+ // ──────────────────────────────────────────────
177
+ function computeScore(gateResults) {
178
+ const evaluable = gateResults.filter((g) => g.outcome !== "skipped");
179
+ if (evaluable.length === 0)
180
+ return 1.0; // nothing to evaluate — no known problems
181
+ const passed = evaluable.filter((g) => g.outcome === "passed").length;
182
+ return passed / evaluable.length;
183
+ }
184
+ function buildDiagnosisHints(failedGateNames) {
185
+ return failedGateNames.map((gate) => {
186
+ const h = HINTS[gate] ?? { fix_zone: "unknown", hint: `Gate ${gate} failed — no hint available.` };
187
+ return { gate, ...h };
188
+ });
189
+ }
190
+ // ──────────────────────────────────────────────
191
+ // Main entry point
192
+ // ──────────────────────────────────────────────
193
+ function scoreRun(repoRoot, runId) {
194
+ const artifacts = loadRunArtifacts(repoRoot, runId);
195
+ const gateResults = gates_js_1.ALL_GATES.map((evaluator) => evaluator(artifacts));
196
+ const failedGates = gateResults.filter((g) => g.outcome === "failed").map((g) => g.gate);
197
+ const score = computeScore(gateResults);
198
+ const diagnosisHints = buildDiagnosisHints(failedGates);
199
+ const clusterId = artifacts.currentState &&
200
+ typeof artifacts.currentState === "object" &&
201
+ !Array.isArray(artifacts.currentState)
202
+ ? artifacts.currentState["cluster_id"] ?? null
203
+ : null;
204
+ return {
205
+ run_id: runId,
206
+ cluster_id: clusterId,
207
+ evaluated_at: new Date().toISOString(),
208
+ gate_results: gateResults,
209
+ failed_gates: failedGates,
210
+ score,
211
+ diagnosis_hints: diagnosisHints,
212
+ };
213
+ }
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.persistApprovedAdoptionPlan = persistApprovedAdoptionPlan;
4
4
  exports.logAdoptionApprovalTelemetry = logAdoptionApprovalTelemetry;
5
5
  exports.promptApproval = promptApproval;
6
+ exports.promptCategoryApproval = promptCategoryApproval;
7
+ exports.requireApprovalGates = requireApprovalGates;
6
8
  const node_fs_1 = require("node:fs");
7
9
  const node_path_1 = require("node:path");
8
10
  const promises_1 = require("node:readline/promises");
@@ -36,9 +38,10 @@ async function promptApproval(plan, options = {}) {
36
38
  const stdout = options.stdout ?? process.stdout;
37
39
  const stdin = options.stdin ?? process.stdin;
38
40
  const markdownPath = adoptionPlanMarkdownPath(repoRoot);
39
- const markdown = (0, node_fs_1.existsSync)(markdownPath)
40
- ? (0, node_fs_1.readFileSync)(markdownPath, "utf-8")
41
- : (0, adoption_plan_js_1.renderAdoptionPlanMarkdown)(plan);
41
+ const markdown = options.markdown ??
42
+ ((0, node_fs_1.existsSync)(markdownPath)
43
+ ? (0, node_fs_1.readFileSync)(markdownPath, "utf-8")
44
+ : (0, adoption_plan_js_1.renderAdoptionPlanMarkdown)(plan));
42
45
  stdout.write(`${markdown.replace(/\s*$/, "")}\n\n`);
43
46
  const rl = (0, promises_1.createInterface)({
44
47
  input: stdin,
@@ -52,9 +55,126 @@ async function promptApproval(plan, options = {}) {
52
55
  rl.close();
53
56
  }
54
57
  if (response.trim().toLowerCase() === "y") {
55
- persistApprovedAdoptionPlan(plan, repoRoot, options.now ?? new Date());
58
+ const approvalNow = options.now ?? new Date();
59
+ if (options.persist) {
60
+ options.persist(plan, repoRoot, approvalNow);
61
+ }
62
+ else {
63
+ persistApprovedAdoptionPlan(plan, repoRoot, approvalNow);
64
+ }
56
65
  return true;
57
66
  }
58
67
  stdout.write("Adoption aborted: explicit approval required.\n");
59
68
  return false;
60
69
  }
70
+ /** Map of category → step categories in AdoptionStep */
71
+ const CATEGORY_STEP_MAP = {
72
+ "doc-movement": ["smartdocs-migrate"],
73
+ "instruction-file": ["instruction-refactor"],
74
+ "graph-root": ["cognition-generate", "atlas-generate"],
75
+ "route-scaffold": ["scaffold"],
76
+ };
77
+ const CATEGORY_LABELS = {
78
+ "doc-movement": "Document Movement",
79
+ "instruction-file": "Instruction-File Changes",
80
+ "graph-root": "Graph-Root / Cognition Changes",
81
+ "route-scaffold": "Route Scaffolding",
82
+ };
83
+ /**
84
+ * Render a compact diff preview for the given steps.
85
+ */
86
+ function renderStepDiff(steps) {
87
+ if (steps.length === 0)
88
+ return " (no steps)\n";
89
+ return steps
90
+ .map((s) => {
91
+ const src = s.source_path ? ` ${s.source_path} →` : "";
92
+ const dst = s.dest_path ? ` ${s.dest_path}` : "";
93
+ const routing = s.routing ? ` [${s.routing}]` : "";
94
+ return ` ${s.action.toUpperCase()}${src}${dst}${routing} — ${s.description}`;
95
+ })
96
+ .join("\n") + "\n";
97
+ }
98
+ /**
99
+ * Ask the operator for approval of a single mutation category.
100
+ * Returns true if approved, false if declined.
101
+ * Logs the decision via adoption telemetry.
102
+ */
103
+ async function promptCategoryApproval(category, steps, options = {}) {
104
+ const repoRoot = options.repoRoot ?? process.cwd();
105
+ const stdout = options.stdout ?? process.stdout;
106
+ const stdin = options.stdin ?? process.stdin;
107
+ const label = CATEGORY_LABELS[category];
108
+ const actionableSteps = steps.filter((s) => s.action !== "skip");
109
+ if (actionableSteps.length === 0)
110
+ return true;
111
+ stdout.write(`\n--- ${label} (${actionableSteps.length} step(s)) ---\n`);
112
+ stdout.write(renderStepDiff(actionableSteps));
113
+ const rl = (0, promises_1.createInterface)({ input: stdin, output: stdout });
114
+ let response = "";
115
+ try {
116
+ response = await rl.question(`Approve ${label}? [y/N] `);
117
+ }
118
+ finally {
119
+ rl.close();
120
+ }
121
+ const approved = response.trim().toLowerCase() === "y";
122
+ const now = (options.now ?? new Date()).toISOString();
123
+ logAdoptionApprovalTelemetry(repoRoot, {
124
+ event: "category-approval",
125
+ category,
126
+ approved,
127
+ step_count: actionableSteps.length,
128
+ timestamp: now,
129
+ });
130
+ if (!approved) {
131
+ stdout.write(`Adoption aborted: ${label} approval required.\n`);
132
+ }
133
+ return approved;
134
+ }
135
+ /**
136
+ * Fire approval gates for all four mutation categories in sequence.
137
+ * Returns false (and stops) at the first declined category.
138
+ *
139
+ * For non-interactive runs (no tty + no supplied stdin), returns false immediately
140
+ * unless `options.stdin` is explicitly set (which signals the caller supplied a context stream).
141
+ */
142
+ async function requireApprovalGates(plan, options = {}) {
143
+ const stdin = options.stdin ?? process.stdin;
144
+ // Non-interactive guard: if stdin is not a TTY and the caller hasn't explicitly supplied one,
145
+ // block before mutation (acceptance criterion 2).
146
+ if (!options.nonInteractiveSafe && stdin === process.stdin && !process.stdin.isTTY) {
147
+ const stdout = options.stdout ?? process.stdout;
148
+ stdout.write("Adoption aborted: non-interactive run requires a supplied context file (--context) or explicit approval.\n");
149
+ logAdoptionApprovalTelemetry(options.repoRoot ?? process.cwd(), {
150
+ event: "category-approval-blocked",
151
+ reason: "non-interactive",
152
+ timestamp: new Date().toISOString(),
153
+ });
154
+ return false;
155
+ }
156
+ const categories = [
157
+ "route-scaffold",
158
+ "doc-movement",
159
+ "instruction-file",
160
+ "graph-root",
161
+ ];
162
+ for (const category of categories) {
163
+ const stepCategories = CATEGORY_STEP_MAP[category];
164
+ const steps = plan.steps.filter((s) => {
165
+ if (!stepCategories.includes(s.category))
166
+ return false;
167
+ // Doc-movement gate only fires for steps that migrateSmartDocs will execute
168
+ if (category === "doc-movement" && s.routing !== "candidate")
169
+ return false;
170
+ return true;
171
+ });
172
+ // Skip gate if no actionable steps in this category
173
+ if (steps.filter((s) => s.action !== "skip").length === 0)
174
+ continue;
175
+ const approved = await promptCategoryApproval(category, steps, options);
176
+ if (!approved)
177
+ return false;
178
+ }
179
+ return true;
180
+ }
@@ -6,6 +6,7 @@ exports.runGraphBuild = runGraphBuild;
6
6
  const node_fs_1 = require("node:fs");
7
7
  const node_child_process_1 = require("node:child_process");
8
8
  const node_path_1 = require("node:path");
9
+ const sync_js_1 = require("../agent-plugin/sync.js");
9
10
  function isAncestorSymlink(repoRoot, relPath) {
10
11
  const parts = relPath.split("/").filter(Boolean);
11
12
  for (let i = 0; i < parts.length - 1; i++) {
@@ -168,7 +169,14 @@ function installWorkspaceAssets(repoRoot, workspaceDir) {
168
169
  installed.push(rulesRel);
169
170
  }
170
171
  }
171
- return { installed, alreadyPresent, skipped, conflicted };
172
+ // 6. Agent-plugin shims (Claude Code commands)
173
+ const shimOutDir = (0, node_path_1.join)(repoRoot, ".claude", "commands");
174
+ if (isAncestorSymlink(repoRoot, ".claude/commands")) {
175
+ skipped.push(".claude/commands");
176
+ return { installed, alreadyPresent, skipped, conflicted };
177
+ }
178
+ const shimSync = (0, sync_js_1.syncShims)(shimOutDir);
179
+ return { installed, alreadyPresent, skipped, conflicted, shimSync };
172
180
  }
173
181
  function runGraphBuild(repoRoot) {
174
182
  try {
@@ -1,9 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runAdoptionInterview = runAdoptionInterview;
3
4
  exports.runAdoptPhase = runAdoptPhase;
4
5
  exports.runFullAdoption = runFullAdoption;
5
6
  exports.createAdoptCommand = createAdoptCommand;
6
7
  const node_path_1 = require("node:path");
8
+ const node_readline_1 = require("node:readline");
9
+ const node_fs_1 = require("node:fs");
7
10
  const commander_1 = require("commander");
8
11
  const adopt_scan_js_1 = require("./adopt-scan.js");
9
12
  const adoption_plan_js_1 = require("./adoption-plan.js");
@@ -14,6 +17,71 @@ const adopt_rules_js_1 = require("./adopt-rules.js");
14
17
  const adopt_cognition_js_1 = require("./adopt-cognition.js");
15
18
  const adopt_canon_js_1 = require("./adopt-canon.js");
16
19
  const adopt_genesis_js_1 = require("./adopt-genesis.js");
20
+ const generator_js_1 = require("../skill-packet/generator.js");
21
+ const foreman_dispatch_js_1 = require("../loop/adapters/foreman-dispatch.js");
22
+ const adoption_context_js_1 = require("./adoption-context.js");
23
+ const adopt_approve_js_1 = require("./adopt-approve.js");
24
+ /** Prompt a single question via stdin and return the trimmed answer. */
25
+ async function prompt(question) {
26
+ const rl = (0, node_readline_1.createInterface)({ input: process.stdin, output: process.stdout });
27
+ return new Promise((resolve) => {
28
+ rl.question(question, (answer) => {
29
+ rl.close();
30
+ resolve(answer.trim());
31
+ });
32
+ });
33
+ }
34
+ /**
35
+ * Run a Foreman-led interview seeded from inventory gaps.
36
+ * Skips questions already answered (resume support).
37
+ */
38
+ async function runAdoptionInterview(repoRoot, inventory) {
39
+ const existing = (0, adoption_context_js_1.loadOperatorContext)(repoRoot);
40
+ const ctx = existing ?? (0, adoption_context_js_1.createEmptyOperatorContext)();
41
+ // Seed: untrusted docs (smartdocs candidates not yet classified as trusted/stale)
42
+ const candidatePaths = inventory.smartdocs_candidates.map((c) => c.path);
43
+ const unclassified = candidatePaths.filter((p) => !ctx.trusted_docs.includes(p) && !ctx.stale_docs.includes(p));
44
+ if (unclassified.length > 0) {
45
+ process.stdout.write(`\nFound ${unclassified.length} doc candidate(s) not yet classified:\n` +
46
+ unclassified.map((p) => ` - ${p}`).join("\n") +
47
+ "\n");
48
+ const trusted = await prompt("Which of these are authoritative/trusted? (comma-separated paths, or leave blank to skip): ");
49
+ const stale = await prompt("Which are stale/outdated? (comma-separated paths, or leave blank to skip): ");
50
+ if (trusted)
51
+ ctx.trusted_docs.push(...trusted.split(",").map((s) => s.trim()).filter(Boolean));
52
+ if (stale)
53
+ ctx.stale_docs.push(...stale.split(",").map((s) => s.trim()).filter(Boolean));
54
+ }
55
+ // Seed: ambiguous source roots (more than one detected)
56
+ if (inventory.source_roots.length > 1 && ctx.priority_systems.length === 0) {
57
+ process.stdout.write(`\nMultiple source roots detected: ${inventory.source_roots.join(", ")}\n`);
58
+ const priority = await prompt("Which are the primary/priority systems? (comma-separated, or leave blank): ");
59
+ if (priority)
60
+ ctx.priority_systems.push(...priority.split(",").map((s) => s.trim()).filter(Boolean));
61
+ }
62
+ // Seed: instruction files not yet assigned intent
63
+ const unintended = inventory.agent_instruction_files
64
+ .map((f) => f.path)
65
+ .filter((p) => !(p in ctx.instruction_file_intent));
66
+ for (const filePath of unintended) {
67
+ const file = inventory.agent_instruction_files.find((f) => f.path === filePath);
68
+ const suggestion = file?.recommendation ?? "preserve";
69
+ const answer = await prompt(`Instruction file "${filePath}" — intent? [preserve/migrate/thin-adapter] (default: ${suggestion}): `);
70
+ const intent = ["preserve", "migrate", "thin-adapter"].find((v) => v === answer) ?? suggestion;
71
+ ctx.instruction_file_intent[filePath] = intent;
72
+ }
73
+ // Seed: canonical-folder candidates not yet classified as trusted or never-touch
74
+ const uncheckedFolders = inventory.likely_canonical_folders.filter((f) => !ctx.trusted_docs.includes(f) && !ctx.never_touch.includes(f));
75
+ if (uncheckedFolders.length > 0) {
76
+ process.stdout.write(`\nLikely canonical folders: ${uncheckedFolders.join(", ")}\n`);
77
+ const neverTouch = await prompt("Any folders that should never be touched by adoption? (comma-separated, or leave blank): ");
78
+ if (neverTouch)
79
+ ctx.never_touch.push(...neverTouch.split(",").map((s) => s.trim()).filter(Boolean));
80
+ }
81
+ ctx.answered_at = new Date().toISOString();
82
+ (0, adoption_context_js_1.saveOperatorContext)(repoRoot, ctx);
83
+ return ctx;
84
+ }
17
85
  function estimateTokenCost(inventory) {
18
86
  const total = inventory.smartdocs_candidates.length +
19
87
  inventory.likely_canonical_folders.length;
@@ -29,11 +97,17 @@ async function runAdoptPhase(phase, repoRoot, options = {}) {
29
97
  switch (phase) {
30
98
  case "scan": {
31
99
  const inventory = await (0, adopt_scan_js_1.scanRepo)(repoRoot, { rescan: true });
32
- const plan = (0, adoption_plan_js_1.generateAdoptionPlan)(inventory);
33
- (0, adoption_plan_js_1.generateAdoptionPlanArtifacts)(repoRoot, inventory);
100
+ const operatorContext = (0, adoption_context_js_1.loadOperatorContext)(repoRoot) ?? undefined;
101
+ (0, adoption_plan_js_1.generateAdoptionPlanArtifacts)(repoRoot, inventory, { operatorContext });
34
102
  console.log(`Scan complete: ${inventory.smartdocs_candidates.length} doc(s), ${inventory.likely_canonical_folders.length} folder(s).`);
35
103
  break;
36
104
  }
105
+ case "interview": {
106
+ const inventory = options.inventory ?? (await (0, adopt_scan_js_1.scanRepo)(repoRoot, { rescan: false }));
107
+ await runAdoptionInterview(repoRoot, inventory);
108
+ console.log("Interview complete. Operator context saved.");
109
+ break;
110
+ }
37
111
  case "agents": {
38
112
  if (options.inventory) {
39
113
  const cost = estimateTokenCost(options.inventory);
@@ -42,6 +116,24 @@ async function runAdoptPhase(phase, repoRoot, options = {}) {
42
116
  if (!options.skipAgents) {
43
117
  await (0, agent_setup_js_1.runAgentSetup)(repoRoot);
44
118
  }
119
+ // Dispatch the Foreman with the setup-bootstrap packet.
120
+ try {
121
+ const configPath = (0, node_path_1.join)(repoRoot, "polaris.config.json");
122
+ const config = JSON.parse((0, node_fs_1.readFileSync)(configPath, "utf-8"));
123
+ const execution = config.execution;
124
+ if (execution?.providers && Object.keys(execution.providers).length > 0) {
125
+ const binding = await (0, agent_setup_js_1.resolveForeman)(repoRoot, config);
126
+ const setupPacket = (0, generator_js_1.generateSetupBootstrapPacket)("adopt");
127
+ await (0, foreman_dispatch_js_1.dispatchForeman)({
128
+ packet: setupPacket,
129
+ provider: binding.provider,
130
+ executionConfig: execution,
131
+ });
132
+ }
133
+ }
134
+ catch {
135
+ // Foreman dispatch is best-effort — don't block adoption.
136
+ }
45
137
  break;
46
138
  }
47
139
  case "consolidate": {
@@ -91,25 +183,34 @@ async function runAdoptPhase(phase, repoRoot, options = {}) {
91
183
  }
92
184
  }
93
185
  async function runFullAdoption(repoRoot, options = {}) {
94
- console.log("[1/7] scan");
186
+ console.log("[1/8] scan");
95
187
  await runAdoptPhase("scan", repoRoot);
96
- // Re-read inventory and plan for later phases
188
+ // Re-read inventory for later phases
97
189
  const inventory = await (0, adopt_scan_js_1.scanRepo)(repoRoot, { rescan: false });
98
- const plan = (0, adoption_plan_js_1.generateAdoptionPlan)(inventory);
99
- console.log("[2/7] agents");
190
+ console.log("[2/8] interview");
191
+ await runAdoptPhase("interview", repoRoot, { inventory });
192
+ // Regenerate plan after interview to incorporate operator context
193
+ const operatorContext = (0, adoption_context_js_1.loadOperatorContext)(repoRoot) ?? undefined;
194
+ const plan = (0, adoption_plan_js_1.generateAdoptionPlan)(inventory, { operatorContext });
195
+ console.log("[3/8] agents");
100
196
  await runAdoptPhase("agents", repoRoot, {
101
197
  inventory,
102
198
  skipAgents: options.skipAgents,
103
199
  });
104
- console.log("[3/7] consolidate");
200
+ // Approval gates: one per mutation category before any broad mutation.
201
+ const gatesApproved = await (0, adopt_approve_js_1.requireApprovalGates)(plan, { repoRoot });
202
+ if (!gatesApproved) {
203
+ return;
204
+ }
205
+ console.log("[4/8] consolidate");
105
206
  await runAdoptPhase("consolidate", repoRoot, { inventory, plan });
106
- console.log("[4/7] map");
207
+ console.log("[5/8] map");
107
208
  await runAdoptPhase("map", repoRoot);
108
- console.log("[5/7] skills");
209
+ console.log("[6/8] skills");
109
210
  await runAdoptPhase("skills", repoRoot);
110
- console.log("[6/7] rules");
211
+ console.log("[7/8] rules");
111
212
  await runAdoptPhase("rules", repoRoot, { inventory });
112
- console.log("[7/7] canon");
213
+ console.log("[8/8] canon");
113
214
  await runAdoptPhase("canon", repoRoot, { inventory, plan });
114
215
  }
115
216
  function createAdoptCommand(opts) {
@@ -124,6 +225,17 @@ function createAdoptCommand(opts) {
124
225
  .action(async () => {
125
226
  await runAdoptPhase("scan", repoRoot);
126
227
  });
228
+ cmd
229
+ .command("interview")
230
+ .description("Run Foreman-led interview to capture operator context")
231
+ .action(async () => {
232
+ if (!process.stdin.isTTY) {
233
+ process.stderr.write("Error: the interview subcommand requires an interactive terminal.\n");
234
+ process.exit(1);
235
+ }
236
+ const inventory = await (0, adopt_scan_js_1.scanRepo)(repoRoot, { rescan: false });
237
+ await runAdoptPhase("interview", repoRoot, { inventory });
238
+ });
127
239
  cmd
128
240
  .command("agents")
129
241
  .description("Set up agent instruction files")
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.handleInstructionFiles = handleInstructionFiles;
4
4
  const node_fs_1 = require("node:fs");
5
5
  const node_path_1 = require("node:path");
6
+ const adopt_approve_js_1 = require("./adopt-approve.js");
6
7
  const DELEGATION_MARKERS = ["<!-- polaris:delegate", "POLARIS_RULES.md", "POLARIS.md", "Polaris runtime"];
7
8
  function isSupportedInstructionPath(path) {
8
9
  return (path === "CLAUDE.md" ||
@@ -100,9 +101,23 @@ function appendInstructionProvenance(repoRoot, records) {
100
101
  };
101
102
  (0, node_fs_1.writeFileSync)(provenancePath, `${JSON.stringify(updated, null, 2)}\n`, "utf-8");
102
103
  }
103
- function handleInstructionFiles(plan, inventory, repoRoot) {
104
+ async function handleInstructionFiles(plan, inventory, repoRoot, options = {}) {
104
105
  if (plan.dry_run) {
105
- return Promise.resolve([]);
106
+ return [];
107
+ }
108
+ // Approval gate: show instruction-refactor steps and ask before mutating,
109
+ // unless the overall plan is already approved or the caller opts out.
110
+ if (!options.skipApprovalGate && !plan.approved) {
111
+ const instructionSteps = plan.steps.filter((s) => s.category === "instruction-refactor");
112
+ if (instructionSteps.filter((s) => s.action !== "skip").length > 0) {
113
+ const approved = await (0, adopt_approve_js_1.promptCategoryApproval)("instruction-file", instructionSteps, {
114
+ repoRoot,
115
+ stdin: options.stdin,
116
+ stdout: options.stdout,
117
+ });
118
+ if (!approved)
119
+ return [];
120
+ }
106
121
  }
107
122
  const doctrineExists = hasDoctrine(repoRoot);
108
123
  const now = new Date().toISOString();
@@ -120,6 +120,15 @@ function migrateSmartDocs(plan, repoRoot = (0, node_path_1.resolve)(process.cwd(
120
120
  if (step.category !== "smartdocs-migrate" || COMPLETE_STATUSES.has(step.status)) {
121
121
  continue;
122
122
  }
123
+ if (step.routing !== undefined && step.routing !== "candidate") {
124
+ effectivePlan.steps[index] = {
125
+ ...step,
126
+ status: "skipped",
127
+ completed_at: now,
128
+ error: `routing not candidate: ${step.routing}`,
129
+ };
130
+ continue;
131
+ }
123
132
  const sourcePath = step.source_path ?? "";
124
133
  const destPath = step.dest_path ?? `smartdocs/raw/${(0, node_path_1.basename)(sourcePath || `step-${step.order}.md`)}`;
125
134
  if (!sourcePath) {