agileflow 2.91.0 → 2.92.0

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