worclaude 1.8.0 → 2.0.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 +11 -7
- package/package.json +1 -1
- package/src/commands/backup.js +5 -3
- package/src/commands/doctor.js +463 -0
- package/src/commands/init.js +42 -17
- package/src/commands/upgrade.js +52 -11
- package/src/core/config.js +19 -1
- package/src/core/detector.js +2 -1
- package/src/core/file-categorizer.js +4 -4
- package/src/core/merger.js +50 -30
- package/src/core/migration.js +144 -0
- package/src/core/remover.js +9 -2
- package/src/core/scaffolder.js +26 -5
- package/src/data/agents.js +2 -0
- package/src/index.js +6 -0
- package/src/utils/file.js +21 -0
- package/templates/agents/optional/backend/api-designer.md +6 -0
- package/templates/agents/optional/backend/auth-auditor.md +2 -0
- package/templates/agents/optional/backend/database-analyst.md +7 -0
- package/templates/agents/optional/data/data-pipeline-reviewer.md +7 -0
- package/templates/agents/optional/data/ml-experiment-tracker.md +7 -0
- package/templates/agents/optional/data/prompt-engineer.md +2 -0
- package/templates/agents/optional/devops/ci-fixer.md +2 -0
- package/templates/agents/optional/devops/dependency-manager.md +7 -0
- package/templates/agents/optional/devops/deploy-validator.md +7 -0
- package/templates/agents/optional/devops/docker-helper.md +2 -0
- package/templates/agents/optional/docs/changelog-generator.md +7 -0
- package/templates/agents/optional/docs/doc-writer.md +3 -0
- package/templates/agents/optional/frontend/style-enforcer.md +2 -0
- package/templates/agents/optional/frontend/ui-reviewer.md +7 -0
- package/templates/agents/optional/quality/bug-fixer.md +2 -0
- package/templates/agents/optional/quality/build-fixer.md +2 -0
- package/templates/agents/optional/quality/e2e-runner.md +3 -0
- package/templates/agents/optional/quality/performance-auditor.md +8 -0
- package/templates/agents/optional/quality/refactorer.md +2 -0
- package/templates/agents/optional/quality/security-reviewer.md +9 -0
- package/templates/agents/universal/build-validator.md +3 -0
- package/templates/agents/universal/code-simplifier.md +2 -0
- package/templates/agents/universal/plan-reviewer.md +8 -0
- package/templates/agents/universal/test-writer.md +4 -1
- package/templates/agents/universal/verify-app.md +44 -0
- package/templates/commands/build-fix.md +4 -0
- package/templates/commands/commit-push-pr.md +42 -9
- package/templates/commands/compact-safe.md +4 -0
- package/templates/commands/conflict-resolver.md +4 -0
- package/templates/commands/end.md +20 -3
- package/templates/commands/refactor-clean.md +43 -31
- package/templates/commands/review-changes.md +4 -0
- package/templates/commands/review-plan.md +4 -0
- package/templates/commands/setup.md +7 -3
- package/templates/commands/start.md +71 -7
- package/templates/commands/status.md +4 -0
- package/templates/commands/sync.md +4 -0
- package/templates/commands/techdebt.md +4 -0
- package/templates/commands/test-coverage.md +4 -0
- package/templates/commands/update-claude-md.md +4 -0
- package/templates/commands/verify.md +4 -0
- package/templates/core/claude-md.md +13 -11
- package/templates/core/memory-md.md +33 -0
- package/templates/settings/base.json +18 -2
- package/templates/skills/templates/backend-conventions.md +6 -0
- package/templates/skills/templates/frontend-design-system.md +10 -0
- package/templates/skills/templates/project-patterns.md +4 -0
- package/templates/skills/universal/claude-md-maintenance.md +1 -0
- package/templates/skills/universal/context-management.md +56 -0
- package/templates/skills/universal/coordinator-mode.md +77 -0
- package/templates/skills/universal/git-conventions.md +1 -0
- package/templates/skills/universal/planning-with-files.md +1 -0
- package/templates/skills/universal/prompt-engineering.md +1 -0
- package/templates/skills/universal/review-and-handoff.md +1 -0
- package/templates/skills/universal/security-checklist.md +7 -0
- package/templates/skills/universal/subagent-usage.md +1 -0
- package/templates/skills/universal/testing.md +7 -0
- package/templates/skills/universal/verification.md +6 -0
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
[Full Documentation](https://sefaertunc.github.io/Worclaude/) · [Interactive Demo](https://sefaertunc.github.io/Worclaude/demo/) · [npm](https://www.npmjs.com/package/worclaude)
|
|
12
12
|
|
|
13
|
-
Worclaude scaffolds a complete Claude Code workflow into any project in seconds. It implements all [tips by Boris Cherny](https://www.howborisusesclaudecode.com/) — the creator of Claude Code at Anthropic — as a reusable, upgradable scaffold. One `init` command gives you
|
|
13
|
+
Worclaude scaffolds a complete Claude Code workflow into any project in seconds. It implements all [tips by Boris Cherny](https://www.howborisusesclaudecode.com/) — the creator of Claude Code at Anthropic — as a reusable, upgradable scaffold. One `init` command gives you 25 agents, 16 slash commands, 14 skills, hooks, permissions, and a CLAUDE.md template tuned for your tech stack. Whether you're starting fresh or adding structure to an existing project, Worclaude handles the setup so you can focus on building.
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
@@ -18,25 +18,27 @@ Worclaude scaffolds a complete Claude Code workflow into any project in seconds.
|
|
|
18
18
|
|
|
19
19
|
`worclaude init` installs a production-ready Claude Code workflow:
|
|
20
20
|
|
|
21
|
-
**Agents (
|
|
21
|
+
**Agents (25 total)**
|
|
22
22
|
|
|
23
23
|
- 5 universal: plan-reviewer, code-simplifier, test-writer, build-validator, verify-app
|
|
24
|
-
-
|
|
24
|
+
- 20 optional across 6 categories: Backend, Frontend, DevOps, Quality, Documentation, Data/AI
|
|
25
25
|
|
|
26
|
-
**Slash Commands (
|
|
27
|
-
`/start` `/end` `/commit-push-pr` `/review-plan` `/techdebt` `/verify` `/compact-safe` `/status` `/update-claude-md` `/setup` `/sync` `/conflict-resolver`
|
|
26
|
+
**Slash Commands (16)**
|
|
27
|
+
`/start` `/end` `/commit-push-pr` `/review-plan` `/techdebt` `/verify` `/compact-safe` `/status` `/update-claude-md` `/setup` `/sync` `/conflict-resolver` `/review-changes` `/build-fix` `/refactor-clean` `/test-coverage`
|
|
28
28
|
|
|
29
|
-
**Skills (
|
|
29
|
+
**Skills (14)**
|
|
30
30
|
|
|
31
|
-
-
|
|
31
|
+
- 10 universal knowledge files (testing, git conventions, context management, security, and more)
|
|
32
32
|
- 3 project-specific templates filled in by `/setup`
|
|
33
33
|
- 1 generated agent routing guide (dynamically built from your agent selection)
|
|
34
34
|
|
|
35
35
|
**Hooks**
|
|
36
36
|
|
|
37
|
+
- SessionStart context injection (auto-loads CLAUDE.md, PROGRESS.md, and last session on launch)
|
|
37
38
|
- PostToolUse formatter (auto-formats on every write)
|
|
38
39
|
- PostCompact re-injection (re-reads key files after compaction)
|
|
39
40
|
- Stop notifications (desktop alert when Claude finishes)
|
|
41
|
+
- Hook profiles (`WORCLAUDE_HOOK_PROFILE`) — minimal, standard, or strict
|
|
40
42
|
|
|
41
43
|
**Configuration**
|
|
42
44
|
|
|
@@ -70,6 +72,8 @@ For parallel tasks, run Claude with worktrees: `claude --worktree --tmux`
|
|
|
70
72
|
| `worclaude backup` | Create timestamped backup of workflow files |
|
|
71
73
|
| `worclaude restore` | Restore from a previous backup |
|
|
72
74
|
| `worclaude diff` | Compare current setup vs latest version |
|
|
75
|
+
| `worclaude delete` | Remove worclaude workflow from project |
|
|
76
|
+
| `worclaude doctor` | Validate workflow installation health |
|
|
73
77
|
|
|
74
78
|
The `init` command detects existing setups and merges intelligently — no data is overwritten without your confirmation. Use `upgrade` to pull in new features while preserving your customizations.
|
|
75
79
|
|
package/package.json
CHANGED
package/src/commands/backup.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import { createBackup } from '../core/backup.js';
|
|
4
|
-
import { fileExists, dirExists, listFiles } from '../utils/file.js';
|
|
4
|
+
import { fileExists, dirExists, listFiles, listSkillDirs } from '../utils/file.js';
|
|
5
5
|
import * as display from '../utils/display.js';
|
|
6
6
|
|
|
7
7
|
export async function backupCommand() {
|
|
@@ -33,11 +33,13 @@ export async function backupCommand() {
|
|
|
33
33
|
if (await dirExists(claudeBackup)) {
|
|
34
34
|
const agents = await listFiles(path.join(claudeBackup, 'agents'));
|
|
35
35
|
const commands = await listFiles(path.join(claudeBackup, 'commands'));
|
|
36
|
-
const
|
|
36
|
+
const skillFiles = await listFiles(path.join(claudeBackup, 'skills'));
|
|
37
|
+
const skillDirs = await listSkillDirs(path.join(claudeBackup, 'skills'));
|
|
38
|
+
const skillCount = skillDirs.length + skillFiles.length;
|
|
37
39
|
const parts = [];
|
|
38
40
|
if (agents.length > 0) parts.push(`${agents.length} agents`);
|
|
39
41
|
if (commands.length > 0) parts.push(`${commands.length} commands`);
|
|
40
|
-
if (
|
|
42
|
+
if (skillCount > 0) parts.push(`${skillCount} skills`);
|
|
41
43
|
contents.push(`.claude/ (${parts.join(', ')})`);
|
|
42
44
|
}
|
|
43
45
|
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { readWorkflowMeta, workflowMetaExists, getPackageVersion } from '../core/config.js';
|
|
4
|
+
import { hashFile } from '../utils/hash.js';
|
|
5
|
+
import { fileExists, readFile, listFilesRecursive } from '../utils/file.js';
|
|
6
|
+
import {
|
|
7
|
+
UNIVERSAL_AGENTS,
|
|
8
|
+
COMMAND_FILES,
|
|
9
|
+
UNIVERSAL_SKILLS,
|
|
10
|
+
TEMPLATE_SKILLS,
|
|
11
|
+
} from '../data/agents.js';
|
|
12
|
+
import * as display from '../utils/display.js';
|
|
13
|
+
|
|
14
|
+
// Check categories
|
|
15
|
+
const PASS = 'pass';
|
|
16
|
+
const WARN = 'warn';
|
|
17
|
+
const FAIL = 'fail';
|
|
18
|
+
|
|
19
|
+
function result(status, label, detail) {
|
|
20
|
+
return { status, label, detail };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function printResult(r) {
|
|
24
|
+
const icon =
|
|
25
|
+
r.status === PASS
|
|
26
|
+
? display.green('✓')
|
|
27
|
+
: r.status === WARN
|
|
28
|
+
? display.yellow('⚠')
|
|
29
|
+
: display.red('✗');
|
|
30
|
+
const text = r.status === PASS ? display.dimColor(r.label) : display.white(r.label);
|
|
31
|
+
console.log(` ${icon} ${text}`);
|
|
32
|
+
if (r.detail && r.status !== PASS) {
|
|
33
|
+
console.log(` ${display.dimColor(r.detail)}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function checkWorkflowMeta(projectRoot) {
|
|
38
|
+
if (!(await workflowMetaExists(projectRoot))) {
|
|
39
|
+
return result(FAIL, 'workflow-meta.json', 'Missing — run `worclaude init` to create');
|
|
40
|
+
}
|
|
41
|
+
const meta = await readWorkflowMeta(projectRoot);
|
|
42
|
+
if (!meta) {
|
|
43
|
+
return result(FAIL, 'workflow-meta.json', 'Exists but contains invalid JSON');
|
|
44
|
+
}
|
|
45
|
+
if (!meta.version || !meta.projectTypes || !meta.techStack) {
|
|
46
|
+
return result(
|
|
47
|
+
WARN,
|
|
48
|
+
'workflow-meta.json',
|
|
49
|
+
'Missing required fields (version, projectTypes, or techStack)'
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return result(PASS, 'workflow-meta.json', null);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function checkClaudeMd(projectRoot) {
|
|
56
|
+
const claudeMdPath = path.join(projectRoot, 'CLAUDE.md');
|
|
57
|
+
if (!(await fileExists(claudeMdPath))) {
|
|
58
|
+
return result(FAIL, 'CLAUDE.md', 'Missing — run `worclaude init` to create');
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const content = await readFile(claudeMdPath);
|
|
62
|
+
const lines = content.split('\n').length;
|
|
63
|
+
if (lines < 10) {
|
|
64
|
+
return result(
|
|
65
|
+
WARN,
|
|
66
|
+
'CLAUDE.md',
|
|
67
|
+
`Only ${lines} lines — may be a stub. Run /setup to fill it in.`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return result(PASS, 'CLAUDE.md', null);
|
|
71
|
+
} catch {
|
|
72
|
+
return result(FAIL, 'CLAUDE.md', 'Exists but could not be read');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
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
|
+
}
|
|
81
|
+
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;
|
|
87
|
+
|
|
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
|
+
}
|
|
106
|
+
return [
|
|
107
|
+
result(
|
|
108
|
+
PASS,
|
|
109
|
+
`CLAUDE.md size: ${charCount.toLocaleString()} chars (limit: ${HARD_LIMIT.toLocaleString()})`,
|
|
110
|
+
null
|
|
111
|
+
),
|
|
112
|
+
];
|
|
113
|
+
} catch {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function checkSettingsJson(projectRoot) {
|
|
119
|
+
const settingsPath = path.join(projectRoot, '.claude', 'settings.json');
|
|
120
|
+
if (!(await fileExists(settingsPath))) {
|
|
121
|
+
return result(FAIL, 'settings.json', 'Missing — run `worclaude init` to create');
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
const raw = await readFile(settingsPath);
|
|
125
|
+
const settings = JSON.parse(raw);
|
|
126
|
+
|
|
127
|
+
const issues = [];
|
|
128
|
+
if (!settings.permissions?.allow || settings.permissions.allow.length === 0) {
|
|
129
|
+
issues.push('no permissions configured');
|
|
130
|
+
}
|
|
131
|
+
if (!settings.hooks || Object.keys(settings.hooks).length === 0) {
|
|
132
|
+
issues.push('no hooks configured');
|
|
133
|
+
}
|
|
134
|
+
if (!settings.hooks?.PostCompact) {
|
|
135
|
+
issues.push('missing PostCompact hook (context recovery after compaction)');
|
|
136
|
+
}
|
|
137
|
+
if (!settings.hooks?.SessionStart) {
|
|
138
|
+
issues.push('missing SessionStart hook (session persistence)');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (issues.length > 0) {
|
|
142
|
+
return result(WARN, 'settings.json', issues.join('; '));
|
|
143
|
+
}
|
|
144
|
+
return result(PASS, 'settings.json', null);
|
|
145
|
+
} catch {
|
|
146
|
+
return result(FAIL, 'settings.json', 'Contains invalid JSON');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function checkAgents(projectRoot, meta) {
|
|
151
|
+
const agentsDir = path.join(projectRoot, '.claude', 'agents');
|
|
152
|
+
const results = [];
|
|
153
|
+
|
|
154
|
+
// Check universal agents
|
|
155
|
+
for (const agent of UNIVERSAL_AGENTS) {
|
|
156
|
+
const agentPath = path.join(agentsDir, `${agent}.md`);
|
|
157
|
+
if (!(await fileExists(agentPath))) {
|
|
158
|
+
results.push(result(FAIL, `agents/${agent}.md`, 'Missing universal agent'));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Check selected optional agents from meta
|
|
163
|
+
if (meta?.optionalAgents) {
|
|
164
|
+
for (const agent of meta.optionalAgents) {
|
|
165
|
+
const agentPath = path.join(agentsDir, `${agent}.md`);
|
|
166
|
+
if (!(await fileExists(agentPath))) {
|
|
167
|
+
results.push(result(WARN, `agents/${agent}.md`, 'Selected optional agent is missing'));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (results.length === 0) {
|
|
173
|
+
const totalExpected = UNIVERSAL_AGENTS.length + (meta?.optionalAgents?.length || 0);
|
|
174
|
+
results.push(result(PASS, `agents/ (${totalExpected} expected, all present)`, null));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return results;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function checkAgentDescription(projectRoot) {
|
|
181
|
+
const agentsDir = path.join(projectRoot, '.claude', 'agents');
|
|
182
|
+
const results = [];
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const entries = await fs.readdir(agentsDir, { withFileTypes: true });
|
|
186
|
+
const mdFiles = entries.filter((e) => e.isFile() && e.name.endsWith('.md'));
|
|
187
|
+
|
|
188
|
+
for (const file of mdFiles) {
|
|
189
|
+
const filePath = path.join(agentsDir, file.name);
|
|
190
|
+
const content = await readFile(filePath);
|
|
191
|
+
|
|
192
|
+
// Parse YAML frontmatter
|
|
193
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
194
|
+
if (!frontmatterMatch) {
|
|
195
|
+
results.push(
|
|
196
|
+
result(
|
|
197
|
+
FAIL,
|
|
198
|
+
`agents/${file.name}`,
|
|
199
|
+
'No YAML frontmatter — agent is invisible to Claude Code'
|
|
200
|
+
)
|
|
201
|
+
);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const frontmatter = frontmatterMatch[1];
|
|
206
|
+
const hasName = /^name:\s*.+/m.test(frontmatter);
|
|
207
|
+
const hasDescription = /^description:\s*.+/m.test(frontmatter);
|
|
208
|
+
|
|
209
|
+
if (!hasName) {
|
|
210
|
+
results.push(
|
|
211
|
+
result(FAIL, `agents/${file.name}`, 'Missing required "name" field in frontmatter')
|
|
212
|
+
);
|
|
213
|
+
} else if (!hasDescription) {
|
|
214
|
+
results.push(
|
|
215
|
+
result(
|
|
216
|
+
FAIL,
|
|
217
|
+
`agents/${file.name}`,
|
|
218
|
+
'Missing required "description" field — agent is invisible to Claude Code\'s /agents and routing'
|
|
219
|
+
)
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (results.length === 0 && mdFiles.length > 0) {
|
|
225
|
+
results.push(
|
|
226
|
+
result(PASS, `agents/ frontmatter (${mdFiles.length} agents have required fields)`, null)
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
} catch {
|
|
230
|
+
// agents/ doesn't exist — covered by existing component checks
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return results;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function checkCommands(projectRoot) {
|
|
237
|
+
const commandsDir = path.join(projectRoot, '.claude', 'commands');
|
|
238
|
+
const missing = [];
|
|
239
|
+
|
|
240
|
+
for (const cmd of COMMAND_FILES) {
|
|
241
|
+
const cmdPath = path.join(commandsDir, `${cmd}.md`);
|
|
242
|
+
if (!(await fileExists(cmdPath))) {
|
|
243
|
+
missing.push(cmd);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (missing.length === 0) {
|
|
248
|
+
return [result(PASS, `commands/ (${COMMAND_FILES.length} expected, all present)`, null)];
|
|
249
|
+
}
|
|
250
|
+
return missing.map((cmd) => result(WARN, `commands/${cmd}.md`, 'Missing command'));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function checkSkills(projectRoot) {
|
|
254
|
+
const skillsDir = path.join(projectRoot, '.claude', 'skills');
|
|
255
|
+
const missing = [];
|
|
256
|
+
const allExpected = [...UNIVERSAL_SKILLS, ...TEMPLATE_SKILLS, 'agent-routing'];
|
|
257
|
+
|
|
258
|
+
for (const skill of allExpected) {
|
|
259
|
+
const skillPath = path.join(skillsDir, skill, 'SKILL.md');
|
|
260
|
+
if (!(await fileExists(skillPath))) {
|
|
261
|
+
missing.push(skill);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (missing.length === 0) {
|
|
266
|
+
return [result(PASS, `skills/ (${allExpected.length} expected, all present)`, null)];
|
|
267
|
+
}
|
|
268
|
+
return missing.map((s) => result(WARN, `skills/${s}/SKILL.md`, 'Missing skill'));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function checkSkillFormat(projectRoot) {
|
|
272
|
+
const skillsDir = path.join(projectRoot, '.claude', 'skills');
|
|
273
|
+
const results = [];
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
const entries = await fs.readdir(skillsDir, { withFileTypes: true });
|
|
277
|
+
const flatMdFiles = entries
|
|
278
|
+
.filter((e) => e.isFile() && e.name.endsWith('.md'))
|
|
279
|
+
.map((e) => e.name);
|
|
280
|
+
|
|
281
|
+
if (flatMdFiles.length > 0) {
|
|
282
|
+
results.push(
|
|
283
|
+
result(
|
|
284
|
+
FAIL,
|
|
285
|
+
`skills/ has ${flatMdFiles.length} flat .md file(s)`,
|
|
286
|
+
`Flat .md files in .claude/skills/ are invisible to Claude Code. Expected format: skill-name/SKILL.md. Run \`worclaude upgrade\` to migrate. Files: ${flatMdFiles.join(', ')}`
|
|
287
|
+
)
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Also check directory-format skills exist
|
|
292
|
+
const skillDirs = entries.filter((e) => e.isDirectory());
|
|
293
|
+
let validDirSkills = 0;
|
|
294
|
+
for (const dir of skillDirs) {
|
|
295
|
+
const skillMd = path.join(skillsDir, dir.name, 'SKILL.md');
|
|
296
|
+
if (await fileExists(skillMd)) {
|
|
297
|
+
validDirSkills++;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (validDirSkills > 0 && flatMdFiles.length === 0) {
|
|
302
|
+
results.push(
|
|
303
|
+
result(PASS, `skills/ format (${validDirSkills} directory-format skills)`, null)
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
} catch {
|
|
307
|
+
// skills/ doesn't exist — covered by existing component checks
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return results;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function checkHashIntegrity(projectRoot, meta) {
|
|
314
|
+
if (!meta?.fileHashes || Object.keys(meta.fileHashes).length === 0) {
|
|
315
|
+
return [result(WARN, 'File integrity', 'No file hashes in workflow-meta.json — cannot verify')];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
let modified = 0;
|
|
319
|
+
let missing = 0;
|
|
320
|
+
let intact = 0;
|
|
321
|
+
|
|
322
|
+
for (const [relPath, storedHash] of Object.entries(meta.fileHashes)) {
|
|
323
|
+
const fullPath = path.join(projectRoot, '.claude', ...relPath.split('/'));
|
|
324
|
+
if (!(await fileExists(fullPath))) {
|
|
325
|
+
missing++;
|
|
326
|
+
} else {
|
|
327
|
+
const currentHash = await hashFile(fullPath);
|
|
328
|
+
if (currentHash !== storedHash) {
|
|
329
|
+
modified++;
|
|
330
|
+
} else {
|
|
331
|
+
intact++;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const total = Object.keys(meta.fileHashes).length;
|
|
337
|
+
const results = [];
|
|
338
|
+
|
|
339
|
+
if (missing > 0) {
|
|
340
|
+
results.push(result(FAIL, `File integrity: ${missing}/${total} files missing`, null));
|
|
341
|
+
}
|
|
342
|
+
if (modified > 0) {
|
|
343
|
+
results.push(
|
|
344
|
+
result(PASS, `File integrity: ${modified}/${total} files customized (expected)`, null)
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
if (results.length === 0) {
|
|
348
|
+
results.push(
|
|
349
|
+
result(PASS, `File integrity: all ${total} files present (${intact} intact)`, null)
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return results;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function checkSessions(projectRoot) {
|
|
357
|
+
const sessionsDir = path.join(projectRoot, '.claude', 'sessions');
|
|
358
|
+
if (!(await fileExists(sessionsDir))) {
|
|
359
|
+
return result(
|
|
360
|
+
WARN,
|
|
361
|
+
'sessions/',
|
|
362
|
+
"Directory missing — session persistence won't work. Run `worclaude init` or create .claude/sessions/ manually."
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
return result(PASS, 'sessions/', null);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function checkDocSpecs(projectRoot) {
|
|
369
|
+
const results = [];
|
|
370
|
+
const progressPath = path.join(projectRoot, 'docs', 'spec', 'PROGRESS.md');
|
|
371
|
+
const specPath = path.join(projectRoot, 'docs', 'spec', 'SPEC.md');
|
|
372
|
+
|
|
373
|
+
if (!(await fileExists(progressPath))) {
|
|
374
|
+
results.push(
|
|
375
|
+
result(WARN, 'docs/spec/PROGRESS.md', 'Missing — /start and /sync depend on this')
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
if (!(await fileExists(specPath))) {
|
|
379
|
+
results.push(result(WARN, 'docs/spec/SPEC.md', 'Missing — plan-reviewer references this'));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (results.length === 0) {
|
|
383
|
+
results.push(result(PASS, 'docs/spec/ (PROGRESS.md + SPEC.md present)', null));
|
|
384
|
+
}
|
|
385
|
+
return results;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async function checkPendingReviewFiles(projectRoot) {
|
|
389
|
+
const pending = [];
|
|
390
|
+
try {
|
|
391
|
+
const claudeDir = path.join(projectRoot, '.claude');
|
|
392
|
+
const allFiles = await listFilesRecursive(claudeDir);
|
|
393
|
+
for (const fp of allFiles) {
|
|
394
|
+
const rel = path.relative(claudeDir, fp).split(path.sep).join('/');
|
|
395
|
+
if (rel.endsWith('.workflow-ref.md')) {
|
|
396
|
+
pending.push(rel);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
} catch {
|
|
400
|
+
// .claude dir might not exist
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const suggestionsPath = path.join(projectRoot, 'CLAUDE.md.workflow-suggestions');
|
|
404
|
+
if (await fileExists(suggestionsPath)) {
|
|
405
|
+
pending.push('CLAUDE.md.workflow-suggestions');
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (pending.length === 0) {
|
|
409
|
+
return [result(PASS, 'No pending review files', null)];
|
|
410
|
+
}
|
|
411
|
+
return pending.map((f) => result(WARN, `Pending review: ${f}`, 'Merge or delete this file'));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export async function doctorCommand() {
|
|
415
|
+
const projectRoot = process.cwd();
|
|
416
|
+
const version = await getPackageVersion();
|
|
417
|
+
|
|
418
|
+
display.newline();
|
|
419
|
+
display.sectionHeader('WORCLAUDE DOCTOR');
|
|
420
|
+
display.dim(`CLI version: v${version}`);
|
|
421
|
+
display.newline();
|
|
422
|
+
|
|
423
|
+
// Core files
|
|
424
|
+
display.barLine(display.white('Core Files'));
|
|
425
|
+
const metaResult = await checkWorkflowMeta(projectRoot);
|
|
426
|
+
printResult(metaResult);
|
|
427
|
+
|
|
428
|
+
const meta = await readWorkflowMeta(projectRoot);
|
|
429
|
+
|
|
430
|
+
printResult(await checkClaudeMd(projectRoot));
|
|
431
|
+
for (const r of await checkClaudeMdSize(projectRoot)) printResult(r);
|
|
432
|
+
printResult(await checkSettingsJson(projectRoot));
|
|
433
|
+
printResult(await checkSessions(projectRoot));
|
|
434
|
+
display.newline();
|
|
435
|
+
|
|
436
|
+
// Components
|
|
437
|
+
display.barLine(display.white('Components'));
|
|
438
|
+
for (const r of await checkAgents(projectRoot, meta)) printResult(r);
|
|
439
|
+
for (const r of await checkAgentDescription(projectRoot)) printResult(r);
|
|
440
|
+
for (const r of await checkCommands(projectRoot)) printResult(r);
|
|
441
|
+
for (const r of await checkSkills(projectRoot)) printResult(r);
|
|
442
|
+
for (const r of await checkSkillFormat(projectRoot)) printResult(r);
|
|
443
|
+
display.newline();
|
|
444
|
+
|
|
445
|
+
// Docs
|
|
446
|
+
display.barLine(display.white('Documentation'));
|
|
447
|
+
for (const r of await checkDocSpecs(projectRoot)) printResult(r);
|
|
448
|
+
display.newline();
|
|
449
|
+
|
|
450
|
+
// Integrity
|
|
451
|
+
display.barLine(display.white('Integrity'));
|
|
452
|
+
for (const r of await checkHashIntegrity(projectRoot, meta)) printResult(r);
|
|
453
|
+
for (const r of await checkPendingReviewFiles(projectRoot)) printResult(r);
|
|
454
|
+
display.newline();
|
|
455
|
+
|
|
456
|
+
// Summary
|
|
457
|
+
if (metaResult.status === FAIL) {
|
|
458
|
+
display.error('Workflow is not installed. Run `worclaude init` to set up.');
|
|
459
|
+
} else {
|
|
460
|
+
display.success('Doctor complete. Review any warnings above.');
|
|
461
|
+
}
|
|
462
|
+
display.newline();
|
|
463
|
+
}
|