agileflow 2.99.1 → 2.99.3

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 (117) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/lib/dashboard-server.js +16 -8
  3. package/lib/feedback.js +4 -3
  4. package/lib/merge-operations.js +17 -8
  5. package/lib/session-operations.js +13 -3
  6. package/lib/session-switching.js +6 -1
  7. package/package.json +1 -1
  8. package/scripts/agileflow-configure.js +2 -1
  9. package/scripts/agileflow-welcome.js +11 -6
  10. package/scripts/claude-tmux.sh +21 -0
  11. package/scripts/damage-control-bash.js +33 -3
  12. package/scripts/damage-control-edit.js +33 -3
  13. package/scripts/damage-control-write.js +33 -3
  14. package/scripts/lib/configure-features.js +4 -3
  15. package/scripts/lib/configure-repair.js +4 -3
  16. package/scripts/lib/process-cleanup.js +188 -10
  17. package/scripts/session-manager.js +108 -35
  18. package/src/core/agents/configuration/archival.md +2 -1
  19. package/src/core/agents/configuration/attribution.md +2 -1
  20. package/src/core/agents/configuration/ci.md +2 -1
  21. package/src/core/agents/configuration/damage-control.md +2 -1
  22. package/src/core/agents/configuration/git-config.md +2 -1
  23. package/src/core/agents/configuration/hooks.md +2 -1
  24. package/src/core/agents/configuration/precompact.md +2 -1
  25. package/src/core/agents/configuration/status-line.md +2 -1
  26. package/src/core/agents/configuration/verify.md +2 -1
  27. package/src/core/commands/adr/list.md +1 -1
  28. package/src/core/commands/adr/update.md +1 -1
  29. package/src/core/commands/adr/view.md +1 -1
  30. package/src/core/commands/adr.md +1 -1
  31. package/src/core/commands/agent.md +1 -1
  32. package/src/core/commands/api.md +1 -1
  33. package/src/core/commands/assign.md +1 -1
  34. package/src/core/commands/audit.md +1 -1
  35. package/src/core/commands/auto.md +1 -1
  36. package/src/core/commands/automate.md +1 -1
  37. package/src/core/commands/babysit.md +1 -1
  38. package/src/core/commands/baseline.md +1 -1
  39. package/src/core/commands/batch.md +1 -1
  40. package/src/core/commands/blockers.md +1 -1
  41. package/src/core/commands/board.md +1 -1
  42. package/src/core/commands/changelog.md +1 -1
  43. package/src/core/commands/choose.md +1 -1
  44. package/src/core/commands/ci.md +1 -1
  45. package/src/core/commands/compress.md +1 -1
  46. package/src/core/commands/configure.md +1 -1
  47. package/src/core/commands/context/export.md +1 -1
  48. package/src/core/commands/context/full.md +1 -1
  49. package/src/core/commands/context/note.md +1 -1
  50. package/src/core/commands/council.md +1 -1
  51. package/src/core/commands/debt.md +1 -1
  52. package/src/core/commands/deploy.md +1 -1
  53. package/src/core/commands/deps.md +1 -1
  54. package/src/core/commands/diagnose.md +1 -1
  55. package/src/core/commands/docs.md +1 -1
  56. package/src/core/commands/epic/list.md +1 -1
  57. package/src/core/commands/epic/view.md +1 -1
  58. package/src/core/commands/epic.md +1 -1
  59. package/src/core/commands/feedback.md +1 -1
  60. package/src/core/commands/handoff.md +1 -1
  61. package/src/core/commands/help.md +4 -190
  62. package/src/core/commands/ideate/history.md +1 -1
  63. package/src/core/commands/ideate/new.md +1 -1
  64. package/src/core/commands/impact.md +1 -1
  65. package/src/core/commands/install.md +1 -1
  66. package/src/core/commands/logic/audit.md +1 -1
  67. package/src/core/commands/maintain.md +1 -1
  68. package/src/core/commands/metrics.md +1 -1
  69. package/src/core/commands/multi-expert.md +1 -1
  70. package/src/core/commands/packages.md +1 -1
  71. package/src/core/commands/pr.md +1 -1
  72. package/src/core/commands/readme-sync.md +1 -1
  73. package/src/core/commands/research/analyze.md +1 -1
  74. package/src/core/commands/research/ask.md +1 -1
  75. package/src/core/commands/research/import.md +1 -1
  76. package/src/core/commands/research/list.md +1 -1
  77. package/src/core/commands/research/synthesize.md +1 -1
  78. package/src/core/commands/research/view.md +1 -1
  79. package/src/core/commands/retro.md +1 -1
  80. package/src/core/commands/review.md +1 -1
  81. package/src/core/commands/rlm.md +1 -1
  82. package/src/core/commands/roadmap/analyze.md +1 -1
  83. package/src/core/commands/rpi.md +1 -1
  84. package/src/core/commands/session/cleanup.md +1 -1
  85. package/src/core/commands/session/end.md +1 -1
  86. package/src/core/commands/session/history.md +1 -1
  87. package/src/core/commands/session/init.md +1 -1
  88. package/src/core/commands/session/new.md +1 -1
  89. package/src/core/commands/session/resume.md +1 -1
  90. package/src/core/commands/session/spawn.md +1 -1
  91. package/src/core/commands/session/status.md +1 -1
  92. package/src/core/commands/skill/create.md +1 -1
  93. package/src/core/commands/skill/delete.md +1 -1
  94. package/src/core/commands/skill/edit.md +1 -1
  95. package/src/core/commands/skill/list.md +1 -1
  96. package/src/core/commands/skill/test.md +1 -1
  97. package/src/core/commands/skill/upgrade.md +1 -1
  98. package/src/core/commands/sprint.md +1 -1
  99. package/src/core/commands/status.md +1 -1
  100. package/src/core/commands/story/list.md +1 -1
  101. package/src/core/commands/story/view.md +1 -1
  102. package/src/core/commands/story-validate.md +1 -1
  103. package/src/core/commands/story.md +1 -1
  104. package/src/core/commands/team/list.md +1 -1
  105. package/src/core/commands/team/start.md +1 -1
  106. package/src/core/commands/team/status.md +1 -1
  107. package/src/core/commands/team/stop.md +1 -1
  108. package/src/core/commands/template.md +1 -1
  109. package/src/core/commands/tests.md +1 -1
  110. package/src/core/commands/update.md +1 -1
  111. package/src/core/commands/validate-expertise.md +1 -1
  112. package/src/core/commands/velocity.md +1 -1
  113. package/src/core/commands/verify.md +1 -1
  114. package/src/core/commands/whats-new.md +1 -1
  115. package/src/core/commands/workflow.md +1 -1
  116. package/tools/cli/installers/ide/codex.js +12 -4
  117. package/tools/cli/lib/content-injector.js +23 -4
package/CHANGELOG.md CHANGED
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.99.3] - 2026-02-09
11
+
12
+ ### Fixed
13
+ - Auto-heal tmux socket directory after macOS reboot
14
+
15
+ ## [2.99.2] - 2026-02-09
16
+
17
+ ### Added
18
+ - Documentation overhaul with teams commands, frontmatter fixes, and damage control resilience
19
+
10
20
  ## [2.99.1] - 2026-02-08
11
21
 
12
22
  ### Added
@@ -1310,8 +1310,10 @@ class DashboardServer extends EventEmitter {
1310
1310
  title: e.title || id,
1311
1311
  status: e.status || 'unknown',
1312
1312
  storyCount: (e.stories || []).length,
1313
- doneCount: (e.stories || []).filter(sid =>
1314
- stories[sid] && (stories[sid].status === 'done' || stories[sid].status === 'completed')
1313
+ doneCount: (e.stories || []).filter(
1314
+ sid =>
1315
+ stories[sid] &&
1316
+ (stories[sid].status === 'done' || stories[sid].status === 'completed')
1315
1317
  ).length,
1316
1318
  })),
1317
1319
  };
@@ -1353,11 +1355,15 @@ class DashboardServer extends EventEmitter {
1353
1355
 
1354
1356
  // Get ahead/behind counts relative to upstream
1355
1357
  try {
1356
- const counts = execFileSync('git', ['rev-list', '--left-right', '--count', 'HEAD...@{u}'], {
1357
- cwd,
1358
- encoding: 'utf8',
1359
- stdio: ['pipe', 'pipe', 'pipe'],
1360
- }).trim();
1358
+ const counts = execFileSync(
1359
+ 'git',
1360
+ ['rev-list', '--left-right', '--count', 'HEAD...@{u}'],
1361
+ {
1362
+ cwd,
1363
+ encoding: 'utf8',
1364
+ stdio: ['pipe', 'pipe', 'pipe'],
1365
+ }
1366
+ ).trim();
1361
1367
  const [ahead, behind] = counts.split(/\s+/).map(Number);
1362
1368
  entry.ahead = ahead || 0;
1363
1369
  entry.behind = behind || 0;
@@ -1433,7 +1439,9 @@ class DashboardServer extends EventEmitter {
1433
1439
  }
1434
1440
  }
1435
1441
 
1436
- session.send(createNotification('info', 'Editor', `Opened ${require('path').basename(fullPath)}`));
1442
+ session.send(
1443
+ createNotification('info', 'Editor', `Opened ${require('path').basename(fullPath)}`)
1444
+ );
1437
1445
  } catch (error) {
1438
1446
  console.error('[Open File Error]', error.message);
1439
1447
  session.send(createError('OPEN_FILE_ERROR', `Failed to open file: ${error.message}`));
package/lib/feedback.js CHANGED
@@ -72,9 +72,10 @@ class Feedback {
72
72
  constructor(options = {}) {
73
73
  this.isTTY = options.isTTY !== undefined ? options.isTTY : process.stdout.isTTY;
74
74
  this.indent = options.indent || 0;
75
- this.quiet = options.quiet !== undefined
76
- ? options.quiet
77
- : (process.env.AGILEFLOW_QUIET === '1' || process.env.AGILEFLOW_QUIET === 'true');
75
+ this.quiet =
76
+ options.quiet !== undefined
77
+ ? options.quiet
78
+ : process.env.AGILEFLOW_QUIET === '1' || process.env.AGILEFLOW_QUIET === 'true';
78
79
  this.verbose = options.verbose || false;
79
80
  }
80
81
 
@@ -270,14 +270,23 @@ function integrateSession(sessionId, options = {}, loadRegistry, saveRegistry, r
270
270
  fs.mkdirSync(notifyDir, { recursive: true });
271
271
  }
272
272
  const notifyPath = path.join(notifyDir, 'last-merge.json');
273
- fs.writeFileSync(notifyPath, JSON.stringify({
274
- merged_at: new Date().toISOString(),
275
- session_id: sessionId,
276
- branch: branchName,
277
- strategy,
278
- commit_message: commitMessage,
279
- }, null, 2));
280
- } catch (e) { /* ignore notification write failures */ }
273
+ fs.writeFileSync(
274
+ notifyPath,
275
+ JSON.stringify(
276
+ {
277
+ merged_at: new Date().toISOString(),
278
+ session_id: sessionId,
279
+ branch: branchName,
280
+ strategy,
281
+ commit_message: commitMessage,
282
+ },
283
+ null,
284
+ 2
285
+ )
286
+ );
287
+ } catch (e) {
288
+ /* ignore notification write failures */
289
+ }
281
290
 
282
291
  // Delete worktree first (before branch, as worktree holds ref)
283
292
  if (deleteWorktree && session.path !== ROOT && fs.existsSync(session.path)) {
@@ -178,7 +178,9 @@ function createSessionOperations(deps) {
178
178
  registry.next_id++;
179
179
  const isMain = cwd === ROOT && !isGitWorktree(cwd);
180
180
  const detectedType =
181
- threadType && THREAD_TYPES.includes(threadType) ? threadType : detectThreadType(null, !isMain);
181
+ threadType && THREAD_TYPES.includes(threadType)
182
+ ? threadType
183
+ : detectThreadType(null, !isMain);
182
184
 
183
185
  registry.sessions[sessionId] = {
184
186
  path: cwd,
@@ -210,7 +212,12 @@ function createSessionOperations(deps) {
210
212
  const session = registry.sessions[sessionId];
211
213
  if (!session) return null;
212
214
  const threadType = session.thread_type || (session.is_main ? 'base' : 'parallel');
213
- return { id: sessionId, ...session, thread_type: threadType, active: isSessionActive(sessionId) };
215
+ return {
216
+ id: sessionId,
217
+ ...session,
218
+ thread_type: threadType,
219
+ active: isSessionActive(sessionId),
220
+ };
214
221
  }
215
222
 
216
223
  async function createSession(options = {}) {
@@ -248,7 +255,10 @@ function createSessionOperations(deps) {
248
255
  );
249
256
  let branchCreatedByUs = false;
250
257
  if (checkRef.status !== 0) {
251
- const createBranch = spawnSync('git', ['branch', branchName], { cwd: ROOT, encoding: 'utf8' });
258
+ const createBranch = spawnSync('git', ['branch', branchName], {
259
+ cwd: ROOT,
260
+ encoding: 'utf8',
261
+ });
252
262
  if (createBranch.status !== 0) {
253
263
  return {
254
264
  success: false,
@@ -127,7 +127,12 @@ function createSessionSwitching(deps) {
127
127
  return { success: false, error: 'Session not found' };
128
128
  const session = registry.sessions[targetId];
129
129
  const threadType = session.thread_type || (session.is_main ? 'base' : 'parallel');
130
- return { success: true, thread_type: threadType, session_id: targetId, is_main: session.is_main };
130
+ return {
131
+ success: true,
132
+ thread_type: threadType,
133
+ session_id: targetId,
134
+ is_main: session.is_main,
135
+ };
131
136
  }
132
137
 
133
138
  function setSessionThreadType(sessionId, threadType) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agileflow",
3
- "version": "2.99.1",
3
+ "version": "2.99.3",
4
4
  "description": "AI-driven agile development system for Claude Code, Cursor, Windsurf, and more",
5
5
  "keywords": [
6
6
  "agile",
@@ -323,7 +323,8 @@ function main() {
323
323
 
324
324
  // Enable/disable specific features with progress tracking
325
325
  const totalChanges = enable.length + disable.length;
326
- const featureTask = totalChanges > 1 ? feedback.task('Applying feature changes', totalChanges) : null;
326
+ const featureTask =
327
+ totalChanges > 1 ? feedback.task('Applying feature changes', totalChanges) : null;
327
328
 
328
329
  // Enable specific features
329
330
  enable.forEach(f => {
@@ -1965,9 +1965,12 @@ async function main() {
1965
1965
  // Check for multiple Claude processes in the same working directory
1966
1966
  if (processCleanup) {
1967
1967
  try {
1968
- // Check if auto-kill is enabled in metadata
1968
+ // Auto-kill is explicitly opt-in at runtime.
1969
+ // Even if metadata has autoKill=true from older configs, we require
1970
+ // AGILEFLOW_PROCESS_CLEANUP_AUTOKILL=1 to prevent accidental session kills.
1969
1971
  const metadata = cache?.metadata;
1970
- const autoKill = metadata?.features?.processCleanup?.autoKill === true;
1972
+ const autoKillConfigured = metadata?.features?.processCleanup?.autoKill === true;
1973
+ const autoKill = autoKillConfigured && process.env.AGILEFLOW_PROCESS_CLEANUP_AUTOKILL === '1';
1971
1974
 
1972
1975
  const cleanupResult = processCleanup.cleanupDuplicateProcesses({
1973
1976
  rootDir,
@@ -1987,15 +1990,17 @@ async function main() {
1987
1990
  console.log(`${c.dim} └─ PID ${proc.pid} (${proc.method})${c.reset}`);
1988
1991
  });
1989
1992
  } else {
1990
- // Warn only (auto-kill not enabled)
1993
+ // Warn only (auto-kill disabled or skipped by safety guards)
1991
1994
  console.log(
1992
1995
  `${c.amber}⚠️ ${cleanupResult.duplicates} other Claude process(es) in same directory${c.reset}`
1993
1996
  );
1994
1997
  console.log(`${c.slate} This may cause slowdowns and freezing. Options:${c.reset}`);
1995
1998
  console.log(`${c.slate} • Close duplicate Claude windows/tabs${c.reset}`);
1996
- console.log(
1997
- `${c.slate} • Run ${c.skyBlue}/agileflow:configure --enable=processcleanup${c.slate} for auto-cleanup${c.reset}`
1998
- );
1999
+ if (autoKillConfigured) {
2000
+ console.log(
2001
+ `${c.slate} • Auto-kill configured but runtime opt-in is off (safer default)${c.reset}`
2002
+ );
2003
+ }
1999
2004
  }
2000
2005
 
2001
2006
  if (cleanupResult.errors.length > 0) {
@@ -122,6 +122,27 @@ if [ "$NO_TMUX" = true ]; then
122
122
  exec claude "$@"
123
123
  fi
124
124
 
125
+ # ── Self-healing: ensure tmux socket directory exists ──────────────────────
126
+ # macOS clears /private/tmp/ on reboot, which removes the tmux socket dir.
127
+ # This causes "error connecting to ... (No such file or directory)" on every
128
+ # tmux command. We fix it automatically so users never see this error.
129
+ # Must run BEFORE any tmux command (including --kill, --attach, --refresh).
130
+ if command -v tmux &> /dev/null; then
131
+ _TMUX_BASE="${TMUX_TMPDIR:-${TMPDIR:-/tmp}}"
132
+ # Strip trailing slash(es) to avoid double-slash in path
133
+ _TMUX_BASE="${_TMUX_BASE%/}"
134
+ _TMUX_SOCK_DIR="${_TMUX_BASE}/tmux-$(id -u)"
135
+ if [ ! -d "$_TMUX_SOCK_DIR" ]; then
136
+ mkdir -p "$_TMUX_SOCK_DIR" 2>/dev/null && chmod 700 "$_TMUX_SOCK_DIR" 2>/dev/null
137
+ if [ ! -d "$_TMUX_SOCK_DIR" ]; then
138
+ echo "Warning: Could not create tmux socket directory ($_TMUX_SOCK_DIR)."
139
+ echo "Running claude without tmux."
140
+ exec claude "$@"
141
+ fi
142
+ fi
143
+ unset _TMUX_BASE _TMUX_SOCK_DIR
144
+ fi
145
+
125
146
  # Generate directory name (used for session name patterns)
126
147
  DIR_NAME=$(basename "$(pwd)")
127
148
 
@@ -15,7 +15,37 @@
15
15
  * Usage: Configured as PreToolUse hook in .claude/settings.json
16
16
  */
17
17
 
18
- const { createBashHook } = require('./lib/damage-control-utils');
18
+ const fs = require('fs');
19
+ const path = require('path');
19
20
 
20
- // Run the hook using factory
21
- createBashHook()();
21
+ function loadDamageControlUtils() {
22
+ const candidates = [
23
+ path.join(__dirname, 'lib', 'damage-control-utils.js'),
24
+ path.join(process.cwd(), '.agileflow', 'scripts', 'lib', 'damage-control-utils.js'),
25
+ ];
26
+
27
+ for (const candidate of candidates) {
28
+ try {
29
+ if (fs.existsSync(candidate)) {
30
+ return require(candidate);
31
+ }
32
+ } catch (e) {
33
+ // Try next candidate
34
+ }
35
+ }
36
+
37
+ return null;
38
+ }
39
+
40
+ const utils = loadDamageControlUtils();
41
+ if (!utils || typeof utils.createBashHook !== 'function') {
42
+ // Fail-open: never block Bash tool because hook bootstrap failed.
43
+ process.exit(0);
44
+ }
45
+
46
+ try {
47
+ utils.createBashHook()();
48
+ } catch (e) {
49
+ // Fail-open on runtime errors to avoid breaking CLI workflows.
50
+ process.exit(0);
51
+ }
@@ -12,7 +12,37 @@
12
12
  * Usage: Configured as PreToolUse hook in .claude/settings.json
13
13
  */
14
14
 
15
- const { createPathHook } = require('./lib/damage-control-utils');
15
+ const fs = require('fs');
16
+ const path = require('path');
16
17
 
17
- // Run the hook using factory
18
- createPathHook('edit')();
18
+ function loadDamageControlUtils() {
19
+ const candidates = [
20
+ path.join(__dirname, 'lib', 'damage-control-utils.js'),
21
+ path.join(process.cwd(), '.agileflow', 'scripts', 'lib', 'damage-control-utils.js'),
22
+ ];
23
+
24
+ for (const candidate of candidates) {
25
+ try {
26
+ if (fs.existsSync(candidate)) {
27
+ return require(candidate);
28
+ }
29
+ } catch (e) {
30
+ // Try next candidate
31
+ }
32
+ }
33
+
34
+ return null;
35
+ }
36
+
37
+ const utils = loadDamageControlUtils();
38
+ if (!utils || typeof utils.createPathHook !== 'function') {
39
+ // Fail-open: never block Edit tool because hook bootstrap failed.
40
+ process.exit(0);
41
+ }
42
+
43
+ try {
44
+ utils.createPathHook('edit')();
45
+ } catch (e) {
46
+ // Fail-open on runtime errors to avoid breaking CLI workflows.
47
+ process.exit(0);
48
+ }
@@ -12,7 +12,37 @@
12
12
  * Usage: Configured as PreToolUse hook in .claude/settings.json
13
13
  */
14
14
 
15
- const { createPathHook } = require('./lib/damage-control-utils');
15
+ const fs = require('fs');
16
+ const path = require('path');
16
17
 
17
- // Run the hook using factory
18
- createPathHook('write')();
18
+ function loadDamageControlUtils() {
19
+ const candidates = [
20
+ path.join(__dirname, 'lib', 'damage-control-utils.js'),
21
+ path.join(process.cwd(), '.agileflow', 'scripts', 'lib', 'damage-control-utils.js'),
22
+ ];
23
+
24
+ for (const candidate of candidates) {
25
+ try {
26
+ if (fs.existsSync(candidate)) {
27
+ return require(candidate);
28
+ }
29
+ } catch (e) {
30
+ // Try next candidate
31
+ }
32
+ }
33
+
34
+ return null;
35
+ }
36
+
37
+ const utils = loadDamageControlUtils();
38
+ if (!utils || typeof utils.createPathHook !== 'function') {
39
+ // Fail-open: never block Write tool because hook bootstrap failed.
40
+ process.exit(0);
41
+ }
42
+
43
+ try {
44
+ utils.createPathHook('write')();
45
+ } catch (e) {
46
+ // Fail-open on runtime errors to avoid breaking CLI workflows.
47
+ process.exit(0);
48
+ }
@@ -287,7 +287,7 @@ function enableFeature(feature, options = {}, version) {
287
287
  features: {
288
288
  processCleanup: {
289
289
  enabled: true,
290
- autoKill: true,
290
+ autoKill: false,
291
291
  version,
292
292
  at: new Date().toISOString(),
293
293
  },
@@ -296,9 +296,10 @@ function enableFeature(feature, options = {}, version) {
296
296
  version
297
297
  );
298
298
  success('Process cleanup enabled');
299
- warn('⚠️ Duplicate Claude processes will be automatically terminated on session start');
299
+ info('Duplicate Claude processes will be detected and reported on session start');
300
+ info('Auto-kill is disabled by default for safety');
300
301
  info(' Only affects processes in the SAME working directory (worktrees are safe)');
301
- info(' Prevents freezing caused by multiple Claude instances competing for resources');
302
+ info(' Set AGILEFLOW_PROCESS_CLEANUP_AUTOKILL=1 to opt in to auto-kill at runtime');
302
303
  return true;
303
304
  }
304
305
 
@@ -265,9 +265,10 @@ function repairScripts(targetFeature = null) {
265
265
  // Ensure scripts directory exists
266
266
  ensureDir(scriptsDir);
267
267
 
268
- const bar = scriptsToCheck.length > 5
269
- ? feedback.progressBar('Checking scripts', scriptsToCheck.length)
270
- : null;
268
+ const bar =
269
+ scriptsToCheck.length > 5
270
+ ? feedback.progressBar('Checking scripts', scriptsToCheck.length)
271
+ : null;
271
272
 
272
273
  for (const [script, scriptInfo] of scriptsToCheck) {
273
274
  const destPath = path.join(scriptsDir, script);
@@ -82,6 +82,153 @@ function getCwdForPid(pid) {
82
82
  }
83
83
  }
84
84
 
85
+ /**
86
+ * Get process start time in milliseconds.
87
+ * Used for safety checks when deciding whether a process is older/newer.
88
+ *
89
+ * @param {number} pid - Process ID
90
+ * @returns {number|null}
91
+ */
92
+ function getProcessStartTime(pid) {
93
+ if (!pid || typeof pid !== 'number') return null;
94
+
95
+ if (process.platform === 'linux') {
96
+ try {
97
+ const stat = fs.statSync(`/proc/${pid}`);
98
+ return Number.isFinite(stat.ctimeMs) ? stat.ctimeMs : null;
99
+ } catch (e) {
100
+ return null;
101
+ }
102
+ }
103
+
104
+ if (process.platform === 'darwin') {
105
+ try {
106
+ const output = execFileSync('ps', ['-o', 'lstart=', '-p', String(pid)], {
107
+ encoding: 'utf8',
108
+ timeout: 2000,
109
+ stdio: ['pipe', 'pipe', 'pipe'],
110
+ });
111
+ const ts = new Date(output.trim()).getTime();
112
+ return Number.isFinite(ts) ? ts : null;
113
+ } catch (e) {
114
+ return null;
115
+ }
116
+ }
117
+
118
+ return null;
119
+ }
120
+
121
+ /**
122
+ * Get parent PID for a process.
123
+ * Works on Linux (/proc) and macOS (ps).
124
+ *
125
+ * @param {number} pid - Process ID
126
+ * @returns {number|null}
127
+ */
128
+ function getParentPid(pid) {
129
+ if (!pid || typeof pid !== 'number') return null;
130
+
131
+ if (process.platform === 'linux') {
132
+ try {
133
+ // /proc/<pid>/stat format:
134
+ // pid (comm) state ppid ...
135
+ const stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf8');
136
+ const closeParen = stat.lastIndexOf(')');
137
+ if (closeParen === -1) return null;
138
+ const remainder = stat.slice(closeParen + 2).trim(); // state ppid ...
139
+ const fields = remainder.split(/\s+/);
140
+ const ppid = parseInt(fields[1], 10);
141
+ return Number.isFinite(ppid) ? ppid : null;
142
+ } catch (e) {
143
+ return null;
144
+ }
145
+ }
146
+
147
+ if (process.platform === 'darwin') {
148
+ try {
149
+ const output = execFileSync('ps', ['-o', 'ppid=', '-p', String(pid)], {
150
+ encoding: 'utf8',
151
+ timeout: 2000,
152
+ stdio: ['pipe', 'pipe', 'pipe'],
153
+ });
154
+ const ppid = parseInt(output.trim(), 10);
155
+ return Number.isFinite(ppid) ? ppid : null;
156
+ } catch (e) {
157
+ return null;
158
+ }
159
+ }
160
+
161
+ return null;
162
+ }
163
+
164
+ /**
165
+ * Get command-line args for a PID.
166
+ *
167
+ * @param {number} pid - Process ID
168
+ * @returns {string[]}
169
+ */
170
+ function getArgsForPid(pid) {
171
+ if (!pid || typeof pid !== 'number') return [];
172
+
173
+ if (process.platform === 'linux') {
174
+ try {
175
+ const cmdline = fs.readFileSync(`/proc/${pid}/cmdline`, 'utf8');
176
+ return parseCmdline(cmdline);
177
+ } catch (e) {
178
+ return [];
179
+ }
180
+ }
181
+
182
+ if (process.platform === 'darwin') {
183
+ try {
184
+ const output = execFileSync('ps', ['-o', 'command=', '-p', String(pid)], {
185
+ encoding: 'utf8',
186
+ timeout: 2000,
187
+ stdio: ['pipe', 'pipe', 'pipe'],
188
+ });
189
+ const cmd = output.trim();
190
+ return cmd ? [cmd] : [];
191
+ } catch (e) {
192
+ return [];
193
+ }
194
+ }
195
+
196
+ return [];
197
+ }
198
+
199
+ /**
200
+ * Walk process ancestry and find the nearest Claude process.
201
+ *
202
+ * Hooks are typically executed as:
203
+ * claude -> shell (bash/sh) -> hook command (node)
204
+ * so `process.ppid` is often the shell, not Claude.
205
+ *
206
+ * @param {number} startPid - PID to start from (defaults to current process)
207
+ * @param {number} maxDepth - Max parent hops
208
+ * @returns {number|null}
209
+ */
210
+ function findClaudeAncestorPid(startPid = process.pid, maxDepth = 12) {
211
+ let pid = startPid;
212
+ const visited = new Set();
213
+
214
+ for (let depth = 0; depth < maxDepth; depth++) {
215
+ const parentPid = getParentPid(pid);
216
+ if (!parentPid || parentPid <= 1 || visited.has(parentPid)) {
217
+ return null;
218
+ }
219
+ visited.add(parentPid);
220
+
221
+ const parentArgs = getArgsForPid(parentPid);
222
+ if (isClaudeProcess(parentArgs)) {
223
+ return parentPid;
224
+ }
225
+
226
+ pid = parentPid;
227
+ }
228
+
229
+ return null;
230
+ }
231
+
85
232
  /**
86
233
  * Find all Claude Code processes on the system
87
234
  * @returns {Array<{pid: number, cwd: string|null, cmdline: string, startTime: number}>}
@@ -195,14 +342,14 @@ function findClaudeProcesses() {
195
342
  *
196
343
  * @param {string} currentCwd - Current working directory
197
344
  * @param {number} currentPid - Current session's PID (to exclude)
198
- * @returns {Array} Duplicate processes (excluding current)
345
+ * @returns {Array} Duplicate processes (excluding current if known)
199
346
  */
200
347
  function findDuplicatesInCwd(currentCwd, currentPid) {
201
348
  const allClaude = findClaudeProcesses();
202
349
 
203
350
  return allClaude.filter(proc => {
204
- // Exclude current session
205
- if (proc.pid === currentPid) return false;
351
+ // Exclude current session when known
352
+ if (currentPid && proc.pid === currentPid) return false;
206
353
 
207
354
  // Must have cwd to compare
208
355
  if (!proc.cwd || !currentCwd) return false;
@@ -274,14 +421,15 @@ function killProcessGracefully(pid, options = {}) {
274
421
  /**
275
422
  * Get current session's PID from process ancestry
276
423
  *
277
- * The Claude Code process is typically the parent of this script.
278
- * We use process.ppid to identify it.
424
+ * The Claude process is usually an ancestor (often grandparent):
425
+ * claude -> bash -> node hook
279
426
  *
280
- * @returns {number}
427
+ * If no Claude ancestor is found, returns null.
428
+ *
429
+ * @returns {number|null}
281
430
  */
282
431
  function getCurrentSessionPid() {
283
- // The claude process is our parent (this script is run by claude)
284
- return process.ppid || process.pid;
432
+ return findClaudeAncestorPid(process.pid);
285
433
  }
286
434
 
287
435
  /**
@@ -298,6 +446,7 @@ function cleanupDuplicateProcesses(options = {}) {
298
446
 
299
447
  const currentCwd = rootDir || process.cwd();
300
448
  const currentPid = getCurrentSessionPid();
449
+ const currentStartTime = getProcessStartTime(currentPid);
301
450
  const duplicates = findDuplicatesInCwd(currentCwd, currentPid);
302
451
 
303
452
  const result = {
@@ -305,8 +454,9 @@ function cleanupDuplicateProcesses(options = {}) {
305
454
  processes: duplicates,
306
455
  killed: [],
307
456
  errors: [],
308
- autoKillEnabled: autoKill,
457
+ autoKillEnabled: autoKill && !!currentPid,
309
458
  currentPid,
459
+ currentStartTime,
310
460
  };
311
461
 
312
462
  if (duplicates.length === 0) {
@@ -318,8 +468,32 @@ function cleanupDuplicateProcesses(options = {}) {
318
468
  return result;
319
469
  }
320
470
 
471
+ // Safety gate: if we can't identify the current Claude session PID,
472
+ // never auto-kill anything.
473
+ if (!currentPid) {
474
+ result.errors.push({
475
+ error: 'Could not determine current Claude session PID; auto-kill skipped',
476
+ });
477
+ return result;
478
+ }
479
+
480
+ // Safety gate: only kill processes that are clearly older than current session.
481
+ // This prevents terminating the session that just started.
482
+ const olderDuplicates = duplicates.filter(proc => {
483
+ if (!proc || !proc.pid || proc.pid === currentPid) return false;
484
+ if (!currentStartTime || !proc.startTime) return false;
485
+ return proc.startTime < currentStartTime;
486
+ });
487
+
488
+ if (olderDuplicates.length === 0) {
489
+ result.errors.push({
490
+ error: 'No clearly older duplicate processes found; auto-kill skipped',
491
+ });
492
+ return result;
493
+ }
494
+
321
495
  // Safety limit - don't kill more than MAX_PROCESSES_TO_KILL
322
- const toKill = duplicates.slice(0, MAX_PROCESSES_TO_KILL);
496
+ const toKill = olderDuplicates.slice(0, MAX_PROCESSES_TO_KILL);
323
497
 
324
498
  for (const proc of toKill) {
325
499
  const killResult = killProcessGracefully(proc.pid, { dryRun });
@@ -364,6 +538,10 @@ module.exports = {
364
538
  parseCmdline,
365
539
  isClaudeProcess,
366
540
  getCwdForPid,
541
+ getProcessStartTime,
542
+ getParentPid,
543
+ getArgsForPid,
544
+ findClaudeAncestorPid,
367
545
 
368
546
  // Constants
369
547
  KILL_GRACE_PERIOD_MS,