claude-launchpad 0.2.2 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -113,30 +113,53 @@ You see Claude working in real-time — same experience as running `claude` your
113
113
 
114
114
  ### `eval` — Prove your config works
115
115
 
116
- Runs Claude headless against 9 reproducible scenarios using the [Agent SDK](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk) and **scores how well your config actually drives correct behavior**.
116
+ Runs Claude headless against 11 reproducible scenarios using the [Agent SDK](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk) and **scores how well your config actually drives correct behavior**.
117
117
 
118
118
  ```bash
119
119
  claude-launchpad eval --suite common
120
120
  ```
121
121
 
122
122
  ```
123
- ✓ security/sql-injection 10/10 PASS
124
- ✓ security/env-protection 10/10 PASS
125
- ✓ security/secret-exposure 10/10 PASS
126
- ✓ security/input-validation 10/10 PASS
127
- ✓ conventions/error-handling 10/10 PASS
128
- ✓ conventions/immutability 10/10 PASS
129
- ✓ conventions/no-hardcoded-values 10/10 PASS
130
- ✓ conventions/file-size 10/10 PASS
131
- workflow/git-conventions 7/10 WARN
132
-
133
- Config Eval Score ━━━━━━━━━━━━━━━━━━── 89%
123
+ ✓ security/sql-injection 10/10 PASS
124
+ ✓ security/env-protection 10/10 PASS
125
+ ✓ security/secret-exposure 10/10 PASS
126
+ ✓ security/input-validation 10/10 PASS
127
+ ✓ conventions/error-handling 10/10 PASS
128
+ ✓ conventions/immutability 10/10 PASS
129
+ ✓ conventions/no-hardcoded-values 10/10 PASS
130
+ ✓ conventions/naming-conventions 10/10 PASS
131
+ conventions/file-size 10/10 PASS
132
+ ✓ workflow/git-conventions 10/10 PASS
133
+ workflow/session-continuity 7/10 WARN
134
+
135
+ Config Eval Score ━━━━━━━━━━━━━━━━━━━─ 95%
134
136
  ```
135
137
 
136
- Each scenario creates an isolated sandbox, runs Claude with explicit tool permissions, and verifies the output with grep/file assertions. [Write your own scenarios](scenarios/CONTRIBUTING.md) in YAML.
138
+ Each scenario is a YAML file. [Write your own](scenarios/CONTRIBUTING.md).
137
139
 
138
140
  This is the part nobody else has built. Template repos scaffold. Audit tools diagnose. **Nobody tests whether your config actually makes Claude better.** Until now.
139
141
 
142
+ ## How It Works Under the Hood
143
+
144
+ ### doctor
145
+ Reads your `CLAUDE.md`, `.claude/settings.json`, `.claude/rules/`, and `.claudeignore`. Runs 7 analyzers that check instruction count, section completeness, hook configuration, rule validity, permission safety, and MCP server configs. Pure static analysis — no API calls, no network, no cost.
146
+
147
+ ### init
148
+ Scans the project root for manifest files (`package.json`, `go.mod`, `pyproject.toml`, `Gemfile`, `Cargo.toml`, `composer.json`, etc.). Detects language, framework, package manager, and available scripts. Generates config files with stack-appropriate hooks (prettier for TypeScript, gofmt for Go, ruff for Python, etc.). Merges with existing `settings.json` if one exists.
149
+
150
+ ### enhance
151
+ Spawns `claude "prompt"` as an interactive child process with `stdio: "inherit"` — you see Claude's full UI. The prompt instructs Claude to read the codebase and fill in CLAUDE.md sections. No data passes through the launchpad — it just launches Claude with a pre-loaded task.
152
+
153
+ ### eval
154
+ 1. Creates a temp directory (`/tmp/claude-eval-<uuid>/`)
155
+ 2. Writes seed files from the scenario YAML (e.g., a `src/db.ts` with a TODO)
156
+ 3. Writes a `CLAUDE.md` with the scenario's instructions
157
+ 4. Initializes a git repo (Claude Code expects one)
158
+ 5. Runs Claude via the [Agent SDK](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk) with `allowedTools: ["Bash", "Read", "Write", "Edit", "Glob", "Grep"]` and `permissionMode: "dontAsk"` — or falls back to `claude -p` if the SDK isn't installed
159
+ 6. After Claude finishes, runs grep/file assertions against the modified files
160
+ 7. Scores: each check has points, total determines pass/fail
161
+ 8. Cleans up the temp directory (or preserves it with `--debug`)
162
+
140
163
  ## Use in CI
141
164
 
142
165
  Add this workflow to block PRs that degrade Claude Code config quality:
@@ -159,7 +182,9 @@ jobs:
159
182
 
160
183
  Exit code is 1 when score is below the threshold, 0 when it passes.
161
184
 
162
- ## Install as a Plugin
185
+ ## Plugin (pending marketplace review)
186
+
187
+ The plugin has been submitted to the Claude Code marketplace. Once approved:
163
188
 
164
189
  ```bash
165
190
  claude plugin install claude-launchpad
@@ -185,7 +210,7 @@ Claude Launchpad gives you a number. Fix the issues, re-run, watch the number go
185
210
  - **Enhance uses Claude.** Spawns an interactive session to understand your codebase — costs tokens but produces a CLAUDE.md that actually knows your project.
186
211
  - **Eval uses the Agent SDK.** Runs Claude headless in sandboxes with explicit tool permissions — proof that your config works.
187
212
  - **Works with any stack.** Auto-detects your project. No fixed menu of supported frameworks.
188
- - **50 tests.** The tool that tests configs is itself well-tested.
213
+ - **57 tests.** The tool that tests configs is itself well-tested.
189
214
  - **You never clone this repo.** It's a tool you run with `npx`, not a template you fork.
190
215
 
191
216
  ## License
package/dist/cli.js CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { Command as Command5 } from "commander";
5
- import { access as access7 } from "fs/promises";
6
- import { join as join9 } from "path";
5
+ import { access as access8 } from "fs/promises";
6
+ import { join as join10 } from "path";
7
7
 
8
8
  // src/commands/init/index.ts
9
9
  import { Command } from "commander";
@@ -324,9 +324,9 @@ async function fileExists(path) {
324
324
  }
325
325
  }
326
326
  async function globExists(dir, pattern) {
327
- const { readdir: readdir4 } = await import("fs/promises");
327
+ const { readdir: readdir5 } = await import("fs/promises");
328
328
  try {
329
- const entries = await readdir4(dir);
329
+ const entries = await readdir5(dir);
330
330
  return entries.some((e) => e.endsWith(pattern.replace("*", "")));
331
331
  } catch {
332
332
  return false;
@@ -425,63 +425,24 @@ function generateSettings(detected) {
425
425
  if (postToolUse.length > 0) hooks.PostToolUse = postToolUse;
426
426
  return Object.keys(hooks).length > 0 ? { hooks } : {};
427
427
  }
428
+ var SAFE_FORMATTERS = {
429
+ TypeScript: { extensions: ["ts", "tsx"], command: "npx prettier --write" },
430
+ JavaScript: { extensions: ["js", "jsx"], command: "npx prettier --write" },
431
+ Python: { extensions: ["py"], command: "ruff format" },
432
+ Go: { extensions: ["go"], command: "gofmt -w" },
433
+ Rust: { extensions: ["rs"], command: "rustfmt" },
434
+ Ruby: { extensions: ["rb"], command: "rubocop -A" },
435
+ Dart: { extensions: ["dart"], command: "dart format" },
436
+ PHP: { extensions: ["php"], command: "vendor/bin/pint" },
437
+ Kotlin: { extensions: ["kt", "kts"], command: "ktlint -F" },
438
+ Java: { extensions: ["java"], command: "google-java-format -i" },
439
+ Swift: { extensions: ["swift"], command: "swift-format format -i" },
440
+ Elixir: { extensions: ["ex", "exs"], command: "mix format" },
441
+ "C#": { extensions: ["cs"], command: "dotnet format" }
442
+ };
428
443
  function buildFormatHook(detected) {
429
444
  if (!detected.language) return null;
430
- const formatters = {
431
- TypeScript: {
432
- extensions: ["ts", "tsx"],
433
- command: detected.formatCommand ?? "npx prettier --write"
434
- },
435
- JavaScript: {
436
- extensions: ["js", "jsx"],
437
- command: detected.formatCommand ?? "npx prettier --write"
438
- },
439
- Python: {
440
- extensions: ["py"],
441
- command: detected.formatCommand ?? "ruff format"
442
- },
443
- Go: {
444
- extensions: ["go"],
445
- command: "gofmt -w"
446
- },
447
- Rust: {
448
- extensions: ["rs"],
449
- command: "rustfmt"
450
- },
451
- Ruby: {
452
- extensions: ["rb"],
453
- command: "rubocop -A"
454
- },
455
- Dart: {
456
- extensions: ["dart"],
457
- command: "dart format"
458
- },
459
- PHP: {
460
- extensions: ["php"],
461
- command: detected.formatCommand ?? "vendor/bin/pint"
462
- },
463
- Kotlin: {
464
- extensions: ["kt", "kts"],
465
- command: "ktlint -F"
466
- },
467
- Java: {
468
- extensions: ["java"],
469
- command: "google-java-format -i"
470
- },
471
- Swift: {
472
- extensions: ["swift"],
473
- command: "swift-format format -i"
474
- },
475
- Elixir: {
476
- extensions: ["ex", "exs"],
477
- command: "mix format"
478
- },
479
- "C#": {
480
- extensions: ["cs"],
481
- command: "dotnet format"
482
- }
483
- };
484
- const config = formatters[detected.language];
445
+ const config = SAFE_FORMATTERS[detected.language];
485
446
  if (!config) return null;
486
447
  const extChecks = config.extensions.map((ext) => `[ "$ext" = "${ext}" ]`).join(" || ");
487
448
  return {
@@ -1028,10 +989,20 @@ async function analyzeHooks(config) {
1028
989
  }
1029
990
 
1030
991
  // src/commands/doctor/analyzers/rules.ts
1031
- import { readFile as readFile4 } from "fs/promises";
1032
- import { basename as basename2 } from "path";
992
+ import { readFile as readFile4, access as access3 } from "fs/promises";
993
+ import { basename as basename2, join as join4, dirname } from "path";
1033
994
  async function analyzeRules(config) {
1034
995
  const issues = [];
996
+ const projectRoot = config.claudeMdPath ? dirname(config.claudeMdPath) : process.cwd();
997
+ const hasClaudeignore = await fileExists3(join4(projectRoot, ".claudeignore"));
998
+ if (!hasClaudeignore) {
999
+ issues.push({
1000
+ analyzer: "Rules",
1001
+ severity: "low",
1002
+ message: "No .claudeignore found \u2014 Claude may read noise files (node_modules, dist, lockfiles)",
1003
+ fix: "Run `claude-launchpad init` or `doctor --fix` to generate one"
1004
+ });
1005
+ }
1035
1006
  if (config.rules.length === 0) {
1036
1007
  issues.push({
1037
1008
  analyzer: "Rules",
@@ -1070,6 +1041,14 @@ async function analyzeRules(config) {
1070
1041
  const score = Math.max(0, 100 - issues.length * 10);
1071
1042
  return { name: "Rules", issues, score };
1072
1043
  }
1044
+ async function fileExists3(path) {
1045
+ try {
1046
+ await access3(path);
1047
+ return true;
1048
+ } catch {
1049
+ return false;
1050
+ }
1051
+ }
1073
1052
 
1074
1053
  // src/commands/doctor/analyzers/permissions.ts
1075
1054
  async function analyzePermissions(config) {
@@ -1116,7 +1095,7 @@ async function analyzePermissions(config) {
1116
1095
  }
1117
1096
 
1118
1097
  // src/commands/doctor/analyzers/mcp.ts
1119
- import { access as access3 } from "fs/promises";
1098
+ import { access as access4 } from "fs/promises";
1120
1099
  async function analyzeMcp(config) {
1121
1100
  const issues = [];
1122
1101
  const servers = config.mcpServers;
@@ -1124,8 +1103,7 @@ async function analyzeMcp(config) {
1124
1103
  issues.push({
1125
1104
  analyzer: "MCP",
1126
1105
  severity: "info",
1127
- message: "No MCP servers configured \u2014 Claude can't connect to external tools",
1128
- fix: "Add MCP servers in .claude/settings.json for GitHub, database, docs, etc."
1106
+ message: "No MCP servers configured. Run `claude-launchpad enhance` to get stack-specific recommendations."
1129
1107
  });
1130
1108
  return { name: "MCP Servers", issues, score: 50 };
1131
1109
  }
@@ -1150,13 +1128,13 @@ async function analyzeMcp(config) {
1150
1128
  const executable = server.command.split(" ")[0];
1151
1129
  if (executable.startsWith("/") || executable.startsWith("./")) {
1152
1130
  try {
1153
- await access3(executable);
1131
+ await access4(executable);
1154
1132
  } catch {
1155
1133
  issues.push({
1156
1134
  analyzer: "MCP",
1157
1135
  severity: "medium",
1158
1136
  message: `MCP server "${server.name}" command not found: ${executable}`,
1159
- fix: `Verify the path exists or install the required package`
1137
+ fix: "Verify the path exists or install the required package"
1160
1138
  });
1161
1139
  }
1162
1140
  }
@@ -1168,7 +1146,7 @@ async function analyzeMcp(config) {
1168
1146
 
1169
1147
  // src/commands/doctor/analyzers/quality.ts
1170
1148
  var ESSENTIAL_SECTIONS = [
1171
- { pattern: /^##\s+Stack/m, name: "Stack", why: "Claude performs worse without knowing the tech stack" },
1149
+ { pattern: /^##\s+(Tech )?Stack/m, name: "Stack", why: "Claude performs worse without knowing the tech stack" },
1172
1150
  { pattern: /^##\s+Commands/m, name: "Commands", why: "Claude guesses wrong without explicit dev/build/test commands" },
1173
1151
  { pattern: /^##\s+Session Start/m, name: "Session Start", why: "Without this, Claude won't read TASKS.md or maintain continuity" },
1174
1152
  { pattern: /^##\s+Off.?Limits/m, name: "Off-Limits", why: "Without guardrails, Claude has no boundaries beyond defaults" },
@@ -1249,8 +1227,8 @@ async function analyzeQuality(config) {
1249
1227
  }
1250
1228
 
1251
1229
  // src/commands/doctor/fixer.ts
1252
- import { readFile as readFile5, writeFile as writeFile2, mkdir as mkdir2, access as access4 } from "fs/promises";
1253
- import { join as join4 } from "path";
1230
+ import { readFile as readFile5, writeFile as writeFile2, mkdir as mkdir2, access as access5 } from "fs/promises";
1231
+ import { join as join5 } from "path";
1254
1232
  async function applyFixes(issues, projectRoot) {
1255
1233
  const detected = await detectProject(projectRoot);
1256
1234
  let fixed = 0;
@@ -1299,6 +1277,9 @@ async function tryFix(issue, root, detected) {
1299
1277
  if (issue.analyzer === "Quality" && issue.message.includes("Session Start")) {
1300
1278
  return addClaudeMdSection(root, "## Session Start", "- ALWAYS read @TASKS.md first \u2014 it tracks progress across sessions\n- Update TASKS.md as you complete work");
1301
1279
  }
1280
+ if (issue.analyzer === "Rules" && issue.message.includes("No .claudeignore")) {
1281
+ return createClaudeignore(root, detected);
1282
+ }
1302
1283
  if (issue.analyzer === "Rules" && issue.message.includes("No .claude/rules/")) {
1303
1284
  return createStarterRules(root);
1304
1285
  }
@@ -1331,13 +1312,13 @@ async function addEnvProtectionHook(root) {
1331
1312
  async function addAutoFormatHook(root, detected) {
1332
1313
  if (!detected.language) return false;
1333
1314
  const formatters = {
1334
- TypeScript: { extensions: ["ts", "tsx"], command: detected.formatCommand ?? "npx prettier --write" },
1335
- JavaScript: { extensions: ["js", "jsx"], command: detected.formatCommand ?? "npx prettier --write" },
1336
- Python: { extensions: ["py"], command: detected.formatCommand ?? "ruff format" },
1315
+ TypeScript: { extensions: ["ts", "tsx"], command: "npx prettier --write" },
1316
+ JavaScript: { extensions: ["js", "jsx"], command: "npx prettier --write" },
1317
+ Python: { extensions: ["py"], command: "ruff format" },
1337
1318
  Go: { extensions: ["go"], command: "gofmt -w" },
1338
1319
  Rust: { extensions: ["rs"], command: "rustfmt" },
1339
1320
  Ruby: { extensions: ["rb"], command: "rubocop -A" },
1340
- PHP: { extensions: ["php"], command: detected.formatCommand ?? "vendor/bin/pint" }
1321
+ PHP: { extensions: ["php"], command: "vendor/bin/pint" }
1341
1322
  };
1342
1323
  const config = formatters[detected.language];
1343
1324
  if (!config) return false;
@@ -1384,7 +1365,7 @@ async function addForcePushProtection(root) {
1384
1365
  return true;
1385
1366
  }
1386
1367
  async function addClaudeMdSection(root, heading, content) {
1387
- const claudeMdPath = join4(root, "CLAUDE.md");
1368
+ const claudeMdPath = join5(root, "CLAUDE.md");
1388
1369
  let existing;
1389
1370
  try {
1390
1371
  existing = await readFile5(claudeMdPath, "utf-8");
@@ -1404,16 +1385,28 @@ ${content}
1404
1385
  log.success(`Added "${heading}" section to CLAUDE.md`);
1405
1386
  return true;
1406
1387
  }
1388
+ async function createClaudeignore(root, detected) {
1389
+ const ignorePath = join5(root, ".claudeignore");
1390
+ try {
1391
+ await access5(ignorePath);
1392
+ return false;
1393
+ } catch {
1394
+ }
1395
+ const content = generateClaudeignore(detected);
1396
+ await writeFile2(ignorePath, content);
1397
+ log.success("Generated .claudeignore with language-specific ignore patterns");
1398
+ return true;
1399
+ }
1407
1400
  async function createStarterRules(root) {
1408
- const rulesDir = join4(root, ".claude", "rules");
1401
+ const rulesDir = join5(root, ".claude", "rules");
1409
1402
  try {
1410
- await access4(rulesDir);
1403
+ await access5(rulesDir);
1411
1404
  return false;
1412
1405
  } catch {
1413
1406
  }
1414
1407
  await mkdir2(rulesDir, { recursive: true });
1415
1408
  await writeFile2(
1416
- join4(rulesDir, "conventions.md"),
1409
+ join5(rulesDir, "conventions.md"),
1417
1410
  `# Project Conventions
1418
1411
 
1419
1412
  - Use conventional commits (feat:, fix:, docs:, refactor:, test:, chore:)
@@ -1426,7 +1419,7 @@ async function createStarterRules(root) {
1426
1419
  return true;
1427
1420
  }
1428
1421
  async function readSettingsJson(root) {
1429
- const path = join4(root, ".claude", "settings.json");
1422
+ const path = join5(root, ".claude", "settings.json");
1430
1423
  try {
1431
1424
  const content = await readFile5(path, "utf-8");
1432
1425
  return JSON.parse(content);
@@ -1435,47 +1428,60 @@ async function readSettingsJson(root) {
1435
1428
  }
1436
1429
  }
1437
1430
  async function writeSettingsJson(root, settings) {
1438
- const dir = join4(root, ".claude");
1431
+ const dir = join5(root, ".claude");
1439
1432
  await mkdir2(dir, { recursive: true });
1440
- await writeFile2(join4(dir, "settings.json"), JSON.stringify(settings, null, 2) + "\n");
1433
+ await writeFile2(join5(dir, "settings.json"), JSON.stringify(settings, null, 2) + "\n");
1441
1434
  }
1442
1435
 
1443
1436
  // src/commands/doctor/watcher.ts
1444
- import { watch } from "fs";
1445
- import { join as join5 } from "path";
1437
+ import { readdir as readdir2, stat } from "fs/promises";
1438
+ import { join as join6 } from "path";
1446
1439
  async function watchConfig(projectRoot) {
1447
- const claudeDir = join5(projectRoot, ".claude");
1448
- const claudeMd = join5(projectRoot, "CLAUDE.md");
1449
- const claudeignore = join5(projectRoot, ".claudeignore");
1450
1440
  await runAndDisplay(projectRoot);
1451
1441
  log.blank();
1452
1442
  log.info("Watching for changes... (Ctrl+C to stop)");
1453
1443
  log.blank();
1454
- let debounce = null;
1455
- const onChange = () => {
1456
- if (debounce) clearTimeout(debounce);
1457
- debounce = setTimeout(async () => {
1444
+ let lastSnapshot = await getFileSnapshot(projectRoot);
1445
+ setInterval(async () => {
1446
+ const currentSnapshot = await getFileSnapshot(projectRoot);
1447
+ if (currentSnapshot !== lastSnapshot) {
1448
+ lastSnapshot = currentSnapshot;
1458
1449
  console.clear();
1459
1450
  await runAndDisplay(projectRoot);
1460
1451
  log.blank();
1461
1452
  log.info("Watching for changes... (Ctrl+C to stop)");
1462
1453
  log.blank();
1463
- }, 300);
1464
- };
1465
- try {
1466
- watch(claudeDir, { recursive: true }, onChange);
1467
- } catch {
1468
- }
1454
+ }
1455
+ }, 1e3);
1456
+ await new Promise(() => {
1457
+ });
1458
+ }
1459
+ async function getFileSnapshot(projectRoot) {
1460
+ const files = [
1461
+ join6(projectRoot, "CLAUDE.md"),
1462
+ join6(projectRoot, ".claudeignore")
1463
+ ];
1464
+ const claudeDir = join6(projectRoot, ".claude");
1469
1465
  try {
1470
- watch(claudeMd, onChange);
1466
+ const entries = await readdir2(claudeDir, { withFileTypes: true, recursive: true });
1467
+ for (const entry of entries) {
1468
+ if (entry.isFile()) {
1469
+ const parentPath = entry.parentPath ?? claudeDir;
1470
+ files.push(join6(parentPath, entry.name));
1471
+ }
1472
+ }
1471
1473
  } catch {
1472
1474
  }
1473
- try {
1474
- watch(claudeignore, onChange);
1475
- } catch {
1475
+ const mtimes = [];
1476
+ for (const file of files) {
1477
+ try {
1478
+ const s = await stat(file);
1479
+ mtimes.push(`${file}:${s.mtimeMs}`);
1480
+ } catch {
1481
+ mtimes.push(`${file}:missing`);
1482
+ }
1476
1483
  }
1477
- await new Promise(() => {
1478
- });
1484
+ return mtimes.join("|");
1479
1485
  }
1480
1486
  async function runAndDisplay(projectRoot) {
1481
1487
  console.log("\x1B[36m\x1B[1m Claude Launchpad\x1B[0m");
@@ -1613,8 +1619,8 @@ import ora from "ora";
1613
1619
  import chalk2 from "chalk";
1614
1620
 
1615
1621
  // src/commands/eval/loader.ts
1616
- import { readFile as readFile6, readdir as readdir2, access as access5 } from "fs/promises";
1617
- import { join as join6, resolve as resolve2, dirname } from "path";
1622
+ import { readFile as readFile6, readdir as readdir3, access as access6 } from "fs/promises";
1623
+ import { join as join7, resolve as resolve2, dirname as dirname2 } from "path";
1618
1624
  import { fileURLToPath } from "url";
1619
1625
  import { parse as parseYaml } from "yaml";
1620
1626
 
@@ -1717,7 +1723,7 @@ var ScenarioError = class extends Error {
1717
1723
 
1718
1724
  // src/commands/eval/loader.ts
1719
1725
  async function findScenariosDir() {
1720
- const thisDir = dirname(fileURLToPath(import.meta.url));
1726
+ const thisDir = dirname2(fileURLToPath(import.meta.url));
1721
1727
  const devPath = resolve2(thisDir, "../../../scenarios");
1722
1728
  if (await dirExists(devPath)) return devPath;
1723
1729
  const bundledPath = resolve2(thisDir, "../scenarios");
@@ -1728,7 +1734,7 @@ async function findScenariosDir() {
1728
1734
  }
1729
1735
  async function dirExists(path) {
1730
1736
  try {
1731
- await access5(path);
1737
+ await access6(path);
1732
1738
  return true;
1733
1739
  } catch {
1734
1740
  return false;
@@ -1737,7 +1743,7 @@ async function dirExists(path) {
1737
1743
  async function loadScenarios(options) {
1738
1744
  const { suite, customPath } = options;
1739
1745
  const scenarioDir = customPath ? resolve2(customPath) : await findScenariosDir();
1740
- const dirs = suite ? [join6(scenarioDir, suite)] : await getSubdirectories(scenarioDir);
1746
+ const dirs = suite ? [join7(scenarioDir, suite)] : await getSubdirectories(scenarioDir);
1741
1747
  const allDirs = [scenarioDir, ...dirs];
1742
1748
  const scenarios = [];
1743
1749
  for (const dir of allDirs) {
@@ -1758,31 +1764,31 @@ async function loadScenarios(options) {
1758
1764
  }
1759
1765
  async function getSubdirectories(dir) {
1760
1766
  try {
1761
- const entries = await readdir2(dir, { withFileTypes: true });
1762
- return entries.filter((e) => e.isDirectory()).map((e) => join6(dir, e.name));
1767
+ const entries = await readdir3(dir, { withFileTypes: true });
1768
+ return entries.filter((e) => e.isDirectory()).map((e) => join7(dir, e.name));
1763
1769
  } catch {
1764
1770
  return [];
1765
1771
  }
1766
1772
  }
1767
1773
  async function listYamlFiles(dir) {
1768
1774
  try {
1769
- const entries = await readdir2(dir, { withFileTypes: true });
1770
- return entries.filter((e) => e.isFile() && (e.name.endsWith(".yaml") || e.name.endsWith(".yml"))).map((e) => join6(dir, e.name));
1775
+ const entries = await readdir3(dir, { withFileTypes: true });
1776
+ return entries.filter((e) => e.isFile() && (e.name.endsWith(".yaml") || e.name.endsWith(".yml"))).map((e) => join7(dir, e.name));
1771
1777
  } catch {
1772
1778
  return [];
1773
1779
  }
1774
1780
  }
1775
1781
 
1776
1782
  // src/commands/eval/runner.ts
1777
- import { mkdir as mkdir3, writeFile as writeFile3, readFile as readFile7, readdir as readdir3, rm } from "fs/promises";
1778
- import { join as join7, dirname as dirname2 } from "path";
1783
+ import { mkdir as mkdir3, writeFile as writeFile3, readFile as readFile7, readdir as readdir4, rm } from "fs/promises";
1784
+ import { join as join8, dirname as dirname3 } from "path";
1779
1785
  import { tmpdir } from "os";
1780
1786
  import { randomUUID } from "crypto";
1781
1787
  import { execFile } from "child_process";
1782
1788
  import { promisify } from "util";
1783
1789
  var exec = promisify(execFile);
1784
1790
  async function runScenario(scenario, options) {
1785
- const sandboxDir = join7(tmpdir(), `claude-eval-${randomUUID()}`);
1791
+ const sandboxDir = join8(tmpdir(), `claude-eval-${randomUUID()}`);
1786
1792
  try {
1787
1793
  await setupSandbox(sandboxDir, scenario);
1788
1794
  await runClaudeInSandbox(sandboxDir, scenario.prompt, options.timeout);
@@ -1808,13 +1814,13 @@ async function runScenarioWithRetries(scenario, options) {
1808
1814
  async function setupSandbox(sandboxDir, scenario) {
1809
1815
  await mkdir3(sandboxDir, { recursive: true });
1810
1816
  for (const file of scenario.setup.files) {
1811
- const filePath = join7(sandboxDir, file.path);
1812
- await mkdir3(dirname2(filePath), { recursive: true });
1817
+ const filePath = join8(sandboxDir, file.path);
1818
+ await mkdir3(dirname3(filePath), { recursive: true });
1813
1819
  await writeFile3(filePath, file.content);
1814
1820
  }
1815
1821
  if (scenario.setup.instructions) {
1816
1822
  await writeFile3(
1817
- join7(sandboxDir, "CLAUDE.md"),
1823
+ join8(sandboxDir, "CLAUDE.md"),
1818
1824
  `# Eval Scenario
1819
1825
 
1820
1826
  ${scenario.setup.instructions}
@@ -1927,7 +1933,7 @@ async function evaluateSingleCheck(check, sandboxDir) {
1927
1933
  async function checkGrep(check, sandboxDir) {
1928
1934
  if (!check.pattern) return false;
1929
1935
  try {
1930
- const content = await readFile7(join7(sandboxDir, check.target), "utf-8");
1936
+ const content = await readFile7(join8(sandboxDir, check.target), "utf-8");
1931
1937
  let found;
1932
1938
  try {
1933
1939
  found = new RegExp(check.pattern).test(content);
@@ -1941,7 +1947,7 @@ async function checkGrep(check, sandboxDir) {
1941
1947
  }
1942
1948
  async function checkFileExists(check, sandboxDir) {
1943
1949
  try {
1944
- await readFile7(join7(sandboxDir, check.target));
1950
+ await readFile7(join8(sandboxDir, check.target));
1945
1951
  return check.expect === "present";
1946
1952
  } catch {
1947
1953
  return check.expect === "absent";
@@ -1949,7 +1955,7 @@ async function checkFileExists(check, sandboxDir) {
1949
1955
  }
1950
1956
  async function checkFileAbsent(check, sandboxDir) {
1951
1957
  try {
1952
- await readFile7(join7(sandboxDir, check.target));
1958
+ await readFile7(join8(sandboxDir, check.target));
1953
1959
  return check.expect === "absent";
1954
1960
  } catch {
1955
1961
  return check.expect === "present";
@@ -1958,7 +1964,7 @@ async function checkFileAbsent(check, sandboxDir) {
1958
1964
  async function checkMaxLines(check, sandboxDir) {
1959
1965
  const maxLines = parseInt(check.pattern ?? "800", 10);
1960
1966
  try {
1961
- const files = await listAllFiles(join7(sandboxDir, check.target));
1967
+ const files = await listAllFiles(join8(sandboxDir, check.target));
1962
1968
  for (const file of files) {
1963
1969
  const content = await readFile7(file, "utf-8");
1964
1970
  if (content.split("\n").length > maxLines) {
@@ -1973,9 +1979,9 @@ async function checkMaxLines(check, sandboxDir) {
1973
1979
  async function listAllFiles(dir) {
1974
1980
  const results = [];
1975
1981
  try {
1976
- const entries = await readdir3(dir, { withFileTypes: true });
1982
+ const entries = await readdir4(dir, { withFileTypes: true });
1977
1983
  for (const entry of entries) {
1978
- const fullPath = join7(dir, entry.name);
1984
+ const fullPath = join8(dir, entry.name);
1979
1985
  if (entry.isDirectory()) {
1980
1986
  results.push(...await listAllFiles(fullPath));
1981
1987
  } else {
@@ -2100,28 +2106,43 @@ async function checkClaudeCli() {
2100
2106
  import { Command as Command4 } from "commander";
2101
2107
  import { spawn, execFile as execFile2 } from "child_process";
2102
2108
  import { promisify as promisify2 } from "util";
2103
- import { access as access6 } from "fs/promises";
2104
- import { join as join8 } from "path";
2109
+ import { access as access7 } from "fs/promises";
2110
+ import { join as join9 } from "path";
2105
2111
  var execAsync = promisify2(execFile2);
2106
- var ENHANCE_PROMPT = `Read CLAUDE.md and the project's codebase, then update CLAUDE.md to fill in missing or incomplete sections:
2112
+ var ENHANCE_PROMPT = `Read CLAUDE.md and the project's codebase, then update CLAUDE.md to fill in missing or incomplete sections.
2107
2113
 
2108
- 1. **## Architecture** or **## Project Structure** \u2014 describe the actual codebase structure (directories, key files, data flow)
2109
- 2. **## Conventions** \u2014 add project-specific patterns you observe (naming, imports, state management, API patterns)
2110
- 3. **## Off-Limits** \u2014 add guardrails based on what you see (protected files, patterns to avoid, things that should never change)
2111
- 4. **## Key Decisions** \u2014 document any architectural decisions visible in the code
2114
+ CRITICAL BUDGET RULE: CLAUDE.md must stay UNDER 120 lines of actionable content (not counting headings, blank lines, or comments). Claude Code starts ignoring rules past ~150 instructions. If you need more detail, create .claude/rules/ files instead:
2115
+ - Create .claude/rules/conventions.md for detailed coding patterns
2116
+ - Create .claude/rules/architecture.md for detailed structure docs
2117
+ - Keep CLAUDE.md to HIGH-LEVEL summaries only (3-5 bullets per section max)
2118
+
2119
+ Sections to fill in or preserve (DO NOT remove any existing section):
2120
+ 1. **## Stack** \u2014 if missing or incomplete, detect and add language, framework, package manager
2121
+ 2. **## Architecture** \u2014 3-5 bullet points describing the codebase shape (not a full directory tree)
2122
+ 3. **## Conventions** \u2014 max 8 key patterns. Move detailed rules to .claude/rules/conventions.md
2123
+ 4. **## Off-Limits** \u2014 max 8 guardrails specific to this project
2124
+ 5. **## Key Decisions** \u2014 only decisions that affect how Claude should work in this codebase
2125
+ 6. **MCP server suggestions** \u2014 look at what external services the project uses (databases, APIs, storage). If you spot Postgres, Redis, Stripe, GitHub API, or similar, suggest relevant MCP servers the user could add. Print these as suggestions at the end, not in CLAUDE.md.
2126
+
2127
+ Also review .claude/settings.json hooks:
2128
+ - Read the existing hooks in .claude/settings.json
2129
+ - If you see project-specific patterns that deserve hooks (e.g., protected directories, test file patterns, migration files), suggest adding them
2130
+ - DO NOT overwrite existing hooks \u2014 only add new ones that are specific to this project
2131
+ - Print hook suggestions at the end with the exact JSON to add, don't modify settings.json directly
2112
2132
 
2113
2133
  Rules:
2114
- - Keep CLAUDE.md under 150 instructions (lines of actionable content)
2115
2134
  - Don't remove existing content \u2014 only add or improve
2116
2135
  - Be specific to THIS project, not generic advice
2117
- - Use bullet points, not paragraphs`;
2136
+ - Use bullet points, not paragraphs
2137
+ - If a section would exceed 8 bullets, split into a .claude/rules/ file and reference it
2138
+ - After editing, count the actionable lines. If over 120, move content to rules files until under`;
2118
2139
  function createEnhanceCommand() {
2119
2140
  return new Command4("enhance").description("Use Claude to analyze your codebase and complete CLAUDE.md").option("-p, --path <path>", "Project root path", process.cwd()).action(async (opts) => {
2120
2141
  printBanner();
2121
2142
  const root = opts.path;
2122
- const claudeMdPath = join8(root, "CLAUDE.md");
2143
+ const claudeMdPath = join9(root, "CLAUDE.md");
2123
2144
  try {
2124
- await access6(claudeMdPath);
2145
+ await access7(claudeMdPath);
2125
2146
  } catch {
2126
2147
  log.error("No CLAUDE.md found. Run `claude-launchpad init` first.");
2127
2148
  process.exit(1);
@@ -2148,8 +2169,8 @@ function createEnhanceCommand() {
2148
2169
  }
2149
2170
 
2150
2171
  // src/cli.ts
2151
- var program = new Command5().name("claude-launchpad").description("CLI toolkit that makes Claude Code setups measurably good").version("0.2.2", "-v, --version").action(async () => {
2152
- const hasConfig = await fileExists3(join9(process.cwd(), "CLAUDE.md")) || await fileExists3(join9(process.cwd(), ".claude", "settings.json"));
2172
+ var program = new Command5().name("claude-launchpad").description("CLI toolkit that makes Claude Code setups measurably good").version("0.3.1", "-v, --version").action(async () => {
2173
+ const hasConfig = await fileExists4(join10(process.cwd(), "CLAUDE.md")) || await fileExists4(join10(process.cwd(), ".claude", "settings.json"));
2153
2174
  if (hasConfig) {
2154
2175
  await program.commands.find((c) => c.name() === "doctor")?.parseAsync([], { from: "user" });
2155
2176
  } else {
@@ -2167,9 +2188,9 @@ program.addCommand(createDoctorCommand());
2167
2188
  program.addCommand(createEnhanceCommand());
2168
2189
  program.addCommand(createEvalCommand());
2169
2190
  program.parse();
2170
- async function fileExists3(path) {
2191
+ async function fileExists4(path) {
2171
2192
  try {
2172
- await access7(path);
2193
+ await access8(path);
2173
2194
  return true;
2174
2195
  } catch {
2175
2196
  return false;