agileflow 2.51.0 → 2.55.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 +82 -460
- package/package.json +18 -3
- package/scripts/agileflow-configure.js +134 -63
- package/scripts/agileflow-welcome.js +161 -31
- package/scripts/generators/agent-registry.js +2 -2
- package/scripts/generators/command-registry.js +6 -6
- package/scripts/generators/index.js +2 -6
- package/scripts/generators/inject-babysit.js +9 -2
- package/scripts/generators/inject-help.js +3 -1
- package/scripts/generators/inject-readme.js +7 -3
- package/scripts/generators/skill-registry.js +5 -5
- package/scripts/get-env.js +13 -12
- package/scripts/obtain-context.js +79 -26
- package/scripts/session-coordinator.sh +232 -0
- package/scripts/session-manager.js +512 -0
- package/src/core/agents/orchestrator.md +275 -0
- package/src/core/commands/adr.md +38 -16
- package/src/core/commands/agent.md +39 -22
- package/src/core/commands/assign.md +17 -0
- package/src/core/commands/auto.md +60 -46
- package/src/core/commands/babysit.md +302 -637
- package/src/core/commands/baseline.md +20 -0
- package/src/core/commands/blockers.md +33 -48
- package/src/core/commands/board.md +19 -0
- package/src/core/commands/changelog.md +20 -0
- package/src/core/commands/ci.md +17 -0
- package/src/core/commands/context.md +43 -40
- package/src/core/commands/debt.md +76 -45
- package/src/core/commands/deploy.md +20 -0
- package/src/core/commands/deps.md +40 -46
- package/src/core/commands/diagnose.md +24 -18
- package/src/core/commands/docs.md +18 -0
- package/src/core/commands/epic.md +31 -0
- package/src/core/commands/feedback.md +33 -21
- package/src/core/commands/handoff.md +29 -0
- package/src/core/commands/help.md +16 -7
- package/src/core/commands/impact.md +31 -61
- package/src/core/commands/metrics.md +17 -35
- package/src/core/commands/packages.md +21 -0
- package/src/core/commands/pr.md +15 -0
- package/src/core/commands/readme-sync.md +42 -9
- package/src/core/commands/research.md +58 -11
- package/src/core/commands/retro.md +42 -50
- package/src/core/commands/review.md +22 -27
- package/src/core/commands/session/end.md +53 -297
- package/src/core/commands/session/history.md +38 -257
- package/src/core/commands/session/init.md +44 -446
- package/src/core/commands/session/new.md +152 -0
- package/src/core/commands/session/resume.md +51 -447
- package/src/core/commands/session/status.md +32 -244
- package/src/core/commands/sprint.md +33 -0
- package/src/core/commands/status.md +18 -0
- package/src/core/commands/story-validate.md +32 -0
- package/src/core/commands/story.md +21 -6
- package/src/core/commands/template.md +18 -0
- package/src/core/commands/tests.md +22 -0
- package/src/core/commands/update.md +72 -58
- package/src/core/commands/validate-expertise.md +25 -37
- package/src/core/commands/velocity.md +33 -74
- package/src/core/commands/verify.md +16 -0
- package/src/core/experts/documentation/expertise.yaml +16 -2
- package/src/core/skills/agileflow-retro-facilitator/SKILL.md +57 -219
- package/src/core/skills/agileflow-retro-facilitator/cookbook/4ls.md +86 -0
- package/src/core/skills/agileflow-retro-facilitator/cookbook/glad-sad-mad.md +79 -0
- package/src/core/skills/agileflow-retro-facilitator/cookbook/start-stop-continue.md +142 -0
- package/src/core/skills/agileflow-retro-facilitator/prompts/action-items.md +83 -0
- package/src/core/skills/writing-skills/SKILL.md +352 -0
- package/src/core/skills/writing-skills/testing-skills-with-subagents.md +232 -0
- package/tools/cli/agileflow-cli.js +4 -2
- package/tools/cli/commands/config.js +20 -13
- package/tools/cli/commands/doctor.js +25 -9
- package/tools/cli/commands/list.js +10 -6
- package/tools/cli/commands/setup.js +54 -3
- package/tools/cli/commands/status.js +6 -8
- package/tools/cli/commands/uninstall.js +5 -5
- package/tools/cli/commands/update.js +51 -7
- package/tools/cli/installers/core/installer.js +8 -4
- package/tools/cli/installers/ide/_base-ide.js +3 -1
- package/tools/cli/installers/ide/claude-code.js +3 -7
- package/tools/cli/installers/ide/codex.js +440 -0
- package/tools/cli/installers/ide/manager.js +2 -6
- package/tools/cli/lib/content-injector.js +3 -3
- package/tools/cli/lib/docs-setup.js +3 -2
- package/tools/cli/lib/npm-utils.js +3 -3
- package/tools/cli/lib/ui.js +7 -7
- package/tools/cli/lib/version-checker.js +3 -3
- package/tools/postinstall.js +2 -3
|
@@ -28,7 +28,11 @@ if (commandName) {
|
|
|
28
28
|
if (fs.existsSync(sessionStatePath)) {
|
|
29
29
|
try {
|
|
30
30
|
const state = JSON.parse(fs.readFileSync(sessionStatePath, 'utf8'));
|
|
31
|
-
state.active_command = {
|
|
31
|
+
state.active_command = {
|
|
32
|
+
name: commandName,
|
|
33
|
+
activated_at: new Date().toISOString(),
|
|
34
|
+
state: {},
|
|
35
|
+
};
|
|
32
36
|
fs.writeFileSync(sessionStatePath, JSON.stringify(state, null, 2) + '\n');
|
|
33
37
|
} catch (e) {
|
|
34
38
|
// Silently continue if session state can't be updated
|
|
@@ -92,9 +96,16 @@ function safeExec(cmd) {
|
|
|
92
96
|
function generateSummary() {
|
|
93
97
|
// Box drawing characters
|
|
94
98
|
const box = {
|
|
95
|
-
tl: '╭',
|
|
96
|
-
|
|
97
|
-
|
|
99
|
+
tl: '╭',
|
|
100
|
+
tr: '╮',
|
|
101
|
+
bl: '╰',
|
|
102
|
+
br: '╯',
|
|
103
|
+
h: '─',
|
|
104
|
+
v: '│',
|
|
105
|
+
lT: '├',
|
|
106
|
+
rT: '┤',
|
|
107
|
+
tT: '┬',
|
|
108
|
+
bT: '┴',
|
|
98
109
|
cross: '┼',
|
|
99
110
|
};
|
|
100
111
|
|
|
@@ -144,7 +155,8 @@ function generateSummary() {
|
|
|
144
155
|
return `${C.dim}${box.v}${C.reset} ${pad(leftStr, L)} ${C.dim}${box.v}${C.reset} ${pad(rightStr, R)} ${C.dim}${box.v}${C.reset}\n`;
|
|
145
156
|
}
|
|
146
157
|
|
|
147
|
-
const divider = () =>
|
|
158
|
+
const divider = () =>
|
|
159
|
+
`${C.dim}${box.lT}${box.h.repeat(L + 2)}${box.cross}${box.h.repeat(R + 2)}${box.rT}${C.reset}\n`;
|
|
148
160
|
const headerTopBorder = `${C.dim}${box.tl}${box.h.repeat(W + 2)}${box.tr}${C.reset}\n`;
|
|
149
161
|
const headerDivider = `${C.dim}${box.lT}${box.h.repeat(L + 2)}${box.tT}${box.h.repeat(R + 2)}${box.rT}${C.reset}\n`;
|
|
150
162
|
const bottomBorder = `${C.dim}${box.bl}${box.h.repeat(L + 2)}${box.bT}${box.h.repeat(R + 2)}${box.br}${C.reset}\n`;
|
|
@@ -156,12 +168,15 @@ function generateSummary() {
|
|
|
156
168
|
const statusLines = (safeExec('git status --short') || '').split('\n').filter(Boolean);
|
|
157
169
|
const statusJson = safeReadJSON('docs/09-agents/status.json');
|
|
158
170
|
const sessionState = safeReadJSON('docs/09-agents/session-state.json');
|
|
159
|
-
const researchFiles = safeLs('docs/10-research')
|
|
171
|
+
const researchFiles = safeLs('docs/10-research')
|
|
172
|
+
.filter(f => f.endsWith('.md') && f !== 'README.md')
|
|
173
|
+
.sort()
|
|
174
|
+
.reverse();
|
|
160
175
|
const epicFiles = safeLs('docs/05-epics').filter(f => f.endsWith('.md') && f !== 'README.md');
|
|
161
176
|
|
|
162
177
|
// Count stories by status
|
|
163
|
-
|
|
164
|
-
|
|
178
|
+
const byStatus = {};
|
|
179
|
+
const readyStories = [];
|
|
165
180
|
if (statusJson && statusJson.stories) {
|
|
166
181
|
Object.entries(statusJson.stories).forEach(([id, story]) => {
|
|
167
182
|
const s = story.status || 'unknown';
|
|
@@ -187,23 +202,45 @@ function generateSummary() {
|
|
|
187
202
|
const title = commandName ? `Context [${commandName}]` : 'Context Summary';
|
|
188
203
|
const branchColor = branch === 'main' ? C.green : branch.startsWith('fix') ? C.red : C.cyan;
|
|
189
204
|
const maxBranchLen = 20;
|
|
190
|
-
const branchDisplay =
|
|
205
|
+
const branchDisplay =
|
|
206
|
+
branch.length > maxBranchLen ? branch.substring(0, maxBranchLen - 2) + '..' : branch;
|
|
191
207
|
const header = `${C.brand}${C.bold}${title}${C.reset} ${branchColor}${branchDisplay}${C.reset} ${C.dim}(${lastCommitShort})${C.reset}`;
|
|
192
208
|
summary += `${C.dim}${box.v}${C.reset} ${pad(header, W)} ${C.dim}${box.v}${C.reset}\n`;
|
|
193
209
|
|
|
194
210
|
summary += headerDivider;
|
|
195
211
|
|
|
196
212
|
// Story counts with colorful labels
|
|
197
|
-
summary += row(
|
|
198
|
-
|
|
199
|
-
|
|
213
|
+
summary += row(
|
|
214
|
+
'In Progress',
|
|
215
|
+
byStatus['in-progress'] ? `${byStatus['in-progress']}` : '0',
|
|
216
|
+
C.yellow,
|
|
217
|
+
byStatus['in-progress'] ? C.brightYellow : C.dim
|
|
218
|
+
);
|
|
219
|
+
summary += row(
|
|
220
|
+
'Blocked',
|
|
221
|
+
byStatus['blocked'] ? `${byStatus['blocked']}` : '0',
|
|
222
|
+
C.red,
|
|
223
|
+
byStatus['blocked'] ? C.red : C.dim
|
|
224
|
+
);
|
|
225
|
+
summary += row(
|
|
226
|
+
'Ready',
|
|
227
|
+
byStatus['ready'] ? `${byStatus['ready']}` : '0',
|
|
228
|
+
C.cyan,
|
|
229
|
+
byStatus['ready'] ? C.brightCyan : C.dim
|
|
230
|
+
);
|
|
200
231
|
const completedColor = `${C.bold}${C.green}`;
|
|
201
|
-
summary += row(
|
|
232
|
+
summary += row(
|
|
233
|
+
'Completed',
|
|
234
|
+
byStatus['done'] ? `${byStatus['done']}` : '0',
|
|
235
|
+
completedColor,
|
|
236
|
+
byStatus['done'] ? completedColor : C.dim
|
|
237
|
+
);
|
|
202
238
|
|
|
203
239
|
summary += divider();
|
|
204
240
|
|
|
205
241
|
// Git status
|
|
206
|
-
const uncommittedStatus =
|
|
242
|
+
const uncommittedStatus =
|
|
243
|
+
statusLines.length > 0 ? `${statusLines.length} uncommitted` : '✓ clean';
|
|
207
244
|
summary += row('Git', uncommittedStatus, C.blue, statusLines.length > 0 ? C.yellow : C.green);
|
|
208
245
|
|
|
209
246
|
// Session
|
|
@@ -228,10 +265,12 @@ function generateSummary() {
|
|
|
228
265
|
{ path: 'docs/04-architecture/README.md', label: 'arch' },
|
|
229
266
|
{ path: 'docs/02-practices/README.md', label: 'practices' },
|
|
230
267
|
];
|
|
231
|
-
const keyFileStatus = keyFileChecks
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
268
|
+
const keyFileStatus = keyFileChecks
|
|
269
|
+
.map(f => {
|
|
270
|
+
const exists = fs.existsSync(f.path);
|
|
271
|
+
return exists ? `${C.brightGreen}✓${C.reset}${f.label}` : `${C.dim}○${f.label}${C.reset}`;
|
|
272
|
+
})
|
|
273
|
+
.join(' ');
|
|
235
274
|
summary += row('Key files', keyFileStatus, C.magenta, '');
|
|
236
275
|
|
|
237
276
|
// Research
|
|
@@ -245,7 +284,12 @@ function generateSummary() {
|
|
|
245
284
|
summary += divider();
|
|
246
285
|
|
|
247
286
|
// Last commit
|
|
248
|
-
summary += row(
|
|
287
|
+
summary += row(
|
|
288
|
+
'Last commit',
|
|
289
|
+
`${C.yellow}${lastCommitShort}${C.reset} ${lastCommitMsg}`,
|
|
290
|
+
C.dim,
|
|
291
|
+
''
|
|
292
|
+
);
|
|
249
293
|
|
|
250
294
|
summary += bottomBorder;
|
|
251
295
|
|
|
@@ -276,8 +320,9 @@ function generateFullContent() {
|
|
|
276
320
|
content += `Last commit: ${C.dim}${lastCommit}${C.reset}\n`;
|
|
277
321
|
if (statusLines.length > 0) {
|
|
278
322
|
content += `Uncommitted: ${C.yellow}${statusLines.length} file(s)${C.reset}\n`;
|
|
279
|
-
statusLines.slice(0, 10).forEach(line => content += ` ${C.dim}${line}${C.reset}\n`);
|
|
280
|
-
if (statusLines.length > 10)
|
|
323
|
+
statusLines.slice(0, 10).forEach(line => (content += ` ${C.dim}${line}${C.reset}\n`));
|
|
324
|
+
if (statusLines.length > 10)
|
|
325
|
+
content += ` ${C.dim}... and ${statusLines.length - 10} more${C.reset}\n`;
|
|
281
326
|
} else {
|
|
282
327
|
content += `Uncommitted: ${C.green}clean${C.reset}\n`;
|
|
283
328
|
}
|
|
@@ -289,7 +334,11 @@ function generateFullContent() {
|
|
|
289
334
|
|
|
290
335
|
if (statusJson) {
|
|
291
336
|
content += `${C.dim}${'─'.repeat(50)}${C.reset}\n`;
|
|
292
|
-
content +=
|
|
337
|
+
content +=
|
|
338
|
+
JSON.stringify(statusJson, null, 2)
|
|
339
|
+
.split('\n')
|
|
340
|
+
.map(l => ` ${l}`)
|
|
341
|
+
.join('\n') + '\n';
|
|
293
342
|
content += `${C.dim}${'─'.repeat(50)}${C.reset}\n`;
|
|
294
343
|
} else {
|
|
295
344
|
content += `${C.dim}No status.json found${C.reset}\n`;
|
|
@@ -321,7 +370,11 @@ function generateFullContent() {
|
|
|
321
370
|
content += `\n${C.cyan}${C.bold}═══ Documentation ═══${C.reset}\n`;
|
|
322
371
|
const docsDir = 'docs';
|
|
323
372
|
const docFolders = safeLs(docsDir).filter(f => {
|
|
324
|
-
try {
|
|
373
|
+
try {
|
|
374
|
+
return fs.statSync(path.join(docsDir, f)).isDirectory();
|
|
375
|
+
} catch {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
325
378
|
});
|
|
326
379
|
|
|
327
380
|
if (docFolders.length > 0) {
|
|
@@ -330,7 +383,7 @@ function generateFullContent() {
|
|
|
330
383
|
const files = safeLs(folderPath);
|
|
331
384
|
const mdFiles = files.filter(f => f.endsWith('.md'));
|
|
332
385
|
const jsonFiles = files.filter(f => f.endsWith('.json') || f.endsWith('.jsonl'));
|
|
333
|
-
|
|
386
|
+
const info = [];
|
|
334
387
|
if (mdFiles.length > 0) info.push(`${mdFiles.length} md`);
|
|
335
388
|
if (jsonFiles.length > 0) info.push(`${jsonFiles.length} json`);
|
|
336
389
|
content += ` ${C.dim}${folder}/${C.reset} ${info.length > 0 ? `(${info.join(', ')})` : ''}\n`;
|
|
@@ -344,7 +397,7 @@ function generateFullContent() {
|
|
|
344
397
|
if (researchFiles.length > 0) {
|
|
345
398
|
researchFiles.sort().reverse();
|
|
346
399
|
content += `${C.dim}───${C.reset} Available Research Notes\n`;
|
|
347
|
-
researchFiles.forEach(file => content += ` ${C.dim}${file}${C.reset}\n`);
|
|
400
|
+
researchFiles.forEach(file => (content += ` ${C.dim}${file}${C.reset}\n`));
|
|
348
401
|
|
|
349
402
|
const mostRecentFile = researchFiles[0];
|
|
350
403
|
const mostRecentPath = path.join(researchDir, mostRecentFile);
|
|
@@ -414,7 +467,7 @@ function generateFullContent() {
|
|
|
414
467
|
content += `\n${C.cyan}${C.bold}═══ Epic Files ═══${C.reset}\n`;
|
|
415
468
|
const epicFiles = safeLs('docs/05-epics').filter(f => f.endsWith('.md') && f !== 'README.md');
|
|
416
469
|
if (epicFiles.length > 0) {
|
|
417
|
-
epicFiles.forEach(file => content += ` ${C.dim}${file}${C.reset}\n`);
|
|
470
|
+
epicFiles.forEach(file => (content += ` ${C.dim}${file}${C.reset}\n`));
|
|
418
471
|
} else {
|
|
419
472
|
content += `${C.dim}No epic files${C.reset}\n`;
|
|
420
473
|
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# session-coordinator.sh - Automatic multi-session coordination
|
|
3
|
+
# Detects concurrent Claude Code sessions and handles conflicts
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
SESSIONS_DIR=".agileflow/sessions"
|
|
8
|
+
LOCK_TIMEOUT=3600 # 1 hour - stale session threshold
|
|
9
|
+
|
|
10
|
+
# Colors
|
|
11
|
+
RED='\033[0;31m'
|
|
12
|
+
GREEN='\033[0;32m'
|
|
13
|
+
YELLOW='\033[1;33m'
|
|
14
|
+
BLUE='\033[0;34m'
|
|
15
|
+
CYAN='\033[0;36m'
|
|
16
|
+
DIM='\033[2m'
|
|
17
|
+
RESET='\033[0m'
|
|
18
|
+
|
|
19
|
+
# Get current session ID (PID + timestamp for uniqueness)
|
|
20
|
+
SESSION_ID="$$-$(date +%s)"
|
|
21
|
+
SESSION_FILE="$SESSIONS_DIR/session-$SESSION_ID.lock"
|
|
22
|
+
|
|
23
|
+
# Ensure sessions directory exists
|
|
24
|
+
mkdir -p "$SESSIONS_DIR"
|
|
25
|
+
|
|
26
|
+
# Function: Check if a PID is still running
|
|
27
|
+
is_pid_alive() {
|
|
28
|
+
local pid=$1
|
|
29
|
+
if [[ -z "$pid" ]]; then
|
|
30
|
+
return 1
|
|
31
|
+
fi
|
|
32
|
+
kill -0 "$pid" 2>/dev/null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Function: Clean up stale sessions
|
|
36
|
+
cleanup_stale_sessions() {
|
|
37
|
+
local now=$(date +%s)
|
|
38
|
+
|
|
39
|
+
for lockfile in "$SESSIONS_DIR"/session-*.lock 2>/dev/null; do
|
|
40
|
+
[[ -f "$lockfile" ]] || continue
|
|
41
|
+
|
|
42
|
+
# Read lock file
|
|
43
|
+
local lock_pid=$(grep "^pid=" "$lockfile" 2>/dev/null | cut -d= -f2)
|
|
44
|
+
local lock_time=$(grep "^started=" "$lockfile" 2>/dev/null | cut -d= -f2)
|
|
45
|
+
|
|
46
|
+
# Check if stale (PID dead or too old)
|
|
47
|
+
if ! is_pid_alive "$lock_pid"; then
|
|
48
|
+
rm -f "$lockfile"
|
|
49
|
+
continue
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Check if expired
|
|
53
|
+
if [[ -n "$lock_time" ]]; then
|
|
54
|
+
local age=$((now - lock_time))
|
|
55
|
+
if [[ $age -gt $LOCK_TIMEOUT ]]; then
|
|
56
|
+
rm -f "$lockfile"
|
|
57
|
+
fi
|
|
58
|
+
fi
|
|
59
|
+
done
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Function: Get list of active sessions
|
|
63
|
+
get_active_sessions() {
|
|
64
|
+
local sessions=()
|
|
65
|
+
|
|
66
|
+
for lockfile in "$SESSIONS_DIR"/session-*.lock 2>/dev/null; do
|
|
67
|
+
[[ -f "$lockfile" ]] || continue
|
|
68
|
+
|
|
69
|
+
local lock_pid=$(grep "^pid=" "$lockfile" 2>/dev/null | cut -d= -f2)
|
|
70
|
+
|
|
71
|
+
if is_pid_alive "$lock_pid"; then
|
|
72
|
+
local branch=$(grep "^branch=" "$lockfile" 2>/dev/null | cut -d= -f2)
|
|
73
|
+
local worktree=$(grep "^worktree=" "$lockfile" 2>/dev/null | cut -d= -f2)
|
|
74
|
+
local story=$(grep "^story=" "$lockfile" 2>/dev/null | cut -d= -f2)
|
|
75
|
+
sessions+=("$lock_pid:$branch:$worktree:$story")
|
|
76
|
+
fi
|
|
77
|
+
done
|
|
78
|
+
|
|
79
|
+
printf '%s\n' "${sessions[@]}"
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# Function: Register this session
|
|
83
|
+
register_session() {
|
|
84
|
+
local branch=$(git branch --show-current 2>/dev/null || echo "unknown")
|
|
85
|
+
local worktree=$(pwd)
|
|
86
|
+
local story=""
|
|
87
|
+
|
|
88
|
+
# Try to get current story from status.json
|
|
89
|
+
if [[ -f "docs/09-agents/status.json" ]]; then
|
|
90
|
+
story=$(grep -o '"current_story"[[:space:]]*:[[:space:]]*"[^"]*"' docs/09-agents/status.json 2>/dev/null | cut -d'"' -f4 || echo "")
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
cat > "$SESSION_FILE" << EOF
|
|
94
|
+
pid=$$
|
|
95
|
+
started=$(date +%s)
|
|
96
|
+
branch=$branch
|
|
97
|
+
worktree=$worktree
|
|
98
|
+
story=$story
|
|
99
|
+
user=$(whoami)
|
|
100
|
+
EOF
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# Function: Unregister session (called on exit)
|
|
104
|
+
unregister_session() {
|
|
105
|
+
rm -f "$SESSION_FILE"
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# Function: Create worktree for parallel work
|
|
109
|
+
create_worktree() {
|
|
110
|
+
local branch_name=$1
|
|
111
|
+
local worktree_path="../$(basename $(pwd))-$branch_name"
|
|
112
|
+
|
|
113
|
+
echo -e "${BLUE}Creating worktree at $worktree_path...${RESET}"
|
|
114
|
+
|
|
115
|
+
# Create branch if it doesn't exist
|
|
116
|
+
if ! git show-ref --verify --quiet "refs/heads/$branch_name"; then
|
|
117
|
+
git branch "$branch_name"
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# Create worktree
|
|
121
|
+
git worktree add "$worktree_path" "$branch_name" 2>/dev/null || {
|
|
122
|
+
echo -e "${RED}Failed to create worktree${RESET}"
|
|
123
|
+
return 1
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
echo -e "${GREEN}✓ Worktree created: $worktree_path${RESET}"
|
|
127
|
+
echo -e "${CYAN}Run: cd $worktree_path && claude${RESET}"
|
|
128
|
+
|
|
129
|
+
echo "$worktree_path"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# Function: Display session status
|
|
133
|
+
show_session_status() {
|
|
134
|
+
local active_sessions=($(get_active_sessions))
|
|
135
|
+
local count=${#active_sessions[@]}
|
|
136
|
+
|
|
137
|
+
if [[ $count -eq 0 ]]; then
|
|
138
|
+
echo -e "${GREEN}✓ No other active sessions${RESET}"
|
|
139
|
+
return 0
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
echo -e "${YELLOW}⚠ $count other session(s) active:${RESET}"
|
|
143
|
+
echo ""
|
|
144
|
+
|
|
145
|
+
for session in "${active_sessions[@]}"; do
|
|
146
|
+
IFS=':' read -r pid branch worktree story <<< "$session"
|
|
147
|
+
echo -e " ${DIM}PID${RESET} $pid"
|
|
148
|
+
echo -e " ${DIM}Branch${RESET} $branch"
|
|
149
|
+
[[ -n "$story" ]] && echo -e " ${DIM}Story${RESET} $story"
|
|
150
|
+
echo -e " ${DIM}Path${RESET} $worktree"
|
|
151
|
+
echo ""
|
|
152
|
+
done
|
|
153
|
+
|
|
154
|
+
return 1
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# Function: Suggest worktree with command
|
|
158
|
+
suggest_worktree() {
|
|
159
|
+
local suggested_branch="session-$(date +%Y%m%d-%H%M%S)"
|
|
160
|
+
|
|
161
|
+
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
|
|
162
|
+
echo -e "${YELLOW}To work in parallel without conflicts:${RESET}"
|
|
163
|
+
echo ""
|
|
164
|
+
echo -e " ${GREEN}Option 1: Create isolated worktree (recommended)${RESET}"
|
|
165
|
+
echo -e " ${DIM}git worktree add ../${PWD##*/}-parallel $suggested_branch${RESET}"
|
|
166
|
+
echo -e " ${DIM}cd ../${PWD##*/}-parallel && claude${RESET}"
|
|
167
|
+
echo ""
|
|
168
|
+
echo -e " ${GREEN}Option 2: Work on different branch${RESET}"
|
|
169
|
+
echo -e " ${DIM}git checkout -b $suggested_branch${RESET}"
|
|
170
|
+
echo ""
|
|
171
|
+
echo -e " ${GREEN}Option 3: Continue anyway (risk conflicts)${RESET}"
|
|
172
|
+
echo -e " ${DIM}Touch different files than other session${RESET}"
|
|
173
|
+
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
# Main execution
|
|
177
|
+
main() {
|
|
178
|
+
local mode="${1:-check}"
|
|
179
|
+
|
|
180
|
+
case "$mode" in
|
|
181
|
+
"check")
|
|
182
|
+
# Clean up stale sessions first
|
|
183
|
+
cleanup_stale_sessions
|
|
184
|
+
|
|
185
|
+
# Check for active sessions
|
|
186
|
+
if show_session_status; then
|
|
187
|
+
# No conflicts - register and continue
|
|
188
|
+
register_session
|
|
189
|
+
trap unregister_session EXIT
|
|
190
|
+
else
|
|
191
|
+
# Conflicts detected - show options
|
|
192
|
+
suggest_worktree
|
|
193
|
+
|
|
194
|
+
# Still register (user may proceed anyway)
|
|
195
|
+
register_session
|
|
196
|
+
trap unregister_session EXIT
|
|
197
|
+
fi
|
|
198
|
+
;;
|
|
199
|
+
|
|
200
|
+
"register")
|
|
201
|
+
register_session
|
|
202
|
+
echo -e "${GREEN}✓ Session registered${RESET}"
|
|
203
|
+
;;
|
|
204
|
+
|
|
205
|
+
"unregister")
|
|
206
|
+
unregister_session
|
|
207
|
+
echo -e "${GREEN}✓ Session unregistered${RESET}"
|
|
208
|
+
;;
|
|
209
|
+
|
|
210
|
+
"status")
|
|
211
|
+
cleanup_stale_sessions
|
|
212
|
+
show_session_status
|
|
213
|
+
;;
|
|
214
|
+
|
|
215
|
+
"worktree")
|
|
216
|
+
local branch="${2:-session-$(date +%Y%m%d-%H%M%S)}"
|
|
217
|
+
create_worktree "$branch"
|
|
218
|
+
;;
|
|
219
|
+
|
|
220
|
+
"cleanup")
|
|
221
|
+
cleanup_stale_sessions
|
|
222
|
+
echo -e "${GREEN}✓ Stale sessions cleaned${RESET}"
|
|
223
|
+
;;
|
|
224
|
+
|
|
225
|
+
*)
|
|
226
|
+
echo "Usage: session-coordinator.sh [check|register|unregister|status|worktree|cleanup]"
|
|
227
|
+
exit 1
|
|
228
|
+
;;
|
|
229
|
+
esac
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
main "$@"
|