@lsctech/polaris 0.3.28 → 0.4.0

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.
@@ -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) {
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.loadOperatorContext = loadOperatorContext;
4
+ exports.saveOperatorContext = saveOperatorContext;
5
+ exports.createEmptyOperatorContext = createEmptyOperatorContext;
6
+ const node_fs_1 = require("node:fs");
7
+ const node_path_1 = require("node:path");
8
+ const ADOPTION_DIR = (0, node_path_1.join)(".polaris", "adoption");
9
+ const CONTEXT_FILE = "operator-context.json";
10
+ function contextPath(repoRoot) {
11
+ return (0, node_path_1.join)(repoRoot, ADOPTION_DIR, CONTEXT_FILE);
12
+ }
13
+ function loadOperatorContext(repoRoot) {
14
+ const path = contextPath(repoRoot);
15
+ if (!(0, node_fs_1.existsSync)(path))
16
+ return null;
17
+ try {
18
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)(path, "utf-8"));
19
+ if (!Array.isArray(parsed?.trusted_docs) ||
20
+ !Array.isArray(parsed?.never_touch) ||
21
+ typeof parsed?.instruction_file_intent !== "object" ||
22
+ parsed?.instruction_file_intent === null ||
23
+ Array.isArray(parsed?.instruction_file_intent)) {
24
+ return null;
25
+ }
26
+ return parsed;
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ function saveOperatorContext(repoRoot, context) {
33
+ (0, node_fs_1.mkdirSync)((0, node_path_1.join)(repoRoot, ADOPTION_DIR), { recursive: true });
34
+ (0, node_fs_1.writeFileSync)(contextPath(repoRoot), `${JSON.stringify(context, null, 2)}\n`, "utf-8");
35
+ }
36
+ function createEmptyOperatorContext(now) {
37
+ return {
38
+ schema_version: "1.0",
39
+ answered_at: (now ?? new Date()).toISOString(),
40
+ trusted_docs: [],
41
+ stale_docs: [],
42
+ never_touch: [],
43
+ priority_systems: [],
44
+ instruction_file_intent: {},
45
+ };
46
+ }
@@ -11,14 +11,49 @@ function formatPlanId(isoTimestamp) {
11
11
  function normalizePath(path) {
12
12
  return path.endsWith("/") ? path.slice(0, -1) : path;
13
13
  }
14
+ function isTrusted(path, operatorContext) {
15
+ const normalized = normalizePath(path);
16
+ return operatorContext?.trusted_docs.some((p) => normalizePath(p) === normalized) ?? false;
17
+ }
18
+ function isStale(path, operatorContext) {
19
+ const normalized = normalizePath(path);
20
+ return operatorContext?.stale_docs.some((p) => normalizePath(p) === normalized) ?? false;
21
+ }
22
+ function isNeverTouch(path, operatorContext) {
23
+ const normalized = normalizePath(path);
24
+ return operatorContext?.never_touch.some((p) => normalizePath(p) === normalized) ?? false;
25
+ }
26
+ function instructionIntent(path, operatorContext) {
27
+ return operatorContext?.instruction_file_intent[path];
28
+ }
29
+ function deriveSmartDocsRouting(candidate, operatorContext) {
30
+ const evidence_refs = [`scan:smartdocs_candidate:${candidate.path}`];
31
+ const operator_refs = [];
32
+ if (isNeverTouch(candidate.path, operatorContext) || isStale(candidate.path, operatorContext)) {
33
+ if (isNeverTouch(candidate.path, operatorContext))
34
+ operator_refs.push(`operator:never_touch:${candidate.path}`);
35
+ if (isStale(candidate.path, operatorContext))
36
+ operator_refs.push(`operator:stale_docs:${candidate.path}`);
37
+ return { routing: "hold", evidence_refs, operator_refs };
38
+ }
39
+ if (isTrusted(candidate.path, operatorContext)) {
40
+ operator_refs.push(`operator:trusted_docs:${candidate.path}`);
41
+ return { routing: "candidate", evidence_refs, operator_refs };
42
+ }
43
+ return { routing: "review-required", evidence_refs, operator_refs };
44
+ }
14
45
  function createStep(order, partial) {
46
+ const { evidence_refs, operator_refs, routing, ...rest } = partial;
15
47
  return {
16
- ...partial,
48
+ evidence_refs: evidence_refs ?? [],
49
+ operator_refs: operator_refs ?? [],
50
+ routing: routing ?? "candidate",
51
+ ...rest,
17
52
  order,
18
53
  status: "pending",
19
54
  };
20
55
  }
21
- function buildSteps(inventory) {
56
+ function buildSteps(inventory, operatorContext) {
22
57
  const steps = [];
23
58
  let order = 1;
24
59
  steps.push(createStep(order++, {
@@ -31,6 +66,9 @@ function buildSteps(inventory) {
31
66
  destructive: true,
32
67
  requires_approval: false,
33
68
  estimated_risk: "low",
69
+ routing: "candidate",
70
+ evidence_refs: ["adoption:step:provider-config-lock"],
71
+ operator_refs: [],
34
72
  }));
35
73
  steps.push(createStep(order++, {
36
74
  step_id: "adoption-scaffold",
@@ -42,6 +80,9 @@ function buildSteps(inventory) {
42
80
  destructive: false,
43
81
  requires_approval: false,
44
82
  estimated_risk: "low",
83
+ routing: "candidate",
84
+ evidence_refs: ["adoption:step:adoption-scaffold"],
85
+ operator_refs: [],
45
86
  }));
46
87
  steps.push(createStep(order++, {
47
88
  step_id: "workspace-root-surfaces",
@@ -53,10 +94,15 @@ function buildSteps(inventory) {
53
94
  destructive: false,
54
95
  requires_approval: false,
55
96
  estimated_risk: "low",
97
+ routing: "candidate",
98
+ evidence_refs: ["adoption:step:workspace-root-surfaces"],
99
+ operator_refs: [],
56
100
  }));
57
101
  for (const candidate of inventory.smartdocs_candidates) {
58
- steps.push(createStep(order++, {
59
- step_id: `smartdocs-migrate-${order.toString().padStart(3, "0")}`,
102
+ const { routing, evidence_refs, operator_refs } = deriveSmartDocsRouting(candidate, operatorContext);
103
+ const stepOrder = order++;
104
+ steps.push(createStep(stepOrder, {
105
+ step_id: `smartdocs-migrate-${stepOrder.toString().padStart(3, "0")}`,
60
106
  phase: "C",
61
107
  category: "smartdocs-migrate",
62
108
  action: "move",
@@ -66,12 +112,27 @@ function buildSteps(inventory) {
66
112
  destructive: true,
67
113
  requires_approval: true,
68
114
  estimated_risk: candidate.estimated_risk,
115
+ routing,
116
+ evidence_refs,
117
+ operator_refs,
69
118
  }));
70
119
  }
71
120
  for (const folder of inventory.likely_canonical_folders) {
72
121
  const normalizedFolder = normalizePath(folder);
73
- steps.push(createStep(order++, {
74
- step_id: `cognition-generate-${order.toString().padStart(3, "0")}`,
122
+ const evidence_refs = [`scan:canonical_folder:${normalizedFolder}`];
123
+ const operator_refs = [];
124
+ let routing = "raw";
125
+ if (isNeverTouch(normalizedFolder, operatorContext)) {
126
+ routing = "hold";
127
+ operator_refs.push(`operator:never_touch:${normalizedFolder}`);
128
+ }
129
+ else if (isTrusted(normalizedFolder, operatorContext)) {
130
+ routing = "candidate";
131
+ operator_refs.push(`operator:trusted_docs:${normalizedFolder}`);
132
+ }
133
+ const stepOrder = order++;
134
+ steps.push(createStep(stepOrder, {
135
+ step_id: `cognition-generate-${stepOrder.toString().padStart(3, "0")}`,
75
136
  phase: "C",
76
137
  category: "cognition-generate",
77
138
  action: "create",
@@ -80,6 +141,9 @@ function buildSteps(inventory) {
80
141
  destructive: false,
81
142
  requires_approval: true,
82
143
  estimated_risk: "medium",
144
+ routing,
145
+ evidence_refs,
146
+ operator_refs,
83
147
  }));
84
148
  }
85
149
  for (const file of inventory.agent_instruction_files) {
@@ -90,8 +154,24 @@ function buildSteps(inventory) {
90
154
  : file.recommendation === "thin-adapter"
91
155
  ? "medium"
92
156
  : "low";
93
- steps.push(createStep(order++, {
94
- step_id: `instruction-refactor-${order.toString().padStart(3, "0")}`,
157
+ const evidence_refs = [`scan:agent_instruction_file:${file.path}`];
158
+ const operator_refs = [];
159
+ const intent = instructionIntent(file.path, operatorContext);
160
+ let routing;
161
+ if (intent === "preserve") {
162
+ routing = "hold";
163
+ operator_refs.push(`operator:instruction_file_intent:${file.path}:preserve`);
164
+ }
165
+ else if (intent === "migrate" || intent === "thin-adapter") {
166
+ routing = "candidate";
167
+ operator_refs.push(`operator:instruction_file_intent:${file.path}:${intent}`);
168
+ }
169
+ else {
170
+ routing = "review-required";
171
+ }
172
+ const stepOrder = order++;
173
+ steps.push(createStep(stepOrder, {
174
+ step_id: `instruction-refactor-${stepOrder.toString().padStart(3, "0")}`,
95
175
  phase: "C",
96
176
  category: "instruction-refactor",
97
177
  action,
@@ -105,11 +185,15 @@ function buildSteps(inventory) {
105
185
  destructive,
106
186
  requires_approval: destructive,
107
187
  estimated_risk: risk,
188
+ routing,
189
+ evidence_refs,
190
+ operator_refs,
108
191
  }));
109
192
  }
110
193
  for (const ignorePath of inventory.ignore_candidates) {
111
- steps.push(createStep(order++, {
112
- step_id: `ignore-rules-${order.toString().padStart(3, "0")}`,
194
+ const stepOrder = order++;
195
+ steps.push(createStep(stepOrder, {
196
+ step_id: `ignore-rules-${stepOrder.toString().padStart(3, "0")}`,
113
197
  phase: "C",
114
198
  category: "ignore-rules",
115
199
  action: "append",
@@ -118,6 +202,9 @@ function buildSteps(inventory) {
118
202
  destructive: true,
119
203
  requires_approval: true,
120
204
  estimated_risk: "low",
205
+ routing: "candidate",
206
+ evidence_refs: [`scan:ignore_candidate:${ignorePath}`],
207
+ operator_refs: [],
121
208
  }));
122
209
  }
123
210
  steps.push(createStep(order++, {
@@ -130,6 +217,9 @@ function buildSteps(inventory) {
130
217
  destructive: true,
131
218
  requires_approval: true,
132
219
  estimated_risk: "medium",
220
+ routing: "candidate",
221
+ evidence_refs: ["adoption:step:atlas-generate"],
222
+ operator_refs: [],
133
223
  }));
134
224
  steps.push(createStep(order++, {
135
225
  step_id: "stage-adoption",
@@ -141,6 +231,9 @@ function buildSteps(inventory) {
141
231
  destructive: true,
142
232
  requires_approval: true,
143
233
  estimated_risk: "medium",
234
+ routing: "candidate",
235
+ evidence_refs: ["adoption:step:stage-adoption"],
236
+ operator_refs: [],
144
237
  }));
145
238
  return steps;
146
239
  }
@@ -160,7 +253,7 @@ function buildImpactSummary(steps, inventory) {
160
253
  }
161
254
  function generateAdoptionPlan(inventory, options = {}) {
162
255
  const generatedAt = (options.now ?? new Date()).toISOString();
163
- const steps = buildSteps(inventory);
256
+ const steps = buildSteps(inventory, options.operatorContext);
164
257
  return {
165
258
  plan_id: formatPlanId(generatedAt),
166
259
  generated_at: generatedAt,
@@ -206,7 +299,7 @@ function renderAdoptionPlanMarkdown(plan) {
206
299
  for (const step of phaseSteps) {
207
300
  const source = step.source_path ? ` source: \`${step.source_path}\`` : "";
208
301
  const dest = step.dest_path ? ` destination: \`${step.dest_path}\`` : "";
209
- lines.push(`- **${step.order}. ${step.step_id}** — ${step.category} / ${step.action} / approval ${step.requires_approval ? "required" : "not required"} / risk ${step.estimated_risk} / status ${step.status}${source}${dest}`);
302
+ lines.push(`- **${step.order}. ${step.step_id}** — ${step.category} / ${step.action} / approval ${step.requires_approval ? "required" : "not required"} / risk ${step.estimated_risk} / routing ${step.routing} / status ${step.status}${source}${dest}`);
210
303
  lines.push(` - ${step.description}`);
211
304
  }
212
305
  lines.push("");
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.resolveForeman = resolveForeman;
36
37
  exports.runAgentSetup = runAgentSetup;
37
38
  const node_child_process_1 = require("node:child_process");
38
39
  const readline = __importStar(require("node:readline"));
@@ -86,6 +87,52 @@ async function selectProvider(role, detected, currentProviders) {
86
87
  });
87
88
  });
88
89
  }
90
+ /**
91
+ * Resolves the Foreman provider for init/adopt.
92
+ *
93
+ * If `execution.providerPolicy.foreman.providers[0]` is already set in config,
94
+ * returns a binding immediately (no prompt). Otherwise prompts once, persists
95
+ * the choice to polaris.config.json, and returns the binding.
96
+ *
97
+ * The binding always references `.polaris/roles/foreman.md` — role text is not
98
+ * duplicated here.
99
+ */
100
+ async function resolveForeman(repoRoot, config, opts) {
101
+ const execution = config.execution ?? {};
102
+ const providerPolicy = execution.providerPolicy ?? {};
103
+ const foremanPolicy = providerPolicy.foreman ?? {};
104
+ const existing = foremanPolicy.providers?.[0];
105
+ const roleFile = ".polaris/roles/foreman.md";
106
+ if (existing) {
107
+ return { provider: existing, roleFile };
108
+ }
109
+ // Prompt once for Foreman provider
110
+ const detected = opts?.detectProviders?.() ?? detectProviders();
111
+ const chosen = opts?.selectProvider
112
+ ? await opts.selectProvider(detected)
113
+ : await selectProvider("foreman", detected, []);
114
+ if (!chosen) {
115
+ throw new Error("No supported agent installed. Cannot assign a Foreman. " +
116
+ "Install one of: " + SUPPORTED_PROVIDERS.map((p) => p.name).join(", "));
117
+ }
118
+ // Persist choice
119
+ const updatedPolicy = {
120
+ ...providerPolicy,
121
+ foreman: { ...foremanPolicy, providers: [chosen] },
122
+ };
123
+ const updatedConfig = {
124
+ ...config,
125
+ execution: { ...execution, providerPolicy: updatedPolicy },
126
+ };
127
+ const configPath = (0, node_path_1.resolve)(repoRoot, "polaris.config.json");
128
+ if (opts?.writeConfig) {
129
+ opts.writeConfig(configPath, updatedConfig);
130
+ }
131
+ else {
132
+ (0, node_fs_1.writeFileSync)(configPath, JSON.stringify(updatedConfig, null, 2) + "\n");
133
+ }
134
+ return { provider: chosen, roleFile };
135
+ }
89
136
  async function runAgentSetup(repoRoot) {
90
137
  console.log("\nPolaris Agent Setup — configure one provider per role\n");
91
138
  const detected = detectProviders();
package/dist/cli/index.js CHANGED
@@ -27,6 +27,7 @@ const librarian_js_1 = require("./librarian.js");
27
27
  const medic_js_1 = require("./medic.js");
28
28
  const welfare_js_1 = require("../map/welfare.js");
29
29
  const adopt_command_js_1 = require("./adopt-command.js");
30
+ const simplicity_js_1 = require("../loop/simplicity.js");
30
31
  function resolveStateFile(repoRoot, explicit) {
31
32
  if (explicit)
32
33
  return (0, node_path_1.resolve)(explicit);
@@ -108,6 +109,19 @@ function createPolarisCommand(options = {}) {
108
109
  repoRoot,
109
110
  }));
110
111
  program.addCommand((0, adopt_command_js_1.createAdoptCommand)({ repoRoot }));
112
+ program
113
+ .command("simplicity")
114
+ .description("View or override the simplicity discipline mode for the active run")
115
+ .option("--bypass", "Omit the discipline ladder from worker prompts for this run")
116
+ .option("--restore", "Re-enable the discipline ladder (clear bypass)")
117
+ .option("--state-file <path>", "Override path to current-state.json")
118
+ .action((cmdOptions) => {
119
+ (0, simplicity_js_1.runSimplicityCommand)({
120
+ bypass: !!cmdOptions.bypass,
121
+ restore: !!cmdOptions.restore,
122
+ stateFile: resolveStateFile(repoRoot, cmdOptions.stateFile),
123
+ });
124
+ });
111
125
  const agentCmd = new commander_1.Command("agent").description("Manage Polaris agent provider configuration");
112
126
  agentCmd.addCommand(new commander_1.Command("setup")
113
127
  .description("Configure agent providers per role (librarian, foreman, worker, analyst)")