@groupby/ai-dev 0.4.1 → 0.5.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.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # @groupby/ai-dev
2
2
 
3
- Interactive CLI installer for Rezolve Ai development skills.
3
+ Interactive CLI installer for Rezolve Ai development skills, prompts, and resources.
4
4
 
5
- Discover, browse, and install shared AI skills (SKILL.md files) into any project with automatic client detection for **GitHub Copilot**, **Claude Code**, and **Codex**.
5
+ Discover, browse, and install shared AI skills (SKILL.md files), team prompts, team resources, and other team content folders into any project with automatic client detection for **GitHub Copilot**, **Claude Code**, and **Codex**.
6
6
 
7
7
  ## Installation
8
8
 
@@ -28,14 +28,14 @@ npm install --save-dev @groupby/ai-dev
28
28
  npx @groupby/ai-dev
29
29
  ```
30
30
 
31
- Walks you through selecting and installing skills via an interactive prompt.
31
+ Walks you through selecting and installing AI content via interactive prompts.
32
32
 
33
- ### List available skills and teams
33
+ ### List available content
34
34
 
35
35
  ```sh
36
36
  npx @groupby/ai-dev list # List everything
37
37
  npx @groupby/ai-dev list skills # List skills only
38
- npx @groupby/ai-dev list teams # List teams only
38
+ npx @groupby/ai-dev list teams # List teams and content counts
39
39
  ```
40
40
 
41
41
  ### Install a specific skill
@@ -44,20 +44,26 @@ npx @groupby/ai-dev list teams # List teams only
44
44
  npx @groupby/ai-dev install skill <skill-name>
45
45
  ```
46
46
 
47
- ### Install all skills for a team
47
+ ### Install all team content
48
48
 
49
49
  ```sh
50
50
  npx @groupby/ai-dev install team <team-name>
51
51
  ```
52
52
 
53
- When installing a team's skills, you will be prompted to optionally include skills from the shared library as well.
53
+ This installs the team's skills, prompts, and resources. A team's `prompts/`
54
+ folder lands beside `skills/` under the AI directory, for example
55
+ `docs/ai/prompts/`. A team's `resources/` folder installs to
56
+ `docs/ai/resources/`. Other direct team content folders follow the same pattern.
57
+ Installing a specific team or toolset skill also installs resources from that
58
+ skill's owning team or toolset. You will also be prompted to optionally include
59
+ skills from the shared library.
54
60
 
55
61
  ### Options
56
62
 
57
63
  | Flag | Description |
58
64
  | --- | --- |
59
65
  | `--target <dir>` | Install into a specific directory (defaults to cwd) |
60
- | `--ai-dir <path>` | Override the AI skills/resources directory (defaults to `docs/ai`) |
66
+ | `--ai-dir <path>` | Override the AI content directory (defaults to `docs/ai`) |
61
67
  | `--dry-run` | Preview what would be installed without writing files |
62
68
  | `--force` | Overwrite existing files without prompting |
63
69
  | `--skip-existing` | Skip files that already exist |
@@ -66,12 +72,15 @@ When installing a team's skills, you will be prompted to optionally include skil
66
72
  Parent-directory segments such as `../shared-ai` are supported so teams can keep
67
73
  AI docs in a workspace-level or shared location while client stubs stay in the
68
74
  target project. Use `--dry-run` first when trying a new install layout.
75
+ Direct `install` subcommands use these options instead of prompting for a
76
+ location. Interactive mode (`npx @groupby/ai-dev`) prompts for the AI content
77
+ directory before showing the final install summary.
69
78
 
70
79
  ## How it works
71
80
 
72
- 1. **Discovery** — The CLI scans bundled `skills/` and `teams/` directories for `SKILL.md` files and reads their YAML frontmatter (name, description, etc.).
81
+ 1. **Discovery** — The CLI scans bundled `skills/`, `teams/`, and `toolsets/` directories for `SKILL.md` files, team prompts, team resources, and other team content folders.
73
82
  2. **Client detection** — It checks the target project for known config directories (`.github/`, `.claude/`, `.agents/`) to determine which LLM clients are in use.
74
- 3. **Installation** — Each selected skill is copied into `<ai-dir>/skills/<name>/` (default: `docs/ai/skills/<name>/`) and a stub `SKILL.md` is generated in each client's skills directory pointing back to the full skill file.
83
+ 3. **Installation** — Each selected skill is copied into `<ai-dir>/skills/<name>/` (default: `docs/ai/skills/<name>/`) and a stub `SKILL.md` is generated in each client's skills directory pointing back to the full skill file. Team prompts, resources, and other content folders are copied beside `skills/`, such as `<ai-dir>/prompts/` and `<ai-dir>/resources/`. Installing a skill also installs resources from its owning team or toolset.
75
84
  4. **Conflict handling** — If a file already exists and its content differs, you are prompted to overwrite or skip (unless `--force` or `--skip-existing` is set).
76
85
  5. **Path patching** — When `--ai-dir` is set to a non-default value, any `docs/ai/` references inside installed `.md` files are replaced with the custom path so LLMs can find resource files at runtime.
77
86
 
package/dist/index.js CHANGED
@@ -1,11 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
+ import fs4 from "fs";
5
+ import path5 from "path";
6
+ import { fileURLToPath as fileURLToPath2 } from "url";
4
7
  import { Command } from "commander";
5
8
 
6
9
  // src/commands/list.ts
7
10
  import chalk from "chalk";
8
- import fs2 from "fs-extra";
11
+ import fg2 from "fast-glob";
9
12
 
10
13
  // src/lib/discovery.ts
11
14
  import path from "path";
@@ -72,20 +75,51 @@ async function discoverSkills() {
72
75
  }
73
76
  return skills;
74
77
  }
78
+ async function discoverTeamAssetFolders() {
79
+ const teamDirs = await fg("teams/*", {
80
+ cwd: PACKAGE_ROOT,
81
+ absolute: true,
82
+ onlyDirectories: true
83
+ });
84
+ const assetFolders = [];
85
+ for (const teamDir of teamDirs) {
86
+ const teamName = path.basename(teamDir);
87
+ const entries = await fs.readdir(teamDir, { withFileTypes: true });
88
+ for (const entry of entries) {
89
+ if (!entry.isDirectory() || entry.name === "skills") continue;
90
+ assetFolders.push({
91
+ name: entry.name,
92
+ sourcePath: path.join(teamDir, entry.name),
93
+ teamName
94
+ });
95
+ }
96
+ }
97
+ return assetFolders.sort(
98
+ (a, b) => a.teamName.localeCompare(b.teamName) || a.name.localeCompare(b.name)
99
+ );
100
+ }
75
101
  async function discoverTeams() {
76
- const skills = await discoverSkills();
102
+ const [skills, assetFolders] = await Promise.all([
103
+ discoverSkills(),
104
+ discoverTeamAssetFolders()
105
+ ]);
77
106
  const teamMap = /* @__PURE__ */ new Map();
107
+ const ensureTeam = (name) => {
108
+ const existing = teamMap.get(name);
109
+ if (existing) return existing;
110
+ const team = { name, skills: [], assetFolders: [] };
111
+ teamMap.set(name, team);
112
+ return team;
113
+ };
78
114
  for (const skill of skills) {
79
115
  if (skill.sourceType === "team" && skill.teamName) {
80
- const existing = teamMap.get(skill.teamName) || [];
81
- existing.push(skill);
82
- teamMap.set(skill.teamName, existing);
116
+ ensureTeam(skill.teamName).skills.push(skill);
83
117
  }
84
118
  }
85
- return Array.from(teamMap.entries()).map(([name, skills2]) => ({
86
- name,
87
- skills: skills2
88
- }));
119
+ for (const folder of assetFolders) {
120
+ ensureTeam(folder.teamName).assetFolders.push(folder);
121
+ }
122
+ return Array.from(teamMap.values()).sort((a, b) => a.name.localeCompare(b.name));
89
123
  }
90
124
  async function discoverToolsets() {
91
125
  const skills = await discoverSkills();
@@ -102,8 +136,11 @@ async function discoverToolsets() {
102
136
  const resourcesPath = path.join(PACKAGE_ROOT, "toolsets", name, "resources");
103
137
  let hasResources = false;
104
138
  if (await fs.pathExists(resourcesPath)) {
105
- const entries = await fs.readdir(resourcesPath);
106
- hasResources = entries.some((e) => e !== "README.md");
139
+ const resourceFiles = await fg("**/*", {
140
+ cwd: resourcesPath,
141
+ onlyFiles: true
142
+ });
143
+ hasResources = resourceFiles.length > 0;
107
144
  }
108
145
  toolsets.push({ name, skills: toolsetSkills, resourcesPath, hasResources });
109
146
  }
@@ -120,6 +157,48 @@ async function findToolset(name) {
120
157
  );
121
158
  }
122
159
 
160
+ // src/lib/content-types.ts
161
+ var PROMPTS_FOLDER = "prompts";
162
+ var RESOURCES_FOLDER = "resources";
163
+ function isPromptFolder(folderName) {
164
+ return folderName === PROMPTS_FOLDER;
165
+ }
166
+ function isResourceFolder(folderName) {
167
+ return folderName === RESOURCES_FOLDER;
168
+ }
169
+ function formatTeamContents(team) {
170
+ const parts = [`${team.skills.length} skill${team.skills.length !== 1 ? "s" : ""}`];
171
+ const promptCount = team.assetFolders.filter((folder) => isPromptFolder(folder.name)).length;
172
+ const resourceCount = team.assetFolders.filter((folder) => isResourceFolder(folder.name)).length;
173
+ const genericCount = team.assetFolders.length - promptCount - resourceCount;
174
+ if (promptCount > 0) {
175
+ parts.push(`${promptCount} prompt${promptCount !== 1 ? "s" : ""}`);
176
+ }
177
+ if (resourceCount > 0) {
178
+ parts.push(`${resourceCount} resource${resourceCount !== 1 ? "s" : ""}`);
179
+ }
180
+ if (genericCount > 0) {
181
+ parts.push(`${genericCount} content folder${genericCount !== 1 ? "s" : ""}`);
182
+ }
183
+ return parts.join(", ");
184
+ }
185
+ function partitionTeamAssetFolders(folders) {
186
+ const prompts = [];
187
+ const resources = [];
188
+ const generic = [];
189
+ for (const folder of folders) {
190
+ const folderName = "name" in folder ? folder.name : folder.folder;
191
+ if (isPromptFolder(folderName)) {
192
+ prompts.push(folder);
193
+ } else if (isResourceFolder(folderName)) {
194
+ resources.push(folder);
195
+ } else {
196
+ generic.push(folder);
197
+ }
198
+ }
199
+ return { prompts, resources, generic };
200
+ }
201
+
123
202
  // src/commands/list.ts
124
203
  function formatTeamName(name) {
125
204
  return name.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
@@ -128,6 +207,13 @@ function truncate(str, max) {
128
207
  if (str.length <= max) return str;
129
208
  return str.slice(0, max - 3) + "...";
130
209
  }
210
+ async function countResourceFiles(resourcesPath) {
211
+ const files = await fg2("**/*", {
212
+ cwd: resourcesPath,
213
+ onlyFiles: true
214
+ });
215
+ return files.length;
216
+ }
131
217
  async function listSkills() {
132
218
  const skills = await discoverSkills();
133
219
  if (skills.length === 0) {
@@ -182,7 +268,7 @@ async function listTeams() {
182
268
  console.log(chalk.bold("\nTeams"));
183
269
  for (const t of teams) {
184
270
  console.log(
185
- ` ${chalk.cyan(formatTeamName(t.name).padEnd(24))} ${t.skills.length} skill${t.skills.length !== 1 ? "s" : ""}`
271
+ ` ${chalk.cyan(formatTeamName(t.name).padEnd(24))} ${formatTeamContents(t)}`
186
272
  );
187
273
  }
188
274
  console.log();
@@ -197,8 +283,7 @@ async function listToolsets() {
197
283
  for (const t of toolsets) {
198
284
  const parts = [`${t.skills.length} skill${t.skills.length !== 1 ? "s" : ""}`];
199
285
  if (t.hasResources) {
200
- const entries = await fs2.readdir(t.resourcesPath);
201
- const count = entries.filter((e) => e !== "README.md").length;
286
+ const count = await countResourceFiles(t.resourcesPath);
202
287
  parts.push(`${count} resource${count !== 1 ? "s" : ""}`);
203
288
  }
204
289
  console.log(` ${chalk.cyan(t.name.padEnd(24))} ${parts.join(", ")}`);
@@ -212,7 +297,7 @@ async function listAll() {
212
297
  console.log(chalk.bold("Teams"));
213
298
  for (const t of teams) {
214
299
  console.log(
215
- ` ${chalk.cyan(formatTeamName(t.name).padEnd(24))} ${t.skills.length} skill${t.skills.length !== 1 ? "s" : ""}`
300
+ ` ${chalk.cyan(formatTeamName(t.name).padEnd(24))} ${formatTeamContents(t)}`
216
301
  );
217
302
  }
218
303
  console.log();
@@ -223,8 +308,7 @@ async function listAll() {
223
308
  for (const t of toolsets) {
224
309
  const parts = [`${t.skills.length} skill${t.skills.length !== 1 ? "s" : ""}`];
225
310
  if (t.hasResources) {
226
- const entries = await fs2.readdir(t.resourcesPath);
227
- const count = entries.filter((e) => e !== "README.md").length;
311
+ const count = await countResourceFiles(t.resourcesPath);
228
312
  parts.push(`${count} resource${count !== 1 ? "s" : ""}`);
229
313
  }
230
314
  console.log(` ${chalk.cyan(t.name.padEnd(24))} ${parts.join(", ")}`);
@@ -233,9 +317,9 @@ async function listAll() {
233
317
  }
234
318
  }
235
319
  function registerListCommand(program2) {
236
- const list = program2.command("list").description("List available skills and teams");
320
+ const list = program2.command("list").description("List available skills, teams, and toolsets");
237
321
  list.command("skills").description("List all available skills").action(listSkills);
238
- list.command("teams").description("List teams and their skill counts").action(listTeams);
322
+ list.command("teams").description("List teams with skill and content counts").action(listTeams);
239
323
  list.command("toolsets").description("List toolsets with skill and resource counts").action(listToolsets);
240
324
  list.action(listAll);
241
325
  }
@@ -246,7 +330,7 @@ import chalk3 from "chalk";
246
330
 
247
331
  // src/lib/clients.ts
248
332
  import path2 from "path";
249
- import fs3 from "fs-extra";
333
+ import fs2 from "fs-extra";
250
334
  var ALL_CLIENTS = [
251
335
  { name: "Copilot", skillsDir: ".github/skills", detectDir: ".github" },
252
336
  { name: "Claude Code", skillsDir: ".claude/skills", detectDir: ".claude" },
@@ -256,7 +340,7 @@ async function detectClients(targetDir) {
256
340
  const detected = [];
257
341
  for (const client of ALL_CLIENTS) {
258
342
  const dirPath = path2.join(targetDir, client.detectDir);
259
- if (await fs3.pathExists(dirPath)) {
343
+ if (await fs2.pathExists(dirPath)) {
260
344
  detected.push(client);
261
345
  }
262
346
  }
@@ -265,21 +349,22 @@ async function detectClients(targetDir) {
265
349
 
266
350
  // src/lib/installer.ts
267
351
  import path3 from "path";
268
- import fs4 from "fs-extra";
352
+ import fs3 from "fs-extra";
353
+ import fg3 from "fast-glob";
269
354
  var DEFAULT_AI_DIR = "docs/ai";
270
355
  async function handleFile(srcPath, destPath, options, result, onConflict, contentOverride) {
271
356
  const relativeDest = path3.relative(options.targetDir, destPath);
272
- if (await fs4.pathExists(destPath)) {
273
- const existingContent = await fs4.readFile(destPath, "utf-8");
274
- const newContent = contentOverride ?? await fs4.readFile(srcPath, "utf-8");
357
+ if (await fs3.pathExists(destPath)) {
358
+ const existingContent = await fs3.readFile(destPath, "utf-8");
359
+ const newContent = contentOverride ?? await fs3.readFile(srcPath, "utf-8");
275
360
  if (existingContent === newContent) {
276
361
  result.skipped.push(relativeDest);
277
362
  return;
278
363
  }
279
364
  if (options.force) {
280
365
  if (!options.dryRun) {
281
- await fs4.ensureDir(path3.dirname(destPath));
282
- await fs4.writeFile(destPath, newContent);
366
+ await fs3.ensureDir(path3.dirname(destPath));
367
+ await fs3.writeFile(destPath, newContent);
283
368
  }
284
369
  result.overwritten.push(relativeDest);
285
370
  } else if (options.skipExisting) {
@@ -288,8 +373,8 @@ async function handleFile(srcPath, destPath, options, result, onConflict, conten
288
373
  const choice = await onConflict(relativeDest);
289
374
  if (choice === "overwrite") {
290
375
  if (!options.dryRun) {
291
- await fs4.ensureDir(path3.dirname(destPath));
292
- await fs4.writeFile(destPath, newContent);
376
+ await fs3.ensureDir(path3.dirname(destPath));
377
+ await fs3.writeFile(destPath, newContent);
293
378
  }
294
379
  result.overwritten.push(relativeDest);
295
380
  } else {
@@ -300,11 +385,11 @@ async function handleFile(srcPath, destPath, options, result, onConflict, conten
300
385
  }
301
386
  } else {
302
387
  if (!options.dryRun) {
303
- await fs4.ensureDir(path3.dirname(destPath));
388
+ await fs3.ensureDir(path3.dirname(destPath));
304
389
  if (contentOverride) {
305
- await fs4.writeFile(destPath, contentOverride);
390
+ await fs3.writeFile(destPath, contentOverride);
306
391
  } else {
307
- await fs4.copy(srcPath, destPath);
392
+ await fs3.copy(srcPath, destPath);
308
393
  }
309
394
  }
310
395
  result.created.push(relativeDest);
@@ -313,9 +398,22 @@ async function handleFile(srcPath, destPath, options, result, onConflict, conten
313
398
  async function patchContent(srcPath, aiDir) {
314
399
  if (aiDir === DEFAULT_AI_DIR) return void 0;
315
400
  if (!srcPath.endsWith(".md")) return void 0;
316
- const content = await fs4.readFile(srcPath, "utf-8");
401
+ const content = await fs3.readFile(srcPath, "utf-8");
317
402
  return content.replaceAll(`${DEFAULT_AI_DIR}/`, `${aiDir}/`);
318
403
  }
404
+ async function installDirectoryFiles(sourceDir, destDir, options, result, onConflict) {
405
+ const files = await fg3("**/*", {
406
+ cwd: sourceDir,
407
+ onlyFiles: true,
408
+ dot: true
409
+ });
410
+ for (const file of files) {
411
+ const srcFile = path3.join(sourceDir, file);
412
+ const destFile = path3.join(destDir, file);
413
+ const contentOverride = await patchContent(srcFile, options.aiDir);
414
+ await handleFile(srcFile, destFile, options, result, onConflict, contentOverride);
415
+ }
416
+ }
319
417
  async function installSkill(skill, options, onConflict) {
320
418
  const result = {
321
419
  skill: skill.name,
@@ -324,10 +422,10 @@ async function installSkill(skill, options, onConflict) {
324
422
  overwritten: []
325
423
  };
326
424
  const destFolder = path3.join(options.targetDir, options.aiDir, "skills", skill.name);
327
- const sourceFiles = await fs4.readdir(skill.sourcePath);
425
+ const sourceFiles = await fs3.readdir(skill.sourcePath);
328
426
  for (const file of sourceFiles) {
329
427
  const srcFile = path3.join(skill.sourcePath, file);
330
- const stat = await fs4.stat(srcFile);
428
+ const stat = await fs3.stat(srcFile);
331
429
  if (stat.isFile()) {
332
430
  const destFile = path3.join(destFolder, file);
333
431
  const contentOverride = await patchContent(srcFile, options.aiDir);
@@ -353,6 +451,22 @@ async function installSkills(skills, options, onConflict) {
353
451
  }
354
452
  return results;
355
453
  }
454
+ async function installTeamAssetFolders(assetFolders, options, onConflict) {
455
+ const results = [];
456
+ for (const folder of assetFolders) {
457
+ const result = {
458
+ team: folder.teamName,
459
+ folder: folder.name,
460
+ created: [],
461
+ skipped: [],
462
+ overwritten: []
463
+ };
464
+ const destFolder = path3.join(options.targetDir, options.aiDir, folder.name);
465
+ await installDirectoryFiles(folder.sourcePath, destFolder, options, result, onConflict);
466
+ results.push(result);
467
+ }
468
+ return results;
469
+ }
356
470
  async function installResources(toolset, options, onConflict) {
357
471
  const result = {
358
472
  toolset: toolset.name,
@@ -361,24 +475,47 @@ async function installResources(toolset, options, onConflict) {
361
475
  overwritten: []
362
476
  };
363
477
  if (!toolset.hasResources) return result;
364
- const entries = await fs4.readdir(toolset.resourcesPath);
365
- const resourceFiles = entries.filter((e) => e !== "README.md");
366
478
  const destDir = path3.join(options.targetDir, options.aiDir, "resources");
367
- const fileResult = { skill: toolset.name, created: [], skipped: [], overwritten: [] };
368
- for (const file of resourceFiles) {
369
- const srcFile = path3.join(toolset.resourcesPath, file);
370
- const stat = await fs4.stat(srcFile);
371
- if (!stat.isFile()) continue;
372
- const destFile = path3.join(destDir, file);
373
- const contentOverride = await patchContent(srcFile, options.aiDir);
374
- await handleFile(srcFile, destFile, options, fileResult, onConflict, contentOverride);
375
- }
479
+ const fileResult = { created: [], skipped: [], overwritten: [] };
480
+ await installDirectoryFiles(toolset.resourcesPath, destDir, options, fileResult, onConflict);
376
481
  result.created = fileResult.created;
377
482
  result.skipped = fileResult.skipped;
378
483
  result.overwritten = fileResult.overwritten;
379
484
  return result;
380
485
  }
381
486
 
487
+ // src/lib/skill-resource-dependencies.ts
488
+ function findTeamResourceFoldersForSkills(skills, teams) {
489
+ const teamNames = new Set(
490
+ skills.filter((skill) => skill.sourceType === "team" && skill.teamName).map((skill) => skill.teamName)
491
+ );
492
+ const folders = /* @__PURE__ */ new Map();
493
+ for (const team of teams) {
494
+ if (!teamNames.has(team.name)) continue;
495
+ for (const folder of team.assetFolders.filter((f) => isResourceFolder(f.name))) {
496
+ folders.set(`${folder.teamName}/${folder.name}`, folder);
497
+ }
498
+ }
499
+ return Array.from(folders.values());
500
+ }
501
+ function findToolsetsForSkills(skills, toolsets) {
502
+ const toolsetNames = new Set(
503
+ skills.filter((skill) => skill.sourceType === "toolset" && skill.toolsetName).map((skill) => skill.toolsetName)
504
+ );
505
+ return toolsets.filter((toolset) => toolsetNames.has(toolset.name) && toolset.hasResources);
506
+ }
507
+ async function installResourceDependenciesForSkills(skills, options, onConflict) {
508
+ const [teams, toolsets] = await Promise.all([discoverTeams(), discoverToolsets()]);
509
+ const teamResourceFolders = findTeamResourceFoldersForSkills(skills, teams);
510
+ const matchingToolsets = findToolsetsForSkills(skills, toolsets);
511
+ const assetResults = await installTeamAssetFolders(teamResourceFolders, options, onConflict);
512
+ const resourceResults = [];
513
+ for (const toolset of matchingToolsets) {
514
+ resourceResults.push(await installResources(toolset, options, onConflict));
515
+ }
516
+ return { resourceResults, assetResults };
517
+ }
518
+
382
519
  // src/lib/prompts.ts
383
520
  import { select, checkbox, confirm, input } from "@inquirer/prompts";
384
521
  import chalk2 from "chalk";
@@ -435,7 +572,7 @@ async function promptSelectTeam(teams) {
435
572
  return select({
436
573
  message: "Select a team:",
437
574
  choices: teams.map((t) => ({
438
- name: `${formatTeamName2(t.name)} (${t.skills.length} skill${t.skills.length !== 1 ? "s" : ""})`,
575
+ name: `${formatTeamName2(t.name)} (${formatTeamContents(t)})`,
439
576
  value: t
440
577
  }))
441
578
  });
@@ -468,20 +605,30 @@ async function promptIncludeLibrary() {
468
605
  }
469
606
  async function promptInstallDir() {
470
607
  const raw = await input({
471
- message: "Install location for AI skills and resources:",
608
+ message: "Install location for AI content:",
472
609
  default: "docs/ai"
473
610
  });
474
611
  return raw.replace(/\/+$/, "") || "docs/ai";
475
612
  }
476
- async function promptConfirmInstall(skills, clients, targetDir, aiDir = "docs/ai") {
613
+ async function promptConfirmInstall(skills, clients, targetDir, aiDir = "docs/ai", assetFolders = []) {
477
614
  console.log();
478
615
  console.log(chalk2.bold("Install summary:"));
479
- console.log(` Skills: ${skills.map((s) => s.name).join(", ")}`);
616
+ console.log(` Skills: ${skills.map((s) => s.name).join(", ") || chalk2.dim("none")}`);
617
+ if (assetFolders.length > 0) {
618
+ const { prompts, resources, generic } = partitionTeamAssetFolders(assetFolders);
619
+ if (prompts.length > 0) {
620
+ console.log(` Prompts: ${prompts.map((f) => f.name).join(", ")}`);
621
+ }
622
+ if (resources.length > 0) {
623
+ console.log(` Resources: ${resources.map((f) => f.name).join(", ")}`);
624
+ }
625
+ if (generic.length > 0) {
626
+ console.log(` Content folders: ${generic.map((f) => f.name).join(", ")}`);
627
+ }
628
+ }
480
629
  console.log(` Clients: ${clients.map((c) => c.name).join(", ") || chalk2.dim("none")}`);
481
630
  console.log(` Target: ${targetDir}`);
482
- if (aiDir !== "docs/ai") {
483
- console.log(` AI directory: ${aiDir}`);
484
- }
631
+ console.log(` AI directory: ${aiDir}`);
485
632
  console.log();
486
633
  return confirm({ message: "Proceed with installation?", default: true });
487
634
  }
@@ -512,35 +659,66 @@ function normalizeAiDir(raw) {
512
659
  }
513
660
  return trimmed;
514
661
  }
515
- function printResults(results, options, resourceResult) {
516
- const prefix = options.dryRun ? chalk3.yellow("[DRY RUN] ") : "";
662
+ function formatFileCounts(result, options) {
517
663
  const createdWord = options.dryRun ? "would create" : "created";
518
664
  const skippedWord = "up to date";
519
665
  const overwrittenWord = options.dryRun ? "would overwrite" : "overwritten";
666
+ const parts = [];
667
+ if (result.created.length > 0) parts.push(chalk3.green(`${result.created.length} ${createdWord}`));
668
+ if (result.skipped.length > 0) parts.push(chalk3.dim(`${result.skipped.length} ${skippedWord}`));
669
+ if (result.overwritten.length > 0) parts.push(chalk3.yellow(`${result.overwritten.length} ${overwrittenWord}`));
670
+ return parts.join(", ");
671
+ }
672
+ function printResults(results, options, resourceResults = [], assetResults = []) {
673
+ const prefix = options.dryRun ? chalk3.yellow("[DRY RUN] ") : "";
520
674
  console.log(`
521
675
  ${prefix}${chalk3.bold("Install complete")}
522
676
  `);
523
- if (resourceResult) {
524
- const rParts = [];
525
- if (resourceResult.created.length > 0) rParts.push(chalk3.green(`${resourceResult.created.length} ${createdWord}`));
526
- if (resourceResult.skipped.length > 0) rParts.push(chalk3.dim(`${resourceResult.skipped.length} ${skippedWord}`));
527
- if (resourceResult.overwritten.length > 0) rParts.push(chalk3.yellow(`${resourceResult.overwritten.length} ${overwrittenWord}`));
528
- if (rParts.length > 0) {
529
- console.log(` ${chalk3.bold("Resources")} ${rParts.join(", ")}`);
677
+ if (resourceResults.length > 0) {
678
+ console.log(" Toolset resources Status");
679
+ console.log(" " + "\u2500".repeat(50));
680
+ for (const r of resourceResults) {
681
+ console.log(` ${chalk3.cyan(r.toolset.padEnd(24))} ${formatFileCounts(r, options)}`);
682
+ }
683
+ console.log();
684
+ }
685
+ if (assetResults.length > 0) {
686
+ const { prompts, resources, generic } = partitionTeamAssetFolders(assetResults);
687
+ if (prompts.length > 0) {
688
+ console.log(" Prompt Status");
689
+ console.log(" " + "\u2500".repeat(50));
690
+ for (const r of prompts) {
691
+ console.log(` ${chalk3.cyan(r.folder.padEnd(24))} ${formatFileCounts(r, options)}`);
692
+ }
693
+ console.log();
694
+ }
695
+ if (resources.length > 0) {
696
+ console.log(" Resource Status");
697
+ console.log(" " + "\u2500".repeat(50));
698
+ for (const r of resources) {
699
+ console.log(` ${chalk3.cyan(r.folder.padEnd(24))} ${formatFileCounts(r, options)}`);
700
+ }
701
+ console.log();
702
+ }
703
+ if (generic.length > 0) {
704
+ console.log(" Content folder Status");
705
+ console.log(" " + "\u2500".repeat(50));
706
+ for (const r of generic) {
707
+ console.log(` ${chalk3.cyan(r.folder.padEnd(24))} ${formatFileCounts(r, options)}`);
708
+ }
530
709
  console.log();
531
710
  }
532
711
  }
533
- console.log(" Skill Status");
534
- console.log(" " + "\u2500".repeat(50));
535
- for (const r of results) {
536
- const parts = [];
537
- if (r.created.length > 0) parts.push(chalk3.green(`${r.created.length} ${createdWord}`));
538
- if (r.skipped.length > 0) parts.push(chalk3.dim(`${r.skipped.length} ${skippedWord}`));
539
- if (r.overwritten.length > 0) parts.push(chalk3.yellow(`${r.overwritten.length} ${overwrittenWord}`));
540
- console.log(` ${chalk3.cyan(r.skill.padEnd(24))} ${parts.join(", ")}`);
712
+ if (results.length > 0) {
713
+ console.log(" Skill Status");
714
+ console.log(" " + "\u2500".repeat(50));
715
+ for (const r of results) {
716
+ console.log(` ${chalk3.cyan(r.skill.padEnd(24))} ${formatFileCounts(r, options)}`);
717
+ }
718
+ console.log();
541
719
  }
542
- console.log(`
543
- Target: ${options.targetDir}`);
720
+ console.log(` Target: ${options.targetDir}`);
721
+ console.log(` AI directory: ${options.aiDir}`);
544
722
  console.log(` Clients: ${options.clients.map((c) => c.name).join(", ") || chalk3.dim("none")}`);
545
723
  console.log();
546
724
  }
@@ -575,15 +753,10 @@ async function installSkillCmd(name, _opts, cmd) {
575
753
  skipExisting,
576
754
  dryRun
577
755
  };
578
- const result = await installSkill(skill, options, force || skipExisting ? void 0 : promptConflict);
579
- let resourceResult;
580
- if (skill.sourceType === "toolset" && skill.toolsetName) {
581
- const toolset = await findToolset(skill.toolsetName);
582
- if (toolset && toolset.hasResources) {
583
- resourceResult = await installResources(toolset, options, force || skipExisting ? void 0 : promptConflict);
584
- }
585
- }
586
- printResults([result], options, resourceResult);
756
+ const conflictHandler = force || skipExisting ? void 0 : promptConflict;
757
+ const resourceDependencies = await installResourceDependenciesForSkills([skill], options, conflictHandler);
758
+ const result = await installSkill(skill, options, conflictHandler);
759
+ printResults([result], options, resourceDependencies.resourceResults, resourceDependencies.assetResults);
587
760
  }
588
761
  async function installTeamCmd(name, _opts, cmd) {
589
762
  const parentOpts = cmd.parent.opts();
@@ -625,12 +798,18 @@ async function installTeamCmd(name, _opts, cmd) {
625
798
  skipExisting,
626
799
  dryRun
627
800
  };
801
+ const conflictHandler = force || skipExisting ? void 0 : promptConflict;
802
+ const assetResults = await installTeamAssetFolders(
803
+ team.assetFolders,
804
+ options,
805
+ conflictHandler
806
+ );
628
807
  const results = await installSkills(
629
808
  skillsToInstall,
630
809
  options,
631
- force || skipExisting ? void 0 : promptConflict
810
+ conflictHandler
632
811
  );
633
- printResults(results, options);
812
+ printResults(results, options, [], assetResults);
634
813
  }
635
814
  async function installToolsetCmd(name, _opts, cmd) {
636
815
  const parentOpts = cmd.parent.opts();
@@ -666,12 +845,12 @@ async function installToolsetCmd(name, _opts, cmd) {
666
845
  dryRun
667
846
  };
668
847
  const conflictHandler = force || skipExisting ? void 0 : promptConflict;
669
- let resourceResult;
848
+ const resourceResults = [];
670
849
  if (toolset.hasResources) {
671
- resourceResult = await installResources(toolset, options, conflictHandler);
850
+ resourceResults.push(await installResources(toolset, options, conflictHandler));
672
851
  }
673
852
  const results = await installSkills(toolset.skills, options, conflictHandler);
674
- printResults(results, options, resourceResult);
853
+ printResults(results, options, resourceResults);
675
854
  }
676
855
  var INHERITED_OPTIONS_HELP = `
677
856
  Parent options (pass before subcommand):
@@ -679,46 +858,77 @@ Parent options (pass before subcommand):
679
858
  --skip-existing Skip files that already exist without prompting
680
859
  --dry-run Show what would be installed without writing files
681
860
  --target <dir> Install to a different directory (default: CWD)
682
- --ai-dir <path> Override the AI skills/resources directory (default: docs/ai)`;
861
+ --ai-dir <path> Override the AI content directory (default: docs/ai)`;
683
862
  function registerInstallCommand(program2) {
684
- const install = program2.command("install").description("Install skills").option("--force", "Overwrite existing files without prompting").option("--skip-existing", "Skip files that already exist without prompting").option("--dry-run", "Show what would be installed without writing files").option("--target <dir>", "Install to a different directory (default: CWD)").option("--ai-dir <path>", "Override the AI skills/resources directory (default: docs/ai)");
863
+ const install = program2.command("install").description("Install AI content").option("--force", "Overwrite existing files without prompting").option("--skip-existing", "Skip files that already exist without prompting").option("--dry-run", "Show what would be installed without writing files").option("--target <dir>", "Install to a different directory (default: CWD)").option("--ai-dir <path>", "Override the AI content directory (default: docs/ai)");
685
864
  install.command("skill <name>").description("Install a specific skill").addHelpText("after", INHERITED_OPTIONS_HELP).action(installSkillCmd);
686
- install.command("team <name>").description("Install all skills for a team").addHelpText("after", INHERITED_OPTIONS_HELP).action(installTeamCmd);
865
+ install.command("team <name>").description("Install all skills, prompts, and content folders for a team").addHelpText("after", INHERITED_OPTIONS_HELP).action(installTeamCmd);
687
866
  install.command("toolset <name>").description("Install all skills and resources for a toolset").addHelpText("after", INHERITED_OPTIONS_HELP).action(installToolsetCmd);
688
867
  }
689
868
 
690
869
  // src/commands/interactive.ts
691
870
  import { select as select2 } from "@inquirer/prompts";
692
871
  import chalk4 from "chalk";
693
- function printResults2(results, options, resourceResult) {
694
- const prefix = options.dryRun ? chalk4.yellow("[DRY RUN] ") : "";
872
+ function formatFileCounts2(result, options) {
695
873
  const createdWord = options.dryRun ? "would create" : "created";
696
874
  const skippedWord = "up to date";
697
875
  const overwrittenWord = options.dryRun ? "would overwrite" : "overwritten";
876
+ const parts = [];
877
+ if (result.created.length > 0) parts.push(chalk4.green(`${result.created.length} ${createdWord}`));
878
+ if (result.skipped.length > 0) parts.push(chalk4.dim(`${result.skipped.length} ${skippedWord}`));
879
+ if (result.overwritten.length > 0) parts.push(chalk4.yellow(`${result.overwritten.length} ${overwrittenWord}`));
880
+ return parts.join(", ");
881
+ }
882
+ function printResults2(results, options, resourceResults = [], assetResults = []) {
883
+ const prefix = options.dryRun ? chalk4.yellow("[DRY RUN] ") : "";
698
884
  console.log(`
699
885
  ${prefix}${chalk4.bold("Install complete")}
700
886
  `);
701
- if (resourceResult) {
702
- const rParts = [];
703
- if (resourceResult.created.length > 0) rParts.push(chalk4.green(`${resourceResult.created.length} ${createdWord}`));
704
- if (resourceResult.skipped.length > 0) rParts.push(chalk4.dim(`${resourceResult.skipped.length} ${skippedWord}`));
705
- if (resourceResult.overwritten.length > 0) rParts.push(chalk4.yellow(`${resourceResult.overwritten.length} ${overwrittenWord}`));
706
- if (rParts.length > 0) {
707
- console.log(` ${chalk4.bold("Resources")} ${rParts.join(", ")}`);
887
+ if (resourceResults.length > 0) {
888
+ console.log(" Toolset resources Status");
889
+ console.log(" " + "\u2500".repeat(50));
890
+ for (const r of resourceResults) {
891
+ console.log(` ${chalk4.cyan(r.toolset.padEnd(24))} ${formatFileCounts2(r, options)}`);
892
+ }
893
+ console.log();
894
+ }
895
+ if (assetResults.length > 0) {
896
+ const { prompts, resources, generic } = partitionTeamAssetFolders(assetResults);
897
+ if (prompts.length > 0) {
898
+ console.log(" Prompt Status");
899
+ console.log(" " + "\u2500".repeat(50));
900
+ for (const r of prompts) {
901
+ console.log(` ${chalk4.cyan(r.folder.padEnd(24))} ${formatFileCounts2(r, options)}`);
902
+ }
903
+ console.log();
904
+ }
905
+ if (resources.length > 0) {
906
+ console.log(" Resource Status");
907
+ console.log(" " + "\u2500".repeat(50));
908
+ for (const r of resources) {
909
+ console.log(` ${chalk4.cyan(r.folder.padEnd(24))} ${formatFileCounts2(r, options)}`);
910
+ }
911
+ console.log();
912
+ }
913
+ if (generic.length > 0) {
914
+ console.log(" Content folder Status");
915
+ console.log(" " + "\u2500".repeat(50));
916
+ for (const r of generic) {
917
+ console.log(` ${chalk4.cyan(r.folder.padEnd(24))} ${formatFileCounts2(r, options)}`);
918
+ }
708
919
  console.log();
709
920
  }
710
921
  }
711
- console.log(" Skill Status");
712
- console.log(" " + "\u2500".repeat(50));
713
- for (const r of results) {
714
- const parts = [];
715
- if (r.created.length > 0) parts.push(chalk4.green(`${r.created.length} ${createdWord}`));
716
- if (r.skipped.length > 0) parts.push(chalk4.dim(`${r.skipped.length} ${skippedWord}`));
717
- if (r.overwritten.length > 0) parts.push(chalk4.yellow(`${r.overwritten.length} ${overwrittenWord}`));
718
- console.log(` ${chalk4.cyan(r.skill.padEnd(24))} ${parts.join(", ")}`);
922
+ if (results.length > 0) {
923
+ console.log(" Skill Status");
924
+ console.log(" " + "\u2500".repeat(50));
925
+ for (const r of results) {
926
+ console.log(` ${chalk4.cyan(r.skill.padEnd(24))} ${formatFileCounts2(r, options)}`);
927
+ }
928
+ console.log();
719
929
  }
720
- console.log(`
721
- Target: ${options.targetDir}`);
930
+ console.log(` Target: ${options.targetDir}`);
931
+ console.log(` AI directory: ${options.aiDir}`);
722
932
  console.log(` Clients: ${options.clients.map((c) => c.name).join(", ") || chalk4.dim("none")}`);
723
933
  console.log();
724
934
  }
@@ -757,7 +967,7 @@ ${formatTeamName3(team)}`));
757
967
  console.log(chalk4.bold("\nTeams"));
758
968
  for (const t of teams) {
759
969
  console.log(
760
- ` ${chalk4.cyan(formatTeamName3(t.name).padEnd(24))} ${t.skills.length} skill${t.skills.length !== 1 ? "s" : ""}`
970
+ ` ${chalk4.cyan(formatTeamName3(t.name).padEnd(24))} ${formatTeamContents(t)}`
761
971
  );
762
972
  }
763
973
  }
@@ -778,7 +988,7 @@ async function runInteractive() {
778
988
  message: "What would you like to do?",
779
989
  choices: [
780
990
  { name: "Install skill(s)", value: "install-skills" },
781
- { name: "Install team skills", value: "install-team" },
991
+ { name: "Install team content", value: "install-team" },
782
992
  { name: "Install toolset", value: "install-toolset" },
783
993
  { name: "List available skills & teams", value: "list" }
784
994
  ]
@@ -811,18 +1021,9 @@ async function runInteractive() {
811
1021
  skipExisting: false,
812
1022
  dryRun: false
813
1023
  };
1024
+ const resourceDependencies = await installResourceDependenciesForSkills(selectedSkills, options, promptConflict);
814
1025
  const results = await installSkills(selectedSkills, options, promptConflict);
815
- let resourceResult;
816
- const toolsetNames = new Set(
817
- selectedSkills.filter((s) => s.sourceType === "toolset" && s.toolsetName).map((s) => s.toolsetName)
818
- );
819
- for (const tsName of toolsetNames) {
820
- const toolset = await findToolset(tsName);
821
- if (toolset && toolset.hasResources) {
822
- resourceResult = await installResources(toolset, options, promptConflict);
823
- }
824
- }
825
- printResults2(results, options, resourceResult);
1026
+ printResults2(results, options, resourceDependencies.resourceResults, resourceDependencies.assetResults);
826
1027
  } else if (action === "install-team") {
827
1028
  const teams = await discoverTeams();
828
1029
  if (teams.length === 0) {
@@ -840,7 +1041,7 @@ async function runInteractive() {
840
1041
  const detected = await detectClients(targetDir);
841
1042
  const clients = await promptSelectClients(ALL_CLIENTS, detected);
842
1043
  const aiDir = await promptInstallDir();
843
- const confirmed = await promptConfirmInstall(skillsToInstall, clients, targetDir, aiDir);
1044
+ const confirmed = await promptConfirmInstall(skillsToInstall, clients, targetDir, aiDir, team.assetFolders);
844
1045
  if (!confirmed) {
845
1046
  console.log(chalk4.dim("Cancelled."));
846
1047
  return;
@@ -853,18 +1054,9 @@ async function runInteractive() {
853
1054
  skipExisting: false,
854
1055
  dryRun: false
855
1056
  };
1057
+ const assetResults = await installTeamAssetFolders(team.assetFolders, options, promptConflict);
856
1058
  const results = await installSkills(skillsToInstall, options, promptConflict);
857
- let resourceResult;
858
- const toolsetNames = new Set(
859
- skillsToInstall.filter((s) => s.sourceType === "toolset" && s.toolsetName).map((s) => s.toolsetName)
860
- );
861
- for (const tsName of toolsetNames) {
862
- const toolset = await findToolset(tsName);
863
- if (toolset && toolset.hasResources) {
864
- resourceResult = await installResources(toolset, options, promptConflict);
865
- }
866
- }
867
- printResults2(results, options, resourceResult);
1059
+ printResults2(results, options, [], assetResults);
868
1060
  } else if (action === "install-toolset") {
869
1061
  const toolsets = await discoverToolsets();
870
1062
  if (toolsets.length === 0) {
@@ -888,18 +1080,24 @@ async function runInteractive() {
888
1080
  skipExisting: false,
889
1081
  dryRun: false
890
1082
  };
891
- let resourceResult;
1083
+ const resourceResults = [];
892
1084
  if (toolset.hasResources) {
893
- resourceResult = await installResources(toolset, options, promptConflict);
1085
+ resourceResults.push(await installResources(toolset, options, promptConflict));
894
1086
  }
895
1087
  const results = await installSkills(toolset.skills, options, promptConflict);
896
- printResults2(results, options, resourceResult);
1088
+ printResults2(results, options, resourceResults);
897
1089
  }
898
1090
  }
899
1091
 
900
1092
  // src/index.ts
1093
+ function getPackageVersion() {
1094
+ const packageRoot = path5.resolve(path5.dirname(fileURLToPath2(import.meta.url)), "..");
1095
+ const packageJsonPath = path5.join(packageRoot, "package.json");
1096
+ const packageJson = JSON.parse(fs4.readFileSync(packageJsonPath, "utf-8"));
1097
+ return packageJson.version || "0.0.0";
1098
+ }
901
1099
  var program = new Command();
902
- program.name("ai-dev").description("Interactive installer for GroupBy AI development skills").version("0.1.0").action(runInteractive);
1100
+ program.name("ai-dev").description("Interactive installer for GroupBy AI development content").version(getPackageVersion()).action(runInteractive);
903
1101
  registerListCommand(program);
904
1102
  registerInstallCommand(program);
905
1103
  program.addHelpText(
@@ -910,7 +1108,7 @@ Examples:
910
1108
  $ npx @groupby/ai-dev list List skills, teams, and toolsets
911
1109
  $ npx @groupby/ai-dev list toolsets List available toolsets
912
1110
  $ npx @groupby/ai-dev install skill frontend-design Install a single skill
913
- $ npx @groupby/ai-dev install team brain-studio Install all team skills
1111
+ $ npx @groupby/ai-dev install team brain-studio Install all team content
914
1112
  $ npx @groupby/ai-dev install toolset rzlv-flow Install all skills + resources for a toolset
915
1113
  $ npx @groupby/ai-dev install team brain-studio --dry-run
916
1114
  $ npx @groupby/ai-dev install skill frontend-design --ai-dir .ai`
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@groupby/ai-dev",
3
- "version": "0.4.1",
4
- "description": "Interactive installer for Rezolve Ai development skills",
3
+ "version": "0.5.0",
4
+ "description": "Interactive installer for Rezolve Ai development content",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "ai-dev": "dist/index.js"
@@ -0,0 +1,60 @@
1
+ # Fix Review Findings
2
+
3
+ Apply actionable findings from `prompts/review-change.md` to the current changes in `components/`.
4
+
5
+ ## Goal
6
+
7
+ Take the review findings/suggestions already produced, implement concrete fixes, run relevant verification, and report
8
+ what was addressed versus what still needs clarification.
9
+
10
+ ## Inputs
11
+
12
+ Use the latest review output from this session. If no findings were reported, stop and state that no fix work is needed.
13
+
14
+ If findings are missing required detail (for example no file path, unclear recommendation, or conflicting guidance), ask
15
+ for clarification before editing.
16
+
17
+ ## Prioritization Rules
18
+
19
+ Process findings in this order:
20
+
21
+ 1. Critical and high-severity correctness/regression issues
22
+ 2. Contract/schema/API mismatches
23
+ 3. Validation and error-handling gaps
24
+ 4. Missing or weak tests
25
+ 5. Medium/low maintainability or style items
26
+
27
+ ## Implementation Rules
28
+
29
+ - Apply minimal, targeted changes per finding.
30
+ - Keep scope limited to the reviewed change set.
31
+ - Do not perform unrelated refactors.
32
+ - If behavior changes, add or update focused tests.
33
+ - If contract/schema changes are required, update specs/contracts first, then runtime code.
34
+ - If a finding is intentionally not implemented, record why.
35
+
36
+ ## Verification
37
+
38
+ Run relevant checks for touched components:
39
+
40
+ ```bash
41
+ uv run pytest test/ -v # bc-agent, bc-mcp
42
+ mix test # bc-api
43
+ mix format --check-formatted # bc-api
44
+ ./gradlew :bc-pay-core:test # bc-pay
45
+ ./gradlew spotlessCheck # bc-pay
46
+ ./gradlew test --no-daemon # bc-pay-stripe
47
+ ./gradlew spotlessCheck --no-daemon # bc-pay-stripe
48
+ ./gradlew test # bc-magento, bc-salesforce, bc-shopify, bc-webhooks
49
+ ```
50
+
51
+ If full verification is too expensive, run focused checks for touched areas and state what was not run.
52
+
53
+ ## Final Response
54
+
55
+ Lead with outcome and provide:
56
+
57
+ - Findings addressed (mapped to files changed)
58
+ - Findings deferred or blocked (with reason)
59
+ - Checks run and results
60
+ - Remaining risks
@@ -0,0 +1,44 @@
1
+ # Review Brain Checkout Component Change
2
+
3
+ Review the current changes in `components/`.
4
+
5
+ ## Review Focus
6
+
7
+ - Correctness and regressions
8
+ - Contract compatibility
9
+ - Producer/consumer alignment for events and APIs
10
+ - Validation and error handling
11
+ - Secret leakage in logs/config/examples
12
+ - Missing or weak tests
13
+ - Build/test risk
14
+
15
+ ## Output Format
16
+
17
+ Lead with findings:
18
+
19
+ ```markdown
20
+ ## Findings
21
+
22
+ - Severity: {critical|high|medium|low}
23
+ File: `{path}:{line}`
24
+ Issue: {specific issue}
25
+ Recommendation: {specific fix}
26
+
27
+ ## Open Questions
28
+
29
+ - {question or "None"}
30
+
31
+ ## Checks
32
+
33
+ - {command}: {result}
34
+
35
+ ## Summary
36
+
37
+ {brief summary}
38
+ ```
39
+
40
+ If no issues are found, say that clearly and list residual risks.
41
+
42
+ ## Optional Next Step
43
+
44
+ If findings or suggestions are reported and code updates are required, run `prompts/fix-review-findings.md`.