claude-capsule-kit 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +281 -0
  2. package/agents/agent-developer.md +206 -0
  3. package/agents/architecture-explorer.md +90 -0
  4. package/agents/brainstorm-coordinator.md +120 -0
  5. package/agents/code-reviewer.md +135 -0
  6. package/agents/context-librarian.md +227 -0
  7. package/agents/context-manager.md +151 -0
  8. package/agents/database-architect.md +107 -0
  9. package/agents/database-navigator.md +136 -0
  10. package/agents/debugger.md +121 -0
  11. package/agents/devops-sre.md +102 -0
  12. package/agents/error-detective.md +128 -0
  13. package/agents/git-workflow-manager.md +212 -0
  14. package/agents/github-issue-tracker.md +252 -0
  15. package/agents/product-dx-specialist.md +99 -0
  16. package/agents/refactoring-specialist.md +159 -0
  17. package/agents/security-engineer.md +102 -0
  18. package/agents/session-summarizer.md +126 -0
  19. package/agents/system-architect.md +103 -0
  20. package/bin/cck.js +1624 -0
  21. package/commands/crew-setup.md +75 -0
  22. package/commands/load-session.md +68 -0
  23. package/commands/sessions.md +55 -0
  24. package/commands/statusline.md +51 -0
  25. package/commands/sync-disable.md +35 -0
  26. package/commands/sync-enable.md +32 -0
  27. package/commands/sync.md +31 -0
  28. package/crew/lib/activity-monitor.js +128 -0
  29. package/crew/lib/crew-config-reader.js +255 -0
  30. package/crew/lib/health-monitor.js +171 -0
  31. package/crew/lib/merge-pilot.js +340 -0
  32. package/crew/lib/prompt-generator.js +268 -0
  33. package/crew/lib/role-presets.js +63 -0
  34. package/crew/lib/task-decomposer.js +382 -0
  35. package/crew/lib/team-spawner.sh +557 -0
  36. package/crew/lib/team-state-manager.js +155 -0
  37. package/crew/lib/worktree-gc.js +357 -0
  38. package/crew/lib/worktree-manager.sh +700 -0
  39. package/docs/AGENT_ROUTING_GUIDE.md +655 -0
  40. package/docs/AGENT_TEAMS_WORKTREE_MODE.md +681 -0
  41. package/docs/BEST_PRACTICES.md +194 -0
  42. package/docs/CAPSULE_DEGRADATION_RCA.md +577 -0
  43. package/docs/SKILLS_ORCHESTRATION_ARCHITECTURE.md +455 -0
  44. package/docs/SUPER_CLAUDE_SYSTEM_ARCHITECTURE.md +1647 -0
  45. package/docs/TOOL_ENFORCEMENT_REFERENCE.md +418 -0
  46. package/hooks/check-refresh-needed.sh +77 -0
  47. package/hooks/detect-changes.sh +90 -0
  48. package/hooks/keyword-triggers.sh +66 -0
  49. package/hooks/lib/crew-detect.js +241 -0
  50. package/hooks/lib/handoff-generator.js +158 -0
  51. package/hooks/load-from-journal.sh +41 -0
  52. package/hooks/post-tool-use.js +212 -0
  53. package/hooks/pre-compact.js +77 -0
  54. package/hooks/pre-edit-analysis.sh +68 -0
  55. package/hooks/pre-tool-use.sh +212 -0
  56. package/hooks/prompt-submit-memory.sh +87 -0
  57. package/hooks/quality-check.sh +48 -0
  58. package/hooks/session-end.js +133 -0
  59. package/hooks/session-start.js +439 -0
  60. package/hooks/stop.sh +66 -0
  61. package/hooks/suggest-discoveries.sh +84 -0
  62. package/hooks/summarize-session.sh +122 -0
  63. package/hooks/sync-to-journal.sh +77 -0
  64. package/hooks/sync-todowrite.sh +37 -0
  65. package/hooks/tool-auto-suggest.sh +77 -0
  66. package/hooks/user-prompt-submit.sh +71 -0
  67. package/lib/audit-logger.sh +120 -0
  68. package/lib/sandbox-validator.sh +194 -0
  69. package/lib/tool-runner.sh +274 -0
  70. package/package.json +67 -0
  71. package/scripts/postinstall.js +4 -0
  72. package/scripts/show-capsule-visual.sh +103 -0
  73. package/scripts/show-capsule.sh +113 -0
  74. package/scripts/show-deps-tree.sh +66 -0
  75. package/scripts/show-stats-dashboard.sh +52 -0
  76. package/scripts/show-stats.sh +79 -0
  77. package/skills/code-review/SKILL.md +520 -0
  78. package/skills/crew/SKILL.md +395 -0
  79. package/skills/debug/SKILL.md +473 -0
  80. package/skills/deep-context/SKILL.md +446 -0
  81. package/skills/task-router/SKILL.md +390 -0
  82. package/skills/workflow/SKILL.md +370 -0
  83. package/templates/CLAUDE.md +124 -0
  84. package/templates/crew-config.json +21 -0
  85. package/templates/settings-hooks.json +74 -0
  86. package/templates/statusline-command.sh +208 -0
  87. package/tools/context-query/context-query.js +312 -0
  88. package/tools/context-query/context-query.sh +5 -0
  89. package/tools/context-query/tool.json +42 -0
  90. package/tools/dependency-scanner/dependency-scanner.sh +53 -0
  91. package/tools/dependency-scanner/tool.json +8 -0
  92. package/tools/find-circular/find-circular.sh +41 -0
  93. package/tools/find-circular/tool.json +36 -0
  94. package/tools/find-dead-code/find-dead-code.sh +41 -0
  95. package/tools/find-dead-code/tool.json +36 -0
  96. package/tools/impact-analysis/impact-analysis.sh +99 -0
  97. package/tools/impact-analysis/tool.json +38 -0
  98. package/tools/progressive-reader/progressive-reader.sh +14 -0
  99. package/tools/progressive-reader/tool.json +69 -0
  100. package/tools/query-deps/query-deps.sh +69 -0
  101. package/tools/query-deps/tool.json +34 -0
  102. package/tools/stats/stats.js +299 -0
  103. package/tools/stats/stats.sh +5 -0
  104. package/tools/stats/tool.json +34 -0
  105. package/tools/token-counter/README.md +73 -0
  106. package/tools/token-counter/token-counter.py +202 -0
  107. package/tools/token-counter/tool.json +40 -0
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Crew Config Reader - Load, validate, hash, and resolve .crew-config.json
3
+ *
4
+ * Supports two config formats:
5
+ * - Old: { team: {...}, project: {...} }
6
+ * - New: { profiles: { dev: {...}, review: {...} }, default: "dev", project: {...} }
7
+ *
8
+ * Old format auto-normalizes at resolve time (no file rewrite needed).
9
+ */
10
+
11
+ import { readFileSync } from 'fs';
12
+ import { resolve } from 'path';
13
+ import { createHash } from 'crypto';
14
+ import { ROLE_PRESETS } from './role-presets.js';
15
+
16
+ /**
17
+ * Load .crew-config.json from project root.
18
+ * @param {string} projectRoot - Path to project root
19
+ * @returns {object} Parsed config
20
+ * @throws {Error} If file not found or invalid JSON
21
+ */
22
+ export function loadCrewConfig(projectRoot) {
23
+ const configPath = resolve(projectRoot, '.crew-config.json');
24
+ const raw = readFileSync(configPath, 'utf-8');
25
+ return JSON.parse(raw);
26
+ }
27
+
28
+ /**
29
+ * Compute a short hash of the config for change detection.
30
+ * @param {object} configObj - Config object to hash
31
+ * @returns {string} 12-char hex hash
32
+ */
33
+ export function hashConfig(configObj) {
34
+ return createHash('sha256')
35
+ .update(JSON.stringify(configObj))
36
+ .digest('hex')
37
+ .slice(0, 12);
38
+ }
39
+
40
+ /**
41
+ * Resolve which profile to use from a config.
42
+ *
43
+ * Old format ({ team }) → returns { profile: config.team, profileName: 'default' }
44
+ * New format ({ profiles }) → looks up requestedName || config.default || first key
45
+ *
46
+ * @param {object} config - Parsed .crew-config.json
47
+ * @param {string} [requestedName] - Profile name from CLI arg
48
+ * @returns {{ profile: object, profileName: string }}
49
+ * @throws {Error} If profile not found
50
+ */
51
+ export function resolveProfile(config, requestedName) {
52
+ // Old format — single team
53
+ if (config.team && !config.profiles) {
54
+ return { profile: config.team, profileName: 'default' };
55
+ }
56
+
57
+ // New format — multiple profiles
58
+ if (!config.profiles || typeof config.profiles !== 'object') {
59
+ throw new Error('Config must have either "team" or "profiles" section');
60
+ }
61
+
62
+ const profileNames = Object.keys(config.profiles);
63
+ if (profileNames.length === 0) {
64
+ throw new Error('No profiles defined in config');
65
+ }
66
+
67
+ const name = requestedName || config.default || profileNames[0];
68
+ const profile = config.profiles[name];
69
+
70
+ if (!profile) {
71
+ throw new Error(
72
+ `Profile "${name}" not found. Available: ${profileNames.join(', ')}`
73
+ );
74
+ }
75
+
76
+ return { profile, profileName: name };
77
+ }
78
+
79
+ /**
80
+ * Resolve teammates from a team/profile config.
81
+ * Supports two formats:
82
+ * - Flat: { teammates: [...] } — backward compatible
83
+ * - Grouped: { crews: [{ name, teammates: [...] }] } — crew grouping
84
+ *
85
+ * Returns a flat array of teammates with `crew` property set on each.
86
+ * When using flat format, crew defaults to 'default'.
87
+ *
88
+ * @param {object} teamOrProfile - Team or profile object
89
+ * @param {string} [filterCrew] - Optional crew name to filter by
90
+ * @returns {object[]} Flat array of teammates with crew metadata
91
+ */
92
+ export function resolveTeammates(teamOrProfile, filterCrew) {
93
+ let teammates = [];
94
+
95
+ if (teamOrProfile.crews && Array.isArray(teamOrProfile.crews)) {
96
+ // Grouped format: flatten crews into teammates with crew metadata
97
+ for (const crew of teamOrProfile.crews) {
98
+ for (const mate of crew.teammates || []) {
99
+ teammates.push({ ...mate, crew: crew.name });
100
+ }
101
+ }
102
+ } else if (teamOrProfile.teammates && Array.isArray(teamOrProfile.teammates)) {
103
+ // Flat format: assign 'default' crew
104
+ teammates = teamOrProfile.teammates.map(m => ({ ...m, crew: m.crew || 'default' }));
105
+ }
106
+
107
+ // Filter by crew if requested
108
+ if (filterCrew) {
109
+ teammates = teammates.filter(m => m.crew === filterCrew);
110
+ }
111
+
112
+ return teammates;
113
+ }
114
+
115
+ /**
116
+ * List crew names from a team/profile config.
117
+ * @param {object} teamOrProfile - Team or profile object
118
+ * @returns {string[]} Array of crew names
119
+ */
120
+ export function listCrews(teamOrProfile) {
121
+ if (teamOrProfile.crews && Array.isArray(teamOrProfile.crews)) {
122
+ return teamOrProfile.crews.map(c => c.name);
123
+ }
124
+ return ['default'];
125
+ }
126
+
127
+ /**
128
+ * Validate a crew config. Returns array of error strings (empty = valid).
129
+ * Handles both old format (team) and new format (profiles).
130
+ * @param {object} config - Config to validate
131
+ * @returns {string[]} Array of validation errors
132
+ */
133
+ export function validateConfig(config) {
134
+ const errors = [];
135
+
136
+ // Detect format
137
+ if (config.profiles) {
138
+ // New multi-profile format
139
+ if (typeof config.profiles !== 'object' || Array.isArray(config.profiles)) {
140
+ errors.push('"profiles" must be an object');
141
+ return errors;
142
+ }
143
+
144
+ const profileNames = Object.keys(config.profiles);
145
+ if (profileNames.length === 0) {
146
+ errors.push('"profiles" must contain at least one profile');
147
+ return errors;
148
+ }
149
+
150
+ if (config.default && !config.profiles[config.default]) {
151
+ errors.push(`"default" profile "${config.default}" does not exist in profiles`);
152
+ }
153
+
154
+ for (const pName of profileNames) {
155
+ const profile = config.profiles[pName];
156
+ const pfx = `profiles.${pName}`;
157
+ errors.push(...validateTeam(profile, pfx));
158
+ }
159
+ } else if (config.team) {
160
+ // Old single-team format
161
+ errors.push(...validateTeam(config.team, 'team'));
162
+ } else {
163
+ errors.push('Missing "team" or "profiles" section');
164
+ }
165
+
166
+ return errors;
167
+ }
168
+
169
+ /**
170
+ * Validate a single team object (used for both old and new format).
171
+ * @param {object} team - Team object with name, teammates[]
172
+ * @param {string} prefix - Error prefix (e.g. "team" or "profiles.dev")
173
+ * @returns {string[]} Validation errors
174
+ */
175
+ function validateTeam(team, prefix) {
176
+ const errors = [];
177
+
178
+ if (!team.name || typeof team.name !== 'string') {
179
+ errors.push(`${prefix}.name is required and must be a string`);
180
+ }
181
+
182
+ // Support both flat teammates and crews grouping
183
+ if (team.crews && Array.isArray(team.crews)) {
184
+ if (team.crews.length === 0) {
185
+ errors.push(`${prefix}.crews must be a non-empty array`);
186
+ return errors;
187
+ }
188
+ for (let c = 0; c < team.crews.length; c++) {
189
+ const crew = team.crews[c];
190
+ if (!crew.name || typeof crew.name !== 'string') {
191
+ errors.push(`${prefix}.crews[${c}]: name is required`);
192
+ }
193
+ if (!Array.isArray(crew.teammates) || crew.teammates.length === 0) {
194
+ errors.push(`${prefix}.crews[${c}].teammates must be a non-empty array`);
195
+ continue;
196
+ }
197
+ for (let i = 0; i < crew.teammates.length; i++) {
198
+ const t = crew.teammates[i];
199
+ if (!t.name || typeof t.name !== 'string') {
200
+ errors.push(`${prefix}.crews[${c}].teammate[${i}]: name is required`);
201
+ }
202
+ if (!t.branch || typeof t.branch !== 'string') {
203
+ errors.push(`${prefix}.crews[${c}].teammate[${i}]: branch is required`);
204
+ }
205
+ if (t.role && !ROLE_PRESETS[t.role]) {
206
+ errors.push(
207
+ `${prefix}.crews[${c}].teammate[${i}]: unknown role "${t.role}". Valid: ${Object.keys(ROLE_PRESETS).join(', ')}`
208
+ );
209
+ }
210
+ }
211
+ }
212
+ } else if (Array.isArray(team.teammates)) {
213
+ if (team.teammates.length === 0) {
214
+ errors.push(`${prefix}.teammates must be a non-empty array`);
215
+ return errors;
216
+ }
217
+ for (let i = 0; i < team.teammates.length; i++) {
218
+ const t = team.teammates[i];
219
+ if (!t.name || typeof t.name !== 'string') {
220
+ errors.push(`${prefix}.teammate[${i}]: name is required`);
221
+ }
222
+ if (!t.branch || typeof t.branch !== 'string') {
223
+ errors.push(`${prefix}.teammate[${i}]: branch is required`);
224
+ }
225
+ if (t.role && !ROLE_PRESETS[t.role]) {
226
+ errors.push(
227
+ `${prefix}.teammate[${i}]: unknown role "${t.role}". Valid: ${Object.keys(ROLE_PRESETS).join(', ')}`
228
+ );
229
+ }
230
+ }
231
+ } else {
232
+ errors.push(`${prefix} must have either "teammates" or "crews" array`);
233
+ }
234
+
235
+ return errors;
236
+ }
237
+
238
+ /**
239
+ * Resolve the worktree path for a teammate branch.
240
+ * Convention:
241
+ * default profile: {projectRoot}-{sanitized-branch}
242
+ * named profile: {projectRoot}-{profileName}-{sanitized-branch}
243
+ *
244
+ * @param {string} projectRoot - Path to main project
245
+ * @param {string} branch - Git branch name
246
+ * @param {string} [profileName] - Profile name (null/'default' = no prefix)
247
+ * @returns {string} Absolute path for worktree
248
+ */
249
+ export function resolveWorktreePath(projectRoot, branch, profileName) {
250
+ const sanitized = branch.replace(/\//g, '--');
251
+ if (!profileName || profileName === 'default') {
252
+ return `${projectRoot}-${sanitized}`;
253
+ }
254
+ return `${projectRoot}-${profileName}-${sanitized}`;
255
+ }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Crew Health Monitor - Detect crashed/hung teammates
3
+ *
4
+ * Checks teammate health by examining:
5
+ * 1. last_active timestamps in team-state.json
6
+ * 2. Recent git commits in worktrees
7
+ * 3. Configurable staleness threshold
8
+ */
9
+
10
+ import { execSync } from 'child_process';
11
+ import { existsSync } from 'fs';
12
+ import { loadTeamState } from './team-state-manager.js';
13
+
14
+ /**
15
+ * Health status values:
16
+ * - active: Updated recently (within threshold)
17
+ * - idle: No recent updates but not yet stale
18
+ * - unresponsive: Stale beyond threshold, may be hung
19
+ * - crashed: Worktree exists but no activity and very stale
20
+ */
21
+
22
+ /**
23
+ * Check health of all teammates in a crew profile.
24
+ * @param {string} projectHash - 12-char project hash
25
+ * @param {string} profileName - Profile name
26
+ * @param {object} options - Health check options
27
+ * @param {number} [options.staleThresholdMinutes=10] - Minutes before teammate is considered stale
28
+ * @param {number} [options.commitWindowMinutes=30] - Window for checking recent commits
29
+ * @returns {Array<object>} Array of teammate health reports
30
+ */
31
+ export function checkHealth(projectHash, profileName, options = {}) {
32
+ const staleThresholdMinutes = options.staleThresholdMinutes || 10;
33
+ const commitWindowMinutes = options.commitWindowMinutes || 30;
34
+
35
+ const state = loadTeamState(projectHash, profileName);
36
+ if (!state || !state.teammates) {
37
+ return [];
38
+ }
39
+
40
+ const staleThresholdMs = staleThresholdMinutes * 60 * 1000;
41
+ const now = Date.now();
42
+ const results = [];
43
+
44
+ for (const [name, mate] of Object.entries(state.teammates)) {
45
+ const report = {
46
+ name,
47
+ branch: mate.branch,
48
+ status: mate.status || 'unknown',
49
+ lastActive: mate.last_active || null,
50
+ worktreePath: mate.worktree_path || null,
51
+ recentCommits: 0,
52
+ health: 'unknown'
53
+ };
54
+
55
+ // Calculate age of last activity
56
+ let ageMs = null;
57
+ if (mate.last_active) {
58
+ ageMs = now - new Date(mate.last_active).getTime();
59
+ }
60
+
61
+ // Check for recent commits in worktree
62
+ if (mate.worktree_path && existsSync(mate.worktree_path)) {
63
+ try {
64
+ const sinceTime = `${commitWindowMinutes} minutes ago`;
65
+ const output = execSync(
66
+ `git -C "${mate.worktree_path}" log --oneline --since="${sinceTime}"`,
67
+ { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
68
+ );
69
+ report.recentCommits = output.trim().split('\n').filter(line => line.length > 0).length;
70
+ } catch {
71
+ report.recentCommits = 0;
72
+ }
73
+ }
74
+
75
+ // Determine health status
76
+ if (!mate.last_active) {
77
+ report.health = 'unresponsive';
78
+ } else if (ageMs < staleThresholdMs) {
79
+ report.health = 'active';
80
+ } else if (ageMs < staleThresholdMs * 2) {
81
+ // Between threshold and 2x threshold: idle
82
+ report.health = 'idle';
83
+ } else if (mate.worktree_path && existsSync(mate.worktree_path) && report.recentCommits === 0) {
84
+ // Very stale and no recent commits: likely crashed
85
+ report.health = 'crashed';
86
+ } else {
87
+ report.health = 'unresponsive';
88
+ }
89
+
90
+ results.push(report);
91
+ }
92
+
93
+ return results;
94
+ }
95
+
96
+ /**
97
+ * Format health report as human-readable text.
98
+ * @param {Array<object>} healthReports - Array from checkHealth()
99
+ * @returns {string} Formatted report text
100
+ */
101
+ export function formatHealthReport(healthReports) {
102
+ if (healthReports.length === 0) {
103
+ return 'No teammates found.';
104
+ }
105
+
106
+ const lines = [];
107
+ lines.push('Crew Health Report');
108
+ lines.push('─'.repeat(100));
109
+ lines.push(
110
+ ' Name'.padEnd(22) +
111
+ 'Branch'.padEnd(32) +
112
+ 'Health'.padEnd(14) +
113
+ 'Last Active'.padEnd(22) +
114
+ 'Commits (30m)'
115
+ );
116
+ lines.push('─'.repeat(100));
117
+
118
+ for (const report of healthReports) {
119
+ const healthIcon = {
120
+ active: '✓',
121
+ idle: '○',
122
+ unresponsive: '⚠',
123
+ crashed: '✗',
124
+ unknown: '?'
125
+ }[report.health] || '?';
126
+
127
+ const healthLabel = `${healthIcon} ${report.health}`;
128
+ const lastActive = report.lastActive
129
+ ? formatAge(new Date(report.lastActive))
130
+ : 'never';
131
+
132
+ lines.push(
133
+ ` ${report.name.padEnd(20)}${report.branch.padEnd(32)}${healthLabel.padEnd(14)}${lastActive.padEnd(22)}${report.recentCommits}`
134
+ );
135
+ }
136
+
137
+ lines.push('');
138
+
139
+ // Add recovery steps for unhealthy teammates
140
+ const unhealthy = healthReports.filter(r => r.health === 'crashed' || r.health === 'unresponsive');
141
+ if (unhealthy.length > 0) {
142
+ lines.push('Recovery Steps:');
143
+ lines.push('─'.repeat(100));
144
+ for (const report of unhealthy) {
145
+ lines.push(`\n${report.name} (${report.health}):`);
146
+ lines.push(` 1. Check worktree status: git -C "${report.worktreePath}" status`);
147
+ lines.push(` 2. Check recent commits: git -C "${report.worktreePath}" log --oneline -5`);
148
+ lines.push(` 3. If crashed, re-spawn the teammate using the stored spawn prompt`);
149
+ lines.push(` 4. If unresponsive, send a message: SendMessage(type="message", recipient="${report.name}", ...)`);
150
+ }
151
+ }
152
+
153
+ return lines.join('\n');
154
+ }
155
+
156
+ /**
157
+ * Format time age as human-readable string.
158
+ * @param {Date} timestamp - Timestamp to format
159
+ * @returns {string} Age string like "5m ago", "2h ago"
160
+ */
161
+ function formatAge(timestamp) {
162
+ const ageMs = Date.now() - timestamp.getTime();
163
+ const ageMinutes = Math.floor(ageMs / 60000);
164
+ const ageHours = Math.floor(ageMs / 3600000);
165
+ const ageDays = Math.floor(ageMs / 86400000);
166
+
167
+ if (ageDays > 0) return `${ageDays}d ago`;
168
+ if (ageHours > 0) return `${ageHours}h ago`;
169
+ if (ageMinutes > 0) return `${ageMinutes}m ago`;
170
+ return 'just now';
171
+ }