worclaude 2.8.0 → 2.9.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/CHANGELOG.md +53 -0
- package/README.md +72 -56
- package/package.json +1 -1
- package/src/commands/doc-lint.js +37 -0
- package/src/commands/doctor.js +77 -0
- package/src/commands/init.js +144 -44
- package/src/commands/observability.js +24 -0
- package/src/commands/regenerate-routing.js +70 -0
- package/src/commands/status.js +14 -0
- package/src/commands/upgrade.js +87 -1
- package/src/commands/worktrees.js +90 -0
- package/src/core/config.js +10 -1
- package/src/core/file-categorizer.js +16 -0
- package/src/core/merger.js +42 -20
- package/src/core/scaffolder.js +26 -0
- package/src/data/agents.js +14 -28
- package/src/data/optional-features.js +46 -0
- package/src/generators/agent-routing.js +189 -34
- package/src/index.js +37 -0
- package/src/prompts/agent-selection.js +11 -3
- package/src/utils/agent-frontmatter.js +109 -0
- package/src/utils/doc-lint.js +196 -0
- package/src/utils/observability.js +300 -0
- package/templates/agents/optional/backend/api-designer.md +7 -1
- package/templates/agents/optional/backend/auth-auditor.md +7 -1
- package/templates/agents/optional/backend/database-analyst.md +7 -1
- package/templates/agents/optional/data/data-pipeline-reviewer.md +7 -1
- package/templates/agents/optional/data/ml-experiment-tracker.md +7 -1
- package/templates/agents/optional/data/prompt-engineer.md +7 -1
- package/templates/agents/optional/devops/ci-fixer.md +7 -1
- package/templates/agents/optional/devops/dependency-manager.md +7 -1
- package/templates/agents/optional/devops/deploy-validator.md +7 -1
- package/templates/agents/optional/devops/docker-helper.md +7 -1
- package/templates/agents/optional/docs/changelog-generator.md +9 -1
- package/templates/agents/optional/docs/doc-writer.md +7 -1
- package/templates/agents/optional/frontend/style-enforcer.md +7 -1
- package/templates/agents/optional/frontend/ui-reviewer.md +7 -1
- package/templates/agents/optional/quality/bug-fixer.md +7 -1
- package/templates/agents/optional/quality/build-fixer.md +7 -1
- package/templates/agents/optional/quality/performance-auditor.md +8 -1
- package/templates/agents/optional/quality/refactorer.md +7 -1
- package/templates/agents/optional/quality/security-reviewer.md +7 -1
- package/templates/agents/universal/build-validator.md +7 -1
- package/templates/agents/universal/code-simplifier.md +8 -1
- package/templates/agents/universal/plan-reviewer.md +8 -1
- package/templates/agents/universal/test-writer.md +7 -1
- package/templates/agents/universal/upstream-watcher.md +8 -1
- package/templates/agents/universal/verify-app.md +33 -3
- package/templates/commands/build-fix.md +30 -11
- package/templates/commands/commit-push-pr.md +47 -24
- package/templates/commands/compact-safe.md +79 -7
- package/templates/commands/conflict-resolver.md +7 -3
- package/templates/commands/end.md +63 -17
- package/templates/commands/learn.md +72 -8
- package/templates/commands/observability.md +59 -0
- package/templates/commands/refactor-clean.md +44 -2
- package/templates/commands/review-changes.md +40 -11
- package/templates/commands/review-plan.md +83 -10
- package/templates/commands/start.md +61 -30
- package/templates/commands/sync.md +86 -6
- package/templates/commands/test-coverage.md +78 -12
- package/templates/commands/update-claude-md.md +96 -7
- package/templates/commands/verify.md +32 -8
- package/templates/core/claude-md.md +9 -0
- package/templates/hooks/correction-detect.cjs +1 -1
- package/templates/hooks/learn-capture.cjs +0 -2
- package/templates/hooks/obs-agent-events.cjs +55 -0
- package/templates/hooks/obs-command-invocations.cjs +53 -0
- package/templates/hooks/obs-skill-loads.cjs +54 -0
- package/templates/hooks/skill-hint.cjs +22 -2
- package/templates/scripts/start-drift.sh +29 -0
- package/templates/scripts/sync-release-scope.sh +17 -0
- package/templates/scripts/test-coverage-changed-files.sh +14 -0
- package/templates/settings/base.json +73 -0
- package/templates/skills/universal/claude-md-maintenance.md +50 -14
- package/templates/skills/universal/git-conventions.md +11 -1
- package/templates/skills/universal/memory-architecture.md +115 -0
- package/templates/skills/universal/subagent-usage.md +1 -1
- package/src/data/agent-registry.js +0 -365
- package/templates/agents/optional/quality/e2e-runner.md +0 -98
- package/templates/commands/status.md +0 -15
- package/templates/commands/techdebt.md +0 -18
- package/templates/commands/upstream-check.md +0 -85
package/src/commands/init.js
CHANGED
|
@@ -6,9 +6,9 @@ import {
|
|
|
6
6
|
scaffoldAgentsMd,
|
|
7
7
|
updateGitignore,
|
|
8
8
|
scaffoldHooks,
|
|
9
|
-
|
|
10
|
-
scaffoldMemoryDocs,
|
|
9
|
+
scaffoldScripts,
|
|
11
10
|
} from '../core/scaffolder.js';
|
|
11
|
+
import { OPTIONAL_FEATURES } from '../data/optional-features.js';
|
|
12
12
|
import {
|
|
13
13
|
computeFileHashes,
|
|
14
14
|
createWorkflowMeta,
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
CONFIRMATION_STEPS,
|
|
35
35
|
SPEC_MD_TEMPLATE_MAP,
|
|
36
36
|
} from '../data/agents.js';
|
|
37
|
-
import { buildAgentRoutingSkill } from '../generators/agent-routing.js';
|
|
37
|
+
import { buildAgentRoutingSkill, loadShippedAgents } from '../generators/agent-routing.js';
|
|
38
38
|
import { buildCommandsBlock } from '../core/variables.js';
|
|
39
39
|
|
|
40
40
|
// --- Helper functions ---
|
|
@@ -70,34 +70,61 @@ async function runTechStack(selections) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
async function runAgents(selections) {
|
|
73
|
-
const
|
|
74
|
-
return {
|
|
73
|
+
const result = await promptAgentSelection(selections.projectTypes);
|
|
74
|
+
return {
|
|
75
|
+
...selections,
|
|
76
|
+
selectedAgents: result.selectedAgents,
|
|
77
|
+
selectedCategories: result.selectedCategories,
|
|
78
|
+
additionalCategories: result.additionalCategories,
|
|
79
|
+
preSelectedCategories: result.preSelectedCategories,
|
|
80
|
+
};
|
|
75
81
|
}
|
|
76
82
|
|
|
77
83
|
async function runOptionalExtras(selections) {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
const previouslySelected = new Set(selections.optionalFeatures || []);
|
|
85
|
+
const questions = OPTIONAL_FEATURES.map((feature) => ({
|
|
86
|
+
type: 'list',
|
|
87
|
+
name: feature.id,
|
|
88
|
+
message: feature.label,
|
|
89
|
+
choices: [
|
|
90
|
+
{ name: 'Yes', value: true },
|
|
91
|
+
{ name: 'No', value: false },
|
|
92
|
+
],
|
|
93
|
+
default: previouslySelected.has(feature.id) ? 0 : 1,
|
|
94
|
+
}));
|
|
95
|
+
const answers = await inquirer.prompt(questions);
|
|
96
|
+
const optionalFeatures = OPTIONAL_FEATURES.filter((f) => answers[f.id]).map((f) => f.id);
|
|
97
|
+
|
|
98
|
+
// GitHub Action (`@claude` PR-comment workflow). Worclaude does NOT run
|
|
99
|
+
// the install — Claude Code provides /install-github-action and we point
|
|
100
|
+
// at it. Phase 7 T7.3.
|
|
101
|
+
const { installGithubAction } = await inquirer.prompt([
|
|
89
102
|
{
|
|
90
103
|
type: 'list',
|
|
91
|
-
name: '
|
|
92
|
-
message:
|
|
104
|
+
name: 'installGithubAction',
|
|
105
|
+
message:
|
|
106
|
+
'Install Claude Code\'s GitHub Action for the @claude "compounding engineering" workflow?',
|
|
93
107
|
choices: [
|
|
94
|
-
{ name: 'Yes', value: true },
|
|
95
|
-
{ name:
|
|
108
|
+
{ name: 'Yes — show me the install instructions now', value: true },
|
|
109
|
+
{ name: "No — I'll do it later", value: false },
|
|
96
110
|
],
|
|
97
|
-
default:
|
|
111
|
+
default: 1,
|
|
98
112
|
},
|
|
99
113
|
]);
|
|
100
|
-
|
|
114
|
+
|
|
115
|
+
return { ...selections, optionalFeatures, installGithubAction };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function displayGithubActionHint(selections) {
|
|
119
|
+
if (!selections.installGithubAction) return;
|
|
120
|
+
display.newline();
|
|
121
|
+
display.barLine('GitHub Action setup:');
|
|
122
|
+
display.barLine(
|
|
123
|
+
` Run ${display.white('/install-github-action')} inside Claude Code to enable the @claude workflow.`
|
|
124
|
+
);
|
|
125
|
+
display.barLine(
|
|
126
|
+
` See ${display.dimColor('docs/guide/claude-code-integration.md#github-action-integration-claude-pattern')} for details.`
|
|
127
|
+
);
|
|
101
128
|
}
|
|
102
129
|
|
|
103
130
|
const STEP_RUNNERS = {
|
|
@@ -142,9 +169,8 @@ async function showConfirmation(selections) {
|
|
|
142
169
|
` ${'Agents'.padEnd(10)}${display.white(`${universalCount} universal + ${optionalCount} optional`)} ${display.dimColor(`(${totalCount} total)`)}`
|
|
143
170
|
);
|
|
144
171
|
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
if (selections.scaffoldGtdMemory) extrasLabels.push('memory docs');
|
|
172
|
+
const opted = new Set(selections.optionalFeatures || []);
|
|
173
|
+
const extrasLabels = OPTIONAL_FEATURES.filter((f) => opted.has(f.id)).map((f) => f.extrasLabel);
|
|
148
174
|
if (extrasLabels.length > 0) {
|
|
149
175
|
console.log(` ${'Extras'.padEnd(10)}${display.white(extrasLabels.join(', '))}`);
|
|
150
176
|
}
|
|
@@ -177,8 +203,7 @@ function createInitialSelections(projectRoot) {
|
|
|
177
203
|
languages: [],
|
|
178
204
|
useDocker: false,
|
|
179
205
|
selectedAgents: [],
|
|
180
|
-
|
|
181
|
-
scaffoldGtdMemory: false,
|
|
206
|
+
optionalFeatures: [],
|
|
182
207
|
};
|
|
183
208
|
}
|
|
184
209
|
|
|
@@ -263,7 +288,7 @@ function buildTemplateVariables(selections) {
|
|
|
263
288
|
);
|
|
264
289
|
const skillsText = skillsLines.join('\n');
|
|
265
290
|
|
|
266
|
-
const memoryArchitectureExtras = selections.
|
|
291
|
+
const memoryArchitectureExtras = (selections.optionalFeatures || []).includes('gtd-memory')
|
|
267
292
|
? '\n- Team decisions: `docs/memory/decisions.md` (version-controlled, shared).\n- Team preferences: `docs/memory/preferences.md` (version-controlled, shared).'
|
|
268
293
|
: '';
|
|
269
294
|
|
|
@@ -281,6 +306,56 @@ function buildTemplateVariables(selections) {
|
|
|
281
306
|
};
|
|
282
307
|
}
|
|
283
308
|
|
|
309
|
+
export function buildInstallationRationale(selections) {
|
|
310
|
+
const projectTypes = selections.projectTypes || [];
|
|
311
|
+
const preSelected = new Set(selections.preSelectedCategories || []);
|
|
312
|
+
const picked = new Set(selections.selectedCategories || []);
|
|
313
|
+
const added = selections.additionalCategories || [];
|
|
314
|
+
|
|
315
|
+
const autoMatched = [...picked].filter((c) => preSelected.has(c));
|
|
316
|
+
const removedAuto = [...preSelected].filter((c) => !picked.has(c));
|
|
317
|
+
const manualPicked = [...picked].filter((c) => !preSelected.has(c));
|
|
318
|
+
|
|
319
|
+
const decisions = [];
|
|
320
|
+
if (autoMatched.length > 0) {
|
|
321
|
+
decisions.push(
|
|
322
|
+
`Accepted auto-recommended categories from project type(s) ${projectTypes.join(', ') || '(none)'}: ${autoMatched.join(', ')}.`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
if (removedAuto.length > 0) {
|
|
326
|
+
decisions.push(`Removed auto-recommended categories: ${removedAuto.join(', ')}.`);
|
|
327
|
+
}
|
|
328
|
+
if (manualPicked.length > 0) {
|
|
329
|
+
decisions.push(
|
|
330
|
+
`Added categories beyond the project-type recommendations: ${manualPicked.join(', ')}.`
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
if (added.length > 0) {
|
|
334
|
+
decisions.push(`Opted into extra categories at the second prompt: ${added.join(', ')}.`);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
let rationale;
|
|
338
|
+
if (
|
|
339
|
+
autoMatched.length > 0 &&
|
|
340
|
+
removedAuto.length === 0 &&
|
|
341
|
+
manualPicked.length === 0 &&
|
|
342
|
+
added.length === 0
|
|
343
|
+
) {
|
|
344
|
+
rationale = `Auto-selected from project type(s) '${projectTypes.join(', ')}'.`;
|
|
345
|
+
} else if (decisions.length === 0) {
|
|
346
|
+
rationale = 'No optional categories selected.';
|
|
347
|
+
} else {
|
|
348
|
+
rationale = decisions.join(' ');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
projectTypes,
|
|
353
|
+
selectedCategories: [...picked, ...added],
|
|
354
|
+
rationale,
|
|
355
|
+
userDecisions: decisions,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
284
359
|
async function computeAndWriteWorkflowMeta(projectRoot, selections, version) {
|
|
285
360
|
const fileHashes = await computeFileHashes(projectRoot);
|
|
286
361
|
|
|
@@ -292,6 +367,11 @@ async function computeAndWriteWorkflowMeta(projectRoot, selections, version) {
|
|
|
292
367
|
optionalAgents: selections.selectedAgents,
|
|
293
368
|
useDocker: selections.useDocker || false,
|
|
294
369
|
fileHashes,
|
|
370
|
+
installation: buildInstallationRationale(selections),
|
|
371
|
+
optionalFeatures: selections.optionalFeatures || [],
|
|
372
|
+
optedOutFeatures: OPTIONAL_FEATURES.filter(
|
|
373
|
+
(f) => !(selections.optionalFeatures || []).includes(f.id)
|
|
374
|
+
).map((f) => f.id),
|
|
295
375
|
});
|
|
296
376
|
await writeWorkflowMeta(projectRoot, meta);
|
|
297
377
|
}
|
|
@@ -368,7 +448,7 @@ async function scaffoldFresh(projectRoot, selections, variables, settingsStr, ve
|
|
|
368
448
|
}
|
|
369
449
|
spinner.text = `Created ${TEMPLATE_SKILLS.length} template skills`;
|
|
370
450
|
|
|
371
|
-
const agentRoutingContent = buildAgentRoutingSkill(selectedAgents
|
|
451
|
+
const agentRoutingContent = buildAgentRoutingSkill(await loadShippedAgents(selectedAgents));
|
|
372
452
|
await writeFile(
|
|
373
453
|
path.join(projectRoot, '.claude', 'skills', 'agent-routing', 'SKILL.md'),
|
|
374
454
|
agentRoutingContent
|
|
@@ -417,20 +497,33 @@ async function scaffoldFresh(projectRoot, selections, variables, settingsStr, ve
|
|
|
417
497
|
await scaffoldHooks(projectRoot);
|
|
418
498
|
spinner.text = 'Created .claude/hooks/';
|
|
419
499
|
|
|
500
|
+
// Copy slash-command helper scripts (.claude/scripts/)
|
|
501
|
+
await scaffoldScripts(projectRoot);
|
|
502
|
+
spinner.text = 'Created .claude/scripts/';
|
|
503
|
+
|
|
420
504
|
// Create learnings directory for correction capture
|
|
421
505
|
await writeFile(path.join(projectRoot, '.claude', 'learnings', '.gitkeep'), '');
|
|
422
506
|
spinner.text = 'Created .claude/learnings/';
|
|
423
507
|
|
|
424
|
-
//
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
508
|
+
// Create scratch directory for SHA-keyed transient artifacts (gitignored)
|
|
509
|
+
await writeFile(path.join(projectRoot, '.claude', 'scratch', '.gitkeep'), '');
|
|
510
|
+
spinner.text = 'Created .claude/scratch/';
|
|
511
|
+
|
|
512
|
+
// Create plans directory for active work guidance (tracked)
|
|
513
|
+
await writeFile(path.join(projectRoot, '.claude', 'plans', '.gitkeep'), '');
|
|
514
|
+
spinner.text = 'Created .claude/plans/';
|
|
515
|
+
|
|
516
|
+
// Create observability directory for hook-captured event logs (gitignored)
|
|
517
|
+
await writeFile(path.join(projectRoot, '.claude', 'observability', '.gitkeep'), '');
|
|
518
|
+
spinner.text = 'Created .claude/observability/';
|
|
519
|
+
|
|
520
|
+
// Opt-in: scaffold each optional feature the user selected. Scaffolders
|
|
521
|
+
// are idempotent — if a file already exists they skip it.
|
|
522
|
+
const optedIn = new Set(selections.optionalFeatures || []);
|
|
523
|
+
for (const feature of OPTIONAL_FEATURES) {
|
|
524
|
+
if (!optedIn.has(feature.id)) continue;
|
|
525
|
+
await feature.scaffold(projectRoot, selections);
|
|
526
|
+
spinner.text = `Created ${feature.successPath}`;
|
|
434
527
|
}
|
|
435
528
|
|
|
436
529
|
await computeAndWriteWorkflowMeta(projectRoot, selections, version);
|
|
@@ -459,11 +552,16 @@ function displayFreshSuccess(selections, skipped) {
|
|
|
459
552
|
display.success(`.claude/skills/${display.dimColor(` ${totalSkills} skills`)}`);
|
|
460
553
|
display.success('.claude/sessions/');
|
|
461
554
|
display.success('.claude/hooks/');
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
555
|
+
const optedIn = new Set(selections.optionalFeatures || []);
|
|
556
|
+
for (const feature of OPTIONAL_FEATURES) {
|
|
557
|
+
if (!optedIn.has(feature.id)) continue;
|
|
558
|
+
if (feature.successDetail) {
|
|
559
|
+
display.success(
|
|
560
|
+
`${feature.successPath}${display.dimColor(` ${feature.successDetail}`)}`
|
|
561
|
+
);
|
|
562
|
+
} else {
|
|
563
|
+
display.success(feature.successPath);
|
|
564
|
+
}
|
|
467
565
|
}
|
|
468
566
|
display.success('.mcp.json');
|
|
469
567
|
display.success('.gitignore');
|
|
@@ -706,6 +804,7 @@ export async function initCommand() {
|
|
|
706
804
|
const { settingsStr } = await buildSettingsJson(selections.languages, selections.useDocker);
|
|
707
805
|
const skipped = await scaffoldFresh(projectRoot, selections, variables, settingsStr, version);
|
|
708
806
|
displayFreshSuccess(selections, skipped);
|
|
807
|
+
displayGithubActionHint(selections);
|
|
709
808
|
} else {
|
|
710
809
|
// Scenario B: merge
|
|
711
810
|
const spinner = ora('Merging workflow...').start();
|
|
@@ -717,6 +816,7 @@ export async function initCommand() {
|
|
|
717
816
|
await computeAndWriteWorkflowMeta(projectRoot, selections, version);
|
|
718
817
|
spinner.succeed('Workflow merged successfully!');
|
|
719
818
|
displayMergeReport(report, backupPath);
|
|
819
|
+
displayGithubActionHint(selections);
|
|
720
820
|
} catch (err) {
|
|
721
821
|
spinner.fail('Failed to merge workflow');
|
|
722
822
|
display.error(err.message);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { computeReport, renderMarkdown } from '../utils/observability.js';
|
|
4
|
+
import * as display from '../utils/display.js';
|
|
5
|
+
|
|
6
|
+
export async function observabilityCommand(options = {}) {
|
|
7
|
+
const projectRoot = process.cwd();
|
|
8
|
+
const report = await computeReport(projectRoot);
|
|
9
|
+
|
|
10
|
+
const output = options.json ? JSON.stringify(report, null, 2) : renderMarkdown(report);
|
|
11
|
+
|
|
12
|
+
if (options.out) {
|
|
13
|
+
const outPath = path.isAbsolute(options.out)
|
|
14
|
+
? options.out
|
|
15
|
+
: path.join(projectRoot, options.out);
|
|
16
|
+
await fs.ensureDir(path.dirname(outPath));
|
|
17
|
+
await fs.writeFile(outPath, output);
|
|
18
|
+
display.success(`Wrote observability report to ${path.relative(projectRoot, outPath)}`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
process.stdout.write(output);
|
|
23
|
+
if (!output.endsWith('\n')) process.stdout.write('\n');
|
|
24
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
3
|
+
import {
|
|
4
|
+
buildAgentRoutingSkill,
|
|
5
|
+
regenerateAgentRoutingContent,
|
|
6
|
+
} from '../generators/agent-routing.js';
|
|
7
|
+
import { loadAgentsFromDir } from '../utils/agent-frontmatter.js';
|
|
8
|
+
import * as display from '../utils/display.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Regenerate `.claude/skills/agent-routing/SKILL.md` from the project's
|
|
12
|
+
* installed agent files. Preserves any user-authored prose that lives
|
|
13
|
+
* outside the AUTO-GENERATED markers.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} projectRoot
|
|
16
|
+
* @returns {Promise<{regenerated: boolean, agentsDir: string, skillPath: string, count: number, reason?: string}>}
|
|
17
|
+
*/
|
|
18
|
+
export async function regenerateRoutingForProject(projectRoot) {
|
|
19
|
+
const agentsDir = path.join(projectRoot, '.claude', 'agents');
|
|
20
|
+
const skillDir = path.join(projectRoot, '.claude', 'skills', 'agent-routing');
|
|
21
|
+
const skillPath = path.join(skillDir, 'SKILL.md');
|
|
22
|
+
|
|
23
|
+
const agents = await loadAgentsFromDir(agentsDir);
|
|
24
|
+
if (agents.length === 0) {
|
|
25
|
+
return {
|
|
26
|
+
regenerated: false,
|
|
27
|
+
agentsDir,
|
|
28
|
+
skillPath,
|
|
29
|
+
count: 0,
|
|
30
|
+
reason: 'no agent files found',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let existing = null;
|
|
35
|
+
try {
|
|
36
|
+
existing = await readFile(skillPath, 'utf8');
|
|
37
|
+
} catch (err) {
|
|
38
|
+
if (err.code !== 'ENOENT') throw err;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const content = existing
|
|
42
|
+
? regenerateAgentRoutingContent(existing, agents)
|
|
43
|
+
: buildAgentRoutingSkill(agents);
|
|
44
|
+
|
|
45
|
+
await mkdir(skillDir, { recursive: true });
|
|
46
|
+
await writeFile(skillPath, content, 'utf8');
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
regenerated: true,
|
|
50
|
+
agentsDir,
|
|
51
|
+
skillPath,
|
|
52
|
+
count: agents.length,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function regenerateRoutingCommand() {
|
|
57
|
+
const projectRoot = process.cwd();
|
|
58
|
+
try {
|
|
59
|
+
const result = await regenerateRoutingForProject(projectRoot);
|
|
60
|
+
if (!result.regenerated) {
|
|
61
|
+
display.warn(`Skipped: ${result.reason} at ${path.relative(projectRoot, result.agentsDir)}/`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const rel = path.relative(projectRoot, result.skillPath);
|
|
65
|
+
display.success(`Regenerated ${rel} from ${result.count} agent file(s).`);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
display.error(`Failed to regenerate agent-routing skill: ${err.message}`);
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
}
|
|
70
|
+
}
|
package/src/commands/status.js
CHANGED
|
@@ -73,6 +73,20 @@ export async function statusCommand() {
|
|
|
73
73
|
display.barLine(`${'Skills'.padEnd(11)}${display.white(String(skillCount))}`);
|
|
74
74
|
display.newline();
|
|
75
75
|
|
|
76
|
+
// Installation rationale (T3.6) — recorded at init time. Older installs
|
|
77
|
+
// pre-dating this field gracefully degrade: nothing surfaces.
|
|
78
|
+
if (meta.installation && meta.installation.rationale) {
|
|
79
|
+
display.info('Installation rationale:');
|
|
80
|
+
display.dim(` ${meta.installation.rationale}`);
|
|
81
|
+
if (
|
|
82
|
+
Array.isArray(meta.installation.selectedCategories) &&
|
|
83
|
+
meta.installation.selectedCategories.length > 0
|
|
84
|
+
) {
|
|
85
|
+
display.dim(` Categories: ${meta.installation.selectedCategories.join(', ')}`);
|
|
86
|
+
}
|
|
87
|
+
display.newline();
|
|
88
|
+
}
|
|
89
|
+
|
|
76
90
|
// Customized files
|
|
77
91
|
const customized = [];
|
|
78
92
|
for (const [key, storedHash] of Object.entries(meta.fileHashes || {})) {
|
package/src/commands/upgrade.js
CHANGED
|
@@ -26,6 +26,8 @@ import {
|
|
|
26
26
|
patchAgentDescriptions,
|
|
27
27
|
migrateWorkflowRefLocation,
|
|
28
28
|
} from '../core/migration.js';
|
|
29
|
+
import { regenerateRoutingForProject } from './regenerate-routing.js';
|
|
30
|
+
import { availableOptionalFeatures } from '../data/optional-features.js';
|
|
29
31
|
|
|
30
32
|
const CONFLICT_CHECK_TYPES = new Set(['hook', 'root-file']);
|
|
31
33
|
|
|
@@ -323,12 +325,63 @@ async function runRepairOnlyFlow({
|
|
|
323
325
|
display.newline();
|
|
324
326
|
display.barLine(`Review files under .claude/workflow-ref/ and merge what's useful.`);
|
|
325
327
|
}
|
|
328
|
+
|
|
329
|
+
const features = await promptAndScaffoldOptionalFeatures(projectRoot, meta, { yes, dryRun });
|
|
330
|
+
if (features.scaffolded.length + features.declined.length > 0) {
|
|
331
|
+
await writeWorkflowMeta(projectRoot, meta);
|
|
332
|
+
}
|
|
326
333
|
} catch (err) {
|
|
327
334
|
spinner.fail('Repair failed.');
|
|
328
335
|
display.error(err.message);
|
|
329
336
|
}
|
|
330
337
|
}
|
|
331
338
|
|
|
339
|
+
async function promptAndScaffoldOptionalFeatures(projectRoot, meta, { yes, dryRun }) {
|
|
340
|
+
const empty = { scaffolded: [], declined: [] };
|
|
341
|
+
if (yes || dryRun) return empty;
|
|
342
|
+
|
|
343
|
+
const available = await availableOptionalFeatures(projectRoot, meta);
|
|
344
|
+
if (available.length === 0) return empty;
|
|
345
|
+
|
|
346
|
+
const upgradeSelections = {
|
|
347
|
+
projectName: path.basename(projectRoot),
|
|
348
|
+
selectedAgents: meta.optionalAgents || [],
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
display.newline();
|
|
352
|
+
display.barLine('Optional features available:');
|
|
353
|
+
|
|
354
|
+
const scaffolded = [];
|
|
355
|
+
const declined = [];
|
|
356
|
+
|
|
357
|
+
for (const feature of available) {
|
|
358
|
+
const { accept } = await inquirer.prompt([
|
|
359
|
+
{
|
|
360
|
+
type: 'list',
|
|
361
|
+
name: 'accept',
|
|
362
|
+
message: feature.label,
|
|
363
|
+
choices: [
|
|
364
|
+
{ name: 'Yes', value: true },
|
|
365
|
+
{ name: 'No, skip (will not ask again)', value: false },
|
|
366
|
+
],
|
|
367
|
+
},
|
|
368
|
+
]);
|
|
369
|
+
|
|
370
|
+
if (accept) {
|
|
371
|
+
await feature.scaffold(projectRoot, upgradeSelections);
|
|
372
|
+
scaffolded.push(feature.id);
|
|
373
|
+
display.success(feature.successPath);
|
|
374
|
+
} else {
|
|
375
|
+
declined.push(feature.id);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
meta.optionalFeatures = [...new Set([...(meta.optionalFeatures || []), ...scaffolded])];
|
|
380
|
+
meta.optedOutFeatures = [...new Set([...(meta.optedOutFeatures || []), ...declined])];
|
|
381
|
+
|
|
382
|
+
return { scaffolded, declined };
|
|
383
|
+
}
|
|
384
|
+
|
|
332
385
|
export async function upgradeCommand(options = {}) {
|
|
333
386
|
const { dryRun = false, yes = false, repairOnly = false } = options;
|
|
334
387
|
const projectRoot = process.cwd();
|
|
@@ -402,7 +455,14 @@ export async function upgradeCommand(options = {}) {
|
|
|
402
455
|
|
|
403
456
|
// Version match + no repair + no template work + no ref relocation → up to date.
|
|
404
457
|
if (versionMatch && !repairWork && !templateWork && !refWork) {
|
|
405
|
-
|
|
458
|
+
const features = await promptAndScaffoldOptionalFeatures(projectRoot, meta, { yes, dryRun });
|
|
459
|
+
if (features.scaffolded.length + features.declined.length > 0) {
|
|
460
|
+
meta.lastUpdated = new Date().toISOString();
|
|
461
|
+
await writeWorkflowMeta(projectRoot, meta);
|
|
462
|
+
}
|
|
463
|
+
if (features.scaffolded.length === 0) {
|
|
464
|
+
display.success(`Already up to date (v${currentVersion}).`);
|
|
465
|
+
}
|
|
406
466
|
return;
|
|
407
467
|
}
|
|
408
468
|
|
|
@@ -528,6 +588,15 @@ export async function upgradeCommand(options = {}) {
|
|
|
528
588
|
// Ensure sessions directory exists for session persistence
|
|
529
589
|
await writeFile(path.join(projectRoot, '.claude', 'sessions', '.gitkeep'), '');
|
|
530
590
|
|
|
591
|
+
// Ensure scratch directory exists for SHA-keyed transient artifacts (gitignored)
|
|
592
|
+
await writeFile(path.join(projectRoot, '.claude', 'scratch', '.gitkeep'), '');
|
|
593
|
+
|
|
594
|
+
// Ensure plans directory exists for active work guidance (tracked)
|
|
595
|
+
await writeFile(path.join(projectRoot, '.claude', 'plans', '.gitkeep'), '');
|
|
596
|
+
|
|
597
|
+
// Ensure observability directory exists for hook-captured event logs (gitignored)
|
|
598
|
+
await writeFile(path.join(projectRoot, '.claude', 'observability', '.gitkeep'), '');
|
|
599
|
+
|
|
531
600
|
// Hash refresh — files we just wrote (repair restored, repair migrated,
|
|
532
601
|
// autoUpdate, templateNewFiles). Modified / conflict / unchanged /
|
|
533
602
|
// userAdded / missingUntracked keep their prior hash; missingUntracked
|
|
@@ -548,6 +617,18 @@ export async function upgradeCommand(options = {}) {
|
|
|
548
617
|
|
|
549
618
|
await updateGitignore(projectRoot);
|
|
550
619
|
|
|
620
|
+
try {
|
|
621
|
+
const routingResult = await regenerateRoutingForProject(projectRoot);
|
|
622
|
+
if (routingResult.regenerated) {
|
|
623
|
+
const rel = path.relative(projectRoot, routingResult.skillPath);
|
|
624
|
+
if (rel in fileHashes || (await fs.pathExists(routingResult.skillPath))) {
|
|
625
|
+
fileHashes[rel] = await hashFile(routingResult.skillPath);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
} catch (err) {
|
|
629
|
+
display.warn(`agent-routing regeneration skipped: ${err.message}`);
|
|
630
|
+
}
|
|
631
|
+
|
|
551
632
|
meta.version = currentVersion;
|
|
552
633
|
meta.lastUpdated = new Date().toISOString();
|
|
553
634
|
meta.fileHashes = fileHashes;
|
|
@@ -619,6 +700,11 @@ export async function upgradeCommand(options = {}) {
|
|
|
619
700
|
display.newline();
|
|
620
701
|
display.barLine(`Review files under .claude/workflow-ref/ and merge what's useful.`);
|
|
621
702
|
}
|
|
703
|
+
|
|
704
|
+
const features = await promptAndScaffoldOptionalFeatures(projectRoot, meta, { yes, dryRun });
|
|
705
|
+
if (features.scaffolded.length + features.declined.length > 0) {
|
|
706
|
+
await writeWorkflowMeta(projectRoot, meta);
|
|
707
|
+
}
|
|
622
708
|
} catch (err) {
|
|
623
709
|
spinner.fail('Upgrade failed.');
|
|
624
710
|
display.error(err.message);
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import * as display from '../utils/display.js';
|
|
5
|
+
|
|
6
|
+
function runGit(cwd, args) {
|
|
7
|
+
const result = spawnSync('git', args, {
|
|
8
|
+
cwd,
|
|
9
|
+
encoding: 'utf8',
|
|
10
|
+
timeout: 10000,
|
|
11
|
+
});
|
|
12
|
+
return {
|
|
13
|
+
status: result.status,
|
|
14
|
+
stdout: (result.stdout || '').trim(),
|
|
15
|
+
stderr: (result.stderr || '').trim(),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function listWorktreeDirs(worktreesDir) {
|
|
20
|
+
const entries = await fs.readdir(worktreesDir).catch(() => []);
|
|
21
|
+
const dirs = [];
|
|
22
|
+
for (const name of entries) {
|
|
23
|
+
if (name.startsWith('.')) continue;
|
|
24
|
+
const full = path.join(worktreesDir, name);
|
|
25
|
+
const stat = await fs.stat(full).catch(() => null);
|
|
26
|
+
if (stat && stat.isDirectory()) dirs.push({ name, full });
|
|
27
|
+
}
|
|
28
|
+
return dirs;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function worktreesCleanCommand(options = {}) {
|
|
32
|
+
const projectRoot = options.path || process.cwd();
|
|
33
|
+
const worktreesDir = path.join(projectRoot, '.claude', 'worktrees');
|
|
34
|
+
|
|
35
|
+
if (!(await fs.pathExists(worktreesDir))) {
|
|
36
|
+
display.info('No .claude/worktrees/ directory — nothing to clean.');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const dirs = await listWorktreeDirs(worktreesDir);
|
|
41
|
+
if (dirs.length === 0) {
|
|
42
|
+
display.info('.claude/worktrees/ is empty — nothing to clean.');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
display.info(`Found ${dirs.length} worktree entr${dirs.length === 1 ? 'y' : 'ies'}.`);
|
|
47
|
+
display.newline();
|
|
48
|
+
|
|
49
|
+
const removed = [];
|
|
50
|
+
const failed = [];
|
|
51
|
+
|
|
52
|
+
for (const { name, full } of dirs) {
|
|
53
|
+
// `git worktree remove -f -f` force-removes locked worktrees. The double
|
|
54
|
+
// `-f` is required because the first `-f` only overrides the lock check,
|
|
55
|
+
// and the second overrides "the worktree contains modifications" check.
|
|
56
|
+
const removeResult = runGit(projectRoot, ['worktree', 'remove', '-f', '-f', full]);
|
|
57
|
+
|
|
58
|
+
if (removeResult.status === 0) {
|
|
59
|
+
removed.push(name);
|
|
60
|
+
display.success(`Removed ${name}`);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// git worktree remove may fail when the worktree is unregistered (the
|
|
65
|
+
// directory exists but git no longer tracks it). Fall back to rm + prune.
|
|
66
|
+
const rmResult = await fs.remove(full).then(
|
|
67
|
+
() => ({ ok: true }),
|
|
68
|
+
(err) => ({ ok: false, err })
|
|
69
|
+
);
|
|
70
|
+
if (rmResult.ok) {
|
|
71
|
+
runGit(projectRoot, ['worktree', 'prune']);
|
|
72
|
+
removed.push(name);
|
|
73
|
+
display.success(`Removed ${name} (orphaned, fs cleanup)`);
|
|
74
|
+
} else {
|
|
75
|
+
failed.push({ name, reason: removeResult.stderr || rmResult.err?.message });
|
|
76
|
+
display.warn(`Failed to remove ${name}: ${removeResult.stderr || rmResult.err?.message}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
display.newline();
|
|
81
|
+
display.info(
|
|
82
|
+
`Cleaned ${removed.length}/${dirs.length} worktrees${
|
|
83
|
+
failed.length > 0 ? ` (${failed.length} failed)` : ''
|
|
84
|
+
}.`
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
if (failed.length > 0) {
|
|
88
|
+
process.exitCode = 1;
|
|
89
|
+
}
|
|
90
|
+
}
|
package/src/core/config.js
CHANGED
|
@@ -24,9 +24,12 @@ export function createWorkflowMeta({
|
|
|
24
24
|
fileHashes = {},
|
|
25
25
|
version,
|
|
26
26
|
useDocker = false,
|
|
27
|
+
installation = null,
|
|
28
|
+
optionalFeatures = [],
|
|
29
|
+
optedOutFeatures = [],
|
|
27
30
|
}) {
|
|
28
31
|
const now = new Date().toISOString();
|
|
29
|
-
|
|
32
|
+
const meta = {
|
|
30
33
|
version: version || '1.0.0',
|
|
31
34
|
installedAt: now,
|
|
32
35
|
lastUpdated: now,
|
|
@@ -36,7 +39,13 @@ export function createWorkflowMeta({
|
|
|
36
39
|
optionalAgents,
|
|
37
40
|
useDocker,
|
|
38
41
|
fileHashes,
|
|
42
|
+
optionalFeatures,
|
|
43
|
+
optedOutFeatures,
|
|
39
44
|
};
|
|
45
|
+
if (installation) {
|
|
46
|
+
meta.installation = installation;
|
|
47
|
+
}
|
|
48
|
+
return meta;
|
|
40
49
|
}
|
|
41
50
|
|
|
42
51
|
export async function readWorkflowMeta(projectRoot) {
|