claude-launchpad 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -133,10 +133,31 @@ claude-launchpad eval --suite common
133
133
  Config Eval Score ━━━━━━━━━━━━━━━━━━── 89%
134
134
  ```
135
135
 
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.
136
+ Each scenario is a YAML file. [Write your own](scenarios/CONTRIBUTING.md).
137
137
 
138
138
  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
139
 
140
+ ## How It Works Under the Hood
141
+
142
+ ### doctor
143
+ 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.
144
+
145
+ ### init
146
+ 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.
147
+
148
+ ### enhance
149
+ 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.
150
+
151
+ ### eval
152
+ 1. Creates a temp directory (`/tmp/claude-eval-<uuid>/`)
153
+ 2. Writes seed files from the scenario YAML (e.g., a `src/db.ts` with a TODO)
154
+ 3. Writes a `CLAUDE.md` with the scenario's instructions
155
+ 4. Initializes a git repo (Claude Code expects one)
156
+ 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
157
+ 6. After Claude finishes, runs grep/file assertions against the modified files
158
+ 7. Scores: each check has points, total determines pass/fail
159
+ 8. Cleans up the temp directory (or preserves it with `--debug`)
160
+
140
161
  ## Use in CI
141
162
 
142
163
  Add this workflow to block PRs that degrade Claude Code config quality:
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;
@@ -1028,10 +1028,20 @@ async function analyzeHooks(config) {
1028
1028
  }
1029
1029
 
1030
1030
  // src/commands/doctor/analyzers/rules.ts
1031
- import { readFile as readFile4 } from "fs/promises";
1032
- import { basename as basename2 } from "path";
1031
+ import { readFile as readFile4, access as access3 } from "fs/promises";
1032
+ import { basename as basename2, join as join4, dirname } from "path";
1033
1033
  async function analyzeRules(config) {
1034
1034
  const issues = [];
1035
+ const projectRoot = config.claudeMdPath ? dirname(config.claudeMdPath) : process.cwd();
1036
+ const hasClaudeignore = await fileExists3(join4(projectRoot, ".claudeignore"));
1037
+ if (!hasClaudeignore) {
1038
+ issues.push({
1039
+ analyzer: "Rules",
1040
+ severity: "low",
1041
+ message: "No .claudeignore found \u2014 Claude may read noise files (node_modules, dist, lockfiles)",
1042
+ fix: "Run `claude-launchpad init` or `doctor --fix` to generate one"
1043
+ });
1044
+ }
1035
1045
  if (config.rules.length === 0) {
1036
1046
  issues.push({
1037
1047
  analyzer: "Rules",
@@ -1070,6 +1080,14 @@ async function analyzeRules(config) {
1070
1080
  const score = Math.max(0, 100 - issues.length * 10);
1071
1081
  return { name: "Rules", issues, score };
1072
1082
  }
1083
+ async function fileExists3(path) {
1084
+ try {
1085
+ await access3(path);
1086
+ return true;
1087
+ } catch {
1088
+ return false;
1089
+ }
1090
+ }
1073
1091
 
1074
1092
  // src/commands/doctor/analyzers/permissions.ts
1075
1093
  async function analyzePermissions(config) {
@@ -1116,7 +1134,7 @@ async function analyzePermissions(config) {
1116
1134
  }
1117
1135
 
1118
1136
  // src/commands/doctor/analyzers/mcp.ts
1119
- import { access as access3 } from "fs/promises";
1137
+ import { access as access4 } from "fs/promises";
1120
1138
  async function analyzeMcp(config) {
1121
1139
  const issues = [];
1122
1140
  const servers = config.mcpServers;
@@ -1124,8 +1142,7 @@ async function analyzeMcp(config) {
1124
1142
  issues.push({
1125
1143
  analyzer: "MCP",
1126
1144
  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."
1145
+ message: "No MCP servers configured. Run `claude-launchpad enhance` to get stack-specific recommendations."
1129
1146
  });
1130
1147
  return { name: "MCP Servers", issues, score: 50 };
1131
1148
  }
@@ -1150,13 +1167,13 @@ async function analyzeMcp(config) {
1150
1167
  const executable = server.command.split(" ")[0];
1151
1168
  if (executable.startsWith("/") || executable.startsWith("./")) {
1152
1169
  try {
1153
- await access3(executable);
1170
+ await access4(executable);
1154
1171
  } catch {
1155
1172
  issues.push({
1156
1173
  analyzer: "MCP",
1157
1174
  severity: "medium",
1158
1175
  message: `MCP server "${server.name}" command not found: ${executable}`,
1159
- fix: `Verify the path exists or install the required package`
1176
+ fix: "Verify the path exists or install the required package"
1160
1177
  });
1161
1178
  }
1162
1179
  }
@@ -1168,7 +1185,7 @@ async function analyzeMcp(config) {
1168
1185
 
1169
1186
  // src/commands/doctor/analyzers/quality.ts
1170
1187
  var ESSENTIAL_SECTIONS = [
1171
- { pattern: /^##\s+Stack/m, name: "Stack", why: "Claude performs worse without knowing the tech stack" },
1188
+ { pattern: /^##\s+(Tech )?Stack/m, name: "Stack", why: "Claude performs worse without knowing the tech stack" },
1172
1189
  { pattern: /^##\s+Commands/m, name: "Commands", why: "Claude guesses wrong without explicit dev/build/test commands" },
1173
1190
  { pattern: /^##\s+Session Start/m, name: "Session Start", why: "Without this, Claude won't read TASKS.md or maintain continuity" },
1174
1191
  { pattern: /^##\s+Off.?Limits/m, name: "Off-Limits", why: "Without guardrails, Claude has no boundaries beyond defaults" },
@@ -1249,8 +1266,8 @@ async function analyzeQuality(config) {
1249
1266
  }
1250
1267
 
1251
1268
  // 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";
1269
+ import { readFile as readFile5, writeFile as writeFile2, mkdir as mkdir2, access as access5 } from "fs/promises";
1270
+ import { join as join5 } from "path";
1254
1271
  async function applyFixes(issues, projectRoot) {
1255
1272
  const detected = await detectProject(projectRoot);
1256
1273
  let fixed = 0;
@@ -1299,6 +1316,9 @@ async function tryFix(issue, root, detected) {
1299
1316
  if (issue.analyzer === "Quality" && issue.message.includes("Session Start")) {
1300
1317
  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
1318
  }
1319
+ if (issue.analyzer === "Rules" && issue.message.includes("No .claudeignore")) {
1320
+ return createClaudeignore(root, detected);
1321
+ }
1302
1322
  if (issue.analyzer === "Rules" && issue.message.includes("No .claude/rules/")) {
1303
1323
  return createStarterRules(root);
1304
1324
  }
@@ -1384,7 +1404,7 @@ async function addForcePushProtection(root) {
1384
1404
  return true;
1385
1405
  }
1386
1406
  async function addClaudeMdSection(root, heading, content) {
1387
- const claudeMdPath = join4(root, "CLAUDE.md");
1407
+ const claudeMdPath = join5(root, "CLAUDE.md");
1388
1408
  let existing;
1389
1409
  try {
1390
1410
  existing = await readFile5(claudeMdPath, "utf-8");
@@ -1404,16 +1424,28 @@ ${content}
1404
1424
  log.success(`Added "${heading}" section to CLAUDE.md`);
1405
1425
  return true;
1406
1426
  }
1427
+ async function createClaudeignore(root, detected) {
1428
+ const ignorePath = join5(root, ".claudeignore");
1429
+ try {
1430
+ await access5(ignorePath);
1431
+ return false;
1432
+ } catch {
1433
+ }
1434
+ const content = generateClaudeignore(detected);
1435
+ await writeFile2(ignorePath, content);
1436
+ log.success("Generated .claudeignore with language-specific ignore patterns");
1437
+ return true;
1438
+ }
1407
1439
  async function createStarterRules(root) {
1408
- const rulesDir = join4(root, ".claude", "rules");
1440
+ const rulesDir = join5(root, ".claude", "rules");
1409
1441
  try {
1410
- await access4(rulesDir);
1442
+ await access5(rulesDir);
1411
1443
  return false;
1412
1444
  } catch {
1413
1445
  }
1414
1446
  await mkdir2(rulesDir, { recursive: true });
1415
1447
  await writeFile2(
1416
- join4(rulesDir, "conventions.md"),
1448
+ join5(rulesDir, "conventions.md"),
1417
1449
  `# Project Conventions
1418
1450
 
1419
1451
  - Use conventional commits (feat:, fix:, docs:, refactor:, test:, chore:)
@@ -1426,7 +1458,7 @@ async function createStarterRules(root) {
1426
1458
  return true;
1427
1459
  }
1428
1460
  async function readSettingsJson(root) {
1429
- const path = join4(root, ".claude", "settings.json");
1461
+ const path = join5(root, ".claude", "settings.json");
1430
1462
  try {
1431
1463
  const content = await readFile5(path, "utf-8");
1432
1464
  return JSON.parse(content);
@@ -1435,47 +1467,60 @@ async function readSettingsJson(root) {
1435
1467
  }
1436
1468
  }
1437
1469
  async function writeSettingsJson(root, settings) {
1438
- const dir = join4(root, ".claude");
1470
+ const dir = join5(root, ".claude");
1439
1471
  await mkdir2(dir, { recursive: true });
1440
- await writeFile2(join4(dir, "settings.json"), JSON.stringify(settings, null, 2) + "\n");
1472
+ await writeFile2(join5(dir, "settings.json"), JSON.stringify(settings, null, 2) + "\n");
1441
1473
  }
1442
1474
 
1443
1475
  // src/commands/doctor/watcher.ts
1444
- import { watch } from "fs";
1445
- import { join as join5 } from "path";
1476
+ import { readdir as readdir2, stat } from "fs/promises";
1477
+ import { join as join6 } from "path";
1446
1478
  async function watchConfig(projectRoot) {
1447
- const claudeDir = join5(projectRoot, ".claude");
1448
- const claudeMd = join5(projectRoot, "CLAUDE.md");
1449
- const claudeignore = join5(projectRoot, ".claudeignore");
1450
1479
  await runAndDisplay(projectRoot);
1451
1480
  log.blank();
1452
1481
  log.info("Watching for changes... (Ctrl+C to stop)");
1453
1482
  log.blank();
1454
- let debounce = null;
1455
- const onChange = () => {
1456
- if (debounce) clearTimeout(debounce);
1457
- debounce = setTimeout(async () => {
1483
+ let lastSnapshot = await getFileSnapshot(projectRoot);
1484
+ setInterval(async () => {
1485
+ const currentSnapshot = await getFileSnapshot(projectRoot);
1486
+ if (currentSnapshot !== lastSnapshot) {
1487
+ lastSnapshot = currentSnapshot;
1458
1488
  console.clear();
1459
1489
  await runAndDisplay(projectRoot);
1460
1490
  log.blank();
1461
1491
  log.info("Watching for changes... (Ctrl+C to stop)");
1462
1492
  log.blank();
1463
- }, 300);
1464
- };
1465
- try {
1466
- watch(claudeDir, { recursive: true }, onChange);
1467
- } catch {
1468
- }
1493
+ }
1494
+ }, 1e3);
1495
+ await new Promise(() => {
1496
+ });
1497
+ }
1498
+ async function getFileSnapshot(projectRoot) {
1499
+ const files = [
1500
+ join6(projectRoot, "CLAUDE.md"),
1501
+ join6(projectRoot, ".claudeignore")
1502
+ ];
1503
+ const claudeDir = join6(projectRoot, ".claude");
1469
1504
  try {
1470
- watch(claudeMd, onChange);
1505
+ const entries = await readdir2(claudeDir, { withFileTypes: true, recursive: true });
1506
+ for (const entry of entries) {
1507
+ if (entry.isFile()) {
1508
+ const parentPath = entry.parentPath ?? claudeDir;
1509
+ files.push(join6(parentPath, entry.name));
1510
+ }
1511
+ }
1471
1512
  } catch {
1472
1513
  }
1473
- try {
1474
- watch(claudeignore, onChange);
1475
- } catch {
1514
+ const mtimes = [];
1515
+ for (const file of files) {
1516
+ try {
1517
+ const s = await stat(file);
1518
+ mtimes.push(`${file}:${s.mtimeMs}`);
1519
+ } catch {
1520
+ mtimes.push(`${file}:missing`);
1521
+ }
1476
1522
  }
1477
- await new Promise(() => {
1478
- });
1523
+ return mtimes.join("|");
1479
1524
  }
1480
1525
  async function runAndDisplay(projectRoot) {
1481
1526
  console.log("\x1B[36m\x1B[1m Claude Launchpad\x1B[0m");
@@ -1613,8 +1658,8 @@ import ora from "ora";
1613
1658
  import chalk2 from "chalk";
1614
1659
 
1615
1660
  // 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";
1661
+ import { readFile as readFile6, readdir as readdir3, access as access6 } from "fs/promises";
1662
+ import { join as join7, resolve as resolve2, dirname as dirname2 } from "path";
1618
1663
  import { fileURLToPath } from "url";
1619
1664
  import { parse as parseYaml } from "yaml";
1620
1665
 
@@ -1717,7 +1762,7 @@ var ScenarioError = class extends Error {
1717
1762
 
1718
1763
  // src/commands/eval/loader.ts
1719
1764
  async function findScenariosDir() {
1720
- const thisDir = dirname(fileURLToPath(import.meta.url));
1765
+ const thisDir = dirname2(fileURLToPath(import.meta.url));
1721
1766
  const devPath = resolve2(thisDir, "../../../scenarios");
1722
1767
  if (await dirExists(devPath)) return devPath;
1723
1768
  const bundledPath = resolve2(thisDir, "../scenarios");
@@ -1728,7 +1773,7 @@ async function findScenariosDir() {
1728
1773
  }
1729
1774
  async function dirExists(path) {
1730
1775
  try {
1731
- await access5(path);
1776
+ await access6(path);
1732
1777
  return true;
1733
1778
  } catch {
1734
1779
  return false;
@@ -1737,7 +1782,7 @@ async function dirExists(path) {
1737
1782
  async function loadScenarios(options) {
1738
1783
  const { suite, customPath } = options;
1739
1784
  const scenarioDir = customPath ? resolve2(customPath) : await findScenariosDir();
1740
- const dirs = suite ? [join6(scenarioDir, suite)] : await getSubdirectories(scenarioDir);
1785
+ const dirs = suite ? [join7(scenarioDir, suite)] : await getSubdirectories(scenarioDir);
1741
1786
  const allDirs = [scenarioDir, ...dirs];
1742
1787
  const scenarios = [];
1743
1788
  for (const dir of allDirs) {
@@ -1758,31 +1803,31 @@ async function loadScenarios(options) {
1758
1803
  }
1759
1804
  async function getSubdirectories(dir) {
1760
1805
  try {
1761
- const entries = await readdir2(dir, { withFileTypes: true });
1762
- return entries.filter((e) => e.isDirectory()).map((e) => join6(dir, e.name));
1806
+ const entries = await readdir3(dir, { withFileTypes: true });
1807
+ return entries.filter((e) => e.isDirectory()).map((e) => join7(dir, e.name));
1763
1808
  } catch {
1764
1809
  return [];
1765
1810
  }
1766
1811
  }
1767
1812
  async function listYamlFiles(dir) {
1768
1813
  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));
1814
+ const entries = await readdir3(dir, { withFileTypes: true });
1815
+ return entries.filter((e) => e.isFile() && (e.name.endsWith(".yaml") || e.name.endsWith(".yml"))).map((e) => join7(dir, e.name));
1771
1816
  } catch {
1772
1817
  return [];
1773
1818
  }
1774
1819
  }
1775
1820
 
1776
1821
  // 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";
1822
+ import { mkdir as mkdir3, writeFile as writeFile3, readFile as readFile7, readdir as readdir4, rm } from "fs/promises";
1823
+ import { join as join8, dirname as dirname3 } from "path";
1779
1824
  import { tmpdir } from "os";
1780
1825
  import { randomUUID } from "crypto";
1781
1826
  import { execFile } from "child_process";
1782
1827
  import { promisify } from "util";
1783
1828
  var exec = promisify(execFile);
1784
1829
  async function runScenario(scenario, options) {
1785
- const sandboxDir = join7(tmpdir(), `claude-eval-${randomUUID()}`);
1830
+ const sandboxDir = join8(tmpdir(), `claude-eval-${randomUUID()}`);
1786
1831
  try {
1787
1832
  await setupSandbox(sandboxDir, scenario);
1788
1833
  await runClaudeInSandbox(sandboxDir, scenario.prompt, options.timeout);
@@ -1808,13 +1853,13 @@ async function runScenarioWithRetries(scenario, options) {
1808
1853
  async function setupSandbox(sandboxDir, scenario) {
1809
1854
  await mkdir3(sandboxDir, { recursive: true });
1810
1855
  for (const file of scenario.setup.files) {
1811
- const filePath = join7(sandboxDir, file.path);
1812
- await mkdir3(dirname2(filePath), { recursive: true });
1856
+ const filePath = join8(sandboxDir, file.path);
1857
+ await mkdir3(dirname3(filePath), { recursive: true });
1813
1858
  await writeFile3(filePath, file.content);
1814
1859
  }
1815
1860
  if (scenario.setup.instructions) {
1816
1861
  await writeFile3(
1817
- join7(sandboxDir, "CLAUDE.md"),
1862
+ join8(sandboxDir, "CLAUDE.md"),
1818
1863
  `# Eval Scenario
1819
1864
 
1820
1865
  ${scenario.setup.instructions}
@@ -1927,7 +1972,7 @@ async function evaluateSingleCheck(check, sandboxDir) {
1927
1972
  async function checkGrep(check, sandboxDir) {
1928
1973
  if (!check.pattern) return false;
1929
1974
  try {
1930
- const content = await readFile7(join7(sandboxDir, check.target), "utf-8");
1975
+ const content = await readFile7(join8(sandboxDir, check.target), "utf-8");
1931
1976
  let found;
1932
1977
  try {
1933
1978
  found = new RegExp(check.pattern).test(content);
@@ -1941,7 +1986,7 @@ async function checkGrep(check, sandboxDir) {
1941
1986
  }
1942
1987
  async function checkFileExists(check, sandboxDir) {
1943
1988
  try {
1944
- await readFile7(join7(sandboxDir, check.target));
1989
+ await readFile7(join8(sandboxDir, check.target));
1945
1990
  return check.expect === "present";
1946
1991
  } catch {
1947
1992
  return check.expect === "absent";
@@ -1949,7 +1994,7 @@ async function checkFileExists(check, sandboxDir) {
1949
1994
  }
1950
1995
  async function checkFileAbsent(check, sandboxDir) {
1951
1996
  try {
1952
- await readFile7(join7(sandboxDir, check.target));
1997
+ await readFile7(join8(sandboxDir, check.target));
1953
1998
  return check.expect === "absent";
1954
1999
  } catch {
1955
2000
  return check.expect === "present";
@@ -1958,7 +2003,7 @@ async function checkFileAbsent(check, sandboxDir) {
1958
2003
  async function checkMaxLines(check, sandboxDir) {
1959
2004
  const maxLines = parseInt(check.pattern ?? "800", 10);
1960
2005
  try {
1961
- const files = await listAllFiles(join7(sandboxDir, check.target));
2006
+ const files = await listAllFiles(join8(sandboxDir, check.target));
1962
2007
  for (const file of files) {
1963
2008
  const content = await readFile7(file, "utf-8");
1964
2009
  if (content.split("\n").length > maxLines) {
@@ -1973,9 +2018,9 @@ async function checkMaxLines(check, sandboxDir) {
1973
2018
  async function listAllFiles(dir) {
1974
2019
  const results = [];
1975
2020
  try {
1976
- const entries = await readdir3(dir, { withFileTypes: true });
2021
+ const entries = await readdir4(dir, { withFileTypes: true });
1977
2022
  for (const entry of entries) {
1978
- const fullPath = join7(dir, entry.name);
2023
+ const fullPath = join8(dir, entry.name);
1979
2024
  if (entry.isDirectory()) {
1980
2025
  results.push(...await listAllFiles(fullPath));
1981
2026
  } else {
@@ -2100,28 +2145,37 @@ async function checkClaudeCli() {
2100
2145
  import { Command as Command4 } from "commander";
2101
2146
  import { spawn, execFile as execFile2 } from "child_process";
2102
2147
  import { promisify as promisify2 } from "util";
2103
- import { access as access6 } from "fs/promises";
2104
- import { join as join8 } from "path";
2148
+ import { access as access7 } from "fs/promises";
2149
+ import { join as join9 } from "path";
2105
2150
  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:
2151
+ var ENHANCE_PROMPT = `Read CLAUDE.md and the project's codebase, then update CLAUDE.md to fill in missing or incomplete sections.
2152
+
2153
+ 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:
2154
+ - Create .claude/rules/conventions.md for detailed coding patterns
2155
+ - Create .claude/rules/architecture.md for detailed structure docs
2156
+ - Keep CLAUDE.md to HIGH-LEVEL summaries only (3-5 bullets per section max)
2107
2157
 
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
2158
+ Sections to fill in or preserve (DO NOT remove any existing section):
2159
+ 1. **## Stack** \u2014 if missing or incomplete, detect and add language, framework, package manager
2160
+ 2. **## Architecture** \u2014 3-5 bullet points describing the codebase shape (not a full directory tree)
2161
+ 3. **## Conventions** \u2014 max 8 key patterns. Move detailed rules to .claude/rules/conventions.md
2162
+ 4. **## Off-Limits** \u2014 max 8 guardrails specific to this project
2163
+ 5. **## Key Decisions** \u2014 only decisions that affect how Claude should work in this codebase
2164
+ 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.
2112
2165
 
2113
2166
  Rules:
2114
- - Keep CLAUDE.md under 150 instructions (lines of actionable content)
2115
2167
  - Don't remove existing content \u2014 only add or improve
2116
2168
  - Be specific to THIS project, not generic advice
2117
- - Use bullet points, not paragraphs`;
2169
+ - Use bullet points, not paragraphs
2170
+ - If a section would exceed 8 bullets, split into a .claude/rules/ file and reference it
2171
+ - After editing, count the actionable lines. If over 120, move content to rules files until under`;
2118
2172
  function createEnhanceCommand() {
2119
2173
  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
2174
  printBanner();
2121
2175
  const root = opts.path;
2122
- const claudeMdPath = join8(root, "CLAUDE.md");
2176
+ const claudeMdPath = join9(root, "CLAUDE.md");
2123
2177
  try {
2124
- await access6(claudeMdPath);
2178
+ await access7(claudeMdPath);
2125
2179
  } catch {
2126
2180
  log.error("No CLAUDE.md found. Run `claude-launchpad init` first.");
2127
2181
  process.exit(1);
@@ -2148,8 +2202,8 @@ function createEnhanceCommand() {
2148
2202
  }
2149
2203
 
2150
2204
  // src/cli.ts
2151
- var program = new Command5().name("claude-launchpad").description("CLI toolkit that makes Claude Code setups measurably good").version("0.2.1").action(async () => {
2152
- const hasConfig = await fileExists3(join9(process.cwd(), "CLAUDE.md")) || await fileExists3(join9(process.cwd(), ".claude", "settings.json"));
2205
+ var program = new Command5().name("claude-launchpad").description("CLI toolkit that makes Claude Code setups measurably good").version("0.3.0", "-v, --version").action(async () => {
2206
+ const hasConfig = await fileExists4(join10(process.cwd(), "CLAUDE.md")) || await fileExists4(join10(process.cwd(), ".claude", "settings.json"));
2153
2207
  if (hasConfig) {
2154
2208
  await program.commands.find((c) => c.name() === "doctor")?.parseAsync([], { from: "user" });
2155
2209
  } else {
@@ -2167,9 +2221,9 @@ program.addCommand(createDoctorCommand());
2167
2221
  program.addCommand(createEnhanceCommand());
2168
2222
  program.addCommand(createEvalCommand());
2169
2223
  program.parse();
2170
- async function fileExists3(path) {
2224
+ async function fileExists4(path) {
2171
2225
  try {
2172
- await access7(path);
2226
+ await access8(path);
2173
2227
  return true;
2174
2228
  } catch {
2175
2229
  return false;