agile-context-engineering 0.5.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 (102) hide show
  1. package/.claude-plugin/marketplace.json +18 -0
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +7 -1
  4. package/README.md +16 -12
  5. package/agents/ace-code-discovery-analyst.md +245 -245
  6. package/agents/ace-code-integration-analyst.md +248 -248
  7. package/agents/ace-code-reviewer.md +375 -375
  8. package/agents/ace-product-owner.md +365 -361
  9. package/agents/ace-project-researcher.md +606 -606
  10. package/agents/ace-technical-application-architect.md +315 -315
  11. package/bin/install.js +587 -173
  12. package/hooks/ace-check-update.js +15 -14
  13. package/hooks/ace-statusline.js +30 -12
  14. package/hooks/hooks.json +14 -0
  15. package/package.json +3 -2
  16. package/shared/lib/ace-core.js +53 -0
  17. package/shared/lib/ace-core.test.js +308 -308
  18. package/shared/lib/ace-story.test.js +250 -250
  19. package/skills/execute-story/SKILL.md +116 -110
  20. package/skills/execute-story/script.js +13 -27
  21. package/skills/execute-story/script.test.js +261 -261
  22. package/skills/execute-story/story-template.xml +451 -451
  23. package/skills/execute-story/workflow.xml +3 -1
  24. package/skills/help/SKILL.md +71 -69
  25. package/skills/help/script.js +32 -35
  26. package/skills/help/script.test.js +183 -183
  27. package/skills/help/workflow.xml +14 -3
  28. package/skills/init-coding-standards/SKILL.md +91 -72
  29. package/skills/init-coding-standards/coding-standards-template.xml +531 -531
  30. package/skills/init-coding-standards/script.js +50 -59
  31. package/skills/init-coding-standards/script.test.js +70 -70
  32. package/skills/init-coding-standards/workflow.xml +1 -1
  33. package/skills/map-cross-cutting/SKILL.md +126 -89
  34. package/skills/map-cross-cutting/workflow.xml +1 -1
  35. package/skills/map-guide/SKILL.md +126 -89
  36. package/skills/map-guide/workflow.xml +1 -1
  37. package/skills/map-pattern/SKILL.md +125 -89
  38. package/skills/map-pattern/workflow.xml +1 -1
  39. package/skills/map-story/SKILL.md +180 -127
  40. package/skills/map-story/templates/tech-debt-index.xml +125 -125
  41. package/skills/map-story/workflow.xml +2 -2
  42. package/skills/map-subsystem/SKILL.md +155 -111
  43. package/skills/map-subsystem/script.js +51 -60
  44. package/skills/map-subsystem/script.test.js +68 -68
  45. package/skills/map-subsystem/templates/subsystem-architecture.xml +343 -343
  46. package/skills/map-subsystem/templates/subsystem-structure.xml +234 -234
  47. package/skills/map-subsystem/workflow.xml +1173 -1173
  48. package/skills/map-sys-doc/SKILL.md +125 -90
  49. package/skills/map-sys-doc/workflow.xml +1 -1
  50. package/skills/map-system/SKILL.md +103 -85
  51. package/skills/map-system/script.js +75 -84
  52. package/skills/map-system/script.test.js +73 -73
  53. package/skills/map-system/templates/system-structure.xml +177 -177
  54. package/skills/map-system/templates/testing-framework.xml +283 -283
  55. package/skills/map-system/workflow.xml +667 -667
  56. package/skills/map-walkthrough/SKILL.md +140 -92
  57. package/skills/map-walkthrough/workflow.xml +457 -457
  58. package/skills/plan-backlog/SKILL.md +93 -75
  59. package/skills/plan-backlog/script.js +121 -136
  60. package/skills/plan-backlog/script.test.js +83 -83
  61. package/skills/plan-backlog/workflow.xml +1348 -1348
  62. package/skills/plan-feature/SKILL.md +99 -76
  63. package/skills/plan-feature/feature-template.xml +361 -361
  64. package/skills/plan-feature/script.js +131 -148
  65. package/skills/plan-feature/script.test.js +80 -80
  66. package/skills/plan-feature/workflow.xml +1 -1
  67. package/skills/plan-product-vision/SKILL.md +91 -75
  68. package/skills/plan-product-vision/product-vision-template.xml +227 -227
  69. package/skills/plan-product-vision/script.js +51 -60
  70. package/skills/plan-product-vision/script.test.js +69 -69
  71. package/skills/plan-product-vision/workflow.xml +337 -337
  72. package/skills/plan-story/SKILL.md +125 -102
  73. package/skills/plan-story/script.js +18 -49
  74. package/skills/plan-story/story-template.xml +8 -1
  75. package/skills/plan-story/workflow.xml +17 -1
  76. package/skills/research-external-solution/SKILL.md +120 -107
  77. package/skills/research-external-solution/external-solution-template.xml +832 -832
  78. package/skills/research-external-solution/script.js +229 -238
  79. package/skills/research-external-solution/script.test.js +134 -134
  80. package/skills/research-external-solution/workflow.xml +657 -657
  81. package/skills/research-integration-solution/SKILL.md +121 -98
  82. package/skills/research-integration-solution/integration-solution-template.xml +1015 -1015
  83. package/skills/research-integration-solution/script.js +223 -231
  84. package/skills/research-integration-solution/script.test.js +134 -134
  85. package/skills/research-integration-solution/workflow.xml +711 -711
  86. package/skills/research-story-wiki/SKILL.md +101 -92
  87. package/skills/research-story-wiki/script.js +223 -231
  88. package/skills/research-story-wiki/script.test.js +138 -138
  89. package/skills/research-story-wiki/story-wiki-template.xml +194 -194
  90. package/skills/research-story-wiki/workflow.xml +473 -473
  91. package/skills/research-technical-solution/SKILL.md +131 -103
  92. package/skills/research-technical-solution/script.js +223 -231
  93. package/skills/research-technical-solution/script.test.js +134 -134
  94. package/skills/research-technical-solution/technical-solution-template.xml +1025 -1025
  95. package/skills/research-technical-solution/workflow.xml +761 -761
  96. package/skills/review-story/SKILL.md +99 -100
  97. package/skills/review-story/script.js +8 -16
  98. package/skills/review-story/script.test.js +169 -169
  99. package/skills/review-story/story-template.xml +451 -451
  100. package/skills/review-story/workflow.xml +1 -1
  101. package/skills/update/SKILL.md +65 -53
  102. package/skills/update/workflow.xml +21 -5
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
- // Legacy folder name — only used for cleanup of pre-plugin installations
43
- const ACE_LEGACY_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,6 +192,10 @@ 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);
@@ -190,8 +208,9 @@ const TRANSFORMABLE_EXTENSIONS = new Set(['.md', '.xml', '.js']);
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,106 +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 agentsPath = path.join(basePath, config.agentsDir);
242
+ function toPosixPath(filePath) {
243
+ return filePath.replace(/\\/g, '/');
244
+ }
229
245
 
230
- // Source directories (plugin structure)
231
- const srcSkills = path.join(packageDir, 'skills');
232
- const srcShared = path.join(packageDir, 'shared');
233
- const srcPlugin = path.join(packageDir, '.claude-plugin');
234
- const srcAgents = path.join(packageDir, 'agents');
246
+ function replaceAceInvocations(content, codexStyle) {
247
+ if (!codexStyle) return content;
248
+ return content.replace(/\/ace:([a-z0-9-]+)/g, '$ace-$1');
249
+ }
235
250
 
236
- log(`\nInstalling ACE for ${config.name}...`, colors.cyan);
237
- 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);
238
261
 
239
- // Clean previous ACE installation to remove stale files
240
- const aceCommandsPath = path.join(basePath, config.commandsDir, 'ace');
241
- if (fs.existsSync(aceCommandsPath)) {
242
- fs.rmSync(aceCommandsPath, { recursive: true });
262
+ if (path.basename(skillDir) === skillName && content.includes('---')) {
263
+ transformed = transformed.replace(/^name:\s*.+$/m, `name: ace-${skillName}`);
243
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
+ }
297
+
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
+ }
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');
244
474
  if (fs.existsSync(agentsPath)) {
245
475
  for (const f of fs.readdirSync(agentsPath)) {
246
476
  if (f.startsWith('ace-')) {
247
477
  fs.rmSync(path.join(agentsPath, f), { recursive: true });
478
+ cleaned = true;
479
+ }
480
+ }
481
+ }
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
+ }
248
505
  }
506
+
507
+ if (modified) {
508
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
509
+ }
510
+ } catch {}
511
+ }
512
+
513
+ return cleaned;
514
+ }
515
+
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);
527
+ }
528
+
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
+ }
539
+ }
540
+
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 };
546
+ }
547
+
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 {}
557
+ }
558
+
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 };
249
584
  }
250
585
  }
251
- // Clean old agile-context-engineering directory from pre-plugin installs
252
- const legacyAcePath = path.join(basePath, ACE_LEGACY_DIR_NAME);
253
- if (fs.existsSync(legacyAcePath)) {
254
- fs.rmSync(legacyAcePath, { recursive: true });
586
+
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 });
255
625
  }
256
- // Clean skills/shared directories from previous plugin installs
626
+
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;
647
+ }
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
257
716
  const skillsPath = path.join(basePath, 'skills');
258
717
  const sharedPath = path.join(basePath, 'shared');
259
718
  const pluginPath = path.join(basePath, '.claude-plugin');
260
- if (fs.existsSync(skillsPath)) {
261
- fs.rmSync(skillsPath, { recursive: true });
262
- }
263
- if (fs.existsSync(sharedPath)) {
264
- fs.rmSync(sharedPath, { recursive: true });
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 });
265
723
  }
266
- if (fs.existsSync(pluginPath)) {
267
- fs.rmSync(pluginPath, { recursive: true });
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 });
727
+ }
268
728
  }
269
729
 
270
- // Create directories
271
730
  fs.mkdirSync(agentsPath, { recursive: true });
272
731
 
273
- // Copy skills (plugin skill directories)
274
732
  if (fs.existsSync(srcSkills)) {
275
- copyDir(srcSkills, skillsPath, runtime);
733
+ copyDir(srcSkills, skillsPath, 'opencode');
276
734
  log(` ✓ Skills installed`, colors.green);
277
735
  }
278
-
279
- // Copy shared libs (ace-core.js, ace-story.js, ace-github.js, utils)
280
736
  if (fs.existsSync(srcShared)) {
281
- copyDir(srcShared, sharedPath, runtime);
737
+ copyDir(srcShared, sharedPath, 'opencode');
282
738
  log(` ✓ Shared libs installed`, colors.green);
283
739
  }
284
-
285
- // Copy plugin manifest
286
740
  if (fs.existsSync(srcPlugin)) {
287
- copyDir(srcPlugin, pluginPath, runtime);
741
+ copyDir(srcPlugin, pluginPath, 'opencode');
288
742
  log(` ✓ Plugin manifest installed`, colors.green);
289
743
  }
290
-
291
- // Copy agents (transform paths for target runtime)
292
744
  if (fs.existsSync(srcAgents)) {
293
- copyDir(srcAgents, agentsPath, runtime);
745
+ copyDir(srcAgents, agentsPath, 'opencode');
294
746
  log(` ✓ Agents installed`, colors.green);
295
747
  }
296
748
 
297
- // Copy hooks
298
- const srcHooks = path.join(packageDir, 'hooks');
299
- const hooksPath = path.join(basePath, 'hooks');
300
- if (fs.existsSync(srcHooks)) {
301
- // Only copy ace-* hook files, preserve non-ACE hooks (e.g. GSD)
302
- if (!fs.existsSync(hooksPath)) {
303
- fs.mkdirSync(hooksPath, { recursive: true });
304
- }
305
- for (const f of fs.readdirSync(srcHooks)) {
306
- if (f.startsWith('ace-')) {
307
- fs.copyFileSync(path.join(srcHooks, f), path.join(hooksPath, f));
308
- }
309
- }
310
- log(` ✓ Hooks installed`, colors.green);
749
+ // Write VERSION file
750
+ const versionFile = path.join(sharedPath, 'VERSION');
751
+ if (!fs.existsSync(sharedPath)) fs.mkdirSync(sharedPath, { recursive: true });
752
+ fs.writeFileSync(versionFile, VERSION, 'utf-8');
753
+
754
+ // Copy CHANGELOG.md
755
+ const changelogSrc = path.join(packageDir, '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);
311
789
  }
312
790
 
313
- // Write VERSION file for update checking (in shared/ alongside libs)
791
+ const agentCount = installCodexAgents(basePath, srcAgents);
792
+ log(` ✓ ${agentCount} agents configured`, colors.green);
793
+
314
794
  const versionFile = path.join(sharedPath, 'VERSION');
315
795
  if (!fs.existsSync(sharedPath)) fs.mkdirSync(sharedPath, { recursive: true });
316
796
  fs.writeFileSync(versionFile, VERSION, 'utf-8');
317
797
 
318
- // Copy CHANGELOG.md
319
798
  const changelogSrc = path.join(packageDir, 'CHANGELOG.md');
320
799
  const changelogDest = path.join(sharedPath, 'CHANGELOG.md');
321
800
  if (fs.existsSync(changelogSrc)) {
322
801
  fs.copyFileSync(changelogSrc, changelogDest);
323
- log(` ✓ CHANGELOG.md installed`, colors.green);
324
802
  }
325
803
 
326
804
  return basePath;
@@ -343,44 +821,41 @@ async function main() {
343
821
 
344
822
  banner();
345
823
 
346
- // Determine package directory (where this script is located)
347
824
  const packageDir = path.join(__dirname, '..');
348
825
 
349
826
  let runtimes = [];
350
827
  let scope = null;
351
828
 
352
- // Check if non-interactive mode
353
- const hasRuntimeFlag = flags.claude || flags.opencode || flags.all;
829
+ const hasRuntimeFlag = flags.claude || flags.codex || flags.opencode || flags.all;
354
830
  const hasScopeFlag = flags.global || flags.local;
355
831
  const isInteractive = !hasRuntimeFlag && !hasScopeFlag;
356
832
 
357
833
  if (isInteractive) {
358
834
  const rl = createPrompt();
359
835
 
360
- // Ask for runtime selection (multiple choice)
361
836
  runtimes = await askMultiple(rl, '\nWhich runtime(s) do you want to install ACE for?', [
362
837
  { label: 'Claude Code', value: 'claude', description: "Anthropic's Claude Code CLI" },
838
+ { label: 'Codex', value: 'codex', description: "OpenAI's Codex CLI" },
363
839
  { label: 'Crush', value: 'opencode', description: 'Crush AI coding assistant (formerly OpenCode)' },
364
840
  ]);
365
841
 
366
- // Ask for scope
367
842
  scope = await ask(rl, '\nWhere should ACE be installed?', [
368
- { label: 'Global', value: 'global', description: 'Install in home directory (~/.claude, ~/.opencode)' },
369
- { 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)' },
370
845
  ]);
371
846
 
372
847
  rl.close();
373
848
  } else {
374
- // Non-interactive mode
375
849
  if (flags.all) {
376
- runtimes = ['claude', 'opencode'];
850
+ runtimes = ['claude', 'codex', 'opencode'];
377
851
  } else {
378
852
  if (flags.claude) runtimes.push('claude');
853
+ if (flags.codex) runtimes.push('codex');
379
854
  if (flags.opencode) runtimes.push('opencode');
380
855
  }
381
856
 
382
857
  if (runtimes.length === 0) {
383
- 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);
384
859
  process.exit(1);
385
860
  }
386
861
 
@@ -396,98 +871,31 @@ async function main() {
396
871
  const installedPaths = [];
397
872
 
398
873
  for (const runtime of runtimes) {
399
- const installedPath = installForRuntime(runtime, scope, packageDir);
400
- installedPaths.push({ runtime, name: RUNTIMES[runtime].name, path: installedPath });
401
- }
402
-
403
- // Configure hooks and statusline in settings.json (Claude Code only, not Crush)
404
- for (const { runtime, path: basePath } of installedPaths) {
405
- if (runtime !== 'claude') continue;
406
-
407
- const settingsPath = path.join(basePath, 'settings.json');
408
- const settings = readSettings(settingsPath);
409
-
410
- const statuslineCommand = buildHookCommand(basePath, 'ace-statusline.js');
411
- const updateCheckCommand = buildHookCommand(basePath, 'ace-check-update.js');
412
-
413
- // Register SessionStart hook for background update checking
414
- if (!settings.hooks) settings.hooks = {};
415
- if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
416
-
417
- const hasAceUpdateHook = settings.hooks.SessionStart.some(entry =>
418
- entry.hooks && entry.hooks.some(h => h.command && h.command.includes('ace-check-update'))
419
- );
420
- if (!hasAceUpdateHook) {
421
- settings.hooks.SessionStart.push({
422
- hooks: [{ type: 'command', command: updateCheckCommand }]
423
- });
424
- log(` ✓ Configured update check hook`, colors.green);
425
- }
426
-
427
- // Handle statusline configuration
428
- const hasExisting = settings.statusLine != null;
429
- const isAceStatusline = hasExisting && settings.statusLine.command &&
430
- settings.statusLine.command.includes('ace-statusline');
431
-
432
- if (!hasExisting || flags.forceStatusline) {
433
- // No existing statusline or force flag — install ACE statusline
434
- settings.statusLine = { type: 'command', command: statuslineCommand };
435
- log(` ✓ Configured statusline`, colors.green);
436
- } else if (isAceStatusline) {
437
- // Already ACE statusline — update path
438
- settings.statusLine = { type: 'command', command: statuslineCommand };
439
- log(` ✓ Updated statusline`, colors.green);
440
- } else if (isInteractive) {
441
- // Existing non-ACE statusline in interactive mode — ask user
442
- const existingCmd = settings.statusLine.command || settings.statusLine.url || '(custom)';
443
- log(`\n ⚠ Existing statusline detected`, colors.yellow);
444
- log(` Current: ${existingCmd}`, colors.dim);
445
- log(`\n ACE statusline shows:`, colors.cyan);
446
- log(` • Model name`, colors.dim);
447
- log(` • Current task (from todo list)`, colors.dim);
448
- log(` • Context window usage (color-coded)`, colors.dim);
449
- log(` • Update notifications`, colors.dim);
450
-
451
- const rl = createPrompt();
452
- const choice = await ask(rl, '\n What would you like to do?', [
453
- { label: 'Keep existing statusline', value: 'keep' },
454
- { label: 'Replace with ACE statusline', value: 'replace' },
455
- ]);
456
- rl.close();
457
-
458
- if (choice === 'replace') {
459
- settings.statusLine = { type: 'command', command: statuslineCommand };
460
- log(` ✓ Configured statusline`, colors.green);
461
- } else {
462
- log(` ⚠ Skipping statusline (kept existing)`, colors.yellow);
463
- }
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 });
464
880
  } else {
465
- // Non-interactive with existing statusline — skip
466
- log(` ⚠ Skipping statusline (already configured, use --force-statusline to replace)`, colors.yellow);
881
+ const p = installForCrush(scope, packageDir);
882
+ installedPaths.push({ runtime, name: RUNTIMES[runtime].name, path: p, success: true });
467
883
  }
468
-
469
- // Write settings
470
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
471
884
  }
472
885
 
473
- // Show success message
474
- log(`\n${'═'.repeat(50)}`, colors.green);
475
- log(` ACE installed successfully!`, colors.green + colors.bright);
476
- log(`${'═'.repeat(50)}`, colors.green);
886
+ const anyFailed = installedPaths.some(p => !p.success);
477
887
 
478
- log(`\nInstalled locations:`, colors.cyan);
479
- for (const { name: runtimeName, path: p } of installedPaths) {
480
- log(` ${runtimeName}: ${p}`, colors.dim);
481
- }
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);
482
892
 
483
- log(`\nInstalled structure:`, colors.cyan);
484
- for (const { path: p } of installedPaths) {
485
- log(` ${p}/`, colors.dim);
486
- log(` skills/ Skill directories (SKILL.md + workflow + templates + script.js)`, colors.dim);
487
- log(` shared/ Shared libraries (ace-core.js, ace-story.js, ace-github.js)`, colors.dim);
488
- log(` agents/ Agent definitions`, colors.dim);
489
- log(` hooks/ Statusline & update hooks`, colors.dim);
490
- log(` .claude-plugin/ Plugin manifest`, colors.dim);
893
+ for (const { name: runtimeName, success } of installedPaths) {
894
+ if (success) {
895
+ log(` ${runtimeName}: installed`, colors.green);
896
+ } else {
897
+ log(` ${runtimeName}: failed (see errors above)`, colors.red);
898
+ }
491
899
  }
492
900
 
493
901
  log(`\nAvailable skills:`, colors.cyan);
@@ -500,11 +908,17 @@ async function main() {
500
908
  log(` /ace:map-system Map system-wide architecture`, colors.dim);
501
909
  log(` /ace:map-subsystem Map a subsystem's internals`, colors.dim);
502
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
+ }
503
917
 
504
918
  log(`\nGet started:`, colors.cyan);
505
- log(` 1. Navigate to your project directory`, colors.dim);
506
- log(` 2. Run /ace:help to initialize ACE`, colors.dim);
507
- log(` 3. Run /ace:plan-product-vision to define your product\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);
508
922
  }
509
923
 
510
924
  main().catch((err) => {