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