agileflow 2.92.0 โ 2.93.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.
- package/CHANGELOG.md +10 -0
- package/README.md +6 -6
- package/lib/codebase-indexer.js +2 -1
- package/package.json +1 -1
- package/scripts/agileflow-statusline.sh +106 -0
- package/scripts/agileflow-welcome.js +135 -22
- package/scripts/document-repl.js +793 -0
- package/scripts/lib/configure-features.js +8 -1
- package/scripts/lib/context-loader.js +16 -16
- package/scripts/query-codebase.js +8 -3
- package/scripts/session-manager.js +374 -16
- package/scripts/spawn-parallel.js +72 -30
- package/src/core/agents/accessibility.md +19 -125
- package/src/core/agents/adr-writer.md +18 -1
- package/src/core/agents/analytics.md +19 -125
- package/src/core/agents/api.md +5 -130
- package/src/core/agents/ci.md +26 -131
- package/src/core/agents/compliance.md +21 -125
- package/src/core/agents/database.md +20 -125
- package/src/core/agents/datamigration.md +20 -125
- package/src/core/agents/design.md +19 -125
- package/src/core/agents/devops.md +12 -129
- package/src/core/agents/documentation.md +18 -1
- package/src/core/agents/epic-planner.md +31 -10
- package/src/core/agents/integrations.md +19 -125
- package/src/core/agents/mobile.md +19 -125
- package/src/core/agents/monitoring.md +19 -125
- package/src/core/agents/performance.md +19 -125
- package/src/core/agents/product.md +18 -1
- package/src/core/agents/qa.md +21 -125
- package/src/core/agents/readme-updater.md +18 -1
- package/src/core/agents/refactor.md +19 -125
- package/src/core/agents/research.md +3 -1
- package/src/core/agents/rlm-subcore.md +202 -0
- package/src/core/agents/security.md +7 -125
- package/src/core/agents/testing.md +20 -125
- package/src/core/agents/ui.md +14 -135
- package/src/core/commands/adr/list.md +20 -0
- package/src/core/commands/adr/update.md +24 -1
- package/src/core/commands/adr/view.md +23 -1
- package/src/core/commands/adr.md +2 -2
- package/src/core/commands/agent.md +11 -1
- package/src/core/commands/assign.md +15 -6
- package/src/core/commands/auto.md +11 -1
- package/src/core/commands/babysit.md +15 -4
- package/src/core/commands/baseline.md +11 -1
- package/src/core/commands/batch.md +11 -1
- package/src/core/commands/blockers.md +11 -1
- package/src/core/commands/board.md +11 -1
- package/src/core/commands/changelog.md +11 -0
- package/src/core/commands/choose.md +16 -1
- package/src/core/commands/ci.md +11 -1
- package/src/core/commands/configure.md +73 -2
- package/src/core/commands/context/export.md +8 -0
- package/src/core/commands/context/full.md +8 -0
- package/src/core/commands/context/note.md +8 -0
- package/src/core/commands/debt.md +11 -0
- package/src/core/commands/deploy.md +10 -0
- package/src/core/commands/deps.md +11 -1
- package/src/core/commands/diagnose.md +10 -0
- package/src/core/commands/docs.md +12 -2
- package/src/core/commands/epic/list.md +20 -0
- package/src/core/commands/epic/view.md +25 -0
- package/src/core/commands/epic.md +5 -6
- package/src/core/commands/feedback.md +11 -0
- package/src/core/commands/handoff.md +12 -2
- package/src/core/commands/help.md +10 -0
- package/src/core/commands/ideate.md +10 -0
- package/src/core/commands/impact.md +11 -1
- package/src/core/commands/metrics.md +11 -1
- package/src/core/commands/multi-expert.md +11 -1
- package/src/core/commands/packages.md +11 -0
- package/src/core/commands/pr.md +10 -0
- package/src/core/commands/readme-sync.md +10 -5
- package/src/core/commands/research/analyze.md +60 -3
- package/src/core/commands/research/ask.md +9 -1
- package/src/core/commands/research/import.md +8 -0
- package/src/core/commands/research/list.md +8 -0
- package/src/core/commands/research/synthesize.md +9 -1
- package/src/core/commands/research/view.md +8 -0
- package/src/core/commands/retro.md +12 -2
- package/src/core/commands/review.md +11 -1
- package/src/core/commands/rlm.md +363 -0
- package/src/core/commands/roadmap/analyze.md +1 -1
- package/src/core/commands/rpi.md +9 -1
- package/src/core/commands/session/cleanup.md +250 -0
- package/src/core/commands/session/end.md +10 -0
- package/src/core/commands/session/history.md +11 -1
- package/src/core/commands/session/init.md +10 -0
- package/src/core/commands/session/new.md +132 -13
- package/src/core/commands/session/resume.md +10 -0
- package/src/core/commands/session/spawn.md +8 -0
- package/src/core/commands/session/status.md +10 -0
- package/src/core/commands/skill/create.md +1 -1
- package/src/core/commands/skill/delete.md +11 -1
- package/src/core/commands/skill/edit.md +11 -1
- package/src/core/commands/skill/test.md +11 -1
- package/src/core/commands/skill/upgrade.md +11 -1
- package/src/core/commands/sprint.md +14 -3
- package/src/core/commands/status.md +15 -6
- package/src/core/commands/story/list.md +23 -0
- package/src/core/commands/story/view.md +24 -0
- package/src/core/commands/story.md +4 -5
- package/src/core/commands/template.md +10 -0
- package/src/core/commands/tests.md +10 -0
- package/src/core/commands/update.md +10 -0
- package/src/core/commands/validate-expertise.md +10 -1
- package/src/core/commands/velocity.md +11 -1
- package/src/core/commands/verify.md +13 -1
- package/src/core/commands/whats-new.md +8 -0
- package/src/core/commands/workflow.md +16 -1
- package/src/core/templates/agent-coordination-pattern.md +38 -0
- package/src/core/templates/agileflow-metadata.json +25 -0
- package/src/core/templates/preserve-rules-common.md +107 -0
- package/src/core/templates/preserve-rules.json +42 -0
- package/src/core/templates/proactive-action-spec.md +29 -0
- package/src/core/templates/quality-gate-priorities.md +34 -0
- package/src/core/templates/session-harness-protocol.md +128 -0
- package/tools/cli/commands/setup.js +12 -3
- package/tools/cli/installers/ide/windsurf.js +1 -1
- package/tools/cli/lib/content-injector.js +336 -0
- package/tools/cli/lib/ide-registry.js +2 -4
- package/tools/cli/lib/ui.js +2 -1
|
@@ -65,7 +65,14 @@ const PROFILES = {
|
|
|
65
65
|
minimal: {
|
|
66
66
|
description: 'SessionStart + archival only',
|
|
67
67
|
enable: ['sessionstart', 'archival'],
|
|
68
|
-
disable: [
|
|
68
|
+
disable: [
|
|
69
|
+
'precompact',
|
|
70
|
+
'statusline',
|
|
71
|
+
'ralphloop',
|
|
72
|
+
'selfimprove',
|
|
73
|
+
'askuserquestion',
|
|
74
|
+
'tmuxautospawn',
|
|
75
|
+
],
|
|
69
76
|
archivalDays: 30,
|
|
70
77
|
},
|
|
71
78
|
none: {
|
|
@@ -63,22 +63,22 @@ const SAFEEXEC_ALLOWED_COMMANDS = [
|
|
|
63
63
|
* Dangerous patterns that should never be executed
|
|
64
64
|
*/
|
|
65
65
|
const SAFEEXEC_BLOCKED_PATTERNS = [
|
|
66
|
-
/\|/,
|
|
67
|
-
/;/,
|
|
68
|
-
/&&/,
|
|
69
|
-
/\|\|/,
|
|
70
|
-
/`/,
|
|
71
|
-
/\$\(/,
|
|
72
|
-
/>/,
|
|
73
|
-
/</,
|
|
74
|
-
/\bsudo\b/,
|
|
75
|
-
/\brm\b/,
|
|
76
|
-
/\bmv\b/,
|
|
77
|
-
/\bcp\b/,
|
|
78
|
-
/\bchmod\b/,
|
|
79
|
-
/\bchown\b/,
|
|
80
|
-
/\bcurl\b/,
|
|
81
|
-
/\bwget\b/,
|
|
66
|
+
/\|/, // Pipe
|
|
67
|
+
/;/, // Command separator
|
|
68
|
+
/&&/, // AND operator
|
|
69
|
+
/\|\|/, // OR operator
|
|
70
|
+
/`/, // Backticks
|
|
71
|
+
/\$\(/, // Command substitution
|
|
72
|
+
/>/, // Redirect output
|
|
73
|
+
/</, // Redirect input
|
|
74
|
+
/\bsudo\b/, // Sudo
|
|
75
|
+
/\brm\b/, // Remove
|
|
76
|
+
/\bmv\b/, // Move
|
|
77
|
+
/\bcp\b/, // Copy
|
|
78
|
+
/\bchmod\b/, // Change permissions
|
|
79
|
+
/\bchown\b/, // Change owner
|
|
80
|
+
/\bcurl\b/, // curl (network)
|
|
81
|
+
/\bwget\b/, // wget (network)
|
|
82
82
|
];
|
|
83
83
|
|
|
84
84
|
/**
|
|
@@ -114,7 +114,7 @@ function explainWorkflow(queryType, queryValue, projectRoot) {
|
|
|
114
114
|
lines.push('# This tool adds: index awareness, budget truncation, structured output.');
|
|
115
115
|
break;
|
|
116
116
|
|
|
117
|
-
case 'tag':
|
|
117
|
+
case 'tag': {
|
|
118
118
|
const tagPatterns = {
|
|
119
119
|
api: '/api/|/routes/|/controllers/',
|
|
120
120
|
ui: '/components/|/views/|/pages/',
|
|
@@ -123,10 +123,13 @@ function explainWorkflow(queryType, queryValue, projectRoot) {
|
|
|
123
123
|
test: '/test/|/__tests__/|/spec/',
|
|
124
124
|
};
|
|
125
125
|
lines.push('# Equivalent to find with path patterns:');
|
|
126
|
-
lines.push(
|
|
126
|
+
lines.push(
|
|
127
|
+
`find ${projectRoot} -type f | grep -E "${tagPatterns[queryValue] || queryValue}"`
|
|
128
|
+
);
|
|
127
129
|
lines.push('');
|
|
128
130
|
lines.push('# This tool uses pre-indexed tags for instant lookup.');
|
|
129
131
|
break;
|
|
132
|
+
}
|
|
130
133
|
|
|
131
134
|
case 'export':
|
|
132
135
|
lines.push('# Equivalent to grep for export statements:');
|
|
@@ -140,7 +143,9 @@ function explainWorkflow(queryType, queryValue, projectRoot) {
|
|
|
140
143
|
lines.push(`grep -n "import.*from" ${queryValue}`);
|
|
141
144
|
lines.push('');
|
|
142
145
|
lines.push('# Plus reverse search for files importing this one:');
|
|
143
|
-
lines.push(
|
|
146
|
+
lines.push(
|
|
147
|
+
`grep -rl "${path.basename(queryValue, path.extname(queryValue))}" ${projectRoot}/src/`
|
|
148
|
+
);
|
|
144
149
|
lines.push('');
|
|
145
150
|
lines.push('# This tool tracks bidirectional dependencies in index.');
|
|
146
151
|
break;
|
|
@@ -11,11 +11,16 @@
|
|
|
11
11
|
|
|
12
12
|
const fs = require('fs');
|
|
13
13
|
const path = require('path');
|
|
14
|
-
const { execSync, spawnSync } = require('child_process');
|
|
14
|
+
const { execSync, spawnSync, spawn } = require('child_process');
|
|
15
15
|
|
|
16
16
|
// Shared utilities
|
|
17
17
|
const { c } = require('../lib/colors');
|
|
18
|
-
const {
|
|
18
|
+
const {
|
|
19
|
+
getProjectRoot,
|
|
20
|
+
getStatusPath,
|
|
21
|
+
getSessionStatePath,
|
|
22
|
+
getAgileflowDir,
|
|
23
|
+
} = require('../lib/paths');
|
|
19
24
|
const { safeReadJSON } = require('../lib/errors');
|
|
20
25
|
const { isValidBranchName, isValidSessionNickname } = require('../lib/validate');
|
|
21
26
|
|
|
@@ -226,6 +231,161 @@ async function cleanupStaleLocksAsync(registry, options = {}) {
|
|
|
226
231
|
return { count: cleanedSessions.length, sessions: cleanedSessions };
|
|
227
232
|
}
|
|
228
233
|
|
|
234
|
+
/**
|
|
235
|
+
* Get detailed file information for a session's changes
|
|
236
|
+
* @param {string} sessionPath - Path to session worktree
|
|
237
|
+
* @param {string[]} changes - Array of git status lines
|
|
238
|
+
* @returns {Object[]} Array of file details with analysis
|
|
239
|
+
*/
|
|
240
|
+
function getFileDetails(sessionPath, changes) {
|
|
241
|
+
return changes.map((change) => {
|
|
242
|
+
const status = change.substring(0, 2).trim();
|
|
243
|
+
const file = change.substring(3);
|
|
244
|
+
|
|
245
|
+
const detail = { status, file, trivial: false, existsInMain: false, diffLines: 0 };
|
|
246
|
+
|
|
247
|
+
// For modified files, get diff stats
|
|
248
|
+
if (status === 'M') {
|
|
249
|
+
try {
|
|
250
|
+
const diffStat = spawnSync('git', ['diff', '--numstat', file], {
|
|
251
|
+
cwd: sessionPath,
|
|
252
|
+
encoding: 'utf8',
|
|
253
|
+
timeout: 3000,
|
|
254
|
+
});
|
|
255
|
+
if (diffStat.stdout) {
|
|
256
|
+
const parts = diffStat.stdout.trim().split('\t');
|
|
257
|
+
const added = parseInt(parts[0], 10) || 0;
|
|
258
|
+
const removed = parseInt(parts[1], 10) || 0;
|
|
259
|
+
detail.diffLines = added + removed;
|
|
260
|
+
// Trivial if only 1-2 lines changed (likely whitespace)
|
|
261
|
+
detail.trivial = detail.diffLines <= 2;
|
|
262
|
+
}
|
|
263
|
+
} catch (e) {
|
|
264
|
+
// Can't get diff, assume not trivial
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// For untracked files, check if exists in main
|
|
269
|
+
if (status === '??') {
|
|
270
|
+
detail.existsInMain = fs.existsSync(path.join(ROOT, file));
|
|
271
|
+
// Trivial if it's a duplicate
|
|
272
|
+
detail.trivial = detail.existsInMain;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Config/cache files are trivial
|
|
276
|
+
if (file.includes('.claude/') || file.includes('.agileflow/cache')) {
|
|
277
|
+
detail.trivial = true;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return detail;
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Get health status for all sessions
|
|
286
|
+
* Detects: stale sessions, uncommitted changes, orphaned entries
|
|
287
|
+
* @param {Object} options - { staleDays: 7, detailed: false }
|
|
288
|
+
* @returns {Object} Health report
|
|
289
|
+
*/
|
|
290
|
+
function getSessionsHealth(options = {}) {
|
|
291
|
+
const { staleDays = 7, detailed = false } = options;
|
|
292
|
+
const registry = loadRegistry();
|
|
293
|
+
const now = Date.now();
|
|
294
|
+
const staleThreshold = staleDays * 24 * 60 * 60 * 1000;
|
|
295
|
+
|
|
296
|
+
const health = {
|
|
297
|
+
stale: [], // Sessions with no activity > staleDays
|
|
298
|
+
uncommitted: [], // Sessions with uncommitted git changes
|
|
299
|
+
orphanedRegistry: [], // Registry entries where path doesn't exist
|
|
300
|
+
orphanedWorktrees: [], // Worktrees not in registry
|
|
301
|
+
healthy: 0,
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
// Check each registered session
|
|
305
|
+
for (const [id, session] of Object.entries(registry.sessions)) {
|
|
306
|
+
if (session.is_main) continue; // Skip main session
|
|
307
|
+
|
|
308
|
+
const age = now - new Date(session.last_active).getTime();
|
|
309
|
+
const pathExists = fs.existsSync(session.path);
|
|
310
|
+
|
|
311
|
+
// Check for orphaned registry entry (path missing)
|
|
312
|
+
if (!pathExists) {
|
|
313
|
+
health.orphanedRegistry.push({ id, ...session, reason: 'path_missing' });
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Check for stale session
|
|
318
|
+
if (age > staleThreshold) {
|
|
319
|
+
health.stale.push({
|
|
320
|
+
id,
|
|
321
|
+
...session,
|
|
322
|
+
ageDays: Math.floor(age / (24 * 60 * 60 * 1000)),
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Check for uncommitted changes
|
|
327
|
+
try {
|
|
328
|
+
const result = spawnSync('git', ['status', '--porcelain'], {
|
|
329
|
+
cwd: session.path,
|
|
330
|
+
encoding: 'utf8',
|
|
331
|
+
timeout: 5000,
|
|
332
|
+
});
|
|
333
|
+
if (result.stdout && result.stdout.trim()) {
|
|
334
|
+
// Don't use trim() on the whole string - it removes leading space from first status
|
|
335
|
+
// Split by newline and filter empty lines instead
|
|
336
|
+
const changes = result.stdout.split('\n').filter((line) => line.length > 0);
|
|
337
|
+
const sessionData = {
|
|
338
|
+
id,
|
|
339
|
+
...session,
|
|
340
|
+
changeCount: changes.length,
|
|
341
|
+
changes: detailed ? changes : changes.slice(0, 5), // All or first 5
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
// Add detailed file analysis if requested
|
|
345
|
+
if (detailed) {
|
|
346
|
+
sessionData.fileDetails = getFileDetails(session.path, changes);
|
|
347
|
+
// Calculate if session is safe to delete (all changes trivial)
|
|
348
|
+
sessionData.allTrivial = sessionData.fileDetails.every((f) => f.trivial);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
health.uncommitted.push(sessionData);
|
|
352
|
+
} else {
|
|
353
|
+
health.healthy++;
|
|
354
|
+
}
|
|
355
|
+
} catch (e) {
|
|
356
|
+
// Can't check, skip
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Check for orphaned worktrees (directories not in registry)
|
|
361
|
+
try {
|
|
362
|
+
const worktreeList = spawnSync('git', ['worktree', 'list', '--porcelain'], {
|
|
363
|
+
encoding: 'utf8',
|
|
364
|
+
});
|
|
365
|
+
if (worktreeList.stdout) {
|
|
366
|
+
const worktrees = worktreeList.stdout
|
|
367
|
+
.split('\n')
|
|
368
|
+
.filter((line) => line.startsWith('worktree '))
|
|
369
|
+
.map((line) => line.replace('worktree ', ''));
|
|
370
|
+
|
|
371
|
+
const mainPath = ROOT;
|
|
372
|
+
for (const wtPath of worktrees) {
|
|
373
|
+
const inRegistry = Object.values(registry.sessions).some((s) => s.path === wtPath);
|
|
374
|
+
if (!inRegistry && wtPath !== mainPath) {
|
|
375
|
+
// Check if it's an AgileFlow worktree (has .agileflow folder)
|
|
376
|
+
if (fs.existsSync(path.join(wtPath, '.agileflow'))) {
|
|
377
|
+
health.orphanedWorktrees.push({ path: wtPath });
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
} catch (e) {
|
|
383
|
+
// Can't list worktrees, skip
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return health;
|
|
387
|
+
}
|
|
388
|
+
|
|
229
389
|
// Git command cache (10 second TTL to avoid stale data)
|
|
230
390
|
const gitCache = {
|
|
231
391
|
data: new Map(),
|
|
@@ -380,8 +540,165 @@ function getSession(sessionId) {
|
|
|
380
540
|
};
|
|
381
541
|
}
|
|
382
542
|
|
|
543
|
+
// Default worktree timeout (2 minutes)
|
|
544
|
+
const DEFAULT_WORKTREE_TIMEOUT_MS = 120000;
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Display progress feedback during long operations.
|
|
548
|
+
* Returns a function to stop the progress indicator.
|
|
549
|
+
*
|
|
550
|
+
* @param {string} message - Progress message
|
|
551
|
+
* @returns {function} Stop function
|
|
552
|
+
*/
|
|
553
|
+
function progressIndicator(message) {
|
|
554
|
+
const frames = ['โ ', 'โ ', 'โ น', 'โ ธ', 'โ ผ', 'โ ด', 'โ ฆ', 'โ ง', 'โ ', 'โ '];
|
|
555
|
+
let frameIndex = 0;
|
|
556
|
+
let elapsed = 0;
|
|
557
|
+
|
|
558
|
+
// For TTY (interactive terminal), show spinner
|
|
559
|
+
if (process.stderr.isTTY) {
|
|
560
|
+
const interval = setInterval(() => {
|
|
561
|
+
process.stderr.write(`\r${frames[frameIndex++ % frames.length]} ${message}`);
|
|
562
|
+
}, 80);
|
|
563
|
+
return () => {
|
|
564
|
+
clearInterval(interval);
|
|
565
|
+
process.stderr.write(`\r${' '.repeat(message.length + 2)}\r`);
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// For non-TTY (Claude Code, piped output), emit periodic updates to stderr
|
|
570
|
+
process.stderr.write(`โณ ${message}...\n`);
|
|
571
|
+
const interval = setInterval(() => {
|
|
572
|
+
elapsed += 10;
|
|
573
|
+
process.stderr.write(`โณ Still working... (${elapsed}s elapsed)\n`);
|
|
574
|
+
}, 10000); // Update every 10 seconds
|
|
575
|
+
|
|
576
|
+
return () => {
|
|
577
|
+
clearInterval(interval);
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Create a git worktree with timeout and progress feedback.
|
|
583
|
+
* Uses async spawn instead of spawnSync for timeout support.
|
|
584
|
+
*
|
|
585
|
+
* @param {string} worktreePath - Path for the new worktree
|
|
586
|
+
* @param {string} branchName - Branch name for the worktree
|
|
587
|
+
* @param {number} timeoutMs - Timeout in milliseconds
|
|
588
|
+
* @returns {Promise<{stdout: string, stderr: string}>}
|
|
589
|
+
*/
|
|
590
|
+
function createWorktreeWithTimeout(
|
|
591
|
+
worktreePath,
|
|
592
|
+
branchName,
|
|
593
|
+
timeoutMs = DEFAULT_WORKTREE_TIMEOUT_MS
|
|
594
|
+
) {
|
|
595
|
+
return new Promise((resolve, reject) => {
|
|
596
|
+
let stdout = '';
|
|
597
|
+
let stderr = '';
|
|
598
|
+
let timedOut = false;
|
|
599
|
+
|
|
600
|
+
const proc = spawn('git', ['worktree', 'add', worktreePath, branchName], {
|
|
601
|
+
cwd: ROOT,
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
const timer = setTimeout(() => {
|
|
605
|
+
timedOut = true;
|
|
606
|
+
proc.kill('SIGTERM');
|
|
607
|
+
// Give it a moment to terminate gracefully, then SIGKILL
|
|
608
|
+
setTimeout(() => {
|
|
609
|
+
try {
|
|
610
|
+
proc.kill('SIGKILL');
|
|
611
|
+
} catch (e) {
|
|
612
|
+
// Process may have already exited
|
|
613
|
+
}
|
|
614
|
+
}, 1000);
|
|
615
|
+
}, timeoutMs);
|
|
616
|
+
|
|
617
|
+
proc.stdout.on('data', data => {
|
|
618
|
+
stdout += data.toString();
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
proc.stderr.on('data', data => {
|
|
622
|
+
stderr += data.toString();
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
proc.on('error', err => {
|
|
626
|
+
clearTimeout(timer);
|
|
627
|
+
reject(new Error(`Failed to spawn git: ${err.message}`));
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
proc.on('close', (code, signal) => {
|
|
631
|
+
clearTimeout(timer);
|
|
632
|
+
|
|
633
|
+
if (timedOut) {
|
|
634
|
+
reject(
|
|
635
|
+
new Error(
|
|
636
|
+
`Worktree creation timed out after ${timeoutMs / 1000}s. Try increasing timeout or check disk space.`
|
|
637
|
+
)
|
|
638
|
+
);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (signal) {
|
|
643
|
+
reject(new Error(`Worktree creation was terminated by signal: ${signal}`));
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (code === 0) {
|
|
648
|
+
resolve({ stdout, stderr });
|
|
649
|
+
} else {
|
|
650
|
+
reject(new Error(`Failed to create worktree: ${stderr || 'unknown error'}`));
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Clean up partial state after failed worktree creation.
|
|
658
|
+
* Removes partial directory and prunes git worktree registry.
|
|
659
|
+
*
|
|
660
|
+
* @param {string} worktreePath - Path of the failed worktree
|
|
661
|
+
* @param {string} branchName - Branch name that was being used
|
|
662
|
+
* @param {boolean} branchCreatedByUs - Whether we created the branch
|
|
663
|
+
*/
|
|
664
|
+
function cleanupFailedWorktree(worktreePath, branchName, branchCreatedByUs = false) {
|
|
665
|
+
// Remove partial worktree directory if it exists
|
|
666
|
+
if (fs.existsSync(worktreePath)) {
|
|
667
|
+
try {
|
|
668
|
+
fs.rmSync(worktreePath, { recursive: true, force: true });
|
|
669
|
+
process.stderr.write(`๐งน Cleaned up partial worktree directory\n`);
|
|
670
|
+
} catch (e) {
|
|
671
|
+
process.stderr.write(`โ ๏ธ Could not remove partial directory: ${e.message}\n`);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Prune git worktree registry to clean up any references
|
|
676
|
+
try {
|
|
677
|
+
spawnSync('git', ['worktree', 'prune'], { cwd: ROOT, encoding: 'utf8' });
|
|
678
|
+
} catch (e) {
|
|
679
|
+
// Non-fatal
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// If we created the branch and the worktree failed, optionally clean up the branch too
|
|
683
|
+
// But only if it has no commits beyond the parent (i.e., we just created it)
|
|
684
|
+
if (branchCreatedByUs) {
|
|
685
|
+
try {
|
|
686
|
+
// Check if branch exists and has no unique commits
|
|
687
|
+
const result = spawnSync('git', ['branch', '-d', branchName], {
|
|
688
|
+
cwd: ROOT,
|
|
689
|
+
encoding: 'utf8',
|
|
690
|
+
});
|
|
691
|
+
if (result.status === 0) {
|
|
692
|
+
process.stderr.write(`๐งน Cleaned up unused branch: ${branchName}\n`);
|
|
693
|
+
}
|
|
694
|
+
} catch (e) {
|
|
695
|
+
// Non-fatal - branch may have commits or not exist
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
383
700
|
// Create new session with worktree
|
|
384
|
-
function createSession(options = {}) {
|
|
701
|
+
async function createSession(options = {}) {
|
|
385
702
|
const registry = loadRegistry();
|
|
386
703
|
const sessionId = String(registry.next_id);
|
|
387
704
|
const projectName = registry.project_name;
|
|
@@ -426,6 +743,7 @@ function createSession(options = {}) {
|
|
|
426
743
|
}
|
|
427
744
|
);
|
|
428
745
|
|
|
746
|
+
let branchCreatedByUs = false;
|
|
429
747
|
if (checkRef.status !== 0) {
|
|
430
748
|
// Branch doesn't exist, create it
|
|
431
749
|
const createBranch = spawnSync('git', ['branch', branchName], {
|
|
@@ -439,18 +757,27 @@ function createSession(options = {}) {
|
|
|
439
757
|
error: `Failed to create branch: ${createBranch.stderr || 'unknown error'}`,
|
|
440
758
|
};
|
|
441
759
|
}
|
|
760
|
+
branchCreatedByUs = true;
|
|
442
761
|
}
|
|
443
762
|
|
|
444
|
-
//
|
|
445
|
-
const
|
|
446
|
-
cwd: ROOT,
|
|
447
|
-
encoding: 'utf8',
|
|
448
|
-
});
|
|
763
|
+
// Get timeout from options (default: 2 minutes)
|
|
764
|
+
const timeoutMs = options.timeout || DEFAULT_WORKTREE_TIMEOUT_MS;
|
|
449
765
|
|
|
450
|
-
|
|
766
|
+
// Create worktree with timeout and progress feedback
|
|
767
|
+
const stopProgress = progressIndicator(
|
|
768
|
+
'Creating worktree (this may take a while for large repos)'
|
|
769
|
+
);
|
|
770
|
+
try {
|
|
771
|
+
await createWorktreeWithTimeout(worktreePath, branchName, timeoutMs);
|
|
772
|
+
stopProgress();
|
|
773
|
+
process.stderr.write(`โ Worktree created successfully\n`);
|
|
774
|
+
} catch (error) {
|
|
775
|
+
stopProgress();
|
|
776
|
+
// Clean up partial state
|
|
777
|
+
cleanupFailedWorktree(worktreePath, branchName, branchCreatedByUs);
|
|
451
778
|
return {
|
|
452
779
|
success: false,
|
|
453
|
-
error:
|
|
780
|
+
error: error.message,
|
|
454
781
|
};
|
|
455
782
|
}
|
|
456
783
|
|
|
@@ -471,9 +798,10 @@ function createSession(options = {}) {
|
|
|
471
798
|
}
|
|
472
799
|
}
|
|
473
800
|
|
|
474
|
-
// Copy Claude Code
|
|
801
|
+
// Copy Claude Code, AgileFlow config, and docs folders (gitignored contents won't copy with worktree)
|
|
475
802
|
// Note: The folder may exist with some tracked files, but gitignored subfolders (commands/, agents/) won't be there
|
|
476
|
-
|
|
803
|
+
// docs/ contains gitignored state files like status.json, session-state.json that need to be shared
|
|
804
|
+
const configFolders = ['.claude', '.agileflow', 'docs'];
|
|
477
805
|
const copiedFolders = [];
|
|
478
806
|
for (const folder of configFolders) {
|
|
479
807
|
const src = path.join(ROOT, folder);
|
|
@@ -1115,7 +1443,7 @@ function main() {
|
|
|
1115
1443
|
case 'create': {
|
|
1116
1444
|
const options = {};
|
|
1117
1445
|
// SECURITY: Only accept whitelisted option keys
|
|
1118
|
-
const allowedKeys = ['nickname', 'branch'];
|
|
1446
|
+
const allowedKeys = ['nickname', 'branch', 'timeout'];
|
|
1119
1447
|
for (let i = 1; i < args.length; i++) {
|
|
1120
1448
|
const arg = args[i];
|
|
1121
1449
|
if (arg.startsWith('--')) {
|
|
@@ -1133,8 +1461,27 @@ function main() {
|
|
|
1133
1461
|
}
|
|
1134
1462
|
}
|
|
1135
1463
|
}
|
|
1136
|
-
|
|
1137
|
-
|
|
1464
|
+
// Parse timeout as number (milliseconds)
|
|
1465
|
+
if (options.timeout) {
|
|
1466
|
+
options.timeout = parseInt(options.timeout, 10);
|
|
1467
|
+
if (isNaN(options.timeout) || options.timeout < 1000) {
|
|
1468
|
+
console.log(
|
|
1469
|
+
JSON.stringify({
|
|
1470
|
+
success: false,
|
|
1471
|
+
error: 'Timeout must be a number >= 1000 (milliseconds)',
|
|
1472
|
+
})
|
|
1473
|
+
);
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
// Handle async createSession
|
|
1478
|
+
createSession(options)
|
|
1479
|
+
.then(result => {
|
|
1480
|
+
console.log(JSON.stringify(result));
|
|
1481
|
+
})
|
|
1482
|
+
.catch(err => {
|
|
1483
|
+
console.log(JSON.stringify({ success: false, error: err.message }));
|
|
1484
|
+
});
|
|
1138
1485
|
break;
|
|
1139
1486
|
}
|
|
1140
1487
|
|
|
@@ -1186,6 +1533,17 @@ function main() {
|
|
|
1186
1533
|
break;
|
|
1187
1534
|
}
|
|
1188
1535
|
|
|
1536
|
+
case 'health': {
|
|
1537
|
+
// Get health status for all sessions
|
|
1538
|
+
// Usage: health [staleDays] [--detailed]
|
|
1539
|
+
const staleDaysArg = args.find((a) => /^\d+$/.test(a));
|
|
1540
|
+
const staleDays = staleDaysArg ? parseInt(staleDaysArg, 10) : 7;
|
|
1541
|
+
const detailed = args.includes('--detailed');
|
|
1542
|
+
const health = getSessionsHealth({ staleDays, detailed });
|
|
1543
|
+
console.log(JSON.stringify(health));
|
|
1544
|
+
break;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1189
1547
|
case 'get': {
|
|
1190
1548
|
const sessionId = args[1];
|
|
1191
1549
|
if (!sessionId) {
|
|
@@ -1448,7 +1806,7 @@ ${c.brand}${c.bold}Session Manager${c.reset} - Multi-session coordination for Cl
|
|
|
1448
1806
|
${c.cyan}Commands:${c.reset}
|
|
1449
1807
|
register [nickname] Register current directory as a session
|
|
1450
1808
|
unregister <id> Unregister a session (remove lock)
|
|
1451
|
-
create [--nickname X]
|
|
1809
|
+
create [--nickname X] [--timeout MS] Create session with worktree (default timeout: 120000ms)
|
|
1452
1810
|
list [--json] List all sessions
|
|
1453
1811
|
count Count other active sessions
|
|
1454
1812
|
delete <id> [--remove-worktree] Delete session
|