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.
Files changed (70) hide show
  1. package/package.json +1 -1
  2. package/src/commands/backup.js +5 -3
  3. package/src/commands/doctor.js +146 -2
  4. package/src/commands/init.js +34 -4
  5. package/src/commands/upgrade.js +46 -0
  6. package/src/core/detector.js +2 -1
  7. package/src/core/file-categorizer.js +4 -4
  8. package/src/core/merger.js +16 -14
  9. package/src/core/migration.js +144 -0
  10. package/src/core/remover.js +3 -1
  11. package/src/core/scaffolder.js +7 -1
  12. package/src/data/agents.js +2 -0
  13. package/src/utils/file.js +21 -0
  14. package/templates/agents/optional/backend/api-designer.md +6 -0
  15. package/templates/agents/optional/backend/auth-auditor.md +2 -0
  16. package/templates/agents/optional/backend/database-analyst.md +7 -0
  17. package/templates/agents/optional/data/data-pipeline-reviewer.md +7 -0
  18. package/templates/agents/optional/data/ml-experiment-tracker.md +7 -0
  19. package/templates/agents/optional/data/prompt-engineer.md +2 -0
  20. package/templates/agents/optional/devops/ci-fixer.md +2 -0
  21. package/templates/agents/optional/devops/dependency-manager.md +7 -0
  22. package/templates/agents/optional/devops/deploy-validator.md +7 -0
  23. package/templates/agents/optional/devops/docker-helper.md +2 -0
  24. package/templates/agents/optional/docs/changelog-generator.md +7 -0
  25. package/templates/agents/optional/docs/doc-writer.md +3 -0
  26. package/templates/agents/optional/frontend/style-enforcer.md +2 -0
  27. package/templates/agents/optional/frontend/ui-reviewer.md +7 -0
  28. package/templates/agents/optional/quality/bug-fixer.md +2 -0
  29. package/templates/agents/optional/quality/build-fixer.md +2 -0
  30. package/templates/agents/optional/quality/e2e-runner.md +3 -0
  31. package/templates/agents/optional/quality/performance-auditor.md +8 -0
  32. package/templates/agents/optional/quality/refactorer.md +2 -0
  33. package/templates/agents/optional/quality/security-reviewer.md +9 -0
  34. package/templates/agents/universal/build-validator.md +3 -0
  35. package/templates/agents/universal/code-simplifier.md +2 -0
  36. package/templates/agents/universal/plan-reviewer.md +8 -0
  37. package/templates/agents/universal/test-writer.md +4 -1
  38. package/templates/agents/universal/verify-app.md +44 -0
  39. package/templates/commands/build-fix.md +4 -0
  40. package/templates/commands/commit-push-pr.md +11 -0
  41. package/templates/commands/compact-safe.md +4 -0
  42. package/templates/commands/conflict-resolver.md +4 -0
  43. package/templates/commands/end.md +13 -0
  44. package/templates/commands/refactor-clean.md +4 -0
  45. package/templates/commands/review-changes.md +4 -0
  46. package/templates/commands/review-plan.md +4 -0
  47. package/templates/commands/setup.md +7 -3
  48. package/templates/commands/start.md +5 -1
  49. package/templates/commands/status.md +4 -0
  50. package/templates/commands/sync.md +4 -0
  51. package/templates/commands/techdebt.md +4 -0
  52. package/templates/commands/test-coverage.md +4 -0
  53. package/templates/commands/update-claude-md.md +4 -0
  54. package/templates/commands/verify.md +4 -0
  55. package/templates/core/claude-md.md +13 -12
  56. package/templates/core/memory-md.md +33 -0
  57. package/templates/skills/templates/backend-conventions.md +6 -0
  58. package/templates/skills/templates/frontend-design-system.md +10 -0
  59. package/templates/skills/templates/project-patterns.md +4 -0
  60. package/templates/skills/universal/claude-md-maintenance.md +1 -0
  61. package/templates/skills/universal/context-management.md +1 -0
  62. package/templates/skills/universal/coordinator-mode.md +77 -0
  63. package/templates/skills/universal/git-conventions.md +1 -0
  64. package/templates/skills/universal/planning-with-files.md +1 -0
  65. package/templates/skills/universal/prompt-engineering.md +1 -0
  66. package/templates/skills/universal/review-and-handoff.md +1 -0
  67. package/templates/skills/universal/security-checklist.md +7 -0
  68. package/templates/skills/universal/subagent-usage.md +1 -0
  69. package/templates/skills/universal/testing.md +7 -0
  70. package/templates/skills/universal/verification.md +6 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "worclaude",
3
- "version": "1.9.0",
3
+ "version": "2.0.0",
4
4
  "description": "CLI tool that scaffolds a comprehensive Claude Code workflow into any project",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 skills = await listFiles(path.join(claudeBackup, 'skills'));
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 (skills.length > 0) parts.push(`${skills.length} skills`);
42
+ if (skillCount > 0) parts.push(`${skillCount} skills`);
41
43
  contents.push(`.claude/ (${parts.join(', ')})`);
42
44
  }
43
45
 
@@ -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, `${skill}.md`);
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
@@ -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((s) => `- ${s}.md — Run /setup to fill automatically`);
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', `${skill}.md`),
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', `${skill}.md`),
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
  }
@@ -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
 
@@ -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' };
@@ -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 filename = `${skill.name}.md`;
115
- const destDir = path.join('.claude', 'skills');
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 (existingScan.existingSkills.includes(filename)) {
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(destDir, `${skill.name}.workflow-ref.md`),
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(filename);
126
+ report.conflicts.skills.push(skill.name);
126
127
  } else {
127
128
  // Tier 1: add
128
- await scaffoldFile(skill.templatePath, path.join(destDir, filename), skill.vars, projectRoot);
129
- report.added.skills.push(filename);
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.md
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 (existingScan.existingSkills.includes(routingFilename)) {
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(routingFilename);
145
+ report.conflicts.skills.push('agent-routing');
144
146
  } else {
145
- await writeFile(path.join(projectRoot, skillsDir, routingFilename), routingContent);
146
- report.added.skills.push(routingFilename);
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
+ }
@@ -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 listFiles(dirPath);
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()));