worclaude 2.2.6 → 2.4.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/CHANGELOG.md +113 -0
- package/README.md +114 -37
- package/package.json +7 -2
- package/src/commands/doctor.js +507 -104
- package/src/commands/init.js +103 -19
- package/src/core/detector.js +34 -10
- package/src/core/merger.js +48 -3
- package/src/core/scaffolder.js +58 -1
- package/src/data/agent-registry.js +17 -2
- package/src/data/agents.js +7 -0
- package/src/index.js +2 -1
- package/src/prompts/claude-md-merge.js +5 -0
- package/templates/agents/universal/build-validator.md +23 -0
- package/templates/agents/universal/code-simplifier.md +11 -0
- package/templates/agents/universal/plan-reviewer.md +39 -0
- package/templates/agents/universal/test-writer.md +25 -0
- package/templates/agents/universal/upstream-watcher.md +120 -0
- package/templates/agents/universal/verify-app.md +12 -0
- package/templates/commands/build-fix.md +5 -0
- package/templates/commands/commit-push-pr.md +6 -0
- package/templates/commands/compact-safe.md +5 -0
- package/templates/commands/conflict-resolver.md +5 -0
- package/templates/commands/end.md +10 -0
- package/templates/commands/learn.md +33 -0
- package/templates/commands/refactor-clean.md +10 -0
- package/templates/commands/review-changes.md +5 -0
- package/templates/commands/review-plan.md +5 -0
- package/templates/commands/setup.md +5 -0
- package/templates/commands/start.md +10 -0
- package/templates/commands/status.md +5 -0
- package/templates/commands/sync.md +5 -0
- package/templates/commands/techdebt.md +5 -0
- package/templates/commands/test-coverage.md +5 -0
- package/templates/commands/update-claude-md.md +5 -0
- package/templates/commands/upstream-check.md +85 -0
- package/templates/commands/verify.md +11 -0
- package/templates/core/agents-md.md +25 -0
- package/templates/core/claude-md.md +17 -0
- package/templates/hooks/README.md +106 -0
- package/templates/hooks/correction-detect.cjs +48 -0
- package/templates/hooks/examples/prompt-hook-commit-validator.json +16 -0
- package/templates/hooks/learn-capture.cjs +179 -0
- package/templates/hooks/pre-compact-save.cjs +60 -0
- package/templates/hooks/skill-hint.cjs +66 -0
- package/templates/memory/decisions.md +11 -0
- package/templates/memory/preferences.md +17 -0
- package/templates/settings/base.json +56 -8
- package/templates/skills/templates/backend-conventions.md +1 -0
- package/templates/skills/templates/frontend-design-system.md +1 -0
- package/templates/skills/templates/project-patterns.md +1 -0
- package/templates/skills/universal/coding-principles.md +60 -0
- package/templates/skills/universal/context-management.md +21 -0
- package/templates/skills/universal/planning-with-files.md +11 -0
- package/templates/skills/universal/verification.md +18 -0
package/src/commands/doctor.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
3
4
|
import { readWorkflowMeta, workflowMetaExists, getPackageVersion } from '../core/config.js';
|
|
4
5
|
import { hashFile } from '../utils/hash.js';
|
|
5
6
|
import { fileExists, readFile, listFilesRecursive } from '../utils/file.js';
|
|
@@ -16,6 +17,54 @@ const PASS = 'pass';
|
|
|
16
17
|
const WARN = 'warn';
|
|
17
18
|
const FAIL = 'fail';
|
|
18
19
|
|
|
20
|
+
// Claude Code v2.1.101 documented hook events (closed set)
|
|
21
|
+
const VALID_HOOK_EVENTS = new Set([
|
|
22
|
+
'PreToolUse',
|
|
23
|
+
'PostToolUse',
|
|
24
|
+
'PostToolUseFailure',
|
|
25
|
+
'Stop',
|
|
26
|
+
'PreCompact',
|
|
27
|
+
'PostCompact',
|
|
28
|
+
'SessionStart',
|
|
29
|
+
'SessionEnd',
|
|
30
|
+
'UserPromptSubmit',
|
|
31
|
+
'Notification',
|
|
32
|
+
'PermissionRequest',
|
|
33
|
+
'PermissionDenied',
|
|
34
|
+
'SubagentStart',
|
|
35
|
+
'SubagentStop',
|
|
36
|
+
'Setup',
|
|
37
|
+
'CwdChanged',
|
|
38
|
+
'FileChanged',
|
|
39
|
+
'WorktreeCreate',
|
|
40
|
+
'WorktreeRemove',
|
|
41
|
+
'TeammateIdle',
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
// Models deprecated in favor of alias-only references (opus, sonnet, haiku)
|
|
45
|
+
const DEPRECATED_MODELS = new Set([
|
|
46
|
+
'opus-4',
|
|
47
|
+
'opus-4.1',
|
|
48
|
+
'claude-3-opus',
|
|
49
|
+
'claude-3-haiku',
|
|
50
|
+
'claude-opus-4',
|
|
51
|
+
'claude-opus-4-1',
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
// Events where blocking is by design — their output or side-effects must
|
|
55
|
+
// complete before Claude proceeds. Flagging these as async would break
|
|
56
|
+
// their purpose (e.g., SessionStart output becomes session context).
|
|
57
|
+
const BLOCKING_BY_DESIGN_EVENTS = new Set([
|
|
58
|
+
'SessionStart',
|
|
59
|
+
'PreToolUse',
|
|
60
|
+
'PostToolUse',
|
|
61
|
+
'PostToolUseFailure',
|
|
62
|
+
'UserPromptSubmit',
|
|
63
|
+
'PreCompact',
|
|
64
|
+
'PermissionRequest',
|
|
65
|
+
'Setup',
|
|
66
|
+
]);
|
|
67
|
+
|
|
19
68
|
function result(status, label, detail) {
|
|
20
69
|
return { status, label, detail };
|
|
21
70
|
}
|
|
@@ -73,46 +122,91 @@ async function checkClaudeMd(projectRoot) {
|
|
|
73
122
|
}
|
|
74
123
|
}
|
|
75
124
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
125
|
+
// Returns CLAUDE.md content or null when missing/unreadable. Missing-file
|
|
126
|
+
// reporting is owned by checkClaudeMd — callers that use this helper should
|
|
127
|
+
// skip reporting (return []) to avoid duplicate complaints.
|
|
128
|
+
async function readClaudeMd(projectRoot) {
|
|
81
129
|
try {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
130
|
+
return await readFile(path.join(projectRoot, 'CLAUDE.md'));
|
|
131
|
+
} catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
87
135
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (charCount > WARN_THRESHOLD) {
|
|
98
|
-
return [
|
|
99
|
-
result(
|
|
100
|
-
WARN,
|
|
101
|
-
`CLAUDE.md size: ${charCount.toLocaleString()} chars`,
|
|
102
|
-
`Approaching limit (${WARN_THRESHOLD.toLocaleString()}/${HARD_LIMIT.toLocaleString()}). Consider moving content to skills.`
|
|
103
|
-
),
|
|
104
|
-
];
|
|
105
|
-
}
|
|
136
|
+
async function checkClaudeMdSize(projectRoot) {
|
|
137
|
+
const content = await readClaudeMd(projectRoot);
|
|
138
|
+
if (content === null) return [];
|
|
139
|
+
const charCount = content.length;
|
|
140
|
+
const WARN_THRESHOLD = 30000;
|
|
141
|
+
const FAIL_THRESHOLD = 38000;
|
|
142
|
+
const HARD_LIMIT = 40000;
|
|
143
|
+
|
|
144
|
+
if (charCount > FAIL_THRESHOLD) {
|
|
106
145
|
return [
|
|
107
146
|
result(
|
|
108
|
-
|
|
109
|
-
`CLAUDE.md size: ${charCount.toLocaleString()} chars
|
|
110
|
-
|
|
147
|
+
FAIL,
|
|
148
|
+
`CLAUDE.md size: ${charCount.toLocaleString()} chars`,
|
|
149
|
+
`Exceeds recommended limit (${FAIL_THRESHOLD.toLocaleString()}/${HARD_LIMIT.toLocaleString()}). Claude Code caps at ${HARD_LIMIT.toLocaleString()} chars. Move domain-specific content to conditional skills with paths frontmatter.`
|
|
111
150
|
),
|
|
112
151
|
];
|
|
113
|
-
} catch {
|
|
114
|
-
return [];
|
|
115
152
|
}
|
|
153
|
+
if (charCount > WARN_THRESHOLD) {
|
|
154
|
+
return [
|
|
155
|
+
result(
|
|
156
|
+
WARN,
|
|
157
|
+
`CLAUDE.md size: ${charCount.toLocaleString()} chars`,
|
|
158
|
+
`Approaching limit (${WARN_THRESHOLD.toLocaleString()}/${HARD_LIMIT.toLocaleString()}). Consider moving content to skills.`
|
|
159
|
+
),
|
|
160
|
+
];
|
|
161
|
+
}
|
|
162
|
+
return [
|
|
163
|
+
result(
|
|
164
|
+
PASS,
|
|
165
|
+
`CLAUDE.md size: ${charCount.toLocaleString()} chars (limit: ${HARD_LIMIT.toLocaleString()})`,
|
|
166
|
+
null
|
|
167
|
+
),
|
|
168
|
+
];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function checkClaudeMdLineCount(projectRoot) {
|
|
172
|
+
const content = await readClaudeMd(projectRoot);
|
|
173
|
+
if (content === null) return [];
|
|
174
|
+
const lines = content.split(/\r?\n/).length;
|
|
175
|
+
const WARN_LINES = 150;
|
|
176
|
+
const FAIL_LINES = 200;
|
|
177
|
+
const detail = `CLAUDE.md is ${lines} lines. Recommended max: 200. Claude Code performance degrades with bloated context files. Move domain content to .claude/rules/ or .claude/skills/.`;
|
|
178
|
+
|
|
179
|
+
if (lines > FAIL_LINES) {
|
|
180
|
+
return [result(FAIL, `CLAUDE.md line count: ${lines}/200`, detail)];
|
|
181
|
+
}
|
|
182
|
+
if (lines > WARN_LINES) {
|
|
183
|
+
return [result(WARN, `CLAUDE.md line count: ${lines}/200`, detail)];
|
|
184
|
+
}
|
|
185
|
+
return [result(PASS, `CLAUDE.md line count: ${lines}/200`, null)];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function checkClaudeMdMemoryGuidance(projectRoot) {
|
|
189
|
+
const content = await readClaudeMd(projectRoot);
|
|
190
|
+
if (content === null) return [];
|
|
191
|
+
const indicators = [
|
|
192
|
+
'memory architecture',
|
|
193
|
+
'native memory',
|
|
194
|
+
'.claude/learnings',
|
|
195
|
+
'[LEARN]',
|
|
196
|
+
'/learn',
|
|
197
|
+
];
|
|
198
|
+
const lower = content.toLowerCase();
|
|
199
|
+
const hasGuidance = indicators.some((i) => lower.includes(i.toLowerCase()));
|
|
200
|
+
if (hasGuidance) {
|
|
201
|
+
return [result(PASS, 'CLAUDE.md memory guidance', null)];
|
|
202
|
+
}
|
|
203
|
+
return [
|
|
204
|
+
result(
|
|
205
|
+
WARN,
|
|
206
|
+
'CLAUDE.md memory guidance',
|
|
207
|
+
'CLAUDE.md has no memory architecture guidance. Auto-learnings may pollute this file. Run worclaude upgrade to add.'
|
|
208
|
+
),
|
|
209
|
+
];
|
|
116
210
|
}
|
|
117
211
|
|
|
118
212
|
async function checkSettingsJson(projectRoot) {
|
|
@@ -147,6 +241,131 @@ async function checkSettingsJson(projectRoot) {
|
|
|
147
241
|
}
|
|
148
242
|
}
|
|
149
243
|
|
|
244
|
+
async function readSettingsJson(projectRoot) {
|
|
245
|
+
const settingsPath = path.join(projectRoot, '.claude', 'settings.json');
|
|
246
|
+
if (!(await fileExists(settingsPath))) return null;
|
|
247
|
+
try {
|
|
248
|
+
return JSON.parse(await readFile(settingsPath));
|
|
249
|
+
} catch {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function checkHookEventNames(projectRoot) {
|
|
255
|
+
const settings = await readSettingsJson(projectRoot);
|
|
256
|
+
if (!settings) {
|
|
257
|
+
return [result(WARN, 'Hook event names', 'settings.json missing or invalid')];
|
|
258
|
+
}
|
|
259
|
+
const events = Object.keys(settings.hooks ?? {});
|
|
260
|
+
if (events.length === 0) {
|
|
261
|
+
return [result(WARN, 'Hook event names', 'No hooks configured')];
|
|
262
|
+
}
|
|
263
|
+
const invalid = events.filter((e) => !VALID_HOOK_EVENTS.has(e));
|
|
264
|
+
if (invalid.length === 0) {
|
|
265
|
+
return [result(PASS, `Hook event names (${events.length} events)`, null)];
|
|
266
|
+
}
|
|
267
|
+
return invalid.map((name) =>
|
|
268
|
+
result(
|
|
269
|
+
FAIL,
|
|
270
|
+
`Hook event: ${name}`,
|
|
271
|
+
`Unknown hook event '${name}'. Check Claude Code docs for valid event names.`
|
|
272
|
+
)
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function extractHookCommands(settings) {
|
|
277
|
+
const out = [];
|
|
278
|
+
for (const [event, arr] of Object.entries(settings.hooks ?? {})) {
|
|
279
|
+
if (!Array.isArray(arr)) continue;
|
|
280
|
+
for (const group of arr) {
|
|
281
|
+
const inner = Array.isArray(group?.hooks) ? group.hooks : [];
|
|
282
|
+
for (const h of inner) {
|
|
283
|
+
if (h && typeof h === 'object') {
|
|
284
|
+
out.push({ event, type: h.type, command: h.command, async: h.async === true });
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return out;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function checkHookScriptFiles(projectRoot) {
|
|
293
|
+
const settings = await readSettingsJson(projectRoot);
|
|
294
|
+
if (!settings) return [];
|
|
295
|
+
const entries = extractHookCommands(settings).filter((h) => h.type === 'command' && h.command);
|
|
296
|
+
const pathRegex = /\.claude\/hooks\/[A-Za-z0-9._-]+\.(?:cjs|js|mjs|sh)\b/g;
|
|
297
|
+
const referenced = new Set();
|
|
298
|
+
for (const { command } of entries) {
|
|
299
|
+
const matches = command.match(pathRegex);
|
|
300
|
+
if (matches) for (const m of matches) referenced.add(m);
|
|
301
|
+
}
|
|
302
|
+
if (referenced.size === 0) {
|
|
303
|
+
return [result(PASS, 'Hook script files (no file-based hooks)', null)];
|
|
304
|
+
}
|
|
305
|
+
const missing = [];
|
|
306
|
+
for (const rel of referenced) {
|
|
307
|
+
if (!(await fileExists(path.join(projectRoot, rel)))) {
|
|
308
|
+
missing.push(rel);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (missing.length === 0) {
|
|
312
|
+
return [result(PASS, `Hook script files (${referenced.size} referenced)`, null)];
|
|
313
|
+
}
|
|
314
|
+
return missing.map((rel) =>
|
|
315
|
+
result(FAIL, 'Hook script', `Hook references '${rel}' but file does not exist`)
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function checkKeyHookCoverage(projectRoot) {
|
|
320
|
+
const settings = await readSettingsJson(projectRoot);
|
|
321
|
+
if (!settings) {
|
|
322
|
+
return [result(WARN, 'Key hook coverage', 'settings.json missing or invalid')];
|
|
323
|
+
}
|
|
324
|
+
const messages = {
|
|
325
|
+
PreCompact: 'PreCompact hook missing — context may be lost during auto-compaction',
|
|
326
|
+
UserPromptSubmit: 'UserPromptSubmit hook missing — correction detection disabled',
|
|
327
|
+
Stop: 'Stop hook missing — learning capture disabled',
|
|
328
|
+
};
|
|
329
|
+
const results = [];
|
|
330
|
+
for (const event of ['PreCompact', 'UserPromptSubmit', 'Stop']) {
|
|
331
|
+
const arr = settings.hooks?.[event];
|
|
332
|
+
if (Array.isArray(arr) && arr.length > 0) {
|
|
333
|
+
results.push(result(PASS, `${event} hook`, null));
|
|
334
|
+
} else {
|
|
335
|
+
results.push(result(WARN, `${event} hook`, messages[event]));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return results;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function checkHookAsync(projectRoot) {
|
|
342
|
+
const settings = await readSettingsJson(projectRoot);
|
|
343
|
+
if (!settings) return [];
|
|
344
|
+
const entries = extractHookCommands(settings);
|
|
345
|
+
const asyncCandidateRegex = /\b(notify|notification|backup|log)\b/i;
|
|
346
|
+
const warnings = [];
|
|
347
|
+
for (const h of entries) {
|
|
348
|
+
if (BLOCKING_BY_DESIGN_EVENTS.has(h.event)) continue;
|
|
349
|
+
const isCandidate =
|
|
350
|
+
h.event === 'Notification' ||
|
|
351
|
+
h.event === 'SessionEnd' ||
|
|
352
|
+
(h.command && asyncCandidateRegex.test(h.command));
|
|
353
|
+
if (isCandidate && !h.async) {
|
|
354
|
+
warnings.push(
|
|
355
|
+
result(
|
|
356
|
+
WARN,
|
|
357
|
+
`Hook async: ${h.event}`,
|
|
358
|
+
`Hook '${h.event}' should use async: true — it blocks Claude unnecessarily`
|
|
359
|
+
)
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (warnings.length === 0) {
|
|
364
|
+
return [result(PASS, 'Hook async flags', null)];
|
|
365
|
+
}
|
|
366
|
+
return warnings;
|
|
367
|
+
}
|
|
368
|
+
|
|
150
369
|
async function checkAgents(projectRoot, meta) {
|
|
151
370
|
const agentsDir = path.join(projectRoot, '.claude', 'agents');
|
|
152
371
|
const results = [];
|
|
@@ -448,56 +667,160 @@ async function checkAgentCompleteness(projectRoot) {
|
|
|
448
667
|
}
|
|
449
668
|
|
|
450
669
|
async function checkClaudeMdSections(projectRoot) {
|
|
451
|
-
const claudeMdPath = path.join(projectRoot, 'CLAUDE.md');
|
|
452
670
|
const SECTION_THRESHOLD = 20000; // Only analyze sections if file > 20KB
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
if (headingMatch) {
|
|
468
|
-
if (currentLines.length > 0) {
|
|
469
|
-
sections.push({ heading: currentHeading, size: currentLines.join('\n').length });
|
|
470
|
-
}
|
|
471
|
-
currentHeading = headingMatch[1];
|
|
472
|
-
currentLines = [];
|
|
473
|
-
} else {
|
|
474
|
-
currentLines.push(line);
|
|
671
|
+
const content = await readClaudeMd(projectRoot);
|
|
672
|
+
if (content === null || content.length < SECTION_THRESHOLD) return [];
|
|
673
|
+
|
|
674
|
+
// Split into ## sections
|
|
675
|
+
const sections = [];
|
|
676
|
+
const lines = content.split('\n');
|
|
677
|
+
let currentHeading = '(top-level)';
|
|
678
|
+
let currentLines = [];
|
|
679
|
+
|
|
680
|
+
for (const line of lines) {
|
|
681
|
+
const headingMatch = line.match(/^##\s+(.+)/);
|
|
682
|
+
if (headingMatch) {
|
|
683
|
+
if (currentLines.length > 0) {
|
|
684
|
+
sections.push({ heading: currentHeading, size: currentLines.join('\n').length });
|
|
475
685
|
}
|
|
686
|
+
currentHeading = headingMatch[1];
|
|
687
|
+
currentLines = [];
|
|
688
|
+
} else {
|
|
689
|
+
currentLines.push(line);
|
|
476
690
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
}
|
|
691
|
+
}
|
|
692
|
+
if (currentLines.length > 0) {
|
|
693
|
+
sections.push({ heading: currentHeading, size: currentLines.join('\n').length });
|
|
694
|
+
}
|
|
480
695
|
|
|
481
|
-
|
|
482
|
-
|
|
696
|
+
// Sort by size, suggest extracting the top 3 sections > 2KB
|
|
697
|
+
const large = sections.filter((s) => s.size > 2000).sort((a, b) => b.size - a.size);
|
|
698
|
+
if (large.length === 0) return [];
|
|
483
699
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
700
|
+
const top = large.slice(0, 3);
|
|
701
|
+
const sectionList = top.map((s) => `"${s.heading}" (${(s.size / 1024).toFixed(1)}KB)`).join(', ');
|
|
702
|
+
return [
|
|
703
|
+
result(
|
|
704
|
+
WARN,
|
|
705
|
+
`CLAUDE.md has large sections: ${sectionList}`,
|
|
706
|
+
'Consider extracting to conditional skills with paths frontmatter to save context budget'
|
|
707
|
+
),
|
|
708
|
+
];
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
async function checkAgentModels(projectRoot) {
|
|
712
|
+
const agents = await readAgentFrontmatters(projectRoot);
|
|
713
|
+
const withFrontmatter = agents.filter((a) => a.frontmatter);
|
|
714
|
+
if (withFrontmatter.length === 0) return [];
|
|
715
|
+
|
|
716
|
+
const warnings = [];
|
|
717
|
+
for (const { name, frontmatter } of withFrontmatter) {
|
|
718
|
+
const m = frontmatter.match(/^model:\s*["']?([^"'\n]+?)["']?\s*$/m);
|
|
719
|
+
if (!m) continue;
|
|
720
|
+
const model = m[1].trim();
|
|
721
|
+
if (DEPRECATED_MODELS.has(model)) {
|
|
722
|
+
warnings.push(
|
|
490
723
|
result(
|
|
491
724
|
WARN,
|
|
492
|
-
`
|
|
493
|
-
'
|
|
725
|
+
`Agent model: ${name}`,
|
|
726
|
+
`Agent '${name}' uses deprecated model '${model}'. Use 'opus', 'sonnet', or 'haiku' instead.`
|
|
494
727
|
)
|
|
495
728
|
);
|
|
496
729
|
}
|
|
730
|
+
}
|
|
731
|
+
if (warnings.length === 0) {
|
|
732
|
+
return [result(PASS, `Agent models (${withFrontmatter.length} agents scanned)`, null)];
|
|
733
|
+
}
|
|
734
|
+
return warnings;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
async function checkAgentsMd(projectRoot) {
|
|
738
|
+
const agentsMdPath = path.join(projectRoot, 'AGENTS.md');
|
|
739
|
+
if (await fileExists(agentsMdPath)) {
|
|
740
|
+
return result(PASS, 'AGENTS.md', null);
|
|
741
|
+
}
|
|
742
|
+
return result(
|
|
743
|
+
WARN,
|
|
744
|
+
'AGENTS.md',
|
|
745
|
+
'AGENTS.md not found. This file enables cross-tool compatibility (Cursor, Codex, Copilot). Run worclaude upgrade to generate.'
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
async function checkLearnings(projectRoot) {
|
|
750
|
+
const learningsDir = path.join(projectRoot, '.claude', 'learnings');
|
|
751
|
+
if (!(await fileExists(learningsDir))) {
|
|
752
|
+
return [
|
|
753
|
+
result(WARN, 'Learnings directory', '`.claude/learnings/` not found — run worclaude upgrade'),
|
|
754
|
+
];
|
|
755
|
+
}
|
|
756
|
+
const indexPath = path.join(learningsDir, 'index.json');
|
|
757
|
+
if (!(await fileExists(indexPath))) {
|
|
758
|
+
return [result(PASS, 'Learnings: 0 entries captured', null)];
|
|
759
|
+
}
|
|
760
|
+
let index;
|
|
761
|
+
try {
|
|
762
|
+
index = JSON.parse(await readFile(indexPath));
|
|
763
|
+
} catch {
|
|
764
|
+
return [
|
|
765
|
+
result(FAIL, 'Learnings index', '`.claude/learnings/index.json` contains invalid JSON'),
|
|
766
|
+
];
|
|
767
|
+
}
|
|
768
|
+
const entries = Array.isArray(index?.learnings) ? index.learnings : [];
|
|
769
|
+
const orphans = [];
|
|
770
|
+
for (const entry of entries) {
|
|
771
|
+
if (entry?.file && !(await fileExists(path.join(learningsDir, entry.file)))) {
|
|
772
|
+
orphans.push(entry.file);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
if (orphans.length > 0) {
|
|
776
|
+
return orphans.map((f) =>
|
|
777
|
+
result(WARN, `Learnings entry: ${f}`, `Entry references missing file '${f}'`)
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
return [result(PASS, `Learnings: ${entries.length} entries captured`, null)];
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function isPathIgnored(projectRoot, relPath) {
|
|
784
|
+
try {
|
|
785
|
+
const r = spawnSync('git', ['check-ignore', '-q', relPath], { cwd: projectRoot });
|
|
786
|
+
if (r.error) return null;
|
|
787
|
+
// status 0 = ignored, 1 = not ignored, 128 = not a git repo
|
|
788
|
+
if (r.status === 128) return null;
|
|
789
|
+
return r.status === 0;
|
|
497
790
|
} catch {
|
|
498
|
-
|
|
791
|
+
return null;
|
|
499
792
|
}
|
|
793
|
+
}
|
|
500
794
|
|
|
795
|
+
async function checkGitignore(projectRoot) {
|
|
796
|
+
const checks = [
|
|
797
|
+
{
|
|
798
|
+
path: '.claude/sessions/',
|
|
799
|
+
detail: 'Session files contain local context and should be gitignored',
|
|
800
|
+
},
|
|
801
|
+
{
|
|
802
|
+
path: '.claude/learnings/',
|
|
803
|
+
detail: 'Learnings are personal and should be gitignored',
|
|
804
|
+
},
|
|
805
|
+
];
|
|
806
|
+
const results = [];
|
|
807
|
+
for (const c of checks) {
|
|
808
|
+
const ignored = isPathIgnored(projectRoot, c.path);
|
|
809
|
+
if (ignored === null) {
|
|
810
|
+
return [
|
|
811
|
+
result(
|
|
812
|
+
WARN,
|
|
813
|
+
'Gitignore check',
|
|
814
|
+
'git check-ignore unavailable — install git or initialize the repo'
|
|
815
|
+
),
|
|
816
|
+
];
|
|
817
|
+
}
|
|
818
|
+
results.push(
|
|
819
|
+
ignored
|
|
820
|
+
? result(PASS, `Gitignore: ${c.path}`, null)
|
|
821
|
+
: result(WARN, `Gitignore: ${c.path}`, c.detail)
|
|
822
|
+
);
|
|
823
|
+
}
|
|
501
824
|
return results;
|
|
502
825
|
}
|
|
503
826
|
|
|
@@ -527,51 +850,131 @@ async function checkPendingReviewFiles(projectRoot) {
|
|
|
527
850
|
return pending.map((f) => result(WARN, `Pending review: ${f}`, 'Merge or delete this file'));
|
|
528
851
|
}
|
|
529
852
|
|
|
530
|
-
|
|
853
|
+
function stripAnsi(s) {
|
|
854
|
+
if (typeof s !== 'string') return s;
|
|
855
|
+
return s.replace(/\u001b\[[0-9;]*m/g, '');
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
export async function doctorCommand(options = {}) {
|
|
531
859
|
const projectRoot = process.cwd();
|
|
532
860
|
const version = await getPackageVersion();
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
861
|
+
const jsonMode = !!options?.json;
|
|
862
|
+
|
|
863
|
+
const allResults = [];
|
|
864
|
+
const record = (category, r) => {
|
|
865
|
+
const items = Array.isArray(r) ? r : r ? [r] : [];
|
|
866
|
+
for (const item of items) {
|
|
867
|
+
allResults.push({ category, ...item });
|
|
868
|
+
if (!jsonMode) printResult(item);
|
|
869
|
+
}
|
|
870
|
+
};
|
|
871
|
+
const section = (title) => {
|
|
872
|
+
if (!jsonMode) display.barLine(display.white(title));
|
|
873
|
+
};
|
|
874
|
+
const spacer = () => {
|
|
875
|
+
if (!jsonMode) display.newline();
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
if (!jsonMode) {
|
|
879
|
+
display.newline();
|
|
880
|
+
display.sectionHeader('WORCLAUDE DOCTOR');
|
|
881
|
+
display.dim(`CLI version: v${version}`);
|
|
882
|
+
display.newline();
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Core Files
|
|
886
|
+
section('Core Files');
|
|
541
887
|
const metaResult = await checkWorkflowMeta(projectRoot);
|
|
542
|
-
|
|
888
|
+
record('core', metaResult);
|
|
543
889
|
|
|
544
890
|
const meta = await readWorkflowMeta(projectRoot);
|
|
545
891
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
892
|
+
record('core', await checkClaudeMd(projectRoot));
|
|
893
|
+
record('core', await checkClaudeMdSize(projectRoot));
|
|
894
|
+
record('core', await checkClaudeMdLineCount(projectRoot));
|
|
895
|
+
record('core', await checkClaudeMdSections(projectRoot));
|
|
896
|
+
record('core', await checkClaudeMdMemoryGuidance(projectRoot));
|
|
897
|
+
record('core', await checkAgentsMd(projectRoot));
|
|
898
|
+
record('core', await checkSettingsJson(projectRoot));
|
|
899
|
+
record('core', await checkSessions(projectRoot));
|
|
900
|
+
spacer();
|
|
901
|
+
|
|
902
|
+
// Hooks
|
|
903
|
+
section('Hooks');
|
|
904
|
+
record('hooks', await checkHookEventNames(projectRoot));
|
|
905
|
+
record('hooks', await checkKeyHookCoverage(projectRoot));
|
|
906
|
+
record('hooks', await checkHookScriptFiles(projectRoot));
|
|
907
|
+
record('hooks', await checkHookAsync(projectRoot));
|
|
908
|
+
spacer();
|
|
552
909
|
|
|
553
910
|
// Components
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
911
|
+
section('Components');
|
|
912
|
+
record('components', await checkAgents(projectRoot, meta));
|
|
913
|
+
record('components', await checkAgentDescription(projectRoot));
|
|
914
|
+
record('components', await checkCommands(projectRoot));
|
|
915
|
+
record('components', await checkSkills(projectRoot));
|
|
916
|
+
record('components', await checkSkillFormat(projectRoot));
|
|
917
|
+
record('components', await checkAgentCompleteness(projectRoot));
|
|
918
|
+
record('components', await checkAgentModels(projectRoot));
|
|
919
|
+
spacer();
|
|
920
|
+
|
|
921
|
+
// Documentation
|
|
922
|
+
section('Documentation');
|
|
923
|
+
record('docs', await checkDocSpecs(projectRoot));
|
|
924
|
+
spacer();
|
|
925
|
+
|
|
926
|
+
// Learnings
|
|
927
|
+
section('Learnings');
|
|
928
|
+
record('learnings', await checkLearnings(projectRoot));
|
|
929
|
+
spacer();
|
|
930
|
+
|
|
931
|
+
// Git Integration
|
|
932
|
+
section('Git Integration');
|
|
933
|
+
record('git', await checkGitignore(projectRoot));
|
|
934
|
+
spacer();
|
|
567
935
|
|
|
568
936
|
// Integrity
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
937
|
+
section('Integrity');
|
|
938
|
+
record('integrity', await checkHashIntegrity(projectRoot, meta));
|
|
939
|
+
record('integrity', await checkPendingReviewFiles(projectRoot));
|
|
940
|
+
spacer();
|
|
941
|
+
|
|
942
|
+
// Exit code
|
|
943
|
+
const fails = allResults.filter((r) => r.status === FAIL).length;
|
|
944
|
+
const warns = allResults.filter((r) => r.status === WARN).length;
|
|
945
|
+
process.exitCode = fails > 0 ? 2 : warns > 0 ? 1 : 0;
|
|
946
|
+
|
|
947
|
+
if (jsonMode) {
|
|
948
|
+
const installed = metaResult.status === PASS;
|
|
949
|
+
const summary = { pass: 0, warn: 0, fail: 0 };
|
|
950
|
+
const checks = allResults.map((r) => {
|
|
951
|
+
summary[r.status] += 1;
|
|
952
|
+
const out = {
|
|
953
|
+
category: r.category,
|
|
954
|
+
status: r.status,
|
|
955
|
+
label: stripAnsi(r.label),
|
|
956
|
+
};
|
|
957
|
+
if (r.detail) out.detail = stripAnsi(r.detail);
|
|
958
|
+
return out;
|
|
959
|
+
});
|
|
960
|
+
console.log(
|
|
961
|
+
JSON.stringify(
|
|
962
|
+
{
|
|
963
|
+
version,
|
|
964
|
+
path: projectRoot,
|
|
965
|
+
timestamp: new Date().toISOString(),
|
|
966
|
+
installed,
|
|
967
|
+
summary,
|
|
968
|
+
checks,
|
|
969
|
+
},
|
|
970
|
+
null,
|
|
971
|
+
2
|
|
972
|
+
)
|
|
973
|
+
);
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
573
976
|
|
|
574
|
-
// Summary
|
|
977
|
+
// Summary (text mode)
|
|
575
978
|
if (metaResult.status === FAIL) {
|
|
576
979
|
display.info('Workflow is not installed. Run `worclaude init` to set up.');
|
|
577
980
|
} else {
|