agile-context-engineering 0.3.0 → 0.5.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 (139) hide show
  1. package/.claude-plugin/marketplace.json +18 -0
  2. package/.claude-plugin/plugin.json +10 -0
  3. package/CHANGELOG.md +7 -1
  4. package/LICENSE +51 -51
  5. package/README.md +330 -318
  6. package/agents/ace-code-discovery-analyst.md +245 -245
  7. package/agents/ace-code-integration-analyst.md +248 -248
  8. package/agents/ace-code-reviewer.md +375 -375
  9. package/agents/ace-product-owner.md +365 -361
  10. package/agents/ace-project-researcher.md +606 -606
  11. package/agents/ace-research-synthesizer.md +228 -228
  12. package/agents/ace-technical-application-architect.md +315 -315
  13. package/agents/ace-wiki-mapper.md +449 -445
  14. package/bin/install.js +605 -195
  15. package/hooks/ace-check-update.js +71 -62
  16. package/hooks/ace-statusline.js +107 -89
  17. package/hooks/hooks.json +14 -0
  18. package/package.json +7 -5
  19. package/shared/lib/ace-core.js +361 -0
  20. package/shared/lib/ace-core.test.js +308 -0
  21. package/shared/lib/ace-github.js +753 -0
  22. package/shared/lib/ace-story.js +400 -0
  23. package/shared/lib/ace-story.test.js +250 -0
  24. package/{agile-context-engineering → shared}/utils/questioning.xml +110 -110
  25. package/{agile-context-engineering → shared}/utils/ui-formatting.md +299 -299
  26. package/{commands/ace/execute-story.md → skills/execute-story/SKILL.md} +116 -138
  27. package/skills/execute-story/script.js +291 -0
  28. package/skills/execute-story/script.test.js +261 -0
  29. package/{agile-context-engineering/templates/product/story.xml → skills/execute-story/story-template.xml} +451 -451
  30. package/skills/execute-story/walkthrough-template.xml +255 -0
  31. package/{agile-context-engineering/workflows/execute-story.xml → skills/execute-story/workflow.xml} +1221 -1219
  32. package/skills/help/SKILL.md +71 -0
  33. package/skills/help/script.js +315 -0
  34. package/skills/help/script.test.js +183 -0
  35. package/{agile-context-engineering/workflows/help.xml → skills/help/workflow.xml} +544 -533
  36. package/{commands/ace/init-coding-standards.md → skills/init-coding-standards/SKILL.md} +91 -83
  37. package/{agile-context-engineering/templates/wiki/coding-standards.xml → skills/init-coding-standards/coding-standards-template.xml} +531 -531
  38. package/skills/init-coding-standards/script.js +50 -0
  39. package/skills/init-coding-standards/script.test.js +70 -0
  40. package/{agile-context-engineering/workflows/init-coding-standards.xml → skills/init-coding-standards/workflow.xml} +381 -386
  41. package/skills/map-cross-cutting/SKILL.md +126 -0
  42. package/{agile-context-engineering/templates/wiki → skills/map-cross-cutting}/system-cross-cutting.xml +197 -197
  43. package/skills/map-cross-cutting/workflow.xml +330 -0
  44. package/skills/map-guide/SKILL.md +126 -0
  45. package/{agile-context-engineering/templates/wiki → skills/map-guide}/guide.xml +137 -137
  46. package/skills/map-guide/workflow.xml +320 -0
  47. package/skills/map-pattern/SKILL.md +125 -0
  48. package/{agile-context-engineering/templates/wiki → skills/map-pattern}/pattern.xml +159 -159
  49. package/skills/map-pattern/workflow.xml +331 -0
  50. package/{commands/ace/map-story.md → skills/map-story/SKILL.md} +180 -165
  51. package/{agile-context-engineering/templates/wiki → skills/map-story/templates}/decizions.xml +115 -115
  52. package/skills/map-story/templates/guide.xml +137 -0
  53. package/skills/map-story/templates/pattern.xml +159 -0
  54. package/skills/map-story/templates/system-cross-cutting.xml +197 -0
  55. package/{agile-context-engineering/templates/wiki → skills/map-story/templates}/system.xml +381 -381
  56. package/{agile-context-engineering/templates/wiki → skills/map-story/templates}/tech-debt-index.xml +125 -125
  57. package/{agile-context-engineering/templates/wiki → skills/map-story/templates}/walkthrough.xml +255 -255
  58. package/{agile-context-engineering/workflows/map-story.xml → skills/map-story/workflow.xml} +1046 -1046
  59. package/{commands/ace/map-subsystem.md → skills/map-subsystem/SKILL.md} +155 -140
  60. package/skills/map-subsystem/script.js +51 -0
  61. package/skills/map-subsystem/script.test.js +68 -0
  62. package/skills/map-subsystem/templates/decizions.xml +115 -0
  63. package/skills/map-subsystem/templates/guide.xml +137 -0
  64. package/{agile-context-engineering/templates/wiki → skills/map-subsystem/templates}/module-discovery.xml +174 -174
  65. package/skills/map-subsystem/templates/pattern.xml +159 -0
  66. package/{agile-context-engineering/templates/wiki → skills/map-subsystem/templates}/subsystem-architecture.xml +343 -343
  67. package/{agile-context-engineering/templates/wiki → skills/map-subsystem/templates}/subsystem-structure.xml +234 -234
  68. package/skills/map-subsystem/templates/system-cross-cutting.xml +197 -0
  69. package/skills/map-subsystem/templates/system.xml +381 -0
  70. package/skills/map-subsystem/templates/walkthrough.xml +255 -0
  71. package/{agile-context-engineering/workflows/map-subsystem.xml → skills/map-subsystem/workflow.xml} +1173 -1178
  72. package/skills/map-sys-doc/SKILL.md +125 -0
  73. package/skills/map-sys-doc/system.xml +381 -0
  74. package/skills/map-sys-doc/workflow.xml +336 -0
  75. package/{commands/ace/map-system.md → skills/map-system/SKILL.md} +103 -92
  76. package/skills/map-system/script.js +75 -0
  77. package/skills/map-system/script.test.js +73 -0
  78. package/{agile-context-engineering/templates/wiki → skills/map-system/templates}/system-architecture.xml +254 -254
  79. package/{agile-context-engineering/templates/wiki → skills/map-system/templates}/system-structure.xml +177 -177
  80. package/{agile-context-engineering/templates/wiki → skills/map-system/templates}/testing-framework.xml +283 -283
  81. package/{agile-context-engineering/templates/wiki → skills/map-system/templates}/wiki-readme.xml +296 -296
  82. package/{agile-context-engineering/workflows/map-system.xml → skills/map-system/workflow.xml} +667 -672
  83. package/{commands/ace/map-walkthrough.md → skills/map-walkthrough/SKILL.md} +140 -127
  84. package/skills/map-walkthrough/walkthrough.xml +255 -0
  85. package/{agile-context-engineering/workflows/map-walkthrough.xml → skills/map-walkthrough/workflow.xml} +457 -457
  86. package/{commands/ace/plan-backlog.md → skills/plan-backlog/SKILL.md} +93 -83
  87. package/{agile-context-engineering/templates/product/product-backlog.xml → skills/plan-backlog/product-backlog-template.xml} +231 -231
  88. package/skills/plan-backlog/script.js +121 -0
  89. package/skills/plan-backlog/script.test.js +83 -0
  90. package/{agile-context-engineering/workflows/plan-backlog.xml → skills/plan-backlog/workflow.xml} +1348 -1356
  91. package/{commands/ace/plan-feature.md → skills/plan-feature/SKILL.md} +99 -89
  92. package/{agile-context-engineering/templates/product/feature.xml → skills/plan-feature/feature-template.xml} +361 -361
  93. package/skills/plan-feature/script.js +131 -0
  94. package/skills/plan-feature/script.test.js +80 -0
  95. package/{agile-context-engineering/workflows/plan-feature.xml → skills/plan-feature/workflow.xml} +1487 -1495
  96. package/{commands/ace/plan-product-vision.md → skills/plan-product-vision/SKILL.md} +91 -81
  97. package/{agile-context-engineering/templates/product/product-vision.xml → skills/plan-product-vision/product-vision-template.xml} +227 -227
  98. package/skills/plan-product-vision/script.js +51 -0
  99. package/skills/plan-product-vision/script.test.js +69 -0
  100. package/{agile-context-engineering/workflows/plan-product-vision.xml → skills/plan-product-vision/workflow.xml} +337 -342
  101. package/{commands/ace/plan-story.md → skills/plan-story/SKILL.md} +139 -159
  102. package/skills/plan-story/script.js +295 -0
  103. package/skills/plan-story/script.test.js +240 -0
  104. package/skills/plan-story/story-template.xml +458 -0
  105. package/{agile-context-engineering/workflows/plan-story.xml → skills/plan-story/workflow.xml} +1301 -944
  106. package/{commands/ace/research-external-solution.md → skills/research-external-solution/SKILL.md} +120 -138
  107. package/{agile-context-engineering/templates/product/external-solution.xml → skills/research-external-solution/external-solution-template.xml} +832 -832
  108. package/skills/research-external-solution/script.js +229 -0
  109. package/skills/research-external-solution/script.test.js +134 -0
  110. package/{agile-context-engineering/workflows/research-external-solution.xml → skills/research-external-solution/workflow.xml} +657 -659
  111. package/{commands/ace/research-integration-solution.md → skills/research-integration-solution/SKILL.md} +121 -135
  112. package/{agile-context-engineering/templates/product/story-integration-solution.xml → skills/research-integration-solution/integration-solution-template.xml} +1015 -1015
  113. package/skills/research-integration-solution/script.js +223 -0
  114. package/skills/research-integration-solution/script.test.js +134 -0
  115. package/{agile-context-engineering/workflows/research-integration-solution.xml → skills/research-integration-solution/workflow.xml} +711 -713
  116. package/{commands/ace/research-story-wiki.md → skills/research-story-wiki/SKILL.md} +101 -116
  117. package/skills/research-story-wiki/script.js +223 -0
  118. package/skills/research-story-wiki/script.test.js +138 -0
  119. package/{agile-context-engineering/templates/product/story-wiki.xml → skills/research-story-wiki/story-wiki-template.xml} +194 -194
  120. package/{agile-context-engineering/workflows/research-story-wiki.xml → skills/research-story-wiki/workflow.xml} +473 -475
  121. package/{commands/ace/research-technical-solution.md → skills/research-technical-solution/SKILL.md} +131 -147
  122. package/skills/research-technical-solution/script.js +223 -0
  123. package/skills/research-technical-solution/script.test.js +134 -0
  124. package/{agile-context-engineering/templates/product/story-technical-solution.xml → skills/research-technical-solution/technical-solution-template.xml} +1025 -1025
  125. package/{agile-context-engineering/workflows/research-technical-solution.xml → skills/research-technical-solution/workflow.xml} +761 -763
  126. package/{commands/ace/review-story.md → skills/review-story/SKILL.md} +99 -109
  127. package/skills/review-story/script.js +249 -0
  128. package/skills/review-story/script.test.js +169 -0
  129. package/skills/review-story/story-template.xml +451 -0
  130. package/{agile-context-engineering/workflows/review-story.xml → skills/review-story/workflow.xml} +279 -281
  131. package/{commands/ace/update.md → skills/update/SKILL.md} +65 -56
  132. package/{agile-context-engineering/workflows/update.xml → skills/update/workflow.xml} +33 -18
  133. package/agile-context-engineering/src/ace-tools.js +0 -2881
  134. package/agile-context-engineering/src/ace-tools.test.js +0 -1089
  135. package/agile-context-engineering/templates/_command.md +0 -54
  136. package/agile-context-engineering/templates/_workflow.xml +0 -17
  137. package/agile-context-engineering/templates/config.json +0 -0
  138. package/agile-context-engineering/templates/product/integration-solution.xml +0 -0
  139. package/commands/ace/help.md +0 -93
package/bin/install.js CHANGED
@@ -4,6 +4,7 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const readline = require('readline');
6
6
  const os = require('os');
7
+ const { execSync } = require('child_process');
7
8
 
8
9
  const VERSION = require('../package.json').version;
9
10
 
@@ -25,22 +26,38 @@ const RUNTIMES = {
25
26
  name: 'Claude Code',
26
27
  description: "Anthropic's Claude Code CLI",
27
28
  globalDir: '.claude',
28
- commandsDir: 'commands',
29
+ supportsPlugin: true,
30
+ },
31
+ codex: {
32
+ name: 'Codex',
33
+ description: "OpenAI's Codex CLI",
34
+ globalDir: '.codex',
29
35
  agentsDir: 'agents',
30
- supportsLocal: true,
36
+ supportsPlugin: false,
31
37
  },
32
38
  opencode: {
33
39
  name: 'Crush',
34
40
  description: 'Crush AI coding assistant (formerly OpenCode)',
35
41
  globalDir: '.opencode',
36
- commandsDir: 'commands',
37
42
  agentsDir: 'agents',
38
- supportsLocal: true,
43
+ supportsPlugin: false,
39
44
  },
40
45
  };
41
46
 
42
- // The folder name installed inside the config directory (e.g. ~/.claude/agile-context-engineering/)
43
- const ACE_DIR_NAME = 'agile-context-engineering';
47
+ const MARKETPLACE_NAME = 'ace-marketplace';
48
+ const CODEX_CONFIG_BEGIN = '# ACE Agent Configuration - managed by agile-context-engineering installer';
49
+ const CODEX_CONFIG_END = '# End ACE Agent Configuration';
50
+
51
+ const CODEX_AGENT_SANDBOX = {
52
+ 'ace-code-reviewer': 'read-only',
53
+ 'ace-code-discovery-analyst': 'read-only',
54
+ 'ace-code-integration-analyst': 'read-only',
55
+ 'ace-project-researcher': 'read-only',
56
+ 'ace-research-synthesizer': 'workspace-write',
57
+ 'ace-product-owner': 'workspace-write',
58
+ 'ace-technical-application-architect': 'workspace-write',
59
+ 'ace-wiki-mapper': 'workspace-write',
60
+ };
44
61
 
45
62
  function log(message, color = '') {
46
63
  console.log(`${color}${message}${colors.reset}`);
@@ -63,6 +80,7 @@ function parseArgs() {
63
80
  const args = process.argv.slice(2);
64
81
  const flags = {
65
82
  claude: args.includes('--claude'),
83
+ codex: args.includes('--codex'),
66
84
  opencode: args.includes('--opencode'),
67
85
  all: args.includes('--all'),
68
86
  global: args.includes('--global'),
@@ -86,22 +104,17 @@ function readSettings(settingsPath) {
86
104
  return {};
87
105
  }
88
106
 
89
- // Build hook command with proper quoting for the target directory
90
- function buildHookCommand(targetDir, hookFile) {
91
- const hookPath = path.join(targetDir, 'hooks', hookFile);
92
- return `node "${hookPath.replace(/\\/g, '/')}"`;
93
- }
94
-
95
107
  function showHelp() {
96
108
  log(`
97
109
  Usage: npx agile-context-engineering [options]
98
110
 
99
111
  Options:
100
112
  --claude Install for Claude Code only
113
+ --codex Install for Codex only
101
114
  --opencode Install for Crush (formerly OpenCode)
102
115
  --all Install for all supported runtimes
103
- --global Install globally (~/.claude, ~/.opencode)
104
- --local Install locally (.claude, .opencode)
116
+ --global Install globally (~/.claude, ~/.codex, ~/.opencode)
117
+ --local Install locally (.claude, .codex, .opencode)
105
118
  --force-statusline Replace existing statusline configuration
106
119
  -h, --help Show this help message
107
120
  -v, --version Show version number
@@ -109,6 +122,7 @@ Options:
109
122
  Examples:
110
123
  npx agile-context-engineering # Interactive installation
111
124
  npx agile-context-engineering --claude --local # Claude Code, local install
125
+ npx agile-context-engineering --codex --global # Codex, global install
112
126
  npx agile-context-engineering --opencode --global # Crush (formerly OpenCode), global install
113
127
  npx agile-context-engineering --all --global # All runtimes, global install
114
128
  `);
@@ -178,20 +192,25 @@ function getBasePath(runtime, scope) {
178
192
  const cwd = process.cwd();
179
193
  const config = RUNTIMES[runtime];
180
194
 
195
+ if (runtime === 'codex' && scope === 'global' && process.env.CODEX_HOME) {
196
+ return path.resolve(process.env.CODEX_HOME);
197
+ }
198
+
181
199
  return scope === 'global'
182
200
  ? path.join(home, config.globalDir)
183
201
  : path.join(cwd, config.globalDir);
184
202
  }
185
203
 
186
204
  // File extensions that contain path references needing runtime transformation
187
- const TRANSFORMABLE_EXTENSIONS = new Set(['.md', '.xml']);
205
+ const TRANSFORMABLE_EXTENSIONS = new Set(['.md', '.xml', '.js']);
188
206
 
189
207
  // Transform file content for a target runtime (replaces .claude/ paths with target runtime paths)
190
208
  function transformForRuntime(content, runtime) {
191
209
  if (runtime === 'claude') return content; // Source files already use .claude paths
192
210
  const targetDir = RUNTIMES[runtime].globalDir; // e.g. '.opencode'
193
- // Replace path references: ~/.claude/ → ~/.opencode/, .claude/settings → .opencode/settings, etc.
194
- return content.replace(/\.claude\//g, `${targetDir}/`);
211
+ return content
212
+ .replace(/\.claude\//g, `${targetDir}/`)
213
+ .replace(/\.claudeignore\b/g, `${targetDir}ignore`);
195
214
  }
196
215
 
197
216
  // Copy directory recursively, optionally transforming text file content for the target runtime
@@ -211,7 +230,6 @@ function copyDir(src, dest, runtime) {
211
230
  } else {
212
231
  const ext = path.extname(entry.name).toLowerCase();
213
232
  if (runtime !== 'claude' && TRANSFORMABLE_EXTENSIONS.has(ext)) {
214
- // Transform path references for non-Claude runtimes
215
233
  const content = fs.readFileSync(srcPath, 'utf-8');
216
234
  fs.writeFileSync(destPath, transformForRuntime(content, runtime), 'utf-8');
217
235
  } else {
@@ -221,109 +239,566 @@ function copyDir(src, dest, runtime) {
221
239
  }
222
240
  }
223
241
 
224
- // Install ACE for a runtime (Claude Code or Crush)
225
- function installForRuntime(runtime, scope, packageDir) {
226
- const config = RUNTIMES[runtime];
227
- const basePath = getBasePath(runtime, scope);
228
- const commandsPath = path.join(basePath, config.commandsDir);
229
- const agentsPath = path.join(basePath, config.agentsDir);
230
- const acePath = path.join(basePath, ACE_DIR_NAME);
242
+ function toPosixPath(filePath) {
243
+ return filePath.replace(/\\/g, '/');
244
+ }
231
245
 
232
- // Source directories
233
- const srcCommands = path.join(packageDir, 'commands');
234
- const srcAgents = path.join(packageDir, 'agents');
235
- const srcTemplates = path.join(packageDir, 'agile-context-engineering', 'templates');
236
- const srcUtils = path.join(packageDir, 'agile-context-engineering', 'utils');
237
- const srcWorkflows = path.join(packageDir, 'agile-context-engineering', 'workflows');
238
- const srcTools = path.join(packageDir, 'agile-context-engineering', 'src');
246
+ function replaceAceInvocations(content, codexStyle) {
247
+ if (!codexStyle) return content;
248
+ return content.replace(/\/ace:([a-z0-9-]+)/g, '$ace-$1');
249
+ }
239
250
 
240
- log(`\nInstalling ACE for ${config.name}...`, colors.cyan);
241
- log(` Target: ${basePath}`, colors.dim);
251
+ function transformCodexSkillContent(content, skillName, skillDir) {
252
+ let transformed = content
253
+ .replace(/\.claude\//g, '.codex/')
254
+ .replace(/\.claudeignore\b/g, '.codexignore')
255
+ .replace(/\$\{CLAUDE_SKILL_DIR\}/g, toPosixPath(skillDir))
256
+ .replace(/"\$CLAUDE_SKILL_DIR\//g, `"${toPosixPath(skillDir)}/`)
257
+ .replace(/\$CLAUDE_SKILL_DIR\//g, `${toPosixPath(skillDir)}/`)
258
+ .replace(/CLAUDE_SKILL_DIR/g, 'CODEX_SKILL_DIR');
259
+
260
+ transformed = replaceAceInvocations(transformed, true);
261
+
262
+ if (path.basename(skillDir) === skillName && content.includes('---')) {
263
+ transformed = transformed.replace(/^name:\s*.+$/m, `name: ace-${skillName}`);
264
+ }
265
+
266
+ return transformed;
267
+ }
268
+
269
+ function codexSkillAdapter(skillName, skillDir) {
270
+ const posixSkillDir = toPosixPath(skillDir);
271
+ return `<codex_skill_adapter>
272
+ ## Codex Invocation
273
+ - Invoke this skill by mentioning \`$ace-${skillName}\`.
274
+ - Treat any user text after \`$ace-${skillName}\` as the command arguments.
275
+ - In examples copied from Claude Code, \`/ace:${skillName}\` means \`$ace-${skillName}\`.
276
+
277
+ ## Runtime Compatibility
278
+ - This repository is authored as a Claude Code plugin. In Codex, the installer copies it to \`${posixSkillDir}\`.
279
+ - Claude's \`!\` resource expansion does not run in Codex. Before following the workflow, manually read the supporting files referenced near the top of this SKILL.md.
280
+ - When a workflow says \`AskUserQuestion\`, use Codex \`request_user_input\` if available; otherwise ask the user directly and continue with a reasonable default only when the choice is low risk.
281
+ - When a workflow says \`Task(..., subagent_type="X")\`, use \`spawn_agent(agent_type="X", message="...")\` only when the user explicitly requested sub-agents. Otherwise complete the work inline in the current agent.
282
+ - Prefer commands that use the absolute skill path above. It keeps Windows and Linux shells from depending on a runtime-specific skill directory environment variable.
283
+ </codex_skill_adapter>
284
+
285
+ `;
286
+ }
287
+
288
+ function copySkillsForCodex(srcSkills, destSkills) {
289
+ if (!fs.existsSync(srcSkills)) return 0;
290
+ fs.mkdirSync(destSkills, { recursive: true });
291
+
292
+ for (const entry of fs.readdirSync(destSkills, { withFileTypes: true })) {
293
+ if (entry.isDirectory() && entry.name.startsWith('ace-')) {
294
+ fs.rmSync(path.join(destSkills, entry.name), { recursive: true });
295
+ }
296
+ }
242
297
 
243
- // Clean previous ACE installation to remove stale files from renamed/deleted commands
244
- const aceCommandsPath = path.join(commandsPath, 'ace');
245
- if (fs.existsSync(aceCommandsPath)) {
246
- fs.rmSync(aceCommandsPath, { recursive: true });
298
+ let count = 0;
299
+ for (const entry of fs.readdirSync(srcSkills, { withFileTypes: true })) {
300
+ if (!entry.isDirectory()) continue;
301
+ const skillName = entry.name;
302
+ const srcDir = path.join(srcSkills, skillName);
303
+ const destDir = path.join(destSkills, `ace-${skillName}`);
304
+ copyCodexSkillDir(srcDir, destDir, skillName);
305
+ count += 1;
306
+ }
307
+ return count;
308
+ }
309
+
310
+ function copyCodexSkillDir(src, dest, skillName) {
311
+ fs.mkdirSync(dest, { recursive: true });
312
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
313
+ const srcPath = path.join(src, entry.name);
314
+ const destPath = path.join(dest, entry.name);
315
+ if (entry.isDirectory()) {
316
+ copyCodexSkillDir(srcPath, destPath, skillName);
317
+ continue;
318
+ }
319
+
320
+ const ext = path.extname(entry.name).toLowerCase();
321
+ if (TRANSFORMABLE_EXTENSIONS.has(ext)) {
322
+ let content = fs.readFileSync(srcPath, 'utf-8');
323
+ content = transformCodexSkillContent(content, skillName, dest);
324
+ if (entry.name === 'SKILL.md') {
325
+ content = content.replace(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/, (match, body) => {
326
+ const lines = body.split(/\r?\n/);
327
+ const filtered = lines.filter(line =>
328
+ !/^(allowed-tools|argument-hint|disable-model-invocation|model|effort):/.test(line.trim())
329
+ );
330
+ const nameIndex = filtered.findIndex(line => /^name:/.test(line));
331
+ if (nameIndex >= 0) {
332
+ filtered[nameIndex] = `name: ace-${skillName}`;
333
+ } else {
334
+ filtered.unshift(`name: ace-${skillName}`);
335
+ }
336
+ return `---\n${filtered.join('\n')}\n---\n\n`;
337
+ });
338
+ content = content.replace(/^(---\r?\n[\s\S]*?\r?\n---\r?\n)/, `$1\n${codexSkillAdapter(skillName, dest)}`);
339
+ }
340
+ fs.writeFileSync(destPath, content, 'utf-8');
341
+ } else {
342
+ fs.copyFileSync(srcPath, destPath);
343
+ }
344
+ }
345
+ }
346
+
347
+ function extractFrontmatter(content) {
348
+ const match = content.match(/^\s*(?:<!--[\s\S]*?-->\s*)*---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
349
+ if (!match) return { frontmatter: '', body: content };
350
+ return { frontmatter: match[1], body: content.slice(match[0].length) };
351
+ }
352
+
353
+ function extractFrontmatterField(frontmatter, field) {
354
+ const re = new RegExp(`^${field}:\\s*(.*)$`, 'm');
355
+ const match = frontmatter.match(re);
356
+ return match ? match[1].trim().replace(/^["']|["']$/g, '') : '';
357
+ }
358
+
359
+ function generateCodexAgentToml(agentName, sourceContent) {
360
+ const content = replaceAceInvocations(
361
+ sourceContent
362
+ .replace(/\.claude\//g, '.codex/')
363
+ .replace(/\.claudeignore\b/g, '.codexignore')
364
+ .replace(/CLAUDE_SKILL_DIR/g, 'CODEX_SKILL_DIR'),
365
+ true
366
+ );
367
+ const { frontmatter, body } = extractFrontmatter(content);
368
+ const name = extractFrontmatterField(frontmatter, 'name') || agentName;
369
+ const description = extractFrontmatterField(frontmatter, 'description') || `ACE agent ${name}`;
370
+ const sandbox = CODEX_AGENT_SANDBOX[name] || 'workspace-write';
371
+ return [
372
+ `name = ${JSON.stringify(name)}`,
373
+ `description = ${JSON.stringify(description)}`,
374
+ `sandbox_mode = ${JSON.stringify(sandbox)}`,
375
+ `developer_instructions = '''`,
376
+ body.trim(),
377
+ `'''`,
378
+ '',
379
+ ].join('\n');
380
+ }
381
+
382
+ function installCodexAgents(configDir, srcAgents) {
383
+ if (!fs.existsSync(srcAgents)) return 0;
384
+ const agentsDir = path.join(configDir, 'agents');
385
+ fs.mkdirSync(agentsDir, { recursive: true });
386
+
387
+ for (const file of fs.readdirSync(agentsDir)) {
388
+ if (file.startsWith('ace-') && file.endsWith('.toml')) {
389
+ fs.rmSync(path.join(agentsDir, file), { force: true });
390
+ }
247
391
  }
392
+
393
+ const agents = [];
394
+ for (const file of fs.readdirSync(srcAgents)) {
395
+ if (!file.startsWith('ace-') || !file.endsWith('.md')) continue;
396
+ const agentName = file.slice(0, -3);
397
+ const content = fs.readFileSync(path.join(srcAgents, file), 'utf-8');
398
+ const { frontmatter } = extractFrontmatter(content);
399
+ const description = extractFrontmatterField(frontmatter, 'description') || `ACE agent ${agentName}`;
400
+ fs.writeFileSync(path.join(agentsDir, `${agentName}.toml`), generateCodexAgentToml(agentName, content), 'utf-8');
401
+ agents.push({ name: agentName, description });
402
+ }
403
+
404
+ mergeCodexAgentConfig(path.join(configDir, 'config.toml'), agents, agentsDir);
405
+ return agents.length;
406
+ }
407
+
408
+ function mergeCodexAgentConfig(configPath, agents, agentsDir) {
409
+ const eol = os.EOL;
410
+ let existing = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf-8') : '';
411
+ const blockPattern = new RegExp(`${escapeRegex(CODEX_CONFIG_BEGIN)}[\\s\\S]*?${escapeRegex(CODEX_CONFIG_END)}\\r?\\n?`, 'm');
412
+ existing = existing.replace(blockPattern, '').trimEnd();
413
+
414
+ const lines = [CODEX_CONFIG_BEGIN, ''];
415
+ for (const agent of agents) {
416
+ lines.push(`[agents.${agent.name}]`);
417
+ lines.push(`description = ${JSON.stringify(agent.description)}`);
418
+ lines.push(`config_file = ${JSON.stringify(`${toPosixPath(agentsDir)}/${agent.name}.toml`)}`);
419
+ lines.push('');
420
+ }
421
+ lines.push(CODEX_CONFIG_END);
422
+
423
+ const prefix = existing ? `${existing}${eol}${eol}` : '';
424
+ fs.writeFileSync(configPath, `${prefix}${lines.join(eol)}${eol}`, 'utf-8');
425
+ }
426
+
427
+ function escapeRegex(value) {
428
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
429
+ }
430
+
431
+ // Check if claude CLI is available
432
+ function hasClaudeCli() {
433
+ try {
434
+ execSync('claude --version', { encoding: 'utf8', stdio: 'pipe', windowsHide: true });
435
+ return true;
436
+ } catch {
437
+ return false;
438
+ }
439
+ }
440
+
441
+ // Run a claude CLI command, return { success, output }
442
+ function runClaude(args) {
443
+ try {
444
+ const output = execSync(`claude ${args}`, { encoding: 'utf8', stdio: 'pipe', windowsHide: true, timeout: 30000 });
445
+ return { success: true, output: output.trim() };
446
+ } catch (e) {
447
+ return { success: false, output: (e.stderr || e.message || '').trim() };
448
+ }
449
+ }
450
+
451
+ // Clean up old standalone ACE installation from ~/.claude/
452
+ function cleanLegacyInstall(basePath) {
453
+ const dirsToClean = [
454
+ path.join(basePath, 'skills'),
455
+ path.join(basePath, 'shared'),
456
+ path.join(basePath, '.claude-plugin'),
457
+ path.join(basePath, 'agile-context-engineering'), // pre-plugin legacy
458
+ ];
459
+ const commandsAce = path.join(basePath, 'commands', 'ace');
460
+ if (fs.existsSync(commandsAce)) {
461
+ dirsToClean.push(commandsAce);
462
+ }
463
+
464
+ let cleaned = false;
465
+ for (const dir of dirsToClean) {
466
+ if (fs.existsSync(dir)) {
467
+ fs.rmSync(dir, { recursive: true });
468
+ cleaned = true;
469
+ }
470
+ }
471
+
472
+ // Clean ace-* agents
473
+ const agentsPath = path.join(basePath, 'agents');
248
474
  if (fs.existsSync(agentsPath)) {
249
- // Only remove ace-* agent files, preserve non-ACE agents
250
475
  for (const f of fs.readdirSync(agentsPath)) {
251
476
  if (f.startsWith('ace-')) {
252
477
  fs.rmSync(path.join(agentsPath, f), { recursive: true });
478
+ cleaned = true;
253
479
  }
254
480
  }
255
481
  }
256
- if (fs.existsSync(acePath)) {
257
- fs.rmSync(acePath, { recursive: true });
482
+
483
+ // Clean legacy hooks from settings.json
484
+ const settingsPath = path.join(basePath, 'settings.json');
485
+ if (fs.existsSync(settingsPath)) {
486
+ try {
487
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
488
+ let modified = false;
489
+
490
+ // Remove ACE SessionStart hooks (now handled by plugin hooks.json)
491
+ if (settings.hooks?.SessionStart) {
492
+ const before = settings.hooks.SessionStart.length;
493
+ settings.hooks.SessionStart = settings.hooks.SessionStart.filter(entry =>
494
+ !(entry.hooks && entry.hooks.some(h => h.command && h.command.includes('ace-')))
495
+ );
496
+ if (settings.hooks.SessionStart.length === 0) {
497
+ delete settings.hooks.SessionStart;
498
+ }
499
+ if (Object.keys(settings.hooks).length === 0) {
500
+ delete settings.hooks;
501
+ }
502
+ if (settings.hooks?.SessionStart?.length !== before) {
503
+ modified = true;
504
+ }
505
+ }
506
+
507
+ if (modified) {
508
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
509
+ }
510
+ } catch {}
258
511
  }
259
512
 
260
- // Create directories
261
- fs.mkdirSync(commandsPath, { recursive: true });
262
- fs.mkdirSync(agentsPath, { recursive: true });
263
- fs.mkdirSync(acePath, { recursive: true });
513
+ return cleaned;
514
+ }
264
515
 
265
- // Copy commands (transform paths for target runtime)
266
- if (fs.existsSync(srcCommands)) {
267
- copyDir(srcCommands, commandsPath, runtime);
268
- log(` ✓ Commands installed`, colors.green);
516
+ // Install ACE for Claude Code using the plugin marketplace system
517
+ function installForClaude(scope, packageDir, flags) {
518
+ const basePath = getBasePath('claude', scope);
519
+
520
+ log(`\nInstalling ACE for Claude Code (plugin marketplace)...`, colors.cyan);
521
+ log(` Scope: ${scope}`, colors.dim);
522
+
523
+ // Step 1: Clean old standalone install
524
+ const hadLegacy = cleanLegacyInstall(basePath);
525
+ if (hadLegacy) {
526
+ log(` ✓ Cleaned legacy standalone installation`, colors.green);
269
527
  }
270
528
 
271
- // Copy agents (transform paths for target runtime)
272
- if (fs.existsSync(srcAgents)) {
273
- copyDir(srcAgents, agentsPath, runtime);
274
- log(` ✓ Agents installed`, colors.green);
529
+ // Step 2: Clean old cached plugin versions
530
+ const aceCacheDir = path.join(basePath, 'plugins', 'cache', MARKETPLACE_NAME, 'ace');
531
+ if (fs.existsSync(aceCacheDir)) {
532
+ const cachedVersions = fs.readdirSync(aceCacheDir);
533
+ if (cachedVersions.length > 0) {
534
+ for (const ver of cachedVersions) {
535
+ fs.rmSync(path.join(aceCacheDir, ver), { recursive: true });
536
+ }
537
+ log(` ✓ Cleaned ${cachedVersions.length} old cached version(s)`, colors.green);
538
+ }
275
539
  }
276
540
 
277
- // Copy templates into agile-context-engineering/
278
- if (fs.existsSync(srcTemplates)) {
279
- copyDir(srcTemplates, path.join(acePath, 'templates'), runtime);
280
- log(` Templates installed`, colors.green);
541
+ // Step 3: Check for claude CLI
542
+ if (!hasClaudeCli()) {
543
+ log(` ✗ Claude CLI not found in PATH`, colors.red);
544
+ log(` Install Claude Code first: https://code.claude.com/docs/en/quickstart`, colors.dim);
545
+ return { success: false, path: basePath };
281
546
  }
282
547
 
283
- // Copy utils into agile-context-engineering/
284
- if (fs.existsSync(srcUtils)) {
285
- copyDir(srcUtils, path.join(acePath, 'utils'), runtime);
286
- log(` Utils installed`, colors.green);
548
+ // Step 4: Add/update marketplace from this package directory
549
+ // First check if marketplace already exists
550
+ const listResult = runClaude('plugin marketplace list --json');
551
+ let marketplaceExists = false;
552
+ if (listResult.success) {
553
+ try {
554
+ // Check output for our marketplace name
555
+ marketplaceExists = listResult.output.includes(MARKETPLACE_NAME);
556
+ } catch {}
287
557
  }
288
558
 
289
- // Copy workflows into agile-context-engineering/
290
- if (fs.existsSync(srcWorkflows)) {
291
- copyDir(srcWorkflows, path.join(acePath, 'workflows'), runtime);
292
- log(` ✓ Workflows installed`, colors.green);
559
+ const packageDirUnix = packageDir.replace(/\\/g, '/');
560
+
561
+ if (marketplaceExists) {
562
+ // Update existing marketplace to pick up changes
563
+ const updateResult = runClaude(`plugin marketplace update ${MARKETPLACE_NAME}`);
564
+ if (updateResult.success) {
565
+ log(` ✓ Updated ACE marketplace`, colors.green);
566
+ } else {
567
+ // If update fails, remove and re-add
568
+ runClaude(`plugin marketplace remove ${MARKETPLACE_NAME}`);
569
+ const addResult = runClaude(`plugin marketplace add "${packageDirUnix}"`);
570
+ if (addResult.success) {
571
+ log(` ✓ Re-added ACE marketplace`, colors.green);
572
+ } else {
573
+ log(` ✗ Failed to add marketplace: ${addResult.output}`, colors.red);
574
+ return { success: false, path: basePath };
575
+ }
576
+ }
577
+ } else {
578
+ const addResult = runClaude(`plugin marketplace add "${packageDirUnix}"`);
579
+ if (addResult.success) {
580
+ log(` ✓ Added ACE marketplace`, colors.green);
581
+ } else {
582
+ log(` ✗ Failed to add marketplace: ${addResult.output}`, colors.red);
583
+ return { success: false, path: basePath };
584
+ }
293
585
  }
294
586
 
295
- // Copy src (ace-tools) into agile-context-engineering/
296
- if (fs.existsSync(srcTools)) {
297
- copyDir(srcTools, path.join(acePath, 'src'), runtime);
298
- log(` ✓ Tools installed`, colors.green);
587
+ // Step 5: Install or update the ACE plugin
588
+ const pluginId = `ace@${MARKETPLACE_NAME}`;
589
+ const scopeFlag = scope === 'global' ? '--scope user' : `--scope ${scope}`;
590
+
591
+ // Try install first; if already installed, try update
592
+ const installResult = runClaude(`plugin install ${pluginId} ${scopeFlag}`);
593
+ if (installResult.success) {
594
+ log(` ✓ ACE plugin installed`, colors.green);
595
+ } else if (installResult.output.includes('already installed') || installResult.output.includes('already enabled')) {
596
+ const updateResult = runClaude(`plugin update ${pluginId} ${scopeFlag}`);
597
+ if (updateResult.success) {
598
+ log(` ✓ ACE plugin updated`, colors.green);
599
+ } else {
600
+ log(` ⚠ Plugin update note: ${updateResult.output}`, colors.yellow);
601
+ }
602
+ } else {
603
+ log(` ✗ Failed to install plugin: ${installResult.output}`, colors.red);
604
+ return { success: false, path: basePath };
605
+ }
606
+
607
+ // Step 6: Configure statusline (not part of plugin hooks — goes in settings.json)
608
+ configureStatusline(basePath, flags);
609
+
610
+ return { success: true, path: basePath };
611
+ }
612
+
613
+ // Configure the ACE statusline in settings.json
614
+ function configureStatusline(basePath, flags) {
615
+ const settingsPath = path.join(basePath, 'settings.json');
616
+ const settings = readSettings(settingsPath);
617
+
618
+ // Statusline is a settings.json config, not a plugin hook, so CLAUDE_PLUGIN_ROOT
619
+ // is not available. We write a thin wrapper to ~/.claude/hooks/ that finds the
620
+ // installed plugin's statusline script at runtime.
621
+ const wrapperPath = path.join(basePath, 'hooks', 'ace-statusline-wrapper.js');
622
+ const wrapperDir = path.join(basePath, 'hooks');
623
+ if (!fs.existsSync(wrapperDir)) {
624
+ fs.mkdirSync(wrapperDir, { recursive: true });
299
625
  }
300
626
 
301
- // Copy hooks
302
- const srcHooks = path.join(packageDir, 'hooks');
303
- const hooksPath = path.join(basePath, 'hooks');
304
- if (fs.existsSync(srcHooks)) {
305
- // Only copy ace-* hook files, preserve non-ACE hooks (e.g. GSD)
306
- if (!fs.existsSync(hooksPath)) {
307
- fs.mkdirSync(hooksPath, { recursive: true });
627
+ // Write a thin wrapper that finds the ace plugin statusline script
628
+ fs.writeFileSync(wrapperPath, `#!/usr/bin/env node
629
+ // ACE statusline wrapper — finds the installed plugin's statusline script
630
+ const fs = require('fs');
631
+ const path = require('path');
632
+ const home = require('os').homedir();
633
+
634
+ // Search plugin cache for ace plugin's statusline
635
+ const cacheDir = path.join(home, '.claude', 'plugins', 'cache');
636
+ let scriptPath = null;
637
+
638
+ function findScript(dir, depth) {
639
+ if (depth > 5 || !fs.existsSync(dir)) return false;
640
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
641
+ const full = path.join(dir, entry.name);
642
+ if (entry.isDirectory()) {
643
+ if (findScript(full, depth + 1)) return true;
644
+ } else if (entry.name === 'ace-statusline.js' && dir.includes('ace')) {
645
+ scriptPath = full;
646
+ return true;
308
647
  }
309
- for (const f of fs.readdirSync(srcHooks)) {
310
- if (f.startsWith('ace-')) {
311
- fs.copyFileSync(path.join(srcHooks, f), path.join(hooksPath, f));
312
- }
648
+ }
649
+ return false;
650
+ }
651
+
652
+ if (fs.existsSync(cacheDir)) {
653
+ findScript(cacheDir, 0);
654
+ }
655
+
656
+ if (scriptPath) {
657
+ // Pipe stdin through to the actual script
658
+ const { spawn } = require('child_process');
659
+ const child = spawn(process.execPath, [scriptPath], {
660
+ stdio: ['pipe', 'inherit', 'inherit'],
661
+ windowsHide: true
662
+ });
663
+ process.stdin.pipe(child.stdin);
664
+ child.on('exit', (code) => process.exit(code || 0));
665
+ } else {
666
+ // Fallback: basic statusline
667
+ let input = '';
668
+ process.stdin.setEncoding('utf8');
669
+ process.stdin.on('data', chunk => input += chunk);
670
+ process.stdin.on('end', () => {
671
+ try {
672
+ const data = JSON.parse(input);
673
+ const model = data.model?.display_name || 'Claude';
674
+ const dir = path.basename(data.workspace?.current_dir || process.cwd());
675
+ process.stdout.write(model + ' | ' + dir);
676
+ } catch {}
677
+ });
678
+ }
679
+ `, 'utf-8');
680
+
681
+ const statuslineCmd = `node "${wrapperPath.replace(/\\/g, '/')}"`;
682
+
683
+ const hasExisting = settings.statusLine != null;
684
+ const isAceStatusline = hasExisting && settings.statusLine.command &&
685
+ (settings.statusLine.command.includes('ace-statusline') || settings.statusLine.command.includes('ace-'));
686
+
687
+ if (!hasExisting || flags.forceStatusline) {
688
+ settings.statusLine = { type: 'command', command: statuslineCmd };
689
+ log(` ✓ Configured statusline`, colors.green);
690
+ } else if (isAceStatusline) {
691
+ settings.statusLine = { type: 'command', command: statuslineCmd };
692
+ log(` ✓ Updated statusline`, colors.green);
693
+ } else {
694
+ log(` ⚠ Skipping statusline (already configured, use --force-statusline to replace)`, colors.yellow);
695
+ }
696
+
697
+ // Write settings
698
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
699
+ }
700
+
701
+ // Install ACE for Crush (legacy copy approach — no plugin system)
702
+ function installForCrush(scope, packageDir) {
703
+ const config = RUNTIMES.opencode;
704
+ const basePath = getBasePath('opencode', scope);
705
+ const agentsPath = path.join(basePath, config.agentsDir);
706
+
707
+ const srcSkills = path.join(packageDir, 'skills');
708
+ const srcShared = path.join(packageDir, 'shared');
709
+ const srcPlugin = path.join(packageDir, '.claude-plugin');
710
+ const srcAgents = path.join(packageDir, 'agents');
711
+
712
+ log(`\nInstalling ACE for ${config.name} (legacy copy)...`, colors.cyan);
713
+ log(` Target: ${basePath}`, colors.dim);
714
+
715
+ // Clean previous installation
716
+ const skillsPath = path.join(basePath, 'skills');
717
+ const sharedPath = path.join(basePath, 'shared');
718
+ const pluginPath = path.join(basePath, '.claude-plugin');
719
+ const legacyPath = path.join(basePath, 'agile-context-engineering');
720
+
721
+ for (const p of [skillsPath, sharedPath, pluginPath, legacyPath]) {
722
+ if (fs.existsSync(p)) fs.rmSync(p, { recursive: true });
723
+ }
724
+ if (fs.existsSync(agentsPath)) {
725
+ for (const f of fs.readdirSync(agentsPath)) {
726
+ if (f.startsWith('ace-')) fs.rmSync(path.join(agentsPath, f), { recursive: true });
313
727
  }
314
- log(` ✓ Hooks installed`, colors.green);
315
728
  }
316
729
 
317
- // Write VERSION file for update checking
318
- const versionFile = path.join(acePath, 'VERSION');
730
+ fs.mkdirSync(agentsPath, { recursive: true });
731
+
732
+ if (fs.existsSync(srcSkills)) {
733
+ copyDir(srcSkills, skillsPath, 'opencode');
734
+ log(` ✓ Skills installed`, colors.green);
735
+ }
736
+ if (fs.existsSync(srcShared)) {
737
+ copyDir(srcShared, sharedPath, 'opencode');
738
+ log(` ✓ Shared libs installed`, colors.green);
739
+ }
740
+ if (fs.existsSync(srcPlugin)) {
741
+ copyDir(srcPlugin, pluginPath, 'opencode');
742
+ log(` ✓ Plugin manifest installed`, colors.green);
743
+ }
744
+ if (fs.existsSync(srcAgents)) {
745
+ copyDir(srcAgents, agentsPath, 'opencode');
746
+ log(` ✓ Agents installed`, colors.green);
747
+ }
748
+
749
+ // Write VERSION file
750
+ const versionFile = path.join(sharedPath, 'VERSION');
751
+ if (!fs.existsSync(sharedPath)) fs.mkdirSync(sharedPath, { recursive: true });
319
752
  fs.writeFileSync(versionFile, VERSION, 'utf-8');
320
753
 
321
754
  // Copy CHANGELOG.md
322
755
  const changelogSrc = path.join(packageDir, 'CHANGELOG.md');
323
- const changelogDest = path.join(acePath, 'CHANGELOG.md');
756
+ const changelogDest = path.join(sharedPath, 'CHANGELOG.md');
757
+ if (fs.existsSync(changelogSrc)) {
758
+ fs.copyFileSync(changelogSrc, changelogDest);
759
+ }
760
+
761
+ return basePath;
762
+ }
763
+
764
+ // Install ACE for Codex as native Codex skills + agent TOML config
765
+ function installForCodex(scope, packageDir) {
766
+ const config = RUNTIMES.codex;
767
+ const basePath = getBasePath('codex', scope);
768
+ const skillsPath = path.join(basePath, 'skills');
769
+ const sharedPath = path.join(basePath, 'shared');
770
+
771
+ const srcSkills = path.join(packageDir, 'skills');
772
+ const srcShared = path.join(packageDir, 'shared');
773
+ const srcAgents = path.join(packageDir, 'agents');
774
+
775
+ log(`\nInstalling ACE for ${config.name} (native skills)...`, colors.cyan);
776
+ log(` Target: ${basePath}`, colors.dim);
777
+
778
+ fs.mkdirSync(basePath, { recursive: true });
779
+
780
+ const skillCount = copySkillsForCodex(srcSkills, skillsPath);
781
+ log(` ✓ ${skillCount} skills installed`, colors.green);
782
+
783
+ if (fs.existsSync(sharedPath)) {
784
+ fs.rmSync(sharedPath, { recursive: true });
785
+ }
786
+ if (fs.existsSync(srcShared)) {
787
+ copyDir(srcShared, sharedPath, 'codex');
788
+ log(` ✓ Shared libs installed`, colors.green);
789
+ }
790
+
791
+ const agentCount = installCodexAgents(basePath, srcAgents);
792
+ log(` ✓ ${agentCount} agents configured`, colors.green);
793
+
794
+ const versionFile = path.join(sharedPath, 'VERSION');
795
+ if (!fs.existsSync(sharedPath)) fs.mkdirSync(sharedPath, { recursive: true });
796
+ fs.writeFileSync(versionFile, VERSION, 'utf-8');
797
+
798
+ const changelogSrc = path.join(packageDir, 'CHANGELOG.md');
799
+ const changelogDest = path.join(sharedPath, 'CHANGELOG.md');
324
800
  if (fs.existsSync(changelogSrc)) {
325
801
  fs.copyFileSync(changelogSrc, changelogDest);
326
- log(` ✓ CHANGELOG.md installed`, colors.green);
327
802
  }
328
803
 
329
804
  return basePath;
@@ -346,44 +821,41 @@ async function main() {
346
821
 
347
822
  banner();
348
823
 
349
- // Determine package directory (where this script is located)
350
824
  const packageDir = path.join(__dirname, '..');
351
825
 
352
826
  let runtimes = [];
353
827
  let scope = null;
354
828
 
355
- // Check if non-interactive mode
356
- const hasRuntimeFlag = flags.claude || flags.opencode || flags.all;
829
+ const hasRuntimeFlag = flags.claude || flags.codex || flags.opencode || flags.all;
357
830
  const hasScopeFlag = flags.global || flags.local;
358
831
  const isInteractive = !hasRuntimeFlag && !hasScopeFlag;
359
832
 
360
833
  if (isInteractive) {
361
834
  const rl = createPrompt();
362
835
 
363
- // Ask for runtime selection (multiple choice)
364
836
  runtimes = await askMultiple(rl, '\nWhich runtime(s) do you want to install ACE for?', [
365
837
  { label: 'Claude Code', value: 'claude', description: "Anthropic's Claude Code CLI" },
838
+ { label: 'Codex', value: 'codex', description: "OpenAI's Codex CLI" },
366
839
  { label: 'Crush', value: 'opencode', description: 'Crush AI coding assistant (formerly OpenCode)' },
367
840
  ]);
368
841
 
369
- // Ask for scope
370
842
  scope = await ask(rl, '\nWhere should ACE be installed?', [
371
- { label: 'Global', value: 'global', description: 'Install in home directory (~/.claude, ~/.opencode)' },
372
- { label: 'Local', value: 'local', description: 'Install in current project (.claude, .opencode)' },
843
+ { label: 'Global', value: 'global', description: 'Install in home directory (~/.claude, ~/.codex, ~/.opencode)' },
844
+ { label: 'Local', value: 'local', description: 'Install in current project (.claude, .codex, .opencode)' },
373
845
  ]);
374
846
 
375
847
  rl.close();
376
848
  } else {
377
- // Non-interactive mode
378
849
  if (flags.all) {
379
- runtimes = ['claude', 'opencode'];
850
+ runtimes = ['claude', 'codex', 'opencode'];
380
851
  } else {
381
852
  if (flags.claude) runtimes.push('claude');
853
+ if (flags.codex) runtimes.push('codex');
382
854
  if (flags.opencode) runtimes.push('opencode');
383
855
  }
384
856
 
385
857
  if (runtimes.length === 0) {
386
- log('Error: No runtime specified. Use --claude, --opencode (Crush), or --all', colors.red);
858
+ log('Error: No runtime specified. Use --claude, --codex, --opencode (Crush), or --all', colors.red);
387
859
  process.exit(1);
388
860
  }
389
861
 
@@ -399,116 +871,54 @@ async function main() {
399
871
  const installedPaths = [];
400
872
 
401
873
  for (const runtime of runtimes) {
402
- const installedPath = installForRuntime(runtime, scope, packageDir);
403
- installedPaths.push({ runtime, name: RUNTIMES[runtime].name, path: installedPath });
874
+ if (runtime === 'claude') {
875
+ const result = installForClaude(scope, packageDir, flags);
876
+ installedPaths.push({ runtime, name: 'Claude Code', ...result });
877
+ } else if (runtime === 'codex') {
878
+ const p = installForCodex(scope, packageDir);
879
+ installedPaths.push({ runtime, name: RUNTIMES[runtime].name, path: p, success: true });
880
+ } else {
881
+ const p = installForCrush(scope, packageDir);
882
+ installedPaths.push({ runtime, name: RUNTIMES[runtime].name, path: p, success: true });
883
+ }
404
884
  }
405
885
 
406
- // Configure hooks and statusline in settings.json (Claude Code only, not Crush)
407
- for (const { runtime, path: basePath } of installedPaths) {
408
- if (runtime !== 'claude') continue;
409
-
410
- const settingsPath = path.join(basePath, 'settings.json');
411
- const settings = readSettings(settingsPath);
412
-
413
- const statuslineCommand = buildHookCommand(basePath, 'ace-statusline.js');
414
- const updateCheckCommand = buildHookCommand(basePath, 'ace-check-update.js');
886
+ const anyFailed = installedPaths.some(p => !p.success);
415
887
 
416
- // Register SessionStart hook for background update checking
417
- if (!settings.hooks) settings.hooks = {};
418
- if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
419
-
420
- const hasAceUpdateHook = settings.hooks.SessionStart.some(entry =>
421
- entry.hooks && entry.hooks.some(h => h.command && h.command.includes('ace-check-update'))
422
- );
423
- if (!hasAceUpdateHook) {
424
- settings.hooks.SessionStart.push({
425
- hooks: [{ type: 'command', command: updateCheckCommand }]
426
- });
427
- log(` ✓ Configured update check hook`, colors.green);
428
- }
888
+ // Show success message
889
+ log(`\n${'═'.repeat(50)}`, anyFailed ? colors.yellow : colors.green);
890
+ log(` ACE installation ${anyFailed ? 'completed with warnings' : 'complete'}!`, (anyFailed ? colors.yellow : colors.green) + colors.bright);
891
+ log(`${'═'.repeat(50)}`, anyFailed ? colors.yellow : colors.green);
429
892
 
430
- // Handle statusline configuration
431
- const hasExisting = settings.statusLine != null;
432
- const isAceStatusline = hasExisting && settings.statusLine.command &&
433
- settings.statusLine.command.includes('ace-statusline');
434
-
435
- if (!hasExisting || flags.forceStatusline) {
436
- // No existing statusline or force flag — install ACE statusline
437
- settings.statusLine = { type: 'command', command: statuslineCommand };
438
- log(` ✓ Configured statusline`, colors.green);
439
- } else if (isAceStatusline) {
440
- // Already ACE statusline — update path
441
- settings.statusLine = { type: 'command', command: statuslineCommand };
442
- log(` ✓ Updated statusline`, colors.green);
443
- } else if (isInteractive) {
444
- // Existing non-ACE statusline in interactive mode — ask user
445
- const existingCmd = settings.statusLine.command || settings.statusLine.url || '(custom)';
446
- log(`\n ⚠ Existing statusline detected`, colors.yellow);
447
- log(` Current: ${existingCmd}`, colors.dim);
448
- log(`\n ACE statusline shows:`, colors.cyan);
449
- log(` • Model name`, colors.dim);
450
- log(` • Current task (from todo list)`, colors.dim);
451
- log(` • Context window usage (color-coded)`, colors.dim);
452
- log(` • Update notifications`, colors.dim);
453
-
454
- const rl = createPrompt();
455
- const choice = await ask(rl, '\n What would you like to do?', [
456
- { label: 'Keep existing statusline', value: 'keep' },
457
- { label: 'Replace with ACE statusline', value: 'replace' },
458
- ]);
459
- rl.close();
460
-
461
- if (choice === 'replace') {
462
- settings.statusLine = { type: 'command', command: statuslineCommand };
463
- log(` ✓ Configured statusline`, colors.green);
464
- } else {
465
- log(` ⚠ Skipping statusline (kept existing)`, colors.yellow);
466
- }
893
+ for (const { name: runtimeName, success } of installedPaths) {
894
+ if (success) {
895
+ log(` ✓ ${runtimeName}: installed`, colors.green);
467
896
  } else {
468
- // Non-interactive with existing statusline skip
469
- log(` ⚠ Skipping statusline (already configured, use --force-statusline to replace)`, colors.yellow);
897
+ log(` ✗ ${runtimeName}: failed (see errors above)`, colors.red);
470
898
  }
471
-
472
- // Write settings
473
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
474
899
  }
475
900
 
476
- // Show success message
477
- log(`\n${'═'.repeat(50)}`, colors.green);
478
- log(` ACE installed successfully!`, colors.green + colors.bright);
479
- log(`${'═'.repeat(50)}`, colors.green);
480
-
481
- log(`\nInstalled locations:`, colors.cyan);
482
- for (const { name: runtimeName, path: p } of installedPaths) {
483
- log(` ${runtimeName}: ${p}`, colors.dim);
484
- }
485
-
486
- log(`\nInstalled structure:`, colors.cyan);
487
- for (const { path: p } of installedPaths) {
488
- log(` ${p}/`, colors.dim);
489
- log(` commands/ace/ Slash commands`, colors.dim);
490
- log(` agents/ Agent definitions`, colors.dim);
491
- log(` hooks/ Statusline & update hooks`, colors.dim);
492
- log(` ${ACE_DIR_NAME}/`, colors.dim);
493
- log(` templates/ Project & artifact templates`, colors.dim);
494
- log(` utils/ Formatting & utility guides`, colors.dim);
495
- log(` workflows/ Workflow definitions`, colors.dim);
496
- }
497
-
498
- log(`\nAvailable commands:`, colors.cyan);
499
- log(` /ace:help Check project status and next steps`, colors.dim);
500
- log(` /ace:plan-project Plan your project with epics and features`, colors.dim);
501
- log(` /ace:plan-epic Plan an epic with features and stories`, colors.dim);
502
- log(` /ace:plan-feature Plan a feature with stories`, colors.dim);
503
- log(` /ace:plan-story Plan a story with tasks`, colors.dim);
504
- log(` /ace:refine-story Refine a story for execution`, colors.dim);
505
- log(` /ace:execute-story Execute a story`, colors.dim);
506
- log(` /ace:verify-story Verify a completed story`, colors.dim);
901
+ log(`\nAvailable skills:`, colors.cyan);
902
+ log(` /ace:help Check project status and next steps`, colors.dim);
903
+ log(` /ace:plan-product-vision Create or update the product vision`, colors.dim);
904
+ log(` /ace:plan-backlog Plan the product backlog`, colors.dim);
905
+ log(` /ace:plan-feature Plan a feature with stories`, colors.dim);
906
+ log(` /ace:plan-story Plan a story specification`, colors.dim);
907
+ log(` /ace:execute-story Execute a planned story`, colors.dim);
908
+ log(` /ace:map-system Map system-wide architecture`, colors.dim);
909
+ log(` /ace:map-subsystem Map a subsystem's internals`, colors.dim);
910
+ log(` /ace:init-coding-standards Generate coding standards`, colors.dim);
911
+ if (runtimes.includes('codex')) {
912
+ log(`\nCodex skill names use $ace-* instead of /ace:*:`, colors.cyan);
913
+ log(` $ace-help Check project status and next steps`, colors.dim);
914
+ log(` $ace-plan-story Plan a story specification`, colors.dim);
915
+ log(` $ace-execute-story Execute a planned story`, colors.dim);
916
+ }
507
917
 
508
918
  log(`\nGet started:`, colors.cyan);
509
- log(` 1. Navigate to your project directory`, colors.dim);
510
- log(` 2. Run /ace:help to initialize ACE`, colors.dim);
511
- log(` 3. Run /ace:plan-project to start planning\n`, colors.dim);
919
+ log(` 1. Restart your AI coding assistant`, colors.dim);
920
+ log(` 2. Run /ace:help in Claude/Crush or $ace-help in Codex`, colors.dim);
921
+ log(` 3. Run /ace:plan-product-vision or $ace-plan-product-vision to define your product\n`, colors.dim);
512
922
  }
513
923
 
514
924
  main().catch((err) => {