whale-igniter 1.4.0 → 1.5.2

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/CHANGELOG.md CHANGED
@@ -2,6 +2,41 @@
2
2
 
3
3
  All notable changes to Whale Igniter are documented here.
4
4
 
5
+ ## 1.5.2 - 2026-06-10
6
+
7
+ ### Changed
8
+
9
+ - Decisions and refinements are now stored as individual Markdown files with frontmatter (`intelligence/decisions/ADR-XXXX-*.md`, `intelligence/refinements/REF-XXXX-*.md`) instead of accumulative JSON stores. IDs are sequential and human-readable. `loadDecisions`/`loadRefinements` keep dual-reading legacy JSON, so existing projects lose no data.
10
+ - `llm-wiki/DECISIONS.md` is now a compact index table linking to each decision's source file, instead of dumping full bodies — reducing the context an AI agent consumes.
11
+ - Updated `glob` to its supported current major and refreshed the transitive MCP dependency tree; production audit reports zero vulnerabilities.
12
+
13
+ ### Security
14
+
15
+ - `whale changes` now invokes git via `execFileSync` with argument arrays instead of shell-interpolated strings, removing a command-injection vector through the `--since` flag.
16
+
17
+ ## 1.5.1 - 2026-06-03
18
+
19
+ ### Added
20
+
21
+ - `whale forge <Component>` — generates a Markdown implementation contract for a registered component, including spec table, derived design tokens, and an implementation checklist. `whale create component` now suggests running forge as a next step.
22
+ - `whale scribe readme <Component>` — uses the configured AI provider (Anthropic/OpenAI) to generate a developer-ready README for a registered component. Falls back to `--prompt-only` if no API key is set.
23
+ - `whale scribe overview` — generates a full project overview README with foundations, component catalog, and architectural decisions via AI.
24
+ - `whale atlas health` — generates an AI-powered project health report for PMs and tech leads: validation status, design system maturity, risk signals, and prioritized next steps.
25
+ - `whale atlas decisions` — lists active architectural decisions grouped by category, filterable by `--category` and `--status`.
26
+ - Block-style ASCII art title in the `whale ignite` splash screen (Larry 3D → Block font, hardcoded, no runtime dependency).
27
+ - Default header and footer in generated landing pages, with nav links to Features, Proof, and Contact.
28
+ - Hover microinteractions on buttons, cards, hero panel, and quote block, with `prefers-reduced-motion` support.
29
+ - Mobile-responsive fixes: media queries corrected to `768px` literal (not CSS vars), header/footer stack on mobile, contact grid single-column at narrow viewports.
30
+
31
+ ## 1.5.0 - 2026-06-03
32
+
33
+ ### Added
34
+
35
+ - Custom landing-page copy without AI via `whale create landing --content <path>`.
36
+ - Hero override flags for generated landing pages: `--title`, `--eyebrow`, `--tagline`, `--cta`, and `--cta-href`.
37
+ - Local onboarding helpers for landing generation: `--sample-content`, `--brief`, and role-based next-edit hints.
38
+ - Validation for local landing content JSON, with clean CLI failures for malformed content.
39
+
5
40
  ## 1.4.0 - 2026-06-03
6
41
 
7
42
  ### Added
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="https://cdn.jsdelivr.net/npm/whale-igniter@1.3.0/brand/lockup.svg" alt="Whale Igniter" width="640">
2
+ <img src="https://cdn.jsdelivr.net/npm/whale-igniter@latest/brand/lockup.svg" alt="Whale Igniter" width="640">
3
3
  </p>
4
4
 
5
5
  <p align="center">
@@ -34,12 +34,12 @@ my-app/
34
34
  ├── CLAUDE.md # agent-readable project brief, auto-synced
35
35
  ├── intelligence/
36
36
  │ ├── components.json # component catalog with variants and states
37
- │ ├── decisions.json # architectural decisions and their rationale
38
- │ └── refinements.json # approved exceptions "this is intentional"
37
+ │ ├── decisions/ # one Markdown ADR per decision
38
+ │ └── refinements/ # one Markdown file per approved exception
39
39
  └── llm-wiki/ # generated markdown, one file per concern
40
40
  ```
41
41
 
42
- `intelligence/*.json` is the source of truth. Markdown is generated from it. When you run `whale sync`, everything regenerates — CLAUDE.md, the wiki, the context your agent reads on the next conversation.
42
+ `whale.config.json` and `intelligence/` are the source of truth. Decisions and refinements use individual Markdown files with frontmatter; component metadata remains structured JSON. When you run `whale sync`, everything regenerates — CLAUDE.md, the wiki, and the context your agent reads on the next conversation.
43
43
 
44
44
  The agent doesn't guess anymore. It reads.
45
45
 
@@ -100,6 +100,55 @@ whale create landing --theme atlas # enterprise minimal, dense, neutral (de
100
100
 
101
101
  Each theme defines four tiers — Identity, Typography, Structural, Interaction — and emits a complete CSS custom property block. Swap themes; everything updates.
102
102
 
103
+ Landing copy can be customized without AI:
104
+
105
+ ```bash
106
+ whale create landing --theme harbor --title "FuelForge" --tagline "Meal planning for athletes who train hard."
107
+ whale create landing --theme aurora --content landing.json
108
+ whale create landing --brief
109
+ ```
110
+
111
+ `landing.json` can define the brand, hero copy, CTA, features, proof, and contact copy. CLI flags override the JSON, so quick edits stay fast. Use `--sample-content` to generate an editable starter file, or `--brief` for a short local questionnaire that creates the JSON and landing together.
112
+
113
+ ---
114
+
115
+ ## Implementation intelligence
116
+
117
+ Once a component exists, Whale helps you build it right and document it thoroughly. Three commands, one chain:
118
+
119
+ ```bash
120
+ # 1. Generate a typed component scaffold
121
+ whale create component Button --variants primary,secondary,ghost --states hover,focus,disabled
122
+
123
+ # 2. Generate an implementation contract — spec, tokens, checklist
124
+ whale forge Button
125
+
126
+ # 3. Generate a developer README using AI
127
+ whale scribe readme Button
128
+ ```
129
+
130
+ `whale forge` produces a Markdown contract in `docs/forge/` with the component's foundation spec (grid, radius, padding), derived design tokens, and an implementation checklist. No API key required.
131
+
132
+ `whale scribe readme` calls your configured AI provider (Anthropic or OpenAI) and writes a developer-ready README to `docs/scribe/`. Use `--prompt-only` to get the prompt without an API call.
133
+
134
+ `whale scribe overview` generates a full project README — foundations, component catalog, architectural decisions — in one pass.
135
+
136
+ ---
137
+
138
+ ## Project health (Atlas)
139
+
140
+ Atlas gives product managers and tech leads a narrative view of the project's operational health.
141
+
142
+ ```bash
143
+ whale atlas health # AI-generated health report: errors, risk areas, next steps
144
+ whale atlas decisions # list active architectural decisions by category
145
+ whale atlas decisions --category design-system
146
+ ```
147
+
148
+ `whale atlas health` scans validation issues, component coverage, and recorded decisions, then generates a prioritized health report in `docs/atlas/health.md`. Use `--prompt-only` without an API key.
149
+
150
+ `whale atlas decisions` lists active decisions inline, grouped by category, with rationale and consequences.
151
+
103
152
  ---
104
153
 
105
154
  ## Core commands
@@ -114,8 +163,12 @@ Each theme defines four tiers — Identity, Typography, Structural, Interaction
114
163
  | `whale changes --since HEAD~5` | What changed in intelligence stores since a git ref |
115
164
  | `whale themes list` | Browse available UI themes |
116
165
  | `whale create landing --theme <name>` | Generate a themed HTML/CSS landing page |
117
- | `whale create component Hero` | Generate a typed component with intelligence registration |
118
- | `whale references add --all` | Install the UI component reference library |
166
+ | `whale create component <Name>` | Generate a typed component with intelligence registration |
167
+ | `whale forge <Component>` | Generate an implementation contract for a registered component |
168
+ | `whale scribe readme <Component>` | Generate a developer README for a component using AI |
169
+ | `whale scribe overview` | Generate a full project README using AI |
170
+ | `whale atlas health` | Generate an AI-powered project health report |
171
+ | `whale atlas decisions` | List active architectural decisions |
119
172
  | `whale selene suggest` | AI-powered improvement suggestions (API key optional) |
120
173
  | `whale mcp config --client cursor` | Configure an MCP client |
121
174
 
@@ -123,14 +176,14 @@ Each theme defines four tiers — Identity, Typography, Structural, Interaction
123
176
 
124
177
  ## Operating packs
125
178
 
126
- Packs are structured roles not autonomous agents, but instructions and context that help the agent you already use work with better information.
179
+ Packs add structured operational context to your project. `forge`, `scribe`, and `atlas` are available as both standalone commands and installable packs installing a pack makes its context visible to AI agents via CLAUDE.md and the MCP server.
127
180
 
128
181
  ```bash
129
- whale team add lighthouse # quality and validation checks
130
- whale team add scribe # documentation and wiki generation
131
- whale team add forge # implementation architecture
132
- whale team add atlas # product strategy
133
- whale team add selene # AI suggestions and audits
182
+ whale team add lighthouse # quality and accessibility validation
183
+ whale team add scribe # documentation generation
184
+ whale team add forge # implementation contracts and handoff
185
+ whale team add atlas # product health and decision tracking
186
+ whale team add selene # AI suggestions and component audits
134
187
  ```
135
188
 
136
189
  ---
@@ -154,7 +207,7 @@ whale selene describe src/components/Button.tsx
154
207
 
155
208
  Whale Igniter runs offline by default. Core flows such as themes, drift detection, wiki generation, MCP, and local project analysis do not require an API key and do not send project data over the network.
156
209
 
157
- AI features are opt-in. When you enable Selene API mode, Whale sends the prompt and project context to the selected provider, Anthropic or OpenAI, under that provider's terms. You are responsible for reviewing context before sending it and for keeping secrets or sensitive data out of prompts.
210
+ AI features are opt-in. When you enable Selene API mode or use `whale scribe` / `whale atlas health`, Whale sends the prompt and project context to the selected provider, Anthropic or OpenAI, under that provider's terms. You are responsible for reviewing context before sending it and for keeping secrets or sensitive data out of prompts.
158
211
 
159
212
  API keys are read from environment variables only: `ANTHROPIC_API_KEY` or `OPENAI_API_KEY`. The key is used only in request headers, and Whale does not log it, write it to config, or persist it on disk.
160
213
 
@@ -167,7 +220,7 @@ Whale Igniter has no telemetry or analytics.
167
220
  ## Requirements
168
221
 
169
222
  - Node ≥ 20
170
- - No API key required for any core flow — themes, drift detection, wiki generation, and MCP are fully local and deterministic
223
+ - No API key required for any core flow — themes, drift detection, wiki generation, MCP, `whale forge`, and `whale atlas decisions` are fully local and deterministic
171
224
 
172
225
  ---
173
226
 
@@ -186,6 +239,6 @@ Brand assets reference JetBrains Mono (SIL Open Font License 1.1) and Special El
186
239
  ---
187
240
 
188
241
  <p align="center">
189
- <img src="https://cdn.jsdelivr.net/npm/whale-igniter@1.3.0/brand/logo.svg" alt="" width="44"><br>
242
+ <img src="https://cdn.jsdelivr.net/npm/whale-igniter@latest/brand/logo.svg" alt="" width="44"><br>
190
243
  <sub><b>Whale Igniter</b> · map the project before the agent moves</sub>
191
244
  </p>
@@ -136,11 +136,13 @@ export async function adoptCommand(targetArg, options = {}) {
136
136
  console.log(ui.note("Updated stack to 'tailwind' in whale.config.json"));
137
137
  }
138
138
  }
139
- // ---- Step 6: ensure other intelligence files exist ------------------------
140
- for (const file of ["refinements.json", "decisions.json", "components.json"]) {
141
- const p = path.join(target, "intelligence", file);
142
- if (!(await fs.pathExists(p)))
143
- await fs.writeJson(p, [], { spaces: 2 });
139
+ // ---- Step 6: ensure other intelligence stores exist -----------------------
140
+ const intelligenceDir = path.join(target, "intelligence");
141
+ await fs.ensureDir(path.join(intelligenceDir, "decisions"));
142
+ await fs.ensureDir(path.join(intelligenceDir, "refinements"));
143
+ const componentsPath = path.join(intelligenceDir, "components.json");
144
+ if (!(await fs.pathExists(componentsPath))) {
145
+ await fs.writeJson(componentsPath, [], { spaces: 2 });
144
146
  }
145
147
  // ---- Step 7: optional Selene enrichment -----------------------------------
146
148
  const ai = await getAiAvailability(target);
@@ -0,0 +1,196 @@
1
+ import path from "node:path";
2
+ import fs from "fs-extra";
3
+ import ora from "ora";
4
+ import { resolveTarget } from "../utils/paths.js";
5
+ import { loadConfig } from "../utils/config.js";
6
+ import { loadComponents } from "../utils/components.js";
7
+ import { loadDecisions } from "../utils/decisions.js";
8
+ import { loadRefinements } from "../utils/refinements.js";
9
+ import { validateCss } from "../validators/cssValidator.js";
10
+ import { resolveProvider } from "../selene/providers.js";
11
+ import { callProvider } from "../selene/apiClient.js";
12
+ import { ui } from "../ui/index.js";
13
+ // ---------------------------------------------------------------------------
14
+ // whale atlas health
15
+ // ---------------------------------------------------------------------------
16
+ export async function atlasHealthCommand(options = {}) {
17
+ const target = resolveTarget();
18
+ const config = await loadConfig(target);
19
+ const spinner = ora("Atlas · scanning project health").start();
20
+ const [components, decisions, refinements, issues] = await Promise.all([
21
+ loadComponents(target),
22
+ loadDecisions(target),
23
+ loadRefinements(target),
24
+ validateCss(target),
25
+ ]);
26
+ spinner.stop();
27
+ const errors = issues.filter((i) => i.severity === "error");
28
+ const warnings = issues.filter((i) => i.severity === "warning");
29
+ // Group issues by type to surface patterns
30
+ const issuesByType = new Map();
31
+ for (const issue of issues) {
32
+ issuesByType.set(issue.type, (issuesByType.get(issue.type) ?? 0) + 1);
33
+ }
34
+ const topIssueTypes = [...issuesByType.entries()]
35
+ .sort((a, b) => b[1] - a[1])
36
+ .slice(0, 5);
37
+ const activeDecisions = decisions.filter((d) => d.status === "active");
38
+ const outDir = options.out ?? "docs/atlas";
39
+ const fileRel = path.join(outDir, "health.md").replace(/\\/g, "/");
40
+ const fileAbs = path.join(target, fileRel);
41
+ if (await fs.pathExists(fileAbs) && !options.force) {
42
+ console.log(ui.fail(`${ui.path(fileRel)} already exists. Use --force to overwrite.`));
43
+ process.exitCode = 1;
44
+ return;
45
+ }
46
+ const prompt = buildHealthPrompt({
47
+ config,
48
+ components,
49
+ activeDecisions,
50
+ refinementsCount: refinements.length,
51
+ errorCount: errors.length,
52
+ warningCount: warnings.length,
53
+ topIssueTypes,
54
+ });
55
+ if (options.promptOnly) {
56
+ console.log(prompt);
57
+ return;
58
+ }
59
+ const resolved = resolveProvider(config);
60
+ if (!resolved) {
61
+ console.log(ui.warn("No API key found — printing prompt instead."));
62
+ console.log(ui.muted(" Set ANTHROPIC_API_KEY or OPENAI_API_KEY to use AI generation."));
63
+ console.log("\n" + prompt);
64
+ return;
65
+ }
66
+ const spinner2 = ora("Atlas · generating health report").start();
67
+ const result = await callProvider(resolved, { prompt, maxTokens: 1400, temperature: 0.3 });
68
+ if (!result.ok) {
69
+ spinner2.fail(`API error: ${result.error}`);
70
+ process.exitCode = 1;
71
+ return;
72
+ }
73
+ spinner2.succeed("Health report generated");
74
+ const content = result.text.trim();
75
+ await fs.ensureDir(path.dirname(fileAbs));
76
+ await fs.writeFile(fileAbs, content + "\n", "utf8");
77
+ console.log(ui.ok(`Atlas → ${ui.path(fileRel)}`));
78
+ console.log(ui.muted(` provider: ${resolved.provider} · ${resolved.model}`));
79
+ // Print a quick inline summary so the user sees signal immediately
80
+ console.log("");
81
+ printInlineSummary({ errorCount: errors.length, warningCount: warnings.length, activeDecisions, components });
82
+ }
83
+ // ---------------------------------------------------------------------------
84
+ // whale atlas decisions
85
+ // ---------------------------------------------------------------------------
86
+ export async function atlasDecisionsCommand(options = {}) {
87
+ const target = resolveTarget();
88
+ const decisions = await loadDecisions(target);
89
+ if (decisions.length === 0) {
90
+ console.log(ui.muted("No decisions recorded yet. Run `whale decision` to add one."));
91
+ return;
92
+ }
93
+ let filtered = decisions;
94
+ if (options.category) {
95
+ filtered = filtered.filter((d) => d.category === options.category);
96
+ }
97
+ if (options.status) {
98
+ filtered = filtered.filter((d) => d.status === options.status);
99
+ }
100
+ else {
101
+ filtered = filtered.filter((d) => d.status === "active");
102
+ }
103
+ if (filtered.length === 0) {
104
+ console.log(ui.muted("No decisions match the given filters."));
105
+ return;
106
+ }
107
+ const byCategory = new Map();
108
+ for (const d of filtered) {
109
+ const list = byCategory.get(d.category) ?? [];
110
+ list.push(d);
111
+ byCategory.set(d.category, list);
112
+ }
113
+ for (const [category, list] of byCategory) {
114
+ console.log(ui.emphasis(`\n${category.toUpperCase()}`));
115
+ for (const d of list) {
116
+ console.log(` ${ui.ok(d.title)}`);
117
+ console.log(` ${ui.muted(d.decision)}`);
118
+ if (d.consequences) {
119
+ console.log(` ${ui.muted("→ " + d.consequences)}`);
120
+ }
121
+ console.log(` ${ui.muted(d.timestamp.slice(0, 10) + " · " + d.status)}`);
122
+ }
123
+ }
124
+ console.log(`\n${ui.muted(`${filtered.length} decision(s) shown.`)}`);
125
+ }
126
+ // ---------------------------------------------------------------------------
127
+ // Prompt builder
128
+ // ---------------------------------------------------------------------------
129
+ function buildHealthPrompt(args) {
130
+ const { config, components, activeDecisions, refinementsCount, errorCount, warningCount, topIssueTypes } = args;
131
+ const decisionSummary = activeDecisions.length > 0
132
+ ? activeDecisions.map((d) => `- [${d.category}] ${d.title}: ${d.decision}${d.consequences ? ` (consequences: ${d.consequences})` : ""}`).join("\n")
133
+ : " (none recorded)";
134
+ const issueBreakdown = topIssueTypes.length > 0
135
+ ? topIssueTypes.map(([type, count]) => `- ${type}: ${count} occurrence(s)`).join("\n")
136
+ : " (no issues detected)";
137
+ const riskSignals = [];
138
+ if (errorCount > 100)
139
+ riskSignals.push("High error volume — likely systemic design-system drift");
140
+ if (errorCount > 0 && refinementsCount === 0)
141
+ riskSignals.push("Errors present but no refinements recorded — exceptions not being tracked");
142
+ if (activeDecisions.length === 0)
143
+ riskSignals.push("No architectural decisions recorded — context risk for future agents and contributors");
144
+ if (components.length === 0)
145
+ riskSignals.push("No components registered — catalog is empty");
146
+ return [
147
+ "# Atlas task — project health report",
148
+ "",
149
+ "You are a technical product advisor reviewing a frontend project's operational health.",
150
+ "Write a concise, actionable health report for a PM or tech lead.",
151
+ "",
152
+ "## Project",
153
+ `- Name: ${config.projectName ?? "unnamed"}`,
154
+ `- Type: ${config.projectType ?? "unspecified"}`,
155
+ `- Stack: ${config.stack ?? "unspecified"}`,
156
+ `- Active packs: ${(config.packs ?? []).join(", ") || "none"}`,
157
+ "",
158
+ "## Validation status",
159
+ `- Errors: ${errorCount}`,
160
+ `- Warnings: ${warningCount}`,
161
+ `- Top issue types:`,
162
+ issueBreakdown,
163
+ "",
164
+ "## Design system posture",
165
+ `- Components registered: ${components.length}`,
166
+ `- Active decisions: ${activeDecisions.length}`,
167
+ `- Refinements (approved exceptions): ${refinementsCount}`,
168
+ "",
169
+ "## Active decisions",
170
+ decisionSummary,
171
+ "",
172
+ ...(riskSignals.length > 0 ? [
173
+ "## Risk signals detected",
174
+ riskSignals.map((s) => `- ${s}`).join("\n"),
175
+ "",
176
+ ] : []),
177
+ "## Task",
178
+ "Write a health report with these sections:",
179
+ "1. **Executive summary** (2-3 sentences on overall project health)",
180
+ "2. **Validation health** (what the error/warning counts mean in practice)",
181
+ "3. **Design system maturity** (component coverage, decision hygiene)",
182
+ "4. **Risk areas** (specific things that need attention)",
183
+ "5. **Recommended next steps** (3-5 concrete actions, ordered by priority)",
184
+ "",
185
+ "Be direct and specific. No generic advice. Audience: PM or tech lead who can act on it.",
186
+ "Write in plain Markdown. No preamble or explanation outside the report.",
187
+ ].join("\n");
188
+ }
189
+ // ---------------------------------------------------------------------------
190
+ // Inline summary
191
+ // ---------------------------------------------------------------------------
192
+ function printInlineSummary(args) {
193
+ const { errorCount, warningCount, activeDecisions, components } = args;
194
+ const healthEmoji = errorCount === 0 ? "●" : errorCount < 50 ? "◐" : "○";
195
+ console.log(`${healthEmoji} ${errorCount} error(s) · ${warningCount} warning(s) · ${components.length} component(s) · ${activeDecisions.length} decision(s)`);
196
+ }
@@ -1,21 +1,22 @@
1
- import { execSync } from "node:child_process";
1
+ import { execFileSync } from "node:child_process";
2
+ import matter from "gray-matter";
2
3
  import { resolveTarget } from "../utils/paths.js";
3
- import { loadDecisions } from "../utils/decisions.js";
4
4
  import { loadComponents } from "../utils/components.js";
5
- import { loadRefinements } from "../utils/refinements.js";
6
5
  import { ui } from "../ui/index.js";
6
+ const DECISIONS_DIR = "intelligence/decisions";
7
+ const REFINEMENTS_DIR = "intelligence/refinements";
7
8
  const INTELLIGENCE_FILES = [
8
- "intelligence/decisions.json",
9
+ DECISIONS_DIR,
10
+ REFINEMENTS_DIR,
9
11
  "intelligence/components.json",
10
- "intelligence/refinements.json",
11
12
  "whale.config.json"
12
13
  ];
14
+ function git(target, args) {
15
+ return execFileSync("git", args, { cwd: target, encoding: "utf8" });
16
+ }
13
17
  function gitDiffFiles(target, since) {
14
18
  try {
15
- const result = execSync(`git diff --name-only ${since} HEAD -- ${INTELLIGENCE_FILES.join(" ")}`, {
16
- cwd: target,
17
- encoding: "utf8"
18
- });
19
+ const result = git(target, ["diff", "--name-only", since, "HEAD", "--", ...INTELLIGENCE_FILES]);
19
20
  return result.trim().split("\n").filter(Boolean);
20
21
  }
21
22
  catch {
@@ -24,13 +25,49 @@ function gitDiffFiles(target, since) {
24
25
  }
25
26
  function gitFileAtRef(target, ref, file) {
26
27
  try {
27
- const raw = execSync(`git show ${ref}:${file}`, { cwd: target, encoding: "utf8" });
28
+ const raw = git(target, ["show", `${ref}:${file}`]);
28
29
  return JSON.parse(raw);
29
30
  }
30
31
  catch {
31
32
  return [];
32
33
  }
33
34
  }
35
+ function mdLabelAtRef(target, ref, file, label) {
36
+ try {
37
+ const raw = git(target, ["show", `${ref}:${file}`]);
38
+ return label(raw) || file;
39
+ }
40
+ catch {
41
+ return file;
42
+ }
43
+ }
44
+ const decisionLabel = (raw) => {
45
+ const title = matter(raw).data?.title;
46
+ return title ? String(title) : "";
47
+ };
48
+ const refinementLabel = (raw) => matter(raw).content.trim().split("\n")[0] ?? "";
49
+ function gitDirChanges(target, since, dir, label) {
50
+ const result = { added: [], removed: [], modified: [] };
51
+ let raw = "";
52
+ try {
53
+ raw = git(target, ["diff", "--name-status", since, "HEAD", "--", dir]);
54
+ }
55
+ catch {
56
+ return result;
57
+ }
58
+ for (const line of raw.trim().split("\n").filter(Boolean)) {
59
+ const [status, file] = line.split("\t");
60
+ if (!file || !file.endsWith(".md"))
61
+ continue;
62
+ if (status.startsWith("A"))
63
+ result.added.push(mdLabelAtRef(target, "HEAD", file, label));
64
+ else if (status.startsWith("D"))
65
+ result.removed.push(mdLabelAtRef(target, since, file, label));
66
+ else if (status.startsWith("M") || status.startsWith("R"))
67
+ result.modified.push(mdLabelAtRef(target, "HEAD", file, label));
68
+ }
69
+ return result;
70
+ }
34
71
  export async function changesCommand(opts) {
35
72
  const target = resolveTarget(opts.target);
36
73
  const since = opts.since ?? "HEAD~1";
@@ -49,21 +86,8 @@ export async function changesCommand(opts) {
49
86
  if (changedFiles.includes("whale.config.json")) {
50
87
  summary.configChanged = true;
51
88
  }
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
- }
89
+ if (changedFiles.some((f) => f.startsWith(`${DECISIONS_DIR}/`))) {
90
+ summary.decisions = gitDirChanges(target, since, DECISIONS_DIR, decisionLabel);
67
91
  }
68
92
  if (changedFiles.includes("intelligence/components.json")) {
69
93
  const before = gitFileAtRef(target, since, "intelligence/components.json");
@@ -79,20 +103,10 @@ export async function changesCommand(opts) {
79
103
  summary.components.removed.push(name);
80
104
  }
81
105
  }
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
- }
106
+ if (changedFiles.some((f) => f.startsWith(`${REFINEMENTS_DIR}/`))) {
107
+ const changes = gitDirChanges(target, since, REFINEMENTS_DIR, refinementLabel);
108
+ summary.refinements.added = changes.added;
109
+ summary.refinements.removed = changes.removed;
96
110
  }
97
111
  // ---- Render ----------------------------------------------------------------
98
112
  if (summary.configChanged) {
@@ -98,6 +98,9 @@ export async function createComponentCommand(name, options = {}) {
98
98
  await generateWiki(target);
99
99
  console.log(ui.muted(`${ui.glyph.check} AI context updated`));
100
100
  }
101
+ if (shouldRegister) {
102
+ console.log(ui.muted(` → run \`whale forge ${name}\` to generate an implementation contract`));
103
+ }
101
104
  }
102
105
  // ---------------------------------------------------------------------------
103
106
  // Heuristics & templates