projecta-rrr 1.23.3 → 1.23.5

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,23 @@ All notable changes to RRR will be documented in this file.
4
4
 
5
5
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
+ ## [1.23.5] - 2026-04-30
8
+
9
+ ### Fixed
10
+ - **Codex command discovery** — RRR now installs Codex skills in the discoverable `~/.codex/skills/rrr-*/SKILL.md` layout, removes legacy flat skill files, and quotes generated YAML metadata so every `$rrr-*` command loads cleanly.
11
+ - **Codex runtime provisioning** — installer now refreshes `~/.codex/rrr` with the support scripts used by RRR commands and skips copied `node_modules` directories so updates do not break on symlinked dependencies.
12
+ - **Codex agent registration** — RRR agent TOML files are registered in `~/.codex/config.toml` under managed `[agents.rrr-*]` blocks while preserving user-managed config.
13
+
14
+ ### Changed
15
+ - **Seamless repo updates for Codex developers** — running `$rrr-update` or `npx projecta-rrr@latest --global` from an RRR repo now refreshes repo-local `AGENTS.md` automatically when Codex is installed.
16
+ - **Codex command adapter** — transformed skills now document `$rrr-*` invocation, `{{RRR_ARGS}}` handling, default-mode question fallback, slash-command conversion, and RRR subagent mapping for Codex.
17
+
18
+ ## [1.23.4] - 2026-04-20
19
+
20
+ ### Fixed
21
+ - **Codex hosted MCP launcher** — installer now writes a Codex-specific bearer env file and registers `bearer_token_env_var` in `~/.codex/config.toml` so the hosted RRR setup works with the new `scripts/codex-rrr.sh` launcher.
22
+ - **Codex launch helper** — added `npm run codex:rrr` and docs for starting Codex with the hosted bearer loaded from `~/.config/projecta-rrr/rrr-hosted.env`.
23
+
7
24
  ## [1.23.3] - 2026-04-20
8
25
 
9
26
  ### Fixed
@@ -31,6 +31,8 @@ const HOSTED_URL = 'https://rrr-search-hosted.fly.dev/mcp';
31
31
  const GH_APP_INSTALL_URL = 'https://github.com/apps/rrr-search/installations/new';
32
32
  const MCP_NAME = 'rrr-search-hosted';
33
33
  const CODEX_CONFIG_PATH = path.join(os.homedir(), '.codex', 'config.toml');
34
+ const CODEX_BEARER_ENV_VAR = 'RRR_HOSTED_BEARER';
35
+ const CODEX_ENV_FILE = path.join(os.homedir(), '.config', 'projecta-rrr', 'rrr-hosted.env');
34
36
 
35
37
  // ──────────────────────────────── UI ──────────────────────────────────────
36
38
 
@@ -136,7 +138,7 @@ function registerCodexMcp (bearer) {
136
138
  content = content.replace(sectionRegex, '').trimEnd();
137
139
 
138
140
  // Append new registration
139
- const section = `\n\n[mcp_servers.${MCP_NAME}]\nurl = "${HOSTED_URL}"\nheaders = { Authorization = "Bearer ${bearer}" }\n`;
141
+ const section = `\n\n[mcp_servers.${MCP_NAME}]\nurl = "${HOSTED_URL}"\nbearer_token_env_var = "${CODEX_BEARER_ENV_VAR}"\n`;
140
142
  content = content + section;
141
143
 
142
144
  // Ensure ~/.codex directory exists
@@ -144,6 +146,15 @@ function registerCodexMcp (bearer) {
144
146
  fs.writeFileSync(CODEX_CONFIG_PATH, content, 'utf8');
145
147
  }
146
148
 
149
+ function writeCodexBearerEnv (bearer) {
150
+ fs.mkdirSync(path.dirname(CODEX_ENV_FILE), { recursive: true });
151
+ fs.writeFileSync(
152
+ CODEX_ENV_FILE,
153
+ `export ${CODEX_BEARER_ENV_VAR}=${JSON.stringify(bearer)}\n`,
154
+ 'utf8'
155
+ );
156
+ }
157
+
147
158
  // ──────────────────────────────── flow ────────────────────────────────────
148
159
 
149
160
  async function main () {
@@ -177,7 +188,7 @@ async function main () {
177
188
  step(2, TOTAL_STEPS, 'Team bearer token');
178
189
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
179
190
 
180
- let bearer = args.bearer || process.env.RRR_HOSTED_MCP_TOKEN || null;
191
+ let bearer = args.bearer || process.env.RRR_HOSTED_BEARER || process.env.RRR_HOSTED_MCP_TOKEN || null;
181
192
  if (!bearer && !args.yes) {
182
193
  info('You need a bearer token of the form: rrr_<prefix>_<32chars>');
183
194
  info('New team? Ask your rrr-search admin to run:');
@@ -192,6 +203,9 @@ async function main () {
192
203
  err('Bearer format invalid. Expected rrr_<prefix>_<32chars>.');
193
204
  process.exit(1);
194
205
  }
206
+ if (!args.bearer && process.env.RRR_HOSTED_MCP_TOKEN && !process.env.RRR_HOSTED_BEARER) {
207
+ warn('RRR_HOSTED_MCP_TOKEN is deprecated for this launcher; prefer RRR_HOSTED_BEARER.');
208
+ }
195
209
  ok(`Bearer captured (team prefix: ${bearer.split('_')[1]})`);
196
210
  } else {
197
211
  warn('No bearer — skipping MCP registration. You can run this wizard again after getting one.');
@@ -294,8 +308,11 @@ async function main () {
294
308
  }
295
309
  try {
296
310
  registerCodexMcp(bearer);
311
+ writeCodexBearerEnv(bearer);
297
312
  ok(`Registered ${MCP_NAME} in ~/.codex/config.toml`);
298
- info('Same bearer token works for both Claude Code and Codex (MCP-02)');
313
+ info(`Codex will read the bearer from ${CODEX_BEARER_ENV_VAR}`);
314
+ info(`Codex launcher env file written to ${CODEX_ENV_FILE}`);
315
+ info(`Run scripts/codex-rrr.sh to launch Codex with RRR ready`);
299
316
  } catch (e) {
300
317
  warn(`Could not write ~/.codex/config.toml: ${e.message}`);
301
318
  info('You can manually add the MCP server entry — see docs/hosted-search-setup.md');
package/bin/install.js CHANGED
@@ -900,6 +900,10 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix) {
900
900
  const entries = fs.readdirSync(srcDir, { withFileTypes: true });
901
901
 
902
902
  for (const entry of entries) {
903
+ if (entry.name === 'node_modules') {
904
+ continue;
905
+ }
906
+
903
907
  const srcPath = path.join(srcDir, entry.name);
904
908
  const destPath = path.join(destDir, entry.name);
905
909
 
@@ -916,6 +920,181 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix) {
916
920
  }
917
921
  }
918
922
 
923
+ function getCodexHome() {
924
+ return expandTilde(process.env.CODEX_HOME) || path.join(os.homedir(), '.codex');
925
+ }
926
+
927
+ function getCodexPathPrefix(codexHome) {
928
+ const defaultCodexHome = path.join(os.homedir(), '.codex');
929
+ const normalizedHome = path.resolve(codexHome);
930
+ const normalizedDefault = path.resolve(defaultCodexHome);
931
+ if (normalizedHome === normalizedDefault) {
932
+ return '~/.codex/';
933
+ }
934
+ return `${codexHome.replace(/\\/g, '/')}/`;
935
+ }
936
+
937
+ function getCodexEnvPathPrefix(codexHome) {
938
+ const defaultCodexHome = path.join(os.homedir(), '.codex');
939
+ const normalizedHome = path.resolve(codexHome);
940
+ const normalizedDefault = path.resolve(defaultCodexHome);
941
+ if (normalizedHome === normalizedDefault) {
942
+ return '$HOME/.codex/';
943
+ }
944
+ return `${codexHome.replace(/\\/g, '/')}/`;
945
+ }
946
+
947
+ function copyFileIfExists(srcFile, destFile, installed) {
948
+ if (!fs.existsSync(srcFile)) return;
949
+ fs.mkdirSync(path.dirname(destFile), { recursive: true });
950
+ fs.copyFileSync(srcFile, destFile);
951
+ installed.push(destFile);
952
+ }
953
+
954
+ function copyDirIfExists(srcDir, destDir, pathPrefix, installed) {
955
+ if (!fs.existsSync(srcDir)) return;
956
+ copyWithPathReplacement(srcDir, destDir, pathPrefix);
957
+ installed.push(destDir);
958
+ }
959
+
960
+ function installCodexRuntimeAssets({ src, codexHome, dryRun = false }) {
961
+ const installed = [];
962
+ const skipped = [];
963
+ const codexPathPrefix = getCodexPathPrefix(codexHome);
964
+
965
+ if (dryRun) {
966
+ return { installed, skipped: ['dry-run'] };
967
+ }
968
+
969
+ const rrrSrc = path.join(src, 'rrr');
970
+ const rrrDest = path.join(codexHome, 'rrr');
971
+ copyDirIfExists(rrrSrc, rrrDest, codexPathPrefix, installed);
972
+
973
+ const scriptsDestDir = path.join(codexHome, 'rrr', 'scripts');
974
+ copyFileIfExists(
975
+ path.join(src, 'scripts', 'refresh-scope-cache.js'),
976
+ path.join(scriptsDestDir, 'refresh-scope-cache.js'),
977
+ installed
978
+ );
979
+ copyFileIfExists(
980
+ path.join(src, 'scripts', 'doctor-rrr.js'),
981
+ path.join(scriptsDestDir, 'doctor-rrr.js'),
982
+ installed
983
+ );
984
+ copyFileIfExists(
985
+ path.join(src, 'scripts', 'cleanup-phases.js'),
986
+ path.join(scriptsDestDir, 'cleanup-phases.js'),
987
+ installed
988
+ );
989
+ copyFileIfExists(
990
+ path.join(src, 'scripts', 'test-install-smoke.js'),
991
+ path.join(scriptsDestDir, 'test-install-smoke.js'),
992
+ installed
993
+ );
994
+ copyFileIfExists(
995
+ path.join(src, 'scripts', 'handoff-preflight.js'),
996
+ path.join(scriptsDestDir, 'handoff-preflight.js'),
997
+ installed
998
+ );
999
+ copyDirIfExists(
1000
+ path.join(src, 'scripts', 'rrr-memory'),
1001
+ path.join(scriptsDestDir, 'rrr-memory'),
1002
+ codexPathPrefix,
1003
+ installed
1004
+ );
1005
+ copyFileIfExists(
1006
+ path.join(src, 'scripts', 'build-project-context.js'),
1007
+ path.join(scriptsDestDir, 'build-project-context.js'),
1008
+ installed
1009
+ );
1010
+ copyFileIfExists(
1011
+ path.join(src, 'scripts', 'bootstrap-external-skills.js'),
1012
+ path.join(scriptsDestDir, 'bootstrap-external-skills.js'),
1013
+ installed
1014
+ );
1015
+ copyFileIfExists(
1016
+ path.join(src, 'scripts', 'gen-agents-md.js'),
1017
+ path.join(scriptsDestDir, 'gen-agents-md.js'),
1018
+ installed
1019
+ );
1020
+ copyFileIfExists(
1021
+ path.join(src, 'scripts', 'jarvis.sh'),
1022
+ path.join(scriptsDestDir, 'jarvis.sh'),
1023
+ installed
1024
+ );
1025
+ copyFileIfExists(
1026
+ path.join(src, 'scripts', 'verify-milestone.js'),
1027
+ path.join(scriptsDestDir, 'verify-milestone.js'),
1028
+ installed
1029
+ );
1030
+ copyFileIfExists(
1031
+ path.join(src, 'rrr', 'lib', 'inline-status.js'),
1032
+ path.join(codexHome, 'rrr', 'lib', 'inline-status.js'),
1033
+ installed
1034
+ );
1035
+ copyFileIfExists(
1036
+ path.join(src, 'CHANGELOG.md'),
1037
+ path.join(codexHome, 'rrr', 'CHANGELOG.md'),
1038
+ installed
1039
+ );
1040
+
1041
+ fs.mkdirSync(path.join(codexHome, 'rrr'), { recursive: true });
1042
+ fs.writeFileSync(path.join(codexHome, 'rrr', 'VERSION'), pkg.version);
1043
+ installed.push(path.join(codexHome, 'rrr', 'VERSION'));
1044
+
1045
+ if (fs.existsSync(path.join(rrrDest, 'package.json'))) {
1046
+ try {
1047
+ execSync('npm install --omit=dev --silent 2>/dev/null || npm install --omit=dev', {
1048
+ cwd: rrrDest,
1049
+ stdio: 'pipe',
1050
+ timeout: 120000
1051
+ });
1052
+ installed.push(path.join(rrrDest, 'node_modules'));
1053
+ } catch (e) {
1054
+ skipped.push(`runtime deps failed: ${e.message}`);
1055
+ }
1056
+ }
1057
+
1058
+ const mcpServerDir = path.join(rrrDest, 'mcp-server');
1059
+ if (fs.existsSync(path.join(mcpServerDir, 'package.json'))) {
1060
+ try {
1061
+ execSync('npm install --omit=dev --silent 2>/dev/null || npm install --omit=dev', {
1062
+ cwd: mcpServerDir,
1063
+ stdio: 'pipe',
1064
+ timeout: 60000
1065
+ });
1066
+ installed.push(path.join(mcpServerDir, 'node_modules'));
1067
+ } catch (e) {
1068
+ skipped.push(`MCP deps failed: ${e.message}`);
1069
+ }
1070
+ }
1071
+
1072
+ return { installed, skipped };
1073
+ }
1074
+
1075
+ function refreshProjectAgentsMdForCodex({ src, projectDir, dryRun = false }) {
1076
+ const planningStatePath = path.join(projectDir, '.planning', 'STATE.md');
1077
+ if (!fs.existsSync(planningStatePath)) {
1078
+ return { refreshed: false, reason: 'no-planning-state' };
1079
+ }
1080
+
1081
+ const generatorPath = path.join(src, 'scripts', 'gen-agents-md.js');
1082
+ const commandsDir = path.join(src, 'commands', 'rrr');
1083
+ const outputPath = path.join(projectDir, 'AGENTS.md');
1084
+
1085
+ if (!fs.existsSync(generatorPath) || !fs.existsSync(commandsDir)) {
1086
+ return { refreshed: false, reason: 'generator-missing' };
1087
+ }
1088
+
1089
+ if (dryRun) {
1090
+ return { refreshed: false, reason: 'dry-run' };
1091
+ }
1092
+
1093
+ const { generateAgentsMd } = require(generatorPath);
1094
+ const result = generateAgentsMd({ commandsDir, outputPath });
1095
+ return { refreshed: true, outputPath, skillCount: result.skillCount };
1096
+ }
1097
+
919
1098
  /**
920
1099
  * Install scripts to target project (for Pushpa Mode and MCP setup)
921
1100
  */
@@ -1563,6 +1742,22 @@ function install(isGlobal) {
1563
1742
  console.log(` ${green}✓${reset} Installed rrr/scripts/build-project-context.js`);
1564
1743
  }
1565
1744
 
1745
+ const additionalRuntimeScripts = [
1746
+ 'bootstrap-external-skills.js',
1747
+ 'gen-agents-md.js',
1748
+ 'jarvis.sh',
1749
+ 'verify-milestone.js',
1750
+ ];
1751
+ for (const scriptName of additionalRuntimeScripts) {
1752
+ const scriptSrc = path.join(src, 'scripts', scriptName);
1753
+ if (fs.existsSync(scriptSrc)) {
1754
+ const scriptsDestDir = path.join(claudeDir, 'rrr', 'scripts');
1755
+ fs.mkdirSync(scriptsDestDir, { recursive: true });
1756
+ fs.copyFileSync(scriptSrc, path.join(scriptsDestDir, scriptName));
1757
+ console.log(` ${green}✓${reset} Installed rrr/scripts/${scriptName}`);
1758
+ }
1759
+ }
1760
+
1566
1761
  // Copy inline-status.js for post-execution status display
1567
1762
  const inlineStatusSrc = path.join(src, 'rrr', 'lib', 'inline-status.js');
1568
1763
  if (fs.existsSync(inlineStatusSrc)) {
@@ -2184,18 +2379,42 @@ function install(isGlobal) {
2184
2379
  // Idempotent: installCodexSkills() overwrites existing files
2185
2380
  const codexStatus = detectCodexCLI();
2186
2381
  if (codexStatus.available) {
2382
+ const codexHome = getCodexHome();
2383
+ const codexHomeLabel = codexHome.replace(os.homedir(), '~');
2384
+
2385
+ try {
2386
+ const runtimeResult = installCodexRuntimeAssets({
2387
+ src,
2388
+ codexHome,
2389
+ dryRun: hasDryRun
2390
+ });
2391
+ const dryRunText = hasDryRun ? ' (dry-run)' : '';
2392
+ console.log(` ${green}✓${reset} Installed Codex runtime assets to ${codexHomeLabel}/rrr/${dryRunText}`);
2393
+ if (runtimeResult.skipped.length > 0 && !hasDryRun) {
2394
+ console.log(` ${yellow}⚠${reset} Codex runtime skipped: ${runtimeResult.skipped.join(', ')}`);
2395
+ }
2396
+ } catch (e) {
2397
+ // Silent fail — Codex runtime install is optional, must not break main Claude install
2398
+ console.log(` ${dim}Codex runtime assets skipped: ${e.message}${reset}`);
2399
+ }
2400
+
2187
2401
  try {
2188
2402
  const { installCodexSkills } = require('../rrr/lib/codex-skill-transform');
2189
- const codexSkillsDir = path.join(os.homedir(), '.codex', 'skills');
2403
+ const codexSkillsDir = path.join(codexHome, 'skills');
2190
2404
  const rrrCommandsDir = path.join(src, 'commands', 'rrr');
2191
2405
  const result = installCodexSkills({
2192
2406
  sourceDir: rrrCommandsDir,
2193
2407
  targetDir: codexSkillsDir,
2194
2408
  dryRun: hasDryRun,
2195
- verbose: false
2409
+ verbose: false,
2410
+ codexPathPrefix: getCodexPathPrefix(codexHome),
2411
+ codexEnvPathPrefix: getCodexEnvPathPrefix(codexHome)
2196
2412
  });
2197
2413
  if (result.errors.length === 0) {
2198
- console.log(` ${green}✓${reset} Installed ${result.written.length} Codex skills to ~/.codex/skills/`);
2414
+ const removedText = result.removed && result.removed.length > 0
2415
+ ? `, removed ${result.removed.length} legacy flat files`
2416
+ : '';
2417
+ console.log(` ${green}✓${reset} Installed ${result.written.length} Codex skills to ${codexHomeLabel}/skills/<skill>/SKILL.md${removedText}`);
2199
2418
  } else {
2200
2419
  console.log(` ${yellow}⚠${reset} Installed ${result.written.length} Codex skills (${result.errors.length} errors)`);
2201
2420
  result.errors.slice(0, 3).forEach(e => {
@@ -2210,18 +2429,21 @@ function install(isGlobal) {
2210
2429
  // Install Codex agent .toml configs (AGENT-01, AGENT-02, AGENT-03)
2211
2430
  try {
2212
2431
  const { installCodexAgents } = require('../rrr/lib/codex-agent-gen');
2213
- const codexAgentsDir = path.join(os.homedir(), '.codex', 'agents');
2432
+ const codexAgentsDir = path.join(codexHome, 'agents');
2433
+ const codexConfigPath = path.join(codexHome, 'config.toml');
2214
2434
  const rrrAgentsDir = path.join(src, 'agents');
2215
2435
  const agentResult = installCodexAgents({
2216
2436
  sourceDir: rrrAgentsDir,
2217
2437
  targetDir: codexAgentsDir,
2438
+ configPath: codexConfigPath,
2218
2439
  dryRun: hasDryRun,
2219
2440
  verbose: false
2220
2441
  });
2221
2442
  if (agentResult.errors.length === 0) {
2222
2443
  const removedCount = agentResult.removed.length;
2223
2444
  const removedText = removedCount > 0 ? `, pruned ${removedCount} stale configs` : '';
2224
- console.log(` ${green}✓${reset} Installed ${agentResult.written.length} Codex agent configs to ~/.codex/agents/${removedText}`);
2445
+ const registeredText = agentResult.registered > 0 ? `, registered ${agentResult.registered} in config.toml` : '';
2446
+ console.log(` ${green}✓${reset} Installed ${agentResult.written.length} Codex agent configs to ${codexHomeLabel}/agents/${removedText}${registeredText}`);
2225
2447
  } else {
2226
2448
  console.log(` ${yellow}⚠${reset} Installed ${agentResult.written.length} agent configs (${agentResult.errors.length} errors)`);
2227
2449
  agentResult.errors.slice(0, 3).forEach(e => {
@@ -2232,6 +2454,21 @@ function install(isGlobal) {
2232
2454
  // Silent fail — agent install is optional, must not break main Claude install
2233
2455
  console.log(` ${dim}Codex agents skipped: ${e.message}${reset}`);
2234
2456
  }
2457
+
2458
+ try {
2459
+ const agentsMdResult = refreshProjectAgentsMdForCodex({
2460
+ src,
2461
+ projectDir: process.cwd(),
2462
+ dryRun: hasDryRun
2463
+ });
2464
+ if (agentsMdResult.refreshed) {
2465
+ const agentsPathLabel = agentsMdResult.outputPath.replace(process.cwd(), '.');
2466
+ console.log(` ${green}✓${reset} Refreshed ${agentsPathLabel} for Codex (${agentsMdResult.skillCount} skills)`);
2467
+ }
2468
+ } catch (e) {
2469
+ // Silent fail — AGENTS.md is helpful for Codex, but should not block install/update
2470
+ console.log(` ${dim}Codex AGENTS.md refresh skipped: ${e.message}${reset}`);
2471
+ }
2235
2472
  }
2236
2473
  // (no else — silently skip when codex is not in PATH)
2237
2474
 
@@ -162,14 +162,14 @@ The `npx projecta-rrr@latest --global` step above automatically detects Codex CL
162
162
 
163
163
  If Codex CLI is installed, you will see:
164
164
  ```
165
- ✓ Installed N Codex skills to ~/.codex/skills/
165
+ ✓ Installed N Codex skills to ~/.codex/skills/<skill>/SKILL.md
166
166
  ```
167
167
 
168
168
  If Codex CLI is not installed, this step is silently skipped.
169
169
 
170
170
  **To verify Codex skills were refreshed:**
171
171
  ```bash
172
- ls ~/.codex/skills/rrr-*.md 2>/dev/null | wc -l
172
+ find ~/.codex/skills -path '*/rrr-*/SKILL.md' 2>/dev/null | wc -l
173
173
  ```
174
174
  Should show 47 (or current command count) skill files.
175
175
  </step>
@@ -177,13 +177,12 @@ Should show 47 (or current command count) skill files.
177
177
  <step name="refresh_agents_md">
178
178
  **AGENTS.md refresh (automatic)**
179
179
 
180
- After updating, regenerate `AGENTS.md` in the current project root so Codex has
181
- the latest trigger phrase catalogue. This uses the generator script shipped with
182
- the updated RRR package.
180
+ The `npx projecta-rrr@latest --global` step above automatically regenerates
181
+ `AGENTS.md` in the current project root when:
182
+ - Codex CLI is installed and on PATH
183
+ - the current repo has `.planning/STATE.md`
183
184
 
184
- ```bash
185
- node "$HOME/.claude/rrr/scripts/gen-agents-md.js" 2>/dev/null || echo "AGENTS.md refresh skipped (run: node ~/.claude/rrr/scripts/gen-agents-md.js manually)"
186
- ```
185
+ No manual action is needed for normal repo updates.
187
186
 
188
187
  **If AGENTS.md was updated:**
189
188
  ```
@@ -193,6 +192,11 @@ node "$HOME/.claude/rrr/scripts/gen-agents-md.js" 2>/dev/null || echo "AGENTS.md
193
192
  **If not in an RRR project or script not found:**
194
193
  Silently skip — AGENTS.md is only relevant when Codex is being used in the project.
195
194
 
195
+ **Manual fallback if needed:**
196
+ ```bash
197
+ node "$HOME/.claude/rrr/scripts/gen-agents-md.js" 2>/dev/null || node "$HOME/.codex/rrr/scripts/gen-agents-md.js"
198
+ ```
199
+
196
200
  **To verify:**
197
201
  ```bash
198
202
  grep -c '\$rrr-' AGENTS.md 2>/dev/null && echo "skills listed" || echo "AGENTS.md not present"
@@ -43,6 +43,12 @@ npx projecta-rrr@1.21.0 --enable-hosted
43
43
  # The installer registered `rrr-search-hosted` pointed at https://rrr-search-hosted.fly.dev/mcp.
44
44
  ```
45
45
 
46
+ For Codex, use `scripts/codex-rrr.sh` to launch the CLI with the hosted bearer loaded from
47
+ `~/.config/projecta-rrr/rrr-hosted.env`. Codex accepts the same `$rrr-*` trigger text, but it
48
+ does not provide Claude-style live autocomplete for those triggers while you type.
49
+ From this repo, `npm run codex:rrr -- <args>` is the shortest way to start Codex with the RRR
50
+ environment loaded.
51
+
46
52
  **What happens under the hood:**
47
53
 
48
54
  1. The installer writes `rrr-search-hosted` into `~/.claude/mcp.registry.json` with `disabled:false`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "projecta-rrr",
3
- "version": "1.23.3",
3
+ "version": "1.23.5",
4
4
  "description": "A meta-prompting, context engineering and spec-driven development system for Claude Code by Projecta.ai",
5
5
  "bin": {
6
6
  "projecta-rrr": "bin/install.js",
@@ -16,6 +16,7 @@
16
16
  "test": "node --test tests/*.test.js && jest",
17
17
  "test:node": "node --test tests/*.test.js",
18
18
  "test:jest": "jest",
19
+ "codex:rrr": "bash scripts/codex-rrr.sh",
19
20
  "test:browser": "vitest --browser",
20
21
  "test:ui": "vitest --ui",
21
22
  "mcp:setup": "bash scripts/mcp-setup.sh",
@@ -21,6 +21,12 @@ const TIER_MAP = {
21
21
  };
22
22
 
23
23
  const LEGACY_TOP_LEVEL_FIELDS = ['tier', 'fallback_model'];
24
+ const CODEX_AGENTS_BEGIN_MARKER = '# BEGIN projecta-rrr managed Codex agents';
25
+ const CODEX_AGENTS_END_MARKER = '# END projecta-rrr managed Codex agents';
26
+ const CODEX_MCP_TOOL_REWRITES = [
27
+ [/mcp__rrr-search-hosted__/g, 'mcp__rrr_search_hosted__'],
28
+ [/mcp__rrr-search__/g, 'mcp__rrr_search__'],
29
+ ];
24
30
 
25
31
  /**
26
32
  * Parse the YAML frontmatter from a markdown string.
@@ -92,6 +98,72 @@ function assertNoLegacyTopLevelFields(toml, filename) {
92
98
  }
93
99
  }
94
100
 
101
+ /**
102
+ * Codex normalizes MCP server names in tool namespaces by replacing hyphens
103
+ * with underscores. Claude-facing RRR agent sources use the raw MCP names, so
104
+ * generated Codex agent prompts must rewrite those tool identifiers.
105
+ */
106
+ function normalizeCodexMcpToolNames(text) {
107
+ let result = text || '';
108
+ for (const [pattern, replacement] of CODEX_MCP_TOOL_REWRITES) {
109
+ result = result.replace(pattern, replacement);
110
+ }
111
+ return result;
112
+ }
113
+
114
+ function tomlString(value) {
115
+ return String(value || '')
116
+ .replace(/\\/g, '\\\\')
117
+ .replace(/"/g, '\\"')
118
+ .replace(/\r?\n/g, ' ');
119
+ }
120
+
121
+ function generateCodexAgentsConfigBlock(agentConfigs) {
122
+ const lines = [
123
+ CODEX_AGENTS_BEGIN_MARKER,
124
+ '# Generated by projecta-rrr — do not edit manually',
125
+ ];
126
+
127
+ for (const agent of agentConfigs) {
128
+ lines.push(
129
+ '',
130
+ `[agents.${agent.name}]`,
131
+ `description = "${tomlString(agent.description || agent.name)}"`,
132
+ `config_file = "${tomlString(agent.configFile)}"`
133
+ );
134
+ }
135
+
136
+ lines.push('', CODEX_AGENTS_END_MARKER);
137
+ return lines.join('\n') + '\n';
138
+ }
139
+
140
+ function stripManagedCodexAgentsConfig(content) {
141
+ if (!content) return '';
142
+
143
+ let result = content;
144
+ const markerPattern = new RegExp(
145
+ `\\n?${CODEX_AGENTS_BEGIN_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${CODEX_AGENTS_END_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\n?`,
146
+ 'g'
147
+ );
148
+ result = result.replace(markerPattern, '\n');
149
+
150
+ // Self-heal older managed blocks that did not have markers.
151
+ result = result.replace(/\n?\[agents\.rrr-[^\]\r\n]+\][\s\S]*?(?=\n\[[^\]]+\]|\s*$)/g, '\n');
152
+
153
+ return result.replace(/\n{3,}/g, '\n\n').trim();
154
+ }
155
+
156
+ function mergeCodexAgentsConfig(configPath, agentConfigs) {
157
+ const block = generateCodexAgentsConfigBlock(agentConfigs);
158
+ const existing = fs.existsSync(configPath)
159
+ ? fs.readFileSync(configPath, 'utf8')
160
+ : '';
161
+ const stripped = stripManagedCodexAgentsConfig(existing);
162
+ const next = stripped ? `${stripped}\n\n${block}` : block;
163
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
164
+ fs.writeFileSync(configPath, next, 'utf8');
165
+ }
166
+
95
167
  /**
96
168
  * Convert a single RRR agent markdown file (Claude Code format) into
97
169
  * a Codex-compatible TOML agent config string.
@@ -117,7 +189,7 @@ function generateCodexAgentToml(sourceMarkdown, filename) {
117
189
  // developer_instructions: body of the markdown file (the system prompt).
118
190
  // Truncated to 3000 chars; uses TOML literal multi-line strings (''') which
119
191
  // need zero escaping — safe as long as the body contains no ''' sequence.
120
- const instructions = body.trim().slice(0, 3000);
192
+ const instructions = normalizeCodexMcpToolNames(body.trim()).slice(0, 3000);
121
193
 
122
194
  // Build TOML — only fields Codex CLI accepts
123
195
  const lines = [
@@ -125,7 +197,7 @@ function generateCodexAgentToml(sourceMarkdown, filename) {
125
197
  `# Source: agents/${filename}`,
126
198
  '',
127
199
  `name = "${agentName}"`,
128
- `description = "${descriptionField || agentName}"`,
200
+ `description = "${normalizeCodexMcpToolNames(descriptionField || agentName)}"`,
129
201
  `model = "${tierEntry.model}"`,
130
202
  `developer_instructions = '''`,
131
203
  instructions,
@@ -147,14 +219,16 @@ function generateCodexAgentToml(sourceMarkdown, filename) {
147
219
  * @param {object} options
148
220
  * @param {string} options.sourceDir - Absolute path to agents/ directory
149
221
  * @param {string} options.targetDir - Absolute path to target directory (e.g. ~/.codex/agents/)
222
+ * @param {string} [options.configPath] - Optional ~/.codex/config.toml path for agent registration
150
223
  * @param {boolean} [options.dryRun=false] - If true, compute paths but skip writing
151
224
  * @param {boolean} [options.verbose=false] - If true, log each file written
152
- * @returns {{ written: string[], skipped: string[], removed: string[], errors: Array<{file: string, error: string}> }}
225
+ * @returns {{ written: string[], skipped: string[], removed: string[], registered: number, errors: Array<{file: string, error: string}> }}
153
226
  */
154
227
  function installCodexAgents(options) {
155
228
  const {
156
229
  sourceDir,
157
230
  targetDir,
231
+ configPath = null,
158
232
  dryRun = false,
159
233
  verbose = false,
160
234
  } = options;
@@ -162,6 +236,7 @@ function installCodexAgents(options) {
162
236
  const written = [];
163
237
  const skipped = [];
164
238
  const removed = [];
239
+ const agentConfigs = [];
165
240
  const errors = [];
166
241
 
167
242
  // Ensure target directory exists (no-op if already exists)
@@ -175,7 +250,7 @@ function installCodexAgents(options) {
175
250
  entries = fs.readdirSync(sourceDir);
176
251
  } catch (err) {
177
252
  errors.push({ file: sourceDir, error: `Cannot read sourceDir: ${err.message}` });
178
- return { written, skipped, errors };
253
+ return { written, skipped, removed, registered: 0, errors };
179
254
  }
180
255
 
181
256
  for (const entry of entries) {
@@ -193,6 +268,9 @@ function installCodexAgents(options) {
193
268
 
194
269
  try {
195
270
  const source = fs.readFileSync(sourcePath, 'utf8');
271
+ const { frontmatterLines } = parseFrontmatter(source);
272
+ const agentName = extractField(frontmatterLines, 'name') || basename;
273
+ const description = normalizeCodexMcpToolNames(extractField(frontmatterLines, 'description') || agentName);
196
274
  const toml = generateCodexAgentToml(source, entry);
197
275
 
198
276
  if (!dryRun) {
@@ -204,6 +282,11 @@ function installCodexAgents(options) {
204
282
  }
205
283
 
206
284
  written.push(outputFilename);
285
+ agentConfigs.push({
286
+ name: agentName,
287
+ description,
288
+ configFile: targetPath,
289
+ });
207
290
  } catch (err) {
208
291
  errors.push({ file: entry, error: err.message });
209
292
  }
@@ -228,11 +311,25 @@ function installCodexAgents(options) {
228
311
  }
229
312
  }
230
313
 
231
- return { written, skipped, removed, errors };
314
+ if (configPath && !dryRun && errors.length === 0) {
315
+ try {
316
+ mergeCodexAgentsConfig(configPath, agentConfigs);
317
+ } catch (err) {
318
+ errors.push({ file: configPath, error: `Cannot merge Codex config: ${err.message}` });
319
+ }
320
+ }
321
+
322
+ return { written, skipped, removed, registered: configPath && !dryRun && errors.length === 0 ? agentConfigs.length : 0, errors };
232
323
  }
233
324
 
234
325
  module.exports = {
326
+ CODEX_AGENTS_BEGIN_MARKER,
327
+ CODEX_AGENTS_END_MARKER,
235
328
  generateCodexAgentToml,
329
+ generateCodexAgentsConfigBlock,
236
330
  installCodexAgents,
331
+ mergeCodexAgentsConfig,
332
+ normalizeCodexMcpToolNames,
333
+ stripManagedCodexAgentsConfig,
237
334
  TIER_MAP,
238
335
  };
@@ -15,6 +15,61 @@ const CLAUDE_ONLY_FIELDS = new Set([
15
15
  'argument-hint',
16
16
  ]);
17
17
 
18
+ const CODEX_MCP_TOOL_REWRITES = [
19
+ [/mcp__rrr-search-hosted__/g, 'mcp__rrr_search_hosted__'],
20
+ [/mcp__rrr-search__/g, 'mcp__rrr_search__'],
21
+ ];
22
+
23
+ function normalizeCodexMcpToolNames(text) {
24
+ let result = text || '';
25
+ for (const [pattern, replacement] of CODEX_MCP_TOOL_REWRITES) {
26
+ result = result.replace(pattern, replacement);
27
+ }
28
+ return result;
29
+ }
30
+
31
+ function convertSlashCommandsToCodexSkillMentions(content) {
32
+ let converted = content.replace(/\/rrr:([a-z0-9-]+)/gi, (_, commandName) => {
33
+ return `$rrr-${String(commandName).toLowerCase()}`;
34
+ });
35
+
36
+ // Convert hyphen-style command references while avoiding file paths.
37
+ converted = converted.replace(/(?<![a-zA-Z0-9./])\/rrr-([a-z0-9-]+)/gi, (_, commandName) => {
38
+ return `$rrr-${String(commandName).toLowerCase()}`;
39
+ });
40
+
41
+ return converted;
42
+ }
43
+
44
+ function getCodexSkillAdapterHeader(skillName) {
45
+ const invocation = `$${skillName}`;
46
+ return `<codex_skill_adapter>
47
+ ## A. Skill Invocation
48
+ - This skill is invoked by mentioning \`${invocation}\`.
49
+ - Treat all user text after \`${invocation}\` as \`{{RRR_ARGS}}\`.
50
+ - If no arguments are present, treat \`{{RRR_ARGS}}\` as empty.
51
+
52
+ ## B. AskUserQuestion -> request_user_input Mapping
53
+ RRR workflows use \`AskUserQuestion\` in Claude Code command sources. Translate it to Codex \`request_user_input\` when that tool is available:
54
+
55
+ - \`header\` -> \`header\`
56
+ - \`question\` -> \`question\`
57
+ - Options formatted as \`"Label" - description\` -> \`{ label: "Label", description: "description" }\`
58
+ - Generate \`id\` from the header: lowercase, replace spaces with underscores
59
+ - Batched questions become one \`request_user_input\` call with multiple entries in \`questions[]\`
60
+ - In Default mode, \`request_user_input\` is unavailable. Do not call it; present a concise numbered list instead and use a reasonable default when the workflow can proceed safely
61
+
62
+ ## C. Task() -> spawn_agent Mapping
63
+ RRR workflows use \`Task(...)\` to delegate to RRR subagents. Translate to Codex collaboration tools when policy allows spawning:
64
+
65
+ - \`Task(subagent_type="X", prompt="Y")\` -> \`spawn_agent(agent_type="X", message="Y")\`
66
+ - Omit inline model parameters; Codex agent configuration owns model selection
67
+ - Default \`fork_context\` to false unless the workflow explicitly needs shared context
68
+ - For parallel fan-out, spawn agents, collect IDs, wait for completion, then close completed agents
69
+ - If spawning is not allowed in the current mode, complete the work inline in the current agent
70
+ </codex_skill_adapter>`;
71
+ }
72
+
18
73
  /**
19
74
  * Parse the YAML frontmatter from a markdown string.
20
75
  *
@@ -125,6 +180,14 @@ function extractField(lines, key) {
125
180
  return null;
126
181
  }
127
182
 
183
+ function quoteYamlScalar(value) {
184
+ return `"${String(value || '')
185
+ .replace(/\\/g, '\\\\')
186
+ .replace(/"/g, '\\"')
187
+ .replace(/\r?\n/g, ' ')
188
+ .trim()}"`;
189
+ }
190
+
128
191
  /**
129
192
  * Serialize filtered frontmatter lines back to a YAML block with delimiters.
130
193
  *
@@ -133,28 +196,13 @@ function extractField(lines, key) {
133
196
  * description: <from source or generated>
134
197
  */
135
198
  function serializeFrontmatter(filteredLines, skillName, sourceDescription) {
136
- // Build final set of lines, ensuring required fields are present
137
- const hasName = filteredLines.some(l => /^name\s*:/.test(l));
138
- const hasDescription = filteredLines.some(l => /^description\s*:/.test(l));
139
-
140
- const outputLines = [...filteredLines];
141
-
142
- // Replace or insert 'name' field (always override with derived skill name)
143
- if (hasName) {
144
- const idx = outputLines.findIndex(l => /^name\s*:/.test(l));
145
- outputLines[idx] = `name: ${skillName}`;
146
- } else {
147
- // Insert name as first field
148
- outputLines.unshift(`name: ${skillName}`);
149
- }
199
+ const description = sourceDescription || `RRR: ${skillName}`;
200
+ const outputLines = filteredLines.filter(l => {
201
+ return !/^name\s*:/.test(l) && !/^description\s*:/.test(l);
202
+ });
150
203
 
151
- // Insert description if missing
152
- if (!hasDescription) {
153
- const description = sourceDescription || `RRR: ${skillName}`;
154
- // Insert after name
155
- const nameIdx = outputLines.findIndex(l => /^name\s*:/.test(l));
156
- outputLines.splice(nameIdx + 1, 0, `description: ${description}`);
157
- }
204
+ outputLines.unshift(`description: ${quoteYamlScalar(description)}`);
205
+ outputLines.unshift(`name: ${quoteYamlScalar(skillName)}`);
158
206
 
159
207
  return `---\n${outputLines.join('\n')}\n---\n`;
160
208
  }
@@ -165,21 +213,33 @@ function serializeFrontmatter(filteredLines, skillName, sourceDescription) {
165
213
  * Conversions:
166
214
  * 1. `~/.claude/` → `~/.codex/`
167
215
  * 2. `.claude/` when preceded by non-word/non-slash → `.codex/`
168
- * 3. `/rrr:` `$rrr-`
216
+ * 3. `$ARGUMENTS` `{{RRR_ARGS}}`
217
+ * 4. `/clear` → removed
218
+ * 5. `/rrr:*` → `$rrr-*`
169
219
  */
170
- function transformBody(body) {
171
- // 1. ~/.claude/ ~/.codex/
172
- let result = body.replace(/~\/\.claude\//g, '~/.codex/');
220
+ function transformBody(body, options = {}) {
221
+ const codexPathPrefix = options.codexPathPrefix || '~/.codex/';
222
+ const codexEnvPathPrefix = options.codexEnvPathPrefix || '$HOME/.codex/';
223
+ let result = convertSlashCommandsToCodexSkillMentions(body);
224
+
225
+ result = result.replace(/\$ARGUMENTS\b/g, '{{RRR_ARGS}}');
226
+
227
+ // Remove /clear references. Codex has no equivalent command.
228
+ result = result.replace(/`\/clear`\s*,?\s*then:?\s*\n?/gi, '');
229
+ result = result.replace(/\/clear\s*,?\s*then:?\s*\n?/gi, '');
230
+ result = result.replace(/^\s*`?\/clear`?\s*$/gm, '');
231
+
232
+ // 1. ~/.claude/ and $HOME/.claude/ -> Codex home.
233
+ result = result.replace(/~\/\.claude\//g, codexPathPrefix);
234
+ result = result.replace(/\$HOME\/\.claude\//g, codexEnvPathPrefix);
173
235
 
174
236
  // 2. .claude/ when NOT preceded by a word character or slash
175
237
  // Use a regex that matches the context prefix so we can reconstruct it.
176
238
  // Matches: start-of-string, whitespace, or punctuation that isn't a word char or slash.
177
239
  result = result.replace(/(^|[^a-zA-Z0-9/_])\.claude\//gm, '$1.codex/');
240
+ result = result.replace(/\.claudeignore\b/g, '.codexignore');
178
241
 
179
- // 3. /rrr: → $rrr-
180
- result = result.replace(/\/rrr:/g, '$rrr-');
181
-
182
- return result;
242
+ return normalizeCodexMcpToolNames(result);
183
243
  }
184
244
 
185
245
  /**
@@ -190,7 +250,7 @@ function transformBody(body) {
190
250
  * @param {string} filename - Filename (e.g. "plan-phase.md")
191
251
  * @returns {string} Transformed skill markdown
192
252
  */
193
- function convertRRRCommandToCodexSkill(sourceMarkdown, filename) {
253
+ function convertRRRCommandToCodexSkill(sourceMarkdown, filename, options = {}) {
194
254
  const { frontmatterLines, body } = parseFrontmatter(sourceMarkdown);
195
255
 
196
256
  const skillName = deriveSkillName(filename);
@@ -205,9 +265,9 @@ function convertRRRCommandToCodexSkill(sourceMarkdown, filename) {
205
265
  const newFrontmatter = serializeFrontmatter(filteredLines, skillName, sourceDescription);
206
266
 
207
267
  // Transform body (path refs + trigger text)
208
- const newBody = transformBody(body);
268
+ const newBody = transformBody(body, options);
209
269
 
210
- return `${newFrontmatter}${newBody}`;
270
+ return `${newFrontmatter}\n${getCodexSkillAdapterHeader(skillName)}\n\n${newBody.trimStart()}`;
211
271
  }
212
272
 
213
273
  /**
@@ -218,7 +278,7 @@ function convertRRRCommandToCodexSkill(sourceMarkdown, filename) {
218
278
  * @param {string} options.targetDir - Absolute path to target directory (e.g. ~/.codex/skills/)
219
279
  * @param {boolean} [options.dryRun=false] - If true, compute paths but skip writing
220
280
  * @param {boolean} [options.verbose=false] - If true, log each file written
221
- * @returns {{ written: string[], skipped: string[], errors: Array<{file: string, error: string}> }}
281
+ * @returns {{ written: string[], skipped: string[], removed: string[], errors: Array<{file: string, error: string}> }}
222
282
  */
223
283
  function installCodexSkills(options) {
224
284
  const {
@@ -226,10 +286,13 @@ function installCodexSkills(options) {
226
286
  targetDir,
227
287
  dryRun = false,
228
288
  verbose = false,
289
+ codexPathPrefix = '~/.codex/',
290
+ codexEnvPathPrefix = '$HOME/.codex/',
229
291
  } = options;
230
292
 
231
293
  const written = [];
232
294
  const skipped = [];
295
+ const removed = [];
233
296
  const errors = [];
234
297
 
235
298
  // Ensure target directory exists (no-op if already exists)
@@ -243,7 +306,7 @@ function installCodexSkills(options) {
243
306
  entries = fs.readdirSync(sourceDir);
244
307
  } catch (err) {
245
308
  errors.push({ file: sourceDir, error: `Cannot read sourceDir: ${err.message}` });
246
- return { written, skipped, errors };
309
+ return { written, skipped, removed, errors };
247
310
  }
248
311
 
249
312
  for (const entry of entries) {
@@ -254,17 +317,27 @@ function installCodexSkills(options) {
254
317
  }
255
318
 
256
319
  const sourcePath = path.join(sourceDir, entry);
257
- // Output filename: rrr-{basename}.md (e.g. plan-phase.md → rrr-plan-phase.md)
258
320
  const basename = path.basename(entry, '.md');
259
- const outputFilename = `rrr-${basename}.md`;
260
- const targetPath = path.join(targetDir, outputFilename);
321
+ const skillName = `rrr-${basename}`;
322
+ const skillDir = path.join(targetDir, skillName);
323
+ const outputFilename = path.join(skillName, 'SKILL.md');
324
+ const targetPath = path.join(skillDir, 'SKILL.md');
325
+ const legacyFlatPath = path.join(targetDir, `${skillName}.md`);
261
326
 
262
327
  try {
263
328
  const source = fs.readFileSync(sourcePath, 'utf8');
264
- const transformed = convertRRRCommandToCodexSkill(source, entry);
329
+ const transformed = convertRRRCommandToCodexSkill(source, entry, {
330
+ codexPathPrefix,
331
+ codexEnvPathPrefix,
332
+ });
265
333
 
266
334
  if (!dryRun) {
335
+ fs.mkdirSync(skillDir, { recursive: true });
267
336
  fs.writeFileSync(targetPath, transformed, 'utf8');
337
+ if (fs.existsSync(legacyFlatPath)) {
338
+ fs.unlinkSync(legacyFlatPath);
339
+ removed.push(`${skillName}.md`);
340
+ }
268
341
  }
269
342
 
270
343
  if (verbose) {
@@ -277,10 +350,12 @@ function installCodexSkills(options) {
277
350
  }
278
351
  }
279
352
 
280
- return { written, skipped, errors };
353
+ return { written, skipped, removed, errors };
281
354
  }
282
355
 
283
356
  module.exports = {
284
357
  convertRRRCommandToCodexSkill,
358
+ getCodexSkillAdapterHeader,
285
359
  installCodexSkills,
360
+ normalizeCodexMcpToolNames,
286
361
  };
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+
5
+ ENV_FILE="${RRR_HOSTED_ENV_FILE:-$HOME/.config/projecta-rrr/rrr-hosted.env}"
6
+
7
+ if [[ -f "$ENV_FILE" ]]; then
8
+ # shellcheck disable=SC1090
9
+ source "$ENV_FILE"
10
+ fi
11
+
12
+ if [[ -z "${RRR_HOSTED_BEARER:-}" ]]; then
13
+ cat >&2 <<'EOF'
14
+ RRR_HOSTED_BEARER is unset.
15
+ Set it in ~/.config/projecta-rrr/rrr-hosted.env or export it before launching Codex.
16
+ EOF
17
+ exit 1
18
+ fi
19
+
20
+ exec codex "$@"