agileflow 2.94.1 → 2.95.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 (73) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/lib/colors.generated.js +117 -0
  3. package/lib/colors.js +59 -109
  4. package/lib/generator-factory.js +333 -0
  5. package/lib/path-utils.js +49 -0
  6. package/lib/session-registry.js +25 -15
  7. package/lib/smart-json-file.js +40 -32
  8. package/lib/state-machine.js +286 -0
  9. package/package.json +1 -1
  10. package/scripts/agileflow-configure.js +7 -6
  11. package/scripts/archive-completed-stories.sh +86 -11
  12. package/scripts/babysit-context-restore.js +89 -0
  13. package/scripts/claude-tmux.sh +111 -5
  14. package/scripts/damage-control/bash-tool-damage-control.js +11 -247
  15. package/scripts/damage-control/edit-tool-damage-control.js +9 -249
  16. package/scripts/damage-control/write-tool-damage-control.js +9 -244
  17. package/scripts/generate-colors.js +314 -0
  18. package/scripts/lib/colors.generated.sh +82 -0
  19. package/scripts/lib/colors.sh +10 -70
  20. package/scripts/lib/configure-features.js +401 -0
  21. package/scripts/lib/context-loader.js +181 -52
  22. package/scripts/precompact-context.sh +54 -17
  23. package/scripts/session-coordinator.sh +2 -2
  24. package/scripts/session-manager.js +653 -10
  25. package/src/core/commands/audit.md +93 -0
  26. package/src/core/commands/auto.md +73 -0
  27. package/src/core/commands/babysit.md +169 -13
  28. package/src/core/commands/baseline.md +73 -0
  29. package/src/core/commands/batch.md +64 -0
  30. package/src/core/commands/blockers.md +60 -0
  31. package/src/core/commands/board.md +66 -0
  32. package/src/core/commands/choose.md +77 -0
  33. package/src/core/commands/ci.md +77 -0
  34. package/src/core/commands/compress.md +27 -1
  35. package/src/core/commands/configure.md +126 -10
  36. package/src/core/commands/council.md +74 -0
  37. package/src/core/commands/debt.md +72 -0
  38. package/src/core/commands/deploy.md +73 -0
  39. package/src/core/commands/deps.md +68 -0
  40. package/src/core/commands/docs.md +60 -0
  41. package/src/core/commands/feedback.md +68 -0
  42. package/src/core/commands/ideate.md +74 -0
  43. package/src/core/commands/impact.md +74 -0
  44. package/src/core/commands/install.md +529 -0
  45. package/src/core/commands/maintain.md +558 -0
  46. package/src/core/commands/metrics.md +75 -0
  47. package/src/core/commands/multi-expert.md +74 -0
  48. package/src/core/commands/packages.md +69 -0
  49. package/src/core/commands/readme-sync.md +64 -0
  50. package/src/core/commands/research/analyze.md +285 -121
  51. package/src/core/commands/research/import.md +281 -109
  52. package/src/core/commands/retro.md +76 -0
  53. package/src/core/commands/review.md +72 -0
  54. package/src/core/commands/rlm.md +83 -0
  55. package/src/core/commands/rpi.md +90 -0
  56. package/src/core/commands/session/cleanup.md +214 -12
  57. package/src/core/commands/session/end.md +155 -17
  58. package/src/core/commands/sprint.md +72 -0
  59. package/src/core/commands/story-validate.md +68 -0
  60. package/src/core/commands/template.md +69 -0
  61. package/src/core/commands/tests.md +83 -0
  62. package/src/core/commands/update.md +59 -0
  63. package/src/core/commands/validate-expertise.md +76 -0
  64. package/src/core/commands/velocity.md +74 -0
  65. package/src/core/commands/verify.md +91 -0
  66. package/src/core/commands/whats-new.md +69 -0
  67. package/src/core/commands/workflow.md +88 -0
  68. package/src/core/templates/command-documentation.md +187 -0
  69. package/tools/cli/commands/session.js +1171 -0
  70. package/tools/cli/commands/setup.js +2 -81
  71. package/tools/cli/installers/core/installer.js +0 -5
  72. package/tools/cli/installers/ide/claude-code.js +6 -0
  73. package/tools/cli/lib/config-manager.js +42 -5
@@ -0,0 +1,49 @@
1
+ /**
2
+ * path-utils.js - Unified Path Utilities Module
3
+ *
4
+ * This module consolidates all path-related utilities for AgileFlow CLI.
5
+ * It re-exports from validate-paths.js for backwards compatibility and
6
+ * discoverability, while adding path resolution helpers from path-resolver.js.
7
+ *
8
+ * US-0194: Consolidate Path Matching and Validation Logic
9
+ *
10
+ * Features:
11
+ * - Path traversal prevention (validatePath, hasUnsafePathPatterns)
12
+ * - Symlink chain validation (checkSymlinkChainDepth)
13
+ * - Filename sanitization (sanitizeFilename)
14
+ * - Path resolution with project root detection (PathResolver)
15
+ *
16
+ * Usage:
17
+ * const { validatePath, PathResolver } = require('./path-utils');
18
+ *
19
+ * // Validate a path is safe and within base directory
20
+ * const result = validatePath('./config.yaml', '/project/root', { allowSymlinks: false });
21
+ *
22
+ * // Use PathResolver for project-aware path operations
23
+ * const resolver = new PathResolver('/project/root');
24
+ * const docsPath = resolver.getDocsDir();
25
+ */
26
+
27
+ // Re-export all path validation utilities from validate-paths.js
28
+ const validatePaths = require('./validate-paths');
29
+
30
+ // Re-export PathResolver for convenient access
31
+ const { PathResolver, getDefaultResolver, getAllPaths } = require('./path-resolver');
32
+
33
+ module.exports = {
34
+ // Path validation (from validate-paths.js)
35
+ PathValidationError: validatePaths.PathValidationError,
36
+ checkSymlinkChainDepth: validatePaths.checkSymlinkChainDepth,
37
+ validatePath: validatePaths.validatePath,
38
+ validatePathSync: validatePaths.validatePathSync,
39
+ hasUnsafePathPatterns: validatePaths.hasUnsafePathPatterns,
40
+ sanitizeFilename: validatePaths.sanitizeFilename,
41
+
42
+ // Path resolution (from path-resolver.js)
43
+ PathResolver,
44
+ getDefaultResolver,
45
+ getAllPaths,
46
+
47
+ // Convenience: full module access for advanced usage
48
+ paths: validatePaths,
49
+ };
@@ -32,6 +32,7 @@ const EventEmitter = require('events');
32
32
  const fs = require('fs');
33
33
  const path = require('path');
34
34
  const SmartJsonFile = require('./smart-json-file');
35
+ const { success, failure, failureFromError } = require('./result-schema');
35
36
 
36
37
  /**
37
38
  * Session Registry Event Bus
@@ -286,13 +287,13 @@ class SessionRegistry extends EventEmitter {
286
287
  /**
287
288
  * Unregister a session
288
289
  * @param {number|string} sessionId - Session ID
289
- * @returns {Promise<{ok: boolean, found: boolean, error?: Error}>}
290
+ * @returns {Promise<Result<{found: boolean}>>}
290
291
  */
291
292
  async unregisterSession(sessionId) {
292
293
  const registry = await this.load(true);
293
294
 
294
295
  if (!registry.sessions || !registry.sessions[sessionId]) {
295
- return { ok: true, found: false };
296
+ return success({ found: false });
296
297
  }
297
298
 
298
299
  delete registry.sessions[sessionId];
@@ -301,23 +302,25 @@ class SessionRegistry extends EventEmitter {
301
302
  if (result.ok) {
302
303
  this._auditLog('unregister', { sessionId });
303
304
  this.emit('unregistered', { sessionId });
304
- return { ok: true, found: true };
305
+ return success({ found: true });
305
306
  }
306
307
 
307
- return { ...result, found: true };
308
+ return failure('EUNKNOWN', result.error || 'Failed to save registry', {
309
+ context: { found: true },
310
+ });
308
311
  }
309
312
 
310
313
  /**
311
314
  * Update a session
312
315
  * @param {number|string} sessionId - Session ID
313
316
  * @param {Object} updates - Fields to update
314
- * @returns {Promise<{ok: boolean, found: boolean, error?: Error}>}
317
+ * @returns {Promise<Result<{found: boolean}>>}
315
318
  */
316
319
  async updateSession(sessionId, updates) {
317
320
  const registry = await this.load(true);
318
321
 
319
322
  if (!registry.sessions || !registry.sessions[sessionId]) {
320
- return { ok: false, found: false, error: new Error(`Session ${sessionId} not found`) };
323
+ return failure('ENOENT', `Session ${sessionId} not found`, { context: { found: false } });
321
324
  }
322
325
 
323
326
  registry.sessions[sessionId] = {
@@ -331,10 +334,12 @@ class SessionRegistry extends EventEmitter {
331
334
  if (result.ok) {
332
335
  this._auditLog('update', { sessionId, updates });
333
336
  this.emit('updated', { sessionId, changes: updates });
334
- return { ok: true, found: true };
337
+ return success({ found: true });
335
338
  }
336
339
 
337
- return { ...result, found: true };
340
+ return failure('EUNKNOWN', result.error || 'Failed to save registry', {
341
+ context: { found: true },
342
+ });
338
343
  }
339
344
 
340
345
  /**
@@ -359,11 +364,11 @@ class SessionRegistry extends EventEmitter {
359
364
 
360
365
  /**
361
366
  * Commit all batched changes in one write
362
- * @returns {Promise<{ok: boolean, applied: number, error?: Error}>}
367
+ * @returns {Promise<Result<{applied: number}>>}
363
368
  */
364
369
  async commitBatch() {
365
370
  if (!this._batchMode) {
366
- return { ok: false, applied: 0, error: new Error('Not in batch mode') };
371
+ return failure('EINVAL', 'Not in batch mode', { context: { applied: 0 } });
367
372
  }
368
373
 
369
374
  const registry = await this.load(true);
@@ -415,10 +420,10 @@ class SessionRegistry extends EventEmitter {
415
420
 
416
421
  if (result.ok) {
417
422
  this._auditLog('batch', { applied });
418
- return { ok: true, applied };
423
+ return success({ applied });
419
424
  }
420
425
 
421
- return { ...result, applied };
426
+ return failure('EUNKNOWN', result.error || 'Failed to save registry', { context: { applied } });
422
427
  }
423
428
 
424
429
  /**
@@ -447,7 +452,7 @@ class SessionRegistry extends EventEmitter {
447
452
  /**
448
453
  * Clean up stale sessions
449
454
  * @param {Function} isAlive - Function to check if session is alive (sessionId) => boolean
450
- * @returns {Promise<{ok: boolean, cleaned: number}>}
455
+ * @returns {Promise<Result<{cleaned: number}>>}
451
456
  */
452
457
  async cleanupStaleSessions(isAlive) {
453
458
  const registry = await this.load(true);
@@ -464,10 +469,15 @@ class SessionRegistry extends EventEmitter {
464
469
 
465
470
  if (cleaned > 0) {
466
471
  const result = await this.save(registry);
467
- return { ok: result.ok, cleaned };
472
+ if (!result.ok) {
473
+ return failure('EUNKNOWN', result.error || 'Failed to save registry', {
474
+ context: { cleaned },
475
+ });
476
+ }
477
+ return success({ cleaned });
468
478
  }
469
479
 
470
- return { ok: true, cleaned: 0 };
480
+ return success({ cleaned: 0 });
471
481
  }
472
482
  }
473
483
 
@@ -31,6 +31,7 @@
31
31
  const fs = require('fs');
32
32
  const path = require('path');
33
33
  const { createTypedError, getErrorCodeFromError, ErrorCodes } = require('./error-codes');
34
+ const { success, failure, failureFromError } = require('./result-schema');
34
35
 
35
36
  // Debug logging
36
37
  const DEBUG = process.env.AGILEFLOW_DEBUG === '1';
@@ -54,7 +55,7 @@ const SECURE_FILE_MODE = 0o600;
54
55
  function checkFilePermissions(mode) {
55
56
  // Skip permission checks on Windows (different permission model)
56
57
  if (process.platform === 'win32') {
57
- return { ok: true };
58
+ return success(undefined);
58
59
  }
59
60
 
60
61
  // Extract permission bits (last 9 bits)
@@ -106,7 +107,7 @@ function checkFilePermissions(mode) {
106
107
  };
107
108
  }
108
109
 
109
- return { ok: true };
110
+ return success(undefined);
110
111
  }
111
112
 
112
113
  /**
@@ -117,13 +118,13 @@ function checkFilePermissions(mode) {
117
118
  function setSecurePermissions(filePath) {
118
119
  // Skip on Windows
119
120
  if (process.platform === 'win32') {
120
- return { ok: true };
121
+ return success(undefined);
121
122
  }
122
123
 
123
124
  try {
124
125
  fs.chmodSync(filePath, SECURE_FILE_MODE);
125
126
  debugLog('setSecurePermissions', { filePath, mode: SECURE_FILE_MODE.toString(8) });
126
- return { ok: true };
127
+ return success(undefined);
127
128
  } catch (err) {
128
129
  const error = createTypedError(
129
130
  `Failed to set secure permissions on ${filePath}: ${err.message}`,
@@ -133,7 +134,7 @@ function setSecurePermissions(filePath) {
133
134
  context: { filePath, mode: SECURE_FILE_MODE },
134
135
  }
135
136
  );
136
- return { ok: false, error };
137
+ return failureFromError(error);
137
138
  }
138
139
  }
139
140
 
@@ -221,12 +222,12 @@ class SmartJsonFile {
221
222
  if (!fs.existsSync(this.filePath)) {
222
223
  if (this.defaultValue !== undefined) {
223
224
  debugLog('read', { status: 'using default value' });
224
- return { ok: true, data: this.defaultValue };
225
+ return success(this.defaultValue);
225
226
  }
226
227
  const error = createTypedError(`File not found: ${this.filePath}`, 'ENOENT', {
227
228
  context: { filePath: this.filePath },
228
229
  });
229
- return { ok: false, error };
230
+ return failureFromError(error);
230
231
  }
231
232
 
232
233
  // Read file
@@ -257,7 +258,7 @@ class SmartJsonFile {
257
258
  'EPARSE',
258
259
  { cause: parseError, context: { filePath: this.filePath } }
259
260
  );
260
- return { ok: false, error };
261
+ return failureFromError(error);
261
262
  }
262
263
 
263
264
  // Validate schema if provided
@@ -270,12 +271,12 @@ class SmartJsonFile {
270
271
  'ESCHEMA',
271
272
  { cause: schemaError, context: { filePath: this.filePath } }
272
273
  );
273
- return { ok: false, error };
274
+ return failureFromError(error);
274
275
  }
275
276
  }
276
277
 
277
278
  debugLog('read', { status: 'success' });
278
- return { ok: true, data };
279
+ return success(data);
279
280
  } catch (err) {
280
281
  lastError = err;
281
282
  debugLog('read', { status: 'error', error: err.message, attempt });
@@ -288,7 +289,7 @@ class SmartJsonFile {
288
289
  cause: err,
289
290
  context: { filePath: this.filePath },
290
291
  });
291
- return { ok: false, error };
292
+ return failureFromError(error);
292
293
  }
293
294
 
294
295
  // Wait before retrying
@@ -308,7 +309,7 @@ class SmartJsonFile {
308
309
  'EUNKNOWN',
309
310
  { cause: lastError, context: { filePath: this.filePath, attempts: this.retries + 1 } }
310
311
  );
311
- return { ok: false, error };
312
+ return failureFromError(error);
312
313
  }
313
314
 
314
315
  /**
@@ -330,7 +331,7 @@ class SmartJsonFile {
330
331
  'ESCHEMA',
331
332
  { cause: schemaError, context: { filePath: this.filePath } }
332
333
  );
333
- return { ok: false, error };
334
+ return failureFromError(error);
334
335
  }
335
336
  }
336
337
 
@@ -368,7 +369,7 @@ class SmartJsonFile {
368
369
 
369
370
  debugLog('write', { status: 'success' });
370
371
 
371
- return { ok: true };
372
+ return success(undefined);
372
373
  } catch (err) {
373
374
  lastError = err;
374
375
  debugLog('write', { status: 'error', error: err.message, attempt });
@@ -394,7 +395,7 @@ class SmartJsonFile {
394
395
  errorCode.code,
395
396
  { cause: err, context: { filePath: this.filePath } }
396
397
  );
397
- return { ok: false, error };
398
+ return failureFromError(error);
398
399
  }
399
400
 
400
401
  // Wait before retrying
@@ -414,7 +415,7 @@ class SmartJsonFile {
414
415
  'EUNKNOWN',
415
416
  { cause: lastError, context: { filePath: this.filePath, attempts: this.retries + 1 } }
416
417
  );
417
- return { ok: false, error };
418
+ return failureFromError(error);
418
419
  }
419
420
 
420
421
  /**
@@ -447,7 +448,7 @@ class SmartJsonFile {
447
448
  cause: modifyError,
448
449
  context: { filePath: this.filePath },
449
450
  });
450
- return { ok: false, error };
451
+ return failureFromError(error);
451
452
  }
452
453
 
453
454
  // Write modified data
@@ -456,7 +457,7 @@ class SmartJsonFile {
456
457
  return writeResult;
457
458
  }
458
459
 
459
- return { ok: true, data: newData };
460
+ return success(newData);
460
461
  }
461
462
 
462
463
  /**
@@ -474,12 +475,12 @@ class SmartJsonFile {
474
475
  async delete() {
475
476
  try {
476
477
  if (!fs.existsSync(this.filePath)) {
477
- return { ok: true }; // Already doesn't exist
478
+ return success(undefined); // Already doesn't exist
478
479
  }
479
480
 
480
481
  fs.unlinkSync(this.filePath);
481
482
  debugLog('delete', { filePath: this.filePath, status: 'success' });
482
- return { ok: true };
483
+ return success(undefined);
483
484
  } catch (err) {
484
485
  const errorCode = getErrorCodeFromError(err);
485
486
  const error = createTypedError(
@@ -487,7 +488,7 @@ class SmartJsonFile {
487
488
  errorCode.code,
488
489
  { cause: err, context: { filePath: this.filePath } }
489
490
  );
490
- return { ok: false, error };
491
+ return failureFromError(error);
491
492
  }
492
493
  }
493
494
 
@@ -499,12 +500,12 @@ class SmartJsonFile {
499
500
  try {
500
501
  if (!fs.existsSync(this.filePath)) {
501
502
  if (this.defaultValue !== undefined) {
502
- return { ok: true, data: this.defaultValue };
503
+ return success(this.defaultValue);
503
504
  }
504
505
  const error = createTypedError(`File not found: ${this.filePath}`, 'ENOENT', {
505
506
  context: { filePath: this.filePath },
506
507
  });
507
- return { ok: false, error };
508
+ return failureFromError(error);
508
509
  }
509
510
 
510
511
  const content = fs.readFileSync(this.filePath, 'utf8');
@@ -514,7 +515,7 @@ class SmartJsonFile {
514
515
  this.schema(data);
515
516
  }
516
517
 
517
- return { ok: true, data };
518
+ return success(data);
518
519
  } catch (err) {
519
520
  const errorCode = getErrorCodeFromError(err);
520
521
  const error = createTypedError(
@@ -522,7 +523,7 @@ class SmartJsonFile {
522
523
  errorCode.code,
523
524
  { cause: err, context: { filePath: this.filePath } }
524
525
  );
525
- return { ok: false, error };
526
+ return failureFromError(error);
526
527
  }
527
528
  }
528
529
 
@@ -559,7 +560,7 @@ class SmartJsonFile {
559
560
  }
560
561
  }
561
562
 
562
- return { ok: true };
563
+ return success(undefined);
563
564
  } catch (err) {
564
565
  // Clean up temp file
565
566
  try {
@@ -576,7 +577,7 @@ class SmartJsonFile {
576
577
  errorCode.code,
577
578
  { cause: err, context: { filePath: this.filePath } }
578
579
  );
579
- return { ok: false, error };
580
+ return failureFromError(error);
580
581
  }
581
582
  }
582
583
  }
@@ -594,7 +595,7 @@ const DEFAULT_TEMP_MAX_AGE_MS = 24 * 60 * 60 * 1000;
594
595
  * @param {Object} [options={}] - Cleanup options
595
596
  * @param {number} [options.maxAgeMs=86400000] - Max age in ms (default: 24 hours)
596
597
  * @param {boolean} [options.dryRun=false] - Don't delete, just report
597
- * @returns {{ok: boolean, cleaned: string[], errors: string[]}}
598
+ * @returns {Result<{cleaned: string[], errors: string[]}>}
598
599
  */
599
600
  function cleanupTempFiles(directory, options = {}) {
600
601
  const { maxAgeMs = DEFAULT_TEMP_MAX_AGE_MS, dryRun = false } = options;
@@ -604,7 +605,7 @@ function cleanupTempFiles(directory, options = {}) {
604
605
 
605
606
  try {
606
607
  if (!fs.existsSync(directory)) {
607
- return { ok: true, cleaned, errors };
608
+ return success({ cleaned, errors });
608
609
  }
609
610
 
610
611
  const now = Date.now();
@@ -646,10 +647,17 @@ function cleanupTempFiles(directory, options = {}) {
646
647
  }
647
648
  }
648
649
 
649
- return { ok: errors.length === 0, cleaned, errors };
650
+ if (errors.length > 0) {
651
+ return failure('EUNKNOWN', 'Some temp files could not be cleaned', {
652
+ context: { cleaned, errors },
653
+ });
654
+ }
655
+ return success({ cleaned, errors });
650
656
  } catch (err) {
651
657
  errors.push(`Directory read error: ${err.message}`);
652
- return { ok: false, cleaned, errors };
658
+ return failure('EUNKNOWN', `Directory read error: ${err.message}`, {
659
+ context: { cleaned, errors },
660
+ });
653
661
  }
654
662
  }
655
663
 
@@ -658,7 +666,7 @@ function cleanupTempFiles(directory, options = {}) {
658
666
  *
659
667
  * @param {string} filePath - Path to the JSON file
660
668
  * @param {Object} [options={}] - Cleanup options
661
- * @returns {{ok: boolean, cleaned: string[], errors: string[]}}
669
+ * @returns {Result<{cleaned: string[], errors: string[]}>}
662
670
  */
663
671
  function cleanupTempFilesFor(filePath, options = {}) {
664
672
  const directory = path.dirname(filePath);