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.
Files changed (87) hide show
  1. package/README.md +82 -460
  2. package/package.json +18 -3
  3. package/scripts/agileflow-configure.js +134 -63
  4. package/scripts/agileflow-welcome.js +161 -31
  5. package/scripts/generators/agent-registry.js +2 -2
  6. package/scripts/generators/command-registry.js +6 -6
  7. package/scripts/generators/index.js +2 -6
  8. package/scripts/generators/inject-babysit.js +9 -2
  9. package/scripts/generators/inject-help.js +3 -1
  10. package/scripts/generators/inject-readme.js +7 -3
  11. package/scripts/generators/skill-registry.js +5 -5
  12. package/scripts/get-env.js +13 -12
  13. package/scripts/obtain-context.js +79 -26
  14. package/scripts/session-coordinator.sh +232 -0
  15. package/scripts/session-manager.js +512 -0
  16. package/src/core/agents/orchestrator.md +275 -0
  17. package/src/core/commands/adr.md +38 -16
  18. package/src/core/commands/agent.md +39 -22
  19. package/src/core/commands/assign.md +17 -0
  20. package/src/core/commands/auto.md +60 -46
  21. package/src/core/commands/babysit.md +302 -637
  22. package/src/core/commands/baseline.md +20 -0
  23. package/src/core/commands/blockers.md +33 -48
  24. package/src/core/commands/board.md +19 -0
  25. package/src/core/commands/changelog.md +20 -0
  26. package/src/core/commands/ci.md +17 -0
  27. package/src/core/commands/context.md +43 -40
  28. package/src/core/commands/debt.md +76 -45
  29. package/src/core/commands/deploy.md +20 -0
  30. package/src/core/commands/deps.md +40 -46
  31. package/src/core/commands/diagnose.md +24 -18
  32. package/src/core/commands/docs.md +18 -0
  33. package/src/core/commands/epic.md +31 -0
  34. package/src/core/commands/feedback.md +33 -21
  35. package/src/core/commands/handoff.md +29 -0
  36. package/src/core/commands/help.md +16 -7
  37. package/src/core/commands/impact.md +31 -61
  38. package/src/core/commands/metrics.md +17 -35
  39. package/src/core/commands/packages.md +21 -0
  40. package/src/core/commands/pr.md +15 -0
  41. package/src/core/commands/readme-sync.md +42 -9
  42. package/src/core/commands/research.md +58 -11
  43. package/src/core/commands/retro.md +42 -50
  44. package/src/core/commands/review.md +22 -27
  45. package/src/core/commands/session/end.md +53 -297
  46. package/src/core/commands/session/history.md +38 -257
  47. package/src/core/commands/session/init.md +44 -446
  48. package/src/core/commands/session/new.md +152 -0
  49. package/src/core/commands/session/resume.md +51 -447
  50. package/src/core/commands/session/status.md +32 -244
  51. package/src/core/commands/sprint.md +33 -0
  52. package/src/core/commands/status.md +18 -0
  53. package/src/core/commands/story-validate.md +32 -0
  54. package/src/core/commands/story.md +21 -6
  55. package/src/core/commands/template.md +18 -0
  56. package/src/core/commands/tests.md +22 -0
  57. package/src/core/commands/update.md +72 -58
  58. package/src/core/commands/validate-expertise.md +25 -37
  59. package/src/core/commands/velocity.md +33 -74
  60. package/src/core/commands/verify.md +16 -0
  61. package/src/core/experts/documentation/expertise.yaml +16 -2
  62. package/src/core/skills/agileflow-retro-facilitator/SKILL.md +57 -219
  63. package/src/core/skills/agileflow-retro-facilitator/cookbook/4ls.md +86 -0
  64. package/src/core/skills/agileflow-retro-facilitator/cookbook/glad-sad-mad.md +79 -0
  65. package/src/core/skills/agileflow-retro-facilitator/cookbook/start-stop-continue.md +142 -0
  66. package/src/core/skills/agileflow-retro-facilitator/prompts/action-items.md +83 -0
  67. package/src/core/skills/writing-skills/SKILL.md +352 -0
  68. package/src/core/skills/writing-skills/testing-skills-with-subagents.md +232 -0
  69. package/tools/cli/agileflow-cli.js +4 -2
  70. package/tools/cli/commands/config.js +20 -13
  71. package/tools/cli/commands/doctor.js +25 -9
  72. package/tools/cli/commands/list.js +10 -6
  73. package/tools/cli/commands/setup.js +54 -3
  74. package/tools/cli/commands/status.js +6 -8
  75. package/tools/cli/commands/uninstall.js +5 -5
  76. package/tools/cli/commands/update.js +51 -7
  77. package/tools/cli/installers/core/installer.js +8 -4
  78. package/tools/cli/installers/ide/_base-ide.js +3 -1
  79. package/tools/cli/installers/ide/claude-code.js +3 -7
  80. package/tools/cli/installers/ide/codex.js +440 -0
  81. package/tools/cli/installers/ide/manager.js +2 -6
  82. package/tools/cli/lib/content-injector.js +3 -3
  83. package/tools/cli/lib/docs-setup.js +3 -2
  84. package/tools/cli/lib/npm-utils.js +3 -3
  85. package/tools/cli/lib/ui.js +7 -7
  86. package/tools/cli/lib/version-checker.js +3 -3
  87. 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 = { name: commandName, activated_at: new Date().toISOString(), state: {} };
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: '╭', tr: '╮', bl: '╰', br: '╯',
96
- h: '', v: '│',
97
- lT: '', rT: '┤', tT: '┬', bT: '┴',
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 = () => `${C.dim}${box.lT}${box.h.repeat(L + 2)}${box.cross}${box.h.repeat(R + 2)}${box.rT}${C.reset}\n`;
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').filter(f => f.endsWith('.md') && f !== 'README.md').sort().reverse();
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
- let byStatus = {};
164
- let readyStories = [];
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 = branch.length > maxBranchLen ? branch.substring(0, maxBranchLen - 2) + '..' : branch;
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('In Progress', byStatus['in-progress'] ? `${byStatus['in-progress']}` : '0', C.yellow, byStatus['in-progress'] ? C.brightYellow : C.dim);
198
- summary += row('Blocked', byStatus['blocked'] ? `${byStatus['blocked']}` : '0', C.red, byStatus['blocked'] ? C.red : C.dim);
199
- summary += row('Ready', byStatus['ready'] ? `${byStatus['ready']}` : '0', C.cyan, byStatus['ready'] ? C.brightCyan : C.dim);
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('Completed', byStatus['done'] ? `${byStatus['done']}` : '0', completedColor, byStatus['done'] ? completedColor : C.dim);
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 = statusLines.length > 0 ? `${statusLines.length} uncommitted` : '✓ clean';
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.map(f => {
232
- const exists = fs.existsSync(f.path);
233
- return exists ? `${C.brightGreen}✓${C.reset}${f.label}` : `${C.dim}○${f.label}${C.reset}`;
234
- }).join(' ');
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('Last commit', `${C.yellow}${lastCommitShort}${C.reset} ${lastCommitMsg}`, C.dim, '');
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) content += ` ${C.dim}... and ${statusLines.length - 10} more${C.reset}\n`;
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 += JSON.stringify(statusJson, null, 2).split('\n').map(l => ` ${l}`).join('\n') + '\n';
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 { return fs.statSync(path.join(docsDir, f)).isDirectory(); } catch { return false; }
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
- let info = [];
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 "$@"