agileflow 2.99.8 → 3.0.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 (65) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/lib/cache-provider.js +155 -0
  3. package/lib/codebase-indexer.js +1 -1
  4. package/lib/content-sanitizer.js +1 -0
  5. package/lib/dashboard-protocol.js +25 -0
  6. package/lib/dashboard-server.js +184 -133
  7. package/lib/errors.js +18 -0
  8. package/lib/file-cache.js +1 -1
  9. package/lib/flag-detection.js +11 -20
  10. package/lib/git-operations.js +15 -33
  11. package/lib/merge-operations.js +40 -34
  12. package/lib/process-executor.js +199 -0
  13. package/lib/registry-cache.js +13 -47
  14. package/lib/skill-loader.js +206 -0
  15. package/lib/smart-json-file.js +2 -4
  16. package/package.json +1 -1
  17. package/scripts/agileflow-configure.js +13 -12
  18. package/scripts/agileflow-statusline.sh +30 -0
  19. package/scripts/agileflow-welcome.js +181 -212
  20. package/scripts/auto-self-improve.js +3 -3
  21. package/scripts/claude-smart.sh +67 -0
  22. package/scripts/claude-tmux.sh +248 -161
  23. package/scripts/damage-control-multi-agent.js +227 -0
  24. package/scripts/lib/bus-utils.js +471 -0
  25. package/scripts/lib/configure-detect.js +5 -6
  26. package/scripts/lib/configure-features.js +44 -0
  27. package/scripts/lib/configure-repair.js +5 -6
  28. package/scripts/lib/configure-utils.js +2 -3
  29. package/scripts/lib/context-formatter.js +87 -8
  30. package/scripts/lib/damage-control-utils.js +37 -3
  31. package/scripts/lib/file-lock.js +392 -0
  32. package/scripts/lib/ideation-index.js +2 -5
  33. package/scripts/lib/lifecycle-detector.js +123 -0
  34. package/scripts/lib/process-cleanup.js +55 -81
  35. package/scripts/lib/scale-detector.js +357 -0
  36. package/scripts/lib/signal-detectors.js +779 -0
  37. package/scripts/lib/story-state-machine.js +1 -1
  38. package/scripts/lib/sync-ideation-status.js +2 -3
  39. package/scripts/lib/task-registry.js +7 -1
  40. package/scripts/lib/team-events.js +357 -0
  41. package/scripts/messaging-bridge.js +79 -36
  42. package/scripts/migrate-ideation-index.js +37 -14
  43. package/scripts/obtain-context.js +37 -19
  44. package/scripts/ralph-loop.js +3 -4
  45. package/scripts/smart-detect.js +390 -0
  46. package/scripts/team-manager.js +174 -30
  47. package/src/core/commands/audit.md +13 -11
  48. package/src/core/commands/babysit.md +162 -115
  49. package/src/core/commands/changelog.md +21 -4
  50. package/src/core/commands/configure.md +105 -2
  51. package/src/core/commands/debt.md +12 -2
  52. package/src/core/commands/feedback.md +7 -6
  53. package/src/core/commands/ideate/history.md +1 -1
  54. package/src/core/commands/ideate/new.md +5 -5
  55. package/src/core/commands/logic/audit.md +2 -2
  56. package/src/core/commands/pr.md +7 -6
  57. package/src/core/commands/research/analyze.md +28 -20
  58. package/src/core/commands/research/ask.md +43 -0
  59. package/src/core/commands/research/import.md +29 -21
  60. package/src/core/commands/research/list.md +8 -7
  61. package/src/core/commands/research/synthesize.md +356 -20
  62. package/src/core/commands/research/view.md +8 -5
  63. package/src/core/commands/review.md +24 -6
  64. package/src/core/commands/skill/create.md +34 -0
  65. package/tools/cli/lib/docs-setup.js +4 -0
@@ -0,0 +1,392 @@
1
+ /**
2
+ * file-lock.js - Atomic file writing with locking for multi-agent safety
3
+ *
4
+ * FAIL-OPEN SEMANTICS:
5
+ * ====================
6
+ * This module prioritizes availability over strict consistency.
7
+ * On lock contention, timeout, or errors, operations proceed
8
+ * with best-effort semantics rather than blocking or crashing.
9
+ *
10
+ * USAGE:
11
+ * ======
12
+ * // Simple atomic JSON write
13
+ * atomicWriteJSON('docs/09-agents/status.json', { stories: {...} });
14
+ *
15
+ * // Read-modify-write (handles concurrency gracefully)
16
+ * atomicReadModifyWrite('docs/09-agents/status.json', (data) => {
17
+ * data.stories['US-0040'].status = 'in-review';
18
+ * return data;
19
+ * });
20
+ *
21
+ * // Manual lock management (use for complex sequences)
22
+ * const lock = acquireLock('docs/09-agents/status.json');
23
+ * if (lock.acquired) {
24
+ * try {
25
+ * // ... do work ...
26
+ * } finally {
27
+ * releaseLock(lock.lockPath);
28
+ * }
29
+ * }
30
+ *
31
+ * IMPLEMENTATION:
32
+ * ===============
33
+ * Lock file + temp file + rename pattern for atomic writes:
34
+ * 1. Create lock file (filePath + '.lock') with PID
35
+ * 2. If lock exists, check if PID is alive. If stale, remove and retry.
36
+ * 3. Write to temp file (filePath + '.tmp.' + random)
37
+ * 4. Rename temp to target (atomic on POSIX)
38
+ * 5. Remove lock file
39
+ *
40
+ * NO EXTERNAL DEPENDENCIES - only Node.js built-ins
41
+ */
42
+
43
+ const fs = require('fs');
44
+ const path = require('path');
45
+ const os = require('os');
46
+ const crypto = require('crypto');
47
+ // Inline colors (no external dependency)
48
+ const c = {
49
+ dim: '\x1b[2m',
50
+ reset: '\x1b[0m',
51
+ };
52
+
53
+ // Default timeout for lock acquisition
54
+ const DEFAULT_LOCK_TIMEOUT_MS = 5000;
55
+
56
+ // Retry configuration
57
+ const LOCK_RETRY_INTERVAL_MS = 50;
58
+ const MAX_LOCK_RETRIES = Math.ceil(DEFAULT_LOCK_TIMEOUT_MS / LOCK_RETRY_INTERVAL_MS);
59
+
60
+ /**
61
+ * Check if a process with given PID is alive
62
+ * Uses process.kill(pid, 0) which is safe (sends no signal, just checks existence)
63
+ *
64
+ * @param {number} pid - Process ID
65
+ * @returns {boolean} True if process exists
66
+ * @private
67
+ */
68
+ function isPidAlive(pid) {
69
+ if (typeof pid !== 'number' || isNaN(pid) || pid <= 0) {
70
+ return false;
71
+ }
72
+ try {
73
+ // process.kill with signal 0 checks if process exists without sending a signal
74
+ process.kill(pid, 0);
75
+ return true;
76
+ } catch (e) {
77
+ // ESRCH = no such process, EPERM = process exists but no permission
78
+ return e.code === 'EPERM';
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Acquire a lock file for a given file path
84
+ * Returns immediately with success/failure - does not wait on lock contention
85
+ *
86
+ * @param {string} filePath - File to lock
87
+ * @param {number} [timeoutMs=5000] - Timeout in milliseconds
88
+ * @returns {{ acquired: boolean, lockPath: string, error?: string }}
89
+ * @public
90
+ */
91
+ function acquireLock(filePath, timeoutMs = DEFAULT_LOCK_TIMEOUT_MS) {
92
+ try {
93
+ const lockPath = filePath + '.lock';
94
+ const startTime = Date.now();
95
+ let retries = 0;
96
+
97
+ while (retries < MAX_LOCK_RETRIES) {
98
+ try {
99
+ // Try to create lock file exclusively
100
+ // fs.openSync with 'wx' flag fails if file exists
101
+ const fd = fs.openSync(lockPath, 'wx');
102
+ fs.writeSync(fd, `${process.pid}\n`);
103
+ fs.closeSync(fd);
104
+
105
+ return {
106
+ acquired: true,
107
+ lockPath: lockPath,
108
+ };
109
+ } catch (e) {
110
+ if (e.code !== 'EEXIST') {
111
+ // Real error (not lock contention)
112
+ return {
113
+ acquired: false,
114
+ lockPath: lockPath,
115
+ error: `Failed to create lock: ${e.message}`,
116
+ };
117
+ }
118
+
119
+ // Lock file exists - check if PID is alive
120
+ try {
121
+ const lockContent = fs.readFileSync(lockPath, 'utf8').trim();
122
+ const lockPid = parseInt(lockContent, 10);
123
+
124
+ if (isNaN(lockPid)) {
125
+ // Corrupted lock file - try to remove and retry
126
+ try {
127
+ fs.unlinkSync(lockPath);
128
+ } catch (unlinkErr) {
129
+ // Ignore unlink errors
130
+ }
131
+ } else if (!isPidAlive(lockPid)) {
132
+ // PID is stale - remove lock and retry
133
+ try {
134
+ fs.unlinkSync(lockPath);
135
+ } catch (unlinkErr) {
136
+ // Ignore unlink errors
137
+ }
138
+ } else {
139
+ // Lock is held by live process
140
+ // Check timeout
141
+ if (Date.now() - startTime > timeoutMs) {
142
+ return {
143
+ acquired: false,
144
+ lockPath: lockPath,
145
+ error: `Lock timeout after ${timeoutMs}ms (held by PID ${lockPid})`,
146
+ };
147
+ }
148
+
149
+ // Wait and retry
150
+ const delay = Math.min(LOCK_RETRY_INTERVAL_MS, 10 + Math.random() * 40);
151
+ const now = Date.now();
152
+ while (Date.now() - now < delay) {
153
+ // Busy-wait for short delays (avoid complexity of setTimeout)
154
+ }
155
+ }
156
+ } catch (readErr) {
157
+ // Could not read lock file - assume stale, retry
158
+ }
159
+ }
160
+
161
+ retries++;
162
+ }
163
+
164
+ // Timeout reached
165
+ return {
166
+ acquired: false,
167
+ lockPath: lockPath,
168
+ error: `Could not acquire lock within ${timeoutMs}ms`,
169
+ };
170
+ } catch (e) {
171
+ // Unexpected error - fail open
172
+ return {
173
+ acquired: false,
174
+ lockPath: filePath + '.lock',
175
+ error: `Unexpected error: ${e.message}`,
176
+ };
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Release a lock file
182
+ * Best-effort - does not throw on errors
183
+ *
184
+ * @param {string} lockPath - Path to lock file (from acquireLock result)
185
+ * @returns {boolean} True if lock was removed, false if already gone or error
186
+ * @public
187
+ */
188
+ function releaseLock(lockPath) {
189
+ try {
190
+ if (fs.existsSync(lockPath)) {
191
+ fs.unlinkSync(lockPath);
192
+ return true;
193
+ }
194
+ return true; // Already gone is success
195
+ } catch (e) {
196
+ // Fail open - don't throw
197
+ return false;
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Generate random string for temp file
203
+ * @returns {string} Random hex string
204
+ * @private
205
+ */
206
+ function generateRandomSuffix() {
207
+ return crypto.randomBytes(8).toString('hex');
208
+ }
209
+
210
+ /**
211
+ * Write JSON data atomically to a file
212
+ * Uses temp file + rename pattern for safety
213
+ *
214
+ * @param {string} filePath - Target file path
215
+ * @param {object} data - Data to write
216
+ * @param {object} [options={}] - Options
217
+ * @param {boolean} [options.force=false] - Skip lock (for non-critical files)
218
+ * @param {number} [options.lockTimeoutMs=5000] - Lock timeout
219
+ * @returns {{ success: boolean, error?: string }}
220
+ * @public
221
+ */
222
+ function atomicWriteJSON(filePath, data, options = {}) {
223
+ const { force = false, lockTimeoutMs = DEFAULT_LOCK_TIMEOUT_MS } = options;
224
+
225
+ try {
226
+ const dir = path.dirname(filePath);
227
+ const tempPath = filePath + '.tmp.' + generateRandomSuffix();
228
+ const lock = force ? null : acquireLock(filePath, lockTimeoutMs);
229
+
230
+ if (!force && !lock.acquired) {
231
+ // Fall back to direct write without lock
232
+ // Log warning in dim text to not clutter output
233
+ if (process.stderr && process.stderr.isTTY) {
234
+ process.stderr.write(
235
+ `${c.dim}[file-lock] Write without lock: ${path.basename(filePath)}${c.reset}\n`
236
+ );
237
+ }
238
+ }
239
+
240
+ try {
241
+ // Ensure directory exists
242
+ if (!fs.existsSync(dir)) {
243
+ fs.mkdirSync(dir, { recursive: true });
244
+ }
245
+
246
+ // Write to temp file
247
+ const jsonStr = JSON.stringify(data, null, 2) + '\n';
248
+ fs.writeFileSync(tempPath, jsonStr, 'utf8');
249
+
250
+ // Atomic rename
251
+ fs.renameSync(tempPath, filePath);
252
+
253
+ return { success: true };
254
+ } finally {
255
+ // Clean up temp file if rename failed
256
+ if (fs.existsSync(tempPath)) {
257
+ try {
258
+ fs.unlinkSync(tempPath);
259
+ } catch (e) {
260
+ // Ignore cleanup errors
261
+ }
262
+ }
263
+
264
+ // Release lock
265
+ if (!force && lock && lock.acquired) {
266
+ releaseLock(lock.lockPath);
267
+ }
268
+ }
269
+ } catch (e) {
270
+ // Fail open - return error but don't throw
271
+ return {
272
+ success: false,
273
+ error: e.message,
274
+ };
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Read, modify, and write JSON file atomically
280
+ * Handles concurrent access gracefully with retry logic
281
+ *
282
+ * @param {string} filePath - Target file path
283
+ * @param {function} modifyFn - Function that takes data, returns modified data
284
+ * @param {object} [options={}] - Options
285
+ * @param {number} [options.lockTimeoutMs=5000] - Lock timeout
286
+ * @param {number} [options.maxRetries=3] - Max retries on write conflict
287
+ * @returns {{ success: boolean, data?: object, error?: string }}
288
+ * @public
289
+ */
290
+ function atomicReadModifyWrite(filePath, modifyFn, options = {}) {
291
+ const { lockTimeoutMs = DEFAULT_LOCK_TIMEOUT_MS, maxRetries = 3 } = options;
292
+
293
+ try {
294
+ let retries = 0;
295
+
296
+ while (retries < maxRetries) {
297
+ const lock = acquireLock(filePath, lockTimeoutMs);
298
+
299
+ if (!lock.acquired) {
300
+ // Could not acquire lock - fail open and return last known good data
301
+ if (fs.existsSync(filePath)) {
302
+ const content = fs.readFileSync(filePath, 'utf8');
303
+ const data = JSON.parse(content);
304
+ return {
305
+ success: false,
306
+ data: data,
307
+ error: `Could not acquire lock (${lock.error || 'unknown'}), returning last known good data`,
308
+ };
309
+ }
310
+
311
+ return {
312
+ success: false,
313
+ error: `Could not acquire lock: ${lock.error || 'unknown'}`,
314
+ };
315
+ }
316
+
317
+ try {
318
+ // Read current data
319
+ if (!fs.existsSync(filePath)) {
320
+ return {
321
+ success: false,
322
+ error: `File does not exist: ${filePath}`,
323
+ };
324
+ }
325
+
326
+ const content = fs.readFileSync(filePath, 'utf8');
327
+ const data = JSON.parse(content);
328
+
329
+ // Apply modification
330
+ const modifiedData = modifyFn(data);
331
+
332
+ // Write atomically
333
+ const dir = path.dirname(filePath);
334
+ const tempPath = filePath + '.tmp.' + generateRandomSuffix();
335
+
336
+ // Ensure directory exists
337
+ if (!fs.existsSync(dir)) {
338
+ fs.mkdirSync(dir, { recursive: true });
339
+ }
340
+
341
+ // Write to temp
342
+ const jsonStr = JSON.stringify(modifiedData, null, 2) + '\n';
343
+ fs.writeFileSync(tempPath, jsonStr, 'utf8');
344
+
345
+ // Atomic rename
346
+ fs.renameSync(tempPath, filePath);
347
+
348
+ return { success: true, data: modifiedData };
349
+ } catch (e) {
350
+ // Clean up temp file if it exists
351
+ const tempPath = filePath + '.tmp.' + generateRandomSuffix();
352
+ if (fs.existsSync(tempPath)) {
353
+ try {
354
+ fs.unlinkSync(tempPath);
355
+ } catch (cleanupErr) {
356
+ // Ignore cleanup errors
357
+ }
358
+ }
359
+
360
+ throw e;
361
+ } finally {
362
+ // Release lock
363
+ if (lock && lock.acquired) {
364
+ releaseLock(lock.lockPath);
365
+ }
366
+ }
367
+ }
368
+
369
+ return {
370
+ success: false,
371
+ error: `Max retries (${maxRetries}) exceeded`,
372
+ };
373
+ } catch (e) {
374
+ // Fail open - return error but don't throw
375
+ return {
376
+ success: false,
377
+ error: e.message,
378
+ };
379
+ }
380
+ }
381
+
382
+ // Export public API
383
+ module.exports = {
384
+ acquireLock,
385
+ releaseLock,
386
+ atomicWriteJSON,
387
+ atomicReadModifyWrite,
388
+
389
+ // For testing only
390
+ _generateRandomSuffix: generateRandomSuffix,
391
+ _isPidAlive: isPidAlive,
392
+ };
@@ -29,6 +29,7 @@
29
29
  const fs = require('fs');
30
30
  const path = require('path');
31
31
  const crypto = require('crypto');
32
+ const { tryOptional } = require('../../lib/errors');
32
33
 
33
34
  // Default index file location
34
35
  const DEFAULT_INDEX_PATH = 'docs/00-meta/ideation-index.json';
@@ -151,11 +152,7 @@ function saveIdeationIndex(rootDir, index) {
151
152
  return { ok: true };
152
153
  } catch (err) {
153
154
  // Clean up temp file if it exists
154
- try {
155
- if (fs.existsSync(tempPath)) {
156
- fs.unlinkSync(tempPath);
157
- }
158
- } catch {}
155
+ tryOptional(() => fs.unlinkSync(tempPath), 'cleanup temp');
159
156
  return { ok: false, error: `Failed to save ideation index: ${err.message}` };
160
157
  }
161
158
  }
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lifecycle-detector.js
4
+ *
5
+ * Determines the current workflow phase based on project signals.
6
+ * Used by smart-detect.js to filter feature recommendations by phase.
7
+ *
8
+ * Phases (in order):
9
+ * pre-story → No active story, selecting what to work on
10
+ * planning → Story selected, planning implementation approach
11
+ * implementation → Actively writing code, files changed
12
+ * post-impl → Code done, reviewing/testing/documenting
13
+ * pre-pr → Ready to create PR, final checks
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ const PHASES = ['pre-story', 'planning', 'implementation', 'post-impl', 'pre-pr'];
19
+
20
+ /**
21
+ * Detect the current lifecycle phase from project signals.
22
+ *
23
+ * @param {Object} signals - Gathered project signals
24
+ * @param {Object} signals.story - Current story info { id, status, owner }
25
+ * @param {Object} signals.git - Git state { branch, filesChanged, isClean, onFeatureBranch }
26
+ * @param {Object} signals.session - Session state { planModeActive, activeCommands }
27
+ * @param {Object} signals.tests - Test state { passing, hasTestSetup }
28
+ * @returns {{ phase: string, confidence: number, reason: string }}
29
+ */
30
+ function detectLifecyclePhase(signals = {}) {
31
+ const { story: rawStory, git: rawGit, session: rawSession, tests: rawTests } = signals;
32
+ const story = rawStory || {};
33
+ const git = rawGit || {};
34
+ const session = rawSession || {};
35
+ const tests = rawTests || {};
36
+
37
+ // Phase 5: pre-pr
38
+ // Story in-progress, tests passing, git clean (or nearly), on feature branch
39
+ if (
40
+ story.status === 'in-progress' &&
41
+ tests.passing === true &&
42
+ git.isClean &&
43
+ git.onFeatureBranch
44
+ ) {
45
+ return {
46
+ phase: 'pre-pr',
47
+ confidence: 0.9,
48
+ reason: 'Tests passing, clean git, on feature branch',
49
+ };
50
+ }
51
+
52
+ // Phase 4: post-impl
53
+ // Story in-progress, tests passing (or test files exist), still has some changes
54
+ if (story.status === 'in-progress' && tests.passing === true && git.filesChanged > 0) {
55
+ return {
56
+ phase: 'post-impl',
57
+ confidence: 0.8,
58
+ reason: 'Tests passing but uncommitted changes remain',
59
+ };
60
+ }
61
+
62
+ // Phase 3: implementation
63
+ // Story in-progress AND files changed (git status non-empty)
64
+ if (story.status === 'in-progress' && git.filesChanged > 0) {
65
+ return {
66
+ phase: 'implementation',
67
+ confidence: 0.85,
68
+ reason: `Actively coding (${git.filesChanged} files changed)`,
69
+ };
70
+ }
71
+
72
+ // Phase 2: planning
73
+ // Story selected/in-progress but no files changed yet, OR plan mode active
74
+ if (session.planModeActive) {
75
+ return {
76
+ phase: 'planning',
77
+ confidence: 0.9,
78
+ reason: 'Plan mode is active',
79
+ };
80
+ }
81
+
82
+ if (story.status === 'in-progress' && (git.filesChanged || 0) === 0) {
83
+ return {
84
+ phase: 'planning',
85
+ confidence: 0.7,
86
+ reason: 'Story in-progress but no files changed yet',
87
+ };
88
+ }
89
+
90
+ // Phase 1: pre-story (default)
91
+ // No current story OR story is ready (not yet started)
92
+ return {
93
+ phase: 'pre-story',
94
+ confidence: story.id ? 0.6 : 0.9,
95
+ reason: story.id ? `Story ${story.id} not yet started (status: ${story.status || 'unknown'})` : 'No active story',
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Get features relevant to a given phase.
101
+ * Returns the set of feature categories that apply.
102
+ *
103
+ * @param {string} phase - Lifecycle phase name
104
+ * @returns {string[]} Array of phase names whose features are relevant
105
+ */
106
+ function getRelevantPhases(phase) {
107
+ // Each phase also includes adjacent phase features (for smooth transitions)
108
+ const phaseMap = {
109
+ 'pre-story': ['pre-story'],
110
+ 'planning': ['pre-story', 'planning'],
111
+ 'implementation': ['planning', 'implementation'],
112
+ 'post-impl': ['implementation', 'post-impl'],
113
+ 'pre-pr': ['post-impl', 'pre-pr'],
114
+ };
115
+
116
+ return phaseMap[phase] || ['pre-story'];
117
+ }
118
+
119
+ module.exports = {
120
+ PHASES,
121
+ detectLifecyclePhase,
122
+ getRelevantPhases,
123
+ };