worclaude 1.8.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -7
- package/package.json +1 -1
- package/src/commands/backup.js +5 -3
- package/src/commands/doctor.js +463 -0
- package/src/commands/init.js +42 -17
- package/src/commands/upgrade.js +52 -11
- package/src/core/config.js +19 -1
- package/src/core/detector.js +2 -1
- package/src/core/file-categorizer.js +4 -4
- package/src/core/merger.js +50 -30
- package/src/core/migration.js +144 -0
- package/src/core/remover.js +9 -2
- package/src/core/scaffolder.js +26 -5
- package/src/data/agents.js +2 -0
- package/src/index.js +6 -0
- package/src/utils/file.js +21 -0
- package/templates/agents/optional/backend/api-designer.md +6 -0
- package/templates/agents/optional/backend/auth-auditor.md +2 -0
- package/templates/agents/optional/backend/database-analyst.md +7 -0
- package/templates/agents/optional/data/data-pipeline-reviewer.md +7 -0
- package/templates/agents/optional/data/ml-experiment-tracker.md +7 -0
- package/templates/agents/optional/data/prompt-engineer.md +2 -0
- package/templates/agents/optional/devops/ci-fixer.md +2 -0
- package/templates/agents/optional/devops/dependency-manager.md +7 -0
- package/templates/agents/optional/devops/deploy-validator.md +7 -0
- package/templates/agents/optional/devops/docker-helper.md +2 -0
- package/templates/agents/optional/docs/changelog-generator.md +7 -0
- package/templates/agents/optional/docs/doc-writer.md +3 -0
- package/templates/agents/optional/frontend/style-enforcer.md +2 -0
- package/templates/agents/optional/frontend/ui-reviewer.md +7 -0
- package/templates/agents/optional/quality/bug-fixer.md +2 -0
- package/templates/agents/optional/quality/build-fixer.md +2 -0
- package/templates/agents/optional/quality/e2e-runner.md +3 -0
- package/templates/agents/optional/quality/performance-auditor.md +8 -0
- package/templates/agents/optional/quality/refactorer.md +2 -0
- package/templates/agents/optional/quality/security-reviewer.md +9 -0
- package/templates/agents/universal/build-validator.md +3 -0
- package/templates/agents/universal/code-simplifier.md +2 -0
- package/templates/agents/universal/plan-reviewer.md +8 -0
- package/templates/agents/universal/test-writer.md +4 -1
- package/templates/agents/universal/verify-app.md +44 -0
- package/templates/commands/build-fix.md +4 -0
- package/templates/commands/commit-push-pr.md +42 -9
- package/templates/commands/compact-safe.md +4 -0
- package/templates/commands/conflict-resolver.md +4 -0
- package/templates/commands/end.md +20 -3
- package/templates/commands/refactor-clean.md +43 -31
- package/templates/commands/review-changes.md +4 -0
- package/templates/commands/review-plan.md +4 -0
- package/templates/commands/setup.md +7 -3
- package/templates/commands/start.md +71 -7
- package/templates/commands/status.md +4 -0
- package/templates/commands/sync.md +4 -0
- package/templates/commands/techdebt.md +4 -0
- package/templates/commands/test-coverage.md +4 -0
- package/templates/commands/update-claude-md.md +4 -0
- package/templates/commands/verify.md +4 -0
- package/templates/core/claude-md.md +13 -11
- package/templates/core/memory-md.md +33 -0
- package/templates/settings/base.json +18 -2
- package/templates/skills/templates/backend-conventions.md +6 -0
- package/templates/skills/templates/frontend-design-system.md +10 -0
- package/templates/skills/templates/project-patterns.md +4 -0
- package/templates/skills/universal/claude-md-maintenance.md +1 -0
- package/templates/skills/universal/context-management.md +56 -0
- package/templates/skills/universal/coordinator-mode.md +77 -0
- package/templates/skills/universal/git-conventions.md +1 -0
- package/templates/skills/universal/planning-with-files.md +1 -0
- package/templates/skills/universal/prompt-engineering.md +1 -0
- package/templates/skills/universal/review-and-handoff.md +1 -0
- package/templates/skills/universal/security-checklist.md +7 -0
- package/templates/skills/universal/subagent-usage.md +1 -0
- package/templates/skills/universal/testing.md +7 -0
- package/templates/skills/universal/verification.md +6 -0
package/src/commands/init.js
CHANGED
|
@@ -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
|
|
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(
|
|
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',
|
|
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',
|
|
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
|
}
|
package/src/commands/upgrade.js
CHANGED
|
@@ -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 {
|
|
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
|
|
package/src/core/config.js
CHANGED
|
@@ -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
|
+
}
|
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
|
|
|
@@ -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
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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,
|
|
271
|
+
const resolution = await promptHookConflict(category, conflictCandidate, workflowEntry);
|
|
256
272
|
|
|
257
273
|
if (resolution === 'replace') {
|
|
258
|
-
const idx = existingEntries.indexOf(
|
|
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(
|
|
280
|
+
const idx = existingEntries.indexOf(conflictCandidate);
|
|
265
281
|
existingEntries[idx] = {
|
|
266
|
-
matcher:
|
|
282
|
+
matcher: conflictCandidate.matcher,
|
|
267
283
|
hooks: [
|
|
268
284
|
{
|
|
269
285
|
type: 'command',
|
|
270
|
-
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
|
|
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
|
+
}
|
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
|
}
|
|
@@ -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([
|
|
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
|
|