worclaude 1.9.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/package.json +1 -1
- package/src/commands/backup.js +5 -3
- package/src/commands/doctor.js +146 -2
- package/src/commands/init.js +34 -4
- package/src/commands/upgrade.js +46 -0
- package/src/core/detector.js +2 -1
- package/src/core/file-categorizer.js +4 -4
- package/src/core/merger.js +16 -14
- package/src/core/migration.js +144 -0
- package/src/core/remover.js +3 -1
- package/src/core/scaffolder.js +7 -1
- package/src/data/agents.js +2 -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 +11 -0
- package/templates/commands/compact-safe.md +4 -0
- package/templates/commands/conflict-resolver.md +4 -0
- package/templates/commands/end.md +13 -0
- package/templates/commands/refactor-clean.md +4 -0
- 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 +5 -1
- 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 -12
- package/templates/core/memory-md.md +33 -0
- 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 +1 -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/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
|
|
package/src/commands/doctor.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
1
2
|
import path from 'node:path';
|
|
2
3
|
import { readWorkflowMeta, workflowMetaExists, getPackageVersion } from '../core/config.js';
|
|
3
4
|
import { hashFile } from '../utils/hash.js';
|
|
@@ -72,6 +73,48 @@ async function checkClaudeMd(projectRoot) {
|
|
|
72
73
|
}
|
|
73
74
|
}
|
|
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
|
+
|
|
75
118
|
async function checkSettingsJson(projectRoot) {
|
|
76
119
|
const settingsPath = path.join(projectRoot, '.claude', 'settings.json');
|
|
77
120
|
if (!(await fileExists(settingsPath))) {
|
|
@@ -134,6 +177,62 @@ async function checkAgents(projectRoot, meta) {
|
|
|
134
177
|
return results;
|
|
135
178
|
}
|
|
136
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
|
+
|
|
137
236
|
async function checkCommands(projectRoot) {
|
|
138
237
|
const commandsDir = path.join(projectRoot, '.claude', 'commands');
|
|
139
238
|
const missing = [];
|
|
@@ -157,7 +256,7 @@ async function checkSkills(projectRoot) {
|
|
|
157
256
|
const allExpected = [...UNIVERSAL_SKILLS, ...TEMPLATE_SKILLS, 'agent-routing'];
|
|
158
257
|
|
|
159
258
|
for (const skill of allExpected) {
|
|
160
|
-
const skillPath = path.join(skillsDir,
|
|
259
|
+
const skillPath = path.join(skillsDir, skill, 'SKILL.md');
|
|
161
260
|
if (!(await fileExists(skillPath))) {
|
|
162
261
|
missing.push(skill);
|
|
163
262
|
}
|
|
@@ -166,7 +265,49 @@ async function checkSkills(projectRoot) {
|
|
|
166
265
|
if (missing.length === 0) {
|
|
167
266
|
return [result(PASS, `skills/ (${allExpected.length} expected, all present)`, null)];
|
|
168
267
|
}
|
|
169
|
-
return missing.map((s) => result(WARN, `skills/${s}.md`, 'Missing skill'));
|
|
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;
|
|
170
311
|
}
|
|
171
312
|
|
|
172
313
|
async function checkHashIntegrity(projectRoot, meta) {
|
|
@@ -287,6 +428,7 @@ export async function doctorCommand() {
|
|
|
287
428
|
const meta = await readWorkflowMeta(projectRoot);
|
|
288
429
|
|
|
289
430
|
printResult(await checkClaudeMd(projectRoot));
|
|
431
|
+
for (const r of await checkClaudeMdSize(projectRoot)) printResult(r);
|
|
290
432
|
printResult(await checkSettingsJson(projectRoot));
|
|
291
433
|
printResult(await checkSessions(projectRoot));
|
|
292
434
|
display.newline();
|
|
@@ -294,8 +436,10 @@ export async function doctorCommand() {
|
|
|
294
436
|
// Components
|
|
295
437
|
display.barLine(display.white('Components'));
|
|
296
438
|
for (const r of await checkAgents(projectRoot, meta)) printResult(r);
|
|
439
|
+
for (const r of await checkAgentDescription(projectRoot)) printResult(r);
|
|
297
440
|
for (const r of await checkCommands(projectRoot)) printResult(r);
|
|
298
441
|
for (const r of await checkSkills(projectRoot)) printResult(r);
|
|
442
|
+
for (const r of await checkSkillFormat(projectRoot)) printResult(r);
|
|
299
443
|
display.newline();
|
|
300
444
|
|
|
301
445
|
// Docs
|
package/src/commands/init.js
CHANGED
|
@@ -185,11 +185,27 @@ async function runAgents(selections) {
|
|
|
185
185
|
return { ...selections, selectedAgents };
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
async function runMemoryMd(selections) {
|
|
189
|
+
const { includeMemoryMd } = await inquirer.prompt([
|
|
190
|
+
{
|
|
191
|
+
type: 'list',
|
|
192
|
+
name: 'includeMemoryMd',
|
|
193
|
+
message: 'Scaffold a MEMORY.md template? (for Claude Code memory system)',
|
|
194
|
+
choices: [
|
|
195
|
+
{ name: 'No', value: false },
|
|
196
|
+
{ name: 'Yes', value: true },
|
|
197
|
+
],
|
|
198
|
+
},
|
|
199
|
+
]);
|
|
200
|
+
return { ...selections, includeMemoryMd };
|
|
201
|
+
}
|
|
202
|
+
|
|
188
203
|
const STEP_RUNNERS = {
|
|
189
204
|
projectInfo: runProjectInfo,
|
|
190
205
|
projectType: runProjectType,
|
|
191
206
|
techStack: runTechStack,
|
|
192
207
|
agents: runAgents,
|
|
208
|
+
memoryMd: runMemoryMd,
|
|
193
209
|
};
|
|
194
210
|
|
|
195
211
|
// --- Confirmation ---
|
|
@@ -253,6 +269,7 @@ async function runInteractivePrompts(projectRoot) {
|
|
|
253
269
|
languages: [],
|
|
254
270
|
useDocker: false,
|
|
255
271
|
selectedAgents: [],
|
|
272
|
+
includeMemoryMd: false,
|
|
256
273
|
};
|
|
257
274
|
|
|
258
275
|
let confirmed = false;
|
|
@@ -264,6 +281,7 @@ async function runInteractivePrompts(projectRoot) {
|
|
|
264
281
|
selections = await runProjectType(selections);
|
|
265
282
|
selections = await runTechStack(selections);
|
|
266
283
|
selections = await runAgents(selections);
|
|
284
|
+
selections = await runMemoryMd(selections);
|
|
267
285
|
firstRun = false;
|
|
268
286
|
}
|
|
269
287
|
|
|
@@ -279,6 +297,7 @@ async function runInteractivePrompts(projectRoot) {
|
|
|
279
297
|
languages: [],
|
|
280
298
|
useDocker: false,
|
|
281
299
|
selectedAgents: [],
|
|
300
|
+
includeMemoryMd: false,
|
|
282
301
|
};
|
|
283
302
|
display.newline();
|
|
284
303
|
display.info('Starting over...');
|
|
@@ -287,6 +306,7 @@ async function runInteractivePrompts(projectRoot) {
|
|
|
287
306
|
selections = await runProjectType(selections);
|
|
288
307
|
selections = await runTechStack(selections);
|
|
289
308
|
selections = await runAgents(selections);
|
|
309
|
+
selections = await runMemoryMd(selections);
|
|
290
310
|
} else if (confirmation === 'adjust') {
|
|
291
311
|
const { step } = await inquirer.prompt([
|
|
292
312
|
{
|
|
@@ -332,7 +352,9 @@ function buildTemplateVariables(selections) {
|
|
|
332
352
|
|
|
333
353
|
const commandsText = buildCommandsBlock(languages, useDocker);
|
|
334
354
|
|
|
335
|
-
const skillsLines = TEMPLATE_SKILLS.map(
|
|
355
|
+
const skillsLines = TEMPLATE_SKILLS.map(
|
|
356
|
+
(s) => `- ${s}/SKILL.md — Run /setup to fill automatically`
|
|
357
|
+
);
|
|
336
358
|
const skillsText = skillsLines.join('\n');
|
|
337
359
|
|
|
338
360
|
return {
|
|
@@ -410,7 +432,7 @@ async function scaffoldFresh(projectRoot, selections, variables, settingsStr, ve
|
|
|
410
432
|
for (const skill of UNIVERSAL_SKILLS) {
|
|
411
433
|
await scaffoldFile(
|
|
412
434
|
`skills/universal/${skill}.md`,
|
|
413
|
-
path.join('.claude', 'skills',
|
|
435
|
+
path.join('.claude', 'skills', skill, 'SKILL.md'),
|
|
414
436
|
{},
|
|
415
437
|
projectRoot
|
|
416
438
|
);
|
|
@@ -420,7 +442,7 @@ async function scaffoldFresh(projectRoot, selections, variables, settingsStr, ve
|
|
|
420
442
|
for (const skill of TEMPLATE_SKILLS) {
|
|
421
443
|
await scaffoldFile(
|
|
422
444
|
`skills/templates/${skill}.md`,
|
|
423
|
-
path.join('.claude', 'skills',
|
|
445
|
+
path.join('.claude', 'skills', skill, 'SKILL.md'),
|
|
424
446
|
variables,
|
|
425
447
|
projectRoot
|
|
426
448
|
);
|
|
@@ -429,7 +451,7 @@ async function scaffoldFresh(projectRoot, selections, variables, settingsStr, ve
|
|
|
429
451
|
|
|
430
452
|
const agentRoutingContent = buildAgentRoutingSkill(selectedAgents, projectTypes);
|
|
431
453
|
await writeFile(
|
|
432
|
-
path.join(projectRoot, '.claude', 'skills', 'agent-routing.md'),
|
|
454
|
+
path.join(projectRoot, '.claude', 'skills', 'agent-routing', 'SKILL.md'),
|
|
433
455
|
agentRoutingContent
|
|
434
456
|
);
|
|
435
457
|
spinner.text = 'Created agent routing guide';
|
|
@@ -472,6 +494,11 @@ async function scaffoldFresh(projectRoot, selections, variables, settingsStr, ve
|
|
|
472
494
|
await writeFile(path.join(projectRoot, '.claude', 'sessions', '.gitkeep'), '');
|
|
473
495
|
spinner.text = 'Created .claude/sessions/';
|
|
474
496
|
|
|
497
|
+
if (selections.includeMemoryMd) {
|
|
498
|
+
await scaffoldFile('core/memory-md.md', 'MEMORY.md', {}, projectRoot);
|
|
499
|
+
spinner.text = 'Created MEMORY.md';
|
|
500
|
+
}
|
|
501
|
+
|
|
475
502
|
await computeAndWriteWorkflowMeta(projectRoot, selections, version);
|
|
476
503
|
spinner.text = 'Created .claude/workflow-meta.json';
|
|
477
504
|
|
|
@@ -498,6 +525,9 @@ function displayFreshSuccess(selections, skipped) {
|
|
|
498
525
|
display.success('.claude/sessions/');
|
|
499
526
|
display.success('.mcp.json');
|
|
500
527
|
display.success('.gitignore');
|
|
528
|
+
if (selections.includeMemoryMd) {
|
|
529
|
+
display.success('MEMORY.md');
|
|
530
|
+
}
|
|
501
531
|
if (skipped.progressMd) {
|
|
502
532
|
display.dim(' docs/spec/PROGRESS.md — already exists, skipped');
|
|
503
533
|
}
|
package/src/commands/upgrade.js
CHANGED
|
@@ -16,6 +16,7 @@ import { readTemplate, updateGitignore } from '../core/scaffolder.js';
|
|
|
16
16
|
import { writeFile, fileExists } from '../utils/file.js';
|
|
17
17
|
import { getLatestNpmVersion } from '../utils/npm.js';
|
|
18
18
|
import * as display from '../utils/display.js';
|
|
19
|
+
import { semverLessThan, migrateSkillFormat, patchAgentDescriptions } from '../core/migration.js';
|
|
19
20
|
|
|
20
21
|
function selfUpdate(latestVersion) {
|
|
21
22
|
const spinner = ora(`Updating worclaude to v${latestVersion}...`).start();
|
|
@@ -181,6 +182,35 @@ export async function upgradeCommand() {
|
|
|
181
182
|
const backupDir = await createBackup(projectRoot);
|
|
182
183
|
spinner.text = 'Backup created, applying updates...';
|
|
183
184
|
|
|
185
|
+
// v2.0.0 migrations (version-gated)
|
|
186
|
+
let skillReport = { migrated: 0, skipped: 0, names: [] };
|
|
187
|
+
let agentReport = { autoPatched: 0, prompted: 0, declined: 0, skipped: [] };
|
|
188
|
+
|
|
189
|
+
if (semverLessThan(installedVersion, '2.0.0')) {
|
|
190
|
+
spinner.text = 'Running v2.0.0 migrations...';
|
|
191
|
+
|
|
192
|
+
// Item 14: Skill format migration (flat .md → skill-name/SKILL.md)
|
|
193
|
+
skillReport = await migrateSkillFormat(projectRoot, meta);
|
|
194
|
+
|
|
195
|
+
// Item 15: Agent frontmatter patch (add missing description)
|
|
196
|
+
spinner.stop();
|
|
197
|
+
agentReport = await patchAgentDescriptions(projectRoot, meta, async (agentName) => {
|
|
198
|
+
const { patch } = await inquirer.prompt([
|
|
199
|
+
{
|
|
200
|
+
type: 'list',
|
|
201
|
+
name: 'patch',
|
|
202
|
+
message: `Agent "${agentName}" has been customized. Add missing description field?`,
|
|
203
|
+
choices: [
|
|
204
|
+
{ name: 'Yes', value: true },
|
|
205
|
+
{ name: 'No, skip', value: false },
|
|
206
|
+
],
|
|
207
|
+
},
|
|
208
|
+
]);
|
|
209
|
+
return patch;
|
|
210
|
+
});
|
|
211
|
+
spinner.start('Applying updates...');
|
|
212
|
+
}
|
|
213
|
+
|
|
184
214
|
// Auto-update files
|
|
185
215
|
for (const { key, templatePath } of categories.autoUpdate) {
|
|
186
216
|
const content = await readTemplate(templatePath);
|
|
@@ -247,6 +277,22 @@ export async function upgradeCommand() {
|
|
|
247
277
|
`Customized: ${categories.modified.length} files ${display.dimColor('(no updates needed)')}`
|
|
248
278
|
);
|
|
249
279
|
}
|
|
280
|
+
if (skillReport.migrated > 0) {
|
|
281
|
+
display.barLine(`Migrated: ${skillReport.migrated} skills to directory format`);
|
|
282
|
+
}
|
|
283
|
+
const patchedTotal = agentReport.autoPatched + agentReport.prompted;
|
|
284
|
+
if (patchedTotal > 0) {
|
|
285
|
+
const detail =
|
|
286
|
+
agentReport.autoPatched > 0 && agentReport.prompted > 0
|
|
287
|
+
? ` (${agentReport.autoPatched} auto, ${agentReport.prompted} confirmed)`
|
|
288
|
+
: '';
|
|
289
|
+
display.barLine(`Patched: ${patchedTotal} agents with description${detail}`);
|
|
290
|
+
}
|
|
291
|
+
if (agentReport.skipped.length > 0) {
|
|
292
|
+
display.barLine(
|
|
293
|
+
`Skipped: ${agentReport.skipped.length} user-created agents ${display.dimColor(`(${agentReport.skipped.join(', ')})`)}`
|
|
294
|
+
);
|
|
295
|
+
}
|
|
250
296
|
display.newline();
|
|
251
297
|
display.barLine(display.dimColor(`Backup: ${path.basename(backupDir)}/`));
|
|
252
298
|
|
package/src/core/detector.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { fileExists, dirExists, readFile, listFiles } from '../utils/file.js';
|
|
2
|
+
import { fileExists, dirExists, readFile, listFiles, listSkillDirs } from '../utils/file.js';
|
|
3
3
|
import { workflowMetaExists } from './config.js';
|
|
4
4
|
|
|
5
5
|
export async function detectScenario(projectRoot) {
|
|
@@ -35,6 +35,7 @@ export async function scanExistingSetup(projectRoot) {
|
|
|
35
35
|
hasSettingsJson: await fileExists(path.join(projectRoot, '.claude', 'settings.json')),
|
|
36
36
|
hasMcpJson: await fileExists(path.join(projectRoot, '.mcp.json')),
|
|
37
37
|
existingSkills: await listFiles(path.join(projectRoot, '.claude', 'skills')),
|
|
38
|
+
existingSkillDirs: await listSkillDirs(path.join(projectRoot, '.claude', 'skills')),
|
|
38
39
|
existingAgents: await listFiles(path.join(projectRoot, '.claude', 'agents')),
|
|
39
40
|
existingCommands: await listFiles(path.join(projectRoot, '.claude', 'commands')),
|
|
40
41
|
hasProgressMd: await fileExists(path.join(projectRoot, 'docs', 'spec', 'PROGRESS.md')),
|
|
@@ -41,18 +41,18 @@ export async function buildTemplateHashMap() {
|
|
|
41
41
|
map[key] = { templatePath, hash: hashContent(content), type: 'command' };
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
// Universal skills: key = skills/{name}.md, templatePath = skills/universal/{name}.md
|
|
44
|
+
// Universal skills: key = skills/{name}/SKILL.md, templatePath = skills/universal/{name}.md
|
|
45
45
|
for (const name of UNIVERSAL_SKILLS) {
|
|
46
|
-
const key = `skills/${name}.md`;
|
|
46
|
+
const key = `skills/${name}/SKILL.md`;
|
|
47
47
|
const templatePath = `skills/universal/${name}.md`;
|
|
48
48
|
const content = await readTemplate(templatePath);
|
|
49
49
|
map[key] = { templatePath, hash: hashContent(content), type: 'universal-skill' };
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
// Template skills: key = skills/{name}.md, templatePath = skills/templates/{name}.md
|
|
52
|
+
// Template skills: key = skills/{name}/SKILL.md, templatePath = skills/templates/{name}.md
|
|
53
53
|
// These contain {variable} placeholders — raw hash won't match stored (substituted) hash
|
|
54
54
|
for (const name of TEMPLATE_SKILLS) {
|
|
55
|
-
const key = `skills/${name}.md`;
|
|
55
|
+
const key = `skills/${name}/SKILL.md`;
|
|
56
56
|
const templatePath = `skills/templates/${name}.md`;
|
|
57
57
|
const content = await readTemplate(templatePath);
|
|
58
58
|
map[key] = { templatePath, hash: hashContent(content), type: 'template-skill' };
|
package/src/core/merger.js
CHANGED
|
@@ -111,39 +111,41 @@ async function mergeSkills(projectRoot, existingScan, variables, report, selecti
|
|
|
111
111
|
];
|
|
112
112
|
|
|
113
113
|
for (const skill of allSkills) {
|
|
114
|
-
const
|
|
115
|
-
const
|
|
114
|
+
const destPath = path.join('.claude', 'skills', skill.name, 'SKILL.md');
|
|
115
|
+
const existsAsDir = existingScan.existingSkillDirs.includes(skill.name);
|
|
116
|
+
const existsAsFlat = existingScan.existingSkills.includes(`${skill.name}.md`);
|
|
116
117
|
|
|
117
|
-
if (
|
|
118
|
+
if (existsAsDir || existsAsFlat) {
|
|
118
119
|
// Tier 2: conflict — save as .workflow-ref.md
|
|
119
120
|
await scaffoldFile(
|
|
120
121
|
skill.templatePath,
|
|
121
|
-
path.join(
|
|
122
|
+
path.join('.claude', 'skills', skill.name, 'SKILL.workflow-ref.md'),
|
|
122
123
|
skill.vars,
|
|
123
124
|
projectRoot
|
|
124
125
|
);
|
|
125
|
-
report.conflicts.skills.push(
|
|
126
|
+
report.conflicts.skills.push(skill.name);
|
|
126
127
|
} else {
|
|
127
128
|
// Tier 1: add
|
|
128
|
-
await scaffoldFile(skill.templatePath,
|
|
129
|
-
report.added.skills.push(
|
|
129
|
+
await scaffoldFile(skill.templatePath, destPath, skill.vars, projectRoot);
|
|
130
|
+
report.added.skills.push(skill.name);
|
|
130
131
|
}
|
|
131
132
|
}
|
|
132
133
|
|
|
133
|
-
// Generated skill: agent-routing
|
|
134
|
-
const routingFilename = 'agent-routing.md';
|
|
134
|
+
// Generated skill: agent-routing
|
|
135
135
|
const skillsDir = path.join('.claude', 'skills');
|
|
136
136
|
const routingContent = buildAgentRoutingSkill(selections.selectedAgents, selections.projectTypes);
|
|
137
|
+
const routingExistsAsDir = existingScan.existingSkillDirs.includes('agent-routing');
|
|
138
|
+
const routingExistsAsFlat = existingScan.existingSkills.includes('agent-routing.md');
|
|
137
139
|
|
|
138
|
-
if (
|
|
140
|
+
if (routingExistsAsDir || routingExistsAsFlat) {
|
|
139
141
|
await writeFile(
|
|
140
|
-
path.join(projectRoot, skillsDir, 'agent-routing.workflow-ref.md'),
|
|
142
|
+
path.join(projectRoot, skillsDir, 'agent-routing', 'SKILL.workflow-ref.md'),
|
|
141
143
|
routingContent
|
|
142
144
|
);
|
|
143
|
-
report.conflicts.skills.push(
|
|
145
|
+
report.conflicts.skills.push('agent-routing');
|
|
144
146
|
} else {
|
|
145
|
-
await writeFile(path.join(projectRoot, skillsDir,
|
|
146
|
-
report.added.skills.push(
|
|
147
|
+
await writeFile(path.join(projectRoot, skillsDir, 'agent-routing', 'SKILL.md'), routingContent);
|
|
148
|
+
report.added.skills.push('agent-routing');
|
|
147
149
|
}
|
|
148
150
|
}
|
|
149
151
|
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { AGENT_CATALOG } from '../data/agents.js';
|
|
3
|
+
import { readFile, writeFile, dirExists, moveFile, listFiles } from '../utils/file.js';
|
|
4
|
+
import { hashFile } from '../utils/hash.js';
|
|
5
|
+
|
|
6
|
+
// --- Version comparison ---
|
|
7
|
+
|
|
8
|
+
export function semverLessThan(a, b) {
|
|
9
|
+
const pa = a.split('.').map(Number);
|
|
10
|
+
const pb = b.split('.').map(Number);
|
|
11
|
+
for (let i = 0; i < 3; i++) {
|
|
12
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return true;
|
|
13
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return false;
|
|
14
|
+
}
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// --- Agent description lookup ---
|
|
19
|
+
|
|
20
|
+
const UNIVERSAL_AGENT_DESCRIPTIONS = {
|
|
21
|
+
'plan-reviewer': 'Reviews implementation plans for specificity, gaps, and executability',
|
|
22
|
+
'code-simplifier': 'Reviews changed code and simplifies overly complex implementations',
|
|
23
|
+
'test-writer': 'Writes comprehensive, meaningful tests for recently changed code',
|
|
24
|
+
'build-validator': 'Validates that the project builds and all tests pass',
|
|
25
|
+
'verify-app':
|
|
26
|
+
'Verifies the running application end-to-end — tests actual behavior, not just code reading',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function getAgentDescription(agentName) {
|
|
30
|
+
if (UNIVERSAL_AGENT_DESCRIPTIONS[agentName]) {
|
|
31
|
+
return UNIVERSAL_AGENT_DESCRIPTIONS[agentName];
|
|
32
|
+
}
|
|
33
|
+
if (AGENT_CATALOG[agentName]) {
|
|
34
|
+
return AGENT_CATALOG[agentName].description;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// --- Item 14: Skill Format Migration ---
|
|
40
|
+
|
|
41
|
+
export async function migrateSkillFormat(projectRoot, meta) {
|
|
42
|
+
const skillsDir = path.join(projectRoot, '.claude', 'skills');
|
|
43
|
+
const report = { migrated: 0, skipped: 0, names: [] };
|
|
44
|
+
|
|
45
|
+
if (!(await dirExists(skillsDir))) return report;
|
|
46
|
+
|
|
47
|
+
const files = await listFiles(skillsDir);
|
|
48
|
+
const mdFiles = files.filter((f) => f.endsWith('.md') && !f.endsWith('.workflow-ref.md'));
|
|
49
|
+
const refFiles = files.filter((f) => f.endsWith('.workflow-ref.md'));
|
|
50
|
+
|
|
51
|
+
for (const file of mdFiles) {
|
|
52
|
+
const skillName = file.replace(/\.md$/, '');
|
|
53
|
+
const newDir = path.join(skillsDir, skillName);
|
|
54
|
+
|
|
55
|
+
if (await dirExists(newDir)) {
|
|
56
|
+
report.skipped++;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await moveFile(path.join(skillsDir, file), path.join(newDir, 'SKILL.md'));
|
|
61
|
+
|
|
62
|
+
// Move corresponding .workflow-ref.md if exists
|
|
63
|
+
const refFile = `${skillName}.workflow-ref.md`;
|
|
64
|
+
if (refFiles.includes(refFile)) {
|
|
65
|
+
await moveFile(path.join(skillsDir, refFile), path.join(newDir, 'SKILL.workflow-ref.md'));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Update meta hash keys
|
|
69
|
+
if (meta?.fileHashes) {
|
|
70
|
+
const oldKey = `skills/${file}`;
|
|
71
|
+
const newKey = `skills/${skillName}/SKILL.md`;
|
|
72
|
+
if (meta.fileHashes[oldKey] !== undefined) {
|
|
73
|
+
meta.fileHashes[newKey] = meta.fileHashes[oldKey];
|
|
74
|
+
delete meta.fileHashes[oldKey];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const oldRefKey = `skills/${refFile}`;
|
|
78
|
+
const newRefKey = `skills/${skillName}/SKILL.workflow-ref.md`;
|
|
79
|
+
if (meta.fileHashes[oldRefKey] !== undefined) {
|
|
80
|
+
meta.fileHashes[newRefKey] = meta.fileHashes[oldRefKey];
|
|
81
|
+
delete meta.fileHashes[oldRefKey];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
report.migrated++;
|
|
86
|
+
report.names.push(skillName);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return report;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// --- Item 15: Agent Frontmatter Patch ---
|
|
93
|
+
|
|
94
|
+
export async function patchAgentDescriptions(projectRoot, meta, promptFn) {
|
|
95
|
+
const agentsDir = path.join(projectRoot, '.claude', 'agents');
|
|
96
|
+
const report = { autoPatched: 0, prompted: 0, declined: 0, skipped: [] };
|
|
97
|
+
|
|
98
|
+
if (!(await dirExists(agentsDir))) return report;
|
|
99
|
+
|
|
100
|
+
const files = await listFiles(agentsDir);
|
|
101
|
+
const mdFiles = files.filter((f) => f.endsWith('.md') && !f.endsWith('.workflow-ref.md'));
|
|
102
|
+
|
|
103
|
+
for (const file of mdFiles) {
|
|
104
|
+
const filePath = path.join(agentsDir, file);
|
|
105
|
+
const content = await readFile(filePath);
|
|
106
|
+
|
|
107
|
+
// Check if frontmatter exists
|
|
108
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
109
|
+
if (!frontmatterMatch) continue;
|
|
110
|
+
|
|
111
|
+
// Skip if description already present
|
|
112
|
+
if (/^description:\s*.+/m.test(frontmatterMatch[1])) continue;
|
|
113
|
+
|
|
114
|
+
const agentName = file.replace(/\.md$/, '');
|
|
115
|
+
const description = getAgentDescription(agentName);
|
|
116
|
+
|
|
117
|
+
if (!description) {
|
|
118
|
+
report.skipped.push(agentName);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check if user has modified the file
|
|
123
|
+
const storedHash = meta?.fileHashes?.[`agents/${file}`];
|
|
124
|
+
const currentHash = await hashFile(filePath);
|
|
125
|
+
const isModified = storedHash && currentHash !== storedHash;
|
|
126
|
+
|
|
127
|
+
if (isModified && promptFn) {
|
|
128
|
+
const confirmed = await promptFn(agentName);
|
|
129
|
+
if (!confirmed) {
|
|
130
|
+
report.declined++;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
report.prompted++;
|
|
134
|
+
} else {
|
|
135
|
+
report.autoPatched++;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Insert description after name line
|
|
139
|
+
const updated = content.replace(/^(name:\s*.+)$/m, `$1\ndescription: "${description}"`);
|
|
140
|
+
await writeFile(filePath, updated);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return report;
|
|
144
|
+
}
|
package/src/core/remover.js
CHANGED
|
@@ -118,7 +118,7 @@ export async function removeTrackedFiles(projectRoot, fileKeys) {
|
|
|
118
118
|
for (const subdir of ['agents', 'commands', 'skills']) {
|
|
119
119
|
const dirPath = path.join(claudeDir, subdir);
|
|
120
120
|
if (await dirExists(dirPath)) {
|
|
121
|
-
const remaining = await
|
|
121
|
+
const remaining = await listFilesRecursive(dirPath);
|
|
122
122
|
if (remaining.length === 0) {
|
|
123
123
|
await removeDirectory(dirPath);
|
|
124
124
|
}
|
|
@@ -185,7 +185,9 @@ export async function cleanGitignore(projectRoot) {
|
|
|
185
185
|
'# Worclaude (generated workflow files)',
|
|
186
186
|
'.claude/',
|
|
187
187
|
'.claude/sessions/',
|
|
188
|
+
'.claude/settings.local.json',
|
|
188
189
|
'.claude/workflow-meta.json',
|
|
190
|
+
'.claude/worktrees/',
|
|
189
191
|
]);
|
|
190
192
|
|
|
191
193
|
const filtered = lines.filter((line) => !REMOVE_LINES.has(line.trim()));
|