worclaude 2.2.5 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +102 -74
  3. package/package.json +7 -2
  4. package/src/commands/doctor.js +507 -104
  5. package/src/commands/init.js +103 -19
  6. package/src/core/detector.js +34 -10
  7. package/src/core/merger.js +48 -3
  8. package/src/core/scaffolder.js +58 -1
  9. package/src/data/agents.js +5 -0
  10. package/src/index.js +2 -1
  11. package/src/prompts/claude-md-merge.js +5 -0
  12. package/templates/agents/universal/build-validator.md +23 -0
  13. package/templates/agents/universal/code-simplifier.md +11 -0
  14. package/templates/agents/universal/plan-reviewer.md +39 -0
  15. package/templates/agents/universal/test-writer.md +25 -0
  16. package/templates/agents/universal/verify-app.md +12 -0
  17. package/templates/commands/build-fix.md +5 -0
  18. package/templates/commands/commit-push-pr.md +6 -0
  19. package/templates/commands/compact-safe.md +5 -0
  20. package/templates/commands/conflict-resolver.md +5 -0
  21. package/templates/commands/end.md +10 -0
  22. package/templates/commands/learn.md +33 -0
  23. package/templates/commands/refactor-clean.md +10 -0
  24. package/templates/commands/review-changes.md +5 -0
  25. package/templates/commands/review-plan.md +5 -0
  26. package/templates/commands/setup.md +5 -0
  27. package/templates/commands/start.md +10 -0
  28. package/templates/commands/status.md +5 -0
  29. package/templates/commands/sync.md +5 -0
  30. package/templates/commands/techdebt.md +5 -0
  31. package/templates/commands/test-coverage.md +5 -0
  32. package/templates/commands/update-claude-md.md +5 -0
  33. package/templates/commands/verify.md +11 -0
  34. package/templates/core/agents-md.md +25 -0
  35. package/templates/core/claude-md.md +17 -0
  36. package/templates/hooks/README.md +106 -0
  37. package/templates/hooks/correction-detect.cjs +48 -0
  38. package/templates/hooks/examples/prompt-hook-commit-validator.json +16 -0
  39. package/templates/hooks/learn-capture.cjs +179 -0
  40. package/templates/hooks/pre-compact-save.cjs +60 -0
  41. package/templates/hooks/skill-hint.cjs +66 -0
  42. package/templates/memory/decisions.md +11 -0
  43. package/templates/memory/preferences.md +17 -0
  44. package/templates/settings/base.json +56 -8
  45. package/templates/skills/templates/backend-conventions.md +1 -0
  46. package/templates/skills/templates/frontend-design-system.md +1 -0
  47. package/templates/skills/templates/project-patterns.md +1 -0
  48. package/templates/skills/universal/coding-principles.md +60 -0
  49. package/templates/skills/universal/context-management.md +21 -0
  50. package/templates/skills/universal/planning-with-files.md +11 -0
  51. package/templates/skills/universal/verification.md +18 -0
@@ -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
- async function checkClaudeMdSize(projectRoot) {
77
- const claudeMdPath = path.join(projectRoot, 'CLAUDE.md');
78
- if (!(await fileExists(claudeMdPath))) {
79
- return []; // Already covered by existing checkClaudeMd
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
- const content = await readFile(claudeMdPath);
83
- const charCount = content.length;
84
- const WARN_THRESHOLD = 30000;
85
- const FAIL_THRESHOLD = 38000;
86
- const HARD_LIMIT = 40000;
130
+ return await readFile(path.join(projectRoot, 'CLAUDE.md'));
131
+ } catch {
132
+ return null;
133
+ }
134
+ }
87
135
 
88
- if (charCount > FAIL_THRESHOLD) {
89
- return [
90
- result(
91
- FAIL,
92
- `CLAUDE.md size: ${charCount.toLocaleString()} chars`,
93
- `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.`
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
- PASS,
109
- `CLAUDE.md size: ${charCount.toLocaleString()} chars (limit: ${HARD_LIMIT.toLocaleString()})`,
110
- null
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 results = [];
454
-
455
- try {
456
- const content = await readFile(claudeMdPath);
457
- if (content.length < SECTION_THRESHOLD) return results;
458
-
459
- // Split into ## sections
460
- const sections = [];
461
- const lines = content.split('\n');
462
- let currentHeading = '(top-level)';
463
- let currentLines = [];
464
-
465
- for (const line of lines) {
466
- const headingMatch = line.match(/^##\s+(.+)/);
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
- if (currentLines.length > 0) {
478
- sections.push({ heading: currentHeading, size: currentLines.join('\n').length });
479
- }
691
+ }
692
+ if (currentLines.length > 0) {
693
+ sections.push({ heading: currentHeading, size: currentLines.join('\n').length });
694
+ }
480
695
 
481
- // Sort by size, suggest extracting the top 3 sections > 2KB
482
- const large = sections.filter((s) => s.size > 2000).sort((a, b) => b.size - a.size);
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
- if (large.length > 0) {
485
- const top = large.slice(0, 3);
486
- const sectionList = top
487
- .map((s) => `"${s.heading}" (${(s.size / 1024).toFixed(1)}KB)`)
488
- .join(', ');
489
- results.push(
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
- `CLAUDE.md has large sections: ${sectionList}`,
493
- 'Consider extracting to conditional skills with paths frontmatter to save context budget'
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
- // Already covered by checkClaudeMd
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
- export async function doctorCommand() {
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
- display.newline();
535
- display.sectionHeader('WORCLAUDE DOCTOR');
536
- display.dim(`CLI version: v${version}`);
537
- display.newline();
538
-
539
- // Core files
540
- display.barLine(display.white('Core Files'));
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
- printResult(metaResult);
888
+ record('core', metaResult);
543
889
 
544
890
  const meta = await readWorkflowMeta(projectRoot);
545
891
 
546
- printResult(await checkClaudeMd(projectRoot));
547
- for (const r of await checkClaudeMdSize(projectRoot)) printResult(r);
548
- for (const r of await checkClaudeMdSections(projectRoot)) printResult(r);
549
- printResult(await checkSettingsJson(projectRoot));
550
- printResult(await checkSessions(projectRoot));
551
- display.newline();
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
- display.barLine(display.white('Components'));
555
- for (const r of await checkAgents(projectRoot, meta)) printResult(r);
556
- for (const r of await checkAgentDescription(projectRoot)) printResult(r);
557
- for (const r of await checkCommands(projectRoot)) printResult(r);
558
- for (const r of await checkSkills(projectRoot)) printResult(r);
559
- for (const r of await checkSkillFormat(projectRoot)) printResult(r);
560
- for (const r of await checkAgentCompleteness(projectRoot)) printResult(r);
561
- display.newline();
562
-
563
- // Docs
564
- display.barLine(display.white('Documentation'));
565
- for (const r of await checkDocSpecs(projectRoot)) printResult(r);
566
- display.newline();
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
- display.barLine(display.white('Integrity'));
570
- for (const r of await checkHashIntegrity(projectRoot, meta)) printResult(r);
571
- for (const r of await checkPendingReviewFiles(projectRoot)) printResult(r);
572
- display.newline();
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 {