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
|
@@ -13,76 +13,50 @@
|
|
|
13
13
|
* - lib/merge-operations.js - Merge, conflict resolution, smart-merge
|
|
14
14
|
* - lib/worktree-operations.js - Worktree creation, cleanup, thread types
|
|
15
15
|
* - lib/session-display.js - Kanban, table formatting, health checks
|
|
16
|
+
* - lib/session-operations.js - Session CRUD, listing, stale lock cleanup
|
|
17
|
+
* - lib/session-switching.js - Session switching, thread type management
|
|
16
18
|
*/
|
|
17
19
|
|
|
18
20
|
const fs = require('fs');
|
|
19
21
|
const path = require('path');
|
|
20
|
-
const { spawnSync } = require('child_process');
|
|
21
22
|
|
|
22
23
|
// Shared utilities
|
|
23
24
|
const { c } = require('../lib/colors');
|
|
24
|
-
const {
|
|
25
|
-
getProjectRoot,
|
|
26
|
-
getStatusPath,
|
|
27
|
-
getSessionStatePath,
|
|
28
|
-
getAgileflowDir,
|
|
29
|
-
} = require('../lib/paths');
|
|
30
|
-
const { safeReadJSON } = require('../lib/errors');
|
|
31
|
-
const { isValidBranchName, isValidSessionNickname } = require('../lib/validate');
|
|
25
|
+
const { getProjectRoot, getAgileflowDir } = require('../lib/paths');
|
|
32
26
|
|
|
33
27
|
// Session registry
|
|
34
28
|
const { SessionRegistry } = require('../lib/session-registry');
|
|
35
|
-
const { sessionThreadMachine } = require('../lib/state-machine');
|
|
36
29
|
|
|
37
30
|
// Lock file operations
|
|
38
31
|
const {
|
|
39
|
-
getLockPath: _getLockPath,
|
|
40
32
|
readLock: _readLock,
|
|
41
33
|
readLockAsync: _readLockAsync,
|
|
42
34
|
writeLock: _writeLock,
|
|
43
35
|
removeLock: _removeLock,
|
|
44
|
-
isPidAlive,
|
|
45
36
|
isSessionActive: _isSessionActive,
|
|
46
37
|
isSessionActiveAsync: _isSessionActiveAsync,
|
|
47
38
|
} = require('../lib/lock-file');
|
|
48
39
|
|
|
49
|
-
// Flag detection for session propagation
|
|
50
|
-
const { getInheritedFlags, detectParentSessionFlags } = require('../lib/flag-detection');
|
|
51
|
-
|
|
52
40
|
// Git operations module
|
|
53
41
|
const gitOps = require('../lib/git-operations');
|
|
54
42
|
const {
|
|
55
43
|
gitCache,
|
|
56
44
|
execGitAsync,
|
|
57
|
-
getCurrentBranch,
|
|
58
45
|
getMainBranch,
|
|
59
46
|
SESSION_PHASES,
|
|
60
|
-
determinePhaseFromGitState,
|
|
61
|
-
getSessionPhaseEarlyExit,
|
|
62
47
|
getSessionPhase,
|
|
63
48
|
getSessionPhaseAsync,
|
|
64
49
|
getSessionPhasesAsync,
|
|
65
50
|
} = gitOps;
|
|
66
51
|
|
|
67
52
|
// Worktree operations module
|
|
68
|
-
const
|
|
69
|
-
const {
|
|
70
|
-
THREAD_TYPES,
|
|
71
|
-
DEFAULT_WORKTREE_TIMEOUT_MS,
|
|
72
|
-
isGitWorktree,
|
|
73
|
-
detectThreadType,
|
|
74
|
-
progressIndicator,
|
|
75
|
-
createWorktreeWithTimeout,
|
|
76
|
-
cleanupFailedWorktree,
|
|
77
|
-
} = worktreeOps;
|
|
53
|
+
const { THREAD_TYPES, detectThreadType } = require('../lib/worktree-operations');
|
|
78
54
|
|
|
79
55
|
// Session display module
|
|
80
56
|
const displayOps = require('../lib/session-display');
|
|
81
57
|
const {
|
|
82
58
|
getFileDetails,
|
|
83
59
|
getSessionsHealth: _getSessionsHealth,
|
|
84
|
-
formatKanbanBoard,
|
|
85
|
-
groupSessionsByPhase,
|
|
86
60
|
renderKanbanBoard,
|
|
87
61
|
renderKanbanBoardAsync,
|
|
88
62
|
formatSessionsTable,
|
|
@@ -91,42 +65,19 @@ const {
|
|
|
91
65
|
// Merge operations module
|
|
92
66
|
const mergeOps = require('../lib/merge-operations');
|
|
93
67
|
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (!_featureFlags) {
|
|
98
|
-
try {
|
|
99
|
-
_featureFlags = require('../lib/feature-flags');
|
|
100
|
-
} catch (e) {
|
|
101
|
-
_featureFlags = null;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
return _featureFlags;
|
|
105
|
-
}
|
|
106
|
-
function getTeamManager() {
|
|
107
|
-
if (!_teamManager) {
|
|
108
|
-
try {
|
|
109
|
-
_teamManager = require('./team-manager');
|
|
110
|
-
} catch (e) {
|
|
111
|
-
_teamManager = null;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
return _teamManager;
|
|
115
|
-
}
|
|
68
|
+
// Extracted modules
|
|
69
|
+
const { createSessionOperations } = require('../lib/session-operations');
|
|
70
|
+
const { createSessionSwitching } = require('../lib/session-switching');
|
|
116
71
|
|
|
117
72
|
// Constants
|
|
118
73
|
const ROOT = getProjectRoot();
|
|
119
74
|
const SESSIONS_DIR = path.join(getAgileflowDir(ROOT), 'sessions');
|
|
120
|
-
const REGISTRY_PATH = path.join(SESSIONS_DIR, 'registry.json');
|
|
121
75
|
|
|
122
76
|
// Injectable registry instance for testing
|
|
123
77
|
let _registryInstance = null;
|
|
124
78
|
let _registryInitialized = false;
|
|
125
79
|
|
|
126
|
-
//
|
|
127
|
-
// Registry Management
|
|
128
|
-
// ============================================================================
|
|
129
|
-
|
|
80
|
+
// --- Registry Management ---
|
|
130
81
|
function getRegistryInstance() {
|
|
131
82
|
if (!_registryInstance) {
|
|
132
83
|
_registryInstance = new SessionRegistry(ROOT);
|
|
@@ -146,12 +97,6 @@ function resetRegistryCache() {
|
|
|
146
97
|
}
|
|
147
98
|
}
|
|
148
99
|
|
|
149
|
-
function ensureSessionsDir() {
|
|
150
|
-
if (!fs.existsSync(SESSIONS_DIR)) {
|
|
151
|
-
fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
100
|
function loadRegistry() {
|
|
156
101
|
const registryInstance = getRegistryInstance();
|
|
157
102
|
if (!_registryInitialized) {
|
|
@@ -171,13 +116,7 @@ function saveRegistry(registryData) {
|
|
|
171
116
|
return registry.saveSync(registryData);
|
|
172
117
|
}
|
|
173
118
|
|
|
174
|
-
//
|
|
175
|
-
// Lock File Wrappers (bind to SESSIONS_DIR)
|
|
176
|
-
// ============================================================================
|
|
177
|
-
|
|
178
|
-
function getLockPath(sessionId) {
|
|
179
|
-
return _getLockPath(SESSIONS_DIR, sessionId);
|
|
180
|
-
}
|
|
119
|
+
// --- Lock File Wrappers (bind to SESSIONS_DIR) ---
|
|
181
120
|
function readLock(sessionId) {
|
|
182
121
|
return _readLock(SESSIONS_DIR, sessionId);
|
|
183
122
|
}
|
|
@@ -197,655 +136,108 @@ async function isSessionActiveAsync(sessionId) {
|
|
|
197
136
|
return _isSessionActiveAsync(SESSIONS_DIR, sessionId);
|
|
198
137
|
}
|
|
199
138
|
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
pid,
|
|
214
|
-
reason: 'pid_dead',
|
|
215
|
-
path: session.path,
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function cleanupStaleLocks(registry, options = {}) {
|
|
220
|
-
const { dryRun = false } = options;
|
|
221
|
-
const cleanedSessions = [];
|
|
222
|
-
for (const [id, session] of Object.entries(registry.sessions)) {
|
|
223
|
-
const result = processStalelock(id, session, readLock(id), dryRun);
|
|
224
|
-
if (result) cleanedSessions.push(result);
|
|
225
|
-
}
|
|
226
|
-
return { count: cleanedSessions.length, sessions: cleanedSessions };
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
async function cleanupStaleLocksAsync(registry, options = {}) {
|
|
230
|
-
const { dryRun = false } = options;
|
|
231
|
-
const sessionEntries = Object.entries(registry.sessions);
|
|
232
|
-
if (sessionEntries.length === 0) return { count: 0, sessions: [] };
|
|
233
|
-
|
|
234
|
-
const lockResults = await Promise.all(
|
|
235
|
-
sessionEntries.map(async ([id, session]) => ({
|
|
236
|
-
id,
|
|
237
|
-
session,
|
|
238
|
-
lock: await readLockAsync(id),
|
|
239
|
-
}))
|
|
240
|
-
);
|
|
139
|
+
// --- Instantiate extracted modules ---
|
|
140
|
+
const sessionOps = createSessionOperations({
|
|
141
|
+
ROOT,
|
|
142
|
+
loadRegistry,
|
|
143
|
+
saveRegistry,
|
|
144
|
+
readLock,
|
|
145
|
+
readLockAsync,
|
|
146
|
+
writeLock,
|
|
147
|
+
removeLock,
|
|
148
|
+
isSessionActive,
|
|
149
|
+
isSessionActiveAsync,
|
|
150
|
+
c,
|
|
151
|
+
});
|
|
241
152
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
153
|
+
const sessionSwitchOps = createSessionSwitching({
|
|
154
|
+
ROOT,
|
|
155
|
+
loadRegistry,
|
|
156
|
+
saveRegistry,
|
|
157
|
+
});
|
|
245
158
|
|
|
246
|
-
|
|
247
|
-
|
|
159
|
+
// Destructure for local use and re-export
|
|
160
|
+
const {
|
|
161
|
+
cleanupStaleLocks,
|
|
162
|
+
cleanupStaleLocksAsync,
|
|
163
|
+
registerSession,
|
|
164
|
+
unregisterSession,
|
|
165
|
+
getSession,
|
|
166
|
+
createSession,
|
|
167
|
+
createTeamSession,
|
|
168
|
+
getSessions,
|
|
169
|
+
getSessionsAsync,
|
|
170
|
+
getActiveSessionCount,
|
|
171
|
+
fullStatus,
|
|
172
|
+
deleteSession,
|
|
173
|
+
} = sessionOps;
|
|
248
174
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
175
|
+
const {
|
|
176
|
+
switchSession,
|
|
177
|
+
clearActiveSession,
|
|
178
|
+
getActiveSession,
|
|
179
|
+
getSessionThreadType,
|
|
180
|
+
setSessionThreadType,
|
|
181
|
+
transitionThread,
|
|
182
|
+
getValidThreadTransitions,
|
|
183
|
+
} = sessionSwitchOps;
|
|
252
184
|
|
|
185
|
+
// --- Session Health ---
|
|
253
186
|
function getSessionsHealth(options = {}) {
|
|
254
187
|
return _getSessionsHealth(options, loadRegistry);
|
|
255
188
|
}
|
|
256
189
|
|
|
257
|
-
//
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
const cwd = process.cwd();
|
|
278
|
-
const branch = getCurrentBranch();
|
|
279
|
-
const story = getCurrentStory();
|
|
280
|
-
const pid = process.ppid || process.pid;
|
|
281
|
-
|
|
282
|
-
let existingId = null;
|
|
283
|
-
for (const [id, session] of Object.entries(registry.sessions)) {
|
|
284
|
-
if (session.path === cwd) {
|
|
285
|
-
existingId = id;
|
|
286
|
-
break;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if (existingId) {
|
|
291
|
-
registry.sessions[existingId].branch = branch;
|
|
292
|
-
registry.sessions[existingId].story = story ? story.id : null;
|
|
293
|
-
registry.sessions[existingId].last_active = new Date().toISOString();
|
|
294
|
-
if (nickname) registry.sessions[existingId].nickname = nickname;
|
|
295
|
-
if (threadType && THREAD_TYPES.includes(threadType)) {
|
|
296
|
-
registry.sessions[existingId].thread_type = threadType;
|
|
297
|
-
}
|
|
298
|
-
writeLock(existingId, pid);
|
|
299
|
-
saveRegistry(registry);
|
|
300
|
-
return { id: existingId, isNew: false };
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const sessionId = String(registry.next_id);
|
|
304
|
-
registry.next_id++;
|
|
305
|
-
const isMain = cwd === ROOT && !isGitWorktree(cwd);
|
|
306
|
-
const detectedType =
|
|
307
|
-
threadType && THREAD_TYPES.includes(threadType) ? threadType : detectThreadType(null, !isMain);
|
|
308
|
-
|
|
309
|
-
registry.sessions[sessionId] = {
|
|
310
|
-
path: cwd,
|
|
311
|
-
branch,
|
|
312
|
-
story: story ? story.id : null,
|
|
313
|
-
nickname: nickname || null,
|
|
314
|
-
created: new Date().toISOString(),
|
|
315
|
-
last_active: new Date().toISOString(),
|
|
316
|
-
is_main: isMain,
|
|
317
|
-
thread_type: detectedType,
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
writeLock(sessionId, pid);
|
|
321
|
-
saveRegistry(registry);
|
|
322
|
-
return { id: sessionId, isNew: true, thread_type: detectedType };
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function unregisterSession(sessionId) {
|
|
326
|
-
const registry = loadRegistry();
|
|
327
|
-
if (registry.sessions[sessionId]) {
|
|
328
|
-
registry.sessions[sessionId].last_active = new Date().toISOString();
|
|
329
|
-
removeLock(sessionId);
|
|
330
|
-
saveRegistry(registry);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function getSession(sessionId) {
|
|
335
|
-
const registry = loadRegistry();
|
|
336
|
-
const session = registry.sessions[sessionId];
|
|
337
|
-
if (!session) return null;
|
|
338
|
-
const threadType = session.thread_type || (session.is_main ? 'base' : 'parallel');
|
|
339
|
-
return { id: sessionId, ...session, thread_type: threadType, active: isSessionActive(sessionId) };
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
async function createSession(options = {}) {
|
|
343
|
-
const registry = loadRegistry();
|
|
344
|
-
const sessionId = String(registry.next_id);
|
|
345
|
-
const projectName = registry.project_name;
|
|
346
|
-
|
|
347
|
-
const nickname = options.nickname || null;
|
|
348
|
-
const branchName = options.branch || `session-${sessionId}`;
|
|
349
|
-
const dirName = nickname || sessionId;
|
|
350
|
-
|
|
351
|
-
if (!isValidBranchName(branchName)) {
|
|
352
|
-
return {
|
|
353
|
-
success: false,
|
|
354
|
-
error: `Invalid branch name: "${branchName}". Use only letters, numbers, hyphens, underscores, and forward slashes.`,
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
if (nickname && !isValidSessionNickname(nickname)) {
|
|
358
|
-
return {
|
|
359
|
-
success: false,
|
|
360
|
-
error: `Invalid nickname: "${nickname}". Use only letters, numbers, hyphens, and underscores.`,
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const worktreePath = path.resolve(ROOT, '..', `${projectName}-${dirName}`);
|
|
365
|
-
if (fs.existsSync(worktreePath)) {
|
|
366
|
-
return { success: false, error: `Directory already exists: ${worktreePath}` };
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// Create branch if needed
|
|
370
|
-
const checkRef = spawnSync(
|
|
371
|
-
'git',
|
|
372
|
-
['show-ref', '--verify', '--quiet', `refs/heads/${branchName}`],
|
|
373
|
-
{ cwd: ROOT, encoding: 'utf8' }
|
|
374
|
-
);
|
|
375
|
-
let branchCreatedByUs = false;
|
|
376
|
-
if (checkRef.status !== 0) {
|
|
377
|
-
const createBranch = spawnSync('git', ['branch', branchName], { cwd: ROOT, encoding: 'utf8' });
|
|
378
|
-
if (createBranch.status !== 0) {
|
|
379
|
-
return {
|
|
380
|
-
success: false,
|
|
381
|
-
error: `Failed to create branch: ${createBranch.stderr || 'unknown error'}`,
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
branchCreatedByUs = true;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const timeoutMs = options.timeout || DEFAULT_WORKTREE_TIMEOUT_MS;
|
|
388
|
-
const stopProgress = progressIndicator(
|
|
389
|
-
'Creating worktree (this may take a while for large repos)'
|
|
390
|
-
);
|
|
391
|
-
|
|
392
|
-
try {
|
|
393
|
-
await createWorktreeWithTimeout(worktreePath, branchName, timeoutMs);
|
|
394
|
-
stopProgress();
|
|
395
|
-
process.stderr.write(`✓ Worktree created successfully\n`);
|
|
396
|
-
} catch (error) {
|
|
397
|
-
stopProgress();
|
|
398
|
-
cleanupFailedWorktree(worktreePath, branchName, branchCreatedByUs);
|
|
399
|
-
return { success: false, error: error.message };
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// Copy env files
|
|
403
|
-
const envFiles = ['.env', '.env.local', '.env.development', '.env.test', '.env.production'];
|
|
404
|
-
const copiedEnvFiles = [];
|
|
405
|
-
for (const envFile of envFiles) {
|
|
406
|
-
const src = path.join(ROOT, envFile);
|
|
407
|
-
const dest = path.join(worktreePath, envFile);
|
|
408
|
-
if (fs.existsSync(src) && !fs.existsSync(dest)) {
|
|
409
|
-
try {
|
|
410
|
-
fs.copyFileSync(src, dest);
|
|
411
|
-
copiedEnvFiles.push(envFile);
|
|
412
|
-
} catch (e) {
|
|
413
|
-
/* ignore */
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// Copy config folders
|
|
419
|
-
const configFoldersToCopy = ['.claude', '.agileflow'];
|
|
420
|
-
const copiedFolders = [];
|
|
421
|
-
for (const folder of configFoldersToCopy) {
|
|
422
|
-
const src = path.join(ROOT, folder);
|
|
423
|
-
const dest = path.join(worktreePath, folder);
|
|
424
|
-
if (fs.existsSync(src)) {
|
|
425
|
-
try {
|
|
426
|
-
fs.cpSync(src, dest, { recursive: true, force: true });
|
|
427
|
-
copiedFolders.push(folder);
|
|
428
|
-
} catch (e) {
|
|
429
|
-
/* ignore */
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// Symlink sessions directory
|
|
435
|
-
const sessionsSymlinkSrc = path.join(ROOT, '.agileflow', 'sessions');
|
|
436
|
-
const sessionsSymlinkDest = path.join(worktreePath, '.agileflow', 'sessions');
|
|
437
|
-
if (fs.existsSync(sessionsSymlinkSrc)) {
|
|
438
|
-
try {
|
|
439
|
-
if (fs.existsSync(sessionsSymlinkDest))
|
|
440
|
-
fs.rmSync(sessionsSymlinkDest, { recursive: true, force: true });
|
|
441
|
-
const relPath = path.relative(path.dirname(sessionsSymlinkDest), sessionsSymlinkSrc);
|
|
442
|
-
fs.symlinkSync(relPath, sessionsSymlinkDest, 'dir');
|
|
443
|
-
} catch (e) {
|
|
444
|
-
/* ignore */
|
|
445
|
-
}
|
|
446
|
-
}
|
|
190
|
+
// Merge operation wrappers (delegate to merge-operations module)
|
|
191
|
+
const checkMergeability = id => mergeOps.checkMergeability(id, loadRegistry);
|
|
192
|
+
const getMergePreview = id => mergeOps.getMergePreview(id, loadRegistry);
|
|
193
|
+
const integrateSession = (id, opts = {}) =>
|
|
194
|
+
mergeOps.integrateSession(id, opts, loadRegistry, saveRegistry, removeLock);
|
|
195
|
+
const commitChanges = (id, opts = {}) => mergeOps.commitChanges(id, opts, loadRegistry);
|
|
196
|
+
const stashChanges = id => mergeOps.stashChanges(id, loadRegistry);
|
|
197
|
+
const unstashChanges = id => mergeOps.unstashChanges(id);
|
|
198
|
+
const discardChanges = id => mergeOps.discardChanges(id, loadRegistry);
|
|
199
|
+
const categorizeFile = fp => mergeOps.categorizeFile(fp);
|
|
200
|
+
const getMergeStrategy = cat => mergeOps.getMergeStrategy(cat);
|
|
201
|
+
const getConflictingFiles = id => mergeOps.getConflictingFiles(id, loadRegistry);
|
|
202
|
+
const getMergeHistory = () => mergeOps.getMergeHistory();
|
|
203
|
+
const smartMerge = (id, opts = {}) =>
|
|
204
|
+
mergeOps.smartMerge(id, opts, loadRegistry, saveRegistry, removeLock, unregisterSession);
|
|
205
|
+
|
|
206
|
+
// --- CLI Interface ---
|
|
207
|
+
function main() {
|
|
208
|
+
const args = process.argv.slice(2);
|
|
209
|
+
const command = args[0];
|
|
447
210
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
const src = path.join(ROOT, folder);
|
|
453
|
-
const dest = path.join(worktreePath, folder);
|
|
454
|
-
if (fs.existsSync(src)) {
|
|
455
|
-
try {
|
|
456
|
-
if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true });
|
|
457
|
-
const relPath = path.relative(worktreePath, src);
|
|
458
|
-
fs.symlinkSync(relPath, dest, 'dir');
|
|
459
|
-
symlinkedFolders.push(folder);
|
|
460
|
-
} catch (e) {
|
|
461
|
-
try {
|
|
462
|
-
fs.cpSync(src, dest, { recursive: true, force: true });
|
|
463
|
-
copiedFolders.push(folder);
|
|
464
|
-
} catch (copyErr) {
|
|
465
|
-
/* ignore */
|
|
466
|
-
}
|
|
467
|
-
}
|
|
211
|
+
function requireId(label = 'Session ID') {
|
|
212
|
+
if (!args[1]) {
|
|
213
|
+
console.log(JSON.stringify({ success: false, error: `${label} required` }));
|
|
214
|
+
return null;
|
|
468
215
|
}
|
|
216
|
+
return args[1];
|
|
469
217
|
}
|
|
470
218
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
inherited_flags: inheritedFlags || null,
|
|
485
|
-
};
|
|
486
|
-
saveRegistry(registry);
|
|
487
|
-
|
|
488
|
-
// Build the command with inherited flags
|
|
489
|
-
const claudeCmd = inheritedFlags ? `claude ${inheritedFlags}` : 'claude';
|
|
490
|
-
|
|
491
|
-
return {
|
|
492
|
-
success: true,
|
|
493
|
-
sessionId,
|
|
494
|
-
path: worktreePath,
|
|
495
|
-
branch: branchName,
|
|
496
|
-
thread_type: registry.sessions[sessionId].thread_type,
|
|
497
|
-
command: `cd "${worktreePath}" && ${claudeCmd}`,
|
|
498
|
-
inheritedFlags: inheritedFlags || null,
|
|
499
|
-
envFilesCopied: copiedEnvFiles,
|
|
500
|
-
foldersCopied: copiedFolders,
|
|
501
|
-
foldersSymlinked: symlinkedFolders,
|
|
502
|
-
};
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
/**
|
|
506
|
-
* Create a native Agent Teams session instead of a worktree session.
|
|
507
|
-
* Falls back to worktree mode if Agent Teams is not enabled.
|
|
508
|
-
*
|
|
509
|
-
* @param {object} options - { template, nickname }
|
|
510
|
-
* @returns {object} Result with session info
|
|
511
|
-
*/
|
|
512
|
-
function createTeamSession(options = {}) {
|
|
513
|
-
const ff = getFeatureFlags();
|
|
514
|
-
const templateName = options.template || 'fullstack';
|
|
515
|
-
|
|
516
|
-
// Check if Agent Teams is enabled
|
|
517
|
-
if (!ff || !ff.isAgentTeamsEnabled({ rootDir: ROOT })) {
|
|
518
|
-
console.error(`${c.yellow}Agent Teams not enabled. Falling back to worktree mode.${c.reset}`);
|
|
519
|
-
return createSession({
|
|
520
|
-
nickname: options.nickname || `team-${templateName}`,
|
|
521
|
-
thread_type: 'parallel',
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
// Use team-manager to start the team
|
|
526
|
-
const tm = getTeamManager();
|
|
527
|
-
if (!tm) {
|
|
528
|
-
return { success: false, error: 'team-manager module not available' };
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
const teamResult = tm.startTeam(ROOT, templateName);
|
|
532
|
-
if (!teamResult.ok) {
|
|
533
|
-
return { success: false, error: teamResult.error || 'Failed to start team' };
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// Register as a session in the registry
|
|
537
|
-
const registry = loadRegistry();
|
|
538
|
-
const sessionId = String(registry.next_id);
|
|
539
|
-
registry.next_id++;
|
|
540
|
-
|
|
541
|
-
registry.sessions[sessionId] = {
|
|
542
|
-
path: ROOT,
|
|
543
|
-
branch: getCurrentBranch(),
|
|
544
|
-
story: null,
|
|
545
|
-
nickname: options.nickname || `team-${templateName}`,
|
|
546
|
-
created: new Date().toISOString(),
|
|
547
|
-
last_active: new Date().toISOString(),
|
|
548
|
-
is_main: true,
|
|
549
|
-
type: 'team',
|
|
550
|
-
thread_type: 'team',
|
|
551
|
-
team_name: templateName,
|
|
552
|
-
team_lead: teamResult.lead || null,
|
|
553
|
-
teammates: teamResult.teammates || [],
|
|
554
|
-
};
|
|
555
|
-
saveRegistry(registry);
|
|
556
|
-
|
|
557
|
-
return {
|
|
558
|
-
success: true,
|
|
559
|
-
sessionId,
|
|
560
|
-
type: 'team',
|
|
561
|
-
template: templateName,
|
|
562
|
-
mode: teamResult.mode,
|
|
563
|
-
teammates: teamResult.teammates || [],
|
|
564
|
-
path: ROOT,
|
|
565
|
-
branch: getCurrentBranch(),
|
|
566
|
-
};
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
function buildSessionsList(registrySessions, activeChecks, cwd) {
|
|
570
|
-
const sessions = Object.entries(registrySessions).map(([id, session]) => ({
|
|
571
|
-
id,
|
|
572
|
-
...session,
|
|
573
|
-
active: activeChecks[id] || false,
|
|
574
|
-
current: session.path === cwd,
|
|
575
|
-
}));
|
|
576
|
-
sessions.sort((a, b) => parseInt(a.id) - parseInt(b.id));
|
|
577
|
-
return sessions;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
function getSessions() {
|
|
581
|
-
const registry = loadRegistry();
|
|
582
|
-
const cleanupResult = cleanupStaleLocks(registry);
|
|
583
|
-
const cwd = process.cwd();
|
|
584
|
-
const activeChecks = {};
|
|
585
|
-
for (const id of Object.keys(registry.sessions)) activeChecks[id] = isSessionActive(id);
|
|
586
|
-
return {
|
|
587
|
-
sessions: buildSessionsList(registry.sessions, activeChecks, cwd),
|
|
588
|
-
cleaned: cleanupResult.count,
|
|
589
|
-
cleanedSessions: cleanupResult.sessions,
|
|
590
|
-
};
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
async function getSessionsAsync() {
|
|
594
|
-
const registry = loadRegistry();
|
|
595
|
-
const cleanupResult = await cleanupStaleLocksAsync(registry);
|
|
596
|
-
const sessionEntries = Object.entries(registry.sessions);
|
|
597
|
-
const cwd = process.cwd();
|
|
598
|
-
const activeResults = await Promise.all(
|
|
599
|
-
sessionEntries.map(async ([id]) => [id, await isSessionActiveAsync(id)])
|
|
600
|
-
);
|
|
601
|
-
const activeChecks = Object.fromEntries(activeResults);
|
|
602
|
-
return {
|
|
603
|
-
sessions: buildSessionsList(registry.sessions, activeChecks, cwd),
|
|
604
|
-
cleaned: cleanupResult.count,
|
|
605
|
-
cleanedSessions: cleanupResult.sessions,
|
|
606
|
-
};
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
function getActiveSessionCount() {
|
|
610
|
-
const { sessions } = getSessions();
|
|
611
|
-
const cwd = process.cwd();
|
|
612
|
-
return sessions.filter(s => s.active && s.path !== cwd).length;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
function deleteSession(sessionId, removeWorktree = false) {
|
|
616
|
-
const registry = loadRegistry();
|
|
617
|
-
const session = registry.sessions[sessionId];
|
|
618
|
-
if (!session) return { success: false, error: `Session ${sessionId} not found` };
|
|
619
|
-
if (session.is_main) return { success: false, error: 'Cannot delete main session' };
|
|
620
|
-
|
|
621
|
-
removeLock(sessionId);
|
|
622
|
-
if (removeWorktree && fs.existsSync(session.path)) {
|
|
623
|
-
const { execFileSync } = require('child_process');
|
|
624
|
-
try {
|
|
625
|
-
execFileSync('git', ['worktree', 'remove', session.path], { cwd: ROOT, encoding: 'utf8' });
|
|
626
|
-
} catch (e) {
|
|
627
|
-
try {
|
|
628
|
-
execFileSync('git', ['worktree', 'remove', '--force', session.path], {
|
|
629
|
-
cwd: ROOT,
|
|
630
|
-
encoding: 'utf8',
|
|
631
|
-
});
|
|
632
|
-
} catch (e2) {
|
|
633
|
-
return { success: false, error: `Failed to remove worktree: ${e2.message}` };
|
|
219
|
+
function parseOpts(startIdx, allowedKeys, boolKeys = []) {
|
|
220
|
+
const options = {};
|
|
221
|
+
for (let i = startIdx; i < args.length; i++) {
|
|
222
|
+
const arg = args[i];
|
|
223
|
+
if (!arg.startsWith('--')) continue;
|
|
224
|
+
const eqIndex = arg.indexOf('=');
|
|
225
|
+
let key, value;
|
|
226
|
+
if (eqIndex !== -1) {
|
|
227
|
+
key = arg.slice(2, eqIndex);
|
|
228
|
+
value = arg.slice(eqIndex + 1);
|
|
229
|
+
} else {
|
|
230
|
+
key = arg.slice(2);
|
|
231
|
+
value = args[++i];
|
|
634
232
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
saveRegistry(registry);
|
|
639
|
-
return { success: true };
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
// ============================================================================
|
|
643
|
-
// Session Switching
|
|
644
|
-
// ============================================================================
|
|
645
|
-
|
|
646
|
-
const SESSION_STATE_PATH = getSessionStatePath(ROOT);
|
|
647
|
-
|
|
648
|
-
function switchSession(sessionIdOrNickname) {
|
|
649
|
-
const registry = loadRegistry();
|
|
650
|
-
let targetSession = null,
|
|
651
|
-
targetId = null;
|
|
652
|
-
for (const [id, session] of Object.entries(registry.sessions)) {
|
|
653
|
-
if (id === sessionIdOrNickname || session.nickname === sessionIdOrNickname) {
|
|
654
|
-
targetSession = session;
|
|
655
|
-
targetId = id;
|
|
656
|
-
break;
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
if (!targetSession)
|
|
660
|
-
return { success: false, error: `Session "${sessionIdOrNickname}" not found` };
|
|
661
|
-
if (!fs.existsSync(targetSession.path))
|
|
662
|
-
return { success: false, error: `Session directory does not exist: ${targetSession.path}` };
|
|
663
|
-
|
|
664
|
-
let sessionState = {};
|
|
665
|
-
if (fs.existsSync(SESSION_STATE_PATH)) {
|
|
666
|
-
try {
|
|
667
|
-
sessionState = JSON.parse(fs.readFileSync(SESSION_STATE_PATH, 'utf8'));
|
|
668
|
-
} catch (e) {
|
|
669
|
-
/* start fresh */
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
sessionState.active_session = {
|
|
674
|
-
id: targetId,
|
|
675
|
-
nickname: targetSession.nickname,
|
|
676
|
-
path: targetSession.path,
|
|
677
|
-
branch: targetSession.branch,
|
|
678
|
-
switched_at: new Date().toISOString(),
|
|
679
|
-
original_cwd: ROOT,
|
|
680
|
-
};
|
|
681
|
-
|
|
682
|
-
const stateDir = path.dirname(SESSION_STATE_PATH);
|
|
683
|
-
if (!fs.existsSync(stateDir)) fs.mkdirSync(stateDir, { recursive: true });
|
|
684
|
-
fs.writeFileSync(SESSION_STATE_PATH, JSON.stringify(sessionState, null, 2) + '\n');
|
|
685
|
-
|
|
686
|
-
registry.sessions[targetId].last_active = new Date().toISOString();
|
|
687
|
-
saveRegistry(registry);
|
|
688
|
-
|
|
689
|
-
return {
|
|
690
|
-
success: true,
|
|
691
|
-
session: {
|
|
692
|
-
id: targetId,
|
|
693
|
-
nickname: targetSession.nickname,
|
|
694
|
-
path: targetSession.path,
|
|
695
|
-
branch: targetSession.branch,
|
|
696
|
-
},
|
|
697
|
-
path: targetSession.path,
|
|
698
|
-
addDirCommand: `/add-dir ${targetSession.path}`,
|
|
699
|
-
};
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
function clearActiveSession() {
|
|
703
|
-
if (!fs.existsSync(SESSION_STATE_PATH)) return { success: true };
|
|
704
|
-
try {
|
|
705
|
-
const sessionState = JSON.parse(fs.readFileSync(SESSION_STATE_PATH, 'utf8'));
|
|
706
|
-
delete sessionState.active_session;
|
|
707
|
-
fs.writeFileSync(SESSION_STATE_PATH, JSON.stringify(sessionState, null, 2) + '\n');
|
|
708
|
-
return { success: true };
|
|
709
|
-
} catch (e) {
|
|
710
|
-
return { success: false, error: e.message };
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
function getActiveSession() {
|
|
715
|
-
if (!fs.existsSync(SESSION_STATE_PATH)) return { active: false };
|
|
716
|
-
try {
|
|
717
|
-
const sessionState = JSON.parse(fs.readFileSync(SESSION_STATE_PATH, 'utf8'));
|
|
718
|
-
return sessionState.active_session
|
|
719
|
-
? { active: true, session: sessionState.active_session }
|
|
720
|
-
: { active: false };
|
|
721
|
-
} catch (e) {
|
|
722
|
-
return { active: false };
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
// ============================================================================
|
|
727
|
-
// Thread Type Management
|
|
728
|
-
// ============================================================================
|
|
729
|
-
|
|
730
|
-
function getSessionThreadType(sessionId = null) {
|
|
731
|
-
const registry = loadRegistry();
|
|
732
|
-
const cwd = process.cwd();
|
|
733
|
-
let targetId = sessionId;
|
|
734
|
-
if (!targetId) {
|
|
735
|
-
for (const [id, session] of Object.entries(registry.sessions)) {
|
|
736
|
-
if (session.path === cwd) {
|
|
737
|
-
targetId = id;
|
|
738
|
-
break;
|
|
233
|
+
if (!allowedKeys.includes(key)) {
|
|
234
|
+
console.log(JSON.stringify({ success: false, error: `Unknown option: --${key}` }));
|
|
235
|
+
return null;
|
|
739
236
|
}
|
|
237
|
+
options[key] = boolKeys.includes(key) ? value !== 'false' : value;
|
|
740
238
|
}
|
|
239
|
+
return options;
|
|
741
240
|
}
|
|
742
|
-
if (!targetId || !registry.sessions[targetId])
|
|
743
|
-
return { success: false, error: 'Session not found' };
|
|
744
|
-
const session = registry.sessions[targetId];
|
|
745
|
-
const threadType = session.thread_type || (session.is_main ? 'base' : 'parallel');
|
|
746
|
-
return { success: true, thread_type: threadType, session_id: targetId, is_main: session.is_main };
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
function setSessionThreadType(sessionId, threadType) {
|
|
750
|
-
if (!THREAD_TYPES.includes(threadType)) {
|
|
751
|
-
return {
|
|
752
|
-
success: false,
|
|
753
|
-
error: `Invalid thread type: ${threadType}. Valid: ${THREAD_TYPES.join(', ')}`,
|
|
754
|
-
};
|
|
755
|
-
}
|
|
756
|
-
const registry = loadRegistry();
|
|
757
|
-
if (!registry.sessions[sessionId])
|
|
758
|
-
return { success: false, error: `Session ${sessionId} not found` };
|
|
759
|
-
registry.sessions[sessionId].thread_type = threadType;
|
|
760
|
-
saveRegistry(registry);
|
|
761
|
-
return { success: true, thread_type: threadType };
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
function transitionThread(sessionId, targetType, options = {}) {
|
|
765
|
-
const { force = false } = options;
|
|
766
|
-
const registry = loadRegistry();
|
|
767
|
-
const session = registry.sessions[sessionId];
|
|
768
|
-
if (!session) return { success: false, error: `Session ${sessionId} not found` };
|
|
769
|
-
|
|
770
|
-
const currentType = session.thread_type || (session.is_main ? 'base' : 'parallel');
|
|
771
|
-
const result = sessionThreadMachine.transition(currentType, targetType, { force });
|
|
772
|
-
if (!result.success)
|
|
773
|
-
return { success: false, from: currentType, to: targetType, error: result.error };
|
|
774
|
-
if (result.noop) return { success: true, from: currentType, to: targetType, noop: true };
|
|
775
|
-
|
|
776
|
-
registry.sessions[sessionId].thread_type = targetType;
|
|
777
|
-
registry.sessions[sessionId].thread_transitioned_at = new Date().toISOString();
|
|
778
|
-
saveRegistry(registry);
|
|
779
|
-
return { success: true, from: currentType, to: targetType, forced: result.forced || false };
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
function getValidThreadTransitions(sessionId) {
|
|
783
|
-
const registry = loadRegistry();
|
|
784
|
-
const session = registry.sessions[sessionId];
|
|
785
|
-
if (!session) return { success: false, error: `Session ${sessionId} not found` };
|
|
786
|
-
const currentType = session.thread_type || (session.is_main ? 'base' : 'parallel');
|
|
787
|
-
const validTransitions = sessionThreadMachine.getValidTransitions(currentType);
|
|
788
|
-
return { success: true, current: currentType, validTransitions };
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
// ============================================================================
|
|
792
|
-
// Merge Operation Wrappers (delegate to merge-operations module)
|
|
793
|
-
// ============================================================================
|
|
794
|
-
|
|
795
|
-
function checkMergeability(sessionId) {
|
|
796
|
-
return mergeOps.checkMergeability(sessionId, loadRegistry);
|
|
797
|
-
}
|
|
798
|
-
function getMergePreview(sessionId) {
|
|
799
|
-
return mergeOps.getMergePreview(sessionId, loadRegistry);
|
|
800
|
-
}
|
|
801
|
-
function integrateSession(sessionId, options = {}) {
|
|
802
|
-
return mergeOps.integrateSession(sessionId, options, loadRegistry, saveRegistry, removeLock);
|
|
803
|
-
}
|
|
804
|
-
function generateCommitMessage(session) {
|
|
805
|
-
return mergeOps.generateCommitMessage(session);
|
|
806
|
-
}
|
|
807
|
-
function commitChanges(sessionId, options = {}) {
|
|
808
|
-
return mergeOps.commitChanges(sessionId, options, loadRegistry);
|
|
809
|
-
}
|
|
810
|
-
function stashChanges(sessionId) {
|
|
811
|
-
return mergeOps.stashChanges(sessionId, loadRegistry);
|
|
812
|
-
}
|
|
813
|
-
function unstashChanges(sessionId) {
|
|
814
|
-
return mergeOps.unstashChanges(sessionId);
|
|
815
|
-
}
|
|
816
|
-
function discardChanges(sessionId) {
|
|
817
|
-
return mergeOps.discardChanges(sessionId, loadRegistry);
|
|
818
|
-
}
|
|
819
|
-
function categorizeFile(filePath) {
|
|
820
|
-
return mergeOps.categorizeFile(filePath);
|
|
821
|
-
}
|
|
822
|
-
function getMergeStrategy(category) {
|
|
823
|
-
return mergeOps.getMergeStrategy(category);
|
|
824
|
-
}
|
|
825
|
-
function getConflictingFiles(sessionId) {
|
|
826
|
-
return mergeOps.getConflictingFiles(sessionId, loadRegistry);
|
|
827
|
-
}
|
|
828
|
-
function getMergeHistory() {
|
|
829
|
-
return mergeOps.getMergeHistory();
|
|
830
|
-
}
|
|
831
|
-
function smartMerge(sessionId, options = {}) {
|
|
832
|
-
return mergeOps.smartMerge(
|
|
833
|
-
sessionId,
|
|
834
|
-
options,
|
|
835
|
-
loadRegistry,
|
|
836
|
-
saveRegistry,
|
|
837
|
-
removeLock,
|
|
838
|
-
unregisterSession
|
|
839
|
-
);
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
// ============================================================================
|
|
843
|
-
// CLI Interface
|
|
844
|
-
// ============================================================================
|
|
845
|
-
|
|
846
|
-
function main() {
|
|
847
|
-
const args = process.argv.slice(2);
|
|
848
|
-
const command = args[0];
|
|
849
241
|
|
|
850
242
|
switch (command) {
|
|
851
243
|
case 'register': {
|
|
@@ -856,41 +248,27 @@ function main() {
|
|
|
856
248
|
}
|
|
857
249
|
|
|
858
250
|
case 'unregister': {
|
|
859
|
-
const
|
|
860
|
-
if (
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
} else console.log(JSON.stringify({ success: false, error: 'Session ID required' }));
|
|
251
|
+
const id = requireId();
|
|
252
|
+
if (!id) return;
|
|
253
|
+
unregisterSession(id);
|
|
254
|
+
console.log(JSON.stringify({ success: true }));
|
|
864
255
|
break;
|
|
865
256
|
}
|
|
866
257
|
|
|
867
258
|
case 'create': {
|
|
868
|
-
const options =
|
|
869
|
-
|
|
870
|
-
for (let i = 1; i < args.length; i++) {
|
|
871
|
-
const arg = args[i];
|
|
872
|
-
if (arg.startsWith('--')) {
|
|
873
|
-
const key = arg.slice(2).split('=')[0];
|
|
874
|
-
if (!allowedKeys.includes(key)) {
|
|
875
|
-
console.log(JSON.stringify({ success: false, error: `Unknown option: --${key}` }));
|
|
876
|
-
return;
|
|
877
|
-
}
|
|
878
|
-
const eqIndex = arg.indexOf('=');
|
|
879
|
-
if (eqIndex !== -1) options[key] = arg.slice(eqIndex + 1);
|
|
880
|
-
else if (args[i + 1] && !args[i + 1].startsWith('--')) options[key] = args[++i];
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
// Team mode: create a native Agent Teams session
|
|
259
|
+
const options = parseOpts(1, ['nickname', 'branch', 'timeout', 'mode', 'template']);
|
|
260
|
+
if (!options) return;
|
|
885
261
|
if (options.mode === 'team') {
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
262
|
+
console.log(
|
|
263
|
+
JSON.stringify(
|
|
264
|
+
createTeamSession({
|
|
265
|
+
template: options.template || 'fullstack',
|
|
266
|
+
nickname: options.nickname,
|
|
267
|
+
})
|
|
268
|
+
)
|
|
269
|
+
);
|
|
891
270
|
break;
|
|
892
271
|
}
|
|
893
|
-
|
|
894
272
|
if (options.timeout) {
|
|
895
273
|
options.timeout = parseInt(options.timeout, 10);
|
|
896
274
|
if (isNaN(options.timeout) || options.timeout < 1000) {
|
|
@@ -958,14 +336,11 @@ function main() {
|
|
|
958
336
|
}
|
|
959
337
|
|
|
960
338
|
case 'get': {
|
|
961
|
-
const
|
|
962
|
-
if (!
|
|
963
|
-
|
|
964
|
-
return;
|
|
965
|
-
}
|
|
966
|
-
const session = getSession(sessionId);
|
|
339
|
+
const id = requireId();
|
|
340
|
+
if (!id) return;
|
|
341
|
+
const session = getSession(id);
|
|
967
342
|
if (!session) {
|
|
968
|
-
console.log(JSON.stringify({ success: false, error: `Session ${
|
|
343
|
+
console.log(JSON.stringify({ success: false, error: `Session ${id} not found` }));
|
|
969
344
|
return;
|
|
970
345
|
}
|
|
971
346
|
console.log(JSON.stringify({ success: true, ...session }));
|
|
@@ -973,219 +348,80 @@ function main() {
|
|
|
973
348
|
}
|
|
974
349
|
|
|
975
350
|
case 'full-status': {
|
|
976
|
-
|
|
977
|
-
const cwd = process.cwd();
|
|
978
|
-
const registry = loadRegistry();
|
|
979
|
-
const branch = getCurrentBranch();
|
|
980
|
-
const story = getCurrentStory();
|
|
981
|
-
const pid = process.ppid || process.pid;
|
|
982
|
-
|
|
983
|
-
let sessionId = null,
|
|
984
|
-
isNew = false;
|
|
985
|
-
for (const [id, session] of Object.entries(registry.sessions)) {
|
|
986
|
-
if (session.path === cwd) {
|
|
987
|
-
sessionId = id;
|
|
988
|
-
break;
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
if (sessionId) {
|
|
993
|
-
registry.sessions[sessionId].branch = branch;
|
|
994
|
-
registry.sessions[sessionId].story = story ? story.id : null;
|
|
995
|
-
registry.sessions[sessionId].last_active = new Date().toISOString();
|
|
996
|
-
if (nickname) registry.sessions[sessionId].nickname = nickname;
|
|
997
|
-
if (!registry.sessions[sessionId].thread_type)
|
|
998
|
-
registry.sessions[sessionId].thread_type = registry.sessions[sessionId].is_main
|
|
999
|
-
? 'base'
|
|
1000
|
-
: 'parallel';
|
|
1001
|
-
writeLock(sessionId, pid);
|
|
1002
|
-
} else {
|
|
1003
|
-
sessionId = String(registry.next_id);
|
|
1004
|
-
registry.next_id++;
|
|
1005
|
-
const isMain = cwd === ROOT && !isGitWorktree(cwd);
|
|
1006
|
-
registry.sessions[sessionId] = {
|
|
1007
|
-
path: cwd,
|
|
1008
|
-
branch,
|
|
1009
|
-
story: story ? story.id : null,
|
|
1010
|
-
nickname: nickname || null,
|
|
1011
|
-
created: new Date().toISOString(),
|
|
1012
|
-
last_active: new Date().toISOString(),
|
|
1013
|
-
is_main: isMain,
|
|
1014
|
-
thread_type: isMain ? 'base' : 'parallel',
|
|
1015
|
-
};
|
|
1016
|
-
writeLock(sessionId, pid);
|
|
1017
|
-
isNew = true;
|
|
1018
|
-
}
|
|
1019
|
-
saveRegistry(registry);
|
|
1020
|
-
|
|
1021
|
-
const cleanupResult = cleanupStaleLocks(registry);
|
|
1022
|
-
const filteredCleanup = {
|
|
1023
|
-
count: cleanupResult.sessions.filter(s => String(s.id) !== String(sessionId)).length,
|
|
1024
|
-
sessions: cleanupResult.sessions.filter(s => String(s.id) !== String(sessionId)),
|
|
1025
|
-
};
|
|
1026
|
-
|
|
1027
|
-
const sessions = [];
|
|
1028
|
-
let otherActive = 0;
|
|
1029
|
-
for (const [id, session] of Object.entries(registry.sessions)) {
|
|
1030
|
-
const active = isSessionActive(id);
|
|
1031
|
-
const isCurrent = session.path === cwd;
|
|
1032
|
-
sessions.push({ id, ...session, active, current: isCurrent });
|
|
1033
|
-
if (active && !isCurrent) otherActive++;
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
console.log(
|
|
1037
|
-
JSON.stringify({
|
|
1038
|
-
registered: true,
|
|
1039
|
-
id: sessionId,
|
|
1040
|
-
isNew,
|
|
1041
|
-
current: sessions.find(s => s.current) || null,
|
|
1042
|
-
otherActive,
|
|
1043
|
-
total: sessions.length,
|
|
1044
|
-
cleaned: filteredCleanup.count,
|
|
1045
|
-
cleanedSessions: filteredCleanup.sessions,
|
|
1046
|
-
})
|
|
1047
|
-
);
|
|
351
|
+
console.log(JSON.stringify(fullStatus(args[1] || null)));
|
|
1048
352
|
break;
|
|
1049
353
|
}
|
|
1050
354
|
|
|
1051
355
|
case 'check-merge': {
|
|
1052
|
-
const
|
|
1053
|
-
if (!
|
|
1054
|
-
|
|
1055
|
-
return;
|
|
1056
|
-
}
|
|
1057
|
-
console.log(JSON.stringify(checkMergeability(sessionId)));
|
|
356
|
+
const id = requireId();
|
|
357
|
+
if (!id) return;
|
|
358
|
+
console.log(JSON.stringify(checkMergeability(id)));
|
|
1058
359
|
break;
|
|
1059
360
|
}
|
|
1060
361
|
case 'merge-preview': {
|
|
1061
|
-
const
|
|
1062
|
-
if (!
|
|
1063
|
-
|
|
1064
|
-
return;
|
|
1065
|
-
}
|
|
1066
|
-
console.log(JSON.stringify(getMergePreview(sessionId)));
|
|
362
|
+
const id = requireId();
|
|
363
|
+
if (!id) return;
|
|
364
|
+
console.log(JSON.stringify(getMergePreview(id)));
|
|
1067
365
|
break;
|
|
1068
366
|
}
|
|
1069
|
-
|
|
1070
367
|
case 'integrate': {
|
|
1071
|
-
const
|
|
1072
|
-
if (!
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
if (arg.startsWith('--')) {
|
|
1081
|
-
const eqIndex = arg.indexOf('=');
|
|
1082
|
-
let key, value;
|
|
1083
|
-
if (eqIndex !== -1) {
|
|
1084
|
-
key = arg.slice(2, eqIndex);
|
|
1085
|
-
value = arg.slice(eqIndex + 1);
|
|
1086
|
-
} else {
|
|
1087
|
-
key = arg.slice(2);
|
|
1088
|
-
value = args[++i];
|
|
1089
|
-
}
|
|
1090
|
-
if (!allowedKeys.includes(key)) {
|
|
1091
|
-
console.log(JSON.stringify({ success: false, error: `Unknown option: --${key}` }));
|
|
1092
|
-
return;
|
|
1093
|
-
}
|
|
1094
|
-
if (key === 'deleteBranch' || key === 'deleteWorktree') options[key] = value !== 'false';
|
|
1095
|
-
else options[key] = value;
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
console.log(JSON.stringify(integrateSession(sessionId, options)));
|
|
368
|
+
const id = requireId();
|
|
369
|
+
if (!id) return;
|
|
370
|
+
const opts = parseOpts(
|
|
371
|
+
2,
|
|
372
|
+
['strategy', 'deleteBranch', 'deleteWorktree', 'message'],
|
|
373
|
+
['deleteBranch', 'deleteWorktree']
|
|
374
|
+
);
|
|
375
|
+
if (!opts) return;
|
|
376
|
+
console.log(JSON.stringify(integrateSession(id, opts)));
|
|
1099
377
|
break;
|
|
1100
378
|
}
|
|
1101
|
-
|
|
1102
379
|
case 'commit-changes': {
|
|
1103
|
-
const
|
|
1104
|
-
if (!
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
const options = {};
|
|
1109
|
-
for (let i = 2; i < args.length; i++) {
|
|
1110
|
-
const arg = args[i];
|
|
1111
|
-
if (arg.startsWith('--message=')) options.message = arg.slice(10);
|
|
1112
|
-
else if (arg === '--message' && args[i + 1]) options.message = args[++i];
|
|
1113
|
-
}
|
|
1114
|
-
console.log(JSON.stringify(commitChanges(sessionId, options)));
|
|
380
|
+
const id = requireId();
|
|
381
|
+
if (!id) return;
|
|
382
|
+
const opts = parseOpts(2, ['message']);
|
|
383
|
+
if (!opts) return;
|
|
384
|
+
console.log(JSON.stringify(commitChanges(id, opts)));
|
|
1115
385
|
break;
|
|
1116
386
|
}
|
|
1117
|
-
|
|
1118
387
|
case 'stash': {
|
|
1119
|
-
const
|
|
1120
|
-
if (!
|
|
1121
|
-
|
|
1122
|
-
return;
|
|
1123
|
-
}
|
|
1124
|
-
console.log(JSON.stringify(stashChanges(sessionId)));
|
|
388
|
+
const id = requireId();
|
|
389
|
+
if (!id) return;
|
|
390
|
+
console.log(JSON.stringify(stashChanges(id)));
|
|
1125
391
|
break;
|
|
1126
392
|
}
|
|
1127
393
|
case 'unstash': {
|
|
1128
|
-
const
|
|
1129
|
-
if (!
|
|
1130
|
-
|
|
1131
|
-
return;
|
|
1132
|
-
}
|
|
1133
|
-
console.log(JSON.stringify(unstashChanges(sessionId)));
|
|
394
|
+
const id = requireId();
|
|
395
|
+
if (!id) return;
|
|
396
|
+
console.log(JSON.stringify(unstashChanges(id)));
|
|
1134
397
|
break;
|
|
1135
398
|
}
|
|
1136
399
|
case 'discard-changes': {
|
|
1137
|
-
const
|
|
1138
|
-
if (!
|
|
1139
|
-
|
|
1140
|
-
return;
|
|
1141
|
-
}
|
|
1142
|
-
console.log(JSON.stringify(discardChanges(sessionId)));
|
|
400
|
+
const id = requireId();
|
|
401
|
+
if (!id) return;
|
|
402
|
+
console.log(JSON.stringify(discardChanges(id)));
|
|
1143
403
|
break;
|
|
1144
404
|
}
|
|
1145
|
-
|
|
1146
405
|
case 'smart-merge': {
|
|
1147
|
-
const
|
|
1148
|
-
if (!
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
if (arg.startsWith('--')) {
|
|
1157
|
-
const eqIndex = arg.indexOf('=');
|
|
1158
|
-
let key, value;
|
|
1159
|
-
if (eqIndex !== -1) {
|
|
1160
|
-
key = arg.slice(2, eqIndex);
|
|
1161
|
-
value = arg.slice(eqIndex + 1);
|
|
1162
|
-
} else {
|
|
1163
|
-
key = arg.slice(2);
|
|
1164
|
-
value = args[++i];
|
|
1165
|
-
}
|
|
1166
|
-
if (!allowedKeys.includes(key)) {
|
|
1167
|
-
console.log(JSON.stringify({ success: false, error: `Unknown option: --${key}` }));
|
|
1168
|
-
return;
|
|
1169
|
-
}
|
|
1170
|
-
if (key === 'deleteBranch' || key === 'deleteWorktree') options[key] = value !== 'false';
|
|
1171
|
-
else options[key] = value;
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
console.log(JSON.stringify(smartMerge(sessionId, options), null, 2));
|
|
406
|
+
const id = requireId();
|
|
407
|
+
if (!id) return;
|
|
408
|
+
const opts = parseOpts(
|
|
409
|
+
2,
|
|
410
|
+
['strategy', 'deleteBranch', 'deleteWorktree', 'message'],
|
|
411
|
+
['deleteBranch', 'deleteWorktree']
|
|
412
|
+
);
|
|
413
|
+
if (!opts) return;
|
|
414
|
+
console.log(JSON.stringify(smartMerge(id, opts), null, 2));
|
|
1175
415
|
break;
|
|
1176
416
|
}
|
|
1177
|
-
|
|
1178
417
|
case 'merge-history': {
|
|
1179
418
|
console.log(JSON.stringify(getMergeHistory(), null, 2));
|
|
1180
419
|
break;
|
|
1181
420
|
}
|
|
1182
421
|
case 'switch': {
|
|
1183
|
-
const
|
|
1184
|
-
if (!
|
|
1185
|
-
|
|
1186
|
-
return;
|
|
1187
|
-
}
|
|
1188
|
-
console.log(JSON.stringify(switchSession(sessionIdOrNickname), null, 2));
|
|
422
|
+
const id = requireId('Session ID or nickname');
|
|
423
|
+
if (!id) return;
|
|
424
|
+
console.log(JSON.stringify(switchSession(id), null, 2));
|
|
1189
425
|
break;
|
|
1190
426
|
}
|
|
1191
427
|
case 'active': {
|
|
@@ -1252,19 +488,13 @@ ${c.cyan}Commands:${c.reset}
|
|
|
1252
488
|
}
|
|
1253
489
|
}
|
|
1254
490
|
|
|
1255
|
-
//
|
|
1256
|
-
// Exports
|
|
1257
|
-
// ============================================================================
|
|
1258
|
-
|
|
491
|
+
// --- Exports ---
|
|
1259
492
|
module.exports = {
|
|
1260
|
-
// Registry injection (for testing)
|
|
1261
493
|
injectRegistry,
|
|
1262
494
|
getRegistryInstance,
|
|
1263
495
|
resetRegistryCache,
|
|
1264
|
-
// Registry access
|
|
1265
496
|
loadRegistry,
|
|
1266
497
|
saveRegistry,
|
|
1267
|
-
// Session management
|
|
1268
498
|
registerSession,
|
|
1269
499
|
unregisterSession,
|
|
1270
500
|
getSession,
|
|
@@ -1278,18 +508,15 @@ module.exports = {
|
|
|
1278
508
|
isSessionActiveAsync,
|
|
1279
509
|
cleanupStaleLocks,
|
|
1280
510
|
cleanupStaleLocksAsync,
|
|
1281
|
-
// Session switching
|
|
1282
511
|
switchSession,
|
|
1283
512
|
clearActiveSession,
|
|
1284
513
|
getActiveSession,
|
|
1285
|
-
// Thread type tracking
|
|
1286
514
|
THREAD_TYPES,
|
|
1287
515
|
detectThreadType,
|
|
1288
516
|
getSessionThreadType,
|
|
1289
517
|
setSessionThreadType,
|
|
1290
518
|
transitionThread,
|
|
1291
519
|
getValidThreadTransitions,
|
|
1292
|
-
// Merge operations (delegated to module)
|
|
1293
520
|
getMainBranch,
|
|
1294
521
|
checkMergeability,
|
|
1295
522
|
getMergePreview,
|
|
@@ -1303,18 +530,15 @@ module.exports = {
|
|
|
1303
530
|
categorizeFile,
|
|
1304
531
|
getMergeStrategy,
|
|
1305
532
|
getMergeHistory,
|
|
1306
|
-
// Kanban visualization
|
|
1307
533
|
SESSION_PHASES,
|
|
1308
534
|
getSessionPhase,
|
|
1309
535
|
getSessionPhaseAsync,
|
|
1310
536
|
getSessionPhasesAsync,
|
|
1311
537
|
renderKanbanBoard,
|
|
1312
538
|
renderKanbanBoardAsync,
|
|
1313
|
-
// Display
|
|
1314
539
|
formatSessionsTable,
|
|
1315
540
|
getFileDetails,
|
|
1316
541
|
getSessionsHealth,
|
|
1317
|
-
// Internal utilities (for testing)
|
|
1318
542
|
execGitAsync,
|
|
1319
543
|
gitCache,
|
|
1320
544
|
};
|