agileflow 2.91.0 → 2.92.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 +5 -0
- package/README.md +3 -3
- package/lib/README.md +178 -0
- package/lib/codebase-indexer.js +31 -23
- package/lib/colors.js +190 -12
- package/lib/consent.js +232 -0
- package/lib/correlation.js +277 -0
- package/lib/error-codes.js +46 -0
- package/lib/errors.js +48 -6
- package/lib/file-cache.js +182 -0
- package/lib/format-error.js +156 -0
- package/lib/path-resolver.js +155 -7
- package/lib/paths.js +212 -20
- package/lib/placeholder-registry.js +205 -0
- package/lib/registry-di.js +358 -0
- package/lib/result-schema.js +363 -0
- package/lib/result.js +210 -0
- package/lib/session-registry.js +13 -0
- package/lib/session-state-machine.js +465 -0
- package/lib/validate-commands.js +308 -0
- package/lib/validate.js +116 -52
- package/package.json +1 -1
- package/scripts/af +34 -0
- package/scripts/agent-loop.js +63 -9
- package/scripts/agileflow-configure.js +2 -2
- package/scripts/agileflow-welcome.js +435 -23
- package/scripts/archive-completed-stories.sh +57 -11
- package/scripts/claude-tmux.sh +102 -0
- package/scripts/damage-control-bash.js +3 -70
- package/scripts/damage-control-edit.js +3 -20
- package/scripts/damage-control-write.js +3 -20
- package/scripts/dependency-check.js +310 -0
- package/scripts/get-env.js +11 -4
- package/scripts/lib/configure-detect.js +23 -1
- package/scripts/lib/configure-features.js +43 -2
- package/scripts/lib/context-formatter.js +771 -0
- package/scripts/lib/context-loader.js +699 -0
- package/scripts/lib/damage-control-utils.js +107 -0
- package/scripts/lib/json-utils.sh +162 -0
- package/scripts/lib/state-migrator.js +353 -0
- package/scripts/lib/story-state-machine.js +437 -0
- package/scripts/obtain-context.js +80 -1248
- package/scripts/pre-push-check.sh +46 -0
- package/scripts/precompact-context.sh +23 -10
- package/scripts/query-codebase.js +122 -14
- package/scripts/ralph-loop.js +5 -5
- package/scripts/session-manager.js +220 -42
- package/scripts/spawn-parallel.js +651 -0
- package/scripts/tui/blessed/data/watcher.js +20 -15
- package/scripts/tui/blessed/index.js +2 -2
- package/scripts/tui/blessed/panels/output.js +14 -8
- package/scripts/tui/blessed/panels/sessions.js +22 -15
- package/scripts/tui/blessed/panels/trace.js +14 -8
- package/scripts/tui/blessed/ui/help.js +3 -3
- package/scripts/tui/blessed/ui/screen.js +4 -4
- package/scripts/tui/blessed/ui/statusbar.js +5 -9
- package/scripts/tui/blessed/ui/tabbar.js +11 -11
- package/scripts/validators/component-validator.js +41 -14
- package/scripts/validators/json-schema-validator.js +11 -4
- package/scripts/validators/markdown-validator.js +1 -2
- package/scripts/validators/migration-validator.js +17 -5
- package/scripts/validators/security-validator.js +137 -33
- package/scripts/validators/story-format-validator.js +31 -10
- package/scripts/validators/test-result-validator.js +19 -4
- package/scripts/validators/workflow-validator.js +12 -5
- package/src/core/agents/codebase-query.md +24 -0
- package/src/core/commands/adr.md +114 -0
- package/src/core/commands/agent.md +120 -0
- package/src/core/commands/assign.md +145 -0
- package/src/core/commands/babysit.md +32 -5
- package/src/core/commands/changelog.md +118 -0
- package/src/core/commands/configure.md +42 -6
- package/src/core/commands/diagnose.md +114 -0
- package/src/core/commands/epic.md +113 -0
- package/src/core/commands/handoff.md +128 -0
- package/src/core/commands/help.md +75 -0
- package/src/core/commands/pr.md +96 -0
- package/src/core/commands/roadmap/analyze.md +400 -0
- package/src/core/commands/session/new.md +113 -6
- package/src/core/commands/session/spawn.md +197 -0
- package/src/core/commands/sprint.md +22 -0
- package/src/core/commands/status.md +74 -0
- package/src/core/commands/story.md +143 -4
- package/src/core/templates/agileflow-metadata.json +55 -2
- package/src/core/templates/plan-template.md +125 -0
- package/src/core/templates/story-lifecycle.md +213 -0
- package/src/core/templates/story-template.md +4 -0
- package/src/core/templates/tdd-test-template.js +241 -0
- package/tools/cli/commands/setup.js +86 -0
- package/tools/cli/installers/core/installer.js +94 -0
- package/tools/cli/installers/ide/_base-ide.js +20 -11
- package/tools/cli/installers/ide/codex.js +29 -47
- package/tools/cli/lib/config-manager.js +17 -2
- package/tools/cli/lib/content-transformer.js +271 -0
- package/tools/cli/lib/error-handler.js +14 -22
- package/tools/cli/lib/ide-error-factory.js +421 -0
- package/tools/cli/lib/ide-health-monitor.js +364 -0
- package/tools/cli/lib/ide-registry.js +114 -1
- package/tools/cli/lib/ui.js +14 -25
|
@@ -15,14 +15,38 @@ const { execSync, spawnSync } = require('child_process');
|
|
|
15
15
|
|
|
16
16
|
// Shared utilities
|
|
17
17
|
const { c } = require('../lib/colors');
|
|
18
|
-
const { getProjectRoot } = require('../lib/paths');
|
|
18
|
+
const { getProjectRoot, getStatusPath, getSessionStatePath, getAgileflowDir } = require('../lib/paths');
|
|
19
19
|
const { safeReadJSON } = require('../lib/errors');
|
|
20
20
|
const { isValidBranchName, isValidSessionNickname } = require('../lib/validate');
|
|
21
21
|
|
|
22
|
+
const { SessionRegistry } = require('../lib/session-registry');
|
|
23
|
+
|
|
22
24
|
const ROOT = getProjectRoot();
|
|
23
|
-
const SESSIONS_DIR = path.join(ROOT, '
|
|
25
|
+
const SESSIONS_DIR = path.join(getAgileflowDir(ROOT), 'sessions');
|
|
24
26
|
const REGISTRY_PATH = path.join(SESSIONS_DIR, 'registry.json');
|
|
25
27
|
|
|
28
|
+
// Injectable registry instance for testing
|
|
29
|
+
let _registryInstance = null;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get the registry instance (singleton, injectable for testing)
|
|
33
|
+
* @returns {SessionRegistry}
|
|
34
|
+
*/
|
|
35
|
+
function getRegistryInstance() {
|
|
36
|
+
if (!_registryInstance) {
|
|
37
|
+
_registryInstance = new SessionRegistry(ROOT);
|
|
38
|
+
}
|
|
39
|
+
return _registryInstance;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Inject a mock registry for testing
|
|
44
|
+
* @param {SessionRegistry|null} registry - Registry to inject, or null to reset
|
|
45
|
+
*/
|
|
46
|
+
function injectRegistry(registry) {
|
|
47
|
+
_registryInstance = registry;
|
|
48
|
+
}
|
|
49
|
+
|
|
26
50
|
// Ensure sessions directory exists
|
|
27
51
|
function ensureSessionsDir() {
|
|
28
52
|
if (!fs.existsSync(SESSIONS_DIR)) {
|
|
@@ -30,35 +54,25 @@ function ensureSessionsDir() {
|
|
|
30
54
|
}
|
|
31
55
|
}
|
|
32
56
|
|
|
33
|
-
// Load or create registry
|
|
57
|
+
// Load or create registry (uses injectable SessionRegistry)
|
|
58
|
+
// Preserves original behavior: saves default registry if file didn't exist
|
|
34
59
|
function loadRegistry() {
|
|
35
|
-
|
|
60
|
+
const registryInstance = getRegistryInstance();
|
|
61
|
+
const fileExistedBefore = fs.existsSync(registryInstance.registryPath);
|
|
62
|
+
const data = registryInstance.loadSync();
|
|
36
63
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
} catch (e) {
|
|
41
|
-
console.error(`${c.red}Error loading registry: ${e.message}${c.reset}`);
|
|
42
|
-
}
|
|
64
|
+
// If file didn't exist, save the default to disk (original behavior)
|
|
65
|
+
if (!fileExistedBefore) {
|
|
66
|
+
registryInstance.saveSync(data);
|
|
43
67
|
}
|
|
44
68
|
|
|
45
|
-
|
|
46
|
-
const registry = {
|
|
47
|
-
schema_version: '1.0.0',
|
|
48
|
-
next_id: 1,
|
|
49
|
-
project_name: path.basename(ROOT),
|
|
50
|
-
sessions: {},
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
saveRegistry(registry);
|
|
54
|
-
return registry;
|
|
69
|
+
return data;
|
|
55
70
|
}
|
|
56
71
|
|
|
57
|
-
// Save registry
|
|
58
|
-
function saveRegistry(
|
|
59
|
-
|
|
60
|
-
registry.
|
|
61
|
-
fs.writeFileSync(REGISTRY_PATH, JSON.stringify(registry, null, 2) + '\n');
|
|
72
|
+
// Save registry (uses injectable SessionRegistry)
|
|
73
|
+
function saveRegistry(registryData) {
|
|
74
|
+
const registry = getRegistryInstance();
|
|
75
|
+
return registry.saveSync(registryData);
|
|
62
76
|
}
|
|
63
77
|
|
|
64
78
|
// Check if PID is alive
|
|
@@ -77,7 +91,7 @@ function getLockPath(sessionId) {
|
|
|
77
91
|
return path.join(SESSIONS_DIR, `${sessionId}.lock`);
|
|
78
92
|
}
|
|
79
93
|
|
|
80
|
-
// Read lock file
|
|
94
|
+
// Read lock file (sync version for backward compatibility)
|
|
81
95
|
function readLock(sessionId) {
|
|
82
96
|
const lockPath = getLockPath(sessionId);
|
|
83
97
|
if (!fs.existsSync(lockPath)) return null;
|
|
@@ -95,6 +109,22 @@ function readLock(sessionId) {
|
|
|
95
109
|
}
|
|
96
110
|
}
|
|
97
111
|
|
|
112
|
+
// Read lock file (async version for parallel operations)
|
|
113
|
+
async function readLockAsync(sessionId) {
|
|
114
|
+
const lockPath = getLockPath(sessionId);
|
|
115
|
+
try {
|
|
116
|
+
const content = await fs.promises.readFile(lockPath, 'utf8');
|
|
117
|
+
const lock = {};
|
|
118
|
+
content.split('\n').forEach(line => {
|
|
119
|
+
const [key, value] = line.split('=');
|
|
120
|
+
if (key && value) lock[key.trim()] = value.trim();
|
|
121
|
+
});
|
|
122
|
+
return lock;
|
|
123
|
+
} catch (e) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
98
128
|
// Write lock file
|
|
99
129
|
function writeLock(sessionId, pid) {
|
|
100
130
|
const lockPath = getLockPath(sessionId);
|
|
@@ -117,7 +147,7 @@ function isSessionActive(sessionId) {
|
|
|
117
147
|
return isPidAlive(parseInt(lock.pid, 10));
|
|
118
148
|
}
|
|
119
149
|
|
|
120
|
-
// Clean up stale locks (with detailed tracking)
|
|
150
|
+
// Clean up stale locks (with detailed tracking) - sync version for backward compatibility
|
|
121
151
|
function cleanupStaleLocks(registry, options = {}) {
|
|
122
152
|
const { verbose = false, dryRun = false } = options;
|
|
123
153
|
let cleaned = 0;
|
|
@@ -152,10 +182,84 @@ function cleanupStaleLocks(registry, options = {}) {
|
|
|
152
182
|
return { count: cleaned, sessions: cleanedSessions };
|
|
153
183
|
}
|
|
154
184
|
|
|
155
|
-
//
|
|
185
|
+
// Clean up stale locks (async parallel version - faster for many sessions)
|
|
186
|
+
async function cleanupStaleLocksAsync(registry, options = {}) {
|
|
187
|
+
const { verbose = false, dryRun = false } = options;
|
|
188
|
+
const cleanedSessions = [];
|
|
189
|
+
|
|
190
|
+
const sessionEntries = Object.entries(registry.sessions);
|
|
191
|
+
if (sessionEntries.length === 0) {
|
|
192
|
+
return { count: 0, sessions: [] };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Read all locks in parallel
|
|
196
|
+
const lockResults = await Promise.all(
|
|
197
|
+
sessionEntries.map(async ([id, session]) => {
|
|
198
|
+
const lock = await readLockAsync(id);
|
|
199
|
+
return { id, session, lock };
|
|
200
|
+
})
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// Process results (sequential - fast since it's just memory operations)
|
|
204
|
+
for (const { id, session, lock } of lockResults) {
|
|
205
|
+
if (lock) {
|
|
206
|
+
const pid = parseInt(lock.pid, 10);
|
|
207
|
+
const isAlive = isPidAlive(pid);
|
|
208
|
+
|
|
209
|
+
if (!isAlive) {
|
|
210
|
+
cleanedSessions.push({
|
|
211
|
+
id,
|
|
212
|
+
nickname: session.nickname,
|
|
213
|
+
branch: session.branch,
|
|
214
|
+
pid,
|
|
215
|
+
reason: 'pid_dead',
|
|
216
|
+
path: session.path,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
if (!dryRun) {
|
|
220
|
+
removeLock(id);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return { count: cleanedSessions.length, sessions: cleanedSessions };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Git command cache (10 second TTL to avoid stale data)
|
|
230
|
+
const gitCache = {
|
|
231
|
+
data: new Map(),
|
|
232
|
+
ttlMs: 10000,
|
|
233
|
+
get(key) {
|
|
234
|
+
const entry = this.data.get(key);
|
|
235
|
+
if (entry && Date.now() - entry.timestamp < this.ttlMs) {
|
|
236
|
+
return entry.value;
|
|
237
|
+
}
|
|
238
|
+
this.data.delete(key);
|
|
239
|
+
return null;
|
|
240
|
+
},
|
|
241
|
+
set(key, value) {
|
|
242
|
+
this.data.set(key, { value, timestamp: Date.now() });
|
|
243
|
+
},
|
|
244
|
+
invalidate(key) {
|
|
245
|
+
if (key) {
|
|
246
|
+
this.data.delete(key);
|
|
247
|
+
} else {
|
|
248
|
+
this.data.clear();
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// Get current git branch (cached for performance)
|
|
156
254
|
function getCurrentBranch() {
|
|
255
|
+
const cacheKey = `branch:${ROOT}`;
|
|
256
|
+
const cached = gitCache.get(cacheKey);
|
|
257
|
+
if (cached !== null) return cached;
|
|
258
|
+
|
|
157
259
|
try {
|
|
158
|
-
|
|
260
|
+
const branch = execSync('git branch --show-current', { cwd: ROOT, encoding: 'utf8' }).trim();
|
|
261
|
+
gitCache.set(cacheKey, branch);
|
|
262
|
+
return branch;
|
|
159
263
|
} catch (e) {
|
|
160
264
|
return 'unknown';
|
|
161
265
|
}
|
|
@@ -163,7 +267,7 @@ function getCurrentBranch() {
|
|
|
163
267
|
|
|
164
268
|
// Get current story from status.json
|
|
165
269
|
function getCurrentStory() {
|
|
166
|
-
const statusPath =
|
|
270
|
+
const statusPath = getStatusPath(ROOT);
|
|
167
271
|
const result = safeReadJSON(statusPath, { defaultValue: null });
|
|
168
272
|
|
|
169
273
|
if (!result.ok || !result.data) return null;
|
|
@@ -350,6 +454,42 @@ function createSession(options = {}) {
|
|
|
350
454
|
};
|
|
351
455
|
}
|
|
352
456
|
|
|
457
|
+
// Copy environment files to new worktree (they don't copy automatically)
|
|
458
|
+
const envFiles = ['.env', '.env.local', '.env.development', '.env.test', '.env.production'];
|
|
459
|
+
const copiedEnvFiles = [];
|
|
460
|
+
for (const envFile of envFiles) {
|
|
461
|
+
const src = path.join(ROOT, envFile);
|
|
462
|
+
const dest = path.join(worktreePath, envFile);
|
|
463
|
+
if (fs.existsSync(src) && !fs.existsSync(dest)) {
|
|
464
|
+
try {
|
|
465
|
+
fs.copyFileSync(src, dest);
|
|
466
|
+
copiedEnvFiles.push(envFile);
|
|
467
|
+
} catch (e) {
|
|
468
|
+
// Non-fatal: log but continue
|
|
469
|
+
console.warn(`Warning: Could not copy ${envFile}: ${e.message}`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Copy Claude Code and AgileFlow config folders (gitignored contents won't copy with worktree)
|
|
475
|
+
// Note: The folder may exist with some tracked files, but gitignored subfolders (commands/, agents/) won't be there
|
|
476
|
+
const configFolders = ['.claude', '.agileflow'];
|
|
477
|
+
const copiedFolders = [];
|
|
478
|
+
for (const folder of configFolders) {
|
|
479
|
+
const src = path.join(ROOT, folder);
|
|
480
|
+
const dest = path.join(worktreePath, folder);
|
|
481
|
+
if (fs.existsSync(src)) {
|
|
482
|
+
try {
|
|
483
|
+
// Use force to overwrite existing files, recursive for subdirs
|
|
484
|
+
fs.cpSync(src, dest, { recursive: true, force: true });
|
|
485
|
+
copiedFolders.push(folder);
|
|
486
|
+
} catch (e) {
|
|
487
|
+
// Non-fatal: log but continue
|
|
488
|
+
console.warn(`Warning: Could not copy ${folder}: ${e.message}`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
353
493
|
// Register session - worktree sessions are always parallel threads
|
|
354
494
|
registry.next_id++;
|
|
355
495
|
registry.sessions[sessionId] = {
|
|
@@ -372,6 +512,8 @@ function createSession(options = {}) {
|
|
|
372
512
|
branch: branchName,
|
|
373
513
|
thread_type: registry.sessions[sessionId].thread_type,
|
|
374
514
|
command: `cd "${worktreePath}" && claude`,
|
|
515
|
+
envFilesCopied: copiedEnvFiles,
|
|
516
|
+
foldersCopied: copiedFolders,
|
|
375
517
|
};
|
|
376
518
|
}
|
|
377
519
|
|
|
@@ -445,22 +587,33 @@ function deleteSession(sessionId, removeWorktree = false) {
|
|
|
445
587
|
return { success: true };
|
|
446
588
|
}
|
|
447
589
|
|
|
448
|
-
// Get main branch name (main or master)
|
|
590
|
+
// Get main branch name (main or master) - cached since it rarely changes
|
|
449
591
|
function getMainBranch() {
|
|
592
|
+
const cacheKey = `mainBranch:${ROOT}`;
|
|
593
|
+
const cached = gitCache.get(cacheKey);
|
|
594
|
+
if (cached !== null) return cached;
|
|
595
|
+
|
|
450
596
|
const checkMain = spawnSync('git', ['show-ref', '--verify', '--quiet', 'refs/heads/main'], {
|
|
451
597
|
cwd: ROOT,
|
|
452
598
|
encoding: 'utf8',
|
|
453
599
|
});
|
|
454
600
|
|
|
455
|
-
if (checkMain.status === 0)
|
|
601
|
+
if (checkMain.status === 0) {
|
|
602
|
+
gitCache.set(cacheKey, 'main');
|
|
603
|
+
return 'main';
|
|
604
|
+
}
|
|
456
605
|
|
|
457
606
|
const checkMaster = spawnSync('git', ['show-ref', '--verify', '--quiet', 'refs/heads/master'], {
|
|
458
607
|
cwd: ROOT,
|
|
459
608
|
encoding: 'utf8',
|
|
460
609
|
});
|
|
461
610
|
|
|
462
|
-
if (checkMaster.status === 0)
|
|
611
|
+
if (checkMaster.status === 0) {
|
|
612
|
+
gitCache.set(cacheKey, 'master');
|
|
613
|
+
return 'master';
|
|
614
|
+
}
|
|
463
615
|
|
|
616
|
+
gitCache.set(cacheKey, 'main');
|
|
464
617
|
return 'main'; // Default fallback
|
|
465
618
|
}
|
|
466
619
|
|
|
@@ -745,7 +898,7 @@ const SESSION_PHASES = {
|
|
|
745
898
|
MERGED: 'merged',
|
|
746
899
|
};
|
|
747
900
|
|
|
748
|
-
// Detect session phase based on git state
|
|
901
|
+
// Detect session phase based on git state (with caching for performance)
|
|
749
902
|
function getSessionPhase(session) {
|
|
750
903
|
// If merged_at field exists, session was merged
|
|
751
904
|
if (session.merged_at) {
|
|
@@ -764,6 +917,11 @@ function getSessionPhase(session) {
|
|
|
764
917
|
return SESSION_PHASES.TODO;
|
|
765
918
|
}
|
|
766
919
|
|
|
920
|
+
// Cache key for this session's git state
|
|
921
|
+
const cacheKey = `phase:${sessionPath}`;
|
|
922
|
+
const cached = gitCache.get(cacheKey);
|
|
923
|
+
if (cached !== null) return cached;
|
|
924
|
+
|
|
767
925
|
// Count commits since branch diverged from main
|
|
768
926
|
const mainBranch = getMainBranch();
|
|
769
927
|
const commitCount = execSync(`git rev-list --count ${mainBranch}..HEAD 2>/dev/null || echo 0`, {
|
|
@@ -774,6 +932,7 @@ function getSessionPhase(session) {
|
|
|
774
932
|
const commits = parseInt(commitCount, 10);
|
|
775
933
|
|
|
776
934
|
if (commits === 0) {
|
|
935
|
+
gitCache.set(cacheKey, SESSION_PHASES.TODO);
|
|
777
936
|
return SESSION_PHASES.TODO;
|
|
778
937
|
}
|
|
779
938
|
|
|
@@ -783,13 +942,17 @@ function getSessionPhase(session) {
|
|
|
783
942
|
encoding: 'utf8',
|
|
784
943
|
}).trim();
|
|
785
944
|
|
|
945
|
+
let phase;
|
|
786
946
|
if (status === '') {
|
|
787
947
|
// No uncommitted changes = ready for review
|
|
788
|
-
|
|
948
|
+
phase = SESSION_PHASES.REVIEW;
|
|
949
|
+
} else {
|
|
950
|
+
// Has commits but also uncommitted changes = still coding
|
|
951
|
+
phase = SESSION_PHASES.CODING;
|
|
789
952
|
}
|
|
790
953
|
|
|
791
|
-
|
|
792
|
-
return
|
|
954
|
+
gitCache.set(cacheKey, phase);
|
|
955
|
+
return phase;
|
|
793
956
|
} catch (e) {
|
|
794
957
|
// On error, assume coding phase
|
|
795
958
|
return SESSION_PHASES.CODING;
|
|
@@ -1046,12 +1209,11 @@ function main() {
|
|
|
1046
1209
|
|
|
1047
1210
|
// Register in single pass (combines register + count + status)
|
|
1048
1211
|
const registry = loadRegistry();
|
|
1049
|
-
const cleanupResult = cleanupStaleLocks(registry);
|
|
1050
1212
|
const branch = getCurrentBranch();
|
|
1051
1213
|
const story = getCurrentStory();
|
|
1052
1214
|
const pid = process.ppid || process.pid;
|
|
1053
1215
|
|
|
1054
|
-
// Find or create session
|
|
1216
|
+
// Find or create session FIRST (so we don't clean our own stale lock)
|
|
1055
1217
|
let sessionId = null;
|
|
1056
1218
|
let isNew = false;
|
|
1057
1219
|
for (const [id, session] of Object.entries(registry.sessions)) {
|
|
@@ -1094,6 +1256,16 @@ function main() {
|
|
|
1094
1256
|
}
|
|
1095
1257
|
saveRegistry(registry);
|
|
1096
1258
|
|
|
1259
|
+
// Clean up stale locks AFTER registering current session (so we don't clean our own lock)
|
|
1260
|
+
const cleanupResult = cleanupStaleLocks(registry);
|
|
1261
|
+
|
|
1262
|
+
// Filter out the current session from cleanup reports (its lock was just refreshed)
|
|
1263
|
+
// Use String() to ensure consistent comparison (sessionId is string, cleanup.id may vary)
|
|
1264
|
+
const filteredCleanup = {
|
|
1265
|
+
count: cleanupResult.sessions.filter(s => String(s.id) !== String(sessionId)).length,
|
|
1266
|
+
sessions: cleanupResult.sessions.filter(s => String(s.id) !== String(sessionId)),
|
|
1267
|
+
};
|
|
1268
|
+
|
|
1097
1269
|
// Build session list and counts
|
|
1098
1270
|
const sessions = [];
|
|
1099
1271
|
let otherActive = 0;
|
|
@@ -1114,8 +1286,8 @@ function main() {
|
|
|
1114
1286
|
current,
|
|
1115
1287
|
otherActive,
|
|
1116
1288
|
total: sessions.length,
|
|
1117
|
-
cleaned:
|
|
1118
|
-
cleanedSessions:
|
|
1289
|
+
cleaned: filteredCleanup.count,
|
|
1290
|
+
cleanedSessions: filteredCleanup.sessions,
|
|
1119
1291
|
})
|
|
1120
1292
|
);
|
|
1121
1293
|
break;
|
|
@@ -1815,7 +1987,7 @@ function getMergeHistory() {
|
|
|
1815
1987
|
}
|
|
1816
1988
|
|
|
1817
1989
|
// Session state file path
|
|
1818
|
-
const SESSION_STATE_PATH =
|
|
1990
|
+
const SESSION_STATE_PATH = getSessionStatePath(ROOT);
|
|
1819
1991
|
|
|
1820
1992
|
/**
|
|
1821
1993
|
* Switch active session context (for use with /add-dir).
|
|
@@ -1998,8 +2170,13 @@ function setSessionThreadType(sessionId, threadType) {
|
|
|
1998
2170
|
|
|
1999
2171
|
// Export for use as module
|
|
2000
2172
|
module.exports = {
|
|
2173
|
+
// Registry injection (for testing)
|
|
2174
|
+
injectRegistry,
|
|
2175
|
+
getRegistryInstance,
|
|
2176
|
+
// Registry access (backward compatible)
|
|
2001
2177
|
loadRegistry,
|
|
2002
2178
|
saveRegistry,
|
|
2179
|
+
// Session management
|
|
2003
2180
|
registerSession,
|
|
2004
2181
|
unregisterSession,
|
|
2005
2182
|
getSession,
|
|
@@ -2009,6 +2186,7 @@ module.exports = {
|
|
|
2009
2186
|
deleteSession,
|
|
2010
2187
|
isSessionActive,
|
|
2011
2188
|
cleanupStaleLocks,
|
|
2189
|
+
cleanupStaleLocksAsync,
|
|
2012
2190
|
// Merge operations
|
|
2013
2191
|
getMainBranch,
|
|
2014
2192
|
checkMergeability,
|