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.
Files changed (83) hide show
  1. package/CHANGELOG.md +53 -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 +77 -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 +7 -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 +7 -1
  47. package/templates/agents/universal/upstream-watcher.md +8 -1
  48. package/templates/agents/universal/verify-app.md +33 -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 +1 -1
  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
@@ -16,6 +16,7 @@ const ALWAYS_SCAFFOLDED_TYPES = new Set([
16
16
  'command',
17
17
  'universal-skill',
18
18
  'hook',
19
+ 'script',
19
20
  'root-file',
20
21
  ]);
21
22
 
@@ -150,6 +151,21 @@ export async function buildTemplateHashMap() {
150
151
  }
151
152
  }
152
153
 
154
+ // Slash-command helper scripts: collapse multi-line bash blocks into a
155
+ // single allowed invocation. Walked from templates/scripts/ so new helpers
156
+ // flow through automatically.
157
+ const scriptsDir = path.join(getTemplatesDir(), 'scripts');
158
+ if (await fs.pathExists(scriptsDir)) {
159
+ const entries = await fs.readdir(scriptsDir);
160
+ for (const entry of entries) {
161
+ if (!entry.endsWith('.sh')) continue;
162
+ const key = `scripts/${entry}`;
163
+ const templatePath = `scripts/${entry}`;
164
+ const content = await readTemplate(templatePath);
165
+ map[key] = { templatePath, hash: hashContent(content), type: 'script' };
166
+ }
167
+ }
168
+
153
169
  // Root-level files: key = root/<path>, templatePath points into templates/
154
170
  // AGENTS.md needs variable substitution at scaffold time; the raw-template hash
155
171
  // is used only to detect drift against a previously-substituted install hash,
@@ -8,9 +8,9 @@ import {
8
8
  scaffoldAgentsMd,
9
9
  mergeSettings,
10
10
  scaffoldHooks,
11
- scaffoldPluginJson,
12
- scaffoldMemoryDocs,
11
+ scaffoldScripts,
13
12
  } from './scaffolder.js';
13
+ import { OPTIONAL_FEATURES } from '../data/optional-features.js';
14
14
  import { workflowRefRelPath } from './file-categorizer.js';
15
15
  import { promptHookConflict } from '../prompts/conflict-resolution.js';
16
16
  import {
@@ -28,7 +28,7 @@ import {
28
28
  NOTIFICATION_COMMANDS,
29
29
  SPEC_MD_TEMPLATE_MAP,
30
30
  } from '../data/agents.js';
31
- import { buildAgentRoutingSkill } from '../generators/agent-routing.js';
31
+ import { buildAgentRoutingSkill, loadShippedAgents } from '../generators/agent-routing.js';
32
32
  import * as display from '../utils/display.js';
33
33
 
34
34
  // --- Settings builder (shared with Scenario A) ---
@@ -144,7 +144,7 @@ async function mergeSkills(projectRoot, existingScan, variables, report, selecti
144
144
 
145
145
  // Generated skill: agent-routing
146
146
  const skillsDir = path.join('.claude', 'skills');
147
- const routingContent = buildAgentRoutingSkill(selections.selectedAgents, selections.projectTypes);
147
+ const routingContent = buildAgentRoutingSkill(await loadShippedAgents(selections.selectedAgents));
148
148
  const routingExistsAsDir = existingScan.existingSkillDirs.includes('agent-routing');
149
149
  const routingExistsAsFlat = existingScan.existingSkills.includes('agent-routing.md');
150
150
 
@@ -240,13 +240,21 @@ export async function mergeSettingsPermissionsAndHooks(
240
240
  const existingRaw = await readFile(path.join(projectRoot, '.claude', 'settings.json'));
241
241
  const existing = parseUserJson(existingRaw, '.claude/settings.json');
242
242
 
243
- // Merge permissions (Tier 1)
243
+ // Merge permissions (Tier 1) — union-merge both allow and deny
244
244
  const existingAllow = existing.permissions?.allow || [];
245
245
  const workflowAllow = workflowSettings.permissions?.allow || [];
246
- const newPerms = workflowAllow.filter((p) => !existingAllow.includes(p));
246
+ const newAllow = workflowAllow.filter((p) => !existingAllow.includes(p));
247
247
  if (!existing.permissions) existing.permissions = {};
248
- existing.permissions.allow = [...existingAllow, ...newPerms];
249
- report.added.permissions = newPerms.length;
248
+ existing.permissions.allow = [...existingAllow, ...newAllow];
249
+
250
+ const existingDeny = existing.permissions?.deny || [];
251
+ const workflowDeny = workflowSettings.permissions?.deny || [];
252
+ const newDeny = workflowDeny.filter((p) => !existingDeny.includes(p));
253
+ if (newDeny.length > 0 || existingDeny.length > 0) {
254
+ existing.permissions.deny = [...existingDeny, ...newDeny];
255
+ }
256
+
257
+ report.added.permissions = newAllow.length + newDeny.length;
250
258
 
251
259
  // Merge hooks (Tier 1 + Tier 3)
252
260
  if (!existing.hooks) existing.hooks = {};
@@ -333,7 +341,9 @@ async function mergeSettingsJson(projectRoot, existingScan, selections, report)
333
341
  if (!existingScan.hasSettingsJson) {
334
342
  // No existing settings — create fresh
335
343
  await writeFile(path.join(projectRoot, '.claude', 'settings.json'), settingsStr);
336
- report.added.permissions = workflowSettings.permissions?.allow?.length || 0;
344
+ report.added.permissions =
345
+ (workflowSettings.permissions?.allow?.length || 0) +
346
+ (workflowSettings.permissions?.deny?.length || 0);
337
347
  report.added.hooks = countHooks(workflowSettings.hooks);
338
348
  return;
339
349
  }
@@ -343,7 +353,9 @@ async function mergeSettingsJson(projectRoot, existingScan, selections, report)
343
353
  } catch {
344
354
  display.warn('Existing settings.json contains invalid JSON — creating fresh settings instead.');
345
355
  await writeFile(path.join(projectRoot, '.claude', 'settings.json'), settingsStr);
346
- report.added.permissions = workflowSettings.permissions?.allow?.length || 0;
356
+ report.added.permissions =
357
+ (workflowSettings.permissions?.allow?.length || 0) +
358
+ (workflowSettings.permissions?.deny?.length || 0);
347
359
  report.added.hooks = countHooks(workflowSettings.hooks);
348
360
  }
349
361
  }
@@ -407,7 +419,8 @@ async function handleClaudeMd(projectRoot, existingScan, variables, selections,
407
419
  // Tier 3 notice: if user opted into GTD memory AND their CLAUDE.md already has
408
420
  // a Memory Architecture section, the pointer bullets from the rendered template
409
421
  // won't be merged in automatically. Surface this so the user can add them manually.
410
- if (selections.scaffoldGtdMemory && !missingSections.includes('Memory Architecture')) {
422
+ const optedIn = new Set(selections.optionalFeatures || []);
423
+ if (optedIn.has('gtd-memory') && !missingSections.includes('Memory Architecture')) {
411
424
  report.memoryArchitectureSectionExists = true;
412
425
  }
413
426
 
@@ -477,19 +490,28 @@ export async function performMerge(
477
490
  // Copy hook scripts (preserves existing user modifications)
478
491
  await scaffoldHooks(projectRoot);
479
492
 
493
+ // Copy slash-command helper scripts (preserves existing user modifications)
494
+ await scaffoldScripts(projectRoot);
495
+
480
496
  // Create learnings directory for correction capture
481
497
  await writeFile(path.join(projectRoot, '.claude', 'learnings', '.gitkeep'), '');
482
498
 
483
- // Opt-in: plugin.json (idempotent scaffolder skips if file exists)
484
- if (selections.generatePluginJson) {
485
- await scaffoldPluginJson(projectRoot, selections);
486
- }
499
+ // Create scratch directory for SHA-keyed transient artifacts (gitignored)
500
+ await writeFile(path.join(projectRoot, '.claude', 'scratch', '.gitkeep'), '');
501
+
502
+ // Create plans directory for active work guidance (tracked)
503
+ await writeFile(path.join(projectRoot, '.claude', 'plans', '.gitkeep'), '');
504
+
505
+ // Create observability directory for hook-captured event logs (gitignored)
506
+ await writeFile(path.join(projectRoot, '.claude', 'observability', '.gitkeep'), '');
487
507
 
488
- // Opt-in: GTD memory scaffold (idempotent per-file). Tier 3 notice for
489
- // existing Memory Architecture section is handled inside handleClaudeMd,
490
- // which already reads CLAUDE.md and runs section detection.
491
- if (selections.scaffoldGtdMemory) {
492
- await scaffoldMemoryDocs(projectRoot);
508
+ // Opt-in scaffolders: each registry feature is idempotent. Tier 3 notices
509
+ // (e.g. existing Memory Architecture section for gtd-memory) are handled
510
+ // inside handleClaudeMd, which runs section detection.
511
+ const optedIn = new Set(selections.optionalFeatures || []);
512
+ for (const feature of OPTIONAL_FEATURES) {
513
+ if (!optedIn.has(feature.id)) continue;
514
+ await feature.scaffold(projectRoot, selections);
493
515
  }
494
516
 
495
517
  await mergeDocSpecs(projectRoot, existingScan, variables, selections, report);
@@ -76,6 +76,8 @@ export async function updateGitignore(projectDir) {
76
76
  '.claude/learnings/',
77
77
  '.claude/.stop-hook-active',
78
78
  '.claude/cache/',
79
+ '.claude/scratch/',
80
+ '.claude/observability/',
79
81
  ];
80
82
  const header = '# Worclaude (generated workflow files)';
81
83
 
@@ -136,6 +138,30 @@ export async function scaffoldHooks(projectRoot) {
136
138
  }
137
139
  }
138
140
 
141
+ export async function scaffoldScripts(projectRoot) {
142
+ const scriptsTemplateDir = path.join(getTemplatesDir(), 'scripts');
143
+ if (!(await fs.pathExists(scriptsTemplateDir))) return;
144
+ const destDir = path.join(projectRoot, '.claude', 'scripts');
145
+ await fs.ensureDir(destDir);
146
+
147
+ const entries = await fs.readdir(scriptsTemplateDir);
148
+ for (const entry of entries) {
149
+ if (!entry.endsWith('.sh')) continue;
150
+ const destPath = path.join(destDir, entry);
151
+ await fs.copy(path.join(scriptsTemplateDir, entry), destPath, {
152
+ overwrite: false,
153
+ errorOnExist: false,
154
+ });
155
+ if (process.platform !== 'win32') {
156
+ try {
157
+ await fs.chmod(destPath, 0o755);
158
+ } catch (err) {
159
+ if (err.code !== 'ENOENT') throw err;
160
+ }
161
+ }
162
+ }
163
+ }
164
+
139
165
  export function slugifyPluginName(projectName) {
140
166
  const slug = String(projectName || '')
141
167
  .toLowerCase()
@@ -68,12 +68,6 @@ export const AGENT_CATALOG = {
68
68
  category: 'quality',
69
69
  description: 'Diagnoses and fixes build failures',
70
70
  },
71
- 'e2e-runner': {
72
- model: 'sonnet',
73
- isolation: 'worktree',
74
- category: 'quality',
75
- description: 'Writes and runs end-to-end tests',
76
- },
77
71
  'dependency-manager': {
78
72
  model: 'haiku',
79
73
  isolation: 'none',
@@ -138,7 +132,6 @@ export const CATEGORY_RECOMMENDATIONS = {
138
132
  'security-reviewer',
139
133
  'bug-fixer',
140
134
  'doc-writer',
141
- 'e2e-runner',
142
135
  ],
143
136
  'Backend / API': [
144
137
  'api-designer',
@@ -149,13 +142,7 @@ export const CATEGORY_RECOMMENDATIONS = {
149
142
  'performance-auditor',
150
143
  'build-fixer',
151
144
  ],
152
- 'Frontend / UI': [
153
- 'ui-reviewer',
154
- 'style-enforcer',
155
- 'performance-auditor',
156
- 'bug-fixer',
157
- 'e2e-runner',
158
- ],
145
+ 'Frontend / UI': ['ui-reviewer', 'style-enforcer', 'performance-auditor', 'bug-fixer'],
159
146
  'CLI tool': ['bug-fixer', 'doc-writer', 'dependency-manager', 'build-fixer'],
160
147
  'Data / ML / AI': [
161
148
  'data-pipeline-reviewer',
@@ -183,10 +170,8 @@ export const COMMAND_FILES = [
183
170
  'end',
184
171
  'commit-push-pr',
185
172
  'review-plan',
186
- 'techdebt',
187
173
  'verify',
188
174
  'compact-safe',
189
- 'status',
190
175
  'update-claude-md',
191
176
  'setup',
192
177
  'sync',
@@ -196,7 +181,7 @@ export const COMMAND_FILES = [
196
181
  'refactor-clean',
197
182
  'test-coverage',
198
183
  'learn',
199
- 'upstream-check',
184
+ 'observability',
200
185
  ];
201
186
 
202
187
  export const UNIVERSAL_SKILLS = [
@@ -208,6 +193,7 @@ export const UNIVERSAL_SKILLS = [
208
193
  'verification',
209
194
  'testing',
210
195
  'claude-md-maintenance',
196
+ 'memory-architecture',
211
197
  'coding-principles',
212
198
  'subagent-usage',
213
199
  'security-checklist',
@@ -220,7 +206,15 @@ export const TEMPLATE_SKILLS = [
220
206
  'project-patterns',
221
207
  ];
222
208
 
223
- export const HOOK_FILES = ['pre-compact-save', 'correction-detect', 'learn-capture', 'skill-hint'];
209
+ export const HOOK_FILES = [
210
+ 'pre-compact-save',
211
+ 'correction-detect',
212
+ 'learn-capture',
213
+ 'skill-hint',
214
+ 'obs-skill-loads',
215
+ 'obs-command-invocations',
216
+ 'obs-agent-events',
217
+ ];
224
218
 
225
219
  export const PROJECT_TYPES = [
226
220
  'Full-stack web application',
@@ -275,16 +269,8 @@ export const AGENT_CATEGORIES = {
275
269
  description: 'ci-fixer, docker-helper, deploy-validator, dependency-manager',
276
270
  },
277
271
  Quality: {
278
- agents: [
279
- 'bug-fixer',
280
- 'security-reviewer',
281
- 'performance-auditor',
282
- 'refactorer',
283
- 'build-fixer',
284
- 'e2e-runner',
285
- ],
286
- description:
287
- 'bug-fixer, security-reviewer, performance-auditor, refactorer, build-fixer, e2e-runner',
272
+ agents: ['bug-fixer', 'security-reviewer', 'performance-auditor', 'refactorer', 'build-fixer'],
273
+ description: 'bug-fixer, security-reviewer, performance-auditor, refactorer, build-fixer',
288
274
  },
289
275
  Documentation: {
290
276
  agents: ['doc-writer', 'changelog-generator'],
@@ -0,0 +1,46 @@
1
+ import path from 'node:path';
2
+ import fs from 'fs-extra';
3
+ import { scaffoldPluginJson, scaffoldMemoryDocs } from '../core/scaffolder.js';
4
+
5
+ export const OPTIONAL_FEATURES = [
6
+ {
7
+ id: 'plugin-json',
8
+ label: 'Generate .claude-plugin/plugin.json for marketplace compatibility?',
9
+ extrasLabel: 'plugin.json',
10
+ successPath: '.claude-plugin/plugin.json',
11
+ async detect(projectRoot) {
12
+ return fs.pathExists(path.join(projectRoot, '.claude-plugin', 'plugin.json'));
13
+ },
14
+ async scaffold(projectRoot, selections) {
15
+ await scaffoldPluginJson(projectRoot, selections);
16
+ },
17
+ },
18
+ {
19
+ id: 'gtd-memory',
20
+ label: 'Scaffold structured memory files (decisions.md, preferences.md)?',
21
+ extrasLabel: 'memory docs',
22
+ successPath: 'docs/memory/',
23
+ successDetail: 'decisions.md, preferences.md',
24
+ async detect(projectRoot) {
25
+ return fs.pathExists(path.join(projectRoot, 'docs', 'memory', 'decisions.md'));
26
+ },
27
+ async scaffold(projectRoot) {
28
+ await scaffoldMemoryDocs(projectRoot);
29
+ },
30
+ },
31
+ ];
32
+
33
+ export function getOptionalFeature(id) {
34
+ return OPTIONAL_FEATURES.find((feature) => feature.id === id) || null;
35
+ }
36
+
37
+ export async function availableOptionalFeatures(projectRoot, meta) {
38
+ const optedOut = new Set(meta?.optedOutFeatures || []);
39
+ const result = [];
40
+ for (const feature of OPTIONAL_FEATURES) {
41
+ if (optedOut.has(feature.id)) continue;
42
+ if (await feature.detect(projectRoot)) continue;
43
+ result.push(feature);
44
+ }
45
+ return result;
46
+ }
@@ -1,38 +1,139 @@
1
+ import path from 'node:path';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { fileURLToPath } from 'node:url';
1
4
  import { UNIVERSAL_AGENTS } from '../data/agents.js';
2
- import { AGENT_REGISTRY } from '../data/agent-registry.js';
5
+ import { loadAgentsFromDir, validateRoutingFields } from '../utils/agent-frontmatter.js';
3
6
 
4
- /**
5
- * Generates the agent-routing.md skill file content based on selected agents.
6
- * @param {string[]} selectedAgentNames - names of optional agents the user selected
7
- * @param {string[]} projectTypes - e.g. ['Backend / API', 'Frontend / UI']
8
- * @returns {string} - complete markdown content for agent-routing.md
9
- */
10
- export function buildAgentRoutingSkill(selectedAgentNames, _projectTypes) {
11
- const allAgents = [...new Set([...UNIVERSAL_AGENTS, ...selectedAgentNames])];
7
+ const AUTO_START = '<!-- AUTO-GENERATED-START -->';
8
+ const AUTO_END = '<!-- AUTO-GENERATED-END -->';
9
+
10
+ const MODEL_LABEL = { opus: 'Opus', sonnet: 'Sonnet', haiku: 'Haiku' };
12
11
 
13
- const automaticAgents = [];
14
- const manualAgents = [];
12
+ function modelLabel(model) {
13
+ if (!model) return 'Sonnet';
14
+ return MODEL_LABEL[String(model).toLowerCase()] ?? model;
15
+ }
16
+
17
+ function isolationLabel(isolation) {
18
+ return String(isolation).toLowerCase() === 'worktree' ? 'Worktree' : 'None';
19
+ }
15
20
 
16
- for (const name of allAgents) {
17
- const entry = AGENT_REGISTRY[name];
18
- if (!entry) continue;
19
- if (entry.triggerType === 'automatic') {
20
- automaticAgents.push({ name, ...entry });
21
+ function partition(agents) {
22
+ const automatic = [];
23
+ const manual = [];
24
+ const reserved = [];
25
+ for (const agent of agents) {
26
+ if (agent.status === 'reserved') {
27
+ reserved.push(agent);
28
+ } else if (agent.triggerType === 'automatic') {
29
+ automatic.push(agent);
21
30
  } else {
22
- manualAgents.push({ name, ...entry });
31
+ manual.push(agent);
23
32
  }
24
33
  }
34
+ return { automatic, manual, reserved };
35
+ }
36
+
37
+ const SKILL_FRONTMATTER = [
38
+ '---',
39
+ 'description: "Agent Routing Guide — when to spawn each installed agent"',
40
+ '---',
41
+ '',
42
+ '',
43
+ ].join('\n');
44
+
45
+ /**
46
+ * Build only the AUTO-GENERATED canonical block — markers + headings + agent
47
+ * entries + decision matrix + rules. Used internally by the public file
48
+ * builder and the regenerator.
49
+ */
50
+ function buildAgentRoutingCanonicalBlock(agents) {
51
+ for (const agent of agents) {
52
+ validateRoutingFields(agent, { filePath: agent.filePath });
53
+ }
54
+
55
+ const { automatic, manual, reserved } = partition(agents);
25
56
 
26
57
  const sections = [
27
58
  buildHeader(),
28
59
  buildHowAgentsWork(),
29
- buildAutomaticTriggers(automaticAgents),
30
- buildManualTriggers(manualAgents),
31
- buildDecisionMatrix(allAgents),
60
+ buildAutomaticTriggers(automatic),
61
+ buildManualTriggers(manual),
62
+ buildReserved(reserved),
63
+ buildDecisionMatrix(agents, reserved),
32
64
  buildRules(),
33
- ];
65
+ ].filter(Boolean);
66
+
67
+ return `${AUTO_START}\n${sections.join('\n')}${AUTO_END}\n`;
68
+ }
69
+
70
+ /**
71
+ * Build a complete agent-routing skill file from a list of fully-parsed
72
+ * agent frontmatter objects. The result is suitable for fresh writes: it
73
+ * starts with a YAML frontmatter block (so Claude Code's skill loader has
74
+ * a description) followed by the canonical block wrapped in
75
+ * `<!-- AUTO-GENERATED-START -->` / `<!-- AUTO-GENERATED-END -->` markers.
76
+ *
77
+ * For in-place updates of files that may carry user-authored prose,
78
+ * use {@link regenerateAgentRoutingContent} instead.
79
+ *
80
+ * @param {object[]} agents - parsed agent frontmatter objects
81
+ * @returns {string} complete file content
82
+ */
83
+ export function buildAgentRoutingSkill(agents) {
84
+ return `${SKILL_FRONTMATTER}${buildAgentRoutingCanonicalBlock(agents)}`;
85
+ }
86
+
87
+ /**
88
+ * Replace the AUTO-GENERATED block in `existingContent` with newly-generated
89
+ * content. If `existingContent` has markers, content outside them is
90
+ * preserved verbatim (frontmatter, user notes). If markers are absent or
91
+ * `existingContent` is null/empty, the result is a fresh complete file.
92
+ *
93
+ * @param {string|null} existingContent - the current file contents, or null/empty for first write
94
+ * @param {object[]} agents - parsed agent frontmatter objects
95
+ * @returns {string} updated file content
96
+ */
97
+ export function regenerateAgentRoutingContent(existingContent, agents) {
98
+ if (!existingContent) return buildAgentRoutingSkill(agents);
99
+ const startIdx = existingContent.indexOf(AUTO_START);
100
+ const endIdx = existingContent.indexOf(AUTO_END, startIdx + AUTO_START.length);
101
+ if (startIdx === -1 || endIdx === -1) return buildAgentRoutingSkill(agents);
102
+ const before = existingContent.slice(0, startIdx);
103
+ const after = existingContent.slice(endIdx + AUTO_END.length);
104
+ const fresh = buildAgentRoutingCanonicalBlock(agents);
105
+ return `${before}${fresh.trimEnd()}${after}`;
106
+ }
107
+
108
+ /**
109
+ * Convenience wrapper: load agent files from a directory, optionally
110
+ * filter to a subset of names, and return the markdown.
111
+ *
112
+ * @param {string} dir - path to a directory containing agent .md files (recursively)
113
+ * @param {object} [opts]
114
+ * @param {string[]|null} [opts.includeNames] - only include agents whose `name` is in this set; null = include all
115
+ * @returns {Promise<string>} marker-wrapped routing markdown
116
+ */
117
+ export async function buildAgentRoutingSkillFromDir(dir, { includeNames = null } = {}) {
118
+ const all = await loadAgentsFromDir(dir);
119
+ const filtered = includeNames ? all.filter((a) => includeNames.includes(a.name)) : all;
120
+ return buildAgentRoutingSkill(filtered);
121
+ }
34
122
 
35
- return sections.join('\n');
123
+ /**
124
+ * Load the default set of agents shipped with worclaude (universal + selected
125
+ * optionals) from the project's `templates/agents/` directory. Used by init
126
+ * and merger when scaffolding into a fresh project.
127
+ *
128
+ * @param {string[]} selectedOptionalNames - names of optional agents the user picked
129
+ * @returns {Promise<object[]>} parsed agent frontmatter objects
130
+ */
131
+ export async function loadShippedAgents(selectedOptionalNames) {
132
+ const here = path.dirname(fileURLToPath(import.meta.url));
133
+ const templatesDir = path.resolve(here, '..', '..', 'templates', 'agents');
134
+ const all = await loadAgentsFromDir(templatesDir);
135
+ const wanted = new Set([...UNIVERSAL_AGENTS, ...selectedOptionalNames]);
136
+ return all.filter((a) => wanted.has(a.name));
36
137
  }
37
138
 
38
139
  function buildHeader() {
@@ -50,12 +151,30 @@ function buildHowAgentsWork() {
50
151
  - Never spawn more than 3 agents simultaneously.
51
152
  - If a task is small enough to do yourself in 2 minutes, don't spawn an agent for it.
52
153
 
154
+ ## Background-Agent Concurrency
155
+
156
+ Two background agents on the same branch coexist cleanly:
157
+
158
+ - **Worktree-isolated agents** (\`isolation: "worktree"\`) each create their own
159
+ sibling worktree off \`origin/HEAD\`. They never collide on files, refs, or the
160
+ index — running multiple in parallel is safe by design.
161
+ - **Non-isolated agents** share the main checkout but are read-only by
162
+ convention. The main session and these agents must avoid editing the same
163
+ files concurrently; otherwise behavior is up to whoever writes last.
164
+
165
+ Worktree lock semantics: Claude Code locks each agent worktree with the agent's
166
+ pid; the lock survives agent completion. Stale locks are normal. Clean up with
167
+ \`git worktree remove -f -f <path>\` or the project's worktree-cleanup helper.
168
+
169
+ The earlier "lock file per branch" plan was rejected after the 2026-04-26
170
+ concurrency test — worktree isolation already provides the guarantee a lock
171
+ file would have, and a lock would block the legitimate parallel-agents case.
172
+
53
173
  ---
54
174
  `;
55
175
  }
56
176
 
57
177
  function buildAgentEntry(agent) {
58
- const isolation = agent.isolation === 'worktree' ? 'Worktree' : 'None';
59
178
  let trigger;
60
179
  if (agent.triggerType === 'automatic' && agent.triggerCommand) {
61
180
  trigger = `Automatic — spawn when trigger condition is met (also: ${agent.triggerCommand})`;
@@ -68,7 +187,7 @@ function buildAgentEntry(agent) {
68
187
  }
69
188
 
70
189
  return `### ${agent.name}
71
- - **Model:** ${agent.model} | **Isolation:** ${isolation}
190
+ - **Model:** ${modelLabel(agent.model)} | **Isolation:** ${isolationLabel(agent.isolation)}
72
191
  - **When:** ${agent.whenToUse}
73
192
  - **Trigger:** ${trigger}
74
193
  - **What it does:** ${agent.whatItDoes}
@@ -91,8 +210,7 @@ No automatic-trigger agents installed.
91
210
 
92
211
  These agents should be spawned without being asked when their trigger condition is met.
93
212
 
94
- ${entries}
95
- ---
213
+ ${entries}---
96
214
  `;
97
215
  }
98
216
 
@@ -111,23 +229,44 @@ No manual-trigger agents installed.
111
229
 
112
230
  These agents are spawned when you or the user explicitly requests them.
113
231
 
114
- ${entries}
115
- ---
232
+ ${entries}---
116
233
  `;
117
234
  }
118
235
 
119
- function buildDecisionMatrix(allAgents) {
236
+ function buildReserved(reservedAgents) {
237
+ if (reservedAgents.length === 0) return '';
238
+
239
+ const entries = reservedAgents
240
+ .map(
241
+ (agent) => `### ${agent.name}
242
+ - **Model:** ${modelLabel(agent.model)} | **Isolation:** ${isolationLabel(agent.isolation)}
243
+ - **Status:** Reserved — no in-session command currently invokes this agent.
244
+ - **Why kept:** ${agent.whenToUse}
245
+ - **Do NOT spawn this agent in regular sessions.** It exists for scheduled
246
+ automation (CI/Actions) and for future revival; spawning it manually has no
247
+ defined entry path today.
248
+ `
249
+ )
250
+ .join('\n');
251
+
252
+ return `## Reserved
253
+
254
+ ${entries}---
255
+ `;
256
+ }
257
+
258
+ function buildDecisionMatrix(allAgents, reservedAgents) {
259
+ const reservedSet = new Set(reservedAgents.map((a) => a.name));
120
260
  const header = `## Decision Matrix
121
261
 
122
262
  | You just... | Spawn this | Auto? |
123
263
  |---|---|---|`;
124
264
 
125
265
  const rows = [];
126
- for (const name of allAgents) {
127
- const entry = AGENT_REGISTRY[name];
128
- if (!entry) continue;
129
- const auto = entry.triggerType === 'automatic' ? 'Yes' : 'Manual';
130
- rows.push(`| ${entry.situationLabel} | ${name} | ${auto} |`);
266
+ for (const agent of allAgents) {
267
+ if (reservedSet.has(agent.name)) continue;
268
+ const auto = agent.triggerType === 'automatic' ? 'Yes' : 'Manual';
269
+ rows.push(`| ${agent.situationLabel} | ${agent.name} | ${auto} |`);
131
270
  }
132
271
 
133
272
  return `${header}
@@ -147,3 +286,19 @@ function buildRules() {
147
286
  6. If you spawn an agent and it's not useful, tell the user — they may remove it.
148
287
  `;
149
288
  }
289
+
290
+ export { AUTO_START, AUTO_END };
291
+
292
+ /**
293
+ * Read a file and run regenerateAgentRoutingContent on it. Convenience for
294
+ * call sites that already have a target path.
295
+ */
296
+ export async function regenerateAgentRoutingFile(filePath, agents) {
297
+ let existing = null;
298
+ try {
299
+ existing = await readFile(filePath, 'utf8');
300
+ } catch (err) {
301
+ if (err.code !== 'ENOENT') throw err;
302
+ }
303
+ return regenerateAgentRoutingContent(existing, agents);
304
+ }
package/src/index.js CHANGED
@@ -12,6 +12,10 @@ import { deleteCommand } from './commands/delete.js';
12
12
  import { doctorCommand } from './commands/doctor.js';
13
13
  import { scanCommand } from './commands/scan.js';
14
14
  import { setupStateCommand } from './commands/setup-state.js';
15
+ import { worktreesCleanCommand } from './commands/worktrees.js';
16
+ import { regenerateRoutingCommand } from './commands/regenerate-routing.js';
17
+ import { docLintCommand } from './commands/doc-lint.js';
18
+ import { observabilityCommand } from './commands/observability.js';
15
19
 
16
20
  const program = new Command();
17
21
 
@@ -64,6 +68,26 @@ program
64
68
  .option('--json', 'Output results as JSON')
65
69
  .action((options) => doctorCommand(options));
66
70
 
71
+ program
72
+ .command('regenerate-routing')
73
+ .description(
74
+ 'Regenerate .claude/skills/agent-routing/SKILL.md from .claude/agents/*.md frontmatter'
75
+ )
76
+ .action(() => regenerateRoutingCommand());
77
+
78
+ program
79
+ .command('doc-lint')
80
+ .description('Lint <!-- references X --> markers across .md files for drift against their source')
81
+ .option('--strict', 'Exit non-zero on any drift (for CI)')
82
+ .action((options) => docLintCommand(options));
83
+
84
+ program
85
+ .command('observability')
86
+ .description('Aggregate .claude/observability/*.jsonl into a per-project Markdown report')
87
+ .option('--json', 'Emit the raw report object as JSON instead of Markdown')
88
+ .option('--out <file>', 'Write the report to a file instead of stdout')
89
+ .action((options) => observabilityCommand(options));
90
+
67
91
  program
68
92
  .command('scan')
69
93
  .description('Scan project for detectable facts (writes .claude/cache/detection-report.json)')
@@ -112,4 +136,17 @@ setupState.on('command:*', (operands) => {
112
136
  process.exitCode = 2;
113
137
  });
114
138
 
139
+ const worktrees = program.command('worktrees').description('Manage agent worktrees');
140
+
141
+ worktrees
142
+ .command('clean')
143
+ .description('Force-remove locked agent worktrees under .claude/worktrees/')
144
+ .option('--path <dir>', 'Project root', process.cwd())
145
+ .action((options) => worktreesCleanCommand(options));
146
+
147
+ worktrees.on('command:*', (operands) => {
148
+ console.error(`Error: unknown worktrees subcommand: ${operands[0]} (expected one of clean)`);
149
+ process.exitCode = 2;
150
+ });
151
+
115
152
  program.parse();