agileflow 2.91.0 → 2.92.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 (100) 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 +32 -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 +491 -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 +50 -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 +127 -14
  46. package/scripts/ralph-loop.js +5 -5
  47. package/scripts/session-manager.js +408 -55
  48. package/scripts/spawn-parallel.js +666 -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 +132 -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 +95 -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/installers/ide/windsurf.js +1 -1
  94. package/tools/cli/lib/config-manager.js +17 -2
  95. package/tools/cli/lib/content-transformer.js +271 -0
  96. package/tools/cli/lib/error-handler.js +14 -22
  97. package/tools/cli/lib/ide-error-factory.js +421 -0
  98. package/tools/cli/lib/ide-health-monitor.js +364 -0
  99. package/tools/cli/lib/ide-registry.js +113 -2
  100. package/tools/cli/lib/ui.js +15 -25
@@ -164,6 +164,10 @@ class Installer {
164
164
  spinner.text = 'Installing changelog...';
165
165
  await this.installChangelog(agileflowDir, { force: effectiveForce });
166
166
 
167
+ // Set up shell aliases for claude command
168
+ spinner.text = 'Setting up shell aliases...';
169
+ const aliasResult = await this.setupShellAliases(directory, { force: effectiveForce });
170
+
167
171
  // Create config.yaml
168
172
  spinner.text = 'Creating configuration...';
169
173
  await this.createConfig(agileflowDir, userName, agileflowFolder, { force: effectiveForce });
@@ -191,6 +195,7 @@ class Installer {
191
195
  projectDir: directory,
192
196
  counts,
193
197
  fileOps,
198
+ shellAliases: aliasResult,
194
199
  };
195
200
  } catch (error) {
196
201
  spinner.fail('Installation failed');
@@ -813,6 +818,95 @@ class Installer {
813
818
  }
814
819
  }
815
820
 
821
+ /**
822
+ * Set up shell aliases for the claude command
823
+ * Adds aliases to ~/.bashrc and/or ~/.zshrc so 'claude' auto-spawns tmux
824
+ * @param {string} directory - Project directory (used for relative path in alias)
825
+ * @param {Object} options - Setup options
826
+ * @param {boolean} options.force - Overwrite existing aliases
827
+ * @returns {Promise<Object>} Result with shells configured
828
+ */
829
+ async setupShellAliases(directory, options = {}) {
830
+ const os = require('os');
831
+ const result = {
832
+ configured: [],
833
+ skipped: [],
834
+ error: null,
835
+ };
836
+
837
+ // Only set up aliases on Unix-like systems
838
+ if (process.platform === 'win32') {
839
+ result.skipped.push('Windows (not supported)');
840
+ return result;
841
+ }
842
+
843
+ const homeDir = os.homedir();
844
+ const aliasBlock = `
845
+ # AgileFlow tmux wrapper
846
+ # Use 'af' or 'agileflow' for tmux, 'claude' stays normal
847
+ alias af="bash .agileflow/scripts/af"
848
+ alias agileflow="bash .agileflow/scripts/af"
849
+ `;
850
+
851
+ const marker = '# AgileFlow tmux wrapper';
852
+ const rcFiles = [
853
+ { name: 'bash', path: path.join(homeDir, '.bashrc') },
854
+ { name: 'zsh', path: path.join(homeDir, '.zshrc') },
855
+ ];
856
+
857
+ for (const rc of rcFiles) {
858
+ try {
859
+ // Check if RC file exists
860
+ if (!(await fs.pathExists(rc.path))) {
861
+ result.skipped.push(`${rc.name} (no ${path.basename(rc.path)})`);
862
+ continue;
863
+ }
864
+
865
+ const content = await fs.readFile(rc.path, 'utf8');
866
+
867
+ // Check if aliases already exist
868
+ if (content.includes(marker)) {
869
+ if (options.force) {
870
+ // Remove existing block and re-add
871
+ const lines = content.split('\n');
872
+ const filteredLines = [];
873
+ let inBlock = false;
874
+
875
+ for (const line of lines) {
876
+ if (line.includes(marker)) {
877
+ inBlock = true;
878
+ continue;
879
+ }
880
+ if (inBlock && line.startsWith('alias ')) {
881
+ continue;
882
+ }
883
+ if (inBlock && line.trim() === '') {
884
+ inBlock = false;
885
+ continue;
886
+ }
887
+ inBlock = false;
888
+ filteredLines.push(line);
889
+ }
890
+
891
+ await fs.writeFile(rc.path, filteredLines.join('\n') + aliasBlock, 'utf8');
892
+ result.configured.push(rc.name);
893
+ } else {
894
+ result.skipped.push(`${rc.name} (already configured)`);
895
+ }
896
+ continue;
897
+ }
898
+
899
+ // Append aliases to RC file
900
+ await fs.appendFile(rc.path, aliasBlock);
901
+ result.configured.push(rc.name);
902
+ } catch (err) {
903
+ result.skipped.push(`${rc.name} (error: ${err.message})`);
904
+ }
905
+ }
906
+
907
+ return result;
908
+ }
909
+
816
910
  /**
817
911
  * Get installation status
818
912
  * @param {string} directory - Project directory
@@ -15,6 +15,11 @@ const {
15
15
  ContentInjectionError,
16
16
  withPermissionHandling,
17
17
  } = require('../../lib/ide-errors');
18
+ const {
19
+ replaceReferences,
20
+ createDocsReplacements,
21
+ injectContent: injectDynamicContentHelper,
22
+ } = require('../../lib/content-transformer');
18
23
 
19
24
  /**
20
25
  * Base class for IDE-specific setup
@@ -48,6 +53,7 @@ class BaseIdeSetup {
48
53
 
49
54
  /**
50
55
  * Replace docs/ references in content with custom folder name
56
+ * Uses content-transformer module for consistent replacements
51
57
  * @param {string} content - File content
52
58
  * @returns {string} Updated content
53
59
  */
@@ -56,28 +62,31 @@ class BaseIdeSetup {
56
62
  return content; // No replacement needed
57
63
  }
58
64
 
59
- // Replace all variations of docs/ references
60
- return content
61
- .replace(/docs\//g, `${this.docsFolder}/`)
62
- .replace(/`docs\//g, `\`${this.docsFolder}/`)
63
- .replace(/"docs\//g, `"${this.docsFolder}/`)
64
- .replace(/'docs\//g, `'${this.docsFolder}/`)
65
- .replace(/\(docs\//g, `(${this.docsFolder}/`)
66
- .replace(/docs\/\)/g, `${this.docsFolder}/)`)
67
- .replace(/\bdocs\b(?!\.)/g, this.docsFolder); // Replace standalone "docs" word
65
+ // Use content-transformer for standard replacements
66
+ let result = replaceReferences(content, createDocsReplacements(this.docsFolder));
67
+
68
+ // Additional patterns not covered by standard replacements
69
+ result = replaceReferences(result, {
70
+ 'docs/)': `${this.docsFolder}/)`,
71
+ });
72
+
73
+ // Replace standalone "docs" word (not followed by .)
74
+ result = result.replace(/\bdocs\b(?!\.)/g, this.docsFolder);
75
+
76
+ return result;
68
77
  }
69
78
 
70
79
  /**
71
80
  * Inject dynamic content into template (agent lists, command lists)
81
+ * Uses content-transformer module for consistent injection
72
82
  * @param {string} content - Template file content
73
83
  * @param {string} agileflowDir - AgileFlow installation directory
74
84
  * @returns {string} Content with placeholders replaced
75
85
  */
76
86
  injectDynamicContent(content, agileflowDir) {
77
- const { injectContent } = require('../../lib/content-injector');
78
87
  // agileflowDir is the user's .agileflow installation directory
79
88
  // which has agents/, commands/, skills/ at the root level
80
- return injectContent(content, {
89
+ return injectDynamicContentHelper(content, {
81
90
  coreDir: agileflowDir,
82
91
  agileflowFolder: this.agileflowFolder,
83
92
  version: this.getVersion(),
@@ -14,9 +14,14 @@ const path = require('node:path');
14
14
  const os = require('node:os');
15
15
  const fs = require('fs-extra');
16
16
  const chalk = require('chalk');
17
- const { safeLoad, yaml } = require('../../../../lib/yaml-utils');
17
+ const { yaml } = require('../../../../lib/yaml-utils');
18
18
  const { BaseIdeSetup } = require('./_base-ide');
19
- const { parseFrontmatter } = require('../../../../scripts/lib/frontmatter-parser');
19
+ const {
20
+ getFrontmatter,
21
+ stripFrontmatter,
22
+ replaceReferences,
23
+ IDE_REPLACEMENTS,
24
+ } = require('../../lib/content-transformer');
20
25
 
21
26
  /**
22
27
  * OpenAI Codex CLI setup handler
@@ -55,24 +60,15 @@ class CodexSetup extends BaseIdeSetup {
55
60
 
56
61
  /**
57
62
  * Convert an AgileFlow agent markdown file to Codex SKILL.md format
63
+ * Uses content-transformer module for consistent transformations
58
64
  * @param {string} content - Original agent markdown content
59
65
  * @param {string} agentName - Agent name (e.g., 'database')
60
66
  * @returns {string} Codex SKILL.md content
61
67
  */
62
68
  convertAgentToSkill(content, agentName) {
63
- // Extract frontmatter using shared parser
64
- let description = `AgileFlow ${agentName} agent`;
65
- let model = 'default';
66
-
67
- const frontmatter = parseFrontmatter(content);
68
- if (frontmatter && Object.keys(frontmatter).length > 0) {
69
- if (frontmatter.description) {
70
- description = frontmatter.description;
71
- }
72
- if (frontmatter.model) {
73
- model = frontmatter.model;
74
- }
75
- }
69
+ // Extract frontmatter using content-transformer
70
+ const frontmatter = getFrontmatter(content);
71
+ const description = frontmatter.description || `AgileFlow ${agentName} agent`;
76
72
 
77
73
  // Create SKILL.md with YAML frontmatter
78
74
  const skillFrontmatter = yaml
@@ -83,8 +79,8 @@ class CodexSetup extends BaseIdeSetup {
83
79
  })
84
80
  .trim();
85
81
 
86
- // Remove original frontmatter from content
87
- let bodyContent = content.replace(/^---\n[\s\S]*?\n---\n*/, '');
82
+ // Remove original frontmatter from content using content-transformer
83
+ let bodyContent = stripFrontmatter(content);
88
84
 
89
85
  // Add Codex-specific header
90
86
  const codexHeader = `# AgileFlow: ${agentName.charAt(0).toUpperCase() + agentName.slice(1)} Agent
@@ -93,12 +89,8 @@ class CodexSetup extends BaseIdeSetup {
93
89
 
94
90
  `;
95
91
 
96
- // Replace Claude-specific references
97
- bodyContent = bodyContent
98
- .replace(/Claude Code/gi, 'Codex CLI')
99
- .replace(/CLAUDE\.md/g, 'AGENTS.md')
100
- .replace(/\.claude\//g, '.codex/')
101
- .replace(/Task tool/gi, 'skill invocation');
92
+ // Replace Claude-specific references using content-transformer
93
+ bodyContent = replaceReferences(bodyContent, IDE_REPLACEMENTS.codex);
102
94
 
103
95
  return `---
104
96
  ${skillFrontmatter}
@@ -109,35 +101,25 @@ ${codexHeader}${bodyContent}`;
109
101
 
110
102
  /**
111
103
  * Convert an AgileFlow command markdown file to Codex prompt format
104
+ * Uses content-transformer module for consistent transformations
112
105
  * @param {string} content - Original command markdown content
113
106
  * @param {string} commandName - Command name (e.g., 'board')
114
107
  * @returns {string} Codex prompt content
115
108
  */
116
109
  convertCommandToPrompt(content, commandName) {
117
- // Extract description from frontmatter if present
118
- let description = `AgileFlow ${commandName} command`;
119
-
120
- const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
121
- if (frontmatterMatch) {
122
- try {
123
- const frontmatter = safeLoad(frontmatterMatch[1]);
124
- if (frontmatter.description) {
125
- description = frontmatter.description;
126
- }
127
- } catch (e) {
128
- // Ignore YAML parse errors
129
- }
130
- }
131
-
132
- // Remove original frontmatter from content
133
- let bodyContent = content.replace(/^---\n[\s\S]*?\n---\n*/, '');
134
-
135
- // Replace Claude-specific references
136
- bodyContent = bodyContent
137
- .replace(/Claude Code/gi, 'Codex CLI')
138
- .replace(/CLAUDE\.md/g, 'AGENTS.md')
139
- .replace(/\.claude\//g, '.codex/')
140
- .replace(/\/agileflow:/g, '$agileflow-');
110
+ // Extract description from frontmatter using content-transformer
111
+ const frontmatter = getFrontmatter(content);
112
+ const description = frontmatter.description || `AgileFlow ${commandName} command`;
113
+
114
+ // Remove original frontmatter from content using content-transformer
115
+ let bodyContent = stripFrontmatter(content);
116
+
117
+ // Replace Claude-specific references using content-transformer
118
+ // Use codex replacements plus command-specific pattern
119
+ bodyContent = replaceReferences(bodyContent, {
120
+ ...IDE_REPLACEMENTS.codex,
121
+ '/agileflow:': '$agileflow-',
122
+ });
141
123
 
142
124
  // Add Codex prompt header
143
125
  const header = `# AgileFlow: ${commandName}
@@ -15,7 +15,7 @@ const { BaseIdeSetup } = require('./_base-ide');
15
15
  */
16
16
  class WindsurfSetup extends BaseIdeSetup {
17
17
  constructor() {
18
- super('windsurf', 'Windsurf', true);
18
+ super('windsurf', 'Windsurf', false);
19
19
  this.configDir = '.windsurf';
20
20
  this.workflowsDir = 'workflows';
21
21
  }
@@ -13,6 +13,7 @@
13
13
  const path = require('path');
14
14
  const fs = require('fs-extra');
15
15
  const { safeLoad, safeDump } = require('../../../lib/yaml-utils');
16
+ const { hasUnsafePathPatterns } = require('../../../lib/validate-paths');
16
17
 
17
18
  /**
18
19
  * Configuration schema definition
@@ -50,13 +51,27 @@ const CONFIG_SCHEMA = {
50
51
  type: 'string',
51
52
  default: '.agileflow',
52
53
  required: true,
53
- validate: v => typeof v === 'string' && v.length > 0 && !v.includes('..'),
54
+ // Security: Use proper path validation instead of simple string check
55
+ validate: v => {
56
+ if (typeof v !== 'string' || v.length === 0) return false;
57
+ // Must be a relative path without unsafe patterns
58
+ if (path.isAbsolute(v)) return false;
59
+ const check = hasUnsafePathPatterns(v);
60
+ return check.safe;
61
+ },
54
62
  },
55
63
  docsFolder: {
56
64
  type: 'string',
57
65
  default: 'docs',
58
66
  required: true,
59
- validate: v => typeof v === 'string' && v.length > 0 && !v.includes('..'),
67
+ // Security: Use proper path validation instead of simple string check
68
+ validate: v => {
69
+ if (typeof v !== 'string' || v.length === 0) return false;
70
+ // Must be a relative path without unsafe patterns
71
+ if (path.isAbsolute(v)) return false;
72
+ const check = hasUnsafePathPatterns(v);
73
+ return check.safe;
74
+ },
60
75
  },
61
76
  installedAt: {
62
77
  type: 'string',
@@ -0,0 +1,271 @@
1
+ /**
2
+ * content-transformer.js - Reusable content transformation utilities
3
+ *
4
+ * Extracts common content transformation patterns from IDE installers:
5
+ * - replaceReferences: Generic string replacement with pattern support
6
+ * - stripFrontmatter: Remove YAML frontmatter from content
7
+ * - convertFrontmatter: Transform frontmatter keys/values between formats
8
+ * - injectContent: Delegate to existing content-injector
9
+ *
10
+ * Created as part of US-0177: Extract content transformation into reusable helper module
11
+ */
12
+
13
+ const { parseFrontmatter, extractBody } = require('../../../scripts/lib/frontmatter-parser');
14
+
15
+ /**
16
+ * Replace multiple string patterns in content
17
+ *
18
+ * @param {string} content - The content to transform
19
+ * @param {Object|Array} replacements - Either an object of {pattern: replacement} pairs,
20
+ * or an array of {pattern, replacement, flags} objects
21
+ * @returns {string} Content with all replacements applied
22
+ *
23
+ * @example
24
+ * // Object form (simple string replacement)
25
+ * replaceReferences(content, {
26
+ * 'Claude Code': 'Codex CLI',
27
+ * '.claude/': '.codex/',
28
+ * 'CLAUDE.md': 'AGENTS.md'
29
+ * });
30
+ *
31
+ * @example
32
+ * // Array form (with regex flags)
33
+ * replaceReferences(content, [
34
+ * { pattern: 'Claude Code', replacement: 'Codex CLI', flags: 'gi' },
35
+ * { pattern: /\.claude\//g, replacement: '.codex/' }
36
+ * ]);
37
+ */
38
+ function replaceReferences(content, replacements) {
39
+ if (!content || typeof content !== 'string') {
40
+ return content || '';
41
+ }
42
+
43
+ let result = content;
44
+
45
+ if (Array.isArray(replacements)) {
46
+ // Array form: [{pattern, replacement, flags?}]
47
+ for (const item of replacements) {
48
+ if (!item || !item.pattern) continue;
49
+
50
+ let regex;
51
+ if (item.pattern instanceof RegExp) {
52
+ regex = item.pattern;
53
+ } else {
54
+ const flags = item.flags || 'g';
55
+ regex = new RegExp(escapeRegex(item.pattern), flags);
56
+ }
57
+ result = result.replace(regex, item.replacement || '');
58
+ }
59
+ } else if (typeof replacements === 'object' && replacements !== null) {
60
+ // Object form: {pattern: replacement}
61
+ for (const [pattern, replacement] of Object.entries(replacements)) {
62
+ result = result.replace(new RegExp(escapeRegex(pattern), 'g'), replacement);
63
+ }
64
+ }
65
+
66
+ return result;
67
+ }
68
+
69
+ /**
70
+ * Escape special regex characters in a string
71
+ * @param {string} str - String to escape
72
+ * @returns {string} Escaped string safe for use in RegExp
73
+ */
74
+ function escapeRegex(str) {
75
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
76
+ }
77
+
78
+ /**
79
+ * Remove YAML frontmatter from content, returning only the body
80
+ *
81
+ * @param {string} content - Content with optional YAML frontmatter
82
+ * @returns {string} Content body without frontmatter
83
+ *
84
+ * @example
85
+ * const body = stripFrontmatter(`---
86
+ * title: My Document
87
+ * ---
88
+ *
89
+ * # Heading
90
+ * Content here`);
91
+ * // Returns: "# Heading\nContent here"
92
+ */
93
+ function stripFrontmatter(content) {
94
+ return extractBody(content);
95
+ }
96
+
97
+ /**
98
+ * Convert frontmatter between formats using a mapping configuration
99
+ *
100
+ * @param {Object} frontmatter - Parsed frontmatter object
101
+ * @param {Object} config - Conversion configuration
102
+ * @param {Object} [config.keyMap] - Map of source keys to target keys
103
+ * @param {Object} [config.valueMap] - Map of key names to value transformation functions
104
+ * @param {Array} [config.include] - Only include these keys (whitelist)
105
+ * @param {Array} [config.exclude] - Exclude these keys (blacklist)
106
+ * @param {Object} [config.defaults] - Default values to add if not present
107
+ * @returns {Object} Transformed frontmatter object
108
+ *
109
+ * @example
110
+ * const converted = convertFrontmatter(
111
+ * { name: 'security', description: 'Security agent', tools: ['Read', 'Write'] },
112
+ * {
113
+ * keyMap: { name: 'skill_name', tools: 'allowed_tools' },
114
+ * valueMap: { description: (v) => v.replace('agent', 'skill') },
115
+ * exclude: ['internal_only'],
116
+ * defaults: { version: '1.0' }
117
+ * }
118
+ * );
119
+ */
120
+ function convertFrontmatter(frontmatter, config = {}) {
121
+ if (!frontmatter || typeof frontmatter !== 'object') {
122
+ return {};
123
+ }
124
+
125
+ const { keyMap = {}, valueMap = {}, include, exclude = [], defaults = {} } = config;
126
+
127
+ const result = { ...defaults };
128
+
129
+ for (const [key, value] of Object.entries(frontmatter)) {
130
+ // Skip excluded keys
131
+ if (exclude.includes(key)) continue;
132
+
133
+ // Skip if not in include list (when include is specified)
134
+ if (include && !include.includes(key)) continue;
135
+
136
+ // Map key name if mapping exists
137
+ const targetKey = keyMap[key] || key;
138
+
139
+ // Transform value if transformation exists
140
+ const targetValue = valueMap[key] ? valueMap[key](value) : value;
141
+
142
+ result[targetKey] = targetValue;
143
+ }
144
+
145
+ return result;
146
+ }
147
+
148
+ /**
149
+ * Inject dynamic content into a template using content-injector
150
+ *
151
+ * @param {string} content - Content with placeholders
152
+ * @param {Object} options - Injection options
153
+ * @param {string} options.coreDir - Path to AgileFlow core directory
154
+ * @param {string} [options.agileflowFolder] - Target AgileFlow folder name (default: '.agileflow')
155
+ * @param {string} [options.version] - Version string to inject
156
+ * @returns {string} Content with placeholders replaced
157
+ */
158
+ function injectContent(content, options) {
159
+ const { injectContent: inject } = require('./content-injector');
160
+ return inject(content, options);
161
+ }
162
+
163
+ /**
164
+ * Parse frontmatter from content
165
+ * Re-exported from frontmatter-parser for convenience
166
+ *
167
+ * @param {string} content - Content with YAML frontmatter
168
+ * @returns {Object} Parsed frontmatter as object
169
+ */
170
+ function getFrontmatter(content) {
171
+ return parseFrontmatter(content);
172
+ }
173
+
174
+ /**
175
+ * Common replacement patterns for IDE conversions
176
+ */
177
+ const IDE_REPLACEMENTS = {
178
+ /**
179
+ * Claude Code to Codex CLI conversions
180
+ */
181
+ codex: {
182
+ 'Claude Code': 'Codex CLI',
183
+ 'claude code': 'Codex CLI',
184
+ CLAUDE_CODE: 'CODEX_CLI',
185
+ 'CLAUDE.md': 'AGENTS.md',
186
+ '.claude/': '.codex/',
187
+ '.claude\\': '.codex\\',
188
+ 'Task tool': 'skill invocation',
189
+ 'Task agent': 'skill invocation',
190
+ },
191
+
192
+ /**
193
+ * Claude Code to Cursor conversions
194
+ */
195
+ cursor: {
196
+ 'Claude Code': 'Cursor',
197
+ 'claude code': 'Cursor',
198
+ '.claude/': '.cursor/',
199
+ '.claude\\': '.cursor\\',
200
+ },
201
+
202
+ /**
203
+ * Claude Code to Windsurf conversions
204
+ */
205
+ windsurf: {
206
+ 'Claude Code': 'Windsurf',
207
+ 'claude code': 'Windsurf',
208
+ '.claude/': '.windsurf/',
209
+ '.claude\\': '.windsurf\\',
210
+ },
211
+ };
212
+
213
+ /**
214
+ * Create docs folder reference replacements
215
+ *
216
+ * @param {string} targetFolder - Target docs folder name (e.g., 'project-docs')
217
+ * @returns {Object} Replacement patterns for docs references
218
+ */
219
+ function createDocsReplacements(targetFolder) {
220
+ if (targetFolder === 'docs') {
221
+ return {}; // No changes needed
222
+ }
223
+
224
+ return {
225
+ 'docs/': `${targetFolder}/`,
226
+ '`docs/': `\`${targetFolder}/`,
227
+ '"docs/': `"${targetFolder}/`,
228
+ "'docs/": `'${targetFolder}/`,
229
+ '(docs/': `(${targetFolder}/`,
230
+ '[docs/': `[${targetFolder}/`,
231
+ };
232
+ }
233
+
234
+ /**
235
+ * Transform content for a specific IDE target
236
+ *
237
+ * @param {string} content - Source content
238
+ * @param {string} targetIde - Target IDE: 'codex', 'cursor', 'windsurf'
239
+ * @param {Object} [options] - Additional options
240
+ * @param {string} [options.docsFolder] - Custom docs folder name
241
+ * @param {Object} [options.additionalReplacements] - Extra replacements to apply
242
+ * @returns {string} Transformed content
243
+ */
244
+ function transformForIde(content, targetIde, options = {}) {
245
+ const { docsFolder, additionalReplacements = {} } = options;
246
+
247
+ // Start with IDE-specific replacements
248
+ const replacements = { ...(IDE_REPLACEMENTS[targetIde] || {}) };
249
+
250
+ // Add docs folder replacements if needed
251
+ if (docsFolder && docsFolder !== 'docs') {
252
+ Object.assign(replacements, createDocsReplacements(docsFolder));
253
+ }
254
+
255
+ // Add any additional custom replacements
256
+ Object.assign(replacements, additionalReplacements);
257
+
258
+ return replaceReferences(content, replacements);
259
+ }
260
+
261
+ module.exports = {
262
+ replaceReferences,
263
+ stripFrontmatter,
264
+ convertFrontmatter,
265
+ injectContent,
266
+ getFrontmatter,
267
+ escapeRegex,
268
+ IDE_REPLACEMENTS,
269
+ createDocsReplacements,
270
+ transformForIde,
271
+ };
@@ -7,9 +7,15 @@
7
7
  * - CRITICAL: Severe errors (exit 1 + stack trace if DEBUG=1)
8
8
  *
9
9
  * Error output format: "X <problem> | Action: <what to do> | Run: <command>"
10
+ *
11
+ * Note: Formatting logic is extracted to lib/format-error.js for standalone use.
10
12
  */
11
13
 
12
- const { c } = require('../../../lib/colors');
14
+ const {
15
+ formatError: formatErrorHelper,
16
+ formatWarning: formatWarningHelper,
17
+ formatErrorWithStack,
18
+ } = require('../../../lib/format-error');
13
19
 
14
20
  class ErrorHandler {
15
21
  /**
@@ -30,14 +36,7 @@ class ErrorHandler {
30
36
  * @returns {string} Formatted error string
31
37
  */
32
38
  formatError(message, actionText, commandHint) {
33
- let output = `${c.red}\u2716${c.reset} ${message}`;
34
- if (actionText) {
35
- output += ` ${c.dim}|${c.reset} ${c.cyan}Action:${c.reset} ${actionText}`;
36
- }
37
- if (commandHint) {
38
- output += ` ${c.dim}|${c.reset} ${c.green}Run:${c.reset} ${c.bold}${commandHint}${c.reset}`;
39
- }
40
- return output;
39
+ return formatErrorHelper(message, actionText, commandHint);
41
40
  }
42
41
 
43
42
  /**
@@ -48,14 +47,7 @@ class ErrorHandler {
48
47
  * @returns {string} Formatted warning string
49
48
  */
50
49
  formatWarning(message, actionText, commandHint) {
51
- let output = `${c.yellow}\u26A0${c.reset} ${message}`;
52
- if (actionText) {
53
- output += ` ${c.dim}|${c.reset} ${c.cyan}Action:${c.reset} ${actionText}`;
54
- }
55
- if (commandHint) {
56
- output += ` ${c.dim}|${c.reset} ${c.green}Run:${c.reset} ${c.bold}${commandHint}${c.reset}`;
57
- }
58
- return output;
50
+ return formatWarningHelper(message, actionText, commandHint);
59
51
  }
60
52
 
61
53
  /**
@@ -94,11 +86,11 @@ class ErrorHandler {
94
86
  * @param {Error} [error] - Original error object for stack trace
95
87
  */
96
88
  critical(message, actionText, commandHint, error) {
97
- console.error(this.formatError(message, actionText, commandHint));
98
- if (process.env.DEBUG === '1' && error?.stack) {
99
- console.error(`\n${c.dim}Stack trace:${c.reset}`);
100
- console.error(c.dim + error.stack + c.reset);
101
- }
89
+ const formatted = formatErrorWithStack(message, error, {
90
+ action: actionText,
91
+ command: commandHint,
92
+ });
93
+ console.error(formatted);
102
94
  process.exit(1);
103
95
  }
104
96