agileflow 3.0.1 → 3.1.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 (69) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +8 -8
  3. package/lib/api-server.js +3 -2
  4. package/lib/feedback.js +9 -2
  5. package/lib/flag-detection.js +4 -2
  6. package/lib/git-operations.js +4 -2
  7. package/lib/lazy-require.js +59 -0
  8. package/lib/process-executor.js +24 -9
  9. package/lib/skill-loader.js +11 -3
  10. package/package.json +1 -1
  11. package/scripts/agileflow-configure.js +12 -0
  12. package/scripts/agileflow-welcome.js +146 -90
  13. package/scripts/claude-tmux.sh +42 -6
  14. package/scripts/damage-control-multi-agent.js +14 -10
  15. package/scripts/lib/bus-utils.js +3 -1
  16. package/scripts/lib/configure-detect.js +12 -9
  17. package/scripts/lib/configure-features.js +128 -7
  18. package/scripts/lib/configure-repair.js +6 -5
  19. package/scripts/lib/context-formatter.js +13 -3
  20. package/scripts/lib/damage-control-utils.js +5 -1
  21. package/scripts/lib/lifecycle-detector.js +5 -3
  22. package/scripts/lib/process-cleanup.js +8 -4
  23. package/scripts/lib/scale-detector.js +47 -8
  24. package/scripts/lib/signal-detectors.js +117 -59
  25. package/scripts/lib/task-registry.js +5 -1
  26. package/scripts/lib/team-events.js +4 -4
  27. package/scripts/messaging-bridge.js +7 -1
  28. package/scripts/ralph-loop.js +10 -8
  29. package/scripts/smart-detect.js +32 -11
  30. package/scripts/team-manager.js +86 -1
  31. package/scripts/tmux-task-name.sh +105 -0
  32. package/scripts/tmux-task-watcher.sh +344 -0
  33. package/src/core/agents/legal-analyzer-a11y.md +110 -0
  34. package/src/core/agents/legal-analyzer-ai.md +117 -0
  35. package/src/core/agents/legal-analyzer-consumer.md +108 -0
  36. package/src/core/agents/legal-analyzer-content.md +113 -0
  37. package/src/core/agents/legal-analyzer-international.md +115 -0
  38. package/src/core/agents/legal-analyzer-licensing.md +115 -0
  39. package/src/core/agents/legal-analyzer-privacy.md +108 -0
  40. package/src/core/agents/legal-analyzer-security.md +112 -0
  41. package/src/core/agents/legal-analyzer-terms.md +111 -0
  42. package/src/core/agents/legal-consensus.md +242 -0
  43. package/src/core/agents/team-lead.md +50 -13
  44. package/src/core/commands/babysit.md +75 -42
  45. package/src/core/commands/blockers.md +7 -7
  46. package/src/core/commands/configure.md +15 -61
  47. package/src/core/commands/discovery/brief.md +363 -0
  48. package/src/core/commands/discovery/new.md +395 -0
  49. package/src/core/commands/ideate/new.md +5 -5
  50. package/src/core/commands/legal/audit.md +446 -0
  51. package/src/core/commands/logic/audit.md +5 -5
  52. package/src/core/commands/review.md +7 -1
  53. package/src/core/commands/rpi.md +61 -26
  54. package/src/core/commands/sprint.md +7 -6
  55. package/src/core/commands/team/start.md +36 -7
  56. package/src/core/commands/team/stop.md +5 -2
  57. package/src/core/templates/product-brief.md +136 -0
  58. package/tools/cli/installers/ide/claude-code.js +69 -2
  59. package/src/core/agents/configuration/archival.md +0 -350
  60. package/src/core/agents/configuration/attribution.md +0 -343
  61. package/src/core/agents/configuration/ci.md +0 -1103
  62. package/src/core/agents/configuration/damage-control.md +0 -375
  63. package/src/core/agents/configuration/git-config.md +0 -537
  64. package/src/core/agents/configuration/hooks.md +0 -623
  65. package/src/core/agents/configuration/precompact.md +0 -302
  66. package/src/core/agents/configuration/status-line.md +0 -557
  67. package/src/core/agents/configuration/verify.md +0 -618
  68. package/src/core/agents/configuration-damage-control.md +0 -259
  69. package/src/core/agents/configuration-visual-e2e.md +0 -339
@@ -30,6 +30,20 @@ const { readJSONCached, readFileCached } = require('../lib/file-cache');
30
30
  // Session manager path (relative to script location)
31
31
  const SESSION_MANAGER_PATH = path.join(__dirname, 'session-manager.js');
32
32
 
33
+ // PERFORMANCE OPTIMIZATION: Lazy-loaded session-manager module
34
+ // Importing directly avoids ~50-150ms subprocess overhead per call.
35
+ let _sessionManager;
36
+ function getSessionManager() {
37
+ if (_sessionManager === undefined) {
38
+ try {
39
+ _sessionManager = require('./session-manager.js');
40
+ } catch (e) {
41
+ _sessionManager = null;
42
+ }
43
+ }
44
+ return _sessionManager;
45
+ }
46
+
33
47
  // Hook metrics module (kept at top level - needed early for timer)
34
48
  let hookMetrics;
35
49
  try {
@@ -43,6 +57,12 @@ try {
43
57
  * Uses file-cache module for automatic caching with 15s TTL.
44
58
  * Files are cached across script invocations within TTL window.
45
59
  * Estimated savings: 60-120ms on cache hits
60
+ *
61
+ * Additional optimizations in this file (US-0356):
62
+ * - Git batching: 3 subprocess calls → 1 (~20-40ms savings)
63
+ * - Session-manager inline: subprocess → direct require() (~50-150ms savings)
64
+ * - Tmux cache: subprocess → session-state lookup (~10-20ms after first run)
65
+ * Total estimated savings: ~130-260ms
46
66
  */
47
67
  function loadProjectFiles(rootDir) {
48
68
  const paths = {
@@ -109,32 +129,57 @@ function detectPlatform() {
109
129
 
110
130
  /**
111
131
  * Check if tmux is installed
132
+ * PERFORMANCE OPTIMIZATION: Caches result in session-state.json (~10-20ms savings on subsequent runs)
112
133
  * Returns object with availability info and platform-specific install suggestion
113
134
  */
114
- function checkTmuxAvailability() {
135
+ function checkTmuxAvailability(cache) {
136
+ // Check session state cache first (tmux availability doesn't change within a session)
137
+ if (cache?.sessionState?.tmux_available !== undefined) {
138
+ if (cache.sessionState.tmux_available) return { available: true };
139
+ return { available: false, platform: detectPlatform(), noSudoCmd: 'conda install -c conda-forge tmux' };
140
+ }
141
+
142
+ // Actually check (first run or no cache)
115
143
  const result = executeCommandSync('which', ['tmux'], { fallback: null });
116
- if (result.data !== null) {
117
- return { available: true };
144
+ const available = result.data !== null;
145
+
146
+ // Cache in session state for next invocation
147
+ try {
148
+ const rootDir = getProjectRoot();
149
+ const sessionStatePath = getSessionStatePath(rootDir);
150
+ if (fs.existsSync(sessionStatePath)) {
151
+ const state = JSON.parse(fs.readFileSync(sessionStatePath, 'utf8'));
152
+ state.tmux_available = available;
153
+ fs.writeFileSync(sessionStatePath, JSON.stringify(state, null, 2) + '\n');
154
+ }
155
+ } catch (e) {
156
+ // Cache write failed, non-critical
118
157
  }
119
- const platform = detectPlatform();
120
- return {
121
- available: false,
122
- platform,
123
- noSudoCmd: 'conda install -c conda-forge tmux',
124
- };
158
+
159
+ if (available) return { available: true };
160
+ return { available: false, platform: detectPlatform(), noSudoCmd: 'conda install -c conda-forge tmux' };
125
161
  }
126
162
 
127
163
  /**
128
164
  * PERFORMANCE OPTIMIZATION: Batch git commands into single call
129
- * Reduces subprocess overhead from 3 calls to 1.
130
- * Estimated savings: 20-40ms
165
+ * Uses `git log -1 --format=%D%n%h%n%s` to get branch, short hash, and subject
166
+ * in a single subprocess instead of 3 separate calls.
167
+ * Savings: ~20-40ms (eliminates 2 subprocess spawns)
131
168
  */
132
169
  function getGitInfo(rootDir) {
133
- const opts = { cwd: rootDir, timeout: 5000, fallback: 'unknown' };
134
- const branch = git(['branch', '--show-current'], opts).data;
135
- const commit = git(['rev-parse', '--short', 'HEAD'], opts).data;
136
- const lastCommit = git(['log', '-1', '--format=%s'], { ...opts, fallback: '' }).data;
137
- return { branch, commit, lastCommit };
170
+ const opts = { cwd: rootDir, timeout: 5000, fallback: '' };
171
+ const result = git(['log', '-1', '--format=%D%n%h%n%s'], opts);
172
+ if (result.data) {
173
+ const lines = result.data.split('\n');
174
+ // %D gives decorations like "HEAD -> main, origin/main, tag: v3.0.0"
175
+ const branchMatch = (lines[0] || '').match(/HEAD -> ([^,\s]+)/);
176
+ return {
177
+ branch: branchMatch ? branchMatch[1] : 'detached',
178
+ commit: (lines[1] || 'unknown').trim(),
179
+ lastCommit: lines.slice(2).join('\n').trim(),
180
+ };
181
+ }
182
+ return { branch: 'unknown', commit: 'unknown', lastCommit: '' };
138
183
  }
139
184
 
140
185
  function getProjectInfo(rootDir, cache = null) {
@@ -403,20 +448,39 @@ function checkParallelSessions(rootDir) {
403
448
  };
404
449
 
405
450
  try {
406
- // Check if session manager exists
451
+ // PERFORMANCE OPTIMIZATION: Import session-manager directly instead of subprocess
452
+ // Saves ~50-150ms by avoiding Node subprocess spawn overhead
453
+ const sm = getSessionManager();
454
+ if (sm && sm.fullStatus) {
455
+ result.available = true;
456
+ const data = sm.fullStatus();
457
+ result.registered = data.registered;
458
+ result.currentId = data.id;
459
+ result.otherActive = data.otherActive || 0;
460
+ result.cleaned = data.cleaned || 0;
461
+ result.cleanedSessions = data.cleanedSessions || [];
462
+
463
+ if (data.current) {
464
+ result.isMain = data.current.is_main === true;
465
+ result.nickname = data.current.nickname;
466
+ result.branch = data.current.branch;
467
+ result.sessionPath = data.current.path;
468
+ }
469
+ return result;
470
+ }
471
+
472
+ // Fallback: check if session manager script exists for subprocess call
407
473
  const managerPath = path.join(getAgileflowDir(rootDir), 'scripts', 'session-manager.js');
408
474
  if (!fs.existsSync(managerPath) && !fs.existsSync(SESSION_MANAGER_PATH)) {
409
475
  return result;
410
476
  }
411
477
 
412
478
  result.available = true;
413
-
414
- // Try to use combined full-status command (saves ~200ms vs 3 separate calls)
415
479
  const scriptPath = fs.existsSync(managerPath) ? managerPath : SESSION_MANAGER_PATH;
416
480
 
417
- // PERFORMANCE: Single subprocess call instead of 3 (register + count + status)
418
481
  const fullStatusResult = executeCommandSync('node', [scriptPath, 'full-status'], {
419
- cwd: rootDir, fallback: null,
482
+ cwd: rootDir,
483
+ fallback: null,
420
484
  });
421
485
 
422
486
  if (fullStatusResult.data) {
@@ -434,49 +498,7 @@ function checkParallelSessions(rootDir) {
434
498
  result.branch = data.current.branch;
435
499
  result.sessionPath = data.current.path;
436
500
  }
437
- } catch (e) {
438
- // JSON parse failed, fall through to individual calls
439
- fullStatusResult.data = null;
440
- }
441
- }
442
-
443
- if (!fullStatusResult.data) {
444
- // Fall back to individual calls if full-status not available (older version)
445
- const registerResult = executeCommandSync('node', [scriptPath, 'register'], {
446
- cwd: rootDir, fallback: null,
447
- });
448
- if (registerResult.data) {
449
- try {
450
- const registerData = JSON.parse(registerResult.data);
451
- result.registered = true;
452
- result.currentId = registerData.id;
453
- } catch (e) {}
454
- }
455
-
456
- const countResult = executeCommandSync('node', [scriptPath, 'count'], {
457
- cwd: rootDir, fallback: null,
458
- });
459
- if (countResult.data) {
460
- try {
461
- const countData = JSON.parse(countResult.data);
462
- result.otherActive = countData.count || 0;
463
- } catch (e) {}
464
- }
465
-
466
- const statusCmdResult = executeCommandSync('node', [scriptPath, 'status'], {
467
- cwd: rootDir, fallback: null,
468
- });
469
- if (statusCmdResult.data) {
470
- try {
471
- const statusData = JSON.parse(statusCmdResult.data);
472
- if (statusData.current) {
473
- result.isMain = statusData.current.is_main === true;
474
- result.nickname = statusData.current.nickname;
475
- result.branch = statusData.current.branch;
476
- result.sessionPath = statusData.current.path;
477
- }
478
- } catch (e) {}
479
- }
501
+ } catch (e) {}
480
502
  }
481
503
  } catch (e) {
482
504
  // Session system not available
@@ -930,7 +952,9 @@ async function checkUpdates() {
930
952
  };
931
953
 
932
954
  let updateChecker;
933
- try { updateChecker = require('./check-update.js'); } catch (e) {}
955
+ try {
956
+ updateChecker = require('./check-update.js');
957
+ } catch (e) {}
934
958
  if (!updateChecker) return result;
935
959
 
936
960
  try {
@@ -1000,7 +1024,8 @@ function getChangelogEntries(version) {
1000
1024
  async function runAutoUpdate(rootDir, fromVersion, toVersion) {
1001
1025
  const runUpdate = () => {
1002
1026
  return executeCommandSync('npx', ['agileflow@latest', 'update', '--force'], {
1003
- cwd: rootDir, timeout: 120000,
1027
+ cwd: rootDir,
1028
+ timeout: 120000,
1004
1029
  });
1005
1030
  };
1006
1031
 
@@ -1528,17 +1553,26 @@ function formatTable(
1528
1553
  // Scale detection (EP-0033)
1529
1554
  if (scaleDetection && scaleDetection.scale) {
1530
1555
  const scaleColors = {
1531
- micro: c.cyan, small: c.teal, medium: c.mintGreen,
1532
- large: c.peach, enterprise: c.coral,
1556
+ micro: c.cyan,
1557
+ small: c.teal,
1558
+ medium: c.mintGreen,
1559
+ large: c.peach,
1560
+ enterprise: c.coral,
1533
1561
  };
1534
1562
  const scaleIcons = {
1535
- micro: '◦', small: '○', medium: '◎', large: '●', enterprise: '◉',
1563
+ micro: '◦',
1564
+ small: '○',
1565
+ medium: '◎',
1566
+ large: '●',
1567
+ enterprise: '◉',
1536
1568
  };
1537
1569
  const scale = scaleDetection.scale;
1538
1570
  const icon = scaleIcons[scale] || '◎';
1539
1571
  const label = scale.charAt(0).toUpperCase() + scale.slice(1);
1540
1572
  const cacheNote = scaleDetection.fromCache ? '' : ` (${scaleDetection.detection_ms}ms)`;
1541
- lines.push(row('Scale', `${icon} ${label}${cacheNote}`, c.lavender, scaleColors[scale] || c.dim));
1573
+ lines.push(
1574
+ row('Scale', `${icon} ${label}${cacheNote}`, c.lavender, scaleColors[scale] || c.dim)
1575
+ );
1542
1576
  }
1543
1577
 
1544
1578
  lines.push(divider());
@@ -1634,13 +1668,20 @@ async function main() {
1634
1668
  // Scale detection not available
1635
1669
  }
1636
1670
 
1637
- const scaleRecommendations = earlyScale ? (() => {
1638
- try { return require('./lib/scale-detector').getScaleRecommendations(earlyScale.scale); } catch { return null; }
1639
- })() : null;
1671
+ const scaleRecommendations = earlyScale
1672
+ ? (() => {
1673
+ try {
1674
+ return require('./lib/scale-detector').getScaleRecommendations(earlyScale.scale);
1675
+ } catch {
1676
+ return null;
1677
+ }
1678
+ })()
1679
+ : null;
1640
1680
 
1641
- const archival = (scaleRecommendations && scaleRecommendations.skipArchival)
1642
- ? { ran: false, threshold: 0, archived: 0, remaining: 0, skippedByScale: true }
1643
- : runArchival(rootDir, cache);
1681
+ const archival =
1682
+ scaleRecommendations && scaleRecommendations.skipArchival
1683
+ ? { ran: false, threshold: 0, archived: 0, remaining: 0, skippedByScale: true }
1684
+ : runArchival(rootDir, cache);
1644
1685
  const session = clearActiveCommands(rootDir, cache);
1645
1686
  const precompact = checkPreCompact(rootDir, cache);
1646
1687
  const parallelSessions = checkParallelSessions(rootDir);
@@ -1654,7 +1695,9 @@ async function main() {
1654
1695
 
1655
1696
  // Agent Teams feature flag detection
1656
1697
  let featureFlags;
1657
- try { featureFlags = require('../lib/feature-flags'); } catch (e) {}
1698
+ try {
1699
+ featureFlags = require('../lib/feature-flags');
1700
+ } catch (e) {}
1658
1701
  let agentTeamsInfo = {};
1659
1702
  if (featureFlags) {
1660
1703
  try {
@@ -1727,7 +1770,7 @@ async function main() {
1727
1770
  let tmuxCheck = { available: true };
1728
1771
  const tmuxAutoSpawnEnabled = cache?.metadata?.features?.tmuxAutoSpawn?.enabled !== false;
1729
1772
  if (tmuxAutoSpawnEnabled) {
1730
- tmuxCheck = checkTmuxAvailability();
1773
+ tmuxCheck = checkTmuxAvailability(cache);
1731
1774
  }
1732
1775
 
1733
1776
  // Show session banner FIRST if in a non-main session
@@ -1776,14 +1819,18 @@ async function main() {
1776
1819
 
1777
1820
  // Mark current version as seen to track for next update
1778
1821
  let updateChecker;
1779
- try { updateChecker = require('./check-update.js'); } catch (e) {}
1822
+ try {
1823
+ updateChecker = require('./check-update.js');
1824
+ } catch (e) {}
1780
1825
  if (freshUpdateInfo.justUpdated && updateChecker) {
1781
1826
  updateChecker.markVersionSeen(info.version);
1782
1827
  }
1783
1828
  } else {
1784
1829
  // Mark current version as seen (for "just updated" case)
1785
1830
  let updateChecker;
1786
- try { updateChecker = require('./check-update.js'); } catch (e) {}
1831
+ try {
1832
+ updateChecker = require('./check-update.js');
1833
+ } catch (e) {}
1787
1834
  if (updateChecker) {
1788
1835
  updateChecker.markVersionSeen(info.version);
1789
1836
  }
@@ -1868,13 +1915,12 @@ async function main() {
1868
1915
 
1869
1916
  // === SESSION HEALTH WARNINGS ===
1870
1917
  // Check for forgotten sessions with uncommitted changes, stale sessions, orphaned entries
1918
+ // PERFORMANCE OPTIMIZATION: Direct function call instead of subprocess (~50-100ms savings)
1871
1919
  try {
1872
- const healthResult = executeCommandSync('node', [SESSION_MANAGER_PATH, 'health'], {
1873
- timeout: 10000, fallback: null,
1874
- });
1920
+ const sm = getSessionManager();
1921
+ const health = sm ? sm.getSessionsHealth({ staleDays: 7 }) : null;
1875
1922
 
1876
- if (healthResult.data) {
1877
- const health = JSON.parse(healthResult.data);
1923
+ if (health) {
1878
1924
  const hasIssues =
1879
1925
  health.uncommitted.length > 0 ||
1880
1926
  health.stale.length > 0 ||
@@ -1922,7 +1968,9 @@ async function main() {
1922
1968
  // === DUPLICATE CLAUDE PROCESS DETECTION ===
1923
1969
  // Check for multiple Claude processes in the same working directory
1924
1970
  let processCleanup;
1925
- try { processCleanup = require('./lib/process-cleanup.js'); } catch (e) {}
1971
+ try {
1972
+ processCleanup = require('./lib/process-cleanup.js');
1973
+ } catch (e) {}
1926
1974
  if (processCleanup) {
1927
1975
  try {
1928
1976
  // Auto-kill is explicitly opt-in at runtime.
@@ -1976,7 +2024,9 @@ async function main() {
1976
2024
 
1977
2025
  // Story claiming: cleanup stale claims and show warnings
1978
2026
  let storyClaiming;
1979
- try { storyClaiming = require('./lib/story-claiming.js'); } catch (e) {}
2027
+ try {
2028
+ storyClaiming = require('./lib/story-claiming.js');
2029
+ } catch (e) {}
1980
2030
  if (storyClaiming) {
1981
2031
  try {
1982
2032
  // Clean up stale claims (dead PIDs, expired TTL)
@@ -2003,7 +2053,9 @@ async function main() {
2003
2053
 
2004
2054
  // File tracking: cleanup stale touches and show overlap warnings
2005
2055
  let fileTracking;
2006
- try { fileTracking = require('./lib/file-tracking.js'); } catch (e) {}
2056
+ try {
2057
+ fileTracking = require('./lib/file-tracking.js');
2058
+ } catch (e) {}
2007
2059
  if (fileTracking) {
2008
2060
  try {
2009
2061
  // Clean up stale file touches (dead PIDs, expired TTL)
@@ -2028,7 +2080,9 @@ async function main() {
2028
2080
 
2029
2081
  // Epic completion check: auto-complete epics where all stories are done
2030
2082
  let storyStateMachine;
2031
- try { storyStateMachine = require('./lib/story-state-machine.js'); } catch (e) {}
2083
+ try {
2084
+ storyStateMachine = require('./lib/story-state-machine.js');
2085
+ } catch (e) {}
2032
2086
  if (storyStateMachine && cache.status) {
2033
2087
  try {
2034
2088
  const statusPath = getStatusPath(rootDir);
@@ -2058,7 +2112,9 @@ async function main() {
2058
2112
 
2059
2113
  // Ideation sync: mark ideas as implemented when linked epics complete
2060
2114
  let syncIdeationStatus;
2061
- try { syncIdeationStatus = require('./lib/sync-ideation-status.js'); } catch (e) {}
2115
+ try {
2116
+ syncIdeationStatus = require('./lib/sync-ideation-status.js');
2117
+ } catch (e) {}
2062
2118
  if (syncIdeationStatus) {
2063
2119
  try {
2064
2120
  const syncResult = syncIdeationStatus.syncImplementedIdeas(rootDir);
@@ -165,6 +165,9 @@ configure_tmux_session() {
165
165
  # Enable mouse support
166
166
  tmux set-option -t "$target_session" mouse on
167
167
 
168
+ # Automatically renumber windows when one is closed (no gaps)
169
+ tmux set-option -t "$target_session" renumber-windows on
170
+
168
171
  # Fix colors - proper terminal support
169
172
  tmux set-option -t "$target_session" default-terminal "xterm-256color"
170
173
  tmux set-option -t "$target_session" -ga terminal-overrides ",xterm-256color:Tc"
@@ -245,7 +248,8 @@ configure_tmux_session() {
245
248
 
246
249
  # ─── Session Creation Keybindings ──────────────────────────────────────────
247
250
  # Alt+s to create a new Claude window (starts fresh, future re-runs in same pane resume)
248
- tmux bind-key -n M-s run-shell "tmux new-window -n claude -c '#{pane_current_path}' && tmux send-keys '\"\$AGILEFLOW_SCRIPTS/claude-smart.sh\" --fresh \$CLAUDE_SESSION_FLAGS' Enter"
251
+ # Window gets sequential name (claude-2, claude-3, ...) so windows are distinguishable
252
+ tmux bind-key -n M-s run-shell "N=\$(( \$(tmux list-windows -F '#{window_name}' 2>/dev/null | grep -c '^claude') + 1 )); tmux new-window -n \"claude-\$N\" -c '#{pane_current_path}' && tmux send-keys '\"\$AGILEFLOW_SCRIPTS/claude-smart.sh\" --fresh \$CLAUDE_SESSION_FLAGS' Enter"
249
253
 
250
254
  # ─── Freeze Recovery Keybindings ───────────────────────────────────────────
251
255
  # Alt+k to send Ctrl+C twice (soft interrupt for frozen processes)
@@ -253,7 +257,7 @@ configure_tmux_session() {
253
257
 
254
258
  # ─── Help Panel ──────────────────────────────────────────────────────────
255
259
  # Alt+h to show all Alt keybindings in a popup
256
- tmux bind-key -n M-h display-popup -E -w 52 -h 26 "\
260
+ tmux bind-key -n M-h display-popup -E -w 52 -h 30 "\
257
261
  printf '\\n';\
258
262
  printf ' \\033[1;38;5;208mSESSIONS\\033[0m\\n';\
259
263
  printf ' Alt+s New Claude session\\n';\
@@ -272,7 +276,9 @@ configure_tmux_session() {
272
276
  printf ' Alt+v Split top / bottom\\n';\
273
277
  printf ' Alt+arrows Navigate panes\\n';\
274
278
  printf ' Alt+z Zoom / unzoom\\n';\
275
- printf ' Alt+x Close pane\\n';\
279
+ printf ' Alt+x Close pane (confirm)\\n';\
280
+ printf ' Alt+K Kill pane (no confirm)\\n';\
281
+ printf ' Alt+R Restart pane\\n';\
276
282
  printf '\\n';\
277
283
  printf ' \\033[1;38;5;208mOTHER\\033[0m\\n';\
278
284
  printf ' Alt+[ Scroll mode\\n';\
@@ -354,7 +360,7 @@ fi
354
360
  # Silently remove sessions where all panes have exited (dead/empty shells).
355
361
  # This prevents accumulation of orphan sessions over time.
356
362
  SESSION_BASE="claude-${DIR_NAME}"
357
- for sid in $(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep "^${SESSION_BASE}\(\$\|-[0-9]*\$\)"); do
363
+ for sid in $(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep -E "^${SESSION_BASE}($|-[0-9]+$)"); do
358
364
  # Count alive panes (pane_dead=0 means alive)
359
365
  ALIVE=$(tmux list-panes -t "$sid" -F '#{pane_dead}' 2>/dev/null | grep -c '^0$' || true)
360
366
  if [ "$ALIVE" = "0" ]; then
@@ -362,6 +368,36 @@ for sid in $(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep "^${SESS
362
368
  fi
363
369
  done
364
370
 
371
+ # ── Consolidate duplicate sessions ───────────────────────────────────────
372
+ # Kill numbered duplicates (e.g. claude-Acuide-2, -3) that were created by
373
+ # a previous bug. If the base session exists, duplicates are unnecessary.
374
+ # If only numbered sessions remain, promote the lowest to the base name.
375
+ if [ "$FORCE_NEW" = false ]; then
376
+ HAS_BASE=false
377
+ NUMBERED=()
378
+ if tmux has-session -t "$SESSION_BASE" 2>/dev/null; then
379
+ HAS_BASE=true
380
+ fi
381
+ while IFS= read -r sid; do
382
+ [ -n "$sid" ] && NUMBERED+=("$sid")
383
+ done < <(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep -E "^${SESSION_BASE}-[0-9]+$" | sort -t- -k3 -n)
384
+
385
+ if [ "$HAS_BASE" = true ] && [ "${#NUMBERED[@]}" -gt 0 ]; then
386
+ # Base exists — kill all numbered duplicates
387
+ for sid in "${NUMBERED[@]}"; do
388
+ tmux kill-session -t "$sid" 2>/dev/null || true
389
+ done
390
+ elif [ "$HAS_BASE" = false ] && [ "${#NUMBERED[@]}" -gt 0 ]; then
391
+ # No base — promote lowest numbered session to base name
392
+ PROMOTE="${NUMBERED[0]}"
393
+ tmux rename-session -t "$PROMOTE" "$SESSION_BASE" 2>/dev/null || true
394
+ # Kill remaining duplicates
395
+ for sid in "${NUMBERED[@]:1}"; do
396
+ tmux kill-session -t "$sid" 2>/dev/null || true
397
+ done
398
+ fi
399
+ fi
400
+
365
401
  # ── Auto-reattach to detached session ──────────────────────────────────────
366
402
  # When user does Alt+Q (detach) and then runs `af` again, reattach to the
367
403
  # existing session instead of creating a new one. This preserves tmux windows,
@@ -371,7 +407,7 @@ if [ "$FORCE_NEW" = false ]; then
371
407
  DETACHED=()
372
408
  while IFS= read -r sid; do
373
409
  [ -n "$sid" ] && DETACHED+=("$sid")
374
- done < <(tmux list-sessions -F '#{session_name} #{session_attached}' 2>/dev/null | awk '$2 == "0" {print $1}' | grep "^${SESSION_BASE}\(\$\|-[0-9]*\$\)")
410
+ done < <(tmux list-sessions -F '#{session_name} #{session_attached}' 2>/dev/null | awk '$2 == "0" {print $1}' | grep -E "^${SESSION_BASE}($|-[0-9]+$)")
375
411
 
376
412
  if [ "${#DETACHED[@]}" -eq 1 ]; then
377
413
  # Single detached session — just reattach
@@ -410,7 +446,7 @@ if [ "$FORCE_NEW" = false ]; then
410
446
  EXISTING=()
411
447
  while IFS= read -r sid; do
412
448
  [ -n "$sid" ] && EXISTING+=("$sid")
413
- done < <(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep "^${SESSION_BASE}\(\$\|-[0-9]*\$\)")
449
+ done < <(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep -E "^${SESSION_BASE}($|-[0-9]+$)")
414
450
 
415
451
  if [ "${#EXISTING[@]}" -gt 0 ]; then
416
452
  # Prefer the base session, otherwise pick the first one
@@ -48,8 +48,12 @@ if (!utils || typeof utils.runDamageControlHook !== 'function') {
48
48
 
49
49
  // Tools this hook handles
50
50
  const MULTI_AGENT_TOOLS = [
51
- 'TeamCreate', 'TeamDelete',
52
- 'TaskCreate', 'TaskUpdate', 'TaskGet', 'TaskList',
51
+ 'TeamCreate',
52
+ 'TeamDelete',
53
+ 'TaskCreate',
54
+ 'TaskUpdate',
55
+ 'TaskGet',
56
+ 'TaskList',
53
57
  'SendMessage',
54
58
  ];
55
59
 
@@ -65,10 +69,10 @@ const MAX_MESSAGE_SIZE = 10240;
65
69
  // Blocked patterns in SendMessage content
66
70
  const BLOCKED_MESSAGE_PATTERNS = [
67
71
  // Command injection attempts
68
- /\$\{.*\}/, // Template injection ${...}
69
- /`[^`]*`/, // Backtick execution
70
- /\bexec\s*\(/, // exec() calls
71
- /\beval\s*\(/, // eval() calls
72
+ /\$\{.*\}/, // Template injection ${...}
73
+ /`[^`]*`/, // Backtick execution
74
+ /\bexec\s*\(/, // exec() calls
75
+ /\beval\s*\(/, // eval() calls
72
76
  // Dangerous git operations
73
77
  /\bgit\s+push\s+--force\b/i,
74
78
  /\bgit\s+reset\s+--hard\b/i,
@@ -143,9 +147,9 @@ function validateTaskOperation(input) {
143
147
  // Check for secrets in task descriptions
144
148
  const secretPatterns = [
145
149
  /\b(?:API_KEY|SECRET|PASSWORD|TOKEN|CREDENTIALS)\s*[:=]\s*\S+/i,
146
- /\bsk-[a-zA-Z0-9]{20,}/, // API keys starting with sk-
147
- /\bghp_[a-zA-Z0-9]{36}/, // GitHub personal access tokens
148
- /\bnpm_[a-zA-Z0-9]{36}/, // npm tokens
150
+ /\bsk-[a-zA-Z0-9]{20,}/, // API keys starting with sk-
151
+ /\bghp_[a-zA-Z0-9]{36}/, // GitHub personal access tokens
152
+ /\bnpm_[a-zA-Z0-9]{36}/, // npm tokens
149
153
  ];
150
154
 
151
155
  for (const pattern of secretPatterns) {
@@ -163,7 +167,7 @@ function validateTaskOperation(input) {
163
167
 
164
168
  try {
165
169
  utils.runDamageControlHook({
166
- getInputValue: (input) => {
170
+ getInputValue: input => {
167
171
  // Check if this is a multi-agent tool
168
172
  const toolName = input.tool_name || '';
169
173
  if (!MULTI_AGENT_TOOLS.includes(toolName)) {
@@ -341,7 +341,9 @@ function formatLogStats(stats) {
341
341
  lines.push('Archives:');
342
342
  if (stats.archives && stats.archives.length > 0) {
343
343
  for (const archive of stats.archives) {
344
- lines.push(` ${archive.filename}: ${archive.lineCount} lines (${Math.round(archive.size / 1024)} KB)`);
344
+ lines.push(
345
+ ` ${archive.filename}: ${archive.lineCount} lines (${Math.round(archive.size / 1024)} KB)`
346
+ );
345
347
  }
346
348
  }
347
349
 
@@ -107,10 +107,15 @@ function detectConfig(version) {
107
107
  // Git detection
108
108
  if (fs.existsSync('.git')) {
109
109
  status.git.initialized = true;
110
- status.git.remote = tryOptional(() => execFileSync('git', ['remote', 'get-url', 'origin'], {
111
- encoding: 'utf8',
112
- stdio: ['pipe', 'pipe', 'pipe'],
113
- }).trim(), 'git remote') ?? null;
110
+ status.git.remote =
111
+ tryOptional(
112
+ () =>
113
+ execFileSync('git', ['remote', 'get-url', 'origin'], {
114
+ encoding: 'utf8',
115
+ stdio: ['pipe', 'pipe', 'pipe'],
116
+ }).trim(),
117
+ 'git remote'
118
+ ) ?? null;
114
119
  }
115
120
 
116
121
  // Settings file detection
@@ -312,17 +317,15 @@ function detectMetadata(status, version) {
312
317
 
313
318
  // Content-based outdated detection
314
319
  const featureConfig = FEATURES[statusKey];
315
- const scriptsToCheck = featureConfig?.scripts
316
- || (featureConfig?.script ? [featureConfig.script] : []);
320
+ const scriptsToCheck =
321
+ featureConfig?.scripts || (featureConfig?.script ? [featureConfig.script] : []);
317
322
 
318
323
  if (scriptsToCheck.length > 0 && packageScriptDir) {
319
324
  // Compare installed scripts against package source
320
325
  let isOutdated = false;
321
326
  for (const scriptName of scriptsToCheck) {
322
327
  const packageScript = path.join(packageScriptDir, scriptName);
323
- const installedScript = path.join(
324
- process.cwd(), '.agileflow', 'scripts', scriptName
325
- );
328
+ const installedScript = path.join(process.cwd(), '.agileflow', 'scripts', scriptName);
326
329
  const packageHash = hashFile(packageScript);
327
330
  const installedHash = hashFile(installedScript);
328
331