agileflow 2.94.1 → 2.95.1

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 (74) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +3 -3
  3. package/lib/colors.generated.js +117 -0
  4. package/lib/colors.js +59 -109
  5. package/lib/generator-factory.js +333 -0
  6. package/lib/path-utils.js +49 -0
  7. package/lib/session-registry.js +25 -15
  8. package/lib/smart-json-file.js +40 -32
  9. package/lib/state-machine.js +286 -0
  10. package/package.json +1 -1
  11. package/scripts/agileflow-configure.js +7 -6
  12. package/scripts/archive-completed-stories.sh +86 -11
  13. package/scripts/babysit-context-restore.js +89 -0
  14. package/scripts/claude-tmux.sh +111 -5
  15. package/scripts/damage-control/bash-tool-damage-control.js +11 -247
  16. package/scripts/damage-control/edit-tool-damage-control.js +9 -249
  17. package/scripts/damage-control/write-tool-damage-control.js +9 -244
  18. package/scripts/generate-colors.js +314 -0
  19. package/scripts/lib/colors.generated.sh +82 -0
  20. package/scripts/lib/colors.sh +10 -70
  21. package/scripts/lib/configure-features.js +401 -0
  22. package/scripts/lib/context-loader.js +181 -52
  23. package/scripts/precompact-context.sh +54 -17
  24. package/scripts/session-coordinator.sh +2 -2
  25. package/scripts/session-manager.js +653 -10
  26. package/src/core/commands/audit.md +93 -0
  27. package/src/core/commands/auto.md +73 -0
  28. package/src/core/commands/babysit.md +169 -13
  29. package/src/core/commands/baseline.md +73 -0
  30. package/src/core/commands/batch.md +64 -0
  31. package/src/core/commands/blockers.md +60 -0
  32. package/src/core/commands/board.md +66 -0
  33. package/src/core/commands/choose.md +77 -0
  34. package/src/core/commands/ci.md +77 -0
  35. package/src/core/commands/compress.md +27 -1
  36. package/src/core/commands/configure.md +126 -10
  37. package/src/core/commands/council.md +74 -0
  38. package/src/core/commands/debt.md +72 -0
  39. package/src/core/commands/deploy.md +73 -0
  40. package/src/core/commands/deps.md +68 -0
  41. package/src/core/commands/docs.md +60 -0
  42. package/src/core/commands/feedback.md +68 -0
  43. package/src/core/commands/ideate.md +74 -0
  44. package/src/core/commands/impact.md +74 -0
  45. package/src/core/commands/install.md +529 -0
  46. package/src/core/commands/maintain.md +558 -0
  47. package/src/core/commands/metrics.md +75 -0
  48. package/src/core/commands/multi-expert.md +74 -0
  49. package/src/core/commands/packages.md +69 -0
  50. package/src/core/commands/readme-sync.md +64 -0
  51. package/src/core/commands/research/analyze.md +285 -121
  52. package/src/core/commands/research/import.md +281 -109
  53. package/src/core/commands/retro.md +76 -0
  54. package/src/core/commands/review.md +72 -0
  55. package/src/core/commands/rlm.md +83 -0
  56. package/src/core/commands/rpi.md +90 -0
  57. package/src/core/commands/session/cleanup.md +214 -12
  58. package/src/core/commands/session/end.md +155 -17
  59. package/src/core/commands/sprint.md +72 -0
  60. package/src/core/commands/story-validate.md +68 -0
  61. package/src/core/commands/template.md +69 -0
  62. package/src/core/commands/tests.md +83 -0
  63. package/src/core/commands/update.md +59 -0
  64. package/src/core/commands/validate-expertise.md +76 -0
  65. package/src/core/commands/velocity.md +74 -0
  66. package/src/core/commands/verify.md +91 -0
  67. package/src/core/commands/whats-new.md +69 -0
  68. package/src/core/commands/workflow.md +88 -0
  69. package/src/core/templates/command-documentation.md +187 -0
  70. package/tools/cli/commands/session.js +1171 -0
  71. package/tools/cli/commands/setup.js +2 -81
  72. package/tools/cli/installers/core/installer.js +0 -5
  73. package/tools/cli/installers/ide/claude-code.js +6 -0
  74. package/tools/cli/lib/config-manager.js +42 -5
@@ -0,0 +1,333 @@
1
+ /**
2
+ * GeneratorFactory - Dependency Injection for Content Generators
3
+ *
4
+ * Provides a centralized factory for creating content generators with:
5
+ * - Shared PlaceholderRegistry built once
6
+ * - Dependency injection for testability
7
+ * - Generator registration pattern
8
+ *
9
+ * Usage:
10
+ * const factory = new GeneratorFactory();
11
+ * factory.registerGenerator('help', HelpGenerator);
12
+ * factory.registerGenerator('readme', ReadmeGenerator);
13
+ *
14
+ * // Build registry once, pass to all generators
15
+ * const context = await factory.buildContext();
16
+ * await factory.runAll(context);
17
+ */
18
+
19
+ 'use strict';
20
+
21
+ const { PlaceholderRegistry, createDefaultRegistry } = require('./placeholder-registry');
22
+ const { createContainer, createScannerFactory } = require('./registry-di');
23
+
24
+ /**
25
+ * Generator interface
26
+ * @typedef {Object} IGenerator
27
+ * @property {string} name - Generator name
28
+ * @property {Function} register - Register placeholders: (registry) => void
29
+ * @property {Function} generate - Generate content: (context) => Promise<void>
30
+ */
31
+
32
+ /**
33
+ * GeneratorFactory - Factory for creating and running generators
34
+ */
35
+ class GeneratorFactory {
36
+ /**
37
+ * Create a new GeneratorFactory
38
+ * @param {Object} options - Factory options
39
+ * @param {Object} [options.container] - DI container (from registry-di.js)
40
+ * @param {PlaceholderRegistry} [options.registry] - Pre-built registry
41
+ * @param {Object} [options.paths] - Path configuration
42
+ */
43
+ constructor(options = {}) {
44
+ this._generators = new Map();
45
+ this._container = options.container || createContainer();
46
+ this._registry = options.registry || null;
47
+ this._paths = options.paths || {};
48
+ this._built = false;
49
+ this._context = null;
50
+ }
51
+
52
+ /**
53
+ * Register a generator with the factory
54
+ * @param {string} name - Generator name
55
+ * @param {IGenerator|Function} generator - Generator instance or class
56
+ * @returns {GeneratorFactory} this for chaining
57
+ */
58
+ registerGenerator(name, generator) {
59
+ if (!name || typeof name !== 'string') {
60
+ throw new Error('Generator name must be a non-empty string');
61
+ }
62
+
63
+ // Support both instances and classes
64
+ const instance = typeof generator === 'function' ? new generator() : generator;
65
+
66
+ if (!instance.register || typeof instance.register !== 'function') {
67
+ throw new Error(`Generator "${name}" must have a register(registry) method`);
68
+ }
69
+
70
+ if (!instance.generate || typeof instance.generate !== 'function') {
71
+ throw new Error(`Generator "${name}" must have a generate(context) method`);
72
+ }
73
+
74
+ this._generators.set(name, instance);
75
+
76
+ // Reset built state when new generator is added
77
+ this._built = false;
78
+ this._context = null;
79
+
80
+ return this;
81
+ }
82
+
83
+ /**
84
+ * Unregister a generator
85
+ * @param {string} name - Generator name
86
+ * @returns {boolean} True if removed
87
+ */
88
+ unregisterGenerator(name) {
89
+ const result = this._generators.delete(name);
90
+ if (result) {
91
+ this._built = false;
92
+ this._context = null;
93
+ }
94
+ return result;
95
+ }
96
+
97
+ /**
98
+ * Get list of registered generator names
99
+ * @returns {string[]}
100
+ */
101
+ getGeneratorNames() {
102
+ return Array.from(this._generators.keys());
103
+ }
104
+
105
+ /**
106
+ * Check if a generator is registered
107
+ * @param {string} name - Generator name
108
+ * @returns {boolean}
109
+ */
110
+ hasGenerator(name) {
111
+ return this._generators.has(name);
112
+ }
113
+
114
+ /**
115
+ * Get the shared registry instance
116
+ * @returns {PlaceholderRegistry}
117
+ */
118
+ getRegistry() {
119
+ if (!this._registry) {
120
+ this._registry = createDefaultRegistry();
121
+ }
122
+ return this._registry;
123
+ }
124
+
125
+ /**
126
+ * Build context by registering all generator placeholders
127
+ * Registry is built once and shared across all generators
128
+ * @param {Object} [baseContext={}] - Base context to merge
129
+ * @returns {Object} Built context
130
+ */
131
+ buildContext(baseContext = {}) {
132
+ if (this._built && this._context) {
133
+ return { ...this._context, ...baseContext };
134
+ }
135
+
136
+ const registry = this.getRegistry();
137
+
138
+ // Let each generator register its placeholders
139
+ for (const [name, generator] of this._generators) {
140
+ try {
141
+ generator.register(registry);
142
+ } catch (error) {
143
+ console.error(`Failed to register placeholders for "${name}":`, error.message);
144
+ }
145
+ }
146
+
147
+ // Build context with resolved values
148
+ this._context = {
149
+ registry,
150
+ container: this._container,
151
+ paths: this._paths,
152
+ scanner: createScannerFactory(this._container),
153
+ ...baseContext,
154
+ };
155
+
156
+ this._built = true;
157
+
158
+ return this._context;
159
+ }
160
+
161
+ /**
162
+ * Run a specific generator
163
+ * @param {string} name - Generator name
164
+ * @param {Object} [context] - Context override
165
+ * @returns {Promise<{success: boolean, error?: Error}>}
166
+ */
167
+ async runGenerator(name, context) {
168
+ const generator = this._generators.get(name);
169
+
170
+ if (!generator) {
171
+ return { success: false, error: new Error(`Generator not found: ${name}`) };
172
+ }
173
+
174
+ const ctx = context || this.buildContext();
175
+
176
+ try {
177
+ await generator.generate(ctx);
178
+ return { success: true };
179
+ } catch (error) {
180
+ return { success: false, error };
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Run all registered generators
186
+ * @param {Object} [context] - Context override
187
+ * @returns {Promise<Map<string, {success: boolean, error?: Error}>>}
188
+ */
189
+ async runAll(context) {
190
+ const ctx = context || this.buildContext();
191
+ const results = new Map();
192
+
193
+ for (const name of this._generators.keys()) {
194
+ results.set(name, await this.runGenerator(name, ctx));
195
+ }
196
+
197
+ return results;
198
+ }
199
+
200
+ /**
201
+ * Run generators in parallel
202
+ * @param {Object} [context] - Context override
203
+ * @returns {Promise<Map<string, {success: boolean, error?: Error}>>}
204
+ */
205
+ async runParallel(context) {
206
+ const ctx = context || this.buildContext();
207
+ const results = new Map();
208
+
209
+ const promises = Array.from(this._generators.keys()).map(async name => {
210
+ const result = await this.runGenerator(name, ctx);
211
+ return { name, result };
212
+ });
213
+
214
+ const settled = await Promise.allSettled(promises);
215
+
216
+ for (const item of settled) {
217
+ if (item.status === 'fulfilled') {
218
+ results.set(item.value.name, item.value.result);
219
+ } else {
220
+ // Shouldn't happen since runGenerator catches errors
221
+ const name = 'unknown';
222
+ results.set(name, { success: false, error: item.reason });
223
+ }
224
+ }
225
+
226
+ return results;
227
+ }
228
+
229
+ /**
230
+ * Reset the factory state
231
+ */
232
+ reset() {
233
+ this._built = false;
234
+ this._context = null;
235
+ this._registry = null;
236
+ }
237
+
238
+ /**
239
+ * Get container (for testing)
240
+ * @returns {Object}
241
+ */
242
+ getContainer() {
243
+ return this._container;
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Base generator class with common utilities
249
+ */
250
+ class BaseGenerator {
251
+ constructor(options = {}) {
252
+ this.name = options.name || this.constructor.name;
253
+ this.placeholders = [];
254
+ }
255
+
256
+ /**
257
+ * Register placeholders - override in subclass
258
+ * @param {PlaceholderRegistry} registry
259
+ */
260
+ register(registry) {
261
+ // Subclasses should override this method
262
+ for (const placeholder of this.placeholders) {
263
+ registry.register(placeholder.name, placeholder.resolver, placeholder.config);
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Generate content - override in subclass
269
+ * @param {Object} context
270
+ * @returns {Promise<void>}
271
+ */
272
+ async generate(context) {
273
+ throw new Error('Subclass must implement generate(context)');
274
+ }
275
+
276
+ /**
277
+ * Helper to add a placeholder definition
278
+ * @param {string} name - Placeholder name
279
+ * @param {Function} resolver - Resolver function
280
+ * @param {Object} [config] - Configuration
281
+ */
282
+ addPlaceholder(name, resolver, config = {}) {
283
+ this.placeholders.push({ name, resolver, config });
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Create a factory with standard generators
289
+ * @param {Object} options - Factory options
290
+ * @returns {GeneratorFactory}
291
+ */
292
+ function createGeneratorFactory(options = {}) {
293
+ const factory = new GeneratorFactory(options);
294
+
295
+ // Register standard generators if provided
296
+ if (options.generators) {
297
+ for (const [name, generator] of Object.entries(options.generators)) {
298
+ factory.registerGenerator(name, generator);
299
+ }
300
+ }
301
+
302
+ return factory;
303
+ }
304
+
305
+ // Singleton instance
306
+ let _factoryInstance = null;
307
+
308
+ /**
309
+ * Get singleton factory instance
310
+ * @param {Object} [options] - Factory options
311
+ * @returns {GeneratorFactory}
312
+ */
313
+ function getGeneratorFactory(options = {}) {
314
+ if (!_factoryInstance || options.forceNew) {
315
+ _factoryInstance = createGeneratorFactory(options);
316
+ }
317
+ return _factoryInstance;
318
+ }
319
+
320
+ /**
321
+ * Reset singleton (for testing)
322
+ */
323
+ function resetGeneratorFactory() {
324
+ _factoryInstance = null;
325
+ }
326
+
327
+ module.exports = {
328
+ GeneratorFactory,
329
+ BaseGenerator,
330
+ createGeneratorFactory,
331
+ getGeneratorFactory,
332
+ resetGeneratorFactory,
333
+ };
@@ -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