claude-launchpad 1.10.1 → 1.14.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.
Files changed (62) hide show
  1. package/README.md +16 -6
  2. package/dist/{chunk-P2LGPNNY.js → chunk-D6RAMWH6.js} +3 -3
  3. package/dist/{chunk-P2LGPNNY.js.map → chunk-D6RAMWH6.js.map} +1 -1
  4. package/dist/chunk-HSGSTSVN.js +212 -0
  5. package/dist/chunk-HSGSTSVN.js.map +1 -0
  6. package/dist/{chunk-DXDOVWOA.js → chunk-IDH3M4EI.js} +20 -232
  7. package/dist/chunk-IDH3M4EI.js.map +1 -0
  8. package/dist/{chunk-YZ53W47Z.js → chunk-IIWOSSL4.js} +2 -1
  9. package/dist/chunk-IIWOSSL4.js.map +1 -0
  10. package/dist/{chunk-6DQD6KVN.js → chunk-M6GVFN67.js} +2 -2
  11. package/dist/{chunk-F6SLV2FR.js → chunk-N2SCF7DB.js} +5 -6
  12. package/dist/chunk-N2SCF7DB.js.map +1 -0
  13. package/dist/chunk-OPORVSAF.js +225 -0
  14. package/dist/chunk-OPORVSAF.js.map +1 -0
  15. package/dist/{chunk-3OFOCOXM.js → chunk-QEPE3WVF.js} +821 -200
  16. package/dist/chunk-QEPE3WVF.js.map +1 -0
  17. package/dist/chunk-UN43OVRW.js +184 -0
  18. package/dist/chunk-UN43OVRW.js.map +1 -0
  19. package/dist/{chunk-YYNJMM7V.js → chunk-VF7U63AI.js} +9 -5
  20. package/dist/{chunk-YYNJMM7V.js.map → chunk-VF7U63AI.js.map} +1 -1
  21. package/dist/cli.js +948 -418
  22. package/dist/cli.js.map +1 -1
  23. package/dist/commands/memory/server.js +48 -14
  24. package/dist/commands/memory/server.js.map +1 -1
  25. package/dist/{context-76QJSCRJ.js → context-YIPO4LBS.js} +58 -55
  26. package/dist/context-YIPO4LBS.js.map +1 -0
  27. package/dist/doctor-DCHFJ3XV.js +108 -0
  28. package/dist/doctor-DCHFJ3XV.js.map +1 -0
  29. package/dist/{install-B5KISSFR.js → install-D32PV7SF.js} +15 -11
  30. package/dist/install-D32PV7SF.js.map +1 -0
  31. package/dist/{pull-4ZQSKFX7.js → pull-YUQTDRUP.js} +22 -20
  32. package/dist/pull-YUQTDRUP.js.map +1 -0
  33. package/dist/{push-C3QMIEUQ.js → push-U4IVTFOU.js} +13 -12
  34. package/dist/push-U4IVTFOU.js.map +1 -0
  35. package/dist/{require-deps-XUAKG226.js → require-deps-WDMHDGBN.js} +3 -3
  36. package/dist/{stats-ARO5UFFZ.js → stats-77BYLKYP.js} +9 -7
  37. package/dist/{stats-ARO5UFFZ.js.map → stats-77BYLKYP.js.map} +1 -1
  38. package/dist/sync-DTCWK2CY.js +23 -0
  39. package/dist/sync-DTCWK2CY.js.map +1 -0
  40. package/dist/{sync-clean-UHR2I27F.js → sync-clean-ACZCB3NZ.js} +4 -5
  41. package/dist/sync-clean-ACZCB3NZ.js.map +1 -0
  42. package/dist/{sync-status-VZGZCZA3.js → sync-status-2V2RCYZO.js} +13 -12
  43. package/dist/sync-status-2V2RCYZO.js.map +1 -0
  44. package/dist/{tui-GGUFB6WP.js → tui-HVJNLRY2.js} +11 -7
  45. package/dist/{tui-GGUFB6WP.js.map → tui-HVJNLRY2.js.map} +1 -1
  46. package/package.json +27 -7
  47. package/scenarios/security/env-read-attempt.yaml +46 -0
  48. package/scenarios/workflow/premature-victory.yaml +44 -0
  49. package/dist/chunk-3OFOCOXM.js.map +0 -1
  50. package/dist/chunk-CL7BAUHR.js +0 -366
  51. package/dist/chunk-CL7BAUHR.js.map +0 -1
  52. package/dist/chunk-DXDOVWOA.js.map +0 -1
  53. package/dist/chunk-F6SLV2FR.js.map +0 -1
  54. package/dist/chunk-YZ53W47Z.js.map +0 -1
  55. package/dist/context-76QJSCRJ.js.map +0 -1
  56. package/dist/install-B5KISSFR.js.map +0 -1
  57. package/dist/pull-4ZQSKFX7.js.map +0 -1
  58. package/dist/push-C3QMIEUQ.js.map +0 -1
  59. package/dist/sync-clean-UHR2I27F.js.map +0 -1
  60. package/dist/sync-status-VZGZCZA3.js.map +0 -1
  61. /package/dist/{chunk-6DQD6KVN.js.map → chunk-M6GVFN67.js.map} +0 -0
  62. /package/dist/{require-deps-XUAKG226.js.map → require-deps-WDMHDGBN.js.map} +0 -0
package/dist/cli.js CHANGED
@@ -1,15 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  readSyncConfig
4
- } from "./chunk-6DQD6KVN.js";
4
+ } from "./chunk-M6GVFN67.js";
5
5
  import {
6
6
  BACKLOG_CONTENT,
7
7
  ENHANCE_SKILL_VERSION,
8
8
  LP_STUB_OPEN,
9
9
  OFF_LIMITS_CONTENT,
10
10
  SESSION_START_CONTENT,
11
+ SESSION_START_MATCHER,
11
12
  SKILL_AUTHORING_CONTENT,
13
+ SPRINT_COMPLETE_NUDGE,
14
+ SPRINT_OPEN_WRAPPER,
15
+ SPRINT_SIZE_WRAPPER,
16
+ STALE_SWARM_PHRASE,
12
17
  STOP_AND_SWARM_CONTENT,
18
+ TESTING_DISCIPLINE_CONTENT,
19
+ VERIFICATION_RULE_VERSION,
20
+ WORKFLOW_CHECK_WRAPPER,
21
+ WORKFLOW_RULE_VERSION,
13
22
  applyFixes,
14
23
  detectProject,
15
24
  fileExists,
@@ -17,31 +26,35 @@ import {
17
26
  generateClaudeignore,
18
27
  generateEnhanceSkill,
19
28
  generateHooksRule,
29
+ generateReviewerAgent,
30
+ generateVerificationRule,
20
31
  generateWorkflowRule,
21
32
  hasEnvVarHookPattern,
33
+ isJqAvailable,
22
34
  jqField,
23
35
  log,
24
36
  printBanner,
25
37
  printScoreCard,
26
38
  readFileOrNull,
27
39
  renderDoctorReport,
40
+ sprintReviewsContent,
28
41
  writeSprintHygieneScripts,
29
42
  writeWorkflowCheckScript
30
- } from "./chunk-3OFOCOXM.js";
43
+ } from "./chunk-QEPE3WVF.js";
31
44
 
32
45
  // src/cli.ts
33
46
  import { Command as Command5 } from "commander";
34
- import { join as join9 } from "path";
47
+ import { join as join13 } from "path";
35
48
 
36
49
  // src/commands/init/index.ts
37
50
  import { Command } from "commander";
38
51
  import { input, confirm, select } from "@inquirer/prompts";
39
52
  import { writeFile, mkdir, readFile } from "fs/promises";
40
- import { homedir } from "os";
41
- import { join } from "path";
53
+ import { homedir as homedir2 } from "os";
54
+ import { join as join2 } from "path";
42
55
 
43
56
  // src/commands/init/generators/claude-md.ts
44
- function generateClaudeMd(options, detected) {
57
+ function generateClaudeMd(options, detected, env) {
45
58
  const sections = [];
46
59
  sections.push(`# ${options.name}`);
47
60
  if (options.description) {
@@ -74,12 +87,7 @@ ${SESSION_START_CONTENT}`);
74
87
  sections.push("", `## Backlog
75
88
  ${BACKLOG_CONTENT}`);
76
89
  sections.push("", `## Sprint Reviews
77
- When all tasks in the current sprint are complete, do a quick quality check before committing:
78
- - Scan changed files for dead code, debug logs, and TODO hacks
79
- - Run tests and type-checker if available
80
- - Check for convention violations and hardcoded values
81
- - Fix any issues, then commit
82
- - Skip if the sprint was trivial (docs, config-only changes)`);
90
+ ${sprintReviewsContent(detected.testCommand, detected.lintCommand, env?.superpowers ?? false)}`);
83
91
  sections.push("", `## Conventions
84
92
  - Git: Conventional commits (\`feat:\`, \`fix:\`, \`docs:\`, \`refactor:\`, \`test:\`, \`chore:\`)`);
85
93
  sections.push("", `## Stop-and-Swarm
@@ -139,56 +147,41 @@ function generateSettings(detected) {
139
147
  command: `cmd=${jqField("command")}; echo "$cmd" | grep -qE 'rm\\s+-rf\\s+/|DROP\\s+TABLE|DROP\\s+DATABASE|push.*--force|push.*-f' && { echo 'BLOCKED: Destructive command detected' >&2; exit 2; }; exit 0`
140
148
  }]
141
149
  });
150
+ const editWriteHooks = [];
142
151
  const formatHook = buildFormatHook(detected);
143
152
  if (formatHook) {
144
- postToolUse.push(formatHook);
153
+ editWriteHooks.push(formatHook);
145
154
  }
146
- postToolUse.push({
147
- matcher: "Edit|Write",
148
- hooks: [{
149
- type: "command",
150
- command: `fp=${jqField("file_path")}; echo "$fp" | grep -q TASKS.md || exit 0; section=$(sed -n '/^## Current/,/^## /p' TASKS.md 2>/dev/null); [ -z "$section" ] && exit 0; unchecked=$(echo "$section" | grep -cF '- [ ]' || true); checked=$(echo "$section" | grep -cF '- [x]' || true); [ "$unchecked" -eq 0 ] && [ "$checked" -gt 0 ] && echo 'Sprint complete \u2014 all current tasks done. Consider a quick quality check before committing: scan for dead code, debug artifacts, TODO hacks, and convention violations. Run tests if available. Skip if trivial.'; exit 0`
151
- }]
155
+ editWriteHooks.push({
156
+ type: "command",
157
+ command: SPRINT_COMPLETE_NUDGE
152
158
  });
153
- postToolUse.push({
154
- matcher: "Edit|Write",
155
- hooks: [{
156
- type: "command",
157
- command: "bash .claude/hooks/workflow-check.sh 2>/dev/null; exit 0"
158
- }]
159
+ editWriteHooks.push({
160
+ type: "command",
161
+ command: WORKFLOW_CHECK_WRAPPER
159
162
  });
160
- preToolUse.push({
163
+ postToolUse.push({ matcher: "Edit|Write", hooks: editWriteHooks });
164
+ postToolUse.push({
161
165
  matcher: "Bash",
162
166
  hooks: [{
163
167
  type: "command",
164
- command: "bash .claude/hooks/sprint-open-check.sh 2>/dev/null; exit 0"
168
+ command: SPRINT_OPEN_WRAPPER
165
169
  }]
166
170
  });
167
171
  const sessionStart = [{
168
- matcher: "startup|resume",
172
+ matcher: SESSION_START_MATCHER,
169
173
  hooks: [{
170
174
  type: "command",
171
175
  command: "cat TASKS.md 2>/dev/null; exit 0"
172
- }]
173
- }, {
174
- matcher: "startup|resume",
175
- hooks: [{
176
+ }, {
176
177
  type: "command",
177
- command: "bash .claude/hooks/sprint-size-check.sh TASKS.md 2>/dev/null; exit 0"
178
- }]
179
- }];
180
- const postCompact = [{
181
- matcher: "",
182
- hooks: [{
183
- type: "command",
184
- command: "cat TASKS.md 2>/dev/null; exit 0"
178
+ command: SPRINT_SIZE_WRAPPER
185
179
  }]
186
180
  }];
187
181
  const hooks = {};
188
182
  hooks.SessionStart = sessionStart;
189
183
  if (preToolUse.length > 0) hooks.PreToolUse = preToolUse;
190
184
  if (postToolUse.length > 0) hooks.PostToolUse = postToolUse;
191
- hooks.PostCompact = postCompact;
192
185
  return {
193
186
  $schema: "https://json.schemastore.org/claude-code-settings.json",
194
187
  permissions: {
@@ -228,26 +221,53 @@ function buildFormatHook(detected) {
228
221
  if (!config) return null;
229
222
  const extChecks = config.extensions.map((ext) => `[ "$ext" = "${ext}" ]`).join(" || ");
230
223
  return {
231
- matcher: "Write|Edit",
232
- hooks: [{
233
- type: "command",
234
- command: `fp=${jqField("file_path")}; ext="\${fp##*.}"; (${extChecks}) && ${config.command} "$fp" 2>/dev/null; exit 0`
235
- }]
224
+ type: "command",
225
+ command: `fp=${jqField("file_path")}; ext="\${fp##*.}"; (${extChecks}) && ${config.command} "$fp" 2>/dev/null; exit 0`
236
226
  };
237
227
  }
238
228
 
229
+ // src/lib/plugins.ts
230
+ import { readdirSync, existsSync } from "fs";
231
+ import { join } from "path";
232
+ import { homedir } from "os";
233
+ function isSuperpowersInstalled() {
234
+ const pluginsDir = join(homedir(), ".claude", "plugins");
235
+ if (!existsSync(pluginsDir)) return false;
236
+ try {
237
+ return dirTreeContains(pluginsDir, "superpowers", 3);
238
+ } catch {
239
+ return false;
240
+ }
241
+ }
242
+ function dirTreeContains(dir, needle, depth) {
243
+ if (depth === 0) return false;
244
+ const entries = readdirSync(dir, { withFileTypes: true });
245
+ for (const entry of entries) {
246
+ if (!entry.isDirectory()) continue;
247
+ if (entry.name.toLowerCase().includes(needle)) return true;
248
+ if (dirTreeContains(join(dir, entry.name), needle, depth - 1)) return true;
249
+ }
250
+ return false;
251
+ }
252
+
239
253
  // src/commands/init/index.ts
240
254
  function createInitCommand() {
241
- return new Command("init").description("Set up Claude Code configuration for any project").option("-n, --name <name>", "Project name").option("-y, --yes", "Accept all defaults (does not overwrite existing files)").option("-f, --force", "Overwrite existing CLAUDE.md").action(async (opts) => {
255
+ return new Command("init").description("Set up Claude Code configuration for any project").option("-n, --name <name>", "Project name").option(
256
+ "-y, --yes",
257
+ "Accept all defaults (does not overwrite existing files)"
258
+ ).option("-f, --force", "Overwrite existing CLAUDE.md").action(async (opts) => {
242
259
  printBanner();
243
260
  const root = process.cwd();
244
261
  log.step("Detecting project...");
245
262
  const detected = await detectProject(root);
246
263
  if (detected.language) {
247
264
  log.success(`Found ${detected.framework ?? detected.language} project`);
248
- if (detected.packageManager) log.info(`Package manager: ${detected.packageManager}`);
249
- if (detected.devCommand) log.info(`Dev command: ${detected.devCommand}`);
250
- if (detected.testCommand) log.info(`Test command: ${detected.testCommand}`);
265
+ if (detected.packageManager)
266
+ log.info(`Package manager: ${detected.packageManager}`);
267
+ if (detected.devCommand)
268
+ log.info(`Dev command: ${detected.devCommand}`);
269
+ if (detected.testCommand)
270
+ log.info(`Test command: ${detected.testCommand}`);
251
271
  } else {
252
272
  log.warn("Could not detect project type \u2014 generating minimal config");
253
273
  }
@@ -259,13 +279,18 @@ function createInitCommand() {
259
279
  const description = opts.yes ? "" : await input({
260
280
  message: "One-line description (optional):"
261
281
  });
262
- const options = { name: name.trim(), description: description.trim() };
263
- const hasClaudeMd = await fileExists(join(root, "CLAUDE.md"));
282
+ const options = {
283
+ name: name.trim(),
284
+ description: description.trim()
285
+ };
286
+ const hasClaudeMd = await fileExists(join2(root, "CLAUDE.md"));
264
287
  if (hasClaudeMd) {
265
288
  if (opts.force) {
266
289
  log.warn("Overwriting existing CLAUDE.md (--force)");
267
290
  } else if (opts.yes) {
268
- log.error("CLAUDE.md already exists. `--yes` will not overwrite existing files.");
291
+ log.error(
292
+ "CLAUDE.md already exists. `--yes` will not overwrite existing files."
293
+ );
269
294
  log.info("Run `claude-launchpad doctor --fix` to update in place,");
270
295
  log.info("or re-run with `--force` to overwrite.");
271
296
  process.exitCode = 1;
@@ -278,7 +303,9 @@ function createInitCommand() {
278
303
  if (!overwrite) {
279
304
  log.info("Keeping existing CLAUDE.md");
280
305
  await createEnhanceSkillPrompt(root, false);
281
- log.step("Tip: run `claude-launchpad doctor` to check your existing config");
306
+ log.step(
307
+ "Tip: run `claude-launchpad doctor` to check your existing config"
308
+ );
282
309
  return;
283
310
  }
284
311
  }
@@ -288,29 +315,41 @@ function createInitCommand() {
288
315
  }
289
316
  async function scaffold(root, options, detected, skipPrompts) {
290
317
  log.step("Generating configuration...");
291
- const claudeMd = generateClaudeMd(options, detected);
318
+ const claudeMd = generateClaudeMd(options, detected, {
319
+ superpowers: isSuperpowersInstalled()
320
+ });
292
321
  const tasksMd = generateTasksMd(options);
293
322
  const backlogMd = generateBacklogMd(options);
294
323
  const settings = generateSettings(detected);
295
324
  const claudeignore = generateClaudeignore(detected);
296
- await mkdir(join(root, ".claude", "rules"), { recursive: true });
297
- const settingsPath = join(root, ".claude", "settings.json");
298
- const mergedSettings = await mergeSettings(settingsPath, settings);
299
- const backlogPath = join(root, "BACKLOG.md");
325
+ await mkdir(join2(root, ".claude", "rules"), { recursive: true });
326
+ const settingsPath = join2(root, ".claude", "settings.json");
327
+ const mergedSettings = await mergeSettings(
328
+ settingsPath,
329
+ settings
330
+ );
331
+ const backlogPath = join2(root, "BACKLOG.md");
300
332
  const hasBacklog = await fileExists(backlogPath);
301
- const claudeignorePath = join(root, ".claudeignore");
333
+ const claudeignorePath = join2(root, ".claudeignore");
302
334
  const hasClaudeignore = await fileExists(claudeignorePath);
303
- const claudeGitignorePath = join(root, ".claude", ".gitignore");
335
+ const claudeGitignorePath = join2(root, ".claude", ".gitignore");
304
336
  const hasClaudeGitignore = await fileExists(claudeGitignorePath);
305
- const rulesPath = join(root, ".claude", "rules", "conventions.md");
337
+ const rulesPath = join2(root, ".claude", "rules", "conventions.md");
306
338
  const hasRules = await fileExists(rulesPath);
307
- const workflowRulePath = join(root, ".claude", "rules", "workflow.md");
339
+ const workflowRulePath = join2(root, ".claude", "rules", "workflow.md");
308
340
  const hasWorkflowRule = await fileExists(workflowRulePath);
309
- const hooksRulePath = join(root, ".claude", "rules", "hooks.md");
341
+ const hooksRulePath = join2(root, ".claude", "rules", "hooks.md");
310
342
  const hasHooksRule = await fileExists(hooksRulePath);
343
+ const verificationRulePath = join2(
344
+ root,
345
+ ".claude",
346
+ "rules",
347
+ "verification.md"
348
+ );
349
+ const hasVerificationRule = await fileExists(verificationRulePath);
311
350
  const writes = [
312
- writeFile(join(root, "CLAUDE.md"), claudeMd),
313
- writeFile(join(root, "TASKS.md"), tasksMd),
351
+ writeFile(join2(root, "CLAUDE.md"), claudeMd),
352
+ writeFile(join2(root, "TASKS.md"), tasksMd),
314
353
  writeFile(settingsPath, JSON.stringify(mergedSettings, null, 2) + "\n")
315
354
  ];
316
355
  if (!hasBacklog) {
@@ -320,16 +359,21 @@ async function scaffold(root, options, detected, skipPrompts) {
320
359
  writes.push(writeFile(claudeignorePath, claudeignore));
321
360
  }
322
361
  if (!hasClaudeGitignore) {
323
- writes.push(writeFile(claudeGitignorePath, [
324
- "# Local-only Claude Code files (never commit these)",
325
- "CLAUDE.md",
326
- "settings.local.json",
327
- "plans/",
328
- "memory/",
329
- "sessions/",
330
- "tmp/",
331
- ""
332
- ].join("\n")));
362
+ writes.push(
363
+ writeFile(
364
+ claudeGitignorePath,
365
+ [
366
+ "# Local-only Claude Code files (never commit these)",
367
+ "CLAUDE.md",
368
+ "settings.local.json",
369
+ "plans/",
370
+ "memory/",
371
+ "sessions/",
372
+ "tmp/",
373
+ ""
374
+ ].join("\n")
375
+ )
376
+ );
333
377
  }
334
378
  if (!hasRules) {
335
379
  const rulesContent = generateStarterRules(detected);
@@ -341,23 +385,60 @@ async function scaffold(root, options, detected, skipPrompts) {
341
385
  if (!hasHooksRule) {
342
386
  writes.push(writeFile(hooksRulePath, generateHooksRule()));
343
387
  }
388
+ if (!hasVerificationRule) {
389
+ writes.push(writeFile(verificationRulePath, generateVerificationRule()));
390
+ }
391
+ const reviewerAgentPath = join2(root, ".claude", "agents", "code-reviewer.md");
392
+ const hasReviewerAgent = await fileExists(reviewerAgentPath);
393
+ if (!hasReviewerAgent) {
394
+ await mkdir(join2(root, ".claude", "agents"), { recursive: true });
395
+ writes.push(writeFile(reviewerAgentPath, generateReviewerAgent()));
396
+ }
344
397
  await Promise.all(writes);
345
398
  await writeSprintHygieneScripts(root);
346
399
  await writeWorkflowCheckScript(root);
347
400
  log.success("Generated CLAUDE.md");
401
+ if (!hasReviewerAgent)
402
+ log.success(
403
+ "Generated .claude/agents/code-reviewer.md (independent sprint reviewer)"
404
+ );
348
405
  log.success("Generated TASKS.md");
349
406
  if (!hasBacklog) log.success("Generated BACKLOG.md");
350
407
  log.success("Generated .claude/settings.json (schema, permissions, hooks)");
351
408
  if (!hasClaudeGitignore) log.success("Generated .claude/.gitignore");
352
409
  if (!hasClaudeignore) log.success("Generated .claudeignore");
353
410
  if (!hasRules) log.success("Generated .claude/rules/conventions.md");
354
- if (!hasWorkflowRule) log.success("Generated .claude/rules/workflow.md (workflow rules, path-scoped)");
355
- if (!hasHooksRule) log.success("Generated .claude/rules/hooks.md (hook authoring rules, path-scoped)");
356
- log.success("Generated .claude/hooks/sprint-{size,open}-check.sh + workflow-check.sh");
411
+ if (!hasWorkflowRule)
412
+ log.success(
413
+ "Generated .claude/rules/workflow.md (workflow rules, path-scoped)"
414
+ );
415
+ if (!hasHooksRule)
416
+ log.success(
417
+ "Generated .claude/rules/hooks.md (hook authoring rules, path-scoped)"
418
+ );
419
+ if (!hasVerificationRule)
420
+ log.success(
421
+ "Generated .claude/rules/verification.md (evidence-before-assertion discipline)"
422
+ );
423
+ log.success(
424
+ "Generated .claude/hooks/sprint-{size,open}-check.sh + workflow-check.sh"
425
+ );
357
426
  await createEnhanceSkillPrompt(root, skipPrompts);
358
427
  log.blank();
428
+ if (!isJqAvailable()) {
429
+ log.warn(
430
+ "jq not found on PATH \u2014 the generated hooks (including the .env and destructive-command guards) will silently no-op until you install it: https://jqlang.github.io/jq/download/"
431
+ );
432
+ }
433
+ if (!isSuperpowersInstalled()) {
434
+ log.info(
435
+ "Optional: the superpowers plugin adds brainstorm/plan/TDD/review discipline \u2014 /plugin install superpowers@claude-plugins-official"
436
+ );
437
+ }
359
438
  log.success("Done! Run `claude` to start.");
360
- log.info("Use `/lp-enhance` inside Claude Code to have AI complete your CLAUDE.md.");
439
+ log.info(
440
+ "Use `/lp-enhance` inside Claude Code to have AI complete your CLAUDE.md."
441
+ );
361
442
  log.info("Run `claude-launchpad doctor` to check your config quality.");
362
443
  log.blank();
363
444
  }
@@ -386,16 +467,35 @@ function generateStarterRules(detected) {
386
467
  lines.push("- Prefer Result over unwrap/expect in library code");
387
468
  lines.push("- No unsafe blocks without a safety comment");
388
469
  }
470
+ lines.push("", "## Testing Discipline", "", TESTING_DISCIPLINE_CONTENT);
471
+ const verify = [detected.testCommand, detected.lintCommand].filter((c) => !!c).map((c) => `\`${c}\``).join(" && ");
472
+ lines.push("", "## Pre-Commit Checklist", "");
473
+ lines.push(
474
+ verify ? `1. Run ${verify} \u2014 never commit if either fails` : "1. Run the project's test and typecheck commands \u2014 never commit if either fails"
475
+ );
476
+ lines.push(
477
+ "2. For hard-TDD surfaces, confirm the test was written before the implementation"
478
+ );
479
+ lines.push(
480
+ "3. Before sprint-ending commits: run /code-review on the sprint diff (see ## Sprint Reviews in CLAUDE.md)"
481
+ );
389
482
  lines.push("", "## Skill Authoring", "", SKILL_AUTHORING_CONTENT);
390
483
  lines.push("");
391
484
  return lines.join("\n");
392
485
  }
393
486
  async function createEnhanceSkillPrompt(root, skipPrompts) {
394
- const projectPath = join(root, ".claude", "skills", "lp-enhance", "SKILL.md");
395
- const globalPath = join(homedir(), ".claude", "skills", "lp-enhance", "SKILL.md");
396
- const legacyProject = join(root, ".claude", "commands", "lp-enhance.md");
397
- const legacyGlobal = join(homedir(), ".claude", "commands", "lp-enhance.md");
398
- if (await fileExists(projectPath) || await fileExists(globalPath) || await fileExists(legacyProject) || await fileExists(legacyGlobal)) return;
487
+ const projectPath = join2(root, ".claude", "skills", "lp-enhance", "SKILL.md");
488
+ const globalPath = join2(
489
+ homedir2(),
490
+ ".claude",
491
+ "skills",
492
+ "lp-enhance",
493
+ "SKILL.md"
494
+ );
495
+ const legacyProject = join2(root, ".claude", "commands", "lp-enhance.md");
496
+ const legacyGlobal = join2(homedir2(), ".claude", "commands", "lp-enhance.md");
497
+ if (await fileExists(projectPath) || await fileExists(globalPath) || await fileExists(legacyProject) || await fileExists(legacyGlobal))
498
+ return;
399
499
  const scope = skipPrompts ? "project" : await select({
400
500
  message: "Install /lp-enhance skill (AI-powered CLAUDE.md improver):",
401
501
  choices: [
@@ -405,14 +505,16 @@ async function createEnhanceSkillPrompt(root, skipPrompts) {
405
505
  ]
406
506
  });
407
507
  if (scope === "skip") return;
408
- const targetDir = scope === "global" ? join(homedir(), ".claude", "skills", "lp-enhance") : join(root, ".claude", "skills", "lp-enhance");
508
+ const targetDir = scope === "global" ? join2(homedir2(), ".claude", "skills", "lp-enhance") : join2(root, ".claude", "skills", "lp-enhance");
409
509
  await mkdir(targetDir, { recursive: true });
410
- await writeFile(join(targetDir, "SKILL.md"), generateEnhanceSkill());
510
+ await writeFile(join2(targetDir, "SKILL.md"), generateEnhanceSkill());
411
511
  log.success(`Generated /lp-enhance skill (${scope} scope)`);
412
512
  }
413
513
  async function mergeSettings(existingPath, generated) {
414
514
  try {
415
- const existing = JSON.parse(await readFile(existingPath, "utf-8"));
515
+ const existing = JSON.parse(
516
+ await readFile(existingPath, "utf-8")
517
+ );
416
518
  const existingHooks = existing.hooks ?? {};
417
519
  const generatedHooks = generated.hooks ?? {};
418
520
  const mergedHooks = { ...existingHooks };
@@ -437,7 +539,7 @@ import chalk from "chalk";
437
539
 
438
540
  // src/lib/parser.ts
439
541
  import { readdir, access } from "fs/promises";
440
- import { join as join2, resolve } from "path";
542
+ import { join as join3, resolve } from "path";
441
543
  var CLAUDE_MD = "CLAUDE.md";
442
544
  var CLAUDE_DIR = ".claude";
443
545
  var SETTINGS_FILE = "settings.json";
@@ -445,26 +547,26 @@ var SETTINGS_LOCAL_FILE = "settings.local.json";
445
547
  var RULES_DIR = "rules";
446
548
  async function parseClaudeConfig(projectRoot) {
447
549
  const root = resolve(projectRoot);
448
- const claudeDir = join2(root, CLAUDE_DIR);
550
+ const claudeDir = join3(root, CLAUDE_DIR);
449
551
  const [claudeMd, localClaudeMd, settings, localSettings, hooks, rules, mcpServers, skills, claudeignore, worktreeInclude, gitWorktreesActive] = await Promise.all([
450
552
  readClaudeMd(root),
451
- readFileOrNull(join2(claudeDir, CLAUDE_MD)),
553
+ readFileOrNull(join3(claudeDir, CLAUDE_MD)),
452
554
  readSettings(claudeDir),
453
555
  readSettingsFromFile(claudeDir, SETTINGS_LOCAL_FILE),
454
556
  readHooks(claudeDir),
455
557
  readRules(claudeDir),
456
558
  readMcpServers(claudeDir, root),
457
559
  readSkills(claudeDir),
458
- readFileOrNull(join2(root, ".claudeignore")),
459
- readFileOrNull(join2(root, ".worktreeinclude")),
560
+ readFileOrNull(join3(root, ".claudeignore")),
561
+ readFileOrNull(join3(root, ".worktreeinclude")),
460
562
  detectGitWorktrees(root)
461
563
  ]);
462
564
  const instructionCount = claudeMd ? countInstructions(claudeMd) : 0;
463
565
  return {
464
- claudeMdPath: claudeMd !== null ? join2(root, CLAUDE_MD) : null,
566
+ claudeMdPath: claudeMd !== null ? join3(root, CLAUDE_MD) : null,
465
567
  claudeMdContent: claudeMd,
466
568
  claudeMdInstructionCount: instructionCount,
467
- settingsPath: settings !== null ? join2(claudeDir, SETTINGS_FILE) : null,
569
+ settingsPath: settings !== null ? join3(claudeDir, SETTINGS_FILE) : null,
468
570
  settings,
469
571
  localClaudeMdContent: localClaudeMd,
470
572
  localSettings,
@@ -472,23 +574,23 @@ async function parseClaudeConfig(projectRoot) {
472
574
  rules,
473
575
  mcpServers,
474
576
  skills,
475
- claudeignorePath: claudeignore !== null ? join2(root, ".claudeignore") : null,
577
+ claudeignorePath: claudeignore !== null ? join3(root, ".claudeignore") : null,
476
578
  claudeignoreContent: claudeignore,
477
- worktreeIncludePath: worktreeInclude !== null ? join2(root, ".worktreeinclude") : null,
579
+ worktreeIncludePath: worktreeInclude !== null ? join3(root, ".worktreeinclude") : null,
478
580
  worktreeIncludeContent: worktreeInclude,
479
581
  gitWorktreesActive
480
582
  };
481
583
  }
482
584
  async function detectGitWorktrees(root) {
483
585
  try {
484
- const entries = await readdir(join2(root, ".git", "worktrees"));
586
+ const entries = await readdir(join3(root, ".git", "worktrees"));
485
587
  return entries.length > 0;
486
588
  } catch {
487
589
  return false;
488
590
  }
489
591
  }
490
592
  async function readClaudeMd(root) {
491
- return readFileOrNull(join2(root, CLAUDE_MD));
593
+ return readFileOrNull(join3(root, CLAUDE_MD));
492
594
  }
493
595
  function countInstructions(content) {
494
596
  const lines = content.split("\n");
@@ -507,7 +609,7 @@ async function readSettings(claudeDir) {
507
609
  return readSettingsFromFile(claudeDir, SETTINGS_FILE);
508
610
  }
509
611
  async function readSettingsFromFile(claudeDir, filename) {
510
- const path = join2(claudeDir, filename);
612
+ const path = join3(claudeDir, filename);
511
613
  const raw = await readFileOrNull(path);
512
614
  if (raw === null) return null;
513
615
  try {
@@ -519,8 +621,8 @@ async function readSettingsFromFile(claudeDir, filename) {
519
621
  }
520
622
  async function readHooks(claudeDir) {
521
623
  const [shared, local] = await Promise.all([
522
- readHooksFromFile(join2(claudeDir, SETTINGS_FILE)),
523
- readHooksFromFile(join2(claudeDir, SETTINGS_LOCAL_FILE))
624
+ readHooksFromFile(join3(claudeDir, SETTINGS_FILE)),
625
+ readHooksFromFile(join3(claudeDir, SETTINGS_LOCAL_FILE))
524
626
  ]);
525
627
  return [...shared, ...local];
526
628
  }
@@ -566,14 +668,14 @@ async function readHooksFromFile(settingsPath) {
566
668
  }
567
669
  }
568
670
  async function readRules(claudeDir) {
569
- const rulesDir = join2(claudeDir, RULES_DIR);
671
+ const rulesDir = join3(claudeDir, RULES_DIR);
570
672
  return listFilesRecursive(rulesDir, ".md");
571
673
  }
572
674
  async function readMcpServers(claudeDir, projectRoot) {
573
675
  const [fromMcpJson, fromSettings, fromLocalSettings] = await Promise.all([
574
- readMcpJsonFile(join2(projectRoot, ".mcp.json")),
575
- readMcpServersFromSettings(join2(claudeDir, SETTINGS_FILE)),
576
- readMcpServersFromSettings(join2(claudeDir, SETTINGS_LOCAL_FILE))
676
+ readMcpJsonFile(join3(projectRoot, ".mcp.json")),
677
+ readMcpServersFromSettings(join3(claudeDir, SETTINGS_FILE)),
678
+ readMcpServersFromSettings(join3(claudeDir, SETTINGS_LOCAL_FILE))
577
679
  ]);
578
680
  const seen = /* @__PURE__ */ new Set();
579
681
  const result = [];
@@ -630,8 +732,8 @@ async function readMcpServersFromSettings(settingsPath) {
630
732
  }
631
733
  }
632
734
  async function readSkills(claudeDir) {
633
- const commandsDir = join2(claudeDir, "commands");
634
- const skillsDir = join2(claudeDir, "skills");
735
+ const commandsDir = join3(claudeDir, "commands");
736
+ const skillsDir = join3(claudeDir, "skills");
635
737
  const [commands, skills] = await Promise.all([
636
738
  listFilesRecursive(commandsDir, ".md"),
637
739
  listFilesRecursive(skillsDir, ".md")
@@ -647,7 +749,7 @@ async function listFilesRecursive(dir, ext) {
647
749
  const results = [];
648
750
  const entries = await readdir(dir, { withFileTypes: true });
649
751
  for (const entry of entries) {
650
- const fullPath = join2(dir, entry.name);
752
+ const fullPath = join3(dir, entry.name);
651
753
  if (entry.isDirectory()) {
652
754
  const nested = await listFilesRecursive(fullPath, ext);
653
755
  results.push(...nested);
@@ -740,12 +842,12 @@ async function analyzeSettings(config) {
740
842
  });
741
843
  }
742
844
  const allowedTools = config.settings.allowedTools;
743
- if (allowedTools && allowedTools.length > 0 && config.hooks.length === 0) {
845
+ if (allowedTools && allowedTools.length > 0) {
744
846
  issues.push({
745
847
  analyzer: "Settings",
746
- severity: "medium",
747
- message: "Tools auto-allowed without any hooks \u2014 no safety net for dangerous operations",
748
- fix: "Add PreToolUse hooks for security or remove allowedTools to use interactive prompting"
848
+ severity: "low",
849
+ message: "Legacy allowedTools key in settings.json \u2014 superseded by permissions.allow",
850
+ fix: 'Move entries to permissions.allow (e.g. "Bash(npm test)") and remove allowedTools'
749
851
  });
750
852
  }
751
853
  if (config.settings.includeCoAuthoredBy !== void 0) {
@@ -780,7 +882,7 @@ async function analyzeSettings(config) {
780
882
  }
781
883
 
782
884
  // src/commands/doctor/analyzers/hooks.ts
783
- async function analyzeHooks(config) {
885
+ async function analyzeHooks(config, projectRoot = "") {
784
886
  const issues = [];
785
887
  const hooks = config.hooks;
786
888
  for (const h of hooks) {
@@ -835,13 +937,35 @@ async function analyzeHooks(config) {
835
937
  fix: "Add PreToolUse hooks for file protection and dangerous command blocking"
836
938
  });
837
939
  }
838
- const hasPostCompact = hooks.some((h) => h.event === "PostCompact");
839
- if (!hasPostCompact) {
940
+ const usesJq = hooks.some((h) => h.command?.includes("jq "));
941
+ if (usesJq && !isJqAvailable()) {
840
942
  issues.push({
841
943
  analyzer: "Hooks",
842
- severity: "low",
843
- message: "No PostCompact hook \u2014 session context is lost when conversation is compacted",
844
- fix: "Add a PostCompact hook that re-injects TASKS.md after compaction"
944
+ severity: "medium",
945
+ message: "jq not found on PATH \u2014 every generated hook (including the .env and destructive-command guards) silently no-ops without it",
946
+ fix: "Install jq (https://jqlang.github.io/jq/download/) \u2014 hooks read tool input as JSON from stdin"
947
+ });
948
+ }
949
+ const hasInertPostCompact = hooks.some(
950
+ (h) => h.event === "PostCompact" && !!h.command && h.command.includes("TASKS.md")
951
+ );
952
+ if (hasInertPostCompact) {
953
+ issues.push({
954
+ analyzer: "Hooks",
955
+ severity: "high",
956
+ message: "PostCompact hooks can't inject context \u2014 this TASKS.md re-injection never reaches the model. Session continuity after compaction is silently broken",
957
+ fix: "Run `doctor --fix` to move the injection to a SessionStart hook with a compact matcher"
958
+ });
959
+ }
960
+ const hasCompactMatcher = hooks.some(
961
+ (h) => h.event === "SessionStart" && ((h.matcher ?? "") === "" || (h.matcher ?? "").includes("compact"))
962
+ );
963
+ if (!hasCompactMatcher) {
964
+ issues.push({
965
+ analyzer: "Hooks",
966
+ severity: "medium",
967
+ message: "No SessionStart hook with a compact matcher \u2014 session context is lost when the conversation is compacted",
968
+ fix: "Run `doctor --fix` to widen the SessionStart matcher to startup|resume|compact|clear"
845
969
  });
846
970
  }
847
971
  const hasSessionStart = hooks.some((h) => h.event === "SessionStart");
@@ -868,7 +992,7 @@ async function analyzeHooks(config) {
868
992
  analyzer: "Hooks",
869
993
  severity: "low",
870
994
  message: "No sprint-open-check hook \u2014 opening a new sprint without removing pulled WPs from BACKLOG silently drifts",
871
- fix: "Add PreToolUse Bash hook calling .claude/hooks/sprint-open-check.sh"
995
+ fix: "Add PostToolUse Bash hook calling .claude/hooks/sprint-open-check.sh"
872
996
  });
873
997
  }
874
998
  if (!hooks.some((h) => h.command?.includes("Sprint complete"))) {
@@ -888,18 +1012,57 @@ async function analyzeHooks(config) {
888
1012
  });
889
1013
  }
890
1014
  }
1015
+ const sprintOpenOnPreToolUse = hooks.some(
1016
+ (h) => h.event === "PreToolUse" && !!h.command && h.command.includes("sprint-open-check.sh")
1017
+ );
1018
+ if (sprintOpenOnPreToolUse) {
1019
+ issues.push({
1020
+ analyzer: "Hooks",
1021
+ severity: "medium",
1022
+ message: "sprint-open-check.sh is registered on PreToolUse \u2014 context injection is impossible there and the current script inspects the last commit. It belongs on PostToolUse",
1023
+ fix: "Run `doctor --fix` to move it to PostToolUse and refresh the script"
1024
+ });
1025
+ }
1026
+ const staleEchoNudge = hooks.some(
1027
+ (h) => !!h.command && h.command.includes("Sprint complete") && !h.command.includes("additionalContext")
1028
+ );
1029
+ if (staleEchoNudge) {
1030
+ issues.push({
1031
+ analyzer: "Hooks",
1032
+ severity: "medium",
1033
+ message: "sprint-complete nudge uses bare stdout \u2014 PostToolUse stdout never reaches the model, so the nudge is invisible",
1034
+ fix: "Run `doctor --fix` to upgrade it to additionalContext JSON"
1035
+ });
1036
+ }
1037
+ if (projectRoot !== "" && await hasStaleHygieneScript(projectRoot)) {
1038
+ issues.push({
1039
+ analyzer: "Hooks",
1040
+ severity: "medium",
1041
+ message: "Outdated hygiene script(s) in .claude/hooks \u2014 their warnings use bare stdout and never reach the model",
1042
+ fix: "Run `doctor --fix` to refresh workflow-check.sh / sprint-open-check.sh"
1043
+ });
1044
+ }
891
1045
  const score = Math.max(0, 100 - issues.length * 15);
892
1046
  return { name: "Hooks", issues, score };
893
1047
  }
1048
+ async function hasStaleHygieneScript(projectRoot) {
1049
+ const { readFile: readFile7 } = await import("fs/promises");
1050
+ const { join: join14 } = await import("path");
1051
+ for (const name of ["workflow-check.sh", "sprint-open-check.sh"]) {
1052
+ const content = await readFile7(join14(projectRoot, ".claude", "hooks", name), "utf-8").catch(() => null);
1053
+ if (content !== null && !content.includes("hookSpecificOutput")) return true;
1054
+ }
1055
+ return false;
1056
+ }
894
1057
 
895
1058
  // src/commands/doctor/analyzers/rules.ts
896
1059
  import { readFile as readFile2 } from "fs/promises";
897
- import { basename, join as join3, dirname } from "path";
898
- import { homedir as homedir2 } from "os";
1060
+ import { basename, join as join4, dirname } from "path";
1061
+ import { homedir as homedir3 } from "os";
899
1062
  async function analyzeRules(config) {
900
1063
  const issues = [];
901
1064
  const projectRoot = config.claudeMdPath ? dirname(config.claudeMdPath) : process.cwd();
902
- const hasBacklog = await fileExists(join3(projectRoot, "BACKLOG.md"));
1065
+ const hasBacklog = await fileExists(join4(projectRoot, "BACKLOG.md"));
903
1066
  if (!hasBacklog) {
904
1067
  issues.push({
905
1068
  analyzer: "Rules",
@@ -908,7 +1071,9 @@ async function analyzeRules(config) {
908
1071
  fix: "Run `claude-launchpad init` or `doctor --fix` to generate one"
909
1072
  });
910
1073
  }
911
- const hasWorkflowRule = await fileExists(join3(projectRoot, ".claude", "rules", "workflow.md"));
1074
+ const hasWorkflowRule = await fileExists(
1075
+ join4(projectRoot, ".claude", "rules", "workflow.md")
1076
+ );
912
1077
  if (!hasWorkflowRule) {
913
1078
  issues.push({
914
1079
  analyzer: "Rules",
@@ -917,7 +1082,43 @@ async function analyzeRules(config) {
917
1082
  fix: "Run `doctor --fix` to generate it"
918
1083
  });
919
1084
  }
920
- const hasHooksRule = await fileExists(join3(projectRoot, ".claude", "rules", "hooks.md"));
1085
+ const hasReviewerAgent = await fileExists(
1086
+ join4(projectRoot, ".claude", "agents", "code-reviewer.md")
1087
+ );
1088
+ if (!hasReviewerAgent) {
1089
+ issues.push({
1090
+ analyzer: "Rules",
1091
+ severity: "low",
1092
+ message: "No .claude/agents/code-reviewer.md \u2014 sprint reviews run as same-model self-checks",
1093
+ fix: "Run `doctor --fix` to generate a fresh-context reviewer agent"
1094
+ });
1095
+ }
1096
+ if (!isSuperpowersInstalled()) {
1097
+ issues.push({
1098
+ analyzer: "Rules",
1099
+ severity: "info",
1100
+ message: "Optional: the superpowers plugin adds brainstorm/plan/TDD/review discipline \u2014 /plugin install superpowers@claude-plugins-official"
1101
+ });
1102
+ }
1103
+ if (hasWorkflowRule) {
1104
+ const wfContent = await readFile2(
1105
+ join4(projectRoot, ".claude", "rules", "workflow.md"),
1106
+ "utf-8"
1107
+ ).catch(() => "");
1108
+ const wfMatch = wfContent.match(/<!-- lp-workflow-version: (\d+) -->/);
1109
+ const wfVersion = wfMatch ? parseInt(wfMatch[1], 10) : null;
1110
+ if (wfVersion !== null && wfVersion < WORKFLOW_RULE_VERSION) {
1111
+ issues.push({
1112
+ analyzer: "Rules",
1113
+ severity: "low",
1114
+ message: `workflow.md rule is outdated (v${wfVersion}, latest v${WORKFLOW_RULE_VERSION})`,
1115
+ fix: "Run `doctor --fix` to update it"
1116
+ });
1117
+ }
1118
+ }
1119
+ const hasHooksRule = await fileExists(
1120
+ join4(projectRoot, ".claude", "rules", "hooks.md")
1121
+ );
921
1122
  if (!hasHooksRule) {
922
1123
  issues.push({
923
1124
  analyzer: "Rules",
@@ -926,7 +1127,34 @@ async function analyzeRules(config) {
926
1127
  fix: "Run `doctor --fix` to generate it"
927
1128
  });
928
1129
  }
929
- const hasClaudeignore = await fileExists(join3(projectRoot, ".claudeignore"));
1130
+ const verificationPath = join4(
1131
+ projectRoot,
1132
+ ".claude",
1133
+ "rules",
1134
+ "verification.md"
1135
+ );
1136
+ const hasVerificationRule = await fileExists(verificationPath);
1137
+ if (!hasVerificationRule) {
1138
+ issues.push({
1139
+ analyzer: "Rules",
1140
+ severity: "medium",
1141
+ message: "No .claude/rules/verification.md found \u2014 nothing stops premature 'done' claims without evidence",
1142
+ fix: "Run `doctor --fix` to generate it"
1143
+ });
1144
+ } else {
1145
+ const vContent = await readFile2(verificationPath, "utf-8").catch(() => "");
1146
+ const vMatch = vContent.match(/<!-- lp-verification-version: (\d+) -->/);
1147
+ const vVersion = vMatch ? parseInt(vMatch[1], 10) : null;
1148
+ if (vVersion !== null && vVersion < VERIFICATION_RULE_VERSION) {
1149
+ issues.push({
1150
+ analyzer: "Rules",
1151
+ severity: "low",
1152
+ message: `verification.md rule is outdated (v${vVersion}, latest v${VERIFICATION_RULE_VERSION})`,
1153
+ fix: "Run `doctor --fix` to update it"
1154
+ });
1155
+ }
1156
+ }
1157
+ const hasClaudeignore = await fileExists(join4(projectRoot, ".claudeignore"));
930
1158
  if (!hasClaudeignore) {
931
1159
  issues.push({
932
1160
  analyzer: "Rules",
@@ -938,7 +1166,9 @@ async function analyzeRules(config) {
938
1166
  const hasSkillInProject = config.skills.some(
939
1167
  (s) => basename(s) === "SKILL.md" && s.includes("lp-enhance") || basename(s) === "lp-enhance.md"
940
1168
  );
941
- const hasSkillGlobal = await fileExists(join3(homedir2(), ".claude", "skills", "lp-enhance", "SKILL.md")) || await fileExists(join3(homedir2(), ".claude", "commands", "lp-enhance.md"));
1169
+ const hasSkillGlobal = await fileExists(
1170
+ join4(homedir3(), ".claude", "skills", "lp-enhance", "SKILL.md")
1171
+ ) || await fileExists(join4(homedir3(), ".claude", "commands", "lp-enhance.md"));
942
1172
  if (!hasSkillInProject && !hasSkillGlobal) {
943
1173
  issues.push({
944
1174
  analyzer: "Rules",
@@ -1012,13 +1242,14 @@ async function analyzeRules(config) {
1012
1242
  });
1013
1243
  }
1014
1244
  }
1015
- const score = Math.max(0, 100 - issues.length * 10);
1245
+ const actionable = issues.filter((i) => i.severity !== "info").length;
1246
+ const score = Math.max(0, 100 - actionable * 10);
1016
1247
  return { name: "Rules", issues, score };
1017
1248
  }
1018
1249
  async function getSkillVersion(projectRoot) {
1019
1250
  const paths = [
1020
- join3(projectRoot, ".claude", "skills", "lp-enhance", "SKILL.md"),
1021
- join3(homedir2(), ".claude", "skills", "lp-enhance", "SKILL.md")
1251
+ join4(projectRoot, ".claude", "skills", "lp-enhance", "SKILL.md"),
1252
+ join4(homedir3(), ".claude", "skills", "lp-enhance", "SKILL.md")
1022
1253
  ];
1023
1254
  for (const p of paths) {
1024
1255
  try {
@@ -1033,13 +1264,44 @@ async function getSkillVersion(projectRoot) {
1033
1264
  return null;
1034
1265
  }
1035
1266
 
1267
+ // src/lib/memory-registration.ts
1268
+ import { readFileSync } from "fs";
1269
+ import { join as join5 } from "path";
1270
+ import { homedir as homedir4 } from "os";
1271
+ function isMemoryMcpRegistered(projectRoot) {
1272
+ return hasMemoryServerInJson(join5(projectRoot, ".mcp.json"), "mcpServers") || hasMemoryServerInJson(join5(projectRoot, ".claude", "settings.local.json"), "mcpServers") || hasMemoryServerInUserConfig(projectRoot);
1273
+ }
1274
+ function hasMemoryServerInJson(path, key) {
1275
+ try {
1276
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
1277
+ const servers = parsed[key];
1278
+ return !!servers && typeof servers === "object" && "agentic-memory" in servers;
1279
+ } catch {
1280
+ return false;
1281
+ }
1282
+ }
1283
+ function hasMemoryServerInUserConfig(projectRoot) {
1284
+ try {
1285
+ const parsed = JSON.parse(readFileSync(join5(homedir4(), ".claude.json"), "utf-8"));
1286
+ const projects = parsed.projects;
1287
+ const project = projects?.[projectRoot];
1288
+ const scoped = project?.mcpServers;
1289
+ if (scoped && "agentic-memory" in scoped) return true;
1290
+ const global = parsed.mcpServers;
1291
+ return !!global && "agentic-memory" in global;
1292
+ } catch {
1293
+ return false;
1294
+ }
1295
+ }
1296
+
1036
1297
  // src/commands/doctor/analyzers/permissions.ts
1037
- async function analyzePermissions(config) {
1298
+ async function analyzePermissions(config, projectRoot) {
1038
1299
  const issues = [];
1039
1300
  const settings = config.settings;
1040
1301
  const permissions = settings?.permissions;
1041
1302
  const denyList = permissions?.deny ?? [];
1042
1303
  const allowList = permissions?.allow ?? [];
1304
+ const legacyAllowList = settings?.allowedTools ?? [];
1043
1305
  const credentialPatterns = ["Read(~/.ssh/*)", "Read(~/.aws/*)", "Read(~/.npmrc)"];
1044
1306
  const missingCreds = credentialPatterns.filter((p) => !denyList.includes(p));
1045
1307
  if (missingCreds.length > 0) {
@@ -1050,8 +1312,8 @@ async function analyzePermissions(config) {
1050
1312
  fix: "Add Read(~/.ssh/*), Read(~/.aws/*), Read(~/.npmrc) to permissions.deny"
1051
1313
  });
1052
1314
  }
1053
- const hasBlanketBash = allowList.some((a) => a === "Bash" || a.startsWith("Bash") && !a.includes("("));
1054
- if (hasBlanketBash) {
1315
+ const isBlanketBash = (a) => a === "Bash";
1316
+ if ([...allowList, ...legacyAllowList].some(isBlanketBash)) {
1055
1317
  issues.push({
1056
1318
  analyzer: "Permissions",
1057
1319
  severity: "high",
@@ -1068,12 +1330,13 @@ async function analyzePermissions(config) {
1068
1330
  });
1069
1331
  }
1070
1332
  const sandbox = settings?.sandbox;
1071
- if (sandbox?.enabled === true) {
1333
+ const memoryInUse = config.mcpServers.some((s) => s.name === "agentic-memory") || isMemoryMcpRegistered(projectRoot);
1334
+ if (sandbox?.enabled === true && memoryInUse && !sandboxAllowsMemoryWrites(sandbox)) {
1072
1335
  issues.push({
1073
1336
  analyzer: "Permissions",
1074
- severity: "high",
1075
- message: "Filesystem sandbox enabled \u2014 blocks memory MCP and other cross-project tooling. Deny rules already cover the threat model.",
1076
- fix: 'Remove the "sandbox" block from settings.json'
1337
+ severity: "medium",
1338
+ message: "Sandbox lacks a write grant for ~/.agentic-memory \u2014 Bash-run memory commands (push/pull hooks, CLI) may fail inside the sandbox",
1339
+ fix: `Add "~/.agentic-memory" to sandbox.filesystem.allowWrite \u2014 scope the sandbox, don't disable it`
1077
1340
  });
1078
1341
  }
1079
1342
  const hasEnvHook = config.hooks.some((h) => h.command?.includes(".env"));
@@ -1089,23 +1352,8 @@ async function analyzePermissions(config) {
1089
1352
  });
1090
1353
  }
1091
1354
  }
1092
- const hasBashSecurity = config.hooks.some(
1093
- (h) => h.event === "PreToolUse" && (h.matcher?.includes("Bash") || !h.matcher)
1094
- );
1095
- const bashAllowed = settings?.allowedTools;
1096
- const hasBashAutoAllow = bashAllowed?.some(
1097
- (t) => typeof t === "string" && t.toLowerCase().includes("bash")
1098
- );
1099
- if (hasBashAutoAllow && !hasBashSecurity) {
1100
- issues.push({
1101
- analyzer: "Permissions",
1102
- severity: "high",
1103
- message: "Bash is auto-allowed without a security hook \u2014 dangerous commands could run unchecked",
1104
- fix: "Add a PreToolUse hook for Bash that blocks destructive commands"
1105
- });
1106
- }
1107
1355
  const hasForceProtection = config.hooks.some(
1108
- (h) => h.event === "PreToolUse" && h.command?.includes("force")
1356
+ (h) => h.event === "PreToolUse" && !!h.command && /push/i.test(h.command) && /--force|-f\b|force/i.test(h.command)
1109
1357
  );
1110
1358
  if (!hasForceProtection) {
1111
1359
  issues.push({
@@ -1115,17 +1363,6 @@ async function analyzePermissions(config) {
1115
1363
  fix: "Add a PreToolUse hook that warns on `git push --force` commands"
1116
1364
  });
1117
1365
  }
1118
- if (config.claudeMdContent) {
1119
- const hasOffLimits = config.claudeMdContent.includes("## Off-Limits") || config.claudeMdContent.includes("## off-limits");
1120
- if (!hasOffLimits) {
1121
- issues.push({
1122
- analyzer: "Permissions",
1123
- severity: "medium",
1124
- message: "No Off-Limits section in CLAUDE.md \u2014 Claude has no guardrails beyond defaults",
1125
- fix: "Add an ## Off-Limits section with project-specific restrictions"
1126
- });
1127
- }
1128
- }
1129
1366
  if (config.gitWorktreesActive) {
1130
1367
  const content = config.worktreeIncludeContent;
1131
1368
  const hasEntries = content !== null && content.split("\n").some((l) => {
@@ -1144,6 +1381,11 @@ async function analyzePermissions(config) {
1144
1381
  const score = Math.max(0, 100 - issues.length * 15);
1145
1382
  return { name: "Permissions", issues, score };
1146
1383
  }
1384
+ function sandboxAllowsMemoryWrites(sandbox) {
1385
+ const filesystem = sandbox.filesystem;
1386
+ const allowWrite = filesystem?.allowWrite ?? [];
1387
+ return allowWrite.some((p) => p.includes(".agentic-memory"));
1388
+ }
1147
1389
 
1148
1390
  // src/commands/doctor/analyzers/mcp.ts
1149
1391
  import { access as access2 } from "fs/promises";
@@ -1397,6 +1639,17 @@ async function analyzeMemory(config, projectRoot) {
1397
1639
  fix: "Run `doctor --fix` to add agentic-memory to allowedMcpServers"
1398
1640
  });
1399
1641
  }
1642
+ const hasBareBinaryHook = config.hooks.some(
1643
+ (h) => h.command?.includes("claude-launchpad memory") && !h.command.includes("npx claude-launchpad")
1644
+ );
1645
+ if (hasBareBinaryHook) {
1646
+ issues.push({
1647
+ analyzer: "Memory",
1648
+ severity: "medium",
1649
+ message: "Memory hook(s) call the bare claude-launchpad binary \u2014 fails silently on machines without a global install",
1650
+ fix: "Run `doctor --fix` to rewrite them to the npx form"
1651
+ });
1652
+ }
1400
1653
  const syncConfig = readSyncConfig();
1401
1654
  if (syncConfig) {
1402
1655
  const hasSessionStartPull = config.hooks.some(
@@ -1706,6 +1959,14 @@ async function analyzeQuality(config, projectRoot) {
1706
1959
  fix: "Fill in the TODO sections or remove them"
1707
1960
  });
1708
1961
  }
1962
+ if (content.includes(STALE_SWARM_PHRASE)) {
1963
+ issues.push({
1964
+ analyzer: "Quality",
1965
+ severity: "low",
1966
+ message: "Stop-and-Swarm section is outdated \u2014 it references the nonexistent 'Agent tool' (subagents are dispatched via the Task tool)",
1967
+ fix: "Run `doctor --fix` to modernize the known phrase (custom content is left alone)"
1968
+ });
1969
+ }
1709
1970
  const plainMemory = (content.match(/^## Memory\s*$/gm) ?? []).length;
1710
1971
  const taggedMemory = (content.match(/^## Memory \(agentic-memory\)\s*$/gm) ?? []).length;
1711
1972
  if (plainMemory + taggedMemory > 1) {
@@ -1724,9 +1985,103 @@ async function analyzeQuality(config, projectRoot) {
1724
1985
  return { name: "CLAUDE.md Quality", issues, score };
1725
1986
  }
1726
1987
 
1988
+ // src/commands/doctor/analyzers/workflow.ts
1989
+ import { readFile as readFile4 } from "fs/promises";
1990
+ import { join as join6 } from "path";
1991
+ var WP_FIELDS = [
1992
+ "Priority:",
1993
+ "Proposed:",
1994
+ "Stories / Docs:",
1995
+ "Depends on:",
1996
+ "Estimate:",
1997
+ "Trigger to pull:",
1998
+ "Definition of done:"
1999
+ ];
2000
+ var STALE_P0_DAYS = 14;
2001
+ var STALE_CHANGELOG_DAYS = 30;
2002
+ async function analyzeWorkflow(projectRoot, now = /* @__PURE__ */ new Date()) {
2003
+ const backlog = await readFile4(join6(projectRoot, "BACKLOG.md"), "utf-8").catch(() => null);
2004
+ if (backlog === null) return null;
2005
+ const issues = [];
2006
+ const pSections = extractPSections(backlog);
2007
+ const entries = parseWpEntries(pSections);
2008
+ const incomplete = entries.map((e) => ({ id: e.id, missing: WP_FIELDS.filter((f) => !e.body.includes(f)) })).filter((e) => e.missing.length > 0);
2009
+ if (incomplete.length > 0) {
2010
+ const sample = incomplete.slice(0, 3).map((e) => `${e.id} (missing ${e.missing.map((f) => f.replace(":", "")).join(", ")})`).join("; ");
2011
+ issues.push({
2012
+ analyzer: "Workflow",
2013
+ severity: "medium",
2014
+ message: `${incomplete.length} WP entr${incomplete.length === 1 ? "y" : "ies"} missing mandatory template fields: ${sample}`,
2015
+ fix: "Fill in all 7 fields per the template in BACKLOG.md \u2014 unpullable WPs rot"
2016
+ });
2017
+ }
2018
+ const p0Section = extractSection(backlog, /^## P0/m);
2019
+ const staleP0 = parseWpEntries(p0Section).filter((e) => {
2020
+ const proposed = e.body.match(/Proposed:\*{0,2}\s*(\d{4}-\d{2}-\d{2})/);
2021
+ return proposed !== null && daysBetween(proposed[1], now) > STALE_P0_DAYS;
2022
+ });
2023
+ if (staleP0.length > 0) {
2024
+ issues.push({
2025
+ analyzer: "Workflow",
2026
+ severity: "medium",
2027
+ message: `${staleP0.length} P0 item(s) older than ${STALE_P0_DAYS} days (${staleP0.map((e) => e.id).join(", ")}) \u2014 P0 means next sprint`,
2028
+ fix: "Pull them into a sprint or demote them (workflow.md priority discipline)"
2029
+ });
2030
+ }
2031
+ const changelog = extractSection(backlog, /^## Changelog/m);
2032
+ const dates = [...changelog.matchAll(/(\d{4}-\d{2}-\d{2})/g)].map((m) => m[1]).sort();
2033
+ const lastDate = dates[dates.length - 1];
2034
+ if (entries.length > 0 && lastDate !== void 0 && daysBetween(lastDate, now) > STALE_CHANGELOG_DAYS) {
2035
+ issues.push({
2036
+ analyzer: "Workflow",
2037
+ severity: "low",
2038
+ message: `BACKLOG.md changelog has been silent for ${daysBetween(lastDate, now)} days while ${entries.length} WP(s) sit in the backlog \u2014 force a review`,
2039
+ fix: "Review priorities: promote, demote, or delete stale WPs, and log the pass in ## Changelog"
2040
+ });
2041
+ }
2042
+ const score = Math.max(0, 100 - issues.length * 15);
2043
+ return { name: "Workflow", issues, score };
2044
+ }
2045
+ function extractPSections(backlog) {
2046
+ const lines = backlog.split("\n");
2047
+ const out = [];
2048
+ let inP = false;
2049
+ for (const line of lines) {
2050
+ if (/^## P[0-3]/.test(line)) inP = true;
2051
+ else if (/^## /.test(line)) inP = false;
2052
+ if (inP) out.push(line);
2053
+ }
2054
+ return out.join("\n");
2055
+ }
2056
+ function extractSection(content, heading) {
2057
+ const lines = content.split("\n");
2058
+ const out = [];
2059
+ let inSection = false;
2060
+ for (const line of lines) {
2061
+ if (heading.test(line)) {
2062
+ inSection = true;
2063
+ continue;
2064
+ } else if (/^## /.test(line)) inSection = false;
2065
+ if (inSection) out.push(line);
2066
+ }
2067
+ return out.join("\n");
2068
+ }
2069
+ function parseWpEntries(section) {
2070
+ const entries = [];
2071
+ const parts = section.split(/^### /m).slice(1);
2072
+ for (const part of parts) {
2073
+ const idMatch = part.match(/^(WP-\d{3,})/);
2074
+ if (idMatch) entries.push({ id: idMatch[1], body: part });
2075
+ }
2076
+ return entries;
2077
+ }
2078
+ function daysBetween(isoDate, now) {
2079
+ return Math.floor((now.getTime() - new Date(isoDate).getTime()) / 864e5);
2080
+ }
2081
+
1727
2082
  // src/commands/doctor/watcher.ts
1728
2083
  import { readdir as readdir2, stat } from "fs/promises";
1729
- import { join as join4 } from "path";
2084
+ import { join as join7 } from "path";
1730
2085
  async function watchConfig(projectRoot) {
1731
2086
  await runAndDisplay(projectRoot);
1732
2087
  log.blank();
@@ -1749,16 +2104,16 @@ async function watchConfig(projectRoot) {
1749
2104
  }
1750
2105
  async function getFileSnapshot(projectRoot) {
1751
2106
  const files = [
1752
- join4(projectRoot, "CLAUDE.md"),
1753
- join4(projectRoot, ".claudeignore")
2107
+ join7(projectRoot, "CLAUDE.md"),
2108
+ join7(projectRoot, ".claudeignore")
1754
2109
  ];
1755
- const claudeDir = join4(projectRoot, ".claude");
2110
+ const claudeDir = join7(projectRoot, ".claude");
1756
2111
  try {
1757
2112
  const entries = await readdir2(claudeDir, { withFileTypes: true, recursive: true });
1758
2113
  for (const entry of entries) {
1759
2114
  if (entry.isFile()) {
1760
2115
  const parentPath = entry.parentPath ?? claudeDir;
1761
- files.push(join4(parentPath, entry.name));
2116
+ files.push(join7(parentPath, entry.name));
1762
2117
  }
1763
2118
  }
1764
2119
  } catch {
@@ -1787,9 +2142,9 @@ async function runAndDisplay(projectRoot) {
1787
2142
  analyzeBudget(config),
1788
2143
  analyzeQuality(config, projectRoot),
1789
2144
  analyzeSettings(config),
1790
- analyzeHooks(config),
2145
+ analyzeHooks(config, projectRoot),
1791
2146
  analyzeRules(config),
1792
- analyzePermissions(config),
2147
+ analyzePermissions(config, projectRoot),
1793
2148
  analyzeMcp(config)
1794
2149
  ]);
1795
2150
  renderDoctorReport(results);
@@ -1813,19 +2168,7 @@ function createDoctorCommand() {
1813
2168
  log.info("Run `claude-launchpad init` to set up a project, or cd into a configured project.");
1814
2169
  process.exit(1);
1815
2170
  }
1816
- const results = await Promise.all([
1817
- analyzeBudget(config),
1818
- analyzeQuality(config, opts.path),
1819
- analyzeSettings(config),
1820
- analyzeHooks(config),
1821
- analyzeRules(config),
1822
- analyzePermissions(config),
1823
- analyzeMcp(config)
1824
- ]);
1825
- const memoryResult = await analyzeMemory(config, opts.path);
1826
- if (memoryResult) {
1827
- results.push(memoryResult);
1828
- }
2171
+ const results = await runAnalyzers(config, opts.path);
1829
2172
  if (opts.json) {
1830
2173
  const overallScore2 = Math.round(
1831
2174
  results.reduce((sum, r) => sum + r.score, 0) / results.length
@@ -1864,25 +2207,20 @@ function createDoctorCommand() {
1864
2207
  log.blank();
1865
2208
  log.step("Applying fixes...");
1866
2209
  log.blank();
1867
- const { fixed } = await applyFixes(fixable, opts.path);
1868
- log.blank();
1869
- if (fixed > 0) {
1870
- log.success(`Applied ${fixed} fix(es). Re-scanning...`);
1871
- log.blank();
2210
+ let updatedResults = results;
2211
+ let pending = fixable;
2212
+ let totalFixed = 0;
2213
+ for (let pass = 0; pass < 3 && pending.length > 0; pass++) {
2214
+ const { fixed } = await applyFixes(pending, opts.path);
2215
+ totalFixed += fixed;
2216
+ updatedResults = await runAnalyzers(await parseClaudeConfig(opts.path), opts.path);
2217
+ if (fixed === 0) break;
2218
+ pending = updatedResults.flatMap((r) => r.issues).filter((i) => i.severity !== "info");
1872
2219
  }
1873
- const updatedConfig = await parseClaudeConfig(opts.path);
1874
- const updatedResults = await Promise.all([
1875
- analyzeBudget(updatedConfig),
1876
- analyzeQuality(updatedConfig, opts.path),
1877
- analyzeSettings(updatedConfig),
1878
- analyzeHooks(updatedConfig),
1879
- analyzeRules(updatedConfig),
1880
- analyzePermissions(updatedConfig),
1881
- analyzeMcp(updatedConfig)
1882
- ]);
1883
- const updatedMemoryResult = await analyzeMemory(updatedConfig, opts.path);
1884
- if (updatedMemoryResult) {
1885
- updatedResults.push(updatedMemoryResult);
2220
+ if (totalFixed > 0) {
2221
+ log.blank();
2222
+ log.success(`Applied ${totalFixed} fix(es). Re-scanning...`);
2223
+ log.blank();
1886
2224
  }
1887
2225
  renderDoctorReport(updatedResults, { afterFix: true });
1888
2226
  log.info(`Then use ${chalk.bold("/lp-enhance")} inside Claude Code to have Claude restructure and complete your CLAUDE.md.`);
@@ -1896,6 +2234,22 @@ function createDoctorCommand() {
1896
2234
  }
1897
2235
  });
1898
2236
  }
2237
+ async function runAnalyzers(config, path) {
2238
+ const results = await Promise.all([
2239
+ analyzeBudget(config),
2240
+ analyzeQuality(config, path),
2241
+ analyzeSettings(config),
2242
+ analyzeHooks(config, path),
2243
+ analyzeRules(config),
2244
+ analyzePermissions(config, path),
2245
+ analyzeMcp(config)
2246
+ ]);
2247
+ const workflowResult = await analyzeWorkflow(path);
2248
+ if (workflowResult) results.push(workflowResult);
2249
+ const memoryResult = await analyzeMemory(config, path);
2250
+ if (memoryResult) results.push(memoryResult);
2251
+ return results;
2252
+ }
1899
2253
 
1900
2254
  // src/commands/eval/index.ts
1901
2255
  import { Command as Command3 } from "commander";
@@ -1903,11 +2257,11 @@ import { select as select2 } from "@inquirer/prompts";
1903
2257
  import ora from "ora";
1904
2258
  import chalk2 from "chalk";
1905
2259
  import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
1906
- import { join as join7 } from "path";
2260
+ import { join as join11 } from "path";
1907
2261
 
1908
2262
  // src/commands/eval/loader.ts
1909
- import { readFile as readFile4, readdir as readdir3, access as access3 } from "fs/promises";
1910
- import { join as join5, resolve as resolve3, dirname as dirname2 } from "path";
2263
+ import { readFile as readFile5, readdir as readdir3 } from "fs/promises";
2264
+ import { join as join8, resolve as resolve3, dirname as dirname2 } from "path";
1911
2265
  import { fileURLToPath } from "url";
1912
2266
  import { parse as parseYaml } from "yaml";
1913
2267
 
@@ -1957,15 +2311,27 @@ function validateChecks(raw, filePath) {
1957
2311
  throw new ScenarioError(filePath, `checks[${i}] must be an object`);
1958
2312
  }
1959
2313
  const check = c;
1960
- const validTypes = ["grep", "file-exists", "file-absent", "max-lines", "custom"];
1961
- if (!validTypes.includes(check.type)) {
2314
+ const validTypes = ["grep", "file-exists", "file-absent", "max-lines", "custom", "transcript", "judge"];
2315
+ const type = check.type;
2316
+ if (!validTypes.includes(type)) {
1962
2317
  throw new ScenarioError(filePath, `checks[${i}].type must be one of: ${validTypes.join(", ")}`);
1963
2318
  }
1964
- if (typeof check.target !== "string") {
1965
- throw new ScenarioError(filePath, `checks[${i}].target must be a string`);
2319
+ const fileBasedTypes = ["grep", "file-exists", "file-absent", "max-lines"];
2320
+ if (fileBasedTypes.includes(type) && typeof check.target !== "string") {
2321
+ throw new ScenarioError(filePath, `checks[${i}].target must be a string for ${type} checks`);
2322
+ }
2323
+ if (type === "custom" && typeof check.script !== "string") {
2324
+ throw new ScenarioError(filePath, `checks[${i}].script must be a string (shell command, exit 0 = pass)`);
2325
+ }
2326
+ if (type === "transcript" && typeof check.pattern !== "string") {
2327
+ throw new ScenarioError(filePath, `checks[${i}].pattern must be a string for transcript checks`);
2328
+ }
2329
+ if (type === "judge" && typeof check.rubric !== "string") {
2330
+ throw new ScenarioError(filePath, `checks[${i}].rubric must be a string for judge checks`);
1966
2331
  }
1967
2332
  const validExpect = ["present", "absent"];
1968
- if (!validExpect.includes(check.expect)) {
2333
+ const expect = check.expect ?? "present";
2334
+ if (!validExpect.includes(expect)) {
1969
2335
  throw new ScenarioError(filePath, `checks[${i}].expect must be "present" or "absent"`);
1970
2336
  }
1971
2337
  if (typeof check.points !== "number" || check.points < 0) {
@@ -1975,12 +2341,14 @@ function validateChecks(raw, filePath) {
1975
2341
  throw new ScenarioError(filePath, `checks[${i}].label must be a string`);
1976
2342
  }
1977
2343
  return {
1978
- type: check.type,
2344
+ type,
1979
2345
  pattern: typeof check.pattern === "string" ? check.pattern : void 0,
1980
- target: check.target,
1981
- expect: check.expect,
2346
+ target: typeof check.target === "string" ? check.target : void 0,
2347
+ expect,
1982
2348
  points: check.points,
1983
- label: check.label
2349
+ label: check.label,
2350
+ script: typeof check.script === "string" ? check.script : void 0,
2351
+ rubric: typeof check.rubric === "string" ? check.rubric : void 0
1984
2352
  };
1985
2353
  });
1986
2354
  }
@@ -2012,32 +2380,24 @@ var ScenarioError = class extends Error {
2012
2380
  async function findScenariosDir() {
2013
2381
  const thisDir = dirname2(fileURLToPath(import.meta.url));
2014
2382
  const devPath = resolve3(thisDir, "../../../scenarios");
2015
- if (await dirExists(devPath)) return devPath;
2383
+ if (await fileExists(devPath)) return devPath;
2016
2384
  const bundledPath = resolve3(thisDir, "../scenarios");
2017
- if (await dirExists(bundledPath)) return bundledPath;
2385
+ if (await fileExists(bundledPath)) return bundledPath;
2018
2386
  const rootPath = resolve3(thisDir, "../../scenarios");
2019
- if (await dirExists(rootPath)) return rootPath;
2387
+ if (await fileExists(rootPath)) return rootPath;
2020
2388
  return devPath;
2021
2389
  }
2022
- async function dirExists(path) {
2023
- try {
2024
- await access3(path);
2025
- return true;
2026
- } catch {
2027
- return false;
2028
- }
2029
- }
2030
2390
  async function loadScenarios(options) {
2031
2391
  const { suite, customPath } = options;
2032
2392
  const scenarioDir = customPath ? resolve3(customPath) : await findScenariosDir();
2033
- const dirs = suite ? [join5(scenarioDir, suite)] : await getSubdirectories(scenarioDir);
2393
+ const dirs = suite ? [join8(scenarioDir, suite)] : await getSubdirectories(scenarioDir);
2034
2394
  const allDirs = [scenarioDir, ...dirs];
2035
2395
  const scenarios = [];
2036
2396
  for (const dir of allDirs) {
2037
2397
  const files = await listYamlFiles(dir);
2038
2398
  for (const file of files) {
2039
2399
  try {
2040
- const content = await readFile4(file, "utf-8");
2400
+ const content = await readFile5(file, "utf-8");
2041
2401
  const raw = parseYaml(content);
2042
2402
  const scenario = validateScenario(raw, file);
2043
2403
  scenarios.push(scenario);
@@ -2052,7 +2412,7 @@ async function loadScenarios(options) {
2052
2412
  async function getSubdirectories(dir) {
2053
2413
  try {
2054
2414
  const entries = await readdir3(dir, { withFileTypes: true });
2055
- return entries.filter((e) => e.isDirectory()).map((e) => join5(dir, e.name));
2415
+ return entries.filter((e) => e.isDirectory()).map((e) => join8(dir, e.name));
2056
2416
  } catch {
2057
2417
  return [];
2058
2418
  }
@@ -2060,26 +2420,182 @@ async function getSubdirectories(dir) {
2060
2420
  async function listYamlFiles(dir) {
2061
2421
  try {
2062
2422
  const entries = await readdir3(dir, { withFileTypes: true });
2063
- return entries.filter((e) => e.isFile() && (e.name.endsWith(".yaml") || e.name.endsWith(".yml"))).map((e) => join5(dir, e.name));
2423
+ return entries.filter((e) => e.isFile() && (e.name.endsWith(".yaml") || e.name.endsWith(".yml"))).map((e) => join8(dir, e.name));
2064
2424
  } catch {
2065
2425
  return [];
2066
2426
  }
2067
2427
  }
2428
+ function resolveRuns(scenarioRuns, cliRuns, userChoseRuns) {
2429
+ if (userChoseRuns) return cliRuns;
2430
+ return scenarioRuns ?? cliRuns;
2431
+ }
2068
2432
 
2069
2433
  // src/commands/eval/runner.ts
2070
- import { mkdir as mkdir2, writeFile as writeFile2, readFile as readFile5, readdir as readdir4, rm, cp } from "fs/promises";
2071
- import { join as join6, dirname as dirname3 } from "path";
2072
- import { tmpdir } from "os";
2434
+ import { mkdir as mkdir2, writeFile as writeFile2, rm, cp } from "fs/promises";
2435
+ import { join as join10, dirname as dirname3 } from "path";
2436
+ import { tmpdir as tmpdir2 } from "os";
2073
2437
  import { randomUUID } from "crypto";
2438
+ import { execFile as execFile2 } from "child_process";
2439
+ import { promisify as promisify2 } from "util";
2440
+
2441
+ // src/commands/eval/checks.ts
2442
+ import { readFile as readFile6, readdir as readdir4 } from "fs/promises";
2443
+ import { join as join9 } from "path";
2444
+ import { tmpdir } from "os";
2074
2445
  import { execFile } from "child_process";
2075
2446
  import { promisify } from "util";
2076
2447
  var exec = promisify(execFile);
2448
+ async function evaluateChecks(checks, sandboxDir, context) {
2449
+ const results = [];
2450
+ for (const check of checks) {
2451
+ const passed = await evaluateSingleCheck(check, sandboxDir, context);
2452
+ results.push({ label: check.label, passed, points: check.points });
2453
+ }
2454
+ return results;
2455
+ }
2456
+ async function evaluateSingleCheck(check, sandboxDir, context) {
2457
+ switch (check.type) {
2458
+ case "grep":
2459
+ return checkGrep(check, sandboxDir);
2460
+ case "file-exists":
2461
+ case "file-absent":
2462
+ return checkFilePresence(check, sandboxDir);
2463
+ case "max-lines":
2464
+ return checkMaxLines(check, sandboxDir);
2465
+ case "custom":
2466
+ return checkCustom(check, sandboxDir);
2467
+ case "transcript":
2468
+ return checkTranscript(check, context.transcript);
2469
+ case "judge":
2470
+ return checkJudge(check, context);
2471
+ default:
2472
+ return false;
2473
+ }
2474
+ }
2475
+ async function checkGrep(check, sandboxDir) {
2476
+ if (!check.pattern || !check.target) return false;
2477
+ try {
2478
+ const content = await readFile6(join9(sandboxDir, check.target), "utf-8");
2479
+ let found;
2480
+ try {
2481
+ found = new RegExp(check.pattern).test(content);
2482
+ } catch {
2483
+ return false;
2484
+ }
2485
+ return check.expect === "present" ? found : !found;
2486
+ } catch {
2487
+ return check.expect === "absent";
2488
+ }
2489
+ }
2490
+ async function checkFilePresence(check, sandboxDir) {
2491
+ if (!check.target) return false;
2492
+ try {
2493
+ await readFile6(join9(sandboxDir, check.target));
2494
+ return check.expect === "present";
2495
+ } catch {
2496
+ return check.expect === "absent";
2497
+ }
2498
+ }
2499
+ async function checkMaxLines(check, sandboxDir) {
2500
+ if (!check.target) return false;
2501
+ const maxLines = parseInt(check.pattern ?? "800", 10);
2502
+ try {
2503
+ const files = await listAllFiles(join9(sandboxDir, check.target));
2504
+ for (const file of files) {
2505
+ const content = await readFile6(file, "utf-8");
2506
+ if (content.split("\n").length > maxLines) {
2507
+ return check.expect === "absent";
2508
+ }
2509
+ }
2510
+ return check.expect === "present";
2511
+ } catch {
2512
+ return check.expect === "absent";
2513
+ }
2514
+ }
2515
+ async function checkCustom(check, sandboxDir) {
2516
+ if (!check.script) return false;
2517
+ const env = {
2518
+ PATH: process.env.PATH ?? "",
2519
+ HOME: process.env.HOME ?? "",
2520
+ TMPDIR: process.env.TMPDIR ?? ""
2521
+ };
2522
+ try {
2523
+ await exec("bash", ["-c", check.script], { cwd: sandboxDir, timeout: 3e4, env });
2524
+ return true;
2525
+ } catch {
2526
+ return false;
2527
+ }
2528
+ }
2529
+ function checkTranscript(check, transcript) {
2530
+ if (!check.pattern) return false;
2531
+ let found;
2532
+ try {
2533
+ found = new RegExp(check.pattern).test(transcript);
2534
+ } catch {
2535
+ return false;
2536
+ }
2537
+ return check.expect === "present" ? found : !found;
2538
+ }
2539
+ async function checkJudge(check, context) {
2540
+ if (!check.rubric) return false;
2541
+ try {
2542
+ return await context.judge(check.rubric, context.transcript);
2543
+ } catch (err) {
2544
+ log.warnOnce("judge-check-error", `judge check could not run (scoring FAIL): ${err instanceof Error ? err.message : String(err)}`);
2545
+ return false;
2546
+ }
2547
+ }
2548
+ var JUDGE_TRANSCRIPT_CHARS = 15e3;
2549
+ function makeClaudeJudge(model) {
2550
+ return async (rubric, transcript) => {
2551
+ const prompt = [
2552
+ "You are grading an AI coding session against a rubric.",
2553
+ `Rubric: ${rubric}`,
2554
+ "Session transcript (stream-json, oldest lines truncated):",
2555
+ transcript.slice(-JUDGE_TRANSCRIPT_CHARS),
2556
+ "",
2557
+ "Does the session satisfy the rubric? Reply with exactly one word: PASS or FAIL."
2558
+ ].join("\n");
2559
+ const args = ["-p", prompt, "--output-format", "text", "--max-turns", "1"];
2560
+ if (model) args.push("--model", model);
2561
+ try {
2562
+ const { stdout } = await exec("claude", args, {
2563
+ cwd: tmpdir(),
2564
+ timeout: 6e4,
2565
+ maxBuffer: 1024 * 1024
2566
+ });
2567
+ return /\bPASS\b/.test(stdout) && !/\bFAIL\b/.test(stdout);
2568
+ } catch (err) {
2569
+ log.warnOnce("judge-cli-error", `judge could not invoke the claude CLI (scoring FAIL): ${err instanceof Error ? err.message : String(err)}`);
2570
+ return false;
2571
+ }
2572
+ };
2573
+ }
2574
+ async function listAllFiles(dir) {
2575
+ const results = [];
2576
+ try {
2577
+ const entries = await readdir4(dir, { withFileTypes: true });
2578
+ for (const entry of entries) {
2579
+ const fullPath = join9(dir, entry.name);
2580
+ if (entry.isDirectory()) {
2581
+ results.push(...await listAllFiles(fullPath));
2582
+ } else {
2583
+ results.push(fullPath);
2584
+ }
2585
+ }
2586
+ } catch {
2587
+ }
2588
+ return results;
2589
+ }
2590
+
2591
+ // src/commands/eval/runner.ts
2592
+ var exec2 = promisify2(execFile2);
2077
2593
  async function runScenario(scenario, options) {
2078
- const sandboxDir = join6(tmpdir(), `claude-eval-${randomUUID()}`);
2594
+ const sandboxDir = join10(tmpdir2(), `claude-eval-${randomUUID()}`);
2079
2595
  try {
2080
2596
  await setupSandbox(sandboxDir, scenario, options.projectRoot);
2081
- await runClaudeInSandbox(sandboxDir, scenario.prompt, options.timeout, options.model);
2082
- return await scoreResults(scenario, sandboxDir);
2597
+ const transcript = await runClaudeInSandbox(sandboxDir, scenario.prompt, options.timeout, options.model);
2598
+ return await scoreResults(scenario, sandboxDir, transcript, options.model);
2083
2599
  } finally {
2084
2600
  if (options.debug) {
2085
2601
  console.log(` DEBUG: Sandbox preserved at ${sandboxDir}`);
@@ -2101,23 +2617,23 @@ async function runScenarioWithRetries(scenario, options) {
2101
2617
  async function setupSandbox(sandboxDir, scenario, projectRoot) {
2102
2618
  await mkdir2(sandboxDir, { recursive: true });
2103
2619
  for (const file of scenario.setup.files) {
2104
- const filePath = join6(sandboxDir, file.path);
2620
+ const filePath = join10(sandboxDir, file.path);
2105
2621
  await mkdir2(dirname3(filePath), { recursive: true });
2106
2622
  await writeFile2(filePath, file.content);
2107
2623
  }
2108
2624
  await copyProjectConfig(sandboxDir, projectRoot);
2109
2625
  if (scenario.setup.instructions) {
2110
2626
  await writeFile2(
2111
- join6(sandboxDir, "CLAUDE.md"),
2627
+ join10(sandboxDir, "CLAUDE.md"),
2112
2628
  `# Eval Scenario
2113
2629
 
2114
2630
  ${scenario.setup.instructions}
2115
2631
  `
2116
2632
  );
2117
2633
  }
2118
- await exec("git", ["init", "-q"], { cwd: sandboxDir });
2119
- await exec("git", ["add", "-A"], { cwd: sandboxDir });
2120
- await exec("git", [
2634
+ await exec2("git", ["init", "-q"], { cwd: sandboxDir });
2635
+ await exec2("git", ["add", "-A"], { cwd: sandboxDir });
2636
+ await exec2("git", [
2121
2637
  "-c",
2122
2638
  "user.name=eval",
2123
2639
  "-c",
@@ -2129,20 +2645,20 @@ ${scenario.setup.instructions}
2129
2645
  ], { cwd: sandboxDir });
2130
2646
  }
2131
2647
  async function copyProjectConfig(sandboxDir, projectRoot) {
2132
- const claudeDir = join6(projectRoot, ".claude");
2133
- const sandboxClaudeDir = join6(sandboxDir, ".claude");
2134
- const settingsPath = join6(claudeDir, "settings.json");
2648
+ const claudeDir = join10(projectRoot, ".claude");
2649
+ const sandboxClaudeDir = join10(sandboxDir, ".claude");
2650
+ const settingsPath = join10(claudeDir, "settings.json");
2135
2651
  if (await fileExists(settingsPath)) {
2136
2652
  await mkdir2(sandboxClaudeDir, { recursive: true });
2137
- await cp(settingsPath, join6(sandboxClaudeDir, "settings.json"));
2653
+ await cp(settingsPath, join10(sandboxClaudeDir, "settings.json"));
2138
2654
  }
2139
- const rulesDir = join6(claudeDir, "rules");
2655
+ const rulesDir = join10(claudeDir, "rules");
2140
2656
  if (await fileExists(rulesDir)) {
2141
- await cp(rulesDir, join6(sandboxClaudeDir, "rules"), { recursive: true });
2657
+ await cp(rulesDir, join10(sandboxClaudeDir, "rules"), { recursive: true });
2142
2658
  }
2143
- const ignorePath = join6(projectRoot, ".claudeignore");
2659
+ const ignorePath = join10(projectRoot, ".claudeignore");
2144
2660
  if (await fileExists(ignorePath)) {
2145
- await cp(ignorePath, join6(sandboxDir, ".claudeignore"));
2661
+ await cp(ignorePath, join10(sandboxDir, ".claudeignore"));
2146
2662
  }
2147
2663
  }
2148
2664
  async function runClaudeInSandbox(cwd, prompt, timeout, model) {
@@ -2150,25 +2666,31 @@ async function runClaudeInSandbox(cwd, prompt, timeout, model) {
2150
2666
  const sdk = await import("@anthropic-ai/claude-agent-sdk");
2151
2667
  const controller = new AbortController();
2152
2668
  const timeoutId = setTimeout(() => controller.abort(), timeout);
2669
+ const lines = [];
2153
2670
  try {
2154
- for await (const _message of sdk.query({
2671
+ for await (const message of sdk.query({
2155
2672
  prompt,
2156
2673
  options: {
2157
2674
  cwd,
2158
2675
  allowedTools: ["Bash", "Read", "Write", "Edit", "Glob", "Grep"],
2159
2676
  permissionMode: "dontAsk",
2160
- settingSources: [],
2677
+ // "project" is required or the SDK loads NOTHING from the sandbox —
2678
+ // no CLAUDE.md, no .claude/settings.json hooks. Eval exists to test
2679
+ // that config surface, and transcript checks assert hooks fire.
2680
+ settingSources: ["project"],
2161
2681
  maxTurns: 20,
2162
2682
  abortController: controller,
2163
2683
  ...model ? { model } : {}
2164
2684
  }
2165
2685
  })) {
2686
+ lines.push(JSON.stringify(message));
2166
2687
  }
2167
2688
  } finally {
2168
2689
  clearTimeout(timeoutId);
2169
2690
  }
2691
+ return lines.join("\n");
2170
2692
  } catch {
2171
- await runClaudeCli(cwd, prompt, timeout, model);
2693
+ return runClaudeCli(cwd, prompt, timeout, model);
2172
2694
  }
2173
2695
  }
2174
2696
  async function runClaudeCli(cwd, prompt, timeout, model) {
@@ -2177,7 +2699,8 @@ async function runClaudeCli(cwd, prompt, timeout, model) {
2177
2699
  "-p",
2178
2700
  prompt,
2179
2701
  "--output-format",
2180
- "text",
2702
+ "stream-json",
2703
+ "--verbose",
2181
2704
  "--max-turns",
2182
2705
  "20",
2183
2706
  "--dangerously-skip-permissions",
@@ -2190,16 +2713,20 @@ async function runClaudeCli(cwd, prompt, timeout, model) {
2190
2713
  "Grep"
2191
2714
  ];
2192
2715
  if (model) args.push("--model", model);
2193
- await exec("claude", args, { cwd, timeout, maxBuffer: 10 * 1024 * 1024 });
2716
+ const { stdout } = await exec2("claude", args, { cwd, timeout, maxBuffer: 10 * 1024 * 1024 });
2717
+ return stdout;
2194
2718
  } catch (error) {
2195
2719
  if (error && typeof error === "object" && "stdout" in error) {
2196
- return;
2720
+ return String(error.stdout ?? "");
2197
2721
  }
2198
2722
  throw error;
2199
2723
  }
2200
2724
  }
2201
- async function scoreResults(scenario, sandboxDir) {
2202
- const checkResults = await evaluateChecks(scenario.checks, sandboxDir);
2725
+ async function scoreResults(scenario, sandboxDir, transcript, model) {
2726
+ const checkResults = await evaluateChecks(scenario.checks, sandboxDir, {
2727
+ transcript,
2728
+ judge: makeClaudeJudge(model)
2729
+ });
2203
2730
  const score = checkResults.filter((c) => c.passed).reduce((sum, c) => sum + c.points, 0);
2204
2731
  const maxScore = scenario.checks.reduce((sum, c) => sum + c.points, 0);
2205
2732
  return {
@@ -2210,97 +2737,31 @@ async function scoreResults(scenario, sandboxDir) {
2210
2737
  checks: checkResults
2211
2738
  };
2212
2739
  }
2213
- async function evaluateChecks(checks, sandboxDir) {
2214
- const results = [];
2215
- for (const check of checks) {
2216
- const passed = await evaluateSingleCheck(check, sandboxDir);
2217
- results.push({ label: check.label, passed, points: check.points });
2218
- }
2219
- return results;
2220
- }
2221
- async function evaluateSingleCheck(check, sandboxDir) {
2222
- switch (check.type) {
2223
- case "grep":
2224
- return checkGrep(check, sandboxDir);
2225
- case "file-exists":
2226
- case "file-absent":
2227
- return checkFilePresence(check, sandboxDir);
2228
- case "max-lines":
2229
- return checkMaxLines(check, sandboxDir);
2230
- case "custom":
2231
- return false;
2232
- default:
2233
- return false;
2234
- }
2235
- }
2236
- async function checkGrep(check, sandboxDir) {
2237
- if (!check.pattern) return false;
2238
- try {
2239
- const content = await readFile5(join6(sandboxDir, check.target), "utf-8");
2240
- let found;
2241
- try {
2242
- found = new RegExp(check.pattern).test(content);
2243
- } catch {
2244
- return false;
2245
- }
2246
- return check.expect === "present" ? found : !found;
2247
- } catch {
2248
- return check.expect === "absent";
2249
- }
2250
- }
2251
- async function checkFilePresence(check, sandboxDir) {
2252
- try {
2253
- await readFile5(join6(sandboxDir, check.target));
2254
- return check.expect === "present";
2255
- } catch {
2256
- return check.expect === "absent";
2257
- }
2258
- }
2259
- async function checkMaxLines(check, sandboxDir) {
2260
- const maxLines = parseInt(check.pattern ?? "800", 10);
2261
- try {
2262
- const files = await listAllFiles(join6(sandboxDir, check.target));
2263
- for (const file of files) {
2264
- const content = await readFile5(file, "utf-8");
2265
- if (content.split("\n").length > maxLines) {
2266
- return check.expect === "absent";
2267
- }
2268
- }
2269
- return check.expect === "present";
2270
- } catch {
2271
- return check.expect === "absent";
2272
- }
2273
- }
2274
- async function listAllFiles(dir) {
2275
- const results = [];
2276
- try {
2277
- const entries = await readdir4(dir, { withFileTypes: true });
2278
- for (const entry of entries) {
2279
- const fullPath = join6(dir, entry.name);
2280
- if (entry.isDirectory()) {
2281
- results.push(...await listAllFiles(fullPath));
2282
- } else {
2283
- results.push(fullPath);
2284
- }
2285
- }
2286
- } catch {
2287
- }
2288
- return results;
2289
- }
2290
2740
 
2291
2741
  // src/commands/eval/index.ts
2292
2742
  function createEvalCommand() {
2293
- return new Command3("eval").description("Test your Claude Code config against eval scenarios").option("-s, --suite <suite>", "Eval suite to run (e.g., security, conventions, workflow)").option("-p, --path <path>", "Project root path", process.cwd()).option("--scenarios <path>", "Custom scenarios directory").option("--runs <n>", "Runs per scenario (default: 3)", "3").option("--timeout <ms>", "Timeout per run in ms (default: 120000)", "120000").option("--json", "Output as JSON").option("--debug", "Keep sandbox directories for inspection").option("--model <model>", "Model to use for eval (e.g., sonnet, haiku, opus)").action(async (opts) => {
2743
+ return new Command3("eval").description("Test your Claude Code config against eval scenarios").option(
2744
+ "-s, --suite <suite>",
2745
+ "Eval suite to run (e.g., security, conventions, workflow)"
2746
+ ).option("-p, --path <path>", "Project root path", process.cwd()).option("--scenarios <path>", "Custom scenarios directory").option("--runs <n>", "Runs per scenario (default: 3)", "3").option(
2747
+ "--timeout <ms>",
2748
+ "Timeout per run in ms (default: 120000)",
2749
+ "120000"
2750
+ ).option("--json", "Output as JSON").option("--debug", "Keep sandbox directories for inspection").option(
2751
+ "--model <model>",
2752
+ "Model to use for eval (e.g., sonnet, haiku, opus)"
2753
+ ).action(async (opts, command) => {
2294
2754
  printBanner();
2755
+ let userChoseRuns = command.getOptionValueSource("runs") === "cli";
2295
2756
  const hasFlags = opts.suite || opts.model || opts.runs !== "3" || opts.timeout !== "120000" || opts.path !== process.cwd() || Boolean(opts.scenarios) || opts.json || opts.debug;
2296
2757
  if (!hasFlags) {
2297
2758
  opts.suite = await select2({
2298
2759
  message: "Suite",
2299
2760
  choices: [
2300
- { name: "security (6 scenarios)", value: "security" },
2761
+ { name: "security (7 scenarios)", value: "security" },
2301
2762
  { name: "conventions (5 scenarios)", value: "conventions" },
2302
- { name: "workflow (4 scenarios)", value: "workflow" },
2303
- { name: "all (15 scenarios)", value: void 0 }
2763
+ { name: "workflow (5 scenarios)", value: "workflow" },
2764
+ { name: "all (17 scenarios)", value: void 0 }
2304
2765
  ]
2305
2766
  });
2306
2767
  opts.runs = await select2({
@@ -2311,6 +2772,7 @@ function createEvalCommand() {
2311
2772
  { name: "5 \u2014 thorough", value: "5" }
2312
2773
  ]
2313
2774
  });
2775
+ userChoseRuns = true;
2314
2776
  opts.model = await select2({
2315
2777
  message: "Model",
2316
2778
  choices: [
@@ -2323,8 +2785,12 @@ function createEvalCommand() {
2323
2785
  }
2324
2786
  const claudeAvailable = await checkClaudeCli();
2325
2787
  if (!claudeAvailable) {
2326
- log.error("Claude CLI not found. Install it: https://docs.anthropic.com/en/docs/claude-code");
2327
- log.info("The eval command runs Claude headless against scenarios \u2014 it requires the CLI.");
2788
+ log.error(
2789
+ "Claude CLI not found. Install it: https://docs.anthropic.com/en/docs/claude-code"
2790
+ );
2791
+ log.info(
2792
+ "The eval command runs Claude headless against scenarios \u2014 it requires the CLI."
2793
+ );
2328
2794
  process.exit(1);
2329
2795
  }
2330
2796
  log.step("Loading eval scenarios...");
@@ -2335,7 +2801,9 @@ function createEvalCommand() {
2335
2801
  if (scenarios.length === 0) {
2336
2802
  log.warn("No matching scenarios found.");
2337
2803
  if (opts.suite) {
2338
- log.info(`Check that the suite "${opts.suite}" exists in the scenarios directory.`);
2804
+ log.info(
2805
+ `Check that the suite "${opts.suite}" exists in the scenarios directory.`
2806
+ );
2339
2807
  }
2340
2808
  return;
2341
2809
  }
@@ -2344,10 +2812,11 @@ function createEvalCommand() {
2344
2812
  log.info(`Model: ${opts.model}`);
2345
2813
  }
2346
2814
  log.blank();
2347
- const runs = parseInt(opts.runs, 10);
2815
+ const cliRuns = parseInt(opts.runs, 10);
2348
2816
  const timeout = parseInt(opts.timeout, 10);
2349
2817
  const results = [];
2350
2818
  for (const scenario of scenarios) {
2819
+ const runs = resolveRuns(scenario.runs, cliRuns, userChoseRuns);
2351
2820
  const spinner = ora({
2352
2821
  text: `Running: ${scenario.name} (${runs} run${runs > 1 ? "s" : ""})`,
2353
2822
  prefixText: " "
@@ -2355,13 +2824,22 @@ function createEvalCommand() {
2355
2824
  try {
2356
2825
  const result = await runScenarioWithRetries(
2357
2826
  { ...scenario, runs },
2358
- { projectRoot: opts.path, timeout, debug: opts.debug, model: opts.model }
2827
+ {
2828
+ projectRoot: opts.path,
2829
+ timeout,
2830
+ debug: opts.debug,
2831
+ model: opts.model
2832
+ }
2359
2833
  );
2360
2834
  results.push(result);
2361
2835
  if (result.passed) {
2362
- spinner.succeed(`${scenario.name} ${result.score}/${result.maxScore}`);
2836
+ spinner.succeed(
2837
+ `${scenario.name} ${result.score}/${result.maxScore}`
2838
+ );
2363
2839
  } else {
2364
- spinner.fail(`${scenario.name} ${result.score}/${result.maxScore}`);
2840
+ spinner.fail(
2841
+ `${scenario.name} ${result.score}/${result.maxScore}`
2842
+ );
2365
2843
  }
2366
2844
  } catch (error) {
2367
2845
  spinner.fail(`${scenario.name} ERROR`);
@@ -2372,7 +2850,11 @@ function createEvalCommand() {
2372
2850
  score: 0,
2373
2851
  maxScore: scenario.checks.reduce((s, c) => s + c.points, 0),
2374
2852
  passed: false,
2375
- checks: scenario.checks.map((c) => ({ label: c.label, passed: false, points: c.points }))
2853
+ checks: scenario.checks.map((c) => ({
2854
+ label: c.label,
2855
+ passed: false,
2856
+ points: c.points
2857
+ }))
2376
2858
  });
2377
2859
  }
2378
2860
  }
@@ -2380,13 +2862,19 @@ function createEvalCommand() {
2380
2862
  if (opts.json) {
2381
2863
  const overallScore = results.reduce((s, r) => s + r.score, 0);
2382
2864
  const overallMax = results.reduce((s, r) => s + r.maxScore, 0);
2383
- console.log(JSON.stringify({
2384
- results,
2385
- overallScore,
2386
- overallMax,
2387
- passed: overallScore >= overallMax * 0.8,
2388
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2389
- }, null, 2));
2865
+ console.log(
2866
+ JSON.stringify(
2867
+ {
2868
+ results,
2869
+ overallScore,
2870
+ overallMax,
2871
+ passed: overallScore >= overallMax * 0.8,
2872
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2873
+ },
2874
+ null,
2875
+ 2
2876
+ )
2877
+ );
2390
2878
  return;
2391
2879
  }
2392
2880
  renderEvalReport(results);
@@ -2398,7 +2886,9 @@ function renderEvalReport(results) {
2398
2886
  const icon = result.passed ? chalk2.green("\u2713") : chalk2.red("\u2717");
2399
2887
  const status = result.passed ? chalk2.green("PASS") : chalk2.red("FAIL");
2400
2888
  const score = `${result.score}/${result.maxScore}`;
2401
- console.log(` ${icon} ${chalk2.bold(result.scenario)} ${score} ${status}`);
2889
+ console.log(
2890
+ ` ${icon} ${chalk2.bold(result.scenario)} ${score} ${status}`
2891
+ );
2402
2892
  const failedChecks = result.checks.filter((c) => !c.passed);
2403
2893
  for (const check of failedChecks) {
2404
2894
  console.log(` ${chalk2.red("\u2717")} ${chalk2.dim(check.label)}`);
@@ -2415,7 +2905,9 @@ function renderEvalReport(results) {
2415
2905
  if (failed === 0) {
2416
2906
  log.success(`All ${passed} scenario(s) passed.`);
2417
2907
  } else {
2418
- log.warn(`${passed} passed, ${failed} failed out of ${results.length} scenario(s).`);
2908
+ log.warn(
2909
+ `${passed} passed, ${failed} failed out of ${results.length} scenario(s).`
2910
+ );
2419
2911
  }
2420
2912
  }
2421
2913
  async function saveEvalReport(results, projectRoot, suite, model) {
@@ -2439,7 +2931,9 @@ async function saveEvalReport(results, projectRoot, suite, model) {
2439
2931
  ];
2440
2932
  for (const result of results) {
2441
2933
  const status = result.passed ? "PASS" : "FAIL";
2442
- lines.push(`### ${result.scenario} \u2014 ${result.score}/${result.maxScore} ${status}`);
2934
+ lines.push(
2935
+ `### ${result.scenario} \u2014 ${result.score}/${result.maxScore} ${status}`
2936
+ );
2443
2937
  const failedChecks = result.checks.filter((c) => !c.passed);
2444
2938
  const passedChecks = result.checks.filter((c) => c.passed);
2445
2939
  for (const check of passedChecks) {
@@ -2457,23 +2951,25 @@ async function saveEvalReport(results, projectRoot, suite, model) {
2457
2951
  lines.push(`### Fix: ${result.scenario}`);
2458
2952
  const failedChecks = result.checks.filter((c) => !c.passed);
2459
2953
  for (const check of failedChecks) {
2460
- lines.push(`- ${check.label} \u2014 update CLAUDE.md instructions or add hooks to enforce this behavior`);
2954
+ lines.push(
2955
+ `- ${check.label} \u2014 update CLAUDE.md instructions or add hooks to enforce this behavior`
2956
+ );
2461
2957
  }
2462
2958
  lines.push("");
2463
2959
  }
2464
2960
  }
2465
- const evalDir = join7(projectRoot, ".claude", "eval");
2961
+ const evalDir = join11(projectRoot, ".claude", "eval");
2466
2962
  await mkdir3(evalDir, { recursive: true });
2467
2963
  const filename = `eval-${suite ?? "all"}-${timestamp}.md`;
2468
- await writeFile3(join7(evalDir, filename), lines.join("\n"));
2964
+ await writeFile3(join11(evalDir, filename), lines.join("\n"));
2469
2965
  log.success(`Report saved to .claude/eval/${filename}`);
2470
2966
  }
2471
2967
  async function checkClaudeCli() {
2472
- const { execFile: execFile2 } = await import("child_process");
2473
- const { promisify: promisify2 } = await import("util");
2474
- const exec2 = promisify2(execFile2);
2968
+ const { execFile: execFile3 } = await import("child_process");
2969
+ const { promisify: promisify3 } = await import("util");
2970
+ const exec3 = promisify3(execFile3);
2475
2971
  try {
2476
- await exec2("claude", ["--version"]);
2972
+ await exec3("claude", ["--version"]);
2477
2973
  return true;
2478
2974
  } catch {
2479
2975
  return false;
@@ -2481,9 +2977,8 @@ async function checkClaudeCli() {
2481
2977
  }
2482
2978
 
2483
2979
  // src/commands/memory/index.ts
2484
- import { readFileSync } from "fs";
2485
- import { join as join8 } from "path";
2486
- import { homedir as homedir3 } from "os";
2980
+ import { readFileSync as readFileSync2 } from "fs";
2981
+ import { join as join12 } from "path";
2487
2982
  import { Command as Command4 } from "commander";
2488
2983
  import { confirm as confirm2 } from "@inquirer/prompts";
2489
2984
  async function handleSyncErrors(fn) {
@@ -2496,80 +2991,78 @@ async function handleSyncErrors(fn) {
2496
2991
  }
2497
2992
  function isMemoryInstalled() {
2498
2993
  const cwd = process.cwd();
2499
- const hookPresent = hasMemoryHook(join8(cwd, ".claude", "settings.json")) || hasMemoryHook(join8(cwd, ".claude", "settings.local.json"));
2994
+ const hookPresent = hasMemoryHook(join12(cwd, ".claude", "settings.json")) || hasMemoryHook(join12(cwd, ".claude", "settings.local.json"));
2500
2995
  if (!hookPresent) return false;
2501
2996
  return isMemoryMcpRegistered(cwd);
2502
2997
  }
2503
- function isMemoryMcpRegistered(projectRoot) {
2504
- return hasMemoryServerInJson(join8(projectRoot, ".mcp.json"), "mcpServers") || hasMemoryServerInJson(join8(projectRoot, ".claude", "settings.local.json"), "mcpServers") || hasMemoryServerInUserConfig(projectRoot);
2505
- }
2506
- function hasMemoryServerInJson(path, key) {
2507
- try {
2508
- const parsed = JSON.parse(readFileSync(path, "utf-8"));
2509
- const servers = parsed[key];
2510
- return !!servers && typeof servers === "object" && "agentic-memory" in servers;
2511
- } catch {
2512
- return false;
2513
- }
2514
- }
2515
- function hasMemoryServerInUserConfig(projectRoot) {
2516
- try {
2517
- const parsed = JSON.parse(readFileSync(join8(homedir3(), ".claude.json"), "utf-8"));
2518
- const projects = parsed.projects;
2519
- const project = projects?.[projectRoot];
2520
- const scoped = project?.mcpServers;
2521
- if (scoped && "agentic-memory" in scoped) return true;
2522
- const global = parsed.mcpServers;
2523
- return !!global && "agentic-memory" in global;
2524
- } catch {
2525
- return false;
2526
- }
2527
- }
2528
2998
  function hasMemoryHook(path) {
2529
2999
  try {
2530
- const settings = JSON.parse(readFileSync(path, "utf-8"));
3000
+ const settings = JSON.parse(readFileSync2(path, "utf-8"));
2531
3001
  const hooks = settings.hooks;
2532
3002
  if (!hooks) return false;
2533
3003
  const sessionStart = hooks.SessionStart;
2534
3004
  return sessionStart?.some((h) => {
2535
3005
  const inner = h.hooks;
2536
- return inner?.some((ih) => String(ih.command ?? "").includes("memory context"));
3006
+ return inner?.some(
3007
+ (ih) => String(ih.command ?? "").includes("memory context")
3008
+ );
2537
3009
  }) ?? false;
2538
3010
  } catch {
2539
3011
  return false;
2540
3012
  }
2541
3013
  }
2542
3014
  function createMemoryCommand() {
2543
- const memory = new Command4("memory").description("Project-scoped memory with decay, sync, and a TUI dashboard").option("--dashboard", "Open the memory dashboard").action(async (opts) => {
3015
+ const memory = new Command4("memory").description("Project-scoped memory with decay, sync, and a TUI dashboard").showHelpAfterError(
3016
+ "(see `claude-launchpad memory --help` for the available subcommands)"
3017
+ ).showSuggestionAfterError(true).option("--dashboard", "Open the memory dashboard").action(async (opts) => {
2544
3018
  if (opts.dashboard) {
2545
3019
  if (!isMemoryInstalled()) {
2546
- log.error("Knowledge base not set up yet. Run `claude-launchpad memory` first.");
3020
+ log.error(
3021
+ "Knowledge base not set up yet. Run `claude-launchpad memory` first."
3022
+ );
2547
3023
  return;
2548
3024
  }
2549
- const { requireMemoryDeps } = await import("./require-deps-XUAKG226.js");
3025
+ const { requireMemoryDeps } = await import("./require-deps-WDMHDGBN.js");
2550
3026
  await requireMemoryDeps();
2551
- const { startTui } = await import("./tui-GGUFB6WP.js");
3027
+ const { startTui } = await import("./tui-HVJNLRY2.js");
2552
3028
  await startTui();
2553
3029
  return;
2554
3030
  }
2555
3031
  if (!isMemoryInstalled()) {
2556
- const { detectExistingSetup } = await import("./install-B5KISSFR.js");
3032
+ if (!process.stdin.isTTY) {
3033
+ log.error(
3034
+ "Knowledge base not set up and no TTY to prompt. Run `claude-launchpad memory install -y`."
3035
+ );
3036
+ process.exitCode = 1;
3037
+ return;
3038
+ }
3039
+ const { detectExistingSetup } = await import("./install-D32PV7SF.js");
2557
3040
  const existing = detectExistingSetup(process.cwd());
2558
3041
  const mcpMissing = existing !== null && !isMemoryMcpRegistered(process.cwd());
2559
3042
  if (existing) {
2560
3043
  const location = existing === "local" ? ".claude/CLAUDE.md + settings.local.json" : "CLAUDE.md + settings.json";
2561
3044
  log.blank();
2562
- log.success(`Memory config found (${location}) but ${mcpMissing ? "MCP server not registered" : "database not set up"}.`);
3045
+ log.success(
3046
+ `Memory config found (${location}) but ${mcpMissing ? "MCP server not registered" : "database not set up"}.`
3047
+ );
2563
3048
  log.info("Run `claude-launchpad memory install` to complete setup.");
2564
3049
  log.blank();
2565
3050
  } else {
2566
3051
  log.blank();
2567
- log.step("Claude doesn't have a knowledge base for this project yet.");
3052
+ log.step(
3053
+ "Claude doesn't have a knowledge base for this project yet."
3054
+ );
2568
3055
  log.blank();
2569
3056
  log.info("After setup, Claude will:");
2570
- log.info(" - Remember decisions, gotchas, and learnings across sessions");
2571
- log.info(" - Automatically recall relevant context when you start a session");
2572
- log.info(" - Save important facts as you work, so nothing gets lost");
3057
+ log.info(
3058
+ " - Remember decisions, gotchas, and learnings across sessions"
3059
+ );
3060
+ log.info(
3061
+ " - Automatically recall relevant context when you start a session"
3062
+ );
3063
+ log.info(
3064
+ " - Save important facts as you work, so nothing gets lost"
3065
+ );
2573
3066
  log.blank();
2574
3067
  }
2575
3068
  const proceed = await confirm2({
@@ -2580,24 +3073,51 @@ function createMemoryCommand() {
2580
3073
  log.info("Skipped.");
2581
3074
  return;
2582
3075
  }
2583
- const { runInstall } = await import("./install-B5KISSFR.js");
3076
+ const { runInstall } = await import("./install-D32PV7SF.js");
2584
3077
  await runInstall({});
2585
3078
  } else {
2586
- const { requireMemoryDeps } = await import("./require-deps-XUAKG226.js");
3079
+ const { requireMemoryDeps } = await import("./require-deps-WDMHDGBN.js");
2587
3080
  await requireMemoryDeps();
2588
- const { runStats } = await import("./stats-ARO5UFFZ.js");
3081
+ const { runStats } = await import("./stats-77BYLKYP.js");
2589
3082
  await runStats({});
2590
3083
  }
2591
3084
  });
2592
3085
  memory.addCommand(
2593
- new Command4("install").description("Install (or re-install) the knowledge base for this project").option("--db-path <path>", "Override the default data directory").action(async (opts) => {
2594
- const { runInstall } = await import("./install-B5KISSFR.js");
2595
- await runInstall(opts.dbPath ? { dbPath: opts.dbPath } : {});
3086
+ new Command4("install").description(
3087
+ "Install (or re-install) the knowledge base for this project"
3088
+ ).option("--db-path <path>", "Override the default data directory").option(
3089
+ "-y, --yes",
3090
+ "Non-interactive: accept defaults (shared placement)"
3091
+ ).action(async (opts) => {
3092
+ try {
3093
+ const { runInstall } = await import("./install-D32PV7SF.js");
3094
+ await runInstall({
3095
+ ...opts.dbPath ? { dbPath: opts.dbPath } : {},
3096
+ yes: opts.yes === true
3097
+ });
3098
+ } catch (err) {
3099
+ log.error(err instanceof Error ? err.message : String(err));
3100
+ process.exitCode = 1;
3101
+ }
3102
+ })
3103
+ );
3104
+ memory.addCommand(
3105
+ new Command4("stats").description("Show memory counts, health, and injection stats").option("--json", "JSON output").action(async (opts) => {
3106
+ const { requireMemoryDeps } = await import("./require-deps-WDMHDGBN.js");
3107
+ await requireMemoryDeps();
3108
+ const { runStats } = await import("./stats-77BYLKYP.js");
3109
+ await runStats(opts);
3110
+ })
3111
+ );
3112
+ memory.addCommand(
3113
+ new Command4("doctor").description("Diagnose the memory setup (DB, hooks, MCP registration)").option("--json", "JSON output").action(async (opts) => {
3114
+ const { runMemoryDoctor } = await import("./doctor-DCHFJ3XV.js");
3115
+ await runMemoryDoctor(opts);
2596
3116
  })
2597
3117
  );
2598
3118
  memory.addCommand(
2599
3119
  new Command4("context").description("Load session context (hook handler)").option("--json", "JSON output").action(async (opts) => {
2600
- const { runContext } = await import("./context-76QJSCRJ.js");
3120
+ const { runContext } = await import("./context-YIPO4LBS.js");
2601
3121
  await runContext(opts);
2602
3122
  }).helpCommand(false),
2603
3123
  { hidden: true }
@@ -2612,24 +3132,34 @@ function createMemoryCommand() {
2612
3132
  memory.addCommand(
2613
3133
  new Command4("push").description("Push current project's memories to GitHub Gist").option("--all", "Push all projects").option("-y, --yes", "Skip confirmation prompt").action(async (opts) => {
2614
3134
  await handleSyncErrors(async () => {
2615
- const { runPush } = await import("./push-C3QMIEUQ.js");
3135
+ const { runPush } = await import("./push-U4IVTFOU.js");
2616
3136
  await runPush(opts);
2617
3137
  });
2618
3138
  })
2619
3139
  );
2620
3140
  memory.addCommand(
2621
- new Command4("pull").description("Pull current project's memories from GitHub Gist").option("--all", "Pull all projects").option("-y, --yes", "Non-interactive (accepted for symmetry with push; pull never prompts)").action(async (opts) => {
3141
+ new Command4("pull").description("Pull current project's memories from GitHub Gist").option("--all", "Pull all projects").option(
3142
+ "-y, --yes",
3143
+ "Non-interactive (accepted for symmetry with push; pull never prompts)"
3144
+ ).action(async (opts) => {
2622
3145
  await handleSyncErrors(async () => {
2623
- const { runPull } = await import("./pull-4ZQSKFX7.js");
3146
+ const { runPull } = await import("./pull-YUQTDRUP.js");
2624
3147
  await runPull(opts);
2625
3148
  });
2626
3149
  })
2627
3150
  );
2628
- const sync = new Command4("sync").description("Manage memory sync");
3151
+ const sync = new Command4("sync").description(
3152
+ "Sync memories with the gist (pull + push); subcommands manage sync state"
3153
+ ).option("--all", "Sync all projects").option("-y, --yes", "Skip confirmation prompt").action(async (opts) => {
3154
+ await handleSyncErrors(async () => {
3155
+ const { runSync } = await import("./sync-DTCWK2CY.js");
3156
+ await runSync(opts);
3157
+ });
3158
+ });
2629
3159
  sync.addCommand(
2630
3160
  new Command4("status").description("Show local vs remote memory counts per project").action(async () => {
2631
3161
  await handleSyncErrors(async () => {
2632
- const { runSyncStatus } = await import("./sync-status-VZGZCZA3.js");
3162
+ const { runSyncStatus } = await import("./sync-status-2V2RCYZO.js");
2633
3163
  await runSyncStatus();
2634
3164
  });
2635
3165
  })
@@ -2637,7 +3167,7 @@ function createMemoryCommand() {
2637
3167
  sync.addCommand(
2638
3168
  new Command4("clean").description("Remove a project from the sync gist").argument("<project>", "Project slug to remove").option("-y, --yes", "Skip confirmation prompt").action(async (project, opts) => {
2639
3169
  await handleSyncErrors(async () => {
2640
- const { runSyncClean } = await import("./sync-clean-UHR2I27F.js");
3170
+ const { runSyncClean } = await import("./sync-clean-ACZCB3NZ.js");
2641
3171
  await runSyncClean(project, opts);
2642
3172
  });
2643
3173
  })
@@ -2647,8 +3177,8 @@ function createMemoryCommand() {
2647
3177
  }
2648
3178
 
2649
3179
  // src/cli.ts
2650
- var program = new Command5().name("claude-launchpad").description("Score your Claude Code config, fix the gaps, prove Claude follows your rules.").version("1.10.1", "-v, --version").action(async () => {
2651
- const hasConfig = await fileExists(join9(process.cwd(), "CLAUDE.md")) || await fileExists(join9(process.cwd(), ".claude", "settings.json"));
3180
+ var program = new Command5().name("claude-launchpad").description("Score your Claude Code config, fix the gaps, prove Claude follows your rules.").version("1.14.0", "-v, --version").action(async () => {
3181
+ const hasConfig = await fileExists(join13(process.cwd(), "CLAUDE.md")) || await fileExists(join13(process.cwd(), ".claude", "settings.json"));
2652
3182
  if (hasConfig) {
2653
3183
  await program.commands.find((c) => c.name() === "doctor")?.parseAsync([], { from: "user" });
2654
3184
  } else {