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.
Files changed (127) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +3 -3
  3. package/lib/dashboard-protocol.js +38 -0
  4. package/lib/dashboard-server.js +197 -7
  5. package/lib/feedback.js +36 -9
  6. package/lib/git-operations.js +4 -1
  7. package/lib/merge-operations.js +25 -0
  8. package/lib/progress.js +7 -6
  9. package/lib/session-operations.js +611 -0
  10. package/lib/session-switching.js +191 -0
  11. package/lib/template-loader.js +4 -2
  12. package/lib/worktree-operations.js +5 -25
  13. package/package.json +1 -1
  14. package/scripts/agileflow-configure.js +13 -0
  15. package/scripts/agileflow-welcome.js +11 -6
  16. package/scripts/batch-pmap-loop.js +11 -4
  17. package/scripts/claude-tmux.sh +186 -103
  18. package/scripts/damage-control-bash.js +33 -3
  19. package/scripts/damage-control-edit.js +33 -3
  20. package/scripts/damage-control-write.js +33 -3
  21. package/scripts/lib/configure-features.js +10 -7
  22. package/scripts/lib/configure-repair.js +12 -2
  23. package/scripts/lib/process-cleanup.js +197 -15
  24. package/scripts/obtain-context.js +5 -0
  25. package/scripts/session-manager.js +156 -932
  26. package/scripts/spawn-parallel.js +15 -11
  27. package/src/core/agents/configuration/archival.md +2 -1
  28. package/src/core/agents/configuration/attribution.md +2 -1
  29. package/src/core/agents/configuration/ci.md +2 -1
  30. package/src/core/agents/configuration/damage-control.md +2 -1
  31. package/src/core/agents/configuration/git-config.md +2 -1
  32. package/src/core/agents/configuration/hooks.md +2 -1
  33. package/src/core/agents/configuration/precompact.md +2 -1
  34. package/src/core/agents/configuration/status-line.md +2 -1
  35. package/src/core/agents/configuration/verify.md +2 -1
  36. package/src/core/commands/adr/list.md +1 -1
  37. package/src/core/commands/adr/update.md +1 -1
  38. package/src/core/commands/adr/view.md +1 -1
  39. package/src/core/commands/adr.md +1 -1
  40. package/src/core/commands/agent.md +1 -1
  41. package/src/core/commands/api.md +1 -1
  42. package/src/core/commands/assign.md +1 -1
  43. package/src/core/commands/audit.md +1 -1
  44. package/src/core/commands/auto.md +1 -1
  45. package/src/core/commands/automate.md +1 -1
  46. package/src/core/commands/babysit.md +1 -1
  47. package/src/core/commands/baseline.md +1 -1
  48. package/src/core/commands/batch.md +1 -1
  49. package/src/core/commands/blockers.md +1 -1
  50. package/src/core/commands/board.md +1 -1
  51. package/src/core/commands/changelog.md +1 -1
  52. package/src/core/commands/choose.md +1 -1
  53. package/src/core/commands/ci.md +1 -1
  54. package/src/core/commands/compress.md +1 -1
  55. package/src/core/commands/configure.md +56 -1
  56. package/src/core/commands/context/export.md +1 -1
  57. package/src/core/commands/context/full.md +1 -1
  58. package/src/core/commands/context/note.md +1 -1
  59. package/src/core/commands/council.md +1 -1
  60. package/src/core/commands/debt.md +1 -1
  61. package/src/core/commands/deploy.md +1 -1
  62. package/src/core/commands/deps.md +1 -1
  63. package/src/core/commands/diagnose.md +1 -1
  64. package/src/core/commands/docs.md +1 -1
  65. package/src/core/commands/epic/list.md +1 -1
  66. package/src/core/commands/epic/view.md +1 -1
  67. package/src/core/commands/epic.md +1 -1
  68. package/src/core/commands/feedback.md +1 -1
  69. package/src/core/commands/handoff.md +1 -1
  70. package/src/core/commands/help.md +4 -190
  71. package/src/core/commands/ideate/history.md +1 -1
  72. package/src/core/commands/ideate/new.md +1 -1
  73. package/src/core/commands/impact.md +1 -1
  74. package/src/core/commands/install.md +1 -1
  75. package/src/core/commands/logic/audit.md +1 -1
  76. package/src/core/commands/maintain.md +1 -1
  77. package/src/core/commands/metrics.md +1 -1
  78. package/src/core/commands/multi-expert.md +1 -1
  79. package/src/core/commands/packages.md +1 -1
  80. package/src/core/commands/pr.md +1 -1
  81. package/src/core/commands/readme-sync.md +1 -1
  82. package/src/core/commands/research/analyze.md +1 -1
  83. package/src/core/commands/research/ask.md +1 -1
  84. package/src/core/commands/research/import.md +1 -1
  85. package/src/core/commands/research/list.md +1 -1
  86. package/src/core/commands/research/synthesize.md +1 -1
  87. package/src/core/commands/research/view.md +1 -1
  88. package/src/core/commands/retro.md +1 -1
  89. package/src/core/commands/review.md +1 -1
  90. package/src/core/commands/rlm.md +1 -1
  91. package/src/core/commands/roadmap/analyze.md +1 -1
  92. package/src/core/commands/rpi.md +1 -1
  93. package/src/core/commands/serve.md +127 -0
  94. package/src/core/commands/session/cleanup.md +1 -1
  95. package/src/core/commands/session/end.md +84 -23
  96. package/src/core/commands/session/history.md +1 -1
  97. package/src/core/commands/session/init.md +1 -1
  98. package/src/core/commands/session/new.md +198 -84
  99. package/src/core/commands/session/resume.md +1 -1
  100. package/src/core/commands/session/spawn.md +1 -1
  101. package/src/core/commands/session/status.md +1 -1
  102. package/src/core/commands/skill/create.md +1 -1
  103. package/src/core/commands/skill/delete.md +1 -1
  104. package/src/core/commands/skill/edit.md +1 -1
  105. package/src/core/commands/skill/list.md +1 -1
  106. package/src/core/commands/skill/test.md +1 -1
  107. package/src/core/commands/skill/upgrade.md +1 -1
  108. package/src/core/commands/sprint.md +1 -1
  109. package/src/core/commands/status.md +1 -1
  110. package/src/core/commands/story/list.md +1 -1
  111. package/src/core/commands/story/view.md +1 -1
  112. package/src/core/commands/story-validate.md +1 -1
  113. package/src/core/commands/story.md +1 -1
  114. package/src/core/commands/team/list.md +1 -1
  115. package/src/core/commands/team/start.md +1 -1
  116. package/src/core/commands/team/status.md +1 -1
  117. package/src/core/commands/team/stop.md +1 -1
  118. package/src/core/commands/template.md +1 -1
  119. package/src/core/commands/tests.md +1 -1
  120. package/src/core/commands/update.md +1 -1
  121. package/src/core/commands/validate-expertise.md +1 -1
  122. package/src/core/commands/velocity.md +1 -1
  123. package/src/core/commands/verify.md +1 -1
  124. package/src/core/commands/whats-new.md +1 -1
  125. package/src/core/commands/workflow.md +1 -1
  126. package/tools/cli/installers/ide/codex.js +12 -4
  127. package/tools/cli/lib/content-injector.js +23 -4
@@ -0,0 +1,191 @@
1
+ /**
2
+ * session-switching.js - Session switching and thread type management
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
+
11
+ const { getSessionStatePath } = require('./paths');
12
+ const { sessionThreadMachine } = require('./state-machine');
13
+ const { THREAD_TYPES } = require('./worktree-operations');
14
+
15
+ /**
16
+ * Create session switching operations bound to the given dependencies.
17
+ *
18
+ * @param {object} deps
19
+ * @param {string} deps.ROOT - Project root path
20
+ * @param {Function} deps.loadRegistry - Load registry data
21
+ * @param {Function} deps.saveRegistry - Save registry data
22
+ */
23
+ function createSessionSwitching(deps) {
24
+ const { ROOT, loadRegistry, saveRegistry } = deps;
25
+
26
+ const SESSION_STATE_PATH = getSessionStatePath(ROOT);
27
+
28
+ // ============================================================================
29
+ // Session Switching
30
+ // ============================================================================
31
+
32
+ function switchSession(sessionIdOrNickname) {
33
+ const registry = loadRegistry();
34
+ let targetSession = null,
35
+ targetId = null;
36
+ for (const [id, session] of Object.entries(registry.sessions)) {
37
+ if (id === sessionIdOrNickname || session.nickname === sessionIdOrNickname) {
38
+ targetSession = session;
39
+ targetId = id;
40
+ break;
41
+ }
42
+ }
43
+ if (!targetSession)
44
+ return { success: false, error: `Session "${sessionIdOrNickname}" not found` };
45
+ if (!fs.existsSync(targetSession.path))
46
+ return { success: false, error: `Session directory does not exist: ${targetSession.path}` };
47
+
48
+ let sessionState = {};
49
+ if (fs.existsSync(SESSION_STATE_PATH)) {
50
+ try {
51
+ sessionState = JSON.parse(fs.readFileSync(SESSION_STATE_PATH, 'utf8'));
52
+ } catch (e) {
53
+ /* start fresh */
54
+ }
55
+ }
56
+
57
+ sessionState.active_session = {
58
+ id: targetId,
59
+ nickname: targetSession.nickname,
60
+ path: targetSession.path,
61
+ branch: targetSession.branch,
62
+ switched_at: new Date().toISOString(),
63
+ original_cwd: ROOT,
64
+ };
65
+
66
+ const stateDir = path.dirname(SESSION_STATE_PATH);
67
+ if (!fs.existsSync(stateDir)) fs.mkdirSync(stateDir, { recursive: true });
68
+ fs.writeFileSync(SESSION_STATE_PATH, JSON.stringify(sessionState, null, 2) + '\n');
69
+
70
+ registry.sessions[targetId].last_active = new Date().toISOString();
71
+ saveRegistry(registry);
72
+
73
+ return {
74
+ success: true,
75
+ session: {
76
+ id: targetId,
77
+ nickname: targetSession.nickname,
78
+ path: targetSession.path,
79
+ branch: targetSession.branch,
80
+ },
81
+ path: targetSession.path,
82
+ addDirCommand: `/add-dir ${targetSession.path}`,
83
+ };
84
+ }
85
+
86
+ function clearActiveSession() {
87
+ if (!fs.existsSync(SESSION_STATE_PATH)) return { success: true };
88
+ try {
89
+ const sessionState = JSON.parse(fs.readFileSync(SESSION_STATE_PATH, 'utf8'));
90
+ delete sessionState.active_session;
91
+ fs.writeFileSync(SESSION_STATE_PATH, JSON.stringify(sessionState, null, 2) + '\n');
92
+ return { success: true };
93
+ } catch (e) {
94
+ return { success: false, error: e.message };
95
+ }
96
+ }
97
+
98
+ function getActiveSession() {
99
+ if (!fs.existsSync(SESSION_STATE_PATH)) return { active: false };
100
+ try {
101
+ const sessionState = JSON.parse(fs.readFileSync(SESSION_STATE_PATH, 'utf8'));
102
+ return sessionState.active_session
103
+ ? { active: true, session: sessionState.active_session }
104
+ : { active: false };
105
+ } catch (e) {
106
+ return { active: false };
107
+ }
108
+ }
109
+
110
+ // ============================================================================
111
+ // Thread Type Management
112
+ // ============================================================================
113
+
114
+ function getSessionThreadType(sessionId = null) {
115
+ const registry = loadRegistry();
116
+ const cwd = process.cwd();
117
+ let targetId = sessionId;
118
+ if (!targetId) {
119
+ for (const [id, session] of Object.entries(registry.sessions)) {
120
+ if (session.path === cwd) {
121
+ targetId = id;
122
+ break;
123
+ }
124
+ }
125
+ }
126
+ if (!targetId || !registry.sessions[targetId])
127
+ return { success: false, error: 'Session not found' };
128
+ const session = registry.sessions[targetId];
129
+ const threadType = session.thread_type || (session.is_main ? 'base' : 'parallel');
130
+ return {
131
+ success: true,
132
+ thread_type: threadType,
133
+ session_id: targetId,
134
+ is_main: session.is_main,
135
+ };
136
+ }
137
+
138
+ function setSessionThreadType(sessionId, threadType) {
139
+ if (!THREAD_TYPES.includes(threadType)) {
140
+ return {
141
+ success: false,
142
+ error: `Invalid thread type: ${threadType}. Valid: ${THREAD_TYPES.join(', ')}`,
143
+ };
144
+ }
145
+ const registry = loadRegistry();
146
+ if (!registry.sessions[sessionId])
147
+ return { success: false, error: `Session ${sessionId} not found` };
148
+ registry.sessions[sessionId].thread_type = threadType;
149
+ saveRegistry(registry);
150
+ return { success: true, thread_type: threadType };
151
+ }
152
+
153
+ function transitionThread(sessionId, targetType, options = {}) {
154
+ const { force = false } = options;
155
+ const registry = loadRegistry();
156
+ const session = registry.sessions[sessionId];
157
+ if (!session) return { success: false, error: `Session ${sessionId} not found` };
158
+
159
+ const currentType = session.thread_type || (session.is_main ? 'base' : 'parallel');
160
+ const result = sessionThreadMachine.transition(currentType, targetType, { force });
161
+ if (!result.success)
162
+ return { success: false, from: currentType, to: targetType, error: result.error };
163
+ if (result.noop) return { success: true, from: currentType, to: targetType, noop: true };
164
+
165
+ registry.sessions[sessionId].thread_type = targetType;
166
+ registry.sessions[sessionId].thread_transitioned_at = new Date().toISOString();
167
+ saveRegistry(registry);
168
+ return { success: true, from: currentType, to: targetType, forced: result.forced || false };
169
+ }
170
+
171
+ function getValidThreadTransitions(sessionId) {
172
+ const registry = loadRegistry();
173
+ const session = registry.sessions[sessionId];
174
+ if (!session) return { success: false, error: `Session ${sessionId} not found` };
175
+ const currentType = session.thread_type || (session.is_main ? 'base' : 'parallel');
176
+ const validTransitions = sessionThreadMachine.getValidTransitions(currentType);
177
+ return { success: true, current: currentType, validTransitions };
178
+ }
179
+
180
+ return {
181
+ switchSession,
182
+ clearActiveSession,
183
+ getActiveSession,
184
+ getSessionThreadType,
185
+ setSessionThreadType,
186
+ transitionThread,
187
+ getValidThreadTransitions,
188
+ };
189
+ }
190
+
191
+ module.exports = { createSessionSwitching };
@@ -90,8 +90,10 @@ function extractSimpleFrontmatter(raw) {
90
90
  const key = line.substring(0, colonIdx).trim();
91
91
  let value = line.substring(colonIdx + 1).trim();
92
92
  // Remove quotes if present
93
- if ((value.startsWith('"') && value.endsWith('"')) ||
94
- (value.startsWith("'") && value.endsWith("'"))) {
93
+ if (
94
+ (value.startsWith('"') && value.endsWith('"')) ||
95
+ (value.startsWith("'") && value.endsWith("'"))
96
+ ) {
95
97
  value = value.slice(1, -1);
96
98
  }
97
99
  frontmatter[key] = value;
@@ -57,36 +57,16 @@ function detectThreadType(session, isWorktree = false) {
57
57
  /**
58
58
  * Display progress feedback during long operations.
59
59
  * Returns a function to stop the progress indicator.
60
+ * Uses stderr to avoid corrupting stdout JSON output from callers.
60
61
  *
61
62
  * @param {string} message - Progress message
62
63
  * @returns {function} Stop function
63
64
  */
64
65
  function progressIndicator(message) {
65
- const frames = ['⠋', '⠙', '', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
66
- let frameIndex = 0;
67
- let elapsed = 0;
68
-
69
- // For TTY (interactive terminal), show spinner
70
- if (process.stderr.isTTY) {
71
- const interval = setInterval(() => {
72
- process.stderr.write(`\r${frames[frameIndex++ % frames.length]} ${message}`);
73
- }, 80);
74
- return () => {
75
- clearInterval(interval);
76
- process.stderr.write(`\r${' '.repeat(message.length + 2)}\r`);
77
- };
78
- }
79
-
80
- // For non-TTY (Claude Code, piped output), emit periodic updates to stderr
81
- process.stderr.write(`⏳ ${message}...\n`);
82
- const interval = setInterval(() => {
83
- elapsed += 10;
84
- process.stderr.write(`⏳ Still working... (${elapsed}s elapsed)\n`);
85
- }, 10000); // Update every 10 seconds
86
-
87
- return () => {
88
- clearInterval(interval);
89
- };
66
+ const { FeedbackSpinner } = require('./feedback');
67
+ const spinner = new FeedbackSpinner(message, { stream: process.stderr });
68
+ spinner.start();
69
+ return () => spinner.succeed();
90
70
  }
91
71
 
92
72
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agileflow",
3
- "version": "2.99.0",
3
+ "version": "2.99.2",
4
4
  "description": "AI-driven agile development system for Claude Code, Cursor, Windsurf, and more",
5
5
  "keywords": [
6
6
  "agile",
@@ -44,6 +44,7 @@ const {
44
44
  upgradeFeatures,
45
45
  } = require('./lib/configure-features');
46
46
  const { listScripts, showVersionInfo, repairScripts } = require('./lib/configure-repair');
47
+ const { feedback } = require('../lib/feedback');
47
48
 
48
49
  // ============================================================================
49
50
  // VERSION
@@ -281,7 +282,10 @@ function main() {
281
282
  }
282
283
 
283
284
  // Always detect first
285
+ const spinner = feedback.spinner('Detecting configuration...');
286
+ spinner.start();
284
287
  const status = detectConfig(VERSION);
288
+ spinner.succeed('Configuration detected');
285
289
  const { hasIssues, hasOutdated } = printStatus(status);
286
290
 
287
291
  // Detect only mode
@@ -317,11 +321,17 @@ function main() {
317
321
  actions.disabled = p.disable || [];
318
322
  }
319
323
 
324
+ // Enable/disable specific features with progress tracking
325
+ const totalChanges = enable.length + disable.length;
326
+ const featureTask =
327
+ totalChanges > 1 ? feedback.task('Applying feature changes', totalChanges) : null;
328
+
320
329
  // Enable specific features
321
330
  enable.forEach(f => {
322
331
  if (enableFeature(f, { archivalDays }, VERSION)) {
323
332
  actions.enabled.push(f);
324
333
  }
334
+ if (featureTask) featureTask.step(`Enabled ${f}`);
325
335
  });
326
336
 
327
337
  // Disable specific features
@@ -329,8 +339,11 @@ function main() {
329
339
  if (disableFeature(f, VERSION)) {
330
340
  actions.disabled.push(f);
331
341
  }
342
+ if (featureTask) featureTask.step(`Disabled ${f}`);
332
343
  });
333
344
 
345
+ if (featureTask) featureTask.complete('Feature changes applied');
346
+
334
347
  // Print summary if anything changed
335
348
  if (actions.enabled.length > 0 || actions.disabled.length > 0) {
336
349
  printSummary(actions);
@@ -1965,9 +1965,12 @@ async function main() {
1965
1965
  // Check for multiple Claude processes in the same working directory
1966
1966
  if (processCleanup) {
1967
1967
  try {
1968
- // Check if auto-kill is enabled in metadata
1968
+ // Auto-kill is explicitly opt-in at runtime.
1969
+ // Even if metadata has autoKill=true from older configs, we require
1970
+ // AGILEFLOW_PROCESS_CLEANUP_AUTOKILL=1 to prevent accidental session kills.
1969
1971
  const metadata = cache?.metadata;
1970
- const autoKill = metadata?.features?.processCleanup?.autoKill === true;
1972
+ const autoKillConfigured = metadata?.features?.processCleanup?.autoKill === true;
1973
+ const autoKill = autoKillConfigured && process.env.AGILEFLOW_PROCESS_CLEANUP_AUTOKILL === '1';
1971
1974
 
1972
1975
  const cleanupResult = processCleanup.cleanupDuplicateProcesses({
1973
1976
  rootDir,
@@ -1987,15 +1990,17 @@ async function main() {
1987
1990
  console.log(`${c.dim} └─ PID ${proc.pid} (${proc.method})${c.reset}`);
1988
1991
  });
1989
1992
  } else {
1990
- // Warn only (auto-kill not enabled)
1993
+ // Warn only (auto-kill disabled or skipped by safety guards)
1991
1994
  console.log(
1992
1995
  `${c.amber}⚠️ ${cleanupResult.duplicates} other Claude process(es) in same directory${c.reset}`
1993
1996
  );
1994
1997
  console.log(`${c.slate} This may cause slowdowns and freezing. Options:${c.reset}`);
1995
1998
  console.log(`${c.slate} • Close duplicate Claude windows/tabs${c.reset}`);
1996
- console.log(
1997
- `${c.slate} • Run ${c.skyBlue}/agileflow:configure --enable=processcleanup${c.slate} for auto-cleanup${c.reset}`
1998
- );
1999
+ if (autoKillConfigured) {
2000
+ console.log(
2001
+ `${c.slate} • Auto-kill configured but runtime opt-in is off (safer default)${c.reset}`
2002
+ );
2003
+ }
1999
2004
  }
2000
2005
 
2001
2006
  if (cleanupResult.errors.length > 0) {
@@ -27,6 +27,7 @@ const { c } = require('../lib/colors');
27
27
  const { getProjectRoot } = require('../lib/paths');
28
28
  const { safeReadJSON, safeWriteJSON } = require('../lib/errors');
29
29
  const { parseIntBounded } = require('../lib/validate');
30
+ const { feedback } = require('../lib/feedback');
30
31
 
31
32
  // ===== SESSION STATE HELPERS =====
32
33
 
@@ -261,10 +262,14 @@ function handleBatchLoop(rootDir) {
261
262
  items[currentItem].iterations = (items[currentItem].iterations || 0) + 1;
262
263
 
263
264
  // Run tests for this file
264
- console.log(`${c.blue}Running tests for:${c.reset} ${currentItem}`);
265
- console.log(`${c.dim}${'─'.repeat(50)}${c.reset}`);
266
-
265
+ const testSpinner = feedback.spinner(`Running tests for: ${currentItem}`);
266
+ testSpinner.start();
267
267
  const testResult = runTestsForFile(rootDir, currentItem);
268
+ if (testResult.passed) {
269
+ testSpinner.succeed(`Tests passed for: ${currentItem}`);
270
+ } else {
271
+ testSpinner.fail(`Tests failed for: ${currentItem}`);
272
+ }
268
273
 
269
274
  if (testResult.passed) {
270
275
  console.log(`${c.green}✓ Tests passed${c.reset} (${(testResult.duration / 1000).toFixed(1)}s)`);
@@ -373,8 +378,10 @@ async function handleInit(args, rootDir) {
373
378
  const maxIterations = parseIntBounded(maxArg ? maxArg.split('=')[1] : null, 50, 1, 200);
374
379
 
375
380
  // Resolve glob pattern
376
- console.log(`${c.dim}Resolving pattern: ${pattern}${c.reset}`);
381
+ const globSpinner = feedback.spinner(`Resolving pattern: ${pattern}`);
382
+ globSpinner.start();
377
383
  const files = await resolveGlob(pattern, rootDir);
384
+ globSpinner.succeed(`Resolved ${files.length} files`);
378
385
 
379
386
  if (files.length === 0) {
380
387
  console.log(`${c.yellow}No files found matching: ${pattern}${c.reset}`);