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.
- package/README.md +281 -0
- package/agents/agent-developer.md +206 -0
- package/agents/architecture-explorer.md +90 -0
- package/agents/brainstorm-coordinator.md +120 -0
- package/agents/code-reviewer.md +135 -0
- package/agents/context-librarian.md +227 -0
- package/agents/context-manager.md +151 -0
- package/agents/database-architect.md +107 -0
- package/agents/database-navigator.md +136 -0
- package/agents/debugger.md +121 -0
- package/agents/devops-sre.md +102 -0
- package/agents/error-detective.md +128 -0
- package/agents/git-workflow-manager.md +212 -0
- package/agents/github-issue-tracker.md +252 -0
- package/agents/product-dx-specialist.md +99 -0
- package/agents/refactoring-specialist.md +159 -0
- package/agents/security-engineer.md +102 -0
- package/agents/session-summarizer.md +126 -0
- package/agents/system-architect.md +103 -0
- package/bin/cck.js +1624 -0
- package/commands/crew-setup.md +75 -0
- package/commands/load-session.md +68 -0
- package/commands/sessions.md +55 -0
- package/commands/statusline.md +51 -0
- package/commands/sync-disable.md +35 -0
- package/commands/sync-enable.md +32 -0
- package/commands/sync.md +31 -0
- package/crew/lib/activity-monitor.js +128 -0
- package/crew/lib/crew-config-reader.js +255 -0
- package/crew/lib/health-monitor.js +171 -0
- package/crew/lib/merge-pilot.js +340 -0
- package/crew/lib/prompt-generator.js +268 -0
- package/crew/lib/role-presets.js +63 -0
- package/crew/lib/task-decomposer.js +382 -0
- package/crew/lib/team-spawner.sh +557 -0
- package/crew/lib/team-state-manager.js +155 -0
- package/crew/lib/worktree-gc.js +357 -0
- package/crew/lib/worktree-manager.sh +700 -0
- package/docs/AGENT_ROUTING_GUIDE.md +655 -0
- package/docs/AGENT_TEAMS_WORKTREE_MODE.md +681 -0
- package/docs/BEST_PRACTICES.md +194 -0
- package/docs/CAPSULE_DEGRADATION_RCA.md +577 -0
- package/docs/SKILLS_ORCHESTRATION_ARCHITECTURE.md +455 -0
- package/docs/SUPER_CLAUDE_SYSTEM_ARCHITECTURE.md +1647 -0
- package/docs/TOOL_ENFORCEMENT_REFERENCE.md +418 -0
- package/hooks/check-refresh-needed.sh +77 -0
- package/hooks/detect-changes.sh +90 -0
- package/hooks/keyword-triggers.sh +66 -0
- package/hooks/lib/crew-detect.js +241 -0
- package/hooks/lib/handoff-generator.js +158 -0
- package/hooks/load-from-journal.sh +41 -0
- package/hooks/post-tool-use.js +212 -0
- package/hooks/pre-compact.js +77 -0
- package/hooks/pre-edit-analysis.sh +68 -0
- package/hooks/pre-tool-use.sh +212 -0
- package/hooks/prompt-submit-memory.sh +87 -0
- package/hooks/quality-check.sh +48 -0
- package/hooks/session-end.js +133 -0
- package/hooks/session-start.js +439 -0
- package/hooks/stop.sh +66 -0
- package/hooks/suggest-discoveries.sh +84 -0
- package/hooks/summarize-session.sh +122 -0
- package/hooks/sync-to-journal.sh +77 -0
- package/hooks/sync-todowrite.sh +37 -0
- package/hooks/tool-auto-suggest.sh +77 -0
- package/hooks/user-prompt-submit.sh +71 -0
- package/lib/audit-logger.sh +120 -0
- package/lib/sandbox-validator.sh +194 -0
- package/lib/tool-runner.sh +274 -0
- package/package.json +67 -0
- package/scripts/postinstall.js +4 -0
- package/scripts/show-capsule-visual.sh +103 -0
- package/scripts/show-capsule.sh +113 -0
- package/scripts/show-deps-tree.sh +66 -0
- package/scripts/show-stats-dashboard.sh +52 -0
- package/scripts/show-stats.sh +79 -0
- package/skills/code-review/SKILL.md +520 -0
- package/skills/crew/SKILL.md +395 -0
- package/skills/debug/SKILL.md +473 -0
- package/skills/deep-context/SKILL.md +446 -0
- package/skills/task-router/SKILL.md +390 -0
- package/skills/workflow/SKILL.md +370 -0
- package/templates/CLAUDE.md +124 -0
- package/templates/crew-config.json +21 -0
- package/templates/settings-hooks.json +74 -0
- package/templates/statusline-command.sh +208 -0
- package/tools/context-query/context-query.js +312 -0
- package/tools/context-query/context-query.sh +5 -0
- package/tools/context-query/tool.json +42 -0
- package/tools/dependency-scanner/dependency-scanner.sh +53 -0
- package/tools/dependency-scanner/tool.json +8 -0
- package/tools/find-circular/find-circular.sh +41 -0
- package/tools/find-circular/tool.json +36 -0
- package/tools/find-dead-code/find-dead-code.sh +41 -0
- package/tools/find-dead-code/tool.json +36 -0
- package/tools/impact-analysis/impact-analysis.sh +99 -0
- package/tools/impact-analysis/tool.json +38 -0
- package/tools/progressive-reader/progressive-reader.sh +14 -0
- package/tools/progressive-reader/tool.json +69 -0
- package/tools/query-deps/query-deps.sh +69 -0
- package/tools/query-deps/tool.json +34 -0
- package/tools/stats/stats.js +299 -0
- package/tools/stats/stats.sh +5 -0
- package/tools/stats/tool.json +34 -0
- package/tools/token-counter/README.md +73 -0
- package/tools/token-counter/token-counter.py +202 -0
- 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
|
+
}
|