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.
Files changed (74) hide show
  1. package/README.md +11 -7
  2. package/package.json +1 -1
  3. package/src/commands/backup.js +5 -3
  4. package/src/commands/doctor.js +463 -0
  5. package/src/commands/init.js +42 -17
  6. package/src/commands/upgrade.js +52 -11
  7. package/src/core/config.js +19 -1
  8. package/src/core/detector.js +2 -1
  9. package/src/core/file-categorizer.js +4 -4
  10. package/src/core/merger.js +50 -30
  11. package/src/core/migration.js +144 -0
  12. package/src/core/remover.js +9 -2
  13. package/src/core/scaffolder.js +26 -5
  14. package/src/data/agents.js +2 -0
  15. package/src/index.js +6 -0
  16. package/src/utils/file.js +21 -0
  17. package/templates/agents/optional/backend/api-designer.md +6 -0
  18. package/templates/agents/optional/backend/auth-auditor.md +2 -0
  19. package/templates/agents/optional/backend/database-analyst.md +7 -0
  20. package/templates/agents/optional/data/data-pipeline-reviewer.md +7 -0
  21. package/templates/agents/optional/data/ml-experiment-tracker.md +7 -0
  22. package/templates/agents/optional/data/prompt-engineer.md +2 -0
  23. package/templates/agents/optional/devops/ci-fixer.md +2 -0
  24. package/templates/agents/optional/devops/dependency-manager.md +7 -0
  25. package/templates/agents/optional/devops/deploy-validator.md +7 -0
  26. package/templates/agents/optional/devops/docker-helper.md +2 -0
  27. package/templates/agents/optional/docs/changelog-generator.md +7 -0
  28. package/templates/agents/optional/docs/doc-writer.md +3 -0
  29. package/templates/agents/optional/frontend/style-enforcer.md +2 -0
  30. package/templates/agents/optional/frontend/ui-reviewer.md +7 -0
  31. package/templates/agents/optional/quality/bug-fixer.md +2 -0
  32. package/templates/agents/optional/quality/build-fixer.md +2 -0
  33. package/templates/agents/optional/quality/e2e-runner.md +3 -0
  34. package/templates/agents/optional/quality/performance-auditor.md +8 -0
  35. package/templates/agents/optional/quality/refactorer.md +2 -0
  36. package/templates/agents/optional/quality/security-reviewer.md +9 -0
  37. package/templates/agents/universal/build-validator.md +3 -0
  38. package/templates/agents/universal/code-simplifier.md +2 -0
  39. package/templates/agents/universal/plan-reviewer.md +8 -0
  40. package/templates/agents/universal/test-writer.md +4 -1
  41. package/templates/agents/universal/verify-app.md +44 -0
  42. package/templates/commands/build-fix.md +4 -0
  43. package/templates/commands/commit-push-pr.md +42 -9
  44. package/templates/commands/compact-safe.md +4 -0
  45. package/templates/commands/conflict-resolver.md +4 -0
  46. package/templates/commands/end.md +20 -3
  47. package/templates/commands/refactor-clean.md +43 -31
  48. package/templates/commands/review-changes.md +4 -0
  49. package/templates/commands/review-plan.md +4 -0
  50. package/templates/commands/setup.md +7 -3
  51. package/templates/commands/start.md +71 -7
  52. package/templates/commands/status.md +4 -0
  53. package/templates/commands/sync.md +4 -0
  54. package/templates/commands/techdebt.md +4 -0
  55. package/templates/commands/test-coverage.md +4 -0
  56. package/templates/commands/update-claude-md.md +4 -0
  57. package/templates/commands/verify.md +4 -0
  58. package/templates/core/claude-md.md +13 -11
  59. package/templates/core/memory-md.md +33 -0
  60. package/templates/settings/base.json +18 -2
  61. package/templates/skills/templates/backend-conventions.md +6 -0
  62. package/templates/skills/templates/frontend-design-system.md +10 -0
  63. package/templates/skills/templates/project-patterns.md +4 -0
  64. package/templates/skills/universal/claude-md-maintenance.md +1 -0
  65. package/templates/skills/universal/context-management.md +56 -0
  66. package/templates/skills/universal/coordinator-mode.md +77 -0
  67. package/templates/skills/universal/git-conventions.md +1 -0
  68. package/templates/skills/universal/planning-with-files.md +1 -0
  69. package/templates/skills/universal/prompt-engineering.md +1 -0
  70. package/templates/skills/universal/review-and-handoff.md +1 -0
  71. package/templates/skills/universal/security-checklist.md +7 -0
  72. package/templates/skills/universal/subagent-usage.md +1 -0
  73. package/templates/skills/universal/testing.md +7 -0
  74. package/templates/skills/universal/verification.md +6 -0
@@ -3,13 +3,13 @@ import inquirer from 'inquirer';
3
3
  import ora from 'ora';
4
4
  import { scaffoldFile, updateGitignore } from '../core/scaffolder.js';
5
5
  import {
6
+ computeFileHashes,
6
7
  createWorkflowMeta,
7
8
  getPackageVersion,
8
9
  readWorkflowMeta,
9
10
  writeWorkflowMeta,
10
11
  } from '../core/config.js';
11
- import { fileExists, writeFile, listFilesRecursive } from '../utils/file.js';
12
- import { hashFile } from '../utils/hash.js';
12
+ import { fileExists, writeFile } from '../utils/file.js';
13
13
  import * as display from '../utils/display.js';
14
14
  import { promptProjectType } from '../prompts/project-type.js';
15
15
  import { promptTechStack } from '../prompts/tech-stack.js';
@@ -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 {
@@ -349,17 +371,7 @@ function buildTemplateVariables(selections) {
349
371
  }
350
372
 
351
373
  async function computeAndWriteWorkflowMeta(projectRoot, selections, version) {
352
- const fileHashes = {};
353
- const claudeFiles = await listFilesRecursive(path.join(projectRoot, '.claude'));
354
- for (const filePath of claudeFiles) {
355
- const relativePath = path
356
- .relative(path.join(projectRoot, '.claude'), filePath)
357
- .split(path.sep)
358
- .join('/');
359
- if (relativePath !== 'workflow-meta.json' && relativePath !== 'settings.json') {
360
- fileHashes[relativePath] = await hashFile(filePath);
361
- }
362
- }
374
+ const fileHashes = await computeFileHashes(projectRoot);
363
375
 
364
376
  const meta = createWorkflowMeta({
365
377
  version,
@@ -420,7 +432,7 @@ async function scaffoldFresh(projectRoot, selections, variables, settingsStr, ve
420
432
  for (const skill of UNIVERSAL_SKILLS) {
421
433
  await scaffoldFile(
422
434
  `skills/universal/${skill}.md`,
423
- path.join('.claude', 'skills', `${skill}.md`),
435
+ path.join('.claude', 'skills', skill, 'SKILL.md'),
424
436
  {},
425
437
  projectRoot
426
438
  );
@@ -430,7 +442,7 @@ async function scaffoldFresh(projectRoot, selections, variables, settingsStr, ve
430
442
  for (const skill of TEMPLATE_SKILLS) {
431
443
  await scaffoldFile(
432
444
  `skills/templates/${skill}.md`,
433
- path.join('.claude', 'skills', `${skill}.md`),
445
+ path.join('.claude', 'skills', skill, 'SKILL.md'),
434
446
  variables,
435
447
  projectRoot
436
448
  );
@@ -439,7 +451,7 @@ async function scaffoldFresh(projectRoot, selections, variables, settingsStr, ve
439
451
 
440
452
  const agentRoutingContent = buildAgentRoutingSkill(selectedAgents, projectTypes);
441
453
  await writeFile(
442
- path.join(projectRoot, '.claude', 'skills', 'agent-routing.md'),
454
+ path.join(projectRoot, '.claude', 'skills', 'agent-routing', 'SKILL.md'),
443
455
  agentRoutingContent
444
456
  );
445
457
  spinner.text = 'Created agent routing guide';
@@ -478,6 +490,15 @@ async function scaffoldFresh(projectRoot, selections, variables, settingsStr, ve
478
490
  }
479
491
  spinner.text = 'Created docs/spec/';
480
492
 
493
+ // Create sessions directory for session persistence
494
+ await writeFile(path.join(projectRoot, '.claude', 'sessions', '.gitkeep'), '');
495
+ spinner.text = 'Created .claude/sessions/';
496
+
497
+ if (selections.includeMemoryMd) {
498
+ await scaffoldFile('core/memory-md.md', 'MEMORY.md', {}, projectRoot);
499
+ spinner.text = 'Created MEMORY.md';
500
+ }
501
+
481
502
  await computeAndWriteWorkflowMeta(projectRoot, selections, version);
482
503
  spinner.text = 'Created .claude/workflow-meta.json';
483
504
 
@@ -501,8 +522,12 @@ function displayFreshSuccess(selections, skipped) {
501
522
  display.success(`.claude/agents/${display.dimColor(` ${totalAgents} agents`)}`);
502
523
  display.success(`.claude/commands/${display.dimColor(` ${COMMAND_FILES.length} commands`)}`);
503
524
  display.success(`.claude/skills/${display.dimColor(` ${totalSkills} skills`)}`);
525
+ display.success('.claude/sessions/');
504
526
  display.success('.mcp.json');
505
527
  display.success('.gitignore');
528
+ if (selections.includeMemoryMd) {
529
+ display.success('MEMORY.md');
530
+ }
506
531
  if (skipped.progressMd) {
507
532
  display.dim(' docs/spec/PROGRESS.md — already exists, skipped');
508
533
  }
@@ -3,6 +3,7 @@ import { execSync } from 'node:child_process';
3
3
  import inquirer from 'inquirer';
4
4
  import ora from 'ora';
5
5
  import {
6
+ computeFileHashes,
6
7
  readWorkflowMeta,
7
8
  workflowMetaExists,
8
9
  writeWorkflowMeta,
@@ -12,10 +13,10 @@ import { createBackup } from '../core/backup.js';
12
13
  import { categorizeFiles } from '../core/file-categorizer.js';
13
14
  import { buildSettingsJson, mergeSettingsPermissionsAndHooks } from '../core/merger.js';
14
15
  import { readTemplate, updateGitignore } from '../core/scaffolder.js';
15
- import { hashFile } from '../utils/hash.js';
16
- import { writeFile, fileExists, listFilesRecursive } from '../utils/file.js';
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);
@@ -211,16 +241,11 @@ export async function upgradeCommand() {
211
241
  spinner.text = 'Settings merged...';
212
242
  }
213
243
 
244
+ // Ensure sessions directory exists for session persistence
245
+ await writeFile(path.join(projectRoot, '.claude', 'sessions', '.gitkeep'), '');
246
+
214
247
  // Recompute file hashes
215
- const fileHashes = {};
216
- const claudeDir = path.join(projectRoot, '.claude');
217
- const allFiles = await listFilesRecursive(claudeDir);
218
- for (const filePath of allFiles) {
219
- const relKey = path.relative(claudeDir, filePath).split(path.sep).join('/');
220
- if (relKey !== 'workflow-meta.json' && relKey !== 'settings.json') {
221
- fileHashes[relKey] = await hashFile(filePath);
222
- }
223
- }
248
+ const fileHashes = await computeFileHashes(projectRoot);
224
249
 
225
250
  // Ensure .gitignore has worclaude entries
226
251
  await updateGitignore(projectRoot);
@@ -252,6 +277,22 @@ export async function upgradeCommand() {
252
277
  `Customized: ${categories.modified.length} files ${display.dimColor('(no updates needed)')}`
253
278
  );
254
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
+ }
255
296
  display.newline();
256
297
  display.barLine(display.dimColor(`Backup: ${path.basename(backupDir)}/`));
257
298
 
@@ -1,7 +1,8 @@
1
1
  import path from 'node:path';
2
2
  import { readFileSync } from 'node:fs';
3
3
  import { fileURLToPath } from 'node:url';
4
- import { readFile, writeFile, fileExists } from '../utils/file.js';
4
+ import { readFile, writeFile, fileExists, listFilesRecursive } from '../utils/file.js';
5
+ import { hashFile } from '../utils/hash.js';
5
6
 
6
7
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
8
  const pkgPath = path.resolve(__dirname, '..', '..', 'package.json');
@@ -57,3 +58,20 @@ export async function writeWorkflowMeta(projectRoot, meta) {
57
58
  const metaPath = path.join(projectRoot, '.claude', 'workflow-meta.json');
58
59
  await writeFile(metaPath, JSON.stringify(meta, null, 2));
59
60
  }
61
+
62
+ export async function computeFileHashes(projectRoot) {
63
+ const claudeDir = path.join(projectRoot, '.claude');
64
+ const allFiles = await listFilesRecursive(claudeDir);
65
+ const fileHashes = {};
66
+ for (const filePath of allFiles) {
67
+ const relKey = path.relative(claudeDir, filePath).split(path.sep).join('/');
68
+ if (
69
+ relKey !== 'workflow-meta.json' &&
70
+ relKey !== 'settings.json' &&
71
+ !relKey.startsWith('sessions/')
72
+ ) {
73
+ fileHashes[relKey] = await hashFile(filePath);
74
+ }
75
+ }
76
+ return fileHashes;
77
+ }
@@ -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
 
@@ -238,36 +240,50 @@ export async function mergeSettingsPermissionsAndHooks(projectRoot, workflowSett
238
240
  if (!existing.hooks[category]) existing.hooks[category] = [];
239
241
 
240
242
  const existingEntries = existing.hooks[category];
241
- const existingMatchers = new Map(existingEntries.map((h) => [h.matcher, h]));
243
+ const existingByMatcher = new Map();
244
+ for (const entry of existingEntries) {
245
+ if (!existingByMatcher.has(entry.matcher)) {
246
+ existingByMatcher.set(entry.matcher, []);
247
+ }
248
+ existingByMatcher.get(entry.matcher).push(entry);
249
+ }
250
+ const matched = new Set();
242
251
 
243
252
  for (const workflowEntry of workflowHooks[category]) {
244
- if (existingMatchers.has(workflowEntry.matcher)) {
245
- const existingEntry = existingMatchers.get(workflowEntry.matcher);
246
-
247
- // If hooks are identical, skip no conflict to resolve
248
- const existingCmd = existingEntry.hooks?.[0]?.command || '';
249
- const workflowCmd = workflowEntry.hooks?.[0]?.command || '';
250
- if (existingCmd === workflowCmd) {
251
- continue;
252
- }
253
+ const candidates = existingByMatcher.get(workflowEntry.matcher) || [];
254
+ const workflowCmd = workflowEntry.hooks?.[0]?.command || '';
255
+
256
+ // Try exact match first (identical command = skip)
257
+ const exactMatch = candidates.find(
258
+ (c) => !matched.has(c) && (c.hooks?.[0]?.command || '') === workflowCmd
259
+ );
260
+ if (exactMatch) {
261
+ matched.add(exactMatch);
262
+ continue;
263
+ }
264
+
265
+ // Try unmatched candidate with same matcher (conflict)
266
+ const conflictCandidate = candidates.find((c) => !matched.has(c));
267
+ if (conflictCandidate) {
268
+ matched.add(conflictCandidate);
253
269
 
254
270
  // Tier 3: conflict — ask user
255
- const resolution = await promptHookConflict(category, existingEntry, workflowEntry);
271
+ const resolution = await promptHookConflict(category, conflictCandidate, workflowEntry);
256
272
 
257
273
  if (resolution === 'replace') {
258
- const idx = existingEntries.indexOf(existingEntry);
274
+ const idx = existingEntries.indexOf(conflictCandidate);
259
275
  existingEntries[idx] = workflowEntry;
260
276
  report.hookConflicts.push(
261
277
  `${category} "${workflowEntry.matcher}": replaced with workflow hook`
262
278
  );
263
279
  } else if (resolution === 'chain') {
264
- const idx = existingEntries.indexOf(existingEntry);
280
+ const idx = existingEntries.indexOf(conflictCandidate);
265
281
  existingEntries[idx] = {
266
- matcher: existingEntry.matcher,
282
+ matcher: conflictCandidate.matcher,
267
283
  hooks: [
268
284
  {
269
285
  type: 'command',
270
- command: `${existingEntry.hooks[0].command} && ${workflowEntry.hooks[0].command}`,
286
+ command: `${conflictCandidate.hooks[0].command} && ${workflowEntry.hooks[0].command}`,
271
287
  },
272
288
  ],
273
289
  };
@@ -276,7 +292,7 @@ export async function mergeSettingsPermissionsAndHooks(projectRoot, workflowSett
276
292
  report.hookConflicts.push(`${category} "${workflowEntry.matcher}": kept existing hook`);
277
293
  }
278
294
  } else {
279
- // Tier 1: no conflict — append
295
+ // Tier 1: no match — append
280
296
  existingEntries.push(workflowEntry);
281
297
  report.added.hooks++;
282
298
  }
@@ -422,6 +438,10 @@ export async function performMerge(
422
438
  if (spinner) spinner.start();
423
439
 
424
440
  await mergeMcpJson(projectRoot, existingScan);
441
+
442
+ // Ensure sessions directory exists for session persistence
443
+ await writeFile(path.join(projectRoot, '.claude', 'sessions', '.gitkeep'), '');
444
+
425
445
  await mergeDocSpecs(projectRoot, existingScan, variables, selections, report);
426
446
 
427
447
  // Stop spinner before CLAUDE.md merge — interactive prompts for section selection
@@ -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
  }
@@ -181,7 +181,14 @@ export async function cleanGitignore(projectRoot) {
181
181
  const content = await readFile(gitignorePath);
182
182
  const lines = content.split(/\r?\n/);
183
183
 
184
- const REMOVE_LINES = new Set(['# Worclaude (generated workflow files)', '.claude/']);
184
+ const REMOVE_LINES = new Set([
185
+ '# Worclaude (generated workflow files)',
186
+ '.claude/',
187
+ '.claude/sessions/',
188
+ '.claude/settings.local.json',
189
+ '.claude/workflow-meta.json',
190
+ '.claude/worktrees/',
191
+ ]);
185
192
 
186
193
  const filtered = lines.filter((line) => !REMOVE_LINES.has(line.trim()));
187
194