claude-launchpad 1.12.0 → 1.15.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 (61) hide show
  1. package/README.md +11 -3
  2. package/dist/{chunk-DTCCQWOU.js → chunk-7KB5YGQZ.js} +3 -3
  3. package/dist/{chunk-DTCCQWOU.js.map → chunk-7KB5YGQZ.js.map} +1 -1
  4. package/dist/chunk-DLDVXHI3.js +184 -0
  5. package/dist/chunk-DLDVXHI3.js.map +1 -0
  6. package/dist/chunk-HSGSTSVN.js +212 -0
  7. package/dist/chunk-HSGSTSVN.js.map +1 -0
  8. package/dist/{chunk-DXDOVWOA.js → chunk-IDH3M4EI.js} +20 -232
  9. package/dist/chunk-IDH3M4EI.js.map +1 -0
  10. package/dist/{chunk-YZ53W47Z.js → chunk-IIWOSSL4.js} +2 -1
  11. package/dist/chunk-IIWOSSL4.js.map +1 -0
  12. package/dist/{chunk-ZLI4LI33.js → chunk-IPGNXDJF.js} +2 -2
  13. package/dist/{chunk-PH5OWJ42.js → chunk-LCKV24Q3.js} +9 -5
  14. package/dist/{chunk-PH5OWJ42.js.map → chunk-LCKV24Q3.js.map} +1 -1
  15. package/dist/chunk-MX3T2VEX.js +225 -0
  16. package/dist/chunk-MX3T2VEX.js.map +1 -0
  17. package/dist/{chunk-F6SLV2FR.js → chunk-N2SCF7DB.js} +5 -6
  18. package/dist/chunk-N2SCF7DB.js.map +1 -0
  19. package/dist/{chunk-KPO4YURF.js → chunk-U2TFIUGE.js} +513 -89
  20. package/dist/chunk-U2TFIUGE.js.map +1 -0
  21. package/dist/cli.js +424 -130
  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-JALSYDYO.js → context-3GHG7KW6.js} +58 -55
  26. package/dist/context-3GHG7KW6.js.map +1 -0
  27. package/dist/doctor-EKGJ2YAO.js +108 -0
  28. package/dist/doctor-EKGJ2YAO.js.map +1 -0
  29. package/dist/{install-Q23F37GK.js → install-543ZKMXX.js} +12 -10
  30. package/dist/install-543ZKMXX.js.map +1 -0
  31. package/dist/{pull-FYKNNLYC.js → pull-ULSZEVCR.js} +17 -19
  32. package/dist/pull-ULSZEVCR.js.map +1 -0
  33. package/dist/{push-QSBK7BQU.js → push-JXCWX4NY.js} +13 -12
  34. package/dist/push-JXCWX4NY.js.map +1 -0
  35. package/dist/{require-deps-ZISUZLYD.js → require-deps-AEDO5YRM.js} +3 -3
  36. package/dist/{stats-SV53G6PQ.js → stats-63L7C24W.js} +9 -7
  37. package/dist/{stats-SV53G6PQ.js.map → stats-63L7C24W.js.map} +1 -1
  38. package/dist/{sync-2LDM44U4.js → sync-QTJ6BXQ4.js} +5 -5
  39. package/dist/{sync-clean-HQ523N7V.js → sync-clean-OJEK4XH4.js} +4 -5
  40. package/dist/sync-clean-OJEK4XH4.js.map +1 -0
  41. package/dist/{sync-status-V6WTI32X.js → sync-status-EAX527SC.js} +13 -12
  42. package/dist/sync-status-EAX527SC.js.map +1 -0
  43. package/dist/{tui-2UJ2OIHB.js → tui-3KITZCNS.js} +11 -7
  44. package/dist/{tui-2UJ2OIHB.js.map → tui-3KITZCNS.js.map} +1 -1
  45. package/package.json +27 -7
  46. package/scenarios/workflow/premature-victory.yaml +44 -0
  47. package/dist/chunk-DXDOVWOA.js.map +0 -1
  48. package/dist/chunk-F6SLV2FR.js.map +0 -1
  49. package/dist/chunk-KPO4YURF.js.map +0 -1
  50. package/dist/chunk-YZ53W47Z.js.map +0 -1
  51. package/dist/chunk-ZIG75MOB.js +0 -366
  52. package/dist/chunk-ZIG75MOB.js.map +0 -1
  53. package/dist/context-JALSYDYO.js.map +0 -1
  54. package/dist/install-Q23F37GK.js.map +0 -1
  55. package/dist/pull-FYKNNLYC.js.map +0 -1
  56. package/dist/push-QSBK7BQU.js.map +0 -1
  57. package/dist/sync-clean-HQ523N7V.js.map +0 -1
  58. package/dist/sync-status-V6WTI32X.js.map +0 -1
  59. /package/dist/{chunk-ZLI4LI33.js.map → chunk-IPGNXDJF.js.map} +0 -0
  60. /package/dist/{require-deps-ZISUZLYD.js.map → require-deps-AEDO5YRM.js.map} +0 -0
  61. /package/dist/{sync-2LDM44U4.js.map → sync-QTJ6BXQ4.js.map} +0 -0
package/dist/cli.js CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  readSyncConfig
4
- } from "./chunk-ZLI4LI33.js";
4
+ } from "./chunk-IPGNXDJF.js";
5
5
  import {
6
6
  BACKLOG_CONTENT,
7
7
  ENHANCE_SKILL_VERSION,
8
+ FORCE_PUSH_ERE,
8
9
  LP_STUB_OPEN,
9
10
  OFF_LIMITS_CONTENT,
10
11
  SESSION_START_CONTENT,
@@ -16,6 +17,7 @@ import {
16
17
  STALE_SWARM_PHRASE,
17
18
  STOP_AND_SWARM_CONTENT,
18
19
  TESTING_DISCIPLINE_CONTENT,
20
+ VERIFICATION_RULE_VERSION,
19
21
  WORKFLOW_CHECK_WRAPPER,
20
22
  WORKFLOW_RULE_VERSION,
21
23
  applyFixes,
@@ -26,6 +28,7 @@ import {
26
28
  generateEnhanceSkill,
27
29
  generateHooksRule,
28
30
  generateReviewerAgent,
31
+ generateVerificationRule,
29
32
  generateWorkflowRule,
30
33
  hasEnvVarHookPattern,
31
34
  isJqAvailable,
@@ -38,11 +41,11 @@ import {
38
41
  sprintReviewsContent,
39
42
  writeSprintHygieneScripts,
40
43
  writeWorkflowCheckScript
41
- } from "./chunk-KPO4YURF.js";
44
+ } from "./chunk-U2TFIUGE.js";
42
45
 
43
46
  // src/cli.ts
44
47
  import { Command as Command5 } from "commander";
45
- import { join as join13 } from "path";
48
+ import { join as join14 } from "path";
46
49
 
47
50
  // src/commands/init/index.ts
48
51
  import { Command } from "commander";
@@ -93,7 +96,8 @@ ${STOP_AND_SWARM_CONTENT}`);
93
96
  sections.push("", `## Off-Limits
94
97
  ${OFF_LIMITS_CONTENT}`);
95
98
  sections.push("", `## Key Decisions
96
- <!-- Record architectural decisions as you make them -->`);
99
+ <!-- Append one entry per non-obvious choice, at the moment it's made \u2014 not at sprint close: -->
100
+ <!-- YYYY-MM-DD \u2014 Chose X over Y because Z. Revisit if W. -->`);
97
101
  return sections.join("\n") + "\n";
98
102
  }
99
103
 
@@ -142,7 +146,7 @@ function generateSettings(detected) {
142
146
  matcher: "Bash",
143
147
  hooks: [{
144
148
  type: "command",
145
- 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`
149
+ command: `cmd=${jqField("command")}; echo "$cmd" | grep -qE 'rm\\s+-rf\\s+/|DROP\\s+TABLE|DROP\\s+DATABASE|${FORCE_PUSH_ERE}' && { echo 'BLOCKED: Destructive command detected' >&2; exit 2; }; exit 0`
146
150
  }]
147
151
  });
148
152
  const editWriteHooks = [];
@@ -250,16 +254,22 @@ function dirTreeContains(dir, needle, depth) {
250
254
 
251
255
  // src/commands/init/index.ts
252
256
  function createInitCommand() {
253
- 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) => {
257
+ return new Command("init").description("Set up Claude Code configuration for any project").option("-n, --name <name>", "Project name").option(
258
+ "-y, --yes",
259
+ "Accept all defaults (does not overwrite existing files)"
260
+ ).option("-f, --force", "Overwrite existing CLAUDE.md").action(async (opts) => {
254
261
  printBanner();
255
262
  const root = process.cwd();
256
263
  log.step("Detecting project...");
257
264
  const detected = await detectProject(root);
258
265
  if (detected.language) {
259
266
  log.success(`Found ${detected.framework ?? detected.language} project`);
260
- if (detected.packageManager) log.info(`Package manager: ${detected.packageManager}`);
261
- if (detected.devCommand) log.info(`Dev command: ${detected.devCommand}`);
262
- if (detected.testCommand) log.info(`Test command: ${detected.testCommand}`);
267
+ if (detected.packageManager)
268
+ log.info(`Package manager: ${detected.packageManager}`);
269
+ if (detected.devCommand)
270
+ log.info(`Dev command: ${detected.devCommand}`);
271
+ if (detected.testCommand)
272
+ log.info(`Test command: ${detected.testCommand}`);
263
273
  } else {
264
274
  log.warn("Could not detect project type \u2014 generating minimal config");
265
275
  }
@@ -271,13 +281,18 @@ function createInitCommand() {
271
281
  const description = opts.yes ? "" : await input({
272
282
  message: "One-line description (optional):"
273
283
  });
274
- const options = { name: name.trim(), description: description.trim() };
284
+ const options = {
285
+ name: name.trim(),
286
+ description: description.trim()
287
+ };
275
288
  const hasClaudeMd = await fileExists(join2(root, "CLAUDE.md"));
276
289
  if (hasClaudeMd) {
277
290
  if (opts.force) {
278
291
  log.warn("Overwriting existing CLAUDE.md (--force)");
279
292
  } else if (opts.yes) {
280
- log.error("CLAUDE.md already exists. `--yes` will not overwrite existing files.");
293
+ log.error(
294
+ "CLAUDE.md already exists. `--yes` will not overwrite existing files."
295
+ );
281
296
  log.info("Run `claude-launchpad doctor --fix` to update in place,");
282
297
  log.info("or re-run with `--force` to overwrite.");
283
298
  process.exitCode = 1;
@@ -290,7 +305,9 @@ function createInitCommand() {
290
305
  if (!overwrite) {
291
306
  log.info("Keeping existing CLAUDE.md");
292
307
  await createEnhanceSkillPrompt(root, false);
293
- log.step("Tip: run `claude-launchpad doctor` to check your existing config");
308
+ log.step(
309
+ "Tip: run `claude-launchpad doctor` to check your existing config"
310
+ );
294
311
  return;
295
312
  }
296
313
  }
@@ -300,14 +317,19 @@ function createInitCommand() {
300
317
  }
301
318
  async function scaffold(root, options, detected, skipPrompts) {
302
319
  log.step("Generating configuration...");
303
- const claudeMd = generateClaudeMd(options, detected, { superpowers: isSuperpowersInstalled() });
320
+ const claudeMd = generateClaudeMd(options, detected, {
321
+ superpowers: isSuperpowersInstalled()
322
+ });
304
323
  const tasksMd = generateTasksMd(options);
305
324
  const backlogMd = generateBacklogMd(options);
306
325
  const settings = generateSettings(detected);
307
326
  const claudeignore = generateClaudeignore(detected);
308
327
  await mkdir(join2(root, ".claude", "rules"), { recursive: true });
309
328
  const settingsPath = join2(root, ".claude", "settings.json");
310
- const mergedSettings = await mergeSettings(settingsPath, settings);
329
+ const mergedSettings = await mergeSettings(
330
+ settingsPath,
331
+ settings
332
+ );
311
333
  const backlogPath = join2(root, "BACKLOG.md");
312
334
  const hasBacklog = await fileExists(backlogPath);
313
335
  const claudeignorePath = join2(root, ".claudeignore");
@@ -320,6 +342,13 @@ async function scaffold(root, options, detected, skipPrompts) {
320
342
  const hasWorkflowRule = await fileExists(workflowRulePath);
321
343
  const hooksRulePath = join2(root, ".claude", "rules", "hooks.md");
322
344
  const hasHooksRule = await fileExists(hooksRulePath);
345
+ const verificationRulePath = join2(
346
+ root,
347
+ ".claude",
348
+ "rules",
349
+ "verification.md"
350
+ );
351
+ const hasVerificationRule = await fileExists(verificationRulePath);
323
352
  const writes = [
324
353
  writeFile(join2(root, "CLAUDE.md"), claudeMd),
325
354
  writeFile(join2(root, "TASKS.md"), tasksMd),
@@ -332,16 +361,21 @@ async function scaffold(root, options, detected, skipPrompts) {
332
361
  writes.push(writeFile(claudeignorePath, claudeignore));
333
362
  }
334
363
  if (!hasClaudeGitignore) {
335
- writes.push(writeFile(claudeGitignorePath, [
336
- "# Local-only Claude Code files (never commit these)",
337
- "CLAUDE.md",
338
- "settings.local.json",
339
- "plans/",
340
- "memory/",
341
- "sessions/",
342
- "tmp/",
343
- ""
344
- ].join("\n")));
364
+ writes.push(
365
+ writeFile(
366
+ claudeGitignorePath,
367
+ [
368
+ "# Local-only Claude Code files (never commit these)",
369
+ "CLAUDE.md",
370
+ "settings.local.json",
371
+ "plans/",
372
+ "memory/",
373
+ "sessions/",
374
+ "tmp/",
375
+ ""
376
+ ].join("\n")
377
+ )
378
+ );
345
379
  }
346
380
  if (!hasRules) {
347
381
  const rulesContent = generateStarterRules(detected);
@@ -353,6 +387,9 @@ async function scaffold(root, options, detected, skipPrompts) {
353
387
  if (!hasHooksRule) {
354
388
  writes.push(writeFile(hooksRulePath, generateHooksRule()));
355
389
  }
390
+ if (!hasVerificationRule) {
391
+ writes.push(writeFile(verificationRulePath, generateVerificationRule()));
392
+ }
356
393
  const reviewerAgentPath = join2(root, ".claude", "agents", "code-reviewer.md");
357
394
  const hasReviewerAgent = await fileExists(reviewerAgentPath);
358
395
  if (!hasReviewerAgent) {
@@ -363,26 +400,47 @@ async function scaffold(root, options, detected, skipPrompts) {
363
400
  await writeSprintHygieneScripts(root);
364
401
  await writeWorkflowCheckScript(root);
365
402
  log.success("Generated CLAUDE.md");
366
- if (!hasReviewerAgent) log.success("Generated .claude/agents/code-reviewer.md (independent sprint reviewer)");
403
+ if (!hasReviewerAgent)
404
+ log.success(
405
+ "Generated .claude/agents/code-reviewer.md (independent sprint reviewer)"
406
+ );
367
407
  log.success("Generated TASKS.md");
368
408
  if (!hasBacklog) log.success("Generated BACKLOG.md");
369
409
  log.success("Generated .claude/settings.json (schema, permissions, hooks)");
370
410
  if (!hasClaudeGitignore) log.success("Generated .claude/.gitignore");
371
411
  if (!hasClaudeignore) log.success("Generated .claudeignore");
372
412
  if (!hasRules) log.success("Generated .claude/rules/conventions.md");
373
- if (!hasWorkflowRule) log.success("Generated .claude/rules/workflow.md (workflow rules, path-scoped)");
374
- if (!hasHooksRule) log.success("Generated .claude/rules/hooks.md (hook authoring rules, path-scoped)");
375
- log.success("Generated .claude/hooks/sprint-{size,open}-check.sh + workflow-check.sh");
413
+ if (!hasWorkflowRule)
414
+ log.success(
415
+ "Generated .claude/rules/workflow.md (workflow rules, path-scoped)"
416
+ );
417
+ if (!hasHooksRule)
418
+ log.success(
419
+ "Generated .claude/rules/hooks.md (hook authoring rules, path-scoped)"
420
+ );
421
+ if (!hasVerificationRule)
422
+ log.success(
423
+ "Generated .claude/rules/verification.md (evidence-before-assertion discipline)"
424
+ );
425
+ log.success(
426
+ "Generated .claude/hooks/sprint-{size,open}-check.sh + workflow-check.sh"
427
+ );
376
428
  await createEnhanceSkillPrompt(root, skipPrompts);
377
429
  log.blank();
378
430
  if (!isJqAvailable()) {
379
- log.warn("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
+ log.warn(
432
+ "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/"
433
+ );
380
434
  }
381
435
  if (!isSuperpowersInstalled()) {
382
- log.info("Optional: the superpowers plugin adds brainstorm/plan/TDD/review discipline \u2014 /plugin install superpowers@claude-plugins-official");
436
+ log.info(
437
+ "Optional: the superpowers plugin adds brainstorm/plan/TDD/review discipline \u2014 /plugin install superpowers@claude-plugins-official"
438
+ );
383
439
  }
384
440
  log.success("Done! Run `claude` to start.");
385
- log.info("Use `/lp-enhance` inside Claude Code to have AI complete your CLAUDE.md.");
441
+ log.info(
442
+ "Use `/lp-enhance` inside Claude Code to have AI complete your CLAUDE.md."
443
+ );
386
444
  log.info("Run `claude-launchpad doctor` to check your config quality.");
387
445
  log.blank();
388
446
  }
@@ -414,19 +472,32 @@ function generateStarterRules(detected) {
414
472
  lines.push("", "## Testing Discipline", "", TESTING_DISCIPLINE_CONTENT);
415
473
  const verify = [detected.testCommand, detected.lintCommand].filter((c) => !!c).map((c) => `\`${c}\``).join(" && ");
416
474
  lines.push("", "## Pre-Commit Checklist", "");
417
- lines.push(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");
418
- lines.push("2. For hard-TDD surfaces, confirm the test was written before the implementation");
419
- lines.push("3. Before sprint-ending commits: run /code-review on the sprint diff (see ## Sprint Reviews in CLAUDE.md)");
475
+ lines.push(
476
+ 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"
477
+ );
478
+ lines.push(
479
+ "2. For hard-TDD surfaces, confirm the test was written before the implementation"
480
+ );
481
+ lines.push(
482
+ "3. Before sprint-ending commits: run /code-review on the sprint diff (see ## Sprint Reviews in CLAUDE.md)"
483
+ );
420
484
  lines.push("", "## Skill Authoring", "", SKILL_AUTHORING_CONTENT);
421
485
  lines.push("");
422
486
  return lines.join("\n");
423
487
  }
424
488
  async function createEnhanceSkillPrompt(root, skipPrompts) {
425
489
  const projectPath = join2(root, ".claude", "skills", "lp-enhance", "SKILL.md");
426
- const globalPath = join2(homedir2(), ".claude", "skills", "lp-enhance", "SKILL.md");
490
+ const globalPath = join2(
491
+ homedir2(),
492
+ ".claude",
493
+ "skills",
494
+ "lp-enhance",
495
+ "SKILL.md"
496
+ );
427
497
  const legacyProject = join2(root, ".claude", "commands", "lp-enhance.md");
428
498
  const legacyGlobal = join2(homedir2(), ".claude", "commands", "lp-enhance.md");
429
- if (await fileExists(projectPath) || await fileExists(globalPath) || await fileExists(legacyProject) || await fileExists(legacyGlobal)) return;
499
+ if (await fileExists(projectPath) || await fileExists(globalPath) || await fileExists(legacyProject) || await fileExists(legacyGlobal))
500
+ return;
430
501
  const scope = skipPrompts ? "project" : await select({
431
502
  message: "Install /lp-enhance skill (AI-powered CLAUDE.md improver):",
432
503
  choices: [
@@ -443,7 +514,9 @@ async function createEnhanceSkillPrompt(root, skipPrompts) {
443
514
  }
444
515
  async function mergeSettings(existingPath, generated) {
445
516
  try {
446
- const existing = JSON.parse(await readFile(existingPath, "utf-8"));
517
+ const existing = JSON.parse(
518
+ await readFile(existingPath, "utf-8")
519
+ );
447
520
  const existingHooks = existing.hooks ?? {};
448
521
  const generatedHooks = generated.hooks ?? {};
449
522
  const mergedHooks = { ...existingHooks };
@@ -834,7 +907,28 @@ async function analyzeHooks(config, projectRoot = "") {
834
907
  });
835
908
  return { name: "Hooks", issues, score: 30 };
836
909
  }
837
- const formatPatterns = ["format", "prettier", "gofmt", "rustfmt", "rubocop", "pint", "ktlint", "swift-format", "dotnet format"];
910
+ const hasStaleForcePattern = config.hooks.some(
911
+ (h) => h.command?.includes("push.*--force|push.*-f")
912
+ );
913
+ if (hasStaleForcePattern) {
914
+ issues.push({
915
+ analyzer: "Hooks",
916
+ severity: "low",
917
+ message: "Force-push guard uses the outdated pattern \u2014 it false-positives on unrelated commands (git stash push, --frozen-lockfile, filenames)",
918
+ fix: "Run `doctor --fix` to upgrade it to the anchored pattern"
919
+ });
920
+ }
921
+ const formatPatterns = [
922
+ "format",
923
+ "prettier",
924
+ "gofmt",
925
+ "rustfmt",
926
+ "rubocop",
927
+ "pint",
928
+ "ktlint",
929
+ "swift-format",
930
+ "dotnet format"
931
+ ];
838
932
  const hasPostFormat = hooks.some(
839
933
  (h) => h.event === "PostToolUse" && h.matcher?.includes("Write") && formatPatterns.some((p) => h.command?.includes(p))
840
934
  );
@@ -976,10 +1070,14 @@ async function analyzeHooks(config, projectRoot = "") {
976
1070
  }
977
1071
  async function hasStaleHygieneScript(projectRoot) {
978
1072
  const { readFile: readFile7 } = await import("fs/promises");
979
- const { join: join14 } = await import("path");
1073
+ const { join: join15 } = await import("path");
980
1074
  for (const name of ["workflow-check.sh", "sprint-open-check.sh"]) {
981
- const content = await readFile7(join14(projectRoot, ".claude", "hooks", name), "utf-8").catch(() => null);
982
- if (content !== null && !content.includes("hookSpecificOutput")) return true;
1075
+ const content = await readFile7(
1076
+ join15(projectRoot, ".claude", "hooks", name),
1077
+ "utf-8"
1078
+ ).catch(() => null);
1079
+ if (content !== null && !content.includes("hookSpecificOutput"))
1080
+ return true;
983
1081
  }
984
1082
  return false;
985
1083
  }
@@ -1000,7 +1098,9 @@ async function analyzeRules(config) {
1000
1098
  fix: "Run `claude-launchpad init` or `doctor --fix` to generate one"
1001
1099
  });
1002
1100
  }
1003
- const hasWorkflowRule = await fileExists(join4(projectRoot, ".claude", "rules", "workflow.md"));
1101
+ const hasWorkflowRule = await fileExists(
1102
+ join4(projectRoot, ".claude", "rules", "workflow.md")
1103
+ );
1004
1104
  if (!hasWorkflowRule) {
1005
1105
  issues.push({
1006
1106
  analyzer: "Rules",
@@ -1009,7 +1109,9 @@ async function analyzeRules(config) {
1009
1109
  fix: "Run `doctor --fix` to generate it"
1010
1110
  });
1011
1111
  }
1012
- const hasReviewerAgent = await fileExists(join4(projectRoot, ".claude", "agents", "code-reviewer.md"));
1112
+ const hasReviewerAgent = await fileExists(
1113
+ join4(projectRoot, ".claude", "agents", "code-reviewer.md")
1114
+ );
1013
1115
  if (!hasReviewerAgent) {
1014
1116
  issues.push({
1015
1117
  analyzer: "Rules",
@@ -1026,7 +1128,10 @@ async function analyzeRules(config) {
1026
1128
  });
1027
1129
  }
1028
1130
  if (hasWorkflowRule) {
1029
- const wfContent = await readFile2(join4(projectRoot, ".claude", "rules", "workflow.md"), "utf-8").catch(() => "");
1131
+ const wfContent = await readFile2(
1132
+ join4(projectRoot, ".claude", "rules", "workflow.md"),
1133
+ "utf-8"
1134
+ ).catch(() => "");
1030
1135
  const wfMatch = wfContent.match(/<!-- lp-workflow-version: (\d+) -->/);
1031
1136
  const wfVersion = wfMatch ? parseInt(wfMatch[1], 10) : null;
1032
1137
  if (wfVersion !== null && wfVersion < WORKFLOW_RULE_VERSION) {
@@ -1038,7 +1143,9 @@ async function analyzeRules(config) {
1038
1143
  });
1039
1144
  }
1040
1145
  }
1041
- const hasHooksRule = await fileExists(join4(projectRoot, ".claude", "rules", "hooks.md"));
1146
+ const hasHooksRule = await fileExists(
1147
+ join4(projectRoot, ".claude", "rules", "hooks.md")
1148
+ );
1042
1149
  if (!hasHooksRule) {
1043
1150
  issues.push({
1044
1151
  analyzer: "Rules",
@@ -1047,6 +1154,33 @@ async function analyzeRules(config) {
1047
1154
  fix: "Run `doctor --fix` to generate it"
1048
1155
  });
1049
1156
  }
1157
+ const verificationPath = join4(
1158
+ projectRoot,
1159
+ ".claude",
1160
+ "rules",
1161
+ "verification.md"
1162
+ );
1163
+ const hasVerificationRule = await fileExists(verificationPath);
1164
+ if (!hasVerificationRule) {
1165
+ issues.push({
1166
+ analyzer: "Rules",
1167
+ severity: "medium",
1168
+ message: "No .claude/rules/verification.md found \u2014 nothing stops premature 'done' claims without evidence",
1169
+ fix: "Run `doctor --fix` to generate it"
1170
+ });
1171
+ } else {
1172
+ const vContent = await readFile2(verificationPath, "utf-8").catch(() => "");
1173
+ const vMatch = vContent.match(/<!-- lp-verification-version: (\d+) -->/);
1174
+ const vVersion = vMatch ? parseInt(vMatch[1], 10) : null;
1175
+ if (vVersion !== null && vVersion < VERIFICATION_RULE_VERSION) {
1176
+ issues.push({
1177
+ analyzer: "Rules",
1178
+ severity: "low",
1179
+ message: `verification.md rule is outdated (v${vVersion}, latest v${VERIFICATION_RULE_VERSION})`,
1180
+ fix: "Run `doctor --fix` to update it"
1181
+ });
1182
+ }
1183
+ }
1050
1184
  const hasClaudeignore = await fileExists(join4(projectRoot, ".claudeignore"));
1051
1185
  if (!hasClaudeignore) {
1052
1186
  issues.push({
@@ -1059,7 +1193,9 @@ async function analyzeRules(config) {
1059
1193
  const hasSkillInProject = config.skills.some(
1060
1194
  (s) => basename(s) === "SKILL.md" && s.includes("lp-enhance") || basename(s) === "lp-enhance.md"
1061
1195
  );
1062
- const hasSkillGlobal = await fileExists(join4(homedir3(), ".claude", "skills", "lp-enhance", "SKILL.md")) || await fileExists(join4(homedir3(), ".claude", "commands", "lp-enhance.md"));
1196
+ const hasSkillGlobal = await fileExists(
1197
+ join4(homedir3(), ".claude", "skills", "lp-enhance", "SKILL.md")
1198
+ ) || await fileExists(join4(homedir3(), ".claude", "commands", "lp-enhance.md"));
1063
1199
  if (!hasSkillInProject && !hasSkillGlobal) {
1064
1200
  issues.push({
1065
1201
  analyzer: "Rules",
@@ -1371,6 +1507,11 @@ function extractAllow(settings) {
1371
1507
  return Array.isArray(allow) ? allow.filter((e) => typeof e === "string") : [];
1372
1508
  }
1373
1509
 
1510
+ // src/commands/doctor/analyzers/quality.ts
1511
+ import { execSync } from "child_process";
1512
+ import { existsSync as existsSync2 } from "fs";
1513
+ import { join as join6 } from "path";
1514
+
1374
1515
  // src/commands/doctor/analyzers/hook-resolver.ts
1375
1516
  import { readFile as readFile3, realpath } from "fs/promises";
1376
1517
  import { resolve as resolve2, sep } from "path";
@@ -1530,6 +1671,17 @@ async function analyzeMemory(config, projectRoot) {
1530
1671
  fix: "Run `doctor --fix` to add agentic-memory to allowedMcpServers"
1531
1672
  });
1532
1673
  }
1674
+ const hasBareBinaryHook = config.hooks.some(
1675
+ (h) => h.command?.includes("claude-launchpad memory") && !h.command.includes("npx claude-launchpad")
1676
+ );
1677
+ if (hasBareBinaryHook) {
1678
+ issues.push({
1679
+ analyzer: "Memory",
1680
+ severity: "medium",
1681
+ message: "Memory hook(s) call the bare claude-launchpad binary \u2014 fails silently on machines without a global install",
1682
+ fix: "Run `doctor --fix` to rewrite them to the npx form"
1683
+ });
1684
+ }
1533
1685
  const syncConfig = readSyncConfig();
1534
1686
  if (syncConfig) {
1535
1687
  const hasSessionStartPull = config.hooks.some(
@@ -1774,7 +1926,10 @@ function documentSatisfiesIntent(sections, rule) {
1774
1926
 
1775
1927
  // src/commands/doctor/analyzers/quality.ts
1776
1928
  var VAGUE_PATTERNS = [
1777
- { pattern: /write (good|clean|quality|nice) code/i, label: "write good code" },
1929
+ {
1930
+ pattern: /write (good|clean|quality|nice) code/i,
1931
+ label: "write good code"
1932
+ },
1778
1933
  { pattern: /be (careful|thorough|diligent)/i, label: "be careful" },
1779
1934
  { pattern: /follow best practices/i, label: "follow best practices" },
1780
1935
  { pattern: /make sure (everything|it) works/i, label: "make sure it works" }
@@ -1857,17 +2012,56 @@ async function analyzeQuality(config, projectRoot) {
1857
2012
  fix: "Run `doctor --fix` to collapse them"
1858
2013
  });
1859
2014
  }
2015
+ if (isKeyDecisionsPlaceholder(content) && commitCount(projectRoot) >= 20) {
2016
+ issues.push({
2017
+ analyzer: "Quality",
2018
+ severity: "low",
2019
+ message: "Key Decisions is still placeholder-only after 20+ commits \u2014 decisions are being made but not logged",
2020
+ fix: "Append entries at decision time: `YYYY-MM-DD \u2014 Chose X over Y because Z. Revisit if W.`"
2021
+ });
2022
+ }
1860
2023
  const criticals = issues.filter((i) => i.severity === "critical").length;
1861
2024
  const highs = issues.filter((i) => i.severity === "high").length;
1862
2025
  const mediums = issues.filter((i) => i.severity === "medium").length;
1863
2026
  const lows = issues.filter((i) => i.severity === "low").length;
1864
- const score = Math.max(0, 100 - criticals * 40 - highs * 30 - mediums * 15 - lows * 5);
2027
+ const score = Math.max(
2028
+ 0,
2029
+ 100 - criticals * 40 - highs * 30 - mediums * 15 - lows * 5
2030
+ );
1865
2031
  return { name: "CLAUDE.md Quality", issues, score };
1866
2032
  }
2033
+ function isKeyDecisionsPlaceholder(content) {
2034
+ const lines = content.split("\n");
2035
+ const start = lines.findIndex((l) => /^## Key Decisions\s*$/.test(l));
2036
+ if (start === -1) return false;
2037
+ let end = lines.length;
2038
+ for (let i = start + 1; i < lines.length; i++) {
2039
+ if (/^## /.test(lines[i])) {
2040
+ end = i;
2041
+ break;
2042
+ }
2043
+ }
2044
+ const body = lines.slice(start + 1, end).join("\n").replace(/<!--[\s\S]*?-->/g, "").trim();
2045
+ return body.length === 0;
2046
+ }
2047
+ function commitCount(projectRoot) {
2048
+ if (!existsSync2(join6(projectRoot, ".git"))) return 0;
2049
+ try {
2050
+ return parseInt(
2051
+ execSync("git rev-list --count HEAD", {
2052
+ cwd: projectRoot,
2053
+ stdio: ["ignore", "pipe", "ignore"]
2054
+ }).toString().trim(),
2055
+ 10
2056
+ ) || 0;
2057
+ } catch {
2058
+ return 0;
2059
+ }
2060
+ }
1867
2061
 
1868
2062
  // src/commands/doctor/analyzers/workflow.ts
1869
2063
  import { readFile as readFile4 } from "fs/promises";
1870
- import { join as join6 } from "path";
2064
+ import { join as join7 } from "path";
1871
2065
  var WP_FIELDS = [
1872
2066
  "Priority:",
1873
2067
  "Proposed:",
@@ -1880,7 +2074,7 @@ var WP_FIELDS = [
1880
2074
  var STALE_P0_DAYS = 14;
1881
2075
  var STALE_CHANGELOG_DAYS = 30;
1882
2076
  async function analyzeWorkflow(projectRoot, now = /* @__PURE__ */ new Date()) {
1883
- const backlog = await readFile4(join6(projectRoot, "BACKLOG.md"), "utf-8").catch(() => null);
2077
+ const backlog = await readFile4(join7(projectRoot, "BACKLOG.md"), "utf-8").catch(() => null);
1884
2078
  if (backlog === null) return null;
1885
2079
  const issues = [];
1886
2080
  const pSections = extractPSections(backlog);
@@ -1961,7 +2155,7 @@ function daysBetween(isoDate, now) {
1961
2155
 
1962
2156
  // src/commands/doctor/watcher.ts
1963
2157
  import { readdir as readdir2, stat } from "fs/promises";
1964
- import { join as join7 } from "path";
2158
+ import { join as join8 } from "path";
1965
2159
  async function watchConfig(projectRoot) {
1966
2160
  await runAndDisplay(projectRoot);
1967
2161
  log.blank();
@@ -1984,16 +2178,16 @@ async function watchConfig(projectRoot) {
1984
2178
  }
1985
2179
  async function getFileSnapshot(projectRoot) {
1986
2180
  const files = [
1987
- join7(projectRoot, "CLAUDE.md"),
1988
- join7(projectRoot, ".claudeignore")
2181
+ join8(projectRoot, "CLAUDE.md"),
2182
+ join8(projectRoot, ".claudeignore")
1989
2183
  ];
1990
- const claudeDir = join7(projectRoot, ".claude");
2184
+ const claudeDir = join8(projectRoot, ".claude");
1991
2185
  try {
1992
2186
  const entries = await readdir2(claudeDir, { withFileTypes: true, recursive: true });
1993
2187
  for (const entry of entries) {
1994
2188
  if (entry.isFile()) {
1995
2189
  const parentPath = entry.parentPath ?? claudeDir;
1996
- files.push(join7(parentPath, entry.name));
2190
+ files.push(join8(parentPath, entry.name));
1997
2191
  }
1998
2192
  }
1999
2193
  } catch {
@@ -2137,11 +2331,11 @@ import { select as select2 } from "@inquirer/prompts";
2137
2331
  import ora from "ora";
2138
2332
  import chalk2 from "chalk";
2139
2333
  import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
2140
- import { join as join11 } from "path";
2334
+ import { join as join12 } from "path";
2141
2335
 
2142
2336
  // src/commands/eval/loader.ts
2143
2337
  import { readFile as readFile5, readdir as readdir3 } from "fs/promises";
2144
- import { join as join8, resolve as resolve3, dirname as dirname2 } from "path";
2338
+ import { join as join9, resolve as resolve3, dirname as dirname2 } from "path";
2145
2339
  import { fileURLToPath } from "url";
2146
2340
  import { parse as parseYaml } from "yaml";
2147
2341
 
@@ -2270,7 +2464,7 @@ async function findScenariosDir() {
2270
2464
  async function loadScenarios(options) {
2271
2465
  const { suite, customPath } = options;
2272
2466
  const scenarioDir = customPath ? resolve3(customPath) : await findScenariosDir();
2273
- const dirs = suite ? [join8(scenarioDir, suite)] : await getSubdirectories(scenarioDir);
2467
+ const dirs = suite ? [join9(scenarioDir, suite)] : await getSubdirectories(scenarioDir);
2274
2468
  const allDirs = [scenarioDir, ...dirs];
2275
2469
  const scenarios = [];
2276
2470
  for (const dir of allDirs) {
@@ -2292,7 +2486,7 @@ async function loadScenarios(options) {
2292
2486
  async function getSubdirectories(dir) {
2293
2487
  try {
2294
2488
  const entries = await readdir3(dir, { withFileTypes: true });
2295
- return entries.filter((e) => e.isDirectory()).map((e) => join8(dir, e.name));
2489
+ return entries.filter((e) => e.isDirectory()).map((e) => join9(dir, e.name));
2296
2490
  } catch {
2297
2491
  return [];
2298
2492
  }
@@ -2300,15 +2494,19 @@ async function getSubdirectories(dir) {
2300
2494
  async function listYamlFiles(dir) {
2301
2495
  try {
2302
2496
  const entries = await readdir3(dir, { withFileTypes: true });
2303
- return entries.filter((e) => e.isFile() && (e.name.endsWith(".yaml") || e.name.endsWith(".yml"))).map((e) => join8(dir, e.name));
2497
+ return entries.filter((e) => e.isFile() && (e.name.endsWith(".yaml") || e.name.endsWith(".yml"))).map((e) => join9(dir, e.name));
2304
2498
  } catch {
2305
2499
  return [];
2306
2500
  }
2307
2501
  }
2502
+ function resolveRuns(scenarioRuns, cliRuns, userChoseRuns) {
2503
+ if (userChoseRuns) return cliRuns;
2504
+ return scenarioRuns ?? cliRuns;
2505
+ }
2308
2506
 
2309
2507
  // src/commands/eval/runner.ts
2310
2508
  import { mkdir as mkdir2, writeFile as writeFile2, rm, cp } from "fs/promises";
2311
- import { join as join10, dirname as dirname3 } from "path";
2509
+ import { join as join11, dirname as dirname3 } from "path";
2312
2510
  import { tmpdir as tmpdir2 } from "os";
2313
2511
  import { randomUUID } from "crypto";
2314
2512
  import { execFile as execFile2 } from "child_process";
@@ -2316,7 +2514,7 @@ import { promisify as promisify2 } from "util";
2316
2514
 
2317
2515
  // src/commands/eval/checks.ts
2318
2516
  import { readFile as readFile6, readdir as readdir4 } from "fs/promises";
2319
- import { join as join9 } from "path";
2517
+ import { join as join10 } from "path";
2320
2518
  import { tmpdir } from "os";
2321
2519
  import { execFile } from "child_process";
2322
2520
  import { promisify } from "util";
@@ -2351,7 +2549,7 @@ async function evaluateSingleCheck(check, sandboxDir, context) {
2351
2549
  async function checkGrep(check, sandboxDir) {
2352
2550
  if (!check.pattern || !check.target) return false;
2353
2551
  try {
2354
- const content = await readFile6(join9(sandboxDir, check.target), "utf-8");
2552
+ const content = await readFile6(join10(sandboxDir, check.target), "utf-8");
2355
2553
  let found;
2356
2554
  try {
2357
2555
  found = new RegExp(check.pattern).test(content);
@@ -2366,7 +2564,7 @@ async function checkGrep(check, sandboxDir) {
2366
2564
  async function checkFilePresence(check, sandboxDir) {
2367
2565
  if (!check.target) return false;
2368
2566
  try {
2369
- await readFile6(join9(sandboxDir, check.target));
2567
+ await readFile6(join10(sandboxDir, check.target));
2370
2568
  return check.expect === "present";
2371
2569
  } catch {
2372
2570
  return check.expect === "absent";
@@ -2376,7 +2574,7 @@ async function checkMaxLines(check, sandboxDir) {
2376
2574
  if (!check.target) return false;
2377
2575
  const maxLines = parseInt(check.pattern ?? "800", 10);
2378
2576
  try {
2379
- const files = await listAllFiles(join9(sandboxDir, check.target));
2577
+ const files = await listAllFiles(join10(sandboxDir, check.target));
2380
2578
  for (const file of files) {
2381
2579
  const content = await readFile6(file, "utf-8");
2382
2580
  if (content.split("\n").length > maxLines) {
@@ -2452,7 +2650,7 @@ async function listAllFiles(dir) {
2452
2650
  try {
2453
2651
  const entries = await readdir4(dir, { withFileTypes: true });
2454
2652
  for (const entry of entries) {
2455
- const fullPath = join9(dir, entry.name);
2653
+ const fullPath = join10(dir, entry.name);
2456
2654
  if (entry.isDirectory()) {
2457
2655
  results.push(...await listAllFiles(fullPath));
2458
2656
  } else {
@@ -2467,7 +2665,7 @@ async function listAllFiles(dir) {
2467
2665
  // src/commands/eval/runner.ts
2468
2666
  var exec2 = promisify2(execFile2);
2469
2667
  async function runScenario(scenario, options) {
2470
- const sandboxDir = join10(tmpdir2(), `claude-eval-${randomUUID()}`);
2668
+ const sandboxDir = join11(tmpdir2(), `claude-eval-${randomUUID()}`);
2471
2669
  try {
2472
2670
  await setupSandbox(sandboxDir, scenario, options.projectRoot);
2473
2671
  const transcript = await runClaudeInSandbox(sandboxDir, scenario.prompt, options.timeout, options.model);
@@ -2493,14 +2691,14 @@ async function runScenarioWithRetries(scenario, options) {
2493
2691
  async function setupSandbox(sandboxDir, scenario, projectRoot) {
2494
2692
  await mkdir2(sandboxDir, { recursive: true });
2495
2693
  for (const file of scenario.setup.files) {
2496
- const filePath = join10(sandboxDir, file.path);
2694
+ const filePath = join11(sandboxDir, file.path);
2497
2695
  await mkdir2(dirname3(filePath), { recursive: true });
2498
2696
  await writeFile2(filePath, file.content);
2499
2697
  }
2500
2698
  await copyProjectConfig(sandboxDir, projectRoot);
2501
2699
  if (scenario.setup.instructions) {
2502
2700
  await writeFile2(
2503
- join10(sandboxDir, "CLAUDE.md"),
2701
+ join11(sandboxDir, "CLAUDE.md"),
2504
2702
  `# Eval Scenario
2505
2703
 
2506
2704
  ${scenario.setup.instructions}
@@ -2521,20 +2719,20 @@ ${scenario.setup.instructions}
2521
2719
  ], { cwd: sandboxDir });
2522
2720
  }
2523
2721
  async function copyProjectConfig(sandboxDir, projectRoot) {
2524
- const claudeDir = join10(projectRoot, ".claude");
2525
- const sandboxClaudeDir = join10(sandboxDir, ".claude");
2526
- const settingsPath = join10(claudeDir, "settings.json");
2722
+ const claudeDir = join11(projectRoot, ".claude");
2723
+ const sandboxClaudeDir = join11(sandboxDir, ".claude");
2724
+ const settingsPath = join11(claudeDir, "settings.json");
2527
2725
  if (await fileExists(settingsPath)) {
2528
2726
  await mkdir2(sandboxClaudeDir, { recursive: true });
2529
- await cp(settingsPath, join10(sandboxClaudeDir, "settings.json"));
2727
+ await cp(settingsPath, join11(sandboxClaudeDir, "settings.json"));
2530
2728
  }
2531
- const rulesDir = join10(claudeDir, "rules");
2729
+ const rulesDir = join11(claudeDir, "rules");
2532
2730
  if (await fileExists(rulesDir)) {
2533
- await cp(rulesDir, join10(sandboxClaudeDir, "rules"), { recursive: true });
2731
+ await cp(rulesDir, join11(sandboxClaudeDir, "rules"), { recursive: true });
2534
2732
  }
2535
- const ignorePath = join10(projectRoot, ".claudeignore");
2733
+ const ignorePath = join11(projectRoot, ".claudeignore");
2536
2734
  if (await fileExists(ignorePath)) {
2537
- await cp(ignorePath, join10(sandboxDir, ".claudeignore"));
2735
+ await cp(ignorePath, join11(sandboxDir, ".claudeignore"));
2538
2736
  }
2539
2737
  }
2540
2738
  async function runClaudeInSandbox(cwd, prompt, timeout, model) {
@@ -2616,8 +2814,19 @@ async function scoreResults(scenario, sandboxDir, transcript, model) {
2616
2814
 
2617
2815
  // src/commands/eval/index.ts
2618
2816
  function createEvalCommand() {
2619
- 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) => {
2817
+ return new Command3("eval").description("Test your Claude Code config against eval scenarios").option(
2818
+ "-s, --suite <suite>",
2819
+ "Eval suite to run (e.g., security, conventions, workflow)"
2820
+ ).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(
2821
+ "--timeout <ms>",
2822
+ "Timeout per run in ms (default: 120000)",
2823
+ "120000"
2824
+ ).option("--json", "Output as JSON").option("--debug", "Keep sandbox directories for inspection").option(
2825
+ "--model <model>",
2826
+ "Model to use for eval (e.g., sonnet, haiku, opus)"
2827
+ ).action(async (opts, command) => {
2620
2828
  printBanner();
2829
+ let userChoseRuns = command.getOptionValueSource("runs") === "cli";
2621
2830
  const hasFlags = opts.suite || opts.model || opts.runs !== "3" || opts.timeout !== "120000" || opts.path !== process.cwd() || Boolean(opts.scenarios) || opts.json || opts.debug;
2622
2831
  if (!hasFlags) {
2623
2832
  opts.suite = await select2({
@@ -2625,8 +2834,8 @@ function createEvalCommand() {
2625
2834
  choices: [
2626
2835
  { name: "security (7 scenarios)", value: "security" },
2627
2836
  { name: "conventions (5 scenarios)", value: "conventions" },
2628
- { name: "workflow (4 scenarios)", value: "workflow" },
2629
- { name: "all (16 scenarios)", value: void 0 }
2837
+ { name: "workflow (5 scenarios)", value: "workflow" },
2838
+ { name: "all (17 scenarios)", value: void 0 }
2630
2839
  ]
2631
2840
  });
2632
2841
  opts.runs = await select2({
@@ -2637,6 +2846,7 @@ function createEvalCommand() {
2637
2846
  { name: "5 \u2014 thorough", value: "5" }
2638
2847
  ]
2639
2848
  });
2849
+ userChoseRuns = true;
2640
2850
  opts.model = await select2({
2641
2851
  message: "Model",
2642
2852
  choices: [
@@ -2649,8 +2859,12 @@ function createEvalCommand() {
2649
2859
  }
2650
2860
  const claudeAvailable = await checkClaudeCli();
2651
2861
  if (!claudeAvailable) {
2652
- log.error("Claude CLI not found. Install it: https://docs.anthropic.com/en/docs/claude-code");
2653
- log.info("The eval command runs Claude headless against scenarios \u2014 it requires the CLI.");
2862
+ log.error(
2863
+ "Claude CLI not found. Install it: https://docs.anthropic.com/en/docs/claude-code"
2864
+ );
2865
+ log.info(
2866
+ "The eval command runs Claude headless against scenarios \u2014 it requires the CLI."
2867
+ );
2654
2868
  process.exit(1);
2655
2869
  }
2656
2870
  log.step("Loading eval scenarios...");
@@ -2661,7 +2875,9 @@ function createEvalCommand() {
2661
2875
  if (scenarios.length === 0) {
2662
2876
  log.warn("No matching scenarios found.");
2663
2877
  if (opts.suite) {
2664
- log.info(`Check that the suite "${opts.suite}" exists in the scenarios directory.`);
2878
+ log.info(
2879
+ `Check that the suite "${opts.suite}" exists in the scenarios directory.`
2880
+ );
2665
2881
  }
2666
2882
  return;
2667
2883
  }
@@ -2670,10 +2886,11 @@ function createEvalCommand() {
2670
2886
  log.info(`Model: ${opts.model}`);
2671
2887
  }
2672
2888
  log.blank();
2673
- const runs = parseInt(opts.runs, 10);
2889
+ const cliRuns = parseInt(opts.runs, 10);
2674
2890
  const timeout = parseInt(opts.timeout, 10);
2675
2891
  const results = [];
2676
2892
  for (const scenario of scenarios) {
2893
+ const runs = resolveRuns(scenario.runs, cliRuns, userChoseRuns);
2677
2894
  const spinner = ora({
2678
2895
  text: `Running: ${scenario.name} (${runs} run${runs > 1 ? "s" : ""})`,
2679
2896
  prefixText: " "
@@ -2681,13 +2898,22 @@ function createEvalCommand() {
2681
2898
  try {
2682
2899
  const result = await runScenarioWithRetries(
2683
2900
  { ...scenario, runs },
2684
- { projectRoot: opts.path, timeout, debug: opts.debug, model: opts.model }
2901
+ {
2902
+ projectRoot: opts.path,
2903
+ timeout,
2904
+ debug: opts.debug,
2905
+ model: opts.model
2906
+ }
2685
2907
  );
2686
2908
  results.push(result);
2687
2909
  if (result.passed) {
2688
- spinner.succeed(`${scenario.name} ${result.score}/${result.maxScore}`);
2910
+ spinner.succeed(
2911
+ `${scenario.name} ${result.score}/${result.maxScore}`
2912
+ );
2689
2913
  } else {
2690
- spinner.fail(`${scenario.name} ${result.score}/${result.maxScore}`);
2914
+ spinner.fail(
2915
+ `${scenario.name} ${result.score}/${result.maxScore}`
2916
+ );
2691
2917
  }
2692
2918
  } catch (error) {
2693
2919
  spinner.fail(`${scenario.name} ERROR`);
@@ -2698,7 +2924,11 @@ function createEvalCommand() {
2698
2924
  score: 0,
2699
2925
  maxScore: scenario.checks.reduce((s, c) => s + c.points, 0),
2700
2926
  passed: false,
2701
- checks: scenario.checks.map((c) => ({ label: c.label, passed: false, points: c.points }))
2927
+ checks: scenario.checks.map((c) => ({
2928
+ label: c.label,
2929
+ passed: false,
2930
+ points: c.points
2931
+ }))
2702
2932
  });
2703
2933
  }
2704
2934
  }
@@ -2706,13 +2936,19 @@ function createEvalCommand() {
2706
2936
  if (opts.json) {
2707
2937
  const overallScore = results.reduce((s, r) => s + r.score, 0);
2708
2938
  const overallMax = results.reduce((s, r) => s + r.maxScore, 0);
2709
- console.log(JSON.stringify({
2710
- results,
2711
- overallScore,
2712
- overallMax,
2713
- passed: overallScore >= overallMax * 0.8,
2714
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2715
- }, null, 2));
2939
+ console.log(
2940
+ JSON.stringify(
2941
+ {
2942
+ results,
2943
+ overallScore,
2944
+ overallMax,
2945
+ passed: overallScore >= overallMax * 0.8,
2946
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2947
+ },
2948
+ null,
2949
+ 2
2950
+ )
2951
+ );
2716
2952
  return;
2717
2953
  }
2718
2954
  renderEvalReport(results);
@@ -2724,7 +2960,9 @@ function renderEvalReport(results) {
2724
2960
  const icon = result.passed ? chalk2.green("\u2713") : chalk2.red("\u2717");
2725
2961
  const status = result.passed ? chalk2.green("PASS") : chalk2.red("FAIL");
2726
2962
  const score = `${result.score}/${result.maxScore}`;
2727
- console.log(` ${icon} ${chalk2.bold(result.scenario)} ${score} ${status}`);
2963
+ console.log(
2964
+ ` ${icon} ${chalk2.bold(result.scenario)} ${score} ${status}`
2965
+ );
2728
2966
  const failedChecks = result.checks.filter((c) => !c.passed);
2729
2967
  for (const check of failedChecks) {
2730
2968
  console.log(` ${chalk2.red("\u2717")} ${chalk2.dim(check.label)}`);
@@ -2741,7 +2979,9 @@ function renderEvalReport(results) {
2741
2979
  if (failed === 0) {
2742
2980
  log.success(`All ${passed} scenario(s) passed.`);
2743
2981
  } else {
2744
- log.warn(`${passed} passed, ${failed} failed out of ${results.length} scenario(s).`);
2982
+ log.warn(
2983
+ `${passed} passed, ${failed} failed out of ${results.length} scenario(s).`
2984
+ );
2745
2985
  }
2746
2986
  }
2747
2987
  async function saveEvalReport(results, projectRoot, suite, model) {
@@ -2765,7 +3005,9 @@ async function saveEvalReport(results, projectRoot, suite, model) {
2765
3005
  ];
2766
3006
  for (const result of results) {
2767
3007
  const status = result.passed ? "PASS" : "FAIL";
2768
- lines.push(`### ${result.scenario} \u2014 ${result.score}/${result.maxScore} ${status}`);
3008
+ lines.push(
3009
+ `### ${result.scenario} \u2014 ${result.score}/${result.maxScore} ${status}`
3010
+ );
2769
3011
  const failedChecks = result.checks.filter((c) => !c.passed);
2770
3012
  const passedChecks = result.checks.filter((c) => c.passed);
2771
3013
  for (const check of passedChecks) {
@@ -2783,15 +3025,17 @@ async function saveEvalReport(results, projectRoot, suite, model) {
2783
3025
  lines.push(`### Fix: ${result.scenario}`);
2784
3026
  const failedChecks = result.checks.filter((c) => !c.passed);
2785
3027
  for (const check of failedChecks) {
2786
- lines.push(`- ${check.label} \u2014 update CLAUDE.md instructions or add hooks to enforce this behavior`);
3028
+ lines.push(
3029
+ `- ${check.label} \u2014 update CLAUDE.md instructions or add hooks to enforce this behavior`
3030
+ );
2787
3031
  }
2788
3032
  lines.push("");
2789
3033
  }
2790
3034
  }
2791
- const evalDir = join11(projectRoot, ".claude", "eval");
3035
+ const evalDir = join12(projectRoot, ".claude", "eval");
2792
3036
  await mkdir3(evalDir, { recursive: true });
2793
3037
  const filename = `eval-${suite ?? "all"}-${timestamp}.md`;
2794
- await writeFile3(join11(evalDir, filename), lines.join("\n"));
3038
+ await writeFile3(join12(evalDir, filename), lines.join("\n"));
2795
3039
  log.success(`Report saved to .claude/eval/${filename}`);
2796
3040
  }
2797
3041
  async function checkClaudeCli() {
@@ -2808,7 +3052,7 @@ async function checkClaudeCli() {
2808
3052
 
2809
3053
  // src/commands/memory/index.ts
2810
3054
  import { readFileSync as readFileSync2 } from "fs";
2811
- import { join as join12 } from "path";
3055
+ import { join as join13 } from "path";
2812
3056
  import { Command as Command4 } from "commander";
2813
3057
  import { confirm as confirm2 } from "@inquirer/prompts";
2814
3058
  async function handleSyncErrors(fn) {
@@ -2821,7 +3065,7 @@ async function handleSyncErrors(fn) {
2821
3065
  }
2822
3066
  function isMemoryInstalled() {
2823
3067
  const cwd = process.cwd();
2824
- const hookPresent = hasMemoryHook(join12(cwd, ".claude", "settings.json")) || hasMemoryHook(join12(cwd, ".claude", "settings.local.json"));
3068
+ const hookPresent = hasMemoryHook(join13(cwd, ".claude", "settings.json")) || hasMemoryHook(join13(cwd, ".claude", "settings.local.json"));
2825
3069
  if (!hookPresent) return false;
2826
3070
  return isMemoryMcpRegistered(cwd);
2827
3071
  }
@@ -2833,43 +3077,66 @@ function hasMemoryHook(path) {
2833
3077
  const sessionStart = hooks.SessionStart;
2834
3078
  return sessionStart?.some((h) => {
2835
3079
  const inner = h.hooks;
2836
- return inner?.some((ih) => String(ih.command ?? "").includes("memory context"));
3080
+ return inner?.some(
3081
+ (ih) => String(ih.command ?? "").includes("memory context")
3082
+ );
2837
3083
  }) ?? false;
2838
3084
  } catch {
2839
3085
  return false;
2840
3086
  }
2841
3087
  }
2842
3088
  function createMemoryCommand() {
2843
- 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) => {
3089
+ const memory = new Command4("memory").description("Project-scoped memory with decay, sync, and a TUI dashboard").showHelpAfterError(
3090
+ "(see `claude-launchpad memory --help` for the available subcommands)"
3091
+ ).showSuggestionAfterError(true).option("--dashboard", "Open the memory dashboard").action(async (opts) => {
2844
3092
  if (opts.dashboard) {
2845
3093
  if (!isMemoryInstalled()) {
2846
- log.error("Knowledge base not set up yet. Run `claude-launchpad memory` first.");
3094
+ log.error(
3095
+ "Knowledge base not set up yet. Run `claude-launchpad memory` first."
3096
+ );
2847
3097
  return;
2848
3098
  }
2849
- const { requireMemoryDeps } = await import("./require-deps-ZISUZLYD.js");
3099
+ const { requireMemoryDeps } = await import("./require-deps-AEDO5YRM.js");
2850
3100
  await requireMemoryDeps();
2851
- const { startTui } = await import("./tui-2UJ2OIHB.js");
3101
+ const { startTui } = await import("./tui-3KITZCNS.js");
2852
3102
  await startTui();
2853
3103
  return;
2854
3104
  }
2855
3105
  if (!isMemoryInstalled()) {
2856
- const { detectExistingSetup } = await import("./install-Q23F37GK.js");
3106
+ if (!process.stdin.isTTY) {
3107
+ log.error(
3108
+ "Knowledge base not set up and no TTY to prompt. Run `claude-launchpad memory install -y`."
3109
+ );
3110
+ process.exitCode = 1;
3111
+ return;
3112
+ }
3113
+ const { detectExistingSetup } = await import("./install-543ZKMXX.js");
2857
3114
  const existing = detectExistingSetup(process.cwd());
2858
3115
  const mcpMissing = existing !== null && !isMemoryMcpRegistered(process.cwd());
2859
3116
  if (existing) {
2860
3117
  const location = existing === "local" ? ".claude/CLAUDE.md + settings.local.json" : "CLAUDE.md + settings.json";
2861
3118
  log.blank();
2862
- log.success(`Memory config found (${location}) but ${mcpMissing ? "MCP server not registered" : "database not set up"}.`);
3119
+ log.success(
3120
+ `Memory config found (${location}) but ${mcpMissing ? "MCP server not registered" : "database not set up"}.`
3121
+ );
2863
3122
  log.info("Run `claude-launchpad memory install` to complete setup.");
2864
3123
  log.blank();
2865
3124
  } else {
2866
3125
  log.blank();
2867
- log.step("Claude doesn't have a knowledge base for this project yet.");
3126
+ log.step(
3127
+ "Claude doesn't have a knowledge base for this project yet."
3128
+ );
2868
3129
  log.blank();
2869
3130
  log.info("After setup, Claude will:");
2870
- log.info(" - Remember decisions, gotchas, and learnings across sessions");
2871
- log.info(" - Automatically recall relevant context when you start a session");
2872
- log.info(" - Save important facts as you work, so nothing gets lost");
3131
+ log.info(
3132
+ " - Remember decisions, gotchas, and learnings across sessions"
3133
+ );
3134
+ log.info(
3135
+ " - Automatically recall relevant context when you start a session"
3136
+ );
3137
+ log.info(
3138
+ " - Save important facts as you work, so nothing gets lost"
3139
+ );
2873
3140
  log.blank();
2874
3141
  }
2875
3142
  const proceed = await confirm2({
@@ -2880,29 +3147,51 @@ function createMemoryCommand() {
2880
3147
  log.info("Skipped.");
2881
3148
  return;
2882
3149
  }
2883
- const { runInstall } = await import("./install-Q23F37GK.js");
3150
+ const { runInstall } = await import("./install-543ZKMXX.js");
2884
3151
  await runInstall({});
2885
3152
  } else {
2886
- const { requireMemoryDeps } = await import("./require-deps-ZISUZLYD.js");
3153
+ const { requireMemoryDeps } = await import("./require-deps-AEDO5YRM.js");
2887
3154
  await requireMemoryDeps();
2888
- const { runStats } = await import("./stats-SV53G6PQ.js");
3155
+ const { runStats } = await import("./stats-63L7C24W.js");
2889
3156
  await runStats({});
2890
3157
  }
2891
3158
  });
2892
3159
  memory.addCommand(
2893
- new Command4("install").description("Install (or re-install) the knowledge base for this project").option("--db-path <path>", "Override the default data directory").option("-y, --yes", "Non-interactive: accept defaults (shared placement)").action(async (opts) => {
3160
+ new Command4("install").description(
3161
+ "Install (or re-install) the knowledge base for this project"
3162
+ ).option("--db-path <path>", "Override the default data directory").option(
3163
+ "-y, --yes",
3164
+ "Non-interactive: accept defaults (shared placement)"
3165
+ ).action(async (opts) => {
2894
3166
  try {
2895
- const { runInstall } = await import("./install-Q23F37GK.js");
2896
- await runInstall({ ...opts.dbPath ? { dbPath: opts.dbPath } : {}, yes: opts.yes === true });
3167
+ const { runInstall } = await import("./install-543ZKMXX.js");
3168
+ await runInstall({
3169
+ ...opts.dbPath ? { dbPath: opts.dbPath } : {},
3170
+ yes: opts.yes === true
3171
+ });
2897
3172
  } catch (err) {
2898
3173
  log.error(err instanceof Error ? err.message : String(err));
2899
3174
  process.exitCode = 1;
2900
3175
  }
2901
3176
  })
2902
3177
  );
3178
+ memory.addCommand(
3179
+ new Command4("stats").description("Show memory counts, health, and injection stats").option("--json", "JSON output").action(async (opts) => {
3180
+ const { requireMemoryDeps } = await import("./require-deps-AEDO5YRM.js");
3181
+ await requireMemoryDeps();
3182
+ const { runStats } = await import("./stats-63L7C24W.js");
3183
+ await runStats(opts);
3184
+ })
3185
+ );
3186
+ memory.addCommand(
3187
+ new Command4("doctor").description("Diagnose the memory setup (DB, hooks, MCP registration)").option("--json", "JSON output").action(async (opts) => {
3188
+ const { runMemoryDoctor } = await import("./doctor-EKGJ2YAO.js");
3189
+ await runMemoryDoctor(opts);
3190
+ })
3191
+ );
2903
3192
  memory.addCommand(
2904
3193
  new Command4("context").description("Load session context (hook handler)").option("--json", "JSON output").action(async (opts) => {
2905
- const { runContext } = await import("./context-JALSYDYO.js");
3194
+ const { runContext } = await import("./context-3GHG7KW6.js");
2906
3195
  await runContext(opts);
2907
3196
  }).helpCommand(false),
2908
3197
  { hidden: true }
@@ -2917,29 +3206,34 @@ function createMemoryCommand() {
2917
3206
  memory.addCommand(
2918
3207
  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) => {
2919
3208
  await handleSyncErrors(async () => {
2920
- const { runPush } = await import("./push-QSBK7BQU.js");
3209
+ const { runPush } = await import("./push-JXCWX4NY.js");
2921
3210
  await runPush(opts);
2922
3211
  });
2923
3212
  })
2924
3213
  );
2925
3214
  memory.addCommand(
2926
- 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) => {
3215
+ new Command4("pull").description("Pull current project's memories from GitHub Gist").option("--all", "Pull all projects").option(
3216
+ "-y, --yes",
3217
+ "Non-interactive (accepted for symmetry with push; pull never prompts)"
3218
+ ).action(async (opts) => {
2927
3219
  await handleSyncErrors(async () => {
2928
- const { runPull } = await import("./pull-FYKNNLYC.js");
3220
+ const { runPull } = await import("./pull-ULSZEVCR.js");
2929
3221
  await runPull(opts);
2930
3222
  });
2931
3223
  })
2932
3224
  );
2933
- const sync = new Command4("sync").description("Sync memories with the gist (pull + push); subcommands manage sync state").option("--all", "Sync all projects").option("-y, --yes", "Skip confirmation prompt").action(async (opts) => {
3225
+ const sync = new Command4("sync").description(
3226
+ "Sync memories with the gist (pull + push); subcommands manage sync state"
3227
+ ).option("--all", "Sync all projects").option("-y, --yes", "Skip confirmation prompt").action(async (opts) => {
2934
3228
  await handleSyncErrors(async () => {
2935
- const { runSync } = await import("./sync-2LDM44U4.js");
3229
+ const { runSync } = await import("./sync-QTJ6BXQ4.js");
2936
3230
  await runSync(opts);
2937
3231
  });
2938
3232
  });
2939
3233
  sync.addCommand(
2940
3234
  new Command4("status").description("Show local vs remote memory counts per project").action(async () => {
2941
3235
  await handleSyncErrors(async () => {
2942
- const { runSyncStatus } = await import("./sync-status-V6WTI32X.js");
3236
+ const { runSyncStatus } = await import("./sync-status-EAX527SC.js");
2943
3237
  await runSyncStatus();
2944
3238
  });
2945
3239
  })
@@ -2947,7 +3241,7 @@ function createMemoryCommand() {
2947
3241
  sync.addCommand(
2948
3242
  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) => {
2949
3243
  await handleSyncErrors(async () => {
2950
- const { runSyncClean } = await import("./sync-clean-HQ523N7V.js");
3244
+ const { runSyncClean } = await import("./sync-clean-OJEK4XH4.js");
2951
3245
  await runSyncClean(project, opts);
2952
3246
  });
2953
3247
  })
@@ -2957,8 +3251,8 @@ function createMemoryCommand() {
2957
3251
  }
2958
3252
 
2959
3253
  // src/cli.ts
2960
- var program = new Command5().name("claude-launchpad").description("Score your Claude Code config, fix the gaps, prove Claude follows your rules.").version("1.12.0", "-v, --version").action(async () => {
2961
- const hasConfig = await fileExists(join13(process.cwd(), "CLAUDE.md")) || await fileExists(join13(process.cwd(), ".claude", "settings.json"));
3254
+ 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 () => {
3255
+ const hasConfig = await fileExists(join14(process.cwd(), "CLAUDE.md")) || await fileExists(join14(process.cwd(), ".claude", "settings.json"));
2962
3256
  if (hasConfig) {
2963
3257
  await program.commands.find((c) => c.name() === "doctor")?.parseAsync([], { from: "user" });
2964
3258
  } else {