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.
- package/CHANGELOG.md +10 -0
- package/lib/dashboard-server.js +16 -8
- package/lib/feedback.js +4 -3
- package/lib/merge-operations.js +17 -8
- package/lib/session-operations.js +13 -3
- package/lib/session-switching.js +6 -1
- package/package.json +1 -1
- package/scripts/agileflow-configure.js +2 -1
- package/scripts/agileflow-welcome.js +11 -6
- package/scripts/claude-tmux.sh +21 -0
- package/scripts/damage-control-bash.js +33 -3
- package/scripts/damage-control-edit.js +33 -3
- package/scripts/damage-control-write.js +33 -3
- package/scripts/lib/configure-features.js +4 -3
- package/scripts/lib/configure-repair.js +4 -3
- package/scripts/lib/process-cleanup.js +188 -10
- package/scripts/session-manager.js +108 -35
- package/src/core/agents/configuration/archival.md +2 -1
- package/src/core/agents/configuration/attribution.md +2 -1
- package/src/core/agents/configuration/ci.md +2 -1
- package/src/core/agents/configuration/damage-control.md +2 -1
- package/src/core/agents/configuration/git-config.md +2 -1
- package/src/core/agents/configuration/hooks.md +2 -1
- package/src/core/agents/configuration/precompact.md +2 -1
- package/src/core/agents/configuration/status-line.md +2 -1
- package/src/core/agents/configuration/verify.md +2 -1
- package/src/core/commands/adr/list.md +1 -1
- package/src/core/commands/adr/update.md +1 -1
- package/src/core/commands/adr/view.md +1 -1
- package/src/core/commands/adr.md +1 -1
- package/src/core/commands/agent.md +1 -1
- package/src/core/commands/api.md +1 -1
- package/src/core/commands/assign.md +1 -1
- package/src/core/commands/audit.md +1 -1
- package/src/core/commands/auto.md +1 -1
- package/src/core/commands/automate.md +1 -1
- package/src/core/commands/babysit.md +1 -1
- package/src/core/commands/baseline.md +1 -1
- package/src/core/commands/batch.md +1 -1
- package/src/core/commands/blockers.md +1 -1
- package/src/core/commands/board.md +1 -1
- package/src/core/commands/changelog.md +1 -1
- package/src/core/commands/choose.md +1 -1
- package/src/core/commands/ci.md +1 -1
- package/src/core/commands/compress.md +1 -1
- package/src/core/commands/configure.md +1 -1
- package/src/core/commands/context/export.md +1 -1
- package/src/core/commands/context/full.md +1 -1
- package/src/core/commands/context/note.md +1 -1
- package/src/core/commands/council.md +1 -1
- package/src/core/commands/debt.md +1 -1
- package/src/core/commands/deploy.md +1 -1
- package/src/core/commands/deps.md +1 -1
- package/src/core/commands/diagnose.md +1 -1
- package/src/core/commands/docs.md +1 -1
- package/src/core/commands/epic/list.md +1 -1
- package/src/core/commands/epic/view.md +1 -1
- package/src/core/commands/epic.md +1 -1
- package/src/core/commands/feedback.md +1 -1
- package/src/core/commands/handoff.md +1 -1
- package/src/core/commands/help.md +4 -190
- package/src/core/commands/ideate/history.md +1 -1
- package/src/core/commands/ideate/new.md +1 -1
- package/src/core/commands/impact.md +1 -1
- package/src/core/commands/install.md +1 -1
- package/src/core/commands/logic/audit.md +1 -1
- package/src/core/commands/maintain.md +1 -1
- package/src/core/commands/metrics.md +1 -1
- package/src/core/commands/multi-expert.md +1 -1
- package/src/core/commands/packages.md +1 -1
- package/src/core/commands/pr.md +1 -1
- package/src/core/commands/readme-sync.md +1 -1
- package/src/core/commands/research/analyze.md +1 -1
- package/src/core/commands/research/ask.md +1 -1
- package/src/core/commands/research/import.md +1 -1
- package/src/core/commands/research/list.md +1 -1
- package/src/core/commands/research/synthesize.md +1 -1
- package/src/core/commands/research/view.md +1 -1
- package/src/core/commands/retro.md +1 -1
- package/src/core/commands/review.md +1 -1
- package/src/core/commands/rlm.md +1 -1
- package/src/core/commands/roadmap/analyze.md +1 -1
- package/src/core/commands/rpi.md +1 -1
- package/src/core/commands/session/cleanup.md +1 -1
- package/src/core/commands/session/end.md +1 -1
- package/src/core/commands/session/history.md +1 -1
- package/src/core/commands/session/init.md +1 -1
- package/src/core/commands/session/new.md +1 -1
- package/src/core/commands/session/resume.md +1 -1
- package/src/core/commands/session/spawn.md +1 -1
- package/src/core/commands/session/status.md +1 -1
- package/src/core/commands/skill/create.md +1 -1
- package/src/core/commands/skill/delete.md +1 -1
- package/src/core/commands/skill/edit.md +1 -1
- package/src/core/commands/skill/list.md +1 -1
- package/src/core/commands/skill/test.md +1 -1
- package/src/core/commands/skill/upgrade.md +1 -1
- package/src/core/commands/sprint.md +1 -1
- package/src/core/commands/status.md +1 -1
- package/src/core/commands/story/list.md +1 -1
- package/src/core/commands/story/view.md +1 -1
- package/src/core/commands/story-validate.md +1 -1
- package/src/core/commands/story.md +1 -1
- package/src/core/commands/team/list.md +1 -1
- package/src/core/commands/team/start.md +1 -1
- package/src/core/commands/team/status.md +1 -1
- package/src/core/commands/team/stop.md +1 -1
- package/src/core/commands/template.md +1 -1
- package/src/core/commands/tests.md +1 -1
- package/src/core/commands/update.md +1 -1
- package/src/core/commands/validate-expertise.md +1 -1
- package/src/core/commands/velocity.md +1 -1
- package/src/core/commands/verify.md +1 -1
- package/src/core/commands/whats-new.md +1 -1
- package/src/core/commands/workflow.md +1 -1
- package/tools/cli/installers/ide/codex.js +12 -4
- 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
|
package/lib/dashboard-server.js
CHANGED
|
@@ -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(
|
|
1314
|
-
|
|
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(
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
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(
|
|
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 =
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
package/lib/merge-operations.js
CHANGED
|
@@ -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(
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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)
|
|
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 {
|
|
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], {
|
|
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,
|
package/lib/session-switching.js
CHANGED
|
@@ -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 {
|
|
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
|
@@ -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 =
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
-
|
|
1997
|
-
|
|
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) {
|
package/scripts/claude-tmux.sh
CHANGED
|
@@ -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
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
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:
|
|
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
|
-
|
|
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('
|
|
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 =
|
|
269
|
-
|
|
270
|
-
|
|
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
|
|
278
|
-
*
|
|
424
|
+
* The Claude process is usually an ancestor (often grandparent):
|
|
425
|
+
* claude -> bash -> node hook
|
|
279
426
|
*
|
|
280
|
-
*
|
|
427
|
+
* If no Claude ancestor is found, returns null.
|
|
428
|
+
*
|
|
429
|
+
* @returns {number|null}
|
|
281
430
|
*/
|
|
282
431
|
function getCurrentSessionPid() {
|
|
283
|
-
|
|
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 =
|
|
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,
|