worclaude 2.7.1 → 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.
Files changed (83) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +72 -56
  3. package/package.json +1 -1
  4. package/src/commands/doc-lint.js +37 -0
  5. package/src/commands/doctor.js +145 -0
  6. package/src/commands/init.js +144 -44
  7. package/src/commands/observability.js +24 -0
  8. package/src/commands/regenerate-routing.js +70 -0
  9. package/src/commands/status.js +14 -0
  10. package/src/commands/upgrade.js +87 -1
  11. package/src/commands/worktrees.js +90 -0
  12. package/src/core/config.js +10 -1
  13. package/src/core/file-categorizer.js +16 -0
  14. package/src/core/merger.js +42 -20
  15. package/src/core/scaffolder.js +26 -0
  16. package/src/data/agents.js +14 -28
  17. package/src/data/optional-features.js +46 -0
  18. package/src/generators/agent-routing.js +189 -34
  19. package/src/index.js +37 -0
  20. package/src/prompts/agent-selection.js +11 -3
  21. package/src/utils/agent-frontmatter.js +109 -0
  22. package/src/utils/doc-lint.js +196 -0
  23. package/src/utils/observability.js +300 -0
  24. package/templates/agents/optional/backend/api-designer.md +7 -1
  25. package/templates/agents/optional/backend/auth-auditor.md +7 -1
  26. package/templates/agents/optional/backend/database-analyst.md +7 -1
  27. package/templates/agents/optional/data/data-pipeline-reviewer.md +7 -1
  28. package/templates/agents/optional/data/ml-experiment-tracker.md +7 -1
  29. package/templates/agents/optional/data/prompt-engineer.md +7 -1
  30. package/templates/agents/optional/devops/ci-fixer.md +7 -1
  31. package/templates/agents/optional/devops/dependency-manager.md +7 -1
  32. package/templates/agents/optional/devops/deploy-validator.md +7 -1
  33. package/templates/agents/optional/devops/docker-helper.md +7 -1
  34. package/templates/agents/optional/docs/changelog-generator.md +9 -1
  35. package/templates/agents/optional/docs/doc-writer.md +7 -1
  36. package/templates/agents/optional/frontend/style-enforcer.md +7 -1
  37. package/templates/agents/optional/frontend/ui-reviewer.md +7 -1
  38. package/templates/agents/optional/quality/bug-fixer.md +19 -1
  39. package/templates/agents/optional/quality/build-fixer.md +7 -1
  40. package/templates/agents/optional/quality/performance-auditor.md +8 -1
  41. package/templates/agents/optional/quality/refactorer.md +7 -1
  42. package/templates/agents/optional/quality/security-reviewer.md +7 -1
  43. package/templates/agents/universal/build-validator.md +7 -1
  44. package/templates/agents/universal/code-simplifier.md +8 -1
  45. package/templates/agents/universal/plan-reviewer.md +8 -1
  46. package/templates/agents/universal/test-writer.md +19 -1
  47. package/templates/agents/universal/upstream-watcher.md +8 -1
  48. package/templates/agents/universal/verify-app.md +45 -3
  49. package/templates/commands/build-fix.md +30 -11
  50. package/templates/commands/commit-push-pr.md +47 -24
  51. package/templates/commands/compact-safe.md +79 -7
  52. package/templates/commands/conflict-resolver.md +7 -3
  53. package/templates/commands/end.md +63 -17
  54. package/templates/commands/learn.md +72 -8
  55. package/templates/commands/observability.md +59 -0
  56. package/templates/commands/refactor-clean.md +44 -2
  57. package/templates/commands/review-changes.md +40 -11
  58. package/templates/commands/review-plan.md +83 -10
  59. package/templates/commands/start.md +61 -30
  60. package/templates/commands/sync.md +86 -6
  61. package/templates/commands/test-coverage.md +78 -12
  62. package/templates/commands/update-claude-md.md +96 -7
  63. package/templates/commands/verify.md +32 -8
  64. package/templates/core/claude-md.md +9 -0
  65. package/templates/hooks/correction-detect.cjs +1 -1
  66. package/templates/hooks/learn-capture.cjs +0 -2
  67. package/templates/hooks/obs-agent-events.cjs +55 -0
  68. package/templates/hooks/obs-command-invocations.cjs +53 -0
  69. package/templates/hooks/obs-skill-loads.cjs +54 -0
  70. package/templates/hooks/skill-hint.cjs +22 -2
  71. package/templates/scripts/start-drift.sh +29 -0
  72. package/templates/scripts/sync-release-scope.sh +17 -0
  73. package/templates/scripts/test-coverage-changed-files.sh +14 -0
  74. package/templates/settings/base.json +73 -0
  75. package/templates/skills/universal/claude-md-maintenance.md +50 -14
  76. package/templates/skills/universal/git-conventions.md +11 -1
  77. package/templates/skills/universal/memory-architecture.md +115 -0
  78. package/templates/skills/universal/subagent-usage.md +15 -2
  79. package/src/data/agent-registry.js +0 -365
  80. package/templates/agents/optional/quality/e2e-runner.md +0 -98
  81. package/templates/commands/status.md +0 -15
  82. package/templates/commands/techdebt.md +0 -18
  83. package/templates/commands/upstream-check.md +0 -85
@@ -6,9 +6,9 @@ import {
6
6
  scaffoldAgentsMd,
7
7
  updateGitignore,
8
8
  scaffoldHooks,
9
- scaffoldPluginJson,
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 selectedAgents = await promptAgentSelection(selections.projectTypes);
74
- return { ...selections, selectedAgents };
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 { generatePluginJson, scaffoldGtdMemory } = await inquirer.prompt([
79
- {
80
- type: 'list',
81
- name: 'generatePluginJson',
82
- message: 'Generate .claude-plugin/plugin.json for marketplace compatibility?',
83
- choices: [
84
- { name: 'Yes', value: true },
85
- { name: 'No', value: false },
86
- ],
87
- default: selections.generatePluginJson === true ? 0 : 1,
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: 'scaffoldGtdMemory',
92
- message: 'Scaffold structured memory files (decisions.md, preferences.md)?',
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: 'No', value: false },
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: selections.scaffoldGtdMemory === true ? 0 : 1,
111
+ default: 1,
98
112
  },
99
113
  ]);
100
- return { ...selections, generatePluginJson, scaffoldGtdMemory };
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 extrasLabels = [];
146
- if (selections.generatePluginJson) extrasLabels.push('plugin.json');
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
- generatePluginJson: false,
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.scaffoldGtdMemory
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, projectTypes);
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
- // Opt-in: plugin.json for Claude Code marketplace compatibility
425
- if (selections.generatePluginJson) {
426
- await scaffoldPluginJson(projectRoot, selections);
427
- spinner.text = 'Created .claude-plugin/plugin.json';
428
- }
429
-
430
- // Opt-in: GTD memory scaffold (docs/memory/decisions.md, preferences.md)
431
- if (selections.scaffoldGtdMemory) {
432
- await scaffoldMemoryDocs(projectRoot);
433
- spinner.text = 'Created docs/memory/';
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
- if (selections.generatePluginJson) {
463
- display.success('.claude-plugin/plugin.json');
464
- }
465
- if (selections.scaffoldGtdMemory) {
466
- display.success('docs/memory/' + display.dimColor(' decisions.md, preferences.md'));
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
+ }
@@ -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 || {})) {
@@ -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
- display.success(`Already up to date (v${currentVersion}).`);
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
+ }
@@ -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
- return {
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) {