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.
- package/CHANGELOG.md +71 -0
- package/README.md +72 -56
- package/package.json +1 -1
- package/src/commands/doc-lint.js +37 -0
- package/src/commands/doctor.js +145 -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 +19 -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 +19 -1
- package/templates/agents/universal/upstream-watcher.md +8 -1
- package/templates/agents/universal/verify-app.md +45 -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 +15 -2
- 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
|
@@ -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,
|
package/src/core/merger.js
CHANGED
|
@@ -8,9 +8,9 @@ import {
|
|
|
8
8
|
scaffoldAgentsMd,
|
|
9
9
|
mergeSettings,
|
|
10
10
|
scaffoldHooks,
|
|
11
|
-
|
|
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
|
|
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
|
|
246
|
+
const newAllow = workflowAllow.filter((p) => !existingAllow.includes(p));
|
|
247
247
|
if (!existing.permissions) existing.permissions = {};
|
|
248
|
-
existing.permissions.allow = [...existingAllow, ...
|
|
249
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
//
|
|
484
|
-
|
|
485
|
-
|
|
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:
|
|
489
|
-
// existing Memory Architecture section
|
|
490
|
-
//
|
|
491
|
-
|
|
492
|
-
|
|
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);
|
package/src/core/scaffolder.js
CHANGED
|
@@ -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()
|
package/src/data/agents.js
CHANGED
|
@@ -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
|
-
'
|
|
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 = [
|
|
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
|
-
|
|
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 {
|
|
5
|
+
import { loadAgentsFromDir, validateRoutingFields } from '../utils/agent-frontmatter.js';
|
|
3
6
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
14
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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(
|
|
30
|
-
buildManualTriggers(
|
|
31
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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();
|