@kiwidata/grimoire 0.1.3 → 0.1.4

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 (127) hide show
  1. package/AGENTS.md +56 -4
  2. package/README.md +28 -1
  3. package/dist/cli/index.js +2 -0
  4. package/dist/cli/index.js.map +1 -1
  5. package/dist/commands/check.js +1 -1
  6. package/dist/commands/check.js.map +1 -1
  7. package/dist/commands/configure.d.ts +3 -0
  8. package/dist/commands/configure.d.ts.map +1 -0
  9. package/dist/commands/configure.js +19 -0
  10. package/dist/commands/configure.js.map +1 -0
  11. package/dist/commands/init.d.ts.map +1 -1
  12. package/dist/commands/init.js +2 -0
  13. package/dist/commands/init.js.map +1 -1
  14. package/dist/commands/map.d.ts.map +1 -1
  15. package/dist/commands/map.js +10 -11
  16. package/dist/commands/map.js.map +1 -1
  17. package/dist/core/archive.d.ts.map +1 -1
  18. package/dist/core/archive.js +32 -43
  19. package/dist/core/archive.js.map +1 -1
  20. package/dist/core/check.d.ts.map +1 -1
  21. package/dist/core/check.js +115 -104
  22. package/dist/core/check.js.map +1 -1
  23. package/dist/core/ci.d.ts.map +1 -1
  24. package/dist/core/ci.js +50 -69
  25. package/dist/core/ci.js.map +1 -1
  26. package/dist/core/configure.d.ts +14 -0
  27. package/dist/core/configure.d.ts.map +1 -0
  28. package/dist/core/configure.js +434 -0
  29. package/dist/core/configure.js.map +1 -0
  30. package/dist/core/detect.d.ts.map +1 -1
  31. package/dist/core/detect.js +153 -26
  32. package/dist/core/detect.js.map +1 -1
  33. package/dist/core/diff.d.ts.map +1 -1
  34. package/dist/core/diff.js +62 -93
  35. package/dist/core/diff.js.map +1 -1
  36. package/dist/core/doc-style.d.ts +0 -4
  37. package/dist/core/doc-style.d.ts.map +1 -1
  38. package/dist/core/doc-style.js +28 -23
  39. package/dist/core/doc-style.js.map +1 -1
  40. package/dist/core/docs.js +106 -100
  41. package/dist/core/docs.js.map +1 -1
  42. package/dist/core/health.js +55 -77
  43. package/dist/core/health.js.map +1 -1
  44. package/dist/core/hooks.d.ts +0 -3
  45. package/dist/core/hooks.d.ts.map +1 -1
  46. package/dist/core/hooks.js +0 -11
  47. package/dist/core/hooks.js.map +1 -1
  48. package/dist/core/init.d.ts +2 -0
  49. package/dist/core/init.d.ts.map +1 -1
  50. package/dist/core/init.js +230 -406
  51. package/dist/core/init.js.map +1 -1
  52. package/dist/core/list.d.ts.map +1 -1
  53. package/dist/core/list.js +55 -65
  54. package/dist/core/list.js.map +1 -1
  55. package/dist/core/log.d.ts.map +1 -1
  56. package/dist/core/log.js +23 -33
  57. package/dist/core/log.js.map +1 -1
  58. package/dist/core/map.d.ts +15 -2
  59. package/dist/core/map.d.ts.map +1 -1
  60. package/dist/core/map.js +257 -194
  61. package/dist/core/map.js.map +1 -1
  62. package/dist/core/shared-setup.d.ts +0 -40
  63. package/dist/core/shared-setup.d.ts.map +1 -1
  64. package/dist/core/shared-setup.js +87 -52
  65. package/dist/core/shared-setup.js.map +1 -1
  66. package/dist/core/status.d.ts.map +1 -1
  67. package/dist/core/status.js +42 -52
  68. package/dist/core/status.js.map +1 -1
  69. package/dist/core/test-quality.d.ts +0 -8
  70. package/dist/core/test-quality.d.ts.map +1 -1
  71. package/dist/core/test-quality.js +24 -30
  72. package/dist/core/test-quality.js.map +1 -1
  73. package/dist/core/trace.d.ts.map +1 -1
  74. package/dist/core/trace.js +31 -41
  75. package/dist/core/trace.js.map +1 -1
  76. package/dist/core/update.d.ts.map +1 -1
  77. package/dist/core/update.js +61 -11
  78. package/dist/core/update.js.map +1 -1
  79. package/dist/core/validate.d.ts +1 -4
  80. package/dist/core/validate.d.ts.map +1 -1
  81. package/dist/core/validate.js +126 -148
  82. package/dist/core/validate.js.map +1 -1
  83. package/dist/utils/config.d.ts +15 -5
  84. package/dist/utils/config.d.ts.map +1 -1
  85. package/dist/utils/config.js +63 -42
  86. package/dist/utils/config.js.map +1 -1
  87. package/dist/utils/fs.d.ts +0 -12
  88. package/dist/utils/fs.d.ts.map +1 -1
  89. package/dist/utils/fs.js +0 -12
  90. package/dist/utils/fs.js.map +1 -1
  91. package/dist/utils/paths.d.ts +0 -6
  92. package/dist/utils/paths.d.ts.map +1 -1
  93. package/dist/utils/paths.js +0 -6
  94. package/dist/utils/paths.js.map +1 -1
  95. package/dist/utils/spawn.d.ts +0 -3
  96. package/dist/utils/spawn.d.ts.map +1 -1
  97. package/dist/utils/spawn.js +0 -3
  98. package/dist/utils/spawn.js.map +1 -1
  99. package/package.json +1 -1
  100. package/skills/grimoire-apply/SKILL.md +84 -16
  101. package/skills/grimoire-audit/SKILL.md +21 -1
  102. package/skills/grimoire-bug/SKILL.md +48 -9
  103. package/skills/grimoire-commit/SKILL.md +2 -1
  104. package/skills/grimoire-design/SKILL.md +259 -0
  105. package/skills/grimoire-design-consult/SKILL.md +200 -0
  106. package/skills/grimoire-discover/SKILL.md +65 -2
  107. package/skills/grimoire-draft/SKILL.md +85 -2
  108. package/skills/grimoire-plan/SKILL.md +61 -18
  109. package/skills/grimoire-pr/SKILL.md +4 -6
  110. package/skills/grimoire-pr-review/SKILL.md +45 -114
  111. package/skills/grimoire-precommit-review/SKILL.md +205 -0
  112. package/skills/grimoire-refactor/SKILL.md +5 -5
  113. package/skills/grimoire-review/SKILL.md +74 -147
  114. package/skills/grimoire-verify/SKILL.md +33 -0
  115. package/skills/references/adversarial-personas.md +225 -0
  116. package/skills/references/brand-tokens-format.md +186 -0
  117. package/skills/references/code-quality.md +140 -0
  118. package/skills/references/design-heuristics.md +138 -0
  119. package/skills/references/design-input-formats.md +190 -0
  120. package/skills/references/pattern-guard.md +180 -0
  121. package/skills/references/refactor-scan-categories.md +152 -0
  122. package/skills/references/review-personas.md +405 -0
  123. package/skills/references/security-compliance.md +22 -1
  124. package/skills/references/visual-fidelity.md +206 -0
  125. package/templates/brand-tokens-example.json +13 -0
  126. package/templates/brand-voice-example.md +22 -0
  127. package/templates/design-tool-setup-stub.md +59 -0
package/dist/core/init.js CHANGED
@@ -7,6 +7,7 @@ import { detectTools } from "./detect.js";
7
7
  import { setupHooks } from "./hooks.js";
8
8
  import { fileExists } from "../utils/fs.js";
9
9
  import { upsertAgentsFile, installSkillFiles, SKILL_NAMES, GRIMOIRE_DIRS, TEMPLATE_FILES, generateAgentFiles, detectAgentFiles, } from "./shared-setup.js";
10
+ import { runSections } from "./configure.js";
10
11
  const __dirname = dirname(fileURLToPath(import.meta.url));
11
12
  const PACKAGE_ROOT = join(__dirname, "..", "..");
12
13
  const CATEGORY_LABELS = {
@@ -39,68 +40,105 @@ const CATEGORY_ORDER = [
39
40
  "doc_tool",
40
41
  "comment_style",
41
42
  ];
42
- export async function initProject(projectPath, options) {
43
- const root = join(process.cwd(), projectPath);
44
- console.log(chalk.bold("Initializing grimoire...\n"));
45
- // Create directory structure
43
+ async function runFullConfigSections(root, config) {
44
+ const ALL_SECTIONS = ["tools", "compliance", "llm", "trackers", "testing"];
45
+ const readline = await import("node:readline/promises");
46
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
47
+ console.log(chalk.bold("\n Advanced configuration (--full):\n"));
48
+ await runSections(rl, config, root, ALL_SECTIONS);
49
+ rl.close();
50
+ const fullSerialized = yamlStringify(config);
51
+ scanForSecrets(fullSerialized);
52
+ await writeFile(join(root, ".grimoire", "config.yaml"), fullSerialized);
53
+ }
54
+ function buildIntegrationFlags(initialFlags, config) {
55
+ return {
56
+ codebaseMemoryMcp: initialFlags.codebaseMemoryMcp ?? config.project.integrations?.codebase_memory_mcp,
57
+ cavemanPlugin: initialFlags.cavemanPlugin ?? config.project.integrations?.caveman_plugin,
58
+ };
59
+ }
60
+ async function createGrimoireConfig(root, options, initialFlags) {
61
+ let projectDetection = null;
62
+ let config;
63
+ if (options.noDetect) {
64
+ config = buildMinimalConfig();
65
+ }
66
+ else {
67
+ const result = await buildDetectedConfig(root, initialFlags);
68
+ config = result.config;
69
+ projectDetection = result.detection;
70
+ }
71
+ const integrationFlags = buildIntegrationFlags(initialFlags, config);
72
+ const serialized = yamlStringify(config);
73
+ scanForSecrets(serialized);
74
+ await writeFile(join(root, ".grimoire", "config.yaml"), serialized);
75
+ console.log(` ${chalk.green("created")} .grimoire/config.yaml`);
76
+ if (options.full)
77
+ await runFullConfigSections(root, config);
78
+ return {
79
+ cavemanLevel: config.project.caveman ?? "lite",
80
+ configAgents: config.project.agents ?? [],
81
+ integrationFlags,
82
+ figmaMcpConfigured: config.project.design_tool?.mcp?.name === "figma-dev-mode",
83
+ projectDetection,
84
+ };
85
+ }
86
+ async function loadExistingConfig(root, initialFlags) {
87
+ console.log(` ${chalk.yellow("exists")} .grimoire/config.yaml`);
88
+ const { loadConfig } = await import("../utils/config.js");
89
+ const existing = await loadConfig(root);
90
+ return {
91
+ cavemanLevel: existing.project.caveman ?? "none",
92
+ configAgents: existing.project.agents ?? [],
93
+ integrationFlags: {
94
+ codebaseMemoryMcp: initialFlags.codebaseMemoryMcp ?? existing.project.integrations?.codebase_memory_mcp,
95
+ cavemanPlugin: initialFlags.cavemanPlugin ?? existing.project.integrations?.caveman_plugin,
96
+ },
97
+ figmaMcpConfigured: existing.project.design_tool?.mcp?.name === "figma-dev-mode",
98
+ };
99
+ }
100
+ function printNextSteps(isExistingProject, full) {
101
+ console.log(`\n${chalk.bold.green("Done!")} Grimoire initialized.\n`);
102
+ console.log("Directory structure:");
103
+ console.log(" features/ Gherkin feature files (behavioral specs)");
104
+ console.log(" .grimoire/decisions/ MADR decision records (architectural specs)");
105
+ console.log(" .grimoire/docs/ Project docs, data schema, and context");
106
+ console.log(" .grimoire/changes/ Changes in progress");
107
+ console.log(" .grimoire/archive/ Completed changes\n");
108
+ console.log("Next steps:");
109
+ if (isExistingProject) {
110
+ console.log(" 1. Install codebase-memory-mcp (required for code discovery):");
111
+ console.log(" macOS / Linux: curl -fsSL https://raw.githubusercontent.com/DeusData/codebase-memory-mcp/main/install.sh | bash");
112
+ console.log(" 2. Run /grimoire:discover in your agent to generate conventions files and data schema");
113
+ console.log(" 3. Run /grimoire:audit in your agent to document existing features and decisions\n");
114
+ }
115
+ else {
116
+ console.log(" Run /grimoire:draft in your agent to write your first feature spec\n");
117
+ }
118
+ if (!full) {
119
+ console.log(chalk.dim(" Run `grimoire configure` to set compliance, design tool, LLM models, bug trackers, and testing tools.\n"));
120
+ }
121
+ }
122
+ const SKILL_SUPPORTED = ["claude", "opencode", "codex"];
123
+ const INSTRUCTION_SUPPORTED = ["cursor", "copilot"];
124
+ async function scaffoldProject(root) {
46
125
  for (const dir of GRIMOIRE_DIRS) {
47
- const fullPath = join(root, dir);
48
- await mkdir(fullPath, { recursive: true });
126
+ await mkdir(join(root, dir), { recursive: true });
49
127
  console.log(` ${chalk.green("created")} ${dir}/`);
50
128
  }
51
- // Copy template files
52
129
  for (const [src, dest] of TEMPLATE_FILES) {
53
- const srcPath = join(PACKAGE_ROOT, "templates", src);
54
130
  const destPath = join(root, dest);
55
131
  if (!(await fileExists(destPath))) {
56
- await copyFile(srcPath, destPath);
132
+ await copyFile(join(PACKAGE_ROOT, "templates", src), destPath);
57
133
  console.log(` ${chalk.green("created")} ${dest}`);
58
134
  }
59
135
  else {
60
136
  console.log(` ${chalk.yellow("exists")} ${dest}`);
61
137
  }
62
138
  }
63
- // Generate config.yaml with optional tool detection
64
- const configPath = join(root, ".grimoire", "config.yaml");
65
- let cavemanLevel = "lite";
66
- let configAgents = [];
67
- let integrationFlags = {
68
- codebaseMemoryMcp: options.installCodebaseMemoryMcp,
69
- cavemanPlugin: options.installCavemanPlugin,
70
- };
71
- if (!(await fileExists(configPath))) {
72
- const config = options.noDetect
73
- ? buildMinimalConfig()
74
- : await buildDetectedConfig(root, integrationFlags);
75
- cavemanLevel = config.project.caveman ?? "lite";
76
- configAgents = config.project.agents ?? [];
77
- integrationFlags = {
78
- codebaseMemoryMcp: integrationFlags.codebaseMemoryMcp ??
79
- config.project.integrations?.codebase_memory_mcp,
80
- cavemanPlugin: integrationFlags.cavemanPlugin ??
81
- config.project.integrations?.caveman_plugin,
82
- };
83
- await writeFile(configPath, yamlStringify(config));
84
- console.log(` ${chalk.green("created")} .grimoire/config.yaml`);
85
- }
86
- else {
87
- console.log(` ${chalk.yellow("exists")} .grimoire/config.yaml`);
88
- // Read existing config to get caveman level + agents
89
- const { loadConfig } = await import("../utils/config.js");
90
- const existing = await loadConfig(root);
91
- cavemanLevel = existing.project.caveman ?? "none";
92
- configAgents = existing.project.agents ?? [];
93
- integrationFlags = {
94
- codebaseMemoryMcp: integrationFlags.codebaseMemoryMcp ??
95
- existing.project.integrations?.codebase_memory_mcp,
96
- cavemanPlugin: integrationFlags.cavemanPlugin ??
97
- existing.project.integrations?.caveman_plugin,
98
- };
99
- }
100
- // Merge agents from config + CLI flag (--agent). De-dup.
101
- const allAgents = Array.from(new Set([...configAgents, ...options.agents]));
102
- const SKILL_SUPPORTED = ["claude", "opencode", "codex"];
103
- const INSTRUCTION_SUPPORTED = ["cursor", "copilot"];
139
+ }
140
+ async function setupAgents(root, options, setup) {
141
+ const allAgents = Array.from(new Set([...setup.configAgents, ...options.agents]));
104
142
  const skillAgents = allAgents.filter((a) => SKILL_SUPPORTED.includes(a));
105
143
  const instructionAgents = allAgents.filter((a) => INSTRUCTION_SUPPORTED.includes(a));
106
144
  for (const a of allAgents) {
@@ -108,37 +146,30 @@ export async function initProject(projectPath, options) {
108
146
  console.log(` ${chalk.yellow("unknown")} agent type: ${a} (supported: ${[...SKILL_SUPPORTED, ...INSTRUCTION_SUPPORTED].join(", ")})`);
109
147
  }
110
148
  }
111
- // Generate AGENTS.md (or append grimoire section)
112
- if (!options.skipAgents) {
113
- await setupAgentsFile(root, cavemanLevel);
114
- }
115
- // Install skills to each selected agent's skill directory
116
- if (!options.skipSkills) {
117
- const targets = skillAgents.length > 0 ? skillAgents : ["claude"];
118
- await installSkills(root, targets);
119
- }
120
- // Generate agent-specific instruction files (cursor, copilot)
121
- if (instructionAgents.length > 0) {
149
+ if (!options.skipAgents)
150
+ await setupAgentsFile(root, setup.cavemanLevel);
151
+ if (!options.skipSkills)
152
+ await installSkills(root, skillAgents.length > 0 ? skillAgents : ["claude"]);
153
+ if (instructionAgents.length > 0)
122
154
  await generateAgentFiles(root, PACKAGE_ROOT, instructionAgents, "created");
123
- }
124
- // Set up hooks (Claude Code + git)
125
- if (!options.skipAgents) {
155
+ if (!options.skipAgents)
126
156
  await setupHooks(root);
127
- }
128
- console.log(`\n${chalk.bold.green("Done!")} Grimoire initialized.\n`);
129
- console.log("Directory structure:");
130
- console.log(" features/ Gherkin feature files (behavioral specs)");
131
- console.log(" .grimoire/decisions/ MADR decision records (architectural specs)");
132
- console.log(" .grimoire/docs/ Project docs, data schema, and context");
133
- console.log(" .grimoire/changes/ Changes in progress");
134
- console.log(" .grimoire/archive/ Completed changes\n");
135
- console.log("Next steps:");
136
- console.log(" Edit .grimoire/docs/context.yml to describe your deployment,");
137
- console.log(" related services, and infrastructure.\n");
138
- printIntegrationInstructions(integrationFlags);
157
+ }
158
+ export async function initProject(projectPath, options) {
159
+ const root = join(process.cwd(), projectPath);
160
+ console.log(chalk.bold("Initializing grimoire...\n"));
161
+ await scaffoldProject(root);
162
+ const configPath = join(root, ".grimoire", "config.yaml");
163
+ const initialFlags = { codebaseMemoryMcp: options.installCodebaseMemoryMcp, cavemanPlugin: options.installCavemanPlugin };
164
+ const setup = await fileExists(configPath)
165
+ ? { ...(await loadExistingConfig(root, initialFlags)), projectDetection: null }
166
+ : await createGrimoireConfig(root, options, initialFlags);
167
+ await setupAgents(root, options, setup);
168
+ printNextSteps(!!setup.projectDetection?.name, options.full);
169
+ printIntegrationInstructions({ ...setup.integrationFlags, figmaMcp: setup.figmaMcpConfigured });
139
170
  }
140
171
  function printIntegrationInstructions(flags) {
141
- if (!flags.codebaseMemoryMcp && !flags.cavemanPlugin)
172
+ if (!flags.codebaseMemoryMcp && !flags.cavemanPlugin && !flags.figmaMcp)
142
173
  return;
143
174
  console.log(chalk.bold("Recommended integrations to install:\n"));
144
175
  if (flags.codebaseMemoryMcp) {
@@ -155,6 +186,26 @@ function printIntegrationInstructions(flags) {
155
186
  console.log(` ${chalk.dim("/plugin marketplace add JuliusBrussee/caveman")}`);
156
187
  console.log(` ${chalk.dim("/plugin install caveman@JuliusBrussee/caveman")}\n`);
157
188
  }
189
+ if (flags.figmaMcp) {
190
+ console.log(` ${chalk.cyan("Figma Dev Mode MCP")} — read Figma frames, variables, and components from the AI agent`);
191
+ console.log(" Set your access token in your shell environment:");
192
+ console.log(` ${chalk.dim("export FIGMA_ACCESS_TOKEN=...")}`);
193
+ console.log(" Install the Figma desktop app and enable Dev Mode for full feature access.");
194
+ console.log(` Restart your agent. The MCP server will spawn via the command in config.yaml.\n`);
195
+ }
196
+ }
197
+ const SECRET_PATTERN = /(.*_TOKEN|.*_KEY|.*_SECRET|.*_PASSWORD)\s*[:=]\s*[^$\s].*/i;
198
+ export function scanForSecrets(serialized) {
199
+ for (const line of serialized.split("\n")) {
200
+ const m = line.match(SECRET_PATTERN);
201
+ if (!m)
202
+ continue;
203
+ const value = line.slice(line.search(/[:=]/) + 1).trim();
204
+ if (value.startsWith("${"))
205
+ continue;
206
+ throw new Error(`Refusing to write a secret to .grimoire/config.yaml: ${line.trim()}\n` +
207
+ `Use \${ENV_VAR} references instead, then export the value in your shell.`);
208
+ }
158
209
  }
159
210
  function buildMinimalConfig() {
160
211
  return {
@@ -184,76 +235,29 @@ function buildMinimalConfig() {
184
235
  },
185
236
  };
186
237
  }
187
- async function buildDetectedConfig(root, prefill = {}) {
188
- console.log(chalk.bold("\nDetecting project tools...\n"));
189
- const detections = await detectTools(root);
190
- const config = buildMinimalConfig();
191
- if (detections.length === 0) {
192
- console.log(chalk.dim(" No tools detected. Using minimal config.\n"));
193
- return await askPreferences(config, root, prefill);
194
- }
195
- // Group detections by category and pick highest confidence per category
238
+ function bestByCategory(detections) {
196
239
  const byCategory = new Map();
197
240
  for (const d of detections) {
198
241
  const existing = byCategory.get(d.category);
199
- if (!existing ||
200
- confidenceRank(d.confidence) > confidenceRank(existing.confidence)) {
242
+ if (!existing || confidenceRank(d.confidence) > confidenceRank(existing.confidence)) {
201
243
  byCategory.set(d.category, d);
202
244
  }
203
245
  }
204
- // Display detections
205
- console.log(chalk.bold(" Detected tools:\n"));
206
- for (const cat of CATEGORY_ORDER) {
207
- const label = (CATEGORY_LABELS[cat] ?? cat).padEnd(14);
246
+ return byCategory;
247
+ }
248
+ function applyProjectDetections(config, byCategory) {
249
+ const projectFields = [
250
+ ["language", "language"],
251
+ ["package_manager", "package_manager"],
252
+ ["doc_tool", "doc_tool"],
253
+ ["comment_style", "comment_style"],
254
+ ];
255
+ for (const [cat, field] of projectFields) {
208
256
  const d = byCategory.get(cat);
209
- if (d) {
210
- console.log(` ${label} ${chalk.cyan(d.name.padEnd(16))} ${chalk.dim(`(${d.signal})`)}`);
211
- }
212
- else {
213
- console.log(` ${label} ${chalk.dim("(none detected)")}`);
214
- }
257
+ if (d)
258
+ config.project[field] = d.name;
215
259
  }
216
- // Ask for confirmation
217
- const readline = await import("node:readline/promises");
218
- const rl = readline.createInterface({
219
- input: process.stdin,
220
- output: process.stdout,
221
- });
222
- console.log();
223
- const answer = await rl.question(" Accept detected tools? (Y/n/edit) ");
224
- if (answer.toLowerCase() === "n") {
225
- rl.close();
226
- console.log(chalk.dim(" Skipping tool detection.\n"));
227
- return await askPreferences(config, root, prefill);
228
- }
229
- if (answer.toLowerCase() === "edit") {
230
- await editDetections(rl, byCategory);
231
- }
232
- rl.close();
233
- // Set project-level detections
234
- const langDetection = byCategory.get("language");
235
- if (langDetection) {
236
- config.project.language = langDetection.name;
237
- }
238
- const pkgMgrDetection = byCategory.get("package_manager");
239
- if (pkgMgrDetection) {
240
- config.project.package_manager = pkgMgrDetection.name;
241
- }
242
- const docToolDetection = byCategory.get("doc_tool");
243
- if (docToolDetection) {
244
- config.project.doc_tool = docToolDetection.name;
245
- }
246
- const commentStyleDetection = byCategory.get("comment_style");
247
- if (commentStyleDetection) {
248
- config.project.comment_style = commentStyleDetection.name;
249
- }
250
- // Build tools from confirmed detections (skip project-level categories)
251
- const projectCategories = new Set([
252
- "language",
253
- "package_manager",
254
- "doc_tool",
255
- "comment_style",
256
- ]);
260
+ const projectCategories = new Set(["language", "package_manager", "doc_tool", "comment_style"]);
257
261
  for (const [category, detection] of byCategory) {
258
262
  if (projectCategories.has(category))
259
263
  continue;
@@ -264,46 +268,73 @@ async function buildDetectedConfig(root, prefill = {}) {
264
268
  tool.check_command = detection.check_command;
265
269
  config.tools[category] = tool;
266
270
  }
267
- // Add LLM-based steps if no dedicated security tool was detected
271
+ }
272
+ function applyLlmFallbacks(config, byCategory) {
268
273
  if (!byCategory.has("security")) {
269
- config.tools.security = {
270
- name: "llm",
271
- prompt: "Review these changed files for security vulnerabilities. Tag each finding with OWASP Top 10 category and CWE ID. Check for: SQL injection (CWE-89), XSS (CWE-79), broken auth (CWE-287), insecure crypto (CWE-327), SSRF (CWE-918), path traversal (CWE-22), insecure deserialization (CWE-502), missing access control (CWE-862), CSRF (CWE-352), hardcoded secrets (CWE-798).",
272
- };
274
+ config.tools.security = { name: "llm", prompt: "Review these changed files for security vulnerabilities. Tag each finding with OWASP Top 10 category and CWE ID. Check for: SQL injection (CWE-89), XSS (CWE-79), broken auth (CWE-287), insecure crypto (CWE-327), SSRF (CWE-918), path traversal (CWE-22), insecure deserialization (CWE-502), missing access control (CWE-862), CSRF (CWE-352), hardcoded secrets (CWE-798)." };
273
275
  }
274
- // Add LLM-based dep audit if no dedicated tool was detected
275
276
  if (!byCategory.has("dep_audit")) {
276
- config.tools.dep_audit = {
277
- name: "llm",
278
- prompt: "Review these changed files for newly added dependencies or imports. Flag potential typosquatting (CWE-1357), packages you cannot verify as real, and packages with known security advisories. Check for misspellings (e.g., 'reqeusts' instead of 'requests').",
279
- };
277
+ config.tools.dep_audit = { name: "llm", prompt: "Review these changed files for newly added dependencies or imports. Flag potential typosquatting (CWE-1357), packages you cannot verify as real, and packages with known security advisories. Check for misspellings (e.g., 'reqeusts' instead of 'requests')." };
280
278
  }
281
- // Add LLM-based secret scanning if no dedicated tool was detected
282
279
  if (!byCategory.has("secrets")) {
283
- config.tools.secrets = {
284
- name: "llm",
285
- prompt: "Review these changed files for hardcoded secrets (CWE-798), API keys, passwords, tokens, private keys, or credentials (CWE-312). Flag any string that looks like a secret value rather than a placeholder or environment variable reference.",
286
- };
280
+ config.tools.secrets = { name: "llm", prompt: "Review these changed files for hardcoded secrets (CWE-798), API keys, passwords, tokens, private keys, or credentials (CWE-312). Flag any string that looks like a secret value rather than a placeholder or environment variable reference." };
287
281
  }
288
- // Add LLM-based dead code detection if no dedicated tool was detected
289
282
  if (!byCategory.has("dead_code")) {
290
- config.tools.dead_code = {
291
- name: "llm",
292
- prompt: "Review these changed files for dead code: unused functions, unreachable branches, unused imports, unused variables, and exports that are never imported elsewhere. Only flag code that is clearly dead, not code that might be used dynamically.",
293
- };
283
+ config.tools.dead_code = { name: "llm", prompt: "Review these changed files for dead code: unused functions, unreachable branches, unused imports, unused variables, and exports that are never imported elsewhere. Only flag code that is clearly dead, not code that might be used dynamically." };
294
284
  }
295
- config.tools.best_practices = {
296
- name: "llm",
297
- prompt: "Review these changed files for best practices violations",
298
- };
299
- // Add jscpd for duplicates if not already set
285
+ config.tools.best_practices = { name: "llm", prompt: "Review these changed files for best practices violations" };
300
286
  if (!config.tools.duplicates) {
301
- config.tools.duplicates = {
302
- name: "jscpd",
303
- command: "npx jscpd --reporters console",
304
- };
287
+ config.tools.duplicates = { name: "jscpd", command: "npx jscpd --reporters console" };
305
288
  }
306
- return await askPreferences(config, root);
289
+ }
290
+ async function buildDetectedConfig(root, prefill = {}) {
291
+ console.log(chalk.bold("\nDetecting project tools...\n"));
292
+ const detections = await detectTools(root);
293
+ const config = buildMinimalConfig();
294
+ if (detections.length === 0) {
295
+ console.log(chalk.dim(" No tools detected. Using minimal config.\n"));
296
+ return { config: await askEssentialPreferences(config, root, prefill), detection: null };
297
+ }
298
+ const byCategory = bestByCategory(detections);
299
+ console.log(chalk.bold(" Detected tools:\n"));
300
+ for (const cat of CATEGORY_ORDER) {
301
+ const label = (CATEGORY_LABELS[cat] ?? cat).padEnd(14);
302
+ const d = byCategory.get(cat);
303
+ if (d)
304
+ console.log(` ${label} ${chalk.cyan(d.name.padEnd(16))} ${chalk.dim(`(${d.signal})`)}`);
305
+ else
306
+ console.log(` ${label} ${chalk.dim("(none detected)")}`);
307
+ }
308
+ const readline = await import("node:readline/promises");
309
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
310
+ console.log();
311
+ const answer = await rl.question(" Accept detected tools? (Y/n/edit) ");
312
+ const prefillWithSurface = { ...prefill, detectedSurface: surfaceFromDetection(byCategory.get("surface")) };
313
+ if (answer.toLowerCase() === "n") {
314
+ rl.close();
315
+ console.log(chalk.dim(" Skipping tool detection.\n"));
316
+ return { config: await askEssentialPreferences(config, root, prefillWithSurface), detection: null };
317
+ }
318
+ if (answer.toLowerCase() === "edit")
319
+ await editDetections(rl, byCategory);
320
+ rl.close();
321
+ applyProjectDetections(config, byCategory);
322
+ applyLlmFallbacks(config, byCategory);
323
+ return { config: await askEssentialPreferences(config, root, prefillWithSurface), detection: byCategory.get("language") ?? null };
324
+ }
325
+ const PROMPT_SURFACES = [
326
+ "tui",
327
+ "web",
328
+ "mobile",
329
+ "api",
330
+ "mixed",
331
+ ];
332
+ function surfaceFromDetection(d) {
333
+ if (!d)
334
+ return undefined;
335
+ return PROMPT_SURFACES.includes(d.name)
336
+ ? d.name
337
+ : undefined;
307
338
  }
308
339
  async function editDetections(rl, byCategory) {
309
340
  console.log(chalk.dim("\n For each tool, press Enter to accept, 'n' to skip, or type a custom name.\n"));
@@ -317,7 +348,6 @@ async function editDetections(rl, byCategory) {
317
348
  byCategory.delete(cat);
318
349
  }
319
350
  else if (trimmed && trimmed !== current) {
320
- // User typed a custom name — create a detection with low confidence
321
351
  byCategory.set(cat, {
322
352
  category: cat,
323
353
  name: trimmed,
@@ -325,15 +355,15 @@ async function editDetections(rl, byCategory) {
325
355
  signal: "user input",
326
356
  });
327
357
  }
328
- // else: accept current (no change)
329
358
  }
330
359
  }
331
- async function askPreferences(config, root, prefill = {}) {
360
+ async function askEssentialPreferences(config, root, prefill = {}) {
332
361
  const readline = await import("node:readline/promises");
333
362
  const rl = readline.createInterface({
334
363
  input: process.stdin,
335
364
  output: process.stdout,
336
365
  });
366
+ // 1. AI agents
337
367
  console.log(chalk.bold("\n AI agents:\n"));
338
368
  console.log(chalk.dim(" Which AI coding tools will use these skills?"));
339
369
  console.log(chalk.dim(" Skills install per tool: claude→.claude/skills, opencode→.opencode/skills, codex→.agents/skills."));
@@ -342,270 +372,64 @@ async function askPreferences(config, root, prefill = {}) {
342
372
  const defaultAgents = detectedAgents.length > 0 ? detectedAgents.join(",") : "claude";
343
373
  const agentsAnswer = await rl.question(` AI agents (comma-separated: claude/opencode/codex/cursor/copilot) [${defaultAgents}]: `);
344
374
  const rawAgents = agentsAnswer.trim() || defaultAgents;
345
- const agents = rawAgents
375
+ config.project.agents = rawAgents
346
376
  .split(",")
347
377
  .map((s) => s.trim().toLowerCase())
348
378
  .filter(Boolean);
349
- config.project.agents = agents;
350
- // Recommended integrations
379
+ // 2. Recommended integrations
351
380
  console.log(chalk.bold("\n Recommended integrations:\n"));
352
- console.log(chalk.dim(" These are optional but recommended. Saying yes records the"));
353
- console.log(chalk.dim(" intent in config and prints install commands at the end.\n"));
381
+ console.log(chalk.dim(" These are optional but recommended. Saying yes records the intent"));
382
+ console.log(chalk.dim(" in config and prints install commands at the end.\n"));
354
383
  const integrations = {};
355
384
  if (prefill.codebaseMemoryMcp === undefined) {
356
385
  const cbmAnswer = await rl.question(" Install codebase-memory-mcp (call graphs, code intelligence)? (Y/n) ");
357
- integrations.codebase_memory_mcp = cbmAnswer.trim().toLowerCase() !== "n";
386
+ integrations.codebase_memory_mcp =
387
+ cbmAnswer.trim().toLowerCase() !== "n";
358
388
  }
359
389
  else {
360
390
  integrations.codebase_memory_mcp = prefill.codebaseMemoryMcp;
361
391
  }
362
392
  if (prefill.cavemanPlugin === undefined) {
363
393
  const cavemanPluginAnswer = await rl.question(" Install caveman skill plugin (Claude Code marketplace)? (y/N) ");
364
- integrations.caveman_plugin = cavemanPluginAnswer.trim().toLowerCase() === "y";
394
+ integrations.caveman_plugin =
395
+ cavemanPluginAnswer.trim().toLowerCase() === "y";
365
396
  }
366
397
  else {
367
398
  integrations.caveman_plugin = prefill.cavemanPlugin;
368
399
  }
369
400
  config.project.integrations = integrations;
370
- console.log(chalk.bold("\n Project preferences:\n"));
401
+ // 3. Surface
402
+ await askSurface(rl, config, prefill.detectedSurface);
403
+ // 4. Caveman level
371
404
  const currentCaveman = config.project.caveman ?? "lite";
372
405
  const cavemanAnswer = await rl.question(` Token optimization (caveman)? (none/lite/full/ultra) [${currentCaveman}]: `);
373
- if (cavemanAnswer.trim()) {
374
- const level = cavemanAnswer.trim().toLowerCase();
375
- config.project.caveman = (level === "none" ? "none" : level);
376
- }
377
- else {
378
- config.project.caveman = currentCaveman;
379
- }
406
+ config.project.caveman = (cavemanAnswer.trim() ? cavemanAnswer.trim().toLowerCase() : currentCaveman);
407
+ // 5. Commit style
380
408
  const commitAnswer = await rl.question(` Commit style? (conventional/angular/custom) [${config.project.commit_style}]: `);
381
409
  if (commitAnswer.trim()) {
382
410
  config.project.commit_style = commitAnswer.trim();
383
411
  }
384
- const currentDocTool = config.project.doc_tool ?? "none";
385
- const docToolAnswer = await rl.question(` Doc generator? (sphinx/mkdocs/typedoc/jsdoc/none) [${currentDocTool}]: `);
386
- if (docToolAnswer.trim()) {
387
- config.project.doc_tool =
388
- docToolAnswer.trim() === "none" ? undefined : docToolAnswer.trim();
389
- }
390
- const currentCommentStyle = config.project.comment_style ?? "none";
391
- const commentAnswer = await rl.question(` Comment/docstring style? (google/numpy/sphinx/jsdoc/tsdoc/none) [${currentCommentStyle}]: `);
392
- if (commentAnswer.trim()) {
393
- config.project.comment_style =
394
- commentAnswer.trim() === "none" ? undefined : commentAnswer.trim();
395
- }
396
- // Design tool preferences
397
- console.log(chalk.bold("\n Front-end design:\n"));
398
- console.log(chalk.dim(" Where do UI/UX designs live? This helps grimoire reference"));
399
- console.log(chalk.dim(" designs during requirements elicitation.\n"));
400
- const designToolAnswer = await rl.question(` Design tool? (figma/storybook/sketch/zeplin/none) [none]: `);
401
- const designTool = designToolAnswer.trim().toLowerCase();
402
- if (designTool && designTool !== "none") {
403
- const designPathAnswer = await rl.question(` Local design assets path? (e.g., designs/, docs/wireframes/) [none]: `);
404
- const designUrlAnswer = await rl.question(` Design project URL? (e.g., Figma project link) [none]: `);
405
- config.project.design_tool = {
406
- name: designTool,
407
- path: designPathAnswer.trim() && designPathAnswer.trim() !== "none"
408
- ? designPathAnswer.trim()
409
- : undefined,
410
- url: designUrlAnswer.trim() && designUrlAnswer.trim() !== "none"
411
- ? designUrlAnswer.trim()
412
- : undefined,
413
- };
414
- }
415
- // LLM agent preferences
416
- console.log(chalk.bold("\n AI agent preferences:\n"));
417
- const currentThinkCmd = config.llm.thinking.command;
418
- const thinkAnswer = await rl.question(` Thinking agent (planning, review)? (claude/codex/cursor/custom) [${currentThinkCmd}]: `);
419
- if (thinkAnswer.trim()) {
420
- config.llm.thinking.command = thinkAnswer.trim();
421
- }
422
- const currentThinkModel = config.llm.thinking.model ?? "default";
423
- const thinkModelAnswer = await rl.question(` Thinking model? (opus/sonnet/o3/auto) [${currentThinkModel}]: `);
424
- if (thinkModelAnswer.trim() && thinkModelAnswer.trim() !== "default") {
425
- config.llm.thinking.model =
426
- thinkModelAnswer.trim() === "auto" ? undefined : thinkModelAnswer.trim();
427
- }
428
- const currentCodeCmd = config.llm.coding.command;
429
- const codeAnswer = await rl.question(` Coding agent (apply, implement)? (claude/codex/cursor/custom) [${currentCodeCmd}]: `);
430
- if (codeAnswer.trim()) {
431
- config.llm.coding.command = codeAnswer.trim();
432
- }
433
- const currentCodeModel = config.llm.coding.model ?? "default";
434
- const codeModelAnswer = await rl.question(` Coding model? (sonnet/opus/gpt-4.1/auto) [${currentCodeModel}]: `);
435
- if (codeModelAnswer.trim() && codeModelAnswer.trim() !== "default") {
436
- config.llm.coding.model =
437
- codeModelAnswer.trim() === "auto" ? undefined : codeModelAnswer.trim();
438
- }
439
- console.log(chalk.bold("\n Security & compliance:\n"));
440
- // Compliance frameworks
441
- console.log(chalk.dim(" Which compliance frameworks apply to this project?"));
442
- console.log(chalk.dim(" Options: owasp, pci-dss, hipaa, soc2, gdpr, iso27001, or Enter to skip.\n"));
443
- const complianceAnswer = await rl.question(` Compliance frameworks (comma-separated) [none]: `);
444
- if (complianceAnswer.trim() && complianceAnswer.trim().toLowerCase() !== "none") {
445
- config.project.compliance = complianceAnswer
446
- .split(",")
447
- .map((s) => s.trim().toLowerCase())
448
- .filter(Boolean);
449
- }
450
- // Dependency audit tool preference
451
- const currentDepAudit = config.tools.dep_audit?.name ?? "auto";
452
- const depAuditAnswer = await rl.question(` Dep audit tool? (npm-audit/pip-audit/safety/yarn-audit/pnpm-audit/none/auto) [${currentDepAudit}]: `);
453
- if (depAuditAnswer.trim() && depAuditAnswer.trim() !== "auto") {
454
- if (depAuditAnswer.trim() === "none") {
455
- delete config.tools.dep_audit;
456
- // Remove from checks
457
- config.checks = config.checks.filter((c) => c !== "dep_audit");
458
- }
459
- else {
460
- const depAuditCommands = {
461
- "npm-audit": "npm audit --audit-level=high",
462
- "pip-audit": "pip-audit",
463
- safety: "safety check",
464
- "yarn-audit": "yarn audit --level high",
465
- "pnpm-audit": "pnpm audit --audit-level=high",
466
- };
467
- config.tools.dep_audit = {
468
- name: depAuditAnswer.trim(),
469
- check_command: depAuditCommands[depAuditAnswer.trim()] ?? depAuditAnswer.trim(),
470
- };
471
- }
472
- }
473
- // Secret scanning tool preference
474
- const currentSecrets = config.tools.secrets?.name ?? "auto";
475
- const secretsAnswer = await rl.question(` Secret scanner? (detect-secrets/gitleaks/trufflehog/none/auto) [${currentSecrets}]: `);
476
- if (secretsAnswer.trim() && secretsAnswer.trim() !== "auto") {
477
- if (secretsAnswer.trim() === "none") {
478
- delete config.tools.secrets;
479
- // Remove from checks
480
- config.checks = config.checks.filter((c) => c !== "secrets");
481
- }
482
- else {
483
- const secretCommands = {
484
- "detect-secrets": "detect-secrets scan --baseline .secrets.baseline",
485
- gitleaks: "gitleaks detect --no-git",
486
- trufflehog: "trufflehog filesystem . --no-update",
487
- };
488
- config.tools.secrets = {
489
- name: secretsAnswer.trim(),
490
- check_command: secretCommands[secretsAnswer.trim()] ?? secretsAnswer.trim(),
491
- };
492
- }
493
- }
494
- console.log(chalk.bold("\n Code quality tools:\n"));
495
- // Dead code detection tool preference
496
- const currentDeadCode = config.tools.dead_code?.name ?? "auto";
497
- const deadCodeAnswer = await rl.question(` Dead code finder? (knip/ts-prune/vulture/deadcode/none/auto) [${currentDeadCode}]: `);
498
- if (deadCodeAnswer.trim() && deadCodeAnswer.trim() !== "auto") {
499
- if (deadCodeAnswer.trim() === "none") {
500
- delete config.tools.dead_code;
501
- config.checks = config.checks.filter((c) => c !== "dead_code");
502
- }
503
- else {
504
- const deadCodeCommands = {
505
- knip: "npx knip",
506
- "ts-prune": "npx ts-prune",
507
- vulture: "vulture .",
508
- deadcode: "deadcode ./...",
509
- };
510
- config.tools.dead_code = {
511
- name: deadCodeAnswer.trim(),
512
- command: deadCodeCommands[deadCodeAnswer.trim()] ?? deadCodeAnswer.trim(),
513
- };
514
- }
515
- }
516
- // Bug tracking & testing tools
517
- console.log(chalk.bold("\n Bug tracking & testing:\n"));
518
- config.bug_trackers = await askBugTrackers(rl);
519
- config.testing_tools = await askTestingTools(rl);
412
+ // 6. Design tool + brand capture
413
+ await runSections(rl, config, root, ["design"]);
520
414
  rl.close();
521
415
  console.log();
522
416
  return config;
523
417
  }
524
- const BUG_TRACKER_MCP = {
525
- jira: {
526
- display: "Atlassian (Jira + Confluence)",
527
- url: "https://mcp.atlassian.com/v1/sse",
528
- transport: "sse",
529
- },
530
- linear: {
531
- display: "Linear",
532
- url: "https://mcp.linear.app/mcp",
533
- transport: "http",
534
- },
535
- github: {
536
- display: "GitHub Issues",
537
- command: "npx",
538
- args: ["-y", "@modelcontextprotocol/server-github"],
539
- },
540
- };
541
- const TESTING_TOOL_MCP = {
542
- playwright: {
543
- display: "Playwright",
544
- command: "npx",
545
- args: ["-y", "@playwright/mcp@latest"],
546
- },
547
- };
548
- async function askBugTrackers(rl) {
549
- const trackers = [];
550
- console.log(chalk.dim(" Where do bug reports live? Add one or more trackers."));
551
- console.log(chalk.dim(" Options: jira, linear, github, other, or press Enter to skip.\n"));
552
- let adding = true;
553
- while (adding) {
554
- const answer = await rl.question(` Bug tracker${trackers.length > 0 ? " (another, or Enter to finish)" : ""}? `);
555
- const trimmed = answer.trim().toLowerCase();
556
- if (!trimmed) {
557
- adding = false;
558
- continue;
559
- }
560
- const known = BUG_TRACKER_MCP[trimmed];
561
- const tracker = { name: trimmed };
562
- if (known) {
563
- const installAnswer = await rl.question(` Install ${known.display} MCP server? (Y/n) `);
564
- if (installAnswer.trim().toLowerCase() !== "n") {
565
- tracker.mcp = {
566
- name: known.display.toLowerCase().replace(/[^a-z0-9]+/g, "-"),
567
- command: known.command,
568
- args: known.args,
569
- url: known.url,
570
- transport: known.transport,
571
- };
572
- console.log(chalk.green(` ✓ ${known.display} MCP configured`));
573
- }
574
- }
575
- trackers.push(tracker);
418
+ async function askSurface(rl, config, detected) {
419
+ const prompt = detected
420
+ ? ` Project surface: ${detected} — confirm or override (tui/web/mobile/api/mixed/Enter to accept): `
421
+ : ` Project surface? (tui/web/mobile/api/mixed/skip) `;
422
+ const answer = (await rl.question(prompt)).trim().toLowerCase();
423
+ if (!answer) {
424
+ if (detected)
425
+ config.project.surface = detected;
426
+ return;
576
427
  }
577
- return trackers;
578
- }
579
- async function askTestingTools(rl) {
580
- const tools = [];
581
- console.log(chalk.dim("\n What testing tools do your testers use? Add one or more."));
582
- console.log(chalk.dim(" Options: playwright, cypress, selenium, postman, other, or Enter to skip.\n"));
583
- let adding = true;
584
- while (adding) {
585
- const answer = await rl.question(` Testing tool${tools.length > 0 ? " (another, or Enter to finish)" : ""}? `);
586
- const trimmed = answer.trim().toLowerCase();
587
- if (!trimmed) {
588
- adding = false;
589
- continue;
590
- }
591
- const purposeAnswer = await rl.question(` Purpose? (e2e/integration/performance/api/general) [general]: `);
592
- const purpose = purposeAnswer.trim().toLowerCase() || "general";
593
- const tool = { name: trimmed, purpose };
594
- const known = TESTING_TOOL_MCP[trimmed];
595
- if (known) {
596
- const installAnswer = await rl.question(` Install ${known.display} MCP server? (Y/n) `);
597
- if (installAnswer.trim().toLowerCase() !== "n") {
598
- tool.mcp = {
599
- name: trimmed,
600
- command: known.command,
601
- args: known.args,
602
- };
603
- console.log(chalk.green(` ✓ ${known.display} MCP configured`));
604
- }
605
- }
606
- tools.push(tool);
428
+ if (answer === "skip")
429
+ return;
430
+ if (PROMPT_SURFACES.includes(answer)) {
431
+ config.project.surface = answer;
607
432
  }
608
- return tools;
609
433
  }
610
434
  function confidenceRank(c) {
611
435
  return c === "high" ? 3 : c === "medium" ? 2 : 1;