agileflow 2.94.1 → 2.95.1

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 (74) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +3 -3
  3. package/lib/colors.generated.js +117 -0
  4. package/lib/colors.js +59 -109
  5. package/lib/generator-factory.js +333 -0
  6. package/lib/path-utils.js +49 -0
  7. package/lib/session-registry.js +25 -15
  8. package/lib/smart-json-file.js +40 -32
  9. package/lib/state-machine.js +286 -0
  10. package/package.json +1 -1
  11. package/scripts/agileflow-configure.js +7 -6
  12. package/scripts/archive-completed-stories.sh +86 -11
  13. package/scripts/babysit-context-restore.js +89 -0
  14. package/scripts/claude-tmux.sh +111 -5
  15. package/scripts/damage-control/bash-tool-damage-control.js +11 -247
  16. package/scripts/damage-control/edit-tool-damage-control.js +9 -249
  17. package/scripts/damage-control/write-tool-damage-control.js +9 -244
  18. package/scripts/generate-colors.js +314 -0
  19. package/scripts/lib/colors.generated.sh +82 -0
  20. package/scripts/lib/colors.sh +10 -70
  21. package/scripts/lib/configure-features.js +401 -0
  22. package/scripts/lib/context-loader.js +181 -52
  23. package/scripts/precompact-context.sh +54 -17
  24. package/scripts/session-coordinator.sh +2 -2
  25. package/scripts/session-manager.js +653 -10
  26. package/src/core/commands/audit.md +93 -0
  27. package/src/core/commands/auto.md +73 -0
  28. package/src/core/commands/babysit.md +169 -13
  29. package/src/core/commands/baseline.md +73 -0
  30. package/src/core/commands/batch.md +64 -0
  31. package/src/core/commands/blockers.md +60 -0
  32. package/src/core/commands/board.md +66 -0
  33. package/src/core/commands/choose.md +77 -0
  34. package/src/core/commands/ci.md +77 -0
  35. package/src/core/commands/compress.md +27 -1
  36. package/src/core/commands/configure.md +126 -10
  37. package/src/core/commands/council.md +74 -0
  38. package/src/core/commands/debt.md +72 -0
  39. package/src/core/commands/deploy.md +73 -0
  40. package/src/core/commands/deps.md +68 -0
  41. package/src/core/commands/docs.md +60 -0
  42. package/src/core/commands/feedback.md +68 -0
  43. package/src/core/commands/ideate.md +74 -0
  44. package/src/core/commands/impact.md +74 -0
  45. package/src/core/commands/install.md +529 -0
  46. package/src/core/commands/maintain.md +558 -0
  47. package/src/core/commands/metrics.md +75 -0
  48. package/src/core/commands/multi-expert.md +74 -0
  49. package/src/core/commands/packages.md +69 -0
  50. package/src/core/commands/readme-sync.md +64 -0
  51. package/src/core/commands/research/analyze.md +285 -121
  52. package/src/core/commands/research/import.md +281 -109
  53. package/src/core/commands/retro.md +76 -0
  54. package/src/core/commands/review.md +72 -0
  55. package/src/core/commands/rlm.md +83 -0
  56. package/src/core/commands/rpi.md +90 -0
  57. package/src/core/commands/session/cleanup.md +214 -12
  58. package/src/core/commands/session/end.md +155 -17
  59. package/src/core/commands/sprint.md +72 -0
  60. package/src/core/commands/story-validate.md +68 -0
  61. package/src/core/commands/template.md +69 -0
  62. package/src/core/commands/tests.md +83 -0
  63. package/src/core/commands/update.md +59 -0
  64. package/src/core/commands/validate-expertise.md +76 -0
  65. package/src/core/commands/velocity.md +74 -0
  66. package/src/core/commands/verify.md +91 -0
  67. package/src/core/commands/whats-new.md +69 -0
  68. package/src/core/commands/workflow.md +88 -0
  69. package/src/core/templates/command-documentation.md +187 -0
  70. package/tools/cli/commands/session.js +1171 -0
  71. package/tools/cli/commands/setup.js +2 -81
  72. package/tools/cli/installers/core/installer.js +0 -5
  73. package/tools/cli/installers/ide/claude-code.js +6 -0
  74. package/tools/cli/lib/config-manager.js +42 -5
@@ -20,7 +20,7 @@ const fs = require('fs');
20
20
  const fsPromises = require('fs').promises;
21
21
  const path = require('path');
22
22
  const os = require('os');
23
- const { execSync, exec } = require('child_process');
23
+ const { spawnSync, spawn } = require('child_process');
24
24
 
25
25
  // Try to use cached reads if available
26
26
  let readJSONCached, readFileCached;
@@ -39,25 +39,26 @@ try {
39
39
  // =============================================================================
40
40
 
41
41
  /**
42
- * Whitelisted commands for safeExec
43
- * Only commands starting with these prefixes are allowed
42
+ * Whitelisted git subcommands with allowed arguments (US-0187)
43
+ * Only these specific read-only git operations are permitted.
44
+ *
45
+ * Format: { subcommand: true } allows any args (read-only commands)
46
+ * { subcommand: ['--flag1', '--flag2'] } allows only listed first args
44
47
  */
45
- const SAFEEXEC_ALLOWED_COMMANDS = [
46
- // Git commands (read-only operations)
47
- 'git ',
48
- 'git branch',
49
- 'git log',
50
- 'git status',
51
- 'git diff',
52
- 'git rev-parse',
53
- 'git describe',
54
- 'git show',
55
- 'git config',
56
- 'git remote',
57
- 'git tag',
58
- // Node commands (for internal AgileFlow scripts only)
59
- 'node ',
60
- ];
48
+ const SAFEEXEC_ALLOWED_GIT_SUBCOMMANDS = {
49
+ // Read-only git operations
50
+ branch: ['--show-current', '-a', '--list', '-r', '--all'],
51
+ log: true, // All log flags are read-only
52
+ status: ['--short', '--porcelain', '-s', '--ignored'],
53
+ diff: true, // All diff flags are read-only
54
+ 'rev-parse': ['HEAD', '--git-dir', '--show-toplevel', '--abbrev-ref', '--is-inside-work-tree'],
55
+ describe: true, // Read-only
56
+ show: true, // Read-only
57
+ config: ['--get', '--list', '-l', '--get-all'], // Read-only config operations only
58
+ remote: ['-v', '--verbose', 'get-url'],
59
+ tag: ['--list', '-l'],
60
+ 'ls-files': true, // Read-only listing
61
+ };
61
62
 
62
63
  /**
63
64
  * Dangerous patterns that should never be executed
@@ -107,32 +108,86 @@ function logSafeExec(level, message, details = {}) {
107
108
  }
108
109
 
109
110
  /**
110
- * Check if a command is allowed
111
- * @param {string} cmd - Command to check
112
- * @returns {{allowed: boolean, reason?: string}}
111
+ * Parse a git command string into executable and arguments (US-0187)
112
+ * @param {string} cmd - Command string (e.g., "git branch --show-current")
113
+ * @returns {{ ok: boolean, data?: { executable: string, subcommand: string, args: string[], fullArgs: string[] }, error?: string }}
113
114
  */
114
- function isCommandAllowed(cmd) {
115
+ function parseGitCommand(cmd) {
115
116
  if (!cmd || typeof cmd !== 'string') {
116
- return { allowed: false, reason: 'Invalid command' };
117
+ return { ok: false, error: 'Invalid command' };
118
+ }
119
+
120
+ const parts = cmd.trim().split(/\s+/);
121
+ if (parts.length < 1 || parts[0] !== 'git') {
122
+ return { ok: false, error: 'Only git commands are supported' };
117
123
  }
118
124
 
119
- const trimmed = cmd.trim();
125
+ // Handle bare 'git' command
126
+ if (parts.length < 2) {
127
+ return { ok: false, error: 'Git subcommand required' };
128
+ }
120
129
 
121
- // Check for blocked patterns
130
+ return {
131
+ ok: true,
132
+ data: {
133
+ executable: 'git',
134
+ subcommand: parts[1],
135
+ args: parts.slice(2),
136
+ fullArgs: parts.slice(1), // ['branch', '--show-current']
137
+ },
138
+ };
139
+ }
140
+
141
+ /**
142
+ * Check if a git subcommand with args is allowed (US-0187)
143
+ * @param {string} subcommand - Git subcommand (e.g., 'branch')
144
+ * @param {string[]} args - Arguments to subcommand
145
+ * @returns {{ allowed: boolean, reason?: string }}
146
+ */
147
+ function isGitCommandAllowed(subcommand, args) {
148
+ // First check blocked patterns in arguments
149
+ const fullCmd = `git ${subcommand} ${args.join(' ')}`;
122
150
  for (const pattern of SAFEEXEC_BLOCKED_PATTERNS) {
123
- if (pattern.test(trimmed)) {
151
+ if (pattern.test(fullCmd)) {
124
152
  return { allowed: false, reason: `Blocked pattern: ${pattern}` };
125
153
  }
126
154
  }
127
155
 
128
- // Check against whitelist
129
- const isWhitelisted = SAFEEXEC_ALLOWED_COMMANDS.some(prefix => trimmed.startsWith(prefix));
156
+ // Check against allowed subcommands
157
+ const allowedArgs = SAFEEXEC_ALLOWED_GIT_SUBCOMMANDS[subcommand];
158
+ if (!allowedArgs) {
159
+ return { allowed: false, reason: `Git subcommand '${subcommand}' not in whitelist` };
160
+ }
161
+
162
+ // If allowedArgs is true, any args are allowed for this subcommand (read-only)
163
+ if (allowedArgs === true) {
164
+ return { allowed: true };
165
+ }
130
166
 
131
- if (!isWhitelisted) {
132
- return { allowed: false, reason: 'Command not in whitelist' };
167
+ // If allowedArgs is an array, first arg must match one of the allowed values
168
+ // (or args can be empty for commands like 'git status')
169
+ if (args.length === 0) {
170
+ return { allowed: true };
133
171
  }
134
172
 
135
- return { allowed: true };
173
+ if (allowedArgs.includes(args[0])) {
174
+ return { allowed: true };
175
+ }
176
+
177
+ return { allowed: false, reason: `Argument '${args[0]}' not allowed for 'git ${subcommand}'` };
178
+ }
179
+
180
+ /**
181
+ * Check if a command is allowed (legacy wrapper for backwards compatibility)
182
+ * @param {string} cmd - Command to check
183
+ * @returns {{allowed: boolean, reason?: string}}
184
+ */
185
+ function isCommandAllowed(cmd) {
186
+ const parsed = parseGitCommand(cmd);
187
+ if (!parsed.ok) {
188
+ return { allowed: false, reason: parsed.error };
189
+ }
190
+ return isGitCommandAllowed(parsed.data.subcommand, parsed.data.args);
136
191
  }
137
192
 
138
193
  // =============================================================================
@@ -184,12 +239,13 @@ function safeLs(dirPath) {
184
239
  }
185
240
 
186
241
  /**
187
- * Safely execute a shell command with whitelist validation.
242
+ * Safely execute a git command with whitelist validation (US-0187).
188
243
  *
189
- * Only whitelisted commands (mainly git operations) are allowed.
244
+ * Uses spawnSync with shell: false to prevent shell injection.
245
+ * Only whitelisted read-only git commands are allowed.
190
246
  * Dangerous patterns (pipes, redirects, etc.) are blocked.
191
247
  *
192
- * @param {string} cmd - Command to execute
248
+ * @param {string} cmd - Command to execute (must be a git command)
193
249
  * @param {Object} [options] - Options
194
250
  * @param {boolean} [options.bypassWhitelist=false] - Skip whitelist check (use with caution)
195
251
  * @returns {string|null} Command output or null
@@ -197,9 +253,19 @@ function safeLs(dirPath) {
197
253
  function safeExec(cmd, options = {}) {
198
254
  const { bypassWhitelist = false } = options;
199
255
 
256
+ // Parse command into executable and arguments
257
+ const parsed = parseGitCommand(cmd);
258
+ if (!parsed.ok) {
259
+ logSafeExec('warn', 'Invalid command format', {
260
+ cmd: cmd?.substring(0, 100),
261
+ error: parsed.error,
262
+ });
263
+ return null;
264
+ }
265
+
200
266
  // Validate command unless bypassed
201
267
  if (!bypassWhitelist) {
202
- const check = isCommandAllowed(cmd);
268
+ const check = isGitCommandAllowed(parsed.data.subcommand, parsed.data.args);
203
269
  if (!check.allowed) {
204
270
  logSafeExec('warn', 'Command blocked by whitelist', {
205
271
  cmd: cmd?.substring(0, 100),
@@ -215,14 +281,38 @@ function safeExec(cmd, options = {}) {
215
281
  });
216
282
 
217
283
  try {
218
- const result = execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
284
+ // Use spawnSync with array arguments - NO SHELL INTERPRETATION (US-0187)
285
+ const result = spawnSync(parsed.data.executable, parsed.data.fullArgs, {
286
+ encoding: 'utf8',
287
+ stdio: ['pipe', 'pipe', 'pipe'],
288
+ shell: false, // CRITICAL: Prevents shell injection
289
+ });
290
+
291
+ if (result.error) {
292
+ logSafeExec('error', 'Command spawn failed', {
293
+ cmd: cmd?.substring(0, 50),
294
+ error: result.error.message,
295
+ });
296
+ return null;
297
+ }
298
+
299
+ if (result.status !== 0) {
300
+ logSafeExec('debug', 'Command exited non-zero', {
301
+ cmd: cmd?.substring(0, 50),
302
+ status: result.status,
303
+ stderr: result.stderr?.substring(0, 100),
304
+ });
305
+ return null;
306
+ }
307
+
308
+ const output = (result.stdout || '').trim();
219
309
  logSafeExec('debug', 'Command succeeded', {
220
310
  cmd: cmd?.substring(0, 50),
221
- outputLength: result?.length || 0,
311
+ outputLength: output.length,
222
312
  });
223
- return result;
313
+ return output;
224
314
  } catch (error) {
225
- logSafeExec('debug', 'Command failed', {
315
+ logSafeExec('error', 'Command execution error', {
226
316
  cmd: cmd?.substring(0, 50),
227
317
  error: error?.message?.substring(0, 100),
228
318
  });
@@ -275,12 +365,13 @@ async function safeLsAsync(dirPath) {
275
365
  }
276
366
 
277
367
  /**
278
- * Execute a command asynchronously with whitelist validation.
368
+ * Execute a git command asynchronously with whitelist validation (US-0187).
279
369
  *
280
- * Only whitelisted commands (mainly git operations) are allowed.
370
+ * Uses spawn with shell: false to prevent shell injection.
371
+ * Only whitelisted read-only git commands are allowed.
281
372
  * Dangerous patterns (pipes, redirects, etc.) are blocked.
282
373
  *
283
- * @param {string} cmd - Command to execute
374
+ * @param {string} cmd - Command to execute (must be a git command)
284
375
  * @param {Object} [options] - Options
285
376
  * @param {boolean} [options.bypassWhitelist=false] - Skip whitelist check (use with caution)
286
377
  * @returns {Promise<string|null>} Command output or null
@@ -288,9 +379,19 @@ async function safeLsAsync(dirPath) {
288
379
  async function safeExecAsync(cmd, options = {}) {
289
380
  const { bypassWhitelist = false } = options;
290
381
 
382
+ // Parse command into executable and arguments
383
+ const parsed = parseGitCommand(cmd);
384
+ if (!parsed.ok) {
385
+ logSafeExec('warn', 'Invalid async command format', {
386
+ cmd: cmd?.substring(0, 100),
387
+ error: parsed.error,
388
+ });
389
+ return null;
390
+ }
391
+
291
392
  // Validate command unless bypassed
292
393
  if (!bypassWhitelist) {
293
- const check = isCommandAllowed(cmd);
394
+ const check = isGitCommandAllowed(parsed.data.subcommand, parsed.data.args);
294
395
  if (!check.allowed) {
295
396
  logSafeExec('warn', 'Async command blocked by whitelist', {
296
397
  cmd: cmd?.substring(0, 100),
@@ -306,18 +407,44 @@ async function safeExecAsync(cmd, options = {}) {
306
407
  });
307
408
 
308
409
  return new Promise(resolve => {
309
- exec(cmd, { encoding: 'utf8' }, (error, stdout) => {
310
- if (error) {
311
- logSafeExec('debug', 'Async command failed', {
410
+ // Use spawn with array arguments - NO SHELL INTERPRETATION (US-0187)
411
+ const proc = spawn(parsed.data.executable, parsed.data.fullArgs, {
412
+ stdio: ['pipe', 'pipe', 'pipe'],
413
+ shell: false, // CRITICAL: Prevents shell injection
414
+ });
415
+
416
+ let stdout = '';
417
+ let stderr = '';
418
+
419
+ proc.stdout.on('data', data => {
420
+ stdout += data;
421
+ });
422
+
423
+ proc.stderr.on('data', data => {
424
+ stderr += data;
425
+ });
426
+
427
+ proc.on('error', error => {
428
+ logSafeExec('error', 'Async spawn error', {
429
+ cmd: cmd?.substring(0, 50),
430
+ error: error.message,
431
+ });
432
+ resolve(null);
433
+ });
434
+
435
+ proc.on('close', code => {
436
+ if (code !== 0) {
437
+ logSafeExec('debug', 'Async command exited non-zero', {
312
438
  cmd: cmd?.substring(0, 50),
313
- error: error?.message?.substring(0, 100),
439
+ code,
440
+ stderr: stderr?.substring(0, 100),
314
441
  });
315
442
  resolve(null);
316
443
  } else {
317
444
  const result = stdout.trim();
318
445
  logSafeExec('debug', 'Async command succeeded', {
319
446
  cmd: cmd?.substring(0, 50),
320
- outputLength: result?.length || 0,
447
+ outputLength: result.length,
321
448
  });
322
449
  resolve(result);
323
450
  }
@@ -676,11 +803,13 @@ module.exports = {
676
803
  safeLsAsync,
677
804
  safeExecAsync,
678
805
 
679
- // Command whitelist (US-0120)
680
- SAFEEXEC_ALLOWED_COMMANDS,
806
+ // Command whitelist (US-0120, US-0187)
807
+ SAFEEXEC_ALLOWED_GIT_SUBCOMMANDS,
681
808
  SAFEEXEC_BLOCKED_PATTERNS,
682
809
  configureSafeExecLogger,
683
- isCommandAllowed,
810
+ parseGitCommand,
811
+ isGitCommandAllowed,
812
+ isCommandAllowed, // Legacy wrapper for backward compatibility
684
813
 
685
814
  // Context tracking
686
815
  getContextPercentage,
@@ -3,10 +3,22 @@
3
3
  # AgileFlow PreCompact Hook
4
4
  # Outputs critical context that should survive conversation compaction.
5
5
  #
6
+ # Supports two modes:
7
+ # 1. Default: Extract COMPACT_SUMMARY sections from active command files
8
+ # 2. Experimental (fullFileInjection): Inject entire command files (more context, may be more reliable)
9
+ #
6
10
 
7
11
  # Get current version from package.json
8
12
  VERSION=$(node -p "require('./package.json').version" 2>/dev/null || echo "unknown")
9
13
 
14
+ # Check if experimental full-file injection mode is enabled
15
+ FULL_FILE_INJECTION=$(node -p "
16
+ try {
17
+ const meta = require('./docs/00-meta/agileflow-metadata.json');
18
+ meta.features?.experimental?.fullFileInjection === true ? 'true' : 'false';
19
+ } catch { 'false'; }
20
+ " 2>/dev/null || echo "false")
21
+
10
22
  # Get current git branch
11
23
  BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
12
24
 
@@ -77,25 +89,50 @@ if [ -f "docs/09-agents/session-state.json" ]; then
77
89
  # Security: Validate COMMAND_FILE contains only safe characters (alphanumeric, /, -, _, .)
78
90
  # and doesn't contain path traversal sequences
79
91
  if [[ "$COMMAND_FILE" =~ ^[a-zA-Z0-9/_.-]+$ ]] && [[ ! "$COMMAND_FILE" =~ \.\. ]]; then
80
- SUMMARY=$(COMMAND_FILE_PATH="$COMMAND_FILE" ACTIVE_CMD="$ACTIVE_COMMAND" node -e "
81
- const fs = require('fs');
82
- const filePath = process.env.COMMAND_FILE_PATH;
83
- const activeCmd = process.env.ACTIVE_CMD;
84
- // Double-check: only allow paths within expected directories
85
- const allowedPrefixes = ['packages/cli/src/core/commands/', '.agileflow/commands/', '.claude/commands/agileflow/'];
86
- if (!allowedPrefixes.some(p => filePath.startsWith(p))) {
87
- process.exit(1);
88
- }
89
- try {
90
- const content = fs.readFileSync(filePath, 'utf8');
91
- const match = content.match(/<!-- COMPACT_SUMMARY_START[\\s\\S]*?-->([\\s\\S]*?)<!-- COMPACT_SUMMARY_END -->/);
92
- if (match) {
93
- console.log('## ACTIVE COMMAND: /agileflow:' + activeCmd);
92
+ if [ "$FULL_FILE_INJECTION" = "true" ]; then
93
+ # EXPERIMENTAL: Inject the entire command file content
94
+ SUMMARY=$(COMMAND_FILE_PATH="$COMMAND_FILE" ACTIVE_CMD="$ACTIVE_COMMAND" node -e "
95
+ const fs = require('fs');
96
+ const filePath = process.env.COMMAND_FILE_PATH;
97
+ const activeCmd = process.env.ACTIVE_CMD;
98
+ // Double-check: only allow paths within expected directories
99
+ const allowedPrefixes = ['packages/cli/src/core/commands/', '.agileflow/commands/', '.claude/commands/agileflow/'];
100
+ if (!allowedPrefixes.some(p => filePath.startsWith(p))) {
101
+ process.exit(1);
102
+ }
103
+ try {
104
+ const content = fs.readFileSync(filePath, 'utf8');
105
+ console.log('## ⚠️ FULL COMMAND FILE (EXPERIMENTAL MODE): /agileflow:' + activeCmd);
106
+ console.log('');
107
+ console.log('The following is the COMPLETE command file. Follow ALL instructions below.');
108
+ console.log('');
109
+ console.log('---');
94
110
  console.log('');
95
- console.log(match[1].trim());
111
+ console.log(content);
112
+ } catch (e) {}
113
+ " 2>/dev/null || echo "")
114
+ else
115
+ # Default: Extract only the compact summary section
116
+ SUMMARY=$(COMMAND_FILE_PATH="$COMMAND_FILE" ACTIVE_CMD="$ACTIVE_COMMAND" node -e "
117
+ const fs = require('fs');
118
+ const filePath = process.env.COMMAND_FILE_PATH;
119
+ const activeCmd = process.env.ACTIVE_CMD;
120
+ // Double-check: only allow paths within expected directories
121
+ const allowedPrefixes = ['packages/cli/src/core/commands/', '.agileflow/commands/', '.claude/commands/agileflow/'];
122
+ if (!allowedPrefixes.some(p => filePath.startsWith(p))) {
123
+ process.exit(1);
96
124
  }
97
- } catch (e) {}
98
- " 2>/dev/null || echo "")
125
+ try {
126
+ const content = fs.readFileSync(filePath, 'utf8');
127
+ const match = content.match(/<!-- COMPACT_SUMMARY_START[\\s\\S]*?-->([\\s\\S]*?)<!-- COMPACT_SUMMARY_END -->/);
128
+ if (match) {
129
+ console.log('## ACTIVE COMMAND: /agileflow:' + activeCmd);
130
+ console.log('');
131
+ console.log(match[1].trim());
132
+ }
133
+ } catch (e) {}
134
+ " 2>/dev/null || echo "")
135
+ fi
99
136
  fi
100
137
 
101
138
  if [ ! -z "$SUMMARY" ]; then
@@ -36,7 +36,7 @@ is_pid_alive() {
36
36
  cleanup_stale_sessions() {
37
37
  local now=$(date +%s)
38
38
 
39
- for lockfile in "$SESSIONS_DIR"/session-*.lock 2>/dev/null; do
39
+ for lockfile in "$SESSIONS_DIR"/session-*.lock; do
40
40
  [[ -f "$lockfile" ]] || continue
41
41
 
42
42
  # Read lock file
@@ -63,7 +63,7 @@ cleanup_stale_sessions() {
63
63
  get_active_sessions() {
64
64
  local sessions=()
65
65
 
66
- for lockfile in "$SESSIONS_DIR"/session-*.lock 2>/dev/null; do
66
+ for lockfile in "$SESSIONS_DIR"/session-*.lock; do
67
67
  [[ -f "$lockfile" ]] || continue
68
68
 
69
69
  local lock_pid=$(grep "^pid=" "$lockfile" 2>/dev/null | cut -d= -f2)