whale-igniter 1.2.2 → 1.3.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.
@@ -0,0 +1,127 @@
1
+ import { execSync } from "node:child_process";
2
+ import { resolveTarget } from "../utils/paths.js";
3
+ import { loadDecisions } from "../utils/decisions.js";
4
+ import { loadComponents } from "../utils/components.js";
5
+ import { loadRefinements } from "../utils/refinements.js";
6
+ import { ui } from "../ui/index.js";
7
+ const INTELLIGENCE_FILES = [
8
+ "intelligence/decisions.json",
9
+ "intelligence/components.json",
10
+ "intelligence/refinements.json",
11
+ "whale.config.json"
12
+ ];
13
+ function gitDiffFiles(target, since) {
14
+ try {
15
+ const result = execSync(`git diff --name-only ${since} HEAD -- ${INTELLIGENCE_FILES.join(" ")}`, {
16
+ cwd: target,
17
+ encoding: "utf8"
18
+ });
19
+ return result.trim().split("\n").filter(Boolean);
20
+ }
21
+ catch {
22
+ return [];
23
+ }
24
+ }
25
+ function gitFileAtRef(target, ref, file) {
26
+ try {
27
+ const raw = execSync(`git show ${ref}:${file}`, { cwd: target, encoding: "utf8" });
28
+ return JSON.parse(raw);
29
+ }
30
+ catch {
31
+ return [];
32
+ }
33
+ }
34
+ export async function changesCommand(opts) {
35
+ const target = resolveTarget(opts.target);
36
+ const since = opts.since ?? "HEAD~1";
37
+ const changedFiles = gitDiffFiles(target, since);
38
+ if (changedFiles.length === 0) {
39
+ console.log(ui.note(`No intelligence store changes since ${since}`));
40
+ return;
41
+ }
42
+ console.log(ui.section(`Changes since ${since}`));
43
+ const summary = {
44
+ decisions: { added: [], removed: [], modified: [] },
45
+ components: { added: [], removed: [] },
46
+ refinements: { added: [], removed: [] },
47
+ configChanged: false
48
+ };
49
+ if (changedFiles.includes("whale.config.json")) {
50
+ summary.configChanged = true;
51
+ }
52
+ if (changedFiles.includes("intelligence/decisions.json")) {
53
+ const before = gitFileAtRef(target, since, "intelligence/decisions.json");
54
+ const after = await loadDecisions(target);
55
+ const beforeIds = new Map(before.map((d) => [d.id, d.title]));
56
+ const afterIds = new Map(after.map((d) => [d.id, d.title]));
57
+ for (const [id, title] of afterIds) {
58
+ if (!beforeIds.has(id))
59
+ summary.decisions.added.push(title);
60
+ }
61
+ for (const [id, title] of beforeIds) {
62
+ if (!afterIds.has(id))
63
+ summary.decisions.removed.push(title);
64
+ else if (afterIds.get(id) !== title)
65
+ summary.decisions.modified.push(title);
66
+ }
67
+ }
68
+ if (changedFiles.includes("intelligence/components.json")) {
69
+ const before = gitFileAtRef(target, since, "intelligence/components.json");
70
+ const after = await loadComponents(target);
71
+ const beforeNames = new Set(before.map((c) => c.name));
72
+ const afterNames = new Set(after.map((c) => c.name));
73
+ for (const name of afterNames) {
74
+ if (!beforeNames.has(name))
75
+ summary.components.added.push(name);
76
+ }
77
+ for (const name of beforeNames) {
78
+ if (!afterNames.has(name))
79
+ summary.components.removed.push(name);
80
+ }
81
+ }
82
+ if (changedFiles.includes("intelligence/refinements.json")) {
83
+ const before = gitFileAtRef(target, since, "intelligence/refinements.json");
84
+ const after = await loadRefinements(target);
85
+ const beforeIds = new Set(before.map((r) => r.id));
86
+ const afterIds = new Set(after.map((r) => r.id));
87
+ const beforeNotes = new Map(before.map((r) => [r.id, r.note]));
88
+ for (const r of after) {
89
+ if (!beforeIds.has(r.id))
90
+ summary.refinements.added.push(r.note);
91
+ }
92
+ for (const [id, note] of beforeNotes) {
93
+ if (!afterIds.has(id))
94
+ summary.refinements.removed.push(note);
95
+ }
96
+ }
97
+ // ---- Render ----------------------------------------------------------------
98
+ if (summary.configChanged) {
99
+ console.log(ui.note("whale.config.json changed"));
100
+ }
101
+ const { decisions: d } = summary;
102
+ if (d.added.length || d.removed.length || d.modified.length) {
103
+ console.log(ui.section("Decisions"));
104
+ for (const t of d.added)
105
+ console.log(ui.ok(`+ ${t}`));
106
+ for (const t of d.removed)
107
+ console.log(ui.fail(`- ${t}`));
108
+ for (const t of d.modified)
109
+ console.log(ui.warn(`~ ${t}`));
110
+ }
111
+ const { components: c } = summary;
112
+ if (c.added.length || c.removed.length) {
113
+ console.log(ui.section("Components"));
114
+ for (const n of c.added)
115
+ console.log(ui.ok(`+ ${n}`));
116
+ for (const n of c.removed)
117
+ console.log(ui.fail(`- ${n}`));
118
+ }
119
+ const { refinements: r } = summary;
120
+ if (r.added.length || r.removed.length) {
121
+ console.log(ui.section("Refinements"));
122
+ for (const n of r.added)
123
+ console.log(ui.ok(`+ ${n}`));
124
+ for (const n of r.removed)
125
+ console.log(ui.fail(`- ${n}`));
126
+ }
127
+ }
@@ -5,6 +5,9 @@ import { loadConfig, saveConfig, DEFAULT_CONFIG } from "../utils/config.js";
5
5
  import { getPack, listPacks } from "../utils/registry.js";
6
6
  import { validateCss } from "../validators/cssValidator.js";
7
7
  import { generateWiki } from "../generators/wikiGenerator.js";
8
+ import { appendDecision } from "../utils/decisions.js";
9
+ import { getAiAvailability } from "../utils/aiAvailability.js";
10
+ import { mapWizardAnswers, suggestUiCategories } from "../utils/wizardMapping.js";
8
11
  import { ui } from "../ui/index.js";
9
12
  import { PACKAGE_VERSION } from "../version.js";
10
13
  const OPINIONATED_PACKS = ["lighthouse", "forge", "scribe"];
@@ -20,8 +23,11 @@ export async function igniteCommand(projectName = "whale-project", options = {})
20
23
  console.log();
21
24
  // ---- Resolve config based on mode -----------------------------------------
22
25
  let config;
26
+ let projectIntentNote;
23
27
  if (mode === "interactive") {
24
- config = await runWizard(projectName);
28
+ const result = await runWizard(projectName);
29
+ config = result.config;
30
+ projectIntentNote = result.projectIntentNote;
25
31
  }
26
32
  else if (mode === "minimal") {
27
33
  config = {
@@ -43,6 +49,16 @@ export async function igniteCommand(projectName = "whale-project", options = {})
43
49
  const target = await initCommand(projectName, { config, silent: true });
44
50
  const targetRel = path.relative(process.cwd(), target) || ".";
45
51
  console.log(ui.ok(`Workspace created at ${ui.path(targetRel)}`));
52
+ // ---- Step 1b: record free-text project intent as first decision ------------
53
+ if (projectIntentNote) {
54
+ await appendDecision(target, {
55
+ title: "Project intent",
56
+ category: "product",
57
+ context: "Captured at project creation via `whale ignite --interactive`.",
58
+ decision: projectIntentNote
59
+ });
60
+ console.log(ui.ok("Project intent recorded as first decision."));
61
+ }
46
62
  // ---- Step 2: confirm packs against registry -------------------------------
47
63
  console.log();
48
64
  console.log(ui.section("Packs"));
@@ -58,6 +74,9 @@ export async function igniteCommand(projectName = "whale-project", options = {})
58
74
  validPacks.push(name);
59
75
  packLines.push(`${ui.glyph.check} ${ui.code(name)} ${ui.muted(`(${pack.kind})`)} — ${pack.description}`);
60
76
  }
77
+ if (packLines.length === 0) {
78
+ packLines.push(ui.muted("(no packs selected)"));
79
+ }
61
80
  console.log(ui.indent(packLines.join("\n")));
62
81
  finalConfig.packs = validPacks;
63
82
  finalConfig.ignited = {
@@ -127,86 +146,133 @@ export async function igniteCommand(projectName = "whale-project", options = {})
127
146
  console.log();
128
147
  }
129
148
  // ---------------------------------------------------------------------------
130
- // Interactive wizard — unchanged logic, just uses ui for the header.
149
+ // Interactive wizard — human-language questions, technical config is derived.
131
150
  // ---------------------------------------------------------------------------
132
151
  async function runWizard(defaultName) {
133
152
  console.log(ui.muted("Answer a few questions to shape your workspace. Ctrl+C to cancel."));
134
153
  console.log();
135
- const allPacks = listPacks();
136
- const answers = await prompts([
137
- { type: "text", name: "projectName", message: "Project name:", initial: defaultName },
138
- {
139
- type: "select",
140
- name: "projectType",
141
- message: "Project type:",
142
- choices: [
143
- { title: "Landing page", value: "landing-page" },
144
- { title: "Web app", value: "web-app" },
145
- { title: "Component library", value: "component-library" },
146
- { title: "Documentation site", value: "docs-site" },
147
- { title: "Other", value: "other" }
148
- ]
149
- },
150
- {
151
- type: "select",
152
- name: "stack",
153
- message: "Styling stack:",
154
- choices: [
155
- { title: "Plain CSS", value: "css" },
156
- { title: "Tailwind", value: "tailwind" },
157
- { title: "SCSS / Sass", value: "scss" },
158
- { title: "CSS-in-JS (styled-components, emotion, ...)", value: "css-in-js" }
159
- ]
160
- },
161
- { type: "number", name: "grid", message: "Grid unit (px):", initial: 8, min: 1, max: 32 },
162
- { type: "number", name: "radiusControl", message: "Radius for controls (buttons, inputs):", initial: 2, min: 0, max: 64 },
163
- { type: "number", name: "radiusContainer", message: "Radius for containers (cards, modals):", initial: 4, min: 0, max: 64 },
164
- {
165
- type: "multiselect",
166
- name: "packs",
167
- message: "Packs to install:",
168
- instructions: false,
169
- hint: "space to toggle, enter to confirm",
170
- choices: allPacks.map((p) => ({
171
- title: `${p.name} (${p.kind})`,
172
- description: p.description,
173
- value: p.name,
174
- selected: OPINIONATED_PACKS.includes(p.name)
175
- }))
176
- },
177
- {
178
- type: "multiselect",
179
- name: "aiTargets",
180
- message: "AI agents to generate context for:",
181
- instructions: false,
182
- hint: "space to toggle, enter to confirm",
183
- choices: [
184
- { title: "Claude (CLAUDE.md)", value: "claude", selected: true },
185
- { title: "Codex / Generic agent (AGENTS.md)", value: "codex" },
186
- { title: "Cursor (.cursorrules)", value: "cursor" },
187
- { title: "GitHub Copilot (.github/copilot-instructions.md)", value: "copilot" }
188
- ]
189
- }
190
- ], {
191
- onCancel: () => {
192
- console.log();
193
- console.log(ui.warn("Ignite cancelled."));
194
- process.exit(130);
154
+ const onCancel = () => {
155
+ console.log();
156
+ console.log(ui.warn("Ignite cancelled."));
157
+ process.exit(130);
158
+ };
159
+ // Q0: project name
160
+ const { projectName } = await prompts({ type: "text", name: "projectName", message: "Project name:", initial: defaultName }, { onCancel });
161
+ // Q1: what are you building?
162
+ const { projectType } = await prompts({
163
+ type: "select",
164
+ name: "projectType",
165
+ message: "What are you building?",
166
+ choices: [
167
+ { title: "Web app", value: "web-app" },
168
+ { title: "Marketing site", value: "marketing-site" },
169
+ { title: "Portfolio", value: "portfolio" },
170
+ { title: "Internal tool", value: "internal-tool" },
171
+ { title: "Prototype", value: "prototype" },
172
+ { title: "Something else", value: "other" }
173
+ ]
174
+ }, { onCancel });
175
+ let projectTypeOther;
176
+ if (projectType === "other") {
177
+ const { freeText } = await prompts({ type: "text", name: "freeText", message: "Describe it briefly:" }, { onCancel });
178
+ projectTypeOther = freeText;
179
+ }
180
+ // Q2: stack choices reordered by what's most likely for this project type
181
+ const appFirstChoices = [
182
+ { title: "TypeScript + React", value: "react-ts" },
183
+ { title: "TypeScript + Vue", value: "vue-ts" },
184
+ { title: "TypeScript + Svelte", value: "svelte-ts" },
185
+ { title: "Plain HTML + CSS + JS", value: "html-css-js" },
186
+ { title: "Other", value: "other" }
187
+ ];
188
+ const staticFirstChoices = [
189
+ { title: "Plain HTML + CSS + JS", value: "html-css-js" },
190
+ { title: "TypeScript + React", value: "react-ts" },
191
+ { title: "TypeScript + Vue", value: "vue-ts" },
192
+ { title: "TypeScript + Svelte", value: "svelte-ts" },
193
+ { title: "Other", value: "other" }
194
+ ];
195
+ const stackChoices = projectType === "web-app" || projectType === "internal-tool"
196
+ ? appFirstChoices
197
+ : staticFirstChoices;
198
+ const { stack } = await prompts({
199
+ type: "select",
200
+ name: "stack",
201
+ message: "What language or framework?",
202
+ choices: stackChoices
203
+ }, { onCancel });
204
+ let stackOther;
205
+ if (stack === "other") {
206
+ const { freeText } = await prompts({ type: "text", name: "freeText", message: "Briefly describe it (we'll use plain CSS defaults):" }, { onCancel });
207
+ stackOther = freeText;
208
+ }
209
+ // Q3: team size
210
+ const { teamSize } = await prompts({
211
+ type: "select",
212
+ name: "teamSize",
213
+ message: "Who will work on this?",
214
+ choices: [
215
+ { title: "Just me", value: "solo" },
216
+ { title: "A small team (2–5 people)", value: "small" },
217
+ { title: "A larger team (6+)", value: "large" }
218
+ ]
219
+ }, { onCancel });
220
+ // Q4: UI references — only if AI is available
221
+ let uiCategories;
222
+ const ai = await getAiAvailability();
223
+ if (ai.available) {
224
+ const suggested = suggestUiCategories(projectType);
225
+ const { addRefs } = await prompts({
226
+ type: "confirm",
227
+ name: "addRefs",
228
+ message: "Add UI component references to this project?",
229
+ initial: true
230
+ }, { onCancel });
231
+ if (addRefs) {
232
+ const allCategories = ["forms", "navigation", "feedback", "surface", "layout", "data-display"];
233
+ const { selected } = await prompts({
234
+ type: "multiselect",
235
+ name: "selected",
236
+ message: "Which categories? (space to toggle, enter to confirm)",
237
+ instructions: false,
238
+ hint: "suggested for this project type are pre-selected",
239
+ choices: allCategories.map((cat) => ({
240
+ title: cat,
241
+ value: cat,
242
+ selected: suggested.includes(cat)
243
+ }))
244
+ }, { onCancel });
245
+ uiCategories = selected ?? suggested;
195
246
  }
196
- });
197
- return {
198
- ...DEFAULT_CONFIG,
199
- projectName: answers.projectName,
200
- projectType: answers.projectType,
201
- stack: answers.stack,
202
- packs: answers.packs && answers.packs.length > 0 ? answers.packs : OPINIONATED_PACKS,
203
- aiTargets: answers.aiTargets && answers.aiTargets.length > 0 ? answers.aiTargets : ["claude"],
204
- foundations: {
205
- grid: answers.grid ?? 8,
206
- radius: {
207
- control: answers.radiusControl ?? 2,
208
- container: answers.radiusContainer ?? 4
209
- }
247
+ else {
248
+ uiCategories = [];
210
249
  }
250
+ }
251
+ // Q5: agent packs
252
+ const allPacks = listPacks();
253
+ const { agentPacks } = await prompts({
254
+ type: "multiselect",
255
+ name: "agentPacks",
256
+ message: "Which agent packs to install?",
257
+ instructions: false,
258
+ hint: "space to toggle, enter to confirm",
259
+ choices: allPacks.map((p) => ({
260
+ title: `${p.name} — ${p.description}`,
261
+ value: p.name,
262
+ selected: p.name === "selene"
263
+ }))
264
+ }, { onCancel });
265
+ // Map human answers to technical config
266
+ const wizardAnswers = {
267
+ projectType,
268
+ projectTypeOther,
269
+ stack,
270
+ stackOther,
271
+ teamSize,
272
+ uiCategories,
273
+ agentPacks
211
274
  };
275
+ const result = mapWizardAnswers(wizardAnswers);
276
+ result.config.projectName = projectName;
277
+ return result;
212
278
  }
@@ -1,16 +1,23 @@
1
1
  import path from "node:path";
2
2
  import fs from "fs-extra";
3
3
  import ora from "ora";
4
+ import prompts from "prompts";
4
5
  import { resolveTarget } from "../utils/paths.js";
5
6
  import { loadConfig } from "../utils/config.js";
6
- import { loadRefinements } from "../utils/refinements.js";
7
+ import { loadRefinements, appendRefinement } from "../utils/refinements.js";
7
8
  import { loadDecisions } from "../utils/decisions.js";
8
9
  import { loadComponents } from "../utils/components.js";
9
- import { analyze } from "../analyzer/insights.js";
10
+ import { analyze, analyzeSpacingDrift, analyzeColorDrift, analyzeRadiiDrift } from "../analyzer/insights.js";
10
11
  import { collectReferencedFiles, normalizePath } from "../analyzer/imports.js";
11
12
  import { scanComponents } from "../scanner/componentScanner.js";
12
13
  import { aggregateTailwind } from "../scanner/tailwindScanner.js";
14
+ import { extractFromCssFiles } from "../scanner/extractors/css.js";
15
+ import { extractFromInlineStyles } from "../scanner/extractors/inline.js";
16
+ import { extractFromStyleBlocks } from "../scanner/extractors/styleBlocks.js";
13
17
  import { ui } from "../ui/index.js";
18
+ import { writeJsonFile } from "../utils/writeJson.js";
19
+ import { getAiAvailability } from "../utils/aiAvailability.js";
20
+ import { randomUUID } from "node:crypto";
14
21
  const SEVERITY_RANK = { info: 0, warning: 1, critical: 2 };
15
22
  export async function insightsCommand(targetArg, options = {}) {
16
23
  const target = resolveTarget(targetArg);
@@ -34,18 +41,28 @@ export async function insightsCommand(targetArg, options = {}) {
34
41
  spin.succeed(`Loaded ${refinements.length} refinement(s), ${decisions.length} decision(s), ${components.length} component(s)`);
35
42
  let referencedFiles;
36
43
  let observations;
44
+ let styleObservations;
37
45
  if (!options.skipScan) {
38
- const spinScan = ora({ text: "Scanning source for imports and Tailwind usage", stream: process.stderr }).start();
46
+ const spinScan = ora({ text: "Scanning source files", stream: process.stderr }).start();
39
47
  try {
40
- const [refs, scanned] = await Promise.all([collectReferencedFiles(target), scanComponents(target)]);
48
+ const [refs, scanned, cssObs, inlineObs, blockObs] = await Promise.all([
49
+ collectReferencedFiles(target),
50
+ scanComponents(target),
51
+ extractFromCssFiles(target),
52
+ extractFromInlineStyles(target),
53
+ extractFromStyleBlocks(target)
54
+ ]);
41
55
  referencedFiles = refs;
42
56
  const allClassStrings = scanned.flatMap((c) => c.classNames);
43
57
  observations = allClassStrings.length > 0 ? aggregateTailwind(allClassStrings) : undefined;
44
- spinScan.succeed(`Scanned ${refs.size} import reference(s)` +
45
- (observations ? ` and ${observations.length} class observation(s)` : ""));
58
+ styleObservations = [...cssObs, ...inlineObs, ...blockObs];
59
+ spinScan.succeed(`Scanned ${refs.size} import(s)` +
60
+ (observations ? `, ${observations.length} Tailwind class(es)` : "") +
61
+ `, ${styleObservations.length} style observation(s)`);
46
62
  }
47
63
  catch (err) {
48
- spinScan.warn(`Scan failed: ${err?.message ?? err}. Continuing without orphan/drift insights.`);
64
+ const msg = err instanceof Error ? err.message : String(err);
65
+ spinScan.warn(`Scan failed: ${msg}. Continuing without drift insights.`);
49
66
  }
50
67
  }
51
68
  const normalisedComponents = referencedFiles
@@ -57,10 +74,130 @@ export async function insightsCommand(targetArg, options = {}) {
57
74
  decisions,
58
75
  components: normalisedComponents,
59
76
  observations,
60
- referencedFiles
77
+ referencedFiles,
78
+ styleObservations
61
79
  });
62
- return printOrEmit(insights, options);
80
+ printOrEmit(insights, options);
81
+ if (!options.json) {
82
+ const ai = await getAiAvailability(target);
83
+ if (ai.available && insights.length > 0) {
84
+ console.log(ui.note("AI-generated UX hints available. Run `whale selene suggest --focus all` to get them."));
85
+ console.log();
86
+ }
87
+ else if (!ai.available) {
88
+ console.log(ui.muted("AI features available with Claude Code, Codex, or an API key."));
89
+ console.log();
90
+ }
91
+ }
92
+ }
93
+ export async function insightsDriftReviewCommand(category, opts) {
94
+ const validCategories = ["spacing", "color", "radii"];
95
+ if (!validCategories.includes(category)) {
96
+ console.log(ui.fail(`Unknown drift category: ${category}. Valid: ${validCategories.join(", ")}`));
97
+ process.exitCode = 1;
98
+ return;
99
+ }
100
+ const cat = category;
101
+ const target = resolveTarget(opts.target);
102
+ console.log();
103
+ console.log(ui.header("Whale Igniter", `insights drift ${cat}`));
104
+ console.log();
105
+ const spin = ora({ text: "Scanning source files", stream: process.stderr }).start();
106
+ const [config, refinements, cssObs, inlineObs, blockObs] = await Promise.all([
107
+ loadConfig(target),
108
+ loadRefinements(target),
109
+ extractFromCssFiles(target),
110
+ extractFromInlineStyles(target),
111
+ extractFromStyleBlocks(target)
112
+ ]);
113
+ const styleObservations = [...cssObs, ...inlineObs, ...blockObs];
114
+ spin.succeed(`${styleObservations.length} style observation(s) found`);
115
+ const driftInsights = cat === "spacing"
116
+ ? analyzeSpacingDrift(styleObservations, config, refinements)
117
+ : cat === "color"
118
+ ? analyzeColorDrift(styleObservations, config, refinements)
119
+ : analyzeRadiiDrift(styleObservations, config, refinements);
120
+ if (driftInsights.length === 0) {
121
+ console.log(ui.ok(`No ${cat} drift found.`));
122
+ console.log();
123
+ return;
124
+ }
125
+ console.log(ui.section(`${cat} drift — ${driftInsights.length} insight(s)`));
126
+ console.log();
127
+ if (!opts.review) {
128
+ for (const ins of driftInsights) {
129
+ console.log(ui.indent(ui.emphasis(ins.title)));
130
+ console.log(ui.indent(ui.muted(ins.detail), 1));
131
+ if (ins.evidence) {
132
+ for (const e of ins.evidence.slice(0, 5)) {
133
+ console.log(ui.indent(ui.dot(e), 2));
134
+ }
135
+ }
136
+ console.log();
137
+ }
138
+ console.log(ui.muted("Use --review to enter the interactive review loop."));
139
+ console.log();
140
+ return;
141
+ }
142
+ // Interactive review loop
143
+ const refactorQueuePath = path.join(target, "intelligence", "refactor-queue.json");
144
+ const refactorQueue = (await fs.pathExists(refactorQueuePath))
145
+ ? await fs.readJson(refactorQueuePath)
146
+ : [];
147
+ let reviewed = 0;
148
+ for (const ins of driftInsights) {
149
+ console.log(ui.section(ins.title));
150
+ console.log(ui.indent(ui.muted(ins.detail)));
151
+ if (ins.evidence) {
152
+ for (const e of ins.evidence)
153
+ console.log(ui.indent(ui.dot(e), 1));
154
+ }
155
+ console.log();
156
+ const { action } = await prompts({
157
+ type: "select",
158
+ name: "action",
159
+ message: "What would you like to do?",
160
+ choices: [
161
+ { title: "Accept as refinement (suppress this drift)", value: "refine" },
162
+ { title: "Flag for refactor (add to queue)", value: "queue" },
163
+ { title: "Skip", value: "skip" },
164
+ { title: "Quit review", value: "quit" }
165
+ ]
166
+ });
167
+ if (!action || action === "quit")
168
+ break;
169
+ if (action === "refine") {
170
+ await appendRefinement(target, {
171
+ id: randomUUID(),
172
+ timestamp: new Date().toISOString(),
173
+ note: ins.title,
174
+ scope: { issueType: cat }
175
+ });
176
+ console.log(ui.ok("Recorded as refinement."));
177
+ }
178
+ else if (action === "queue") {
179
+ refactorQueue.push({
180
+ id: randomUUID(),
181
+ insightId: ins.id,
182
+ title: ins.title,
183
+ evidence: ins.evidence ?? [],
184
+ addedAt: new Date().toISOString()
185
+ });
186
+ console.log(ui.ok("Added to refactor queue."));
187
+ }
188
+ reviewed++;
189
+ console.log();
190
+ }
191
+ if (refactorQueue.length > 0) {
192
+ await fs.ensureDir(path.join(target, "intelligence"));
193
+ await writeJsonFile(refactorQueuePath, refactorQueue);
194
+ }
195
+ console.log(ui.accent(`Reviewed ${reviewed} insight(s).`));
196
+ console.log();
63
197
  }
198
+ // ---------------------------------------------------------------------------
199
+ // Shared rendering
200
+ // ---------------------------------------------------------------------------
64
201
  function printOrEmit(insights, options) {
65
202
  let filtered = insights;
66
203
  if (options.category)