agileflow 2.90.7 → 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 (144) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +6 -6
  3. package/lib/README.md +178 -0
  4. package/lib/codebase-indexer.js +818 -0
  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-names.js +3 -3
  22. package/lib/validate.js +116 -52
  23. package/package.json +4 -1
  24. package/scripts/af +34 -0
  25. package/scripts/agent-loop.js +63 -9
  26. package/scripts/agileflow-configure.js +2 -2
  27. package/scripts/agileflow-welcome.js +435 -23
  28. package/scripts/archive-completed-stories.sh +57 -11
  29. package/scripts/claude-tmux.sh +102 -0
  30. package/scripts/damage-control-bash.js +3 -70
  31. package/scripts/damage-control-edit.js +3 -20
  32. package/scripts/damage-control-write.js +3 -20
  33. package/scripts/dependency-check.js +310 -0
  34. package/scripts/get-env.js +11 -4
  35. package/scripts/lib/configure-detect.js +23 -1
  36. package/scripts/lib/configure-features.js +43 -2
  37. package/scripts/lib/context-formatter.js +771 -0
  38. package/scripts/lib/context-loader.js +699 -0
  39. package/scripts/lib/damage-control-utils.js +107 -0
  40. package/scripts/lib/json-utils.sh +162 -0
  41. package/scripts/lib/state-migrator.js +353 -0
  42. package/scripts/lib/story-state-machine.js +437 -0
  43. package/scripts/obtain-context.js +118 -1048
  44. package/scripts/pre-push-check.sh +46 -0
  45. package/scripts/precompact-context.sh +36 -11
  46. package/scripts/query-codebase.js +538 -0
  47. package/scripts/ralph-loop.js +5 -5
  48. package/scripts/session-manager.js +220 -42
  49. package/scripts/spawn-parallel.js +651 -0
  50. package/scripts/tui/blessed/data/watcher.js +180 -0
  51. package/scripts/tui/blessed/index.js +244 -0
  52. package/scripts/tui/blessed/panels/output.js +101 -0
  53. package/scripts/tui/blessed/panels/sessions.js +150 -0
  54. package/scripts/tui/blessed/panels/trace.js +97 -0
  55. package/scripts/tui/blessed/ui/help.js +77 -0
  56. package/scripts/tui/blessed/ui/screen.js +52 -0
  57. package/scripts/tui/blessed/ui/statusbar.js +47 -0
  58. package/scripts/tui/blessed/ui/tabbar.js +99 -0
  59. package/scripts/tui/index.js +38 -30
  60. package/scripts/validators/README.md +143 -0
  61. package/scripts/validators/component-validator.js +239 -0
  62. package/scripts/validators/json-schema-validator.js +186 -0
  63. package/scripts/validators/markdown-validator.js +152 -0
  64. package/scripts/validators/migration-validator.js +129 -0
  65. package/scripts/validators/security-validator.js +380 -0
  66. package/scripts/validators/story-format-validator.js +197 -0
  67. package/scripts/validators/test-result-validator.js +114 -0
  68. package/scripts/validators/workflow-validator.js +247 -0
  69. package/src/core/agents/accessibility.md +6 -0
  70. package/src/core/agents/adr-writer.md +6 -0
  71. package/src/core/agents/analytics.md +6 -0
  72. package/src/core/agents/api.md +6 -0
  73. package/src/core/agents/ci.md +6 -0
  74. package/src/core/agents/codebase-query.md +261 -0
  75. package/src/core/agents/compliance.md +6 -0
  76. package/src/core/agents/configuration-damage-control.md +6 -0
  77. package/src/core/agents/configuration-visual-e2e.md +6 -0
  78. package/src/core/agents/database.md +10 -0
  79. package/src/core/agents/datamigration.md +6 -0
  80. package/src/core/agents/design.md +6 -0
  81. package/src/core/agents/devops.md +6 -0
  82. package/src/core/agents/documentation.md +6 -0
  83. package/src/core/agents/epic-planner.md +6 -0
  84. package/src/core/agents/integrations.md +6 -0
  85. package/src/core/agents/mentor.md +6 -0
  86. package/src/core/agents/mobile.md +6 -0
  87. package/src/core/agents/monitoring.md +6 -0
  88. package/src/core/agents/multi-expert.md +6 -0
  89. package/src/core/agents/performance.md +6 -0
  90. package/src/core/agents/product.md +6 -0
  91. package/src/core/agents/qa.md +6 -0
  92. package/src/core/agents/readme-updater.md +6 -0
  93. package/src/core/agents/refactor.md +6 -0
  94. package/src/core/agents/research.md +6 -0
  95. package/src/core/agents/security.md +6 -0
  96. package/src/core/agents/testing.md +10 -0
  97. package/src/core/agents/ui.md +6 -0
  98. package/src/core/commands/adr.md +114 -0
  99. package/src/core/commands/agent.md +120 -0
  100. package/src/core/commands/assign.md +145 -0
  101. package/src/core/commands/audit.md +401 -0
  102. package/src/core/commands/babysit.md +32 -5
  103. package/src/core/commands/board.md +1 -0
  104. package/src/core/commands/changelog.md +118 -0
  105. package/src/core/commands/configure.md +42 -6
  106. package/src/core/commands/diagnose.md +114 -0
  107. package/src/core/commands/epic.md +205 -1
  108. package/src/core/commands/handoff.md +128 -0
  109. package/src/core/commands/help.md +76 -0
  110. package/src/core/commands/metrics.md +1 -0
  111. package/src/core/commands/pr.md +96 -0
  112. package/src/core/commands/research/analyze.md +1 -0
  113. package/src/core/commands/research/ask.md +2 -0
  114. package/src/core/commands/research/import.md +1 -0
  115. package/src/core/commands/research/list.md +2 -0
  116. package/src/core/commands/research/synthesize.md +584 -0
  117. package/src/core/commands/research/view.md +2 -0
  118. package/src/core/commands/roadmap/analyze.md +400 -0
  119. package/src/core/commands/session/new.md +113 -6
  120. package/src/core/commands/session/spawn.md +197 -0
  121. package/src/core/commands/sprint.md +22 -0
  122. package/src/core/commands/status.md +200 -1
  123. package/src/core/commands/story/list.md +9 -9
  124. package/src/core/commands/story/view.md +1 -0
  125. package/src/core/commands/story.md +143 -4
  126. package/src/core/experts/codebase-query/expertise.yaml +190 -0
  127. package/src/core/experts/codebase-query/question.md +73 -0
  128. package/src/core/experts/codebase-query/self-improve.md +105 -0
  129. package/src/core/templates/agileflow-metadata.json +55 -2
  130. package/src/core/templates/plan-template.md +125 -0
  131. package/src/core/templates/story-lifecycle.md +213 -0
  132. package/src/core/templates/story-template.md +4 -0
  133. package/src/core/templates/tdd-test-template.js +241 -0
  134. package/tools/cli/commands/setup.js +86 -0
  135. package/tools/cli/installers/core/installer.js +94 -0
  136. package/tools/cli/installers/ide/_base-ide.js +20 -11
  137. package/tools/cli/installers/ide/codex.js +29 -47
  138. package/tools/cli/lib/config-manager.js +17 -2
  139. package/tools/cli/lib/content-transformer.js +271 -0
  140. package/tools/cli/lib/error-handler.js +14 -22
  141. package/tools/cli/lib/ide-error-factory.js +421 -0
  142. package/tools/cli/lib/ide-health-monitor.js +364 -0
  143. package/tools/cli/lib/ide-registry.js +114 -1
  144. package/tools/cli/lib/ui.js +14 -25
@@ -39,6 +39,29 @@ const {
39
39
  * @property {string} [source='builtin'] - Source: 'builtin' | 'plugin'
40
40
  */
41
41
 
42
+ /**
43
+ * Schema version for serialization compatibility
44
+ * Increment when making breaking changes to serialized format
45
+ *
46
+ * Version history:
47
+ * - 1: Initial schema with name, type, description, secure, cacheable, source
48
+ * - 2: Added validator serialization support, context metadata
49
+ */
50
+ const SCHEMA_VERSION = 2;
51
+
52
+ /**
53
+ * Schema migrations for upgrading old configurations
54
+ */
55
+ const SCHEMA_MIGRATIONS = {
56
+ // Migrate from v1 to v2
57
+ 1: config => ({
58
+ ...config,
59
+ schemaVersion: 2,
60
+ contextMetadata: config.contextMetadata || {},
61
+ validatorName: config.validatorName || null,
62
+ }),
63
+ };
64
+
42
65
  /**
43
66
  * Default resolver types with their sanitization rules
44
67
  */
@@ -470,6 +493,186 @@ class PlaceholderRegistry extends EventEmitter {
470
493
  // Could be extended to include context hash
471
494
  return name;
472
495
  }
496
+
497
+ // ===========================================================================
498
+ // Schema Versioning and Serialization
499
+ // ===========================================================================
500
+
501
+ /**
502
+ * Get current schema version
503
+ * @returns {number}
504
+ */
505
+ static getSchemaVersion() {
506
+ return SCHEMA_VERSION;
507
+ }
508
+
509
+ /**
510
+ * Serialize registry configuration for persistence
511
+ * @returns {Object} Serializable configuration
512
+ */
513
+ serialize() {
514
+ const configs = {};
515
+
516
+ for (const [name, config] of this._resolvers) {
517
+ configs[name] = {
518
+ schemaVersion: SCHEMA_VERSION,
519
+ name: config.name,
520
+ description: config.description,
521
+ type: config.type,
522
+ secure: config.secure,
523
+ cacheable: config.cacheable,
524
+ source: config.source,
525
+ contextMetadata: config.contextMetadata || {},
526
+ validatorName: config.validatorName || null,
527
+ // Note: resolver function cannot be serialized - must be re-registered
528
+ };
529
+ }
530
+
531
+ return {
532
+ schemaVersion: SCHEMA_VERSION,
533
+ registryOptions: {
534
+ cacheEnabled: this.cacheEnabled,
535
+ strictMode: this.strictMode,
536
+ secureByDefault: this.secureByDefault,
537
+ },
538
+ placeholders: configs,
539
+ plugins: Array.from(this._plugins.keys()),
540
+ };
541
+ }
542
+
543
+ /**
544
+ * Export configuration as JSON string
545
+ * @param {number} [spaces=2] - JSON indentation
546
+ * @returns {string}
547
+ */
548
+ toJSON(spaces = 2) {
549
+ return JSON.stringify(this.serialize(), null, spaces);
550
+ }
551
+
552
+ /**
553
+ * Migrate configuration to current schema version
554
+ * @param {Object} config - Configuration to migrate
555
+ * @returns {Object} Migrated configuration
556
+ */
557
+ static migrateConfig(config) {
558
+ let migrated = { ...config };
559
+ let version = config.schemaVersion || 1;
560
+
561
+ // Apply migrations sequentially
562
+ while (version < SCHEMA_VERSION) {
563
+ const migration = SCHEMA_MIGRATIONS[version];
564
+ if (migration) {
565
+ migrated = migration(migrated);
566
+ }
567
+ version++;
568
+ }
569
+
570
+ return migrated;
571
+ }
572
+
573
+ /**
574
+ * Validate configuration against current schema
575
+ * @param {Object} config - Configuration to validate
576
+ * @returns {{valid: boolean, errors: string[]}}
577
+ */
578
+ static validateConfig(config) {
579
+ const errors = [];
580
+
581
+ if (!config || typeof config !== 'object') {
582
+ return { valid: false, errors: ['Configuration must be an object'] };
583
+ }
584
+
585
+ // Check schema version
586
+ if (config.schemaVersion && config.schemaVersion > SCHEMA_VERSION) {
587
+ errors.push(
588
+ `Configuration schema version ${config.schemaVersion} is newer than supported ${SCHEMA_VERSION}`
589
+ );
590
+ }
591
+
592
+ // Validate required fields for placeholder configs
593
+ if (config.placeholders) {
594
+ for (const [name, placeholder] of Object.entries(config.placeholders)) {
595
+ if (!placeholder.name) {
596
+ errors.push(`Placeholder "${name}" missing required field: name`);
597
+ }
598
+ if (!placeholder.type) {
599
+ errors.push(`Placeholder "${name}" missing required field: type`);
600
+ }
601
+ if (placeholder.type && !RESOLVER_TYPES[placeholder.type]) {
602
+ errors.push(`Placeholder "${name}" has invalid type: ${placeholder.type}`);
603
+ }
604
+ }
605
+ }
606
+
607
+ return { valid: errors.length === 0, errors };
608
+ }
609
+
610
+ /**
611
+ * Load configuration from serialized data
612
+ * Note: Only loads metadata - resolvers must be re-registered
613
+ * @param {Object} data - Serialized configuration
614
+ * @param {Object} [resolvers={}] - Map of resolver functions by name
615
+ * @returns {PlaceholderRegistry} New registry instance
616
+ */
617
+ static deserialize(data, resolvers = {}) {
618
+ // Validate
619
+ const validation = PlaceholderRegistry.validateConfig(data);
620
+ if (!validation.valid) {
621
+ throw new Error(`Invalid configuration: ${validation.errors.join(', ')}`);
622
+ }
623
+
624
+ // Migrate if needed
625
+ const migrated = PlaceholderRegistry.migrateConfig(data);
626
+
627
+ // Create registry with options
628
+ const options = migrated.registryOptions || {};
629
+ const registry = new PlaceholderRegistry({
630
+ cache: options.cacheEnabled,
631
+ strict: options.strictMode,
632
+ secure: options.secureByDefault,
633
+ });
634
+
635
+ // Register placeholders with provided resolvers
636
+ if (migrated.placeholders) {
637
+ for (const [name, config] of Object.entries(migrated.placeholders)) {
638
+ const resolver = resolvers[name];
639
+ if (resolver) {
640
+ registry.register(name, resolver, {
641
+ description: config.description,
642
+ type: config.type,
643
+ secure: config.secure,
644
+ cacheable: config.cacheable,
645
+ source: config.source,
646
+ contextMetadata: config.contextMetadata,
647
+ validatorName: config.validatorName,
648
+ });
649
+ }
650
+ }
651
+ }
652
+
653
+ return registry;
654
+ }
655
+
656
+ /**
657
+ * Parse JSON configuration and create registry
658
+ * @param {string} json - JSON string
659
+ * @param {Object} [resolvers={}] - Map of resolver functions by name
660
+ * @returns {PlaceholderRegistry}
661
+ */
662
+ static fromJSON(json, resolvers = {}) {
663
+ const data = JSON.parse(json);
664
+ return PlaceholderRegistry.deserialize(data, resolvers);
665
+ }
666
+
667
+ /**
668
+ * Check if configuration needs migration
669
+ * @param {Object} config - Configuration to check
670
+ * @returns {boolean}
671
+ */
672
+ static needsMigration(config) {
673
+ const version = config.schemaVersion || 1;
674
+ return version < SCHEMA_VERSION;
675
+ }
473
676
  }
474
677
 
475
678
  // =============================================================================
@@ -608,6 +811,8 @@ function resetRegistry() {
608
811
  module.exports = {
609
812
  PlaceholderRegistry,
610
813
  RESOLVER_TYPES,
814
+ SCHEMA_VERSION,
815
+ SCHEMA_MIGRATIONS,
611
816
  createCountResolver,
612
817
  createListResolver,
613
818
  createStaticResolver,
@@ -0,0 +1,358 @@
1
+ /**
2
+ * Registry Dependency Injection Module
3
+ *
4
+ * Provides dependency injection patterns for registry classes to enable:
5
+ * - Easier testing with mock dependencies
6
+ * - Configuration without modifying code
7
+ * - Clear separation of concerns
8
+ *
9
+ * Usage:
10
+ * const { createContainer, createPlaceholderRegistry } = require('./registry-di');
11
+ *
12
+ * // Create a container with custom dependencies
13
+ * const container = createContainer({
14
+ * sanitizer: mockSanitizer,
15
+ * fs: mockFs,
16
+ * });
17
+ *
18
+ * // Create registry using container
19
+ * const registry = createPlaceholderRegistry(container);
20
+ */
21
+
22
+ 'use strict';
23
+
24
+ // Default dependencies
25
+ const fs = require('fs');
26
+ const path = require('path');
27
+
28
+ /**
29
+ * @typedef {Object} IDependencyContainer
30
+ * @property {Object} fs - File system module
31
+ * @property {Object} path - Path module
32
+ * @property {Object} sanitizer - Content sanitizer module
33
+ * @property {Object} [logger] - Optional logger interface
34
+ */
35
+
36
+ /**
37
+ * @typedef {Object} ISanitizer
38
+ * @property {Function} sanitize.count - Sanitize count values
39
+ * @property {Function} sanitize.description - Sanitize description strings
40
+ * @property {Function} sanitize.date - Sanitize date values
41
+ * @property {Function} sanitize.version - Sanitize version strings
42
+ * @property {Function} sanitize.folderName - Sanitize folder names
43
+ * @property {Function} validatePlaceholderValue - Validate placeholder values
44
+ * @property {Function} detectInjectionAttempt - Detect injection attempts
45
+ * @property {Function} escapeMarkdown - Escape markdown characters
46
+ */
47
+
48
+ /**
49
+ * @typedef {Object} ILogger
50
+ * @property {Function} debug - Debug level logging
51
+ * @property {Function} info - Info level logging
52
+ * @property {Function} warn - Warning level logging
53
+ * @property {Function} error - Error level logging
54
+ */
55
+
56
+ /**
57
+ * Default no-op logger
58
+ */
59
+ const noopLogger = {
60
+ debug: () => {},
61
+ info: () => {},
62
+ warn: () => {},
63
+ error: () => {},
64
+ };
65
+
66
+ /**
67
+ * Console logger adapter
68
+ */
69
+ const consoleLogger = {
70
+ debug: (...args) => console.debug('[Registry]', ...args),
71
+ info: (...args) => console.log('[Registry]', ...args),
72
+ warn: (...args) => console.warn('[Registry]', ...args),
73
+ error: (...args) => console.error('[Registry]', ...args),
74
+ };
75
+
76
+ /**
77
+ * Load default sanitizer module
78
+ * @returns {ISanitizer}
79
+ */
80
+ function loadDefaultSanitizer() {
81
+ try {
82
+ return require('./content-sanitizer');
83
+ } catch (e) {
84
+ // Return stub sanitizer if module not found
85
+ return {
86
+ sanitize: {
87
+ count: v => (typeof v === 'number' ? Math.max(0, Math.floor(v)) : 0),
88
+ description: v => String(v || ''),
89
+ date: v => (v instanceof Date ? v.toISOString().split('T')[0] : String(v || '')),
90
+ version: v => String(v || 'unknown'),
91
+ folderName: v => String(v || '.agileflow'),
92
+ },
93
+ validatePlaceholderValue: () => ({ valid: true }),
94
+ detectInjectionAttempt: () => ({ safe: true }),
95
+ escapeMarkdown: v => String(v || ''),
96
+ };
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Create a dependency container with defaults
102
+ * @param {Partial<IDependencyContainer>} [overrides={}] - Override specific dependencies
103
+ * @returns {IDependencyContainer}
104
+ */
105
+ function createContainer(overrides = {}) {
106
+ const defaultSanitizer = loadDefaultSanitizer();
107
+
108
+ return {
109
+ fs: overrides.fs || fs,
110
+ path: overrides.path || path,
111
+ sanitizer: overrides.sanitizer || defaultSanitizer,
112
+ logger: overrides.logger || noopLogger,
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Create a test container with mock-friendly defaults
118
+ * @param {Partial<IDependencyContainer>} [overrides={}] - Override specific dependencies
119
+ * @returns {IDependencyContainer}
120
+ */
121
+ function createTestContainer(overrides = {}) {
122
+ const mockFs = {
123
+ readdirSync: () => [],
124
+ readFileSync: () => '',
125
+ existsSync: () => false,
126
+ writeFileSync: () => {},
127
+ mkdirSync: () => {},
128
+ ...overrides.fs,
129
+ };
130
+
131
+ const mockSanitizer = {
132
+ sanitize: {
133
+ count: v => v,
134
+ description: v => v,
135
+ date: v => v,
136
+ version: v => v,
137
+ folderName: v => v,
138
+ },
139
+ validatePlaceholderValue: () => ({ valid: true }),
140
+ detectInjectionAttempt: () => ({ safe: true }),
141
+ escapeMarkdown: v => v,
142
+ ...overrides.sanitizer,
143
+ };
144
+
145
+ return createContainer({
146
+ fs: mockFs,
147
+ sanitizer: mockSanitizer,
148
+ logger: overrides.logger || noopLogger,
149
+ ...overrides,
150
+ });
151
+ }
152
+
153
+ /**
154
+ * Registry factory configuration
155
+ * @typedef {Object} RegistryFactoryConfig
156
+ * @property {Object} [options] - Registry options
157
+ * @property {IDependencyContainer} [container] - Dependency container
158
+ */
159
+
160
+ /**
161
+ * Create PlaceholderRegistry with injected dependencies
162
+ * @param {IDependencyContainer} container - Dependency container
163
+ * @param {Object} [options={}] - Registry options
164
+ * @returns {PlaceholderRegistry}
165
+ */
166
+ function createPlaceholderRegistry(container, options = {}) {
167
+ const { PlaceholderRegistry } = require('./placeholder-registry');
168
+
169
+ // Create registry with injected container
170
+ const registry = new PlaceholderRegistry({
171
+ ...options,
172
+ // Inject dependencies via context
173
+ _container: container,
174
+ });
175
+
176
+ return registry;
177
+ }
178
+
179
+ /**
180
+ * Scan registry factory - creates scanner functions with injected dependencies
181
+ * @param {IDependencyContainer} container - Dependency container
182
+ * @returns {Object} Scanner functions
183
+ */
184
+ function createScannerFactory(container) {
185
+ const { fs: fsModule, path: pathModule, logger } = container;
186
+
187
+ /**
188
+ * Generic file scanner
189
+ * @param {string} directory - Directory to scan
190
+ * @param {string} extension - File extension (e.g., '.md')
191
+ * @param {Function} parser - Frontmatter parser function
192
+ * @returns {Array} Parsed items
193
+ */
194
+ function scanDirectory(directory, extension, parser) {
195
+ logger.debug(`Scanning directory: ${directory}`);
196
+
197
+ if (!fsModule.existsSync(directory)) {
198
+ logger.warn(`Directory not found: ${directory}`);
199
+ return [];
200
+ }
201
+
202
+ let files;
203
+ try {
204
+ files = fsModule.readdirSync(directory);
205
+ } catch (err) {
206
+ logger.error(`Failed to read directory: ${err.message}`);
207
+ return [];
208
+ }
209
+
210
+ const items = [];
211
+
212
+ for (const file of files) {
213
+ if (!file.endsWith(extension)) {
214
+ continue;
215
+ }
216
+
217
+ const filePath = pathModule.join(directory, file);
218
+
219
+ try {
220
+ const content = fsModule.readFileSync(filePath, 'utf8');
221
+ const parsed = parser(content, file, filePath);
222
+
223
+ if (parsed) {
224
+ items.push(parsed);
225
+ }
226
+ } catch (err) {
227
+ logger.warn(`Failed to parse ${file}: ${err.message}`);
228
+ }
229
+ }
230
+
231
+ logger.debug(`Found ${items.length} items`);
232
+ return items;
233
+ }
234
+
235
+ return {
236
+ scanDirectory,
237
+
238
+ /**
239
+ * Scan for command files
240
+ * @param {string} commandsDir - Commands directory path
241
+ * @param {Function} extractFrontmatter - Frontmatter extractor
242
+ * @returns {Array} Command metadata
243
+ */
244
+ scanCommands(commandsDir, extractFrontmatter) {
245
+ return scanDirectory(commandsDir, '.md', (content, file, filePath) => {
246
+ const frontmatter = extractFrontmatter(filePath);
247
+ if (Object.keys(frontmatter).length === 0) {
248
+ return null;
249
+ }
250
+
251
+ const name = file.replace('.md', '');
252
+ return {
253
+ name,
254
+ file,
255
+ path: filePath,
256
+ description: frontmatter.description || '',
257
+ argumentHint: frontmatter['argument-hint'] || '',
258
+ };
259
+ });
260
+ },
261
+
262
+ /**
263
+ * Scan for agent files
264
+ * @param {string} agentsDir - Agents directory path
265
+ * @param {Function} extractFrontmatter - Frontmatter extractor
266
+ * @param {Function} normalizeTools - Tools normalizer
267
+ * @returns {Array} Agent metadata
268
+ */
269
+ scanAgents(agentsDir, extractFrontmatter, normalizeTools) {
270
+ return scanDirectory(agentsDir, '.md', (content, file, filePath) => {
271
+ const frontmatter = extractFrontmatter(filePath);
272
+ if (Object.keys(frontmatter).length === 0) {
273
+ return null;
274
+ }
275
+
276
+ const name = file.replace('.md', '');
277
+ const tools = normalizeTools ? normalizeTools(frontmatter.tools) : frontmatter.tools || [];
278
+
279
+ return {
280
+ name,
281
+ file,
282
+ path: filePath,
283
+ displayName: frontmatter.name || name,
284
+ description: frontmatter.description || '',
285
+ tools,
286
+ model: frontmatter.model || 'haiku',
287
+ color: frontmatter.color || 'blue',
288
+ };
289
+ });
290
+ },
291
+ };
292
+ }
293
+
294
+ /**
295
+ * Resolve paths factory - creates path resolver with injected dependencies
296
+ * @param {IDependencyContainer} container - Dependency container
297
+ * @param {string} [baseDir] - Base directory for relative paths
298
+ * @returns {Object} Path resolver functions
299
+ */
300
+ function createPathResolver(container, baseDir) {
301
+ const { path: pathModule, fs: fsModule } = container;
302
+ const resolvedBaseDir = baseDir || process.cwd();
303
+
304
+ return {
305
+ /**
306
+ * Resolve path relative to base directory
307
+ * @param {...string} segments - Path segments
308
+ * @returns {string} Resolved path
309
+ */
310
+ resolve(...segments) {
311
+ return pathModule.resolve(resolvedBaseDir, ...segments);
312
+ },
313
+
314
+ /**
315
+ * Join path segments
316
+ * @param {...string} segments - Path segments
317
+ * @returns {string} Joined path
318
+ */
319
+ join(...segments) {
320
+ return pathModule.join(...segments);
321
+ },
322
+
323
+ /**
324
+ * Check if path exists
325
+ * @param {string} targetPath - Path to check
326
+ * @returns {boolean}
327
+ */
328
+ exists(targetPath) {
329
+ return fsModule.existsSync(targetPath);
330
+ },
331
+
332
+ /**
333
+ * Get base directory
334
+ * @returns {string}
335
+ */
336
+ getBaseDir() {
337
+ return resolvedBaseDir;
338
+ },
339
+ };
340
+ }
341
+
342
+ module.exports = {
343
+ // Container factories
344
+ createContainer,
345
+ createTestContainer,
346
+
347
+ // Registry factories
348
+ createPlaceholderRegistry,
349
+ createScannerFactory,
350
+ createPathResolver,
351
+
352
+ // Loggers
353
+ noopLogger,
354
+ consoleLogger,
355
+
356
+ // Utilities
357
+ loadDefaultSanitizer,
358
+ };