agileflow 2.99.0 → 2.99.2
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 +3 -3
- package/lib/dashboard-protocol.js +38 -0
- package/lib/dashboard-server.js +197 -7
- package/lib/feedback.js +36 -9
- package/lib/git-operations.js +4 -1
- package/lib/merge-operations.js +25 -0
- package/lib/progress.js +7 -6
- package/lib/session-operations.js +611 -0
- package/lib/session-switching.js +191 -0
- package/lib/template-loader.js +4 -2
- package/lib/worktree-operations.js +5 -25
- package/package.json +1 -1
- package/scripts/agileflow-configure.js +13 -0
- package/scripts/agileflow-welcome.js +11 -6
- package/scripts/batch-pmap-loop.js +11 -4
- package/scripts/claude-tmux.sh +186 -103
- 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 +10 -7
- package/scripts/lib/configure-repair.js +12 -2
- package/scripts/lib/process-cleanup.js +197 -15
- package/scripts/obtain-context.js +5 -0
- package/scripts/session-manager.js +156 -932
- package/scripts/spawn-parallel.js +15 -11
- 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 +56 -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/serve.md +127 -0
- package/src/core/commands/session/cleanup.md +1 -1
- package/src/core/commands/session/end.md +84 -23
- 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 +198 -84
- 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
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-operations.js - Session CRUD, listing, and cleanup operations
|
|
3
|
+
*
|
|
4
|
+
* Extracted from session-manager.js to reduce file size.
|
|
5
|
+
* Uses factory pattern for dependency injection.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { spawnSync } = require('child_process');
|
|
11
|
+
|
|
12
|
+
const { getStatusPath } = require('./paths');
|
|
13
|
+
const { safeReadJSON } = require('./errors');
|
|
14
|
+
const { isValidBranchName, isValidSessionNickname } = require('./validate');
|
|
15
|
+
const { getInheritedFlags } = require('./flag-detection');
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
THREAD_TYPES,
|
|
19
|
+
DEFAULT_WORKTREE_TIMEOUT_MS,
|
|
20
|
+
isGitWorktree,
|
|
21
|
+
detectThreadType,
|
|
22
|
+
progressIndicator,
|
|
23
|
+
createWorktreeWithTimeout,
|
|
24
|
+
cleanupFailedWorktree,
|
|
25
|
+
} = require('./worktree-operations');
|
|
26
|
+
|
|
27
|
+
const { isPidAlive } = require('./lock-file');
|
|
28
|
+
const { getCurrentBranch } = require('./git-operations');
|
|
29
|
+
|
|
30
|
+
// Agent Teams integration (lazy-loaded)
|
|
31
|
+
let _featureFlags, _teamManager;
|
|
32
|
+
function getFeatureFlags() {
|
|
33
|
+
if (!_featureFlags) {
|
|
34
|
+
try {
|
|
35
|
+
_featureFlags = require('./feature-flags');
|
|
36
|
+
} catch (e) {
|
|
37
|
+
_featureFlags = null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return _featureFlags;
|
|
41
|
+
}
|
|
42
|
+
function getTeamManager() {
|
|
43
|
+
if (!_teamManager) {
|
|
44
|
+
try {
|
|
45
|
+
_teamManager = require('../scripts/team-manager');
|
|
46
|
+
} catch (e) {
|
|
47
|
+
_teamManager = null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return _teamManager;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Create session operations bound to the given dependencies.
|
|
55
|
+
*
|
|
56
|
+
* @param {object} deps
|
|
57
|
+
* @param {string} deps.ROOT - Project root path
|
|
58
|
+
* @param {Function} deps.loadRegistry - Load registry data
|
|
59
|
+
* @param {Function} deps.saveRegistry - Save registry data
|
|
60
|
+
* @param {Function} deps.readLock - Read lock file for session
|
|
61
|
+
* @param {Function} deps.readLockAsync - Async read lock file
|
|
62
|
+
* @param {Function} deps.writeLock - Write lock file for session
|
|
63
|
+
* @param {Function} deps.removeLock - Remove lock file for session
|
|
64
|
+
* @param {Function} deps.isSessionActive - Check if session is active
|
|
65
|
+
* @param {Function} deps.isSessionActiveAsync - Async check if session is active
|
|
66
|
+
* @param {object} deps.c - Color utilities
|
|
67
|
+
*/
|
|
68
|
+
function createSessionOperations(deps) {
|
|
69
|
+
const {
|
|
70
|
+
ROOT,
|
|
71
|
+
loadRegistry,
|
|
72
|
+
saveRegistry,
|
|
73
|
+
readLock,
|
|
74
|
+
readLockAsync,
|
|
75
|
+
writeLock,
|
|
76
|
+
removeLock,
|
|
77
|
+
isSessionActive,
|
|
78
|
+
isSessionActiveAsync,
|
|
79
|
+
c,
|
|
80
|
+
} = deps;
|
|
81
|
+
|
|
82
|
+
// ============================================================================
|
|
83
|
+
// Stale Lock Cleanup
|
|
84
|
+
// ============================================================================
|
|
85
|
+
|
|
86
|
+
function processStalelock(id, session, lock, dryRun) {
|
|
87
|
+
if (!lock) return null;
|
|
88
|
+
const pid = parseInt(lock.pid, 10);
|
|
89
|
+
if (isPidAlive(pid)) return null;
|
|
90
|
+
if (!dryRun) removeLock(id);
|
|
91
|
+
return {
|
|
92
|
+
id,
|
|
93
|
+
nickname: session.nickname,
|
|
94
|
+
branch: session.branch,
|
|
95
|
+
pid,
|
|
96
|
+
reason: 'pid_dead',
|
|
97
|
+
path: session.path,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function cleanupStaleLocks(registry, options = {}) {
|
|
102
|
+
const { dryRun = false } = options;
|
|
103
|
+
const cleanedSessions = [];
|
|
104
|
+
for (const [id, session] of Object.entries(registry.sessions)) {
|
|
105
|
+
const result = processStalelock(id, session, readLock(id), dryRun);
|
|
106
|
+
if (result) cleanedSessions.push(result);
|
|
107
|
+
}
|
|
108
|
+
return { count: cleanedSessions.length, sessions: cleanedSessions };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function cleanupStaleLocksAsync(registry, options = {}) {
|
|
112
|
+
const { dryRun = false } = options;
|
|
113
|
+
const sessionEntries = Object.entries(registry.sessions);
|
|
114
|
+
if (sessionEntries.length === 0) return { count: 0, sessions: [] };
|
|
115
|
+
|
|
116
|
+
const lockResults = await Promise.all(
|
|
117
|
+
sessionEntries.map(async ([id, session]) => ({
|
|
118
|
+
id,
|
|
119
|
+
session,
|
|
120
|
+
lock: await readLockAsync(id),
|
|
121
|
+
}))
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const cleanedSessions = lockResults
|
|
125
|
+
.map(({ id, session, lock }) => processStalelock(id, session, lock, dryRun))
|
|
126
|
+
.filter(Boolean);
|
|
127
|
+
|
|
128
|
+
return { count: cleanedSessions.length, sessions: cleanedSessions };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ============================================================================
|
|
132
|
+
// Current Story Helper
|
|
133
|
+
// ============================================================================
|
|
134
|
+
|
|
135
|
+
function getCurrentStory() {
|
|
136
|
+
const statusPath = getStatusPath(ROOT);
|
|
137
|
+
const result = safeReadJSON(statusPath, { defaultValue: null });
|
|
138
|
+
if (!result.ok || !result.data) return null;
|
|
139
|
+
for (const [id, story] of Object.entries(result.data.stories || {})) {
|
|
140
|
+
if (story.status === 'in_progress') return { id, title: story.title };
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ============================================================================
|
|
146
|
+
// Session CRUD Operations
|
|
147
|
+
// ============================================================================
|
|
148
|
+
|
|
149
|
+
function registerSession(nickname = null, threadType = null) {
|
|
150
|
+
const registry = loadRegistry();
|
|
151
|
+
const cwd = process.cwd();
|
|
152
|
+
const branch = getCurrentBranch();
|
|
153
|
+
const story = getCurrentStory();
|
|
154
|
+
const pid = process.ppid || process.pid;
|
|
155
|
+
|
|
156
|
+
let existingId = null;
|
|
157
|
+
for (const [id, session] of Object.entries(registry.sessions)) {
|
|
158
|
+
if (session.path === cwd) {
|
|
159
|
+
existingId = id;
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (existingId) {
|
|
165
|
+
registry.sessions[existingId].branch = branch;
|
|
166
|
+
registry.sessions[existingId].story = story ? story.id : null;
|
|
167
|
+
registry.sessions[existingId].last_active = new Date().toISOString();
|
|
168
|
+
if (nickname) registry.sessions[existingId].nickname = nickname;
|
|
169
|
+
if (threadType && THREAD_TYPES.includes(threadType)) {
|
|
170
|
+
registry.sessions[existingId].thread_type = threadType;
|
|
171
|
+
}
|
|
172
|
+
writeLock(existingId, pid);
|
|
173
|
+
saveRegistry(registry);
|
|
174
|
+
return { id: existingId, isNew: false };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const sessionId = String(registry.next_id);
|
|
178
|
+
registry.next_id++;
|
|
179
|
+
const isMain = cwd === ROOT && !isGitWorktree(cwd);
|
|
180
|
+
const detectedType =
|
|
181
|
+
threadType && THREAD_TYPES.includes(threadType)
|
|
182
|
+
? threadType
|
|
183
|
+
: detectThreadType(null, !isMain);
|
|
184
|
+
|
|
185
|
+
registry.sessions[sessionId] = {
|
|
186
|
+
path: cwd,
|
|
187
|
+
branch,
|
|
188
|
+
story: story ? story.id : null,
|
|
189
|
+
nickname: nickname || null,
|
|
190
|
+
created: new Date().toISOString(),
|
|
191
|
+
last_active: new Date().toISOString(),
|
|
192
|
+
is_main: isMain,
|
|
193
|
+
thread_type: detectedType,
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
writeLock(sessionId, pid);
|
|
197
|
+
saveRegistry(registry);
|
|
198
|
+
return { id: sessionId, isNew: true, thread_type: detectedType };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function unregisterSession(sessionId) {
|
|
202
|
+
const registry = loadRegistry();
|
|
203
|
+
if (registry.sessions[sessionId]) {
|
|
204
|
+
registry.sessions[sessionId].last_active = new Date().toISOString();
|
|
205
|
+
removeLock(sessionId);
|
|
206
|
+
saveRegistry(registry);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function getSession(sessionId) {
|
|
211
|
+
const registry = loadRegistry();
|
|
212
|
+
const session = registry.sessions[sessionId];
|
|
213
|
+
if (!session) return null;
|
|
214
|
+
const threadType = session.thread_type || (session.is_main ? 'base' : 'parallel');
|
|
215
|
+
return {
|
|
216
|
+
id: sessionId,
|
|
217
|
+
...session,
|
|
218
|
+
thread_type: threadType,
|
|
219
|
+
active: isSessionActive(sessionId),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function createSession(options = {}) {
|
|
224
|
+
const registry = loadRegistry();
|
|
225
|
+
const sessionId = String(registry.next_id);
|
|
226
|
+
const projectName = registry.project_name;
|
|
227
|
+
|
|
228
|
+
const nickname = options.nickname || null;
|
|
229
|
+
const branchName = options.branch || `session-${sessionId}`;
|
|
230
|
+
const dirName = nickname || sessionId;
|
|
231
|
+
|
|
232
|
+
if (!isValidBranchName(branchName)) {
|
|
233
|
+
return {
|
|
234
|
+
success: false,
|
|
235
|
+
error: `Invalid branch name: "${branchName}". Use only letters, numbers, hyphens, underscores, and forward slashes.`,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
if (nickname && !isValidSessionNickname(nickname)) {
|
|
239
|
+
return {
|
|
240
|
+
success: false,
|
|
241
|
+
error: `Invalid nickname: "${nickname}". Use only letters, numbers, hyphens, and underscores.`,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const worktreePath = path.resolve(ROOT, '..', `${projectName}-${dirName}`);
|
|
246
|
+
if (fs.existsSync(worktreePath)) {
|
|
247
|
+
return { success: false, error: `Directory already exists: ${worktreePath}` };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Create branch if needed
|
|
251
|
+
const checkRef = spawnSync(
|
|
252
|
+
'git',
|
|
253
|
+
['show-ref', '--verify', '--quiet', `refs/heads/${branchName}`],
|
|
254
|
+
{ cwd: ROOT, encoding: 'utf8' }
|
|
255
|
+
);
|
|
256
|
+
let branchCreatedByUs = false;
|
|
257
|
+
if (checkRef.status !== 0) {
|
|
258
|
+
const createBranch = spawnSync('git', ['branch', branchName], {
|
|
259
|
+
cwd: ROOT,
|
|
260
|
+
encoding: 'utf8',
|
|
261
|
+
});
|
|
262
|
+
if (createBranch.status !== 0) {
|
|
263
|
+
return {
|
|
264
|
+
success: false,
|
|
265
|
+
error: `Failed to create branch: ${createBranch.stderr || 'unknown error'}`,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
branchCreatedByUs = true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const timeoutMs = options.timeout || DEFAULT_WORKTREE_TIMEOUT_MS;
|
|
272
|
+
const stopProgress = progressIndicator(
|
|
273
|
+
'Creating worktree (this may take a while for large repos)'
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
await createWorktreeWithTimeout(worktreePath, branchName, timeoutMs);
|
|
278
|
+
stopProgress();
|
|
279
|
+
process.stderr.write(`✓ Worktree created successfully\n`);
|
|
280
|
+
} catch (error) {
|
|
281
|
+
stopProgress();
|
|
282
|
+
cleanupFailedWorktree(worktreePath, branchName, branchCreatedByUs);
|
|
283
|
+
return { success: false, error: error.message };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Copy env files
|
|
287
|
+
const envFiles = ['.env', '.env.local', '.env.development', '.env.test', '.env.production'];
|
|
288
|
+
const copiedEnvFiles = [];
|
|
289
|
+
for (const envFile of envFiles) {
|
|
290
|
+
const src = path.join(ROOT, envFile);
|
|
291
|
+
const dest = path.join(worktreePath, envFile);
|
|
292
|
+
if (fs.existsSync(src) && !fs.existsSync(dest)) {
|
|
293
|
+
try {
|
|
294
|
+
fs.copyFileSync(src, dest);
|
|
295
|
+
copiedEnvFiles.push(envFile);
|
|
296
|
+
} catch (e) {
|
|
297
|
+
/* ignore */
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Copy config folders
|
|
303
|
+
const configFoldersToCopy = ['.claude', '.agileflow'];
|
|
304
|
+
const copiedFolders = [];
|
|
305
|
+
for (const folder of configFoldersToCopy) {
|
|
306
|
+
const src = path.join(ROOT, folder);
|
|
307
|
+
const dest = path.join(worktreePath, folder);
|
|
308
|
+
if (fs.existsSync(src)) {
|
|
309
|
+
try {
|
|
310
|
+
fs.cpSync(src, dest, { recursive: true, force: true });
|
|
311
|
+
copiedFolders.push(folder);
|
|
312
|
+
} catch (e) {
|
|
313
|
+
/* ignore */
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Symlink sessions directory
|
|
319
|
+
const sessionsSymlinkSrc = path.join(ROOT, '.agileflow', 'sessions');
|
|
320
|
+
const sessionsSymlinkDest = path.join(worktreePath, '.agileflow', 'sessions');
|
|
321
|
+
if (fs.existsSync(sessionsSymlinkSrc)) {
|
|
322
|
+
try {
|
|
323
|
+
if (fs.existsSync(sessionsSymlinkDest))
|
|
324
|
+
fs.rmSync(sessionsSymlinkDest, { recursive: true, force: true });
|
|
325
|
+
const relPath = path.relative(path.dirname(sessionsSymlinkDest), sessionsSymlinkSrc);
|
|
326
|
+
fs.symlinkSync(relPath, sessionsSymlinkDest, 'dir');
|
|
327
|
+
} catch (e) {
|
|
328
|
+
/* ignore */
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Symlink docs
|
|
333
|
+
const foldersToSymlink = ['docs'];
|
|
334
|
+
const symlinkedFolders = [];
|
|
335
|
+
for (const folder of foldersToSymlink) {
|
|
336
|
+
const src = path.join(ROOT, folder);
|
|
337
|
+
const dest = path.join(worktreePath, folder);
|
|
338
|
+
if (fs.existsSync(src)) {
|
|
339
|
+
try {
|
|
340
|
+
if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true });
|
|
341
|
+
const relPath = path.relative(worktreePath, src);
|
|
342
|
+
fs.symlinkSync(relPath, dest, 'dir');
|
|
343
|
+
symlinkedFolders.push(folder);
|
|
344
|
+
} catch (e) {
|
|
345
|
+
try {
|
|
346
|
+
fs.cpSync(src, dest, { recursive: true, force: true });
|
|
347
|
+
copiedFolders.push(folder);
|
|
348
|
+
} catch (copyErr) {
|
|
349
|
+
/* ignore */
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Detect inherited flags from parent Claude session
|
|
356
|
+
const inheritedFlags = options.inheritFlags !== false ? getInheritedFlags() : '';
|
|
357
|
+
|
|
358
|
+
registry.next_id++;
|
|
359
|
+
registry.sessions[sessionId] = {
|
|
360
|
+
path: worktreePath,
|
|
361
|
+
branch: branchName,
|
|
362
|
+
story: null,
|
|
363
|
+
nickname,
|
|
364
|
+
created: new Date().toISOString(),
|
|
365
|
+
last_active: new Date().toISOString(),
|
|
366
|
+
is_main: false,
|
|
367
|
+
thread_type: options.thread_type || 'parallel',
|
|
368
|
+
inherited_flags: inheritedFlags || null,
|
|
369
|
+
};
|
|
370
|
+
saveRegistry(registry);
|
|
371
|
+
|
|
372
|
+
// Build the command with inherited flags
|
|
373
|
+
const claudeCmd = inheritedFlags ? `claude ${inheritedFlags}` : 'claude';
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
success: true,
|
|
377
|
+
sessionId,
|
|
378
|
+
path: worktreePath,
|
|
379
|
+
branch: branchName,
|
|
380
|
+
thread_type: registry.sessions[sessionId].thread_type,
|
|
381
|
+
command: `cd "${worktreePath}" && ${claudeCmd}`,
|
|
382
|
+
inheritedFlags: inheritedFlags || null,
|
|
383
|
+
envFilesCopied: copiedEnvFiles,
|
|
384
|
+
foldersCopied: copiedFolders,
|
|
385
|
+
foldersSymlinked: symlinkedFolders,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function createTeamSession(options = {}) {
|
|
390
|
+
const ff = getFeatureFlags();
|
|
391
|
+
const templateName = options.template || 'fullstack';
|
|
392
|
+
|
|
393
|
+
if (!ff || !ff.isAgentTeamsEnabled({ rootDir: ROOT })) {
|
|
394
|
+
console.error(`${c.yellow}Agent Teams not enabled. Falling back to worktree mode.${c.reset}`);
|
|
395
|
+
return createSession({
|
|
396
|
+
nickname: options.nickname || `team-${templateName}`,
|
|
397
|
+
thread_type: 'parallel',
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const tm = getTeamManager();
|
|
402
|
+
if (!tm) {
|
|
403
|
+
return { success: false, error: 'team-manager module not available' };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const teamResult = tm.startTeam(ROOT, templateName);
|
|
407
|
+
if (!teamResult.ok) {
|
|
408
|
+
return { success: false, error: teamResult.error || 'Failed to start team' };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const registry = loadRegistry();
|
|
412
|
+
const sessionId = String(registry.next_id);
|
|
413
|
+
registry.next_id++;
|
|
414
|
+
|
|
415
|
+
registry.sessions[sessionId] = {
|
|
416
|
+
path: ROOT,
|
|
417
|
+
branch: getCurrentBranch(),
|
|
418
|
+
story: null,
|
|
419
|
+
nickname: options.nickname || `team-${templateName}`,
|
|
420
|
+
created: new Date().toISOString(),
|
|
421
|
+
last_active: new Date().toISOString(),
|
|
422
|
+
is_main: true,
|
|
423
|
+
type: 'team',
|
|
424
|
+
thread_type: 'team',
|
|
425
|
+
team_name: templateName,
|
|
426
|
+
team_lead: teamResult.lead || null,
|
|
427
|
+
teammates: teamResult.teammates || [],
|
|
428
|
+
};
|
|
429
|
+
saveRegistry(registry);
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
success: true,
|
|
433
|
+
sessionId,
|
|
434
|
+
type: 'team',
|
|
435
|
+
template: templateName,
|
|
436
|
+
mode: teamResult.mode,
|
|
437
|
+
teammates: teamResult.teammates || [],
|
|
438
|
+
path: ROOT,
|
|
439
|
+
branch: getCurrentBranch(),
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ============================================================================
|
|
444
|
+
// Session Listing
|
|
445
|
+
// ============================================================================
|
|
446
|
+
|
|
447
|
+
function buildSessionsList(registrySessions, activeChecks, cwd) {
|
|
448
|
+
const sessions = Object.entries(registrySessions).map(([id, session]) => ({
|
|
449
|
+
id,
|
|
450
|
+
...session,
|
|
451
|
+
active: activeChecks[id] || false,
|
|
452
|
+
current: session.path === cwd,
|
|
453
|
+
}));
|
|
454
|
+
sessions.sort((a, b) => parseInt(a.id) - parseInt(b.id));
|
|
455
|
+
return sessions;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function getSessions() {
|
|
459
|
+
const registry = loadRegistry();
|
|
460
|
+
const cleanupResult = cleanupStaleLocks(registry);
|
|
461
|
+
const cwd = process.cwd();
|
|
462
|
+
const activeChecks = {};
|
|
463
|
+
for (const id of Object.keys(registry.sessions)) activeChecks[id] = isSessionActive(id);
|
|
464
|
+
return {
|
|
465
|
+
sessions: buildSessionsList(registry.sessions, activeChecks, cwd),
|
|
466
|
+
cleaned: cleanupResult.count,
|
|
467
|
+
cleanedSessions: cleanupResult.sessions,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async function getSessionsAsync() {
|
|
472
|
+
const registry = loadRegistry();
|
|
473
|
+
const cleanupResult = await cleanupStaleLocksAsync(registry);
|
|
474
|
+
const sessionEntries = Object.entries(registry.sessions);
|
|
475
|
+
const cwd = process.cwd();
|
|
476
|
+
const activeResults = await Promise.all(
|
|
477
|
+
sessionEntries.map(async ([id]) => [id, await isSessionActiveAsync(id)])
|
|
478
|
+
);
|
|
479
|
+
const activeChecks = Object.fromEntries(activeResults);
|
|
480
|
+
return {
|
|
481
|
+
sessions: buildSessionsList(registry.sessions, activeChecks, cwd),
|
|
482
|
+
cleaned: cleanupResult.count,
|
|
483
|
+
cleanedSessions: cleanupResult.sessions,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function getActiveSessionCount() {
|
|
488
|
+
const { sessions } = getSessions();
|
|
489
|
+
const cwd = process.cwd();
|
|
490
|
+
return sessions.filter(s => s.active && s.path !== cwd).length;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function fullStatus(nickname = null) {
|
|
494
|
+
const cwd = process.cwd();
|
|
495
|
+
const registry = loadRegistry();
|
|
496
|
+
const branch = getCurrentBranch();
|
|
497
|
+
const story = getCurrentStory();
|
|
498
|
+
const pid = process.ppid || process.pid;
|
|
499
|
+
|
|
500
|
+
let sessionId = null,
|
|
501
|
+
isNew = false;
|
|
502
|
+
for (const [id, session] of Object.entries(registry.sessions)) {
|
|
503
|
+
if (session.path === cwd) {
|
|
504
|
+
sessionId = id;
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (sessionId) {
|
|
510
|
+
registry.sessions[sessionId].branch = branch;
|
|
511
|
+
registry.sessions[sessionId].story = story ? story.id : null;
|
|
512
|
+
registry.sessions[sessionId].last_active = new Date().toISOString();
|
|
513
|
+
if (nickname) registry.sessions[sessionId].nickname = nickname;
|
|
514
|
+
if (!registry.sessions[sessionId].thread_type)
|
|
515
|
+
registry.sessions[sessionId].thread_type = registry.sessions[sessionId].is_main
|
|
516
|
+
? 'base'
|
|
517
|
+
: 'parallel';
|
|
518
|
+
writeLock(sessionId, pid);
|
|
519
|
+
} else {
|
|
520
|
+
sessionId = String(registry.next_id);
|
|
521
|
+
registry.next_id++;
|
|
522
|
+
const isMain = cwd === ROOT && !isGitWorktree(cwd);
|
|
523
|
+
registry.sessions[sessionId] = {
|
|
524
|
+
path: cwd,
|
|
525
|
+
branch,
|
|
526
|
+
story: story ? story.id : null,
|
|
527
|
+
nickname: nickname || null,
|
|
528
|
+
created: new Date().toISOString(),
|
|
529
|
+
last_active: new Date().toISOString(),
|
|
530
|
+
is_main: isMain,
|
|
531
|
+
thread_type: isMain ? 'base' : 'parallel',
|
|
532
|
+
};
|
|
533
|
+
writeLock(sessionId, pid);
|
|
534
|
+
isNew = true;
|
|
535
|
+
}
|
|
536
|
+
saveRegistry(registry);
|
|
537
|
+
|
|
538
|
+
const cleanupResult = cleanupStaleLocks(registry);
|
|
539
|
+
const filteredCleanup = {
|
|
540
|
+
count: cleanupResult.sessions.filter(s => String(s.id) !== String(sessionId)).length,
|
|
541
|
+
sessions: cleanupResult.sessions.filter(s => String(s.id) !== String(sessionId)),
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
const sessions = [];
|
|
545
|
+
let otherActive = 0;
|
|
546
|
+
for (const [id, session] of Object.entries(registry.sessions)) {
|
|
547
|
+
const active = isSessionActive(id);
|
|
548
|
+
const isCurrent = session.path === cwd;
|
|
549
|
+
sessions.push({ id, ...session, active, current: isCurrent });
|
|
550
|
+
if (active && !isCurrent) otherActive++;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return {
|
|
554
|
+
registered: true,
|
|
555
|
+
id: sessionId,
|
|
556
|
+
isNew,
|
|
557
|
+
current: sessions.find(s => s.current) || null,
|
|
558
|
+
otherActive,
|
|
559
|
+
total: sessions.length,
|
|
560
|
+
cleaned: filteredCleanup.count,
|
|
561
|
+
cleanedSessions: filteredCleanup.sessions,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function deleteSession(sessionId, removeWorktree = false) {
|
|
566
|
+
const registry = loadRegistry();
|
|
567
|
+
const session = registry.sessions[sessionId];
|
|
568
|
+
if (!session) return { success: false, error: `Session ${sessionId} not found` };
|
|
569
|
+
if (session.is_main) return { success: false, error: 'Cannot delete main session' };
|
|
570
|
+
|
|
571
|
+
removeLock(sessionId);
|
|
572
|
+
if (removeWorktree && fs.existsSync(session.path)) {
|
|
573
|
+
const { execFileSync } = require('child_process');
|
|
574
|
+
try {
|
|
575
|
+
execFileSync('git', ['worktree', 'remove', session.path], { cwd: ROOT, encoding: 'utf8' });
|
|
576
|
+
} catch (e) {
|
|
577
|
+
try {
|
|
578
|
+
execFileSync('git', ['worktree', 'remove', '--force', session.path], {
|
|
579
|
+
cwd: ROOT,
|
|
580
|
+
encoding: 'utf8',
|
|
581
|
+
});
|
|
582
|
+
} catch (e2) {
|
|
583
|
+
return { success: false, error: `Failed to remove worktree: ${e2.message}` };
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
delete registry.sessions[sessionId];
|
|
588
|
+
saveRegistry(registry);
|
|
589
|
+
return { success: true };
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return {
|
|
593
|
+
processStalelock,
|
|
594
|
+
cleanupStaleLocks,
|
|
595
|
+
cleanupStaleLocksAsync,
|
|
596
|
+
getCurrentStory,
|
|
597
|
+
registerSession,
|
|
598
|
+
unregisterSession,
|
|
599
|
+
getSession,
|
|
600
|
+
createSession,
|
|
601
|
+
createTeamSession,
|
|
602
|
+
buildSessionsList,
|
|
603
|
+
getSessions,
|
|
604
|
+
getSessionsAsync,
|
|
605
|
+
getActiveSessionCount,
|
|
606
|
+
fullStatus,
|
|
607
|
+
deleteSession,
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
module.exports = { createSessionOperations };
|