projecta-rrr 1.22.4 → 1.23.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.
package/AGENTS.md ADDED
@@ -0,0 +1,87 @@
1
+ <!-- auto-generated by scripts/gen-agents-md.js — do NOT hand-edit; regenerate with: node scripts/gen-agents-md.js -->
2
+
3
+ # RRR — AI Workflow for Codex
4
+
5
+ RRR is a structured planning and execution workflow that reduces cognitive load while maintaining quality across brownfield projects. Codex invokes RRR capabilities via `$rrr-*` trigger phrases, each corresponding to a specific workflow skill.
6
+
7
+ ## Invocation
8
+
9
+ Use `$rrr-{skill-name}` syntax to invoke a skill. Examples:
10
+
11
+ - `$rrr-plan-phase` — create a detailed execution plan for a phase
12
+ - `$rrr-execute-phase` — run all plans in a phase with wave-based parallelization
13
+ - `$rrr-next` — see the recommended next action
14
+ - `$rrr-help` — show full skill catalogue
15
+
16
+ Trigger phrases are case-sensitive: use `$rrr-plan-phase` not `$rrr-PlanPhase`.
17
+
18
+ ## Core Workflow
19
+
20
+ The RRR loop follows five steps:
21
+
22
+ 1. **Discuss** — Align on goals, scope, and approach
23
+ 2. **Plan** — Generate structured PLAN.md files with tasks and verification
24
+ 3. **Execute** — Run plans atomically, one task per commit
25
+ 4. **Verify** — Confirm output matches success criteria
26
+ 5. **Complete** — Archive milestone, update STATE.md, tag release
27
+
28
+ ## Available Skills
29
+
30
+ | Trigger | Description |
31
+ | ------- | ----------- |
32
+ | `$rrr-add-phase` | Add phase to end of current milestone in roadmap |
33
+ | `$rrr-add-todo` | Capture idea or task as todo from current conversation context |
34
+ | `$rrr-audit-milestone` | Audit milestone completion against original intent before archiving |
35
+ | `$rrr-bootstrap-nextjs` | Scaffold a Next.js App Router MVP with TypeScript, Tailwind, shadcn/ui, Vitest, and Playwright |
36
+ | `$rrr-brownfield-audit` | Audit brownfield repos for scattered planning docs before RRR initialization |
37
+ | `$rrr-check-browser-uat` | Verify browser UAT setup and report tool selection |
38
+ | `$rrr-check-todos` | List pending todos and select one to work on |
39
+ | `$rrr-check-upstream` | Run check-upstream workflow |
40
+ | `$rrr-check-version` | Check version consistency across package.json, CHANGELOG, and planning docs |
41
+ | `$rrr-complete-milestone` | Archive completed milestone and prepare for next version |
42
+ | `$rrr-create-roadmap` | Create roadmap with phases for the project |
43
+ | `$rrr-debug` | Systematic debugging with persistent state across context resets |
44
+ | `$rrr-define-requirements` | Define what "done" looks like with checkable requirements |
45
+ | `$rrr-discuss-milestone` | Gather context for next milestone through adaptive questioning |
46
+ | `$rrr-discuss-phase` | Gather phase context through adaptive questioning before planning |
47
+ | `$rrr-doctor` | Diagnose and fix duplicate RRR installs - non-destructive with rollback |
48
+ | `$rrr-execute-phase` | Execute all plans in a phase with wave-based parallelization |
49
+ | `$rrr-execute-plan` | Execute a PLAN.md file |
50
+ | `$rrr-fix-visual-failure` | Analyze visual proof failures and suggest fixes |
51
+ | `$rrr-help` | Show available RRR commands and usage guide |
52
+ | `$rrr-insert-phase` | Insert urgent work as decimal phase (e.g., 72.1) between existing phases |
53
+ | `$rrr-install-skill` | Install a skill from GitHub or marketplace |
54
+ | `$rrr-list-phase-assumptions` | Surface Claude's assumptions about a phase approach before planning |
55
+ | `$rrr-list-skills` | List all available skills (vendored and community) |
56
+ | `$rrr-map-codebase` | Analyze codebase with parallel mapper agents to produce .planning/codebase/ documents |
57
+ | `$rrr-migrate-phases` | Assign orphan phases to milestones (for v1.11 migration) |
58
+ | `$rrr-mvp` | One-command router - detects project state, estimates complexity, and tells you exactly what to run next |
59
+ | `$rrr-new-milestone` | Start a new milestone cycle — update PROJECT.md and route to requirements |
60
+ | `$rrr-new-project` | Initialize a new project with deep context gathering and PROJECT.md |
61
+ | `$rrr-next` | Determine and execute the next action based on project state |
62
+ | `$rrr-optimize-cpu` | Optimize CPU, RAM, and disk - clean caches and kill zombies - clean caches, kill zombies, free RAM/CPU |
63
+ | `$rrr-overnight` | Jarvis (overnight automation) guidance - preflight checks and safe run instructions |
64
+ | `$rrr-pause-work` | Create context handoff when pausing work mid-phase |
65
+ | `$rrr-plan-milestone-gaps` | Create phases to close all gaps identified by milestone audit |
66
+ | `$rrr-plan-phase` | Create detailed execution plan for a phase (PLAN.md) with verification loop |
67
+ | `$rrr-progress` | Check project progress, show context, and route to next action (execute or plan) |
68
+ | `$rrr-remove-phase` | Remove a future phase from roadmap and renumber subsequent phases |
69
+ | `$rrr-research-phase` | Research how to implement a phase (standalone - usually use /rrr:plan-phase instead) |
70
+ | `$rrr-research-project` | Research domain ecosystem before creating roadmap |
71
+ | `$rrr-resume-work` | Resume work from previous session with full context restoration |
72
+ | `$rrr-savings` | Show token-savings report (session + lifetime) with tier distribution and Opus-rate dashboard |
73
+ | `$rrr-search-skills` | Search for skills on skillsmp.com marketplace |
74
+ | `$rrr-ship` | Create PR from verified work with planning context, or generate SHIP-READY.md |
75
+ | `$rrr-think` | Apply structured thinking models for architecture decisions, risk assessment, scope cuts, and root cause analysis |
76
+ | `$rrr-update` | Update RRR to latest version with idempotent install + duplicate prevention |
77
+ | `$rrr-verify-work` | Validate built features through audit (default) or interactive UAT |
78
+ | `$rrr-whats-new` | See what's new in RRR since your installed version |
79
+
80
+ ## Key Conventions
81
+
82
+ - Always check `.planning/STATE.md` for current project position before invoking a workflow skill
83
+ - Use `$rrr-next` to see recommended next action
84
+ - Use `$rrr-help` for full skill catalogue
85
+ - Trigger phrases are case-sensitive: `$rrr-plan-phase` not `$rrr-PlanPhase`
86
+
87
+ <!-- generated: 2026-04-20T21:10:01.139Z | source: commands/rrr/*.md | count: 47 skills -->
package/CHANGELOG.md CHANGED
@@ -4,6 +4,29 @@ 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.1] - 2026-04-20
8
+
9
+ ### Fixed
10
+ - **Codex agent TOML generation** — removed `tier` and `fallback_model` fields from generated `.toml` files; Codex CLI rejects these unknown fields and silently ignores the entire agent definition. Generated files now contain only `name`, `description`, and `model`.
11
+
12
+ ## [1.23.0] - 2026-04-20
13
+
14
+ **v1.23 Multi-Provider Compatibility — RRR workflow now works on OpenAI Codex with zero extra setup.**
15
+
16
+ ### Added
17
+ - **Codex skill transformation** (`rrr/lib/codex-skill-transform.js`) — `convertRRRCommandToCodexSkill()` strips Claude-specific frontmatter, rewrites `~/.claude/` → `~/.codex/` and `/rrr:` → `$rrr-`; all 47 RRR commands available as `$rrr-*` Codex skills
18
+ - **Codex agent configs** (`rrr/lib/codex-agent-gen.js`) — generates `.toml` files for all 13 agents with provider-agnostic tier labels (FAST/STANDARD/ADAPTIVE); no Anthropic model names in Codex configs
19
+ - **Auto-detection + auto-install** — `bin/install.js` detects Codex CLI in PATH and installs skills/agents automatically; brownfield-safe (`.claude/` never touched)
20
+ - **`/rrr:update` Codex support** — existing users get Codex skills and fresh AGENTS.md without a re-install
21
+ - **Hosted MCP for Codex** (`bin/hosted-setup.js`) — registers `rrr-search-hosted` in `.codex/config.toml`; same bearer token, no second prompt; idempotent via remove-then-append
22
+ - **`AGENTS.md` auto-generation** (`scripts/gen-agents-md.js`) — 47-row trigger table + workflow conventions for Codex; ships in npm package
23
+ - **AGENTS.md sync gate** — `prepublish:check` section 16 fails if AGENTS.md drifts from skill files (hard block, not warning)
24
+
25
+ ### Changed
26
+ - Installer idempotent for Codex: re-running `npx projecta-rrr` or `/rrr:update` does not duplicate skill files
27
+
28
+ ---
29
+
7
30
  ## [1.22.4] - 2026-04-19
8
31
 
9
32
  **Patch: semantic search wired into 5 agents; rrr-explore tool names fixed.**
@@ -23,12 +23,14 @@
23
23
 
24
24
  const fs = require('fs');
25
25
  const path = require('path');
26
+ const os = require('os');
26
27
  const readline = require('readline');
27
28
  const { execSync, spawnSync } = require('child_process');
28
29
 
29
30
  const HOSTED_URL = 'https://rrr-search-hosted.fly.dev/mcp';
30
31
  const GH_APP_INSTALL_URL = 'https://github.com/apps/rrr-search/installations/new';
31
32
  const MCP_NAME = 'rrr-search-hosted';
33
+ const CODEX_CONFIG_PATH = path.join(os.homedir(), '.codex', 'config.toml');
32
34
 
33
35
  // ──────────────────────────────── UI ──────────────────────────────────────
34
36
 
@@ -115,12 +117,39 @@ function mcpAlreadyRegistered (name) {
115
117
  } catch { return false; }
116
118
  }
117
119
 
120
+ // ──────────────────────── Codex MCP helpers ───────────────────────────────
121
+
122
+ function codexAlreadyRegistered () {
123
+ try {
124
+ const content = fs.readFileSync(CODEX_CONFIG_PATH, 'utf8');
125
+ return content.includes('[mcp_servers.' + MCP_NAME + ']');
126
+ } catch { return false; }
127
+ }
128
+
129
+ function registerCodexMcp (bearer) {
130
+ // Read existing config or start empty
131
+ let content = '';
132
+ try { content = fs.readFileSync(CODEX_CONFIG_PATH, 'utf8'); } catch { /* file may not exist */ }
133
+
134
+ // Remove existing registration if present (idempotent re-register)
135
+ const sectionRegex = /\[mcp_servers\.rrr-search-hosted\][\s\S]*?(?=\[|\s*$)/;
136
+ content = content.replace(sectionRegex, '').trimEnd();
137
+
138
+ // Append new registration
139
+ const section = `\n\n[mcp_servers.${MCP_NAME}]\nurl = "${HOSTED_URL}"\nheaders = { Authorization = "Bearer ${bearer}" }\n`;
140
+ content = content + section;
141
+
142
+ // Ensure ~/.codex directory exists
143
+ fs.mkdirSync(path.dirname(CODEX_CONFIG_PATH), { recursive: true });
144
+ fs.writeFileSync(CODEX_CONFIG_PATH, content, 'utf8');
145
+ }
146
+
118
147
  // ──────────────────────────────── flow ────────────────────────────────────
119
148
 
120
149
  async function main () {
121
150
  const args = parseArgs(process.argv.slice(2));
122
151
  const cwd = process.cwd();
123
- const TOTAL_STEPS = 6;
152
+ const TOTAL_STEPS = 7;
124
153
 
125
154
  banner(` rrr-search-hosted setup — ${path.basename(cwd)}`);
126
155
 
@@ -248,8 +277,33 @@ async function main () {
248
277
  } catch { warn('Health check skipped'); }
249
278
  }
250
279
 
251
- // ---------- Step 6: GitHub App + first index ----------
252
- step(6, TOTAL_STEPS, 'GitHub App + first index');
280
+ // ---------- Step 6: Register MCP in Codex (if Codex present) ----------
281
+ step(6, TOTAL_STEPS, 'Register MCP in Codex (optional)');
282
+
283
+ // Detect codex CLI
284
+ const hasCodex = (() => { try { execSync('command -v codex', { stdio: 'pipe' }); return true; } catch { return false; } })();
285
+
286
+ if (!bearer) {
287
+ warn('Skipping (no bearer).');
288
+ } else if (!hasCodex) {
289
+ info('Codex CLI not found — skipping ~/.codex/config.toml registration.');
290
+ info('If you install Codex later, re-run: projecta-rrr hosted-setup --bearer <your-token>');
291
+ } else {
292
+ if (codexAlreadyRegistered()) {
293
+ info('Removing existing Codex registration to re-register with current bearer');
294
+ }
295
+ try {
296
+ registerCodexMcp(bearer);
297
+ ok(`Registered ${MCP_NAME} in ~/.codex/config.toml`);
298
+ info('Same bearer token works for both Claude Code and Codex (MCP-02)');
299
+ } catch (e) {
300
+ warn(`Could not write ~/.codex/config.toml: ${e.message}`);
301
+ info('You can manually add the MCP server entry — see docs/hosted-search-setup.md');
302
+ }
303
+ }
304
+
305
+ // ---------- Step 7: GitHub App + first index ----------
306
+ step(7, TOTAL_STEPS, 'GitHub App + first index');
253
307
  console.log();
254
308
  console.log(c.bold(' Install GitHub App (required for ingest):'));
255
309
  console.log(c.cyan(` ${GH_APP_INSTALL_URL}`));
package/bin/install.js CHANGED
@@ -74,6 +74,22 @@ function checkBashAvailability() {
74
74
  }
75
75
  }
76
76
 
77
+ /**
78
+ * Detect if OpenAI Codex CLI is available in PATH
79
+ * Returns { available: boolean, version?: string }
80
+ */
81
+ function detectCodexCLI() {
82
+ try {
83
+ const version = execSync('codex --version', { encoding: 'utf8', timeout: 5000 })
84
+ .split('\n')[0]
85
+ .trim();
86
+ return { available: true, version };
87
+ } catch (e) {
88
+ // codex not in PATH — not an error, just not installed
89
+ return { available: false };
90
+ }
91
+ }
92
+
77
93
  /**
78
94
  * Print Windows bash remediation message
79
95
  */
@@ -2163,6 +2179,60 @@ function install(isGlobal) {
2163
2179
  }
2164
2180
  }
2165
2181
 
2182
+ // Install Codex skills if Codex CLI is in PATH (INST-01, INST-03, INST-04, SKILL-02)
2183
+ // Brownfield-safe: ~/.claude/ is never modified by this block
2184
+ // Idempotent: installCodexSkills() overwrites existing files
2185
+ const codexStatus = detectCodexCLI();
2186
+ if (codexStatus.available) {
2187
+ try {
2188
+ const { installCodexSkills } = require('../rrr/lib/codex-skill-transform');
2189
+ const codexSkillsDir = path.join(os.homedir(), '.codex', 'skills');
2190
+ const rrrCommandsDir = path.join(src, 'commands', 'rrr');
2191
+ const result = installCodexSkills({
2192
+ sourceDir: rrrCommandsDir,
2193
+ targetDir: codexSkillsDir,
2194
+ dryRun: hasDryRun,
2195
+ verbose: false
2196
+ });
2197
+ if (result.errors.length === 0) {
2198
+ console.log(` ${green}✓${reset} Installed ${result.written.length} Codex skills to ~/.codex/skills/`);
2199
+ } else {
2200
+ console.log(` ${yellow}⚠${reset} Installed ${result.written.length} Codex skills (${result.errors.length} errors)`);
2201
+ result.errors.slice(0, 3).forEach(e => {
2202
+ console.log(` - ${e.file}: ${e.error}`);
2203
+ });
2204
+ }
2205
+ } catch (e) {
2206
+ // Silent fail — Codex skill install is optional, must not break main Claude install
2207
+ console.log(` ${dim}Codex skills skipped: ${e.message}${reset}`);
2208
+ }
2209
+
2210
+ // Install Codex agent .toml configs (AGENT-01, AGENT-02, AGENT-03)
2211
+ try {
2212
+ const { installCodexAgents } = require('../rrr/lib/codex-agent-gen');
2213
+ const codexAgentsDir = path.join(os.homedir(), '.codex', 'agents');
2214
+ const rrrAgentsDir = path.join(src, 'agents');
2215
+ const agentResult = installCodexAgents({
2216
+ sourceDir: rrrAgentsDir,
2217
+ targetDir: codexAgentsDir,
2218
+ dryRun: hasDryRun,
2219
+ verbose: false
2220
+ });
2221
+ if (agentResult.errors.length === 0) {
2222
+ console.log(` ${green}✓${reset} Installed ${agentResult.written.length} Codex agent configs to ~/.codex/agents/`);
2223
+ } else {
2224
+ console.log(` ${yellow}⚠${reset} Installed ${agentResult.written.length} agent configs (${agentResult.errors.length} errors)`);
2225
+ agentResult.errors.slice(0, 3).forEach(e => {
2226
+ console.log(` - ${e.file}: ${e.error}`);
2227
+ });
2228
+ }
2229
+ } catch (e) {
2230
+ // Silent fail — agent install is optional, must not break main Claude install
2231
+ console.log(` ${dim}Codex agents skipped: ${e.message}${reset}`);
2232
+ }
2233
+ }
2234
+ // (no else — silently skip when codex is not in PATH)
2235
+
2166
2236
  return { settingsPath, settings, statuslineCommand, notifyCommand, claudeDir, localDirName, isGlobal, bashAvailable: bashStatus.available };
2167
2237
  }
2168
2238
 
@@ -155,6 +155,50 @@ If the version shown is older than what just installed, print a warning:
155
155
  Show error and STOP. Do not proceed.
156
156
  </step>
157
157
 
158
+ <step name="refresh_codex_skills">
159
+ **Codex skill refresh (automatic)**
160
+
161
+ The `npx projecta-rrr@latest --global` step above automatically detects Codex CLI in PATH and re-installs transformed RRR skills to `~/.codex/skills/`. No manual action needed.
162
+
163
+ If Codex CLI is installed, you will see:
164
+ ```
165
+ ✓ Installed N Codex skills to ~/.codex/skills/
166
+ ```
167
+
168
+ If Codex CLI is not installed, this step is silently skipped.
169
+
170
+ **To verify Codex skills were refreshed:**
171
+ ```bash
172
+ ls ~/.codex/skills/rrr-*.md 2>/dev/null | wc -l
173
+ ```
174
+ Should show 47 (or current command count) skill files.
175
+ </step>
176
+
177
+ <step name="refresh_agents_md">
178
+ **AGENTS.md refresh (automatic)**
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.
183
+
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
+ ```
187
+
188
+ **If AGENTS.md was updated:**
189
+ ```
190
+ ✓ AGENTS.md refreshed (N skills)
191
+ ```
192
+
193
+ **If not in an RRR project or script not found:**
194
+ Silently skip — AGENTS.md is only relevant when Codex is being used in the project.
195
+
196
+ **To verify:**
197
+ ```bash
198
+ grep -c '\$rrr-' AGENTS.md 2>/dev/null && echo "skills listed" || echo "AGENTS.md not present"
199
+ ```
200
+ </step>
201
+
158
202
  <step name="verify_installation_integrity">
159
203
  Run the smoke test to verify installation succeeded:
160
204
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "projecta-rrr",
3
- "version": "1.22.4",
3
+ "version": "1.23.1",
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",
@@ -36,6 +36,7 @@
36
36
  },
37
37
  "files": [
38
38
  "bin",
39
+ "AGENTS.md",
39
40
  "commands",
40
41
  "docs",
41
42
  "agents",
@@ -0,0 +1,191 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * Provider-agnostic tier mapping for RRR agents.
8
+ *
9
+ * Maps Claude model labels (from agents/*.md frontmatter) to Codex-compatible
10
+ * tier labels and OpenAI model names. No Anthropic model names appear in output.
11
+ *
12
+ * AGENT-02, AGENT-03 tier definitions:
13
+ * haiku → FAST (gpt-4o-mini)
14
+ * sonnet → STANDARD (gpt-4o)
15
+ * inherit → ADAPTIVE (gpt-4o with o1 fallback for complex reasoning)
16
+ */
17
+ const TIER_MAP = {
18
+ haiku: { tier: 'FAST', model: 'gpt-4o-mini' },
19
+ sonnet: { tier: 'STANDARD', model: 'gpt-4o' },
20
+ inherit: { tier: 'ADAPTIVE', model: 'gpt-4o', fallback_model: 'o1' },
21
+ };
22
+
23
+ /**
24
+ * Parse the YAML frontmatter from a markdown string.
25
+ *
26
+ * Supports files that start with a `---` block.
27
+ *
28
+ * Returns { frontmatterLines: string[], body: string } where:
29
+ * - frontmatterLines: raw key-value lines from the YAML block (no delimiters)
30
+ * - body: everything after the closing `---` delimiter (may start with \n)
31
+ *
32
+ * If there is no frontmatter, frontmatterLines is [] and body is the full content.
33
+ */
34
+ function parseFrontmatter(source) {
35
+ // Must begin with ---\n
36
+ if (!source.startsWith('---\n') && !source.startsWith('---\r\n')) {
37
+ return { frontmatterLines: [], body: source };
38
+ }
39
+
40
+ // Find the closing --- delimiter (must be on its own line)
41
+ const afterOpen = source.slice(4); // skip the opening "---\n"
42
+ const closingMatch = afterOpen.match(/^---[ \t]*(\r?\n|$)/m);
43
+ if (!closingMatch) {
44
+ // No closing delimiter — treat entire file as body
45
+ return { frontmatterLines: [], body: source };
46
+ }
47
+
48
+ const closingIndex = closingMatch.index;
49
+ const frontmatterText = afterOpen.slice(0, closingIndex);
50
+ const body = afterOpen.slice(closingIndex + closingMatch[0].length);
51
+
52
+ // Split into lines for individual field processing
53
+ const frontmatterLines = frontmatterText.split('\n');
54
+ // Remove trailing empty line if present
55
+ if (frontmatterLines[frontmatterLines.length - 1] === '') {
56
+ frontmatterLines.pop();
57
+ }
58
+
59
+ return { frontmatterLines, body };
60
+ }
61
+
62
+ /**
63
+ * Extract a specific field value from frontmatter lines.
64
+ * Only looks at simple scalar values on the same line as the key.
65
+ * Returns null if the key is not found.
66
+ */
67
+ function extractField(lines, key) {
68
+ for (const line of lines) {
69
+ const match = line.match(new RegExp(`^${key}\\s*:\\s*(.+)$`));
70
+ if (match) {
71
+ // Strip surrounding quotes if present
72
+ return match[1].replace(/^["']|["']$/g, '').trim();
73
+ }
74
+ }
75
+ return null;
76
+ }
77
+
78
+ /**
79
+ * Convert a single RRR agent markdown file (Claude Code format) into
80
+ * a Codex-compatible TOML agent config string.
81
+ *
82
+ * @param {string} sourceMarkdown - Raw content of an agents/rrr-*.md file
83
+ * @param {string} filename - Filename (e.g. "rrr-executor.md")
84
+ * @returns {string} Generated TOML string for ~/.codex/agents/<basename>.toml
85
+ */
86
+ function generateCodexAgentToml(sourceMarkdown, filename) {
87
+ const { frontmatterLines } = parseFrontmatter(sourceMarkdown);
88
+
89
+ // Extract fields from frontmatter
90
+ const nameField = extractField(frontmatterLines, 'name');
91
+ const descriptionField = extractField(frontmatterLines, 'description');
92
+ const modelField = extractField(frontmatterLines, 'model');
93
+
94
+ // Derive agent name: use name: field if present, else strip .md from filename
95
+ const agentName = nameField || path.basename(filename, '.md');
96
+
97
+ // Look up tier mapping; default to STANDARD (sonnet) if model not found
98
+ const tierEntry = TIER_MAP[modelField] || TIER_MAP.sonnet;
99
+
100
+ // Build TOML lines — only fields Codex CLI accepts
101
+ const lines = [
102
+ '# Generated by projecta-rrr — do not edit manually',
103
+ `# Source: agents/${filename}`,
104
+ '',
105
+ `name = "${agentName}"`,
106
+ `description = "${descriptionField || agentName}"`,
107
+ `model = "${tierEntry.model}"`,
108
+ ];
109
+
110
+ return lines.join('\n') + '\n';
111
+ }
112
+
113
+ /**
114
+ * Install all RRR agent TOML config files to a target directory.
115
+ *
116
+ * Reads all .md files from sourceDir (agents/), generates a .toml config
117
+ * for each via generateCodexAgentToml(), and writes to targetDir as
118
+ * <basename>.toml (e.g. rrr-executor.md → rrr-executor.toml).
119
+ *
120
+ * @param {object} options
121
+ * @param {string} options.sourceDir - Absolute path to agents/ directory
122
+ * @param {string} options.targetDir - Absolute path to target directory (e.g. ~/.codex/agents/)
123
+ * @param {boolean} [options.dryRun=false] - If true, compute paths but skip writing
124
+ * @param {boolean} [options.verbose=false] - If true, log each file written
125
+ * @returns {{ written: string[], skipped: string[], errors: Array<{file: string, error: string}> }}
126
+ */
127
+ function installCodexAgents(options) {
128
+ const {
129
+ sourceDir,
130
+ targetDir,
131
+ dryRun = false,
132
+ verbose = false,
133
+ } = options;
134
+
135
+ const written = [];
136
+ const skipped = [];
137
+ const errors = [];
138
+
139
+ // Ensure target directory exists (no-op if already exists)
140
+ if (!dryRun) {
141
+ fs.mkdirSync(targetDir, { recursive: true });
142
+ }
143
+
144
+ // Read all files in sourceDir
145
+ let entries;
146
+ try {
147
+ entries = fs.readdirSync(sourceDir);
148
+ } catch (err) {
149
+ errors.push({ file: sourceDir, error: `Cannot read sourceDir: ${err.message}` });
150
+ return { written, skipped, errors };
151
+ }
152
+
153
+ for (const entry of entries) {
154
+ // Only process .md files
155
+ if (!entry.endsWith('.md')) {
156
+ skipped.push(entry);
157
+ continue;
158
+ }
159
+
160
+ const sourcePath = path.join(sourceDir, entry);
161
+ // Output filename: <basename>.toml (source files are already rrr-*.md)
162
+ const basename = path.basename(entry, '.md');
163
+ const outputFilename = `${basename}.toml`;
164
+ const targetPath = path.join(targetDir, outputFilename);
165
+
166
+ try {
167
+ const source = fs.readFileSync(sourcePath, 'utf8');
168
+ const toml = generateCodexAgentToml(source, entry);
169
+
170
+ if (!dryRun) {
171
+ fs.writeFileSync(targetPath, toml, 'utf8');
172
+ }
173
+
174
+ if (verbose) {
175
+ console.log(` ${dryRun ? '[dry-run] would write' : 'wrote'}: ${outputFilename}`);
176
+ }
177
+
178
+ written.push(outputFilename);
179
+ } catch (err) {
180
+ errors.push({ file: entry, error: err.message });
181
+ }
182
+ }
183
+
184
+ return { written, skipped, errors };
185
+ }
186
+
187
+ module.exports = {
188
+ generateCodexAgentToml,
189
+ installCodexAgents,
190
+ TIER_MAP,
191
+ };
@@ -0,0 +1,286 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * Fields in YAML frontmatter that are Claude Code-specific and must be stripped
8
+ * when generating a Codex-compatible skill.
9
+ */
10
+ const CLAUDE_ONLY_FIELDS = new Set([
11
+ 'model',
12
+ 'allowed_tools',
13
+ 'allowed-tools',
14
+ 'agent',
15
+ 'argument-hint',
16
+ ]);
17
+
18
+ /**
19
+ * Parse the YAML frontmatter from a markdown string.
20
+ *
21
+ * Supports files that start with a `---` block.
22
+ *
23
+ * Returns { frontmatterLines: string[], body: string } where:
24
+ * - frontmatterLines: raw key-value lines from the YAML block (no delimiters)
25
+ * - body: everything after the closing `---` delimiter (may start with \n)
26
+ *
27
+ * If there is no frontmatter, frontmatterLines is [] and body is the full content.
28
+ */
29
+ function parseFrontmatter(source) {
30
+ // Must begin with ---\n
31
+ if (!source.startsWith('---\n') && !source.startsWith('---\r\n')) {
32
+ return { frontmatterLines: [], body: source };
33
+ }
34
+
35
+ // Find the closing --- delimiter (must be on its own line)
36
+ const afterOpen = source.slice(4); // skip the opening "---\n"
37
+ const closingMatch = afterOpen.match(/^---[ \t]*(\r?\n|$)/m);
38
+ if (!closingMatch) {
39
+ // No closing delimiter — treat entire file as body
40
+ return { frontmatterLines: [], body: source };
41
+ }
42
+
43
+ const closingIndex = closingMatch.index;
44
+ const frontmatterText = afterOpen.slice(0, closingIndex);
45
+ const body = afterOpen.slice(closingIndex + closingMatch[0].length);
46
+
47
+ // Split into lines for individual field processing
48
+ const frontmatterLines = frontmatterText.split('\n');
49
+ // Remove trailing empty line if present
50
+ if (frontmatterLines[frontmatterLines.length - 1] === '') {
51
+ frontmatterLines.pop();
52
+ }
53
+
54
+ return { frontmatterLines, body };
55
+ }
56
+
57
+ /**
58
+ * Strip Claude-specific fields from parsed frontmatter lines.
59
+ *
60
+ * Handles multi-line values (indented continuation lines) for fields like:
61
+ * allowed-tools:
62
+ * - Read
63
+ * - Write
64
+ *
65
+ * Returns filtered lines (no delimiters).
66
+ */
67
+ function stripClaudeFields(lines) {
68
+ const result = [];
69
+ let skipping = false;
70
+
71
+ for (const line of lines) {
72
+ // Detect a top-level YAML key (no leading spaces/tabs before the key)
73
+ const keyMatch = line.match(/^([a-zA-Z_-][a-zA-Z0-9_-]*)\s*:/);
74
+ if (keyMatch) {
75
+ const key = keyMatch[1];
76
+ if (CLAUDE_ONLY_FIELDS.has(key)) {
77
+ skipping = true;
78
+ continue;
79
+ } else {
80
+ skipping = false;
81
+ }
82
+ } else if (skipping) {
83
+ // Check if this is a continuation/value line (indented or starts with -)
84
+ // If the line is non-empty and starts with whitespace or is a list item,
85
+ // it belongs to the field being skipped.
86
+ if (/^[ \t]/.test(line) || line === '') {
87
+ continue;
88
+ }
89
+ // Otherwise it's a new top-level key that wasn't caught above — stop skipping
90
+ skipping = false;
91
+ }
92
+
93
+ if (!skipping) {
94
+ result.push(line);
95
+ }
96
+ }
97
+
98
+ return result;
99
+ }
100
+
101
+ /**
102
+ * Derive the skill name from a filename.
103
+ *
104
+ * e.g. "plan-phase.md" → "rrr-plan-phase"
105
+ * "execute-phase.md" → "rrr-execute-phase"
106
+ */
107
+ function deriveSkillName(filename) {
108
+ const basename = path.basename(filename, '.md');
109
+ return `rrr-${basename}`;
110
+ }
111
+
112
+ /**
113
+ * Extract a specific field value from frontmatter lines.
114
+ * Only looks at simple scalar values on the same line as the key.
115
+ * Returns null if the key is not found.
116
+ */
117
+ function extractField(lines, key) {
118
+ for (const line of lines) {
119
+ const match = line.match(new RegExp(`^${key}\\s*:\\s*(.+)$`));
120
+ if (match) {
121
+ // Strip surrounding quotes if present
122
+ return match[1].replace(/^["']|["']$/g, '').trim();
123
+ }
124
+ }
125
+ return null;
126
+ }
127
+
128
+ /**
129
+ * Serialize filtered frontmatter lines back to a YAML block with delimiters.
130
+ *
131
+ * Ensures the output frontmatter always contains at minimum:
132
+ * name: <derived>
133
+ * description: <from source or generated>
134
+ */
135
+ 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
+ }
150
+
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
+ }
158
+
159
+ return `---\n${outputLines.join('\n')}\n---\n`;
160
+ }
161
+
162
+ /**
163
+ * Apply path reference and trigger text conversions to the skill body.
164
+ *
165
+ * Conversions:
166
+ * 1. `~/.claude/` → `~/.codex/`
167
+ * 2. `.claude/` when preceded by non-word/non-slash → `.codex/`
168
+ * 3. `/rrr:` → `$rrr-`
169
+ */
170
+ function transformBody(body) {
171
+ // 1. ~/.claude/ → ~/.codex/
172
+ let result = body.replace(/~\/\.claude\//g, '~/.codex/');
173
+
174
+ // 2. .claude/ when NOT preceded by a word character or slash
175
+ // Use a regex that matches the context prefix so we can reconstruct it.
176
+ // Matches: start-of-string, whitespace, or punctuation that isn't a word char or slash.
177
+ result = result.replace(/(^|[^a-zA-Z0-9/_])\.claude\//gm, '$1.codex/');
178
+
179
+ // 3. /rrr: → $rrr-
180
+ result = result.replace(/\/rrr:/g, '$rrr-');
181
+
182
+ return result;
183
+ }
184
+
185
+ /**
186
+ * Convert a single RRR command markdown file (Claude Code format) into
187
+ * a Codex-compatible skill markdown string.
188
+ *
189
+ * @param {string} sourceMarkdown - Raw content of a commands/rrr/*.md file
190
+ * @param {string} filename - Filename (e.g. "plan-phase.md")
191
+ * @returns {string} Transformed skill markdown
192
+ */
193
+ function convertRRRCommandToCodexSkill(sourceMarkdown, filename) {
194
+ const { frontmatterLines, body } = parseFrontmatter(sourceMarkdown);
195
+
196
+ const skillName = deriveSkillName(filename);
197
+
198
+ // Extract description before stripping fields (it should survive, but grab it now)
199
+ const sourceDescription = extractField(frontmatterLines, 'description');
200
+
201
+ // Strip Claude-specific fields
202
+ const filteredLines = stripClaudeFields(frontmatterLines);
203
+
204
+ // Serialize with required fields guaranteed
205
+ const newFrontmatter = serializeFrontmatter(filteredLines, skillName, sourceDescription);
206
+
207
+ // Transform body (path refs + trigger text)
208
+ const newBody = transformBody(body);
209
+
210
+ return `${newFrontmatter}${newBody}`;
211
+ }
212
+
213
+ /**
214
+ * Install all RRR command skills as Codex-compatible skill files to a target directory.
215
+ *
216
+ * @param {object} options
217
+ * @param {string} options.sourceDir - Absolute path to commands/rrr/
218
+ * @param {string} options.targetDir - Absolute path to target directory (e.g. ~/.codex/skills/)
219
+ * @param {boolean} [options.dryRun=false] - If true, compute paths but skip writing
220
+ * @param {boolean} [options.verbose=false] - If true, log each file written
221
+ * @returns {{ written: string[], skipped: string[], errors: Array<{file: string, error: string}> }}
222
+ */
223
+ function installCodexSkills(options) {
224
+ const {
225
+ sourceDir,
226
+ targetDir,
227
+ dryRun = false,
228
+ verbose = false,
229
+ } = options;
230
+
231
+ const written = [];
232
+ const skipped = [];
233
+ const errors = [];
234
+
235
+ // Ensure target directory exists (no-op if already exists)
236
+ if (!dryRun) {
237
+ fs.mkdirSync(targetDir, { recursive: true });
238
+ }
239
+
240
+ // Read all files in sourceDir
241
+ let entries;
242
+ try {
243
+ entries = fs.readdirSync(sourceDir);
244
+ } catch (err) {
245
+ errors.push({ file: sourceDir, error: `Cannot read sourceDir: ${err.message}` });
246
+ return { written, skipped, errors };
247
+ }
248
+
249
+ for (const entry of entries) {
250
+ // Only process .md files
251
+ if (!entry.endsWith('.md')) {
252
+ skipped.push(entry);
253
+ continue;
254
+ }
255
+
256
+ const sourcePath = path.join(sourceDir, entry);
257
+ // Output filename: rrr-{basename}.md (e.g. plan-phase.md → rrr-plan-phase.md)
258
+ const basename = path.basename(entry, '.md');
259
+ const outputFilename = `rrr-${basename}.md`;
260
+ const targetPath = path.join(targetDir, outputFilename);
261
+
262
+ try {
263
+ const source = fs.readFileSync(sourcePath, 'utf8');
264
+ const transformed = convertRRRCommandToCodexSkill(source, entry);
265
+
266
+ if (!dryRun) {
267
+ fs.writeFileSync(targetPath, transformed, 'utf8');
268
+ }
269
+
270
+ if (verbose) {
271
+ console.log(` ${dryRun ? '[dry-run] would write' : 'wrote'}: ${outputFilename}`);
272
+ }
273
+
274
+ written.push(outputFilename);
275
+ } catch (err) {
276
+ errors.push({ file: entry, error: err.message });
277
+ }
278
+ }
279
+
280
+ return { written, skipped, errors };
281
+ }
282
+
283
+ module.exports = {
284
+ convertRRRCommandToCodexSkill,
285
+ installCodexSkills,
286
+ };
@@ -0,0 +1,146 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * gen-agents-md.js — Generate AGENTS.md for RRR project root.
5
+ *
6
+ * Reads every commands/rrr/*.md filename, derives the $rrr-{stem} trigger
7
+ * phrase, and writes a fresh AGENTS.md to the project root.
8
+ *
9
+ * Usage:
10
+ * node scripts/gen-agents-md.js
11
+ *
12
+ * DO NOT hand-edit AGENTS.md — regenerate with:
13
+ * node scripts/gen-agents-md.js
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+
19
+ /**
20
+ * Build the full AGENTS.md content string from a commandsDir.
21
+ *
22
+ * @param {string} commandsDir - Absolute path to commands/rrr/ directory
23
+ * @returns {{ content: string, skillCount: number }} Generated content and skill count
24
+ */
25
+ function buildContent(commandsDir) {
26
+ // Read all .md files, sort alphabetically
27
+ const allFiles = fs.readdirSync(commandsDir);
28
+ const mdFiles = allFiles.filter(f => f.endsWith('.md')).sort();
29
+
30
+ const skills = mdFiles.map(filename => {
31
+ const stem = path.basename(filename, '.md');
32
+ const filePath = path.join(commandsDir, filename);
33
+
34
+ let description = `Run ${stem} workflow`;
35
+ try {
36
+ const content = fs.readFileSync(filePath, 'utf8');
37
+ const descMatch = content.match(/^description:\s*(.+)$/m);
38
+ if (descMatch) {
39
+ description = descMatch[1].trim();
40
+ // Strip surrounding quotes if present
41
+ description = description.replace(/^["']|["']$/g, '').trim();
42
+ }
43
+ } catch (err) {
44
+ // Use default description on read error
45
+ }
46
+
47
+ return { stem, trigger: `$rrr-${stem}`, description };
48
+ });
49
+
50
+ const skillCount = skills.length;
51
+
52
+ // Build trigger table rows
53
+ const tableRows = skills
54
+ .map(s => `| \`${s.trigger}\` | ${s.description} |`)
55
+ .join('\n');
56
+
57
+ const timestamp = new Date().toISOString();
58
+
59
+ const content = [
60
+ `<!-- auto-generated by scripts/gen-agents-md.js — do NOT hand-edit; regenerate with: node scripts/gen-agents-md.js -->`,
61
+ ``,
62
+ `# RRR — AI Workflow for Codex`,
63
+ ``,
64
+ `RRR is a structured planning and execution workflow that reduces cognitive load while maintaining quality across brownfield projects. Codex invokes RRR capabilities via \`$rrr-*\` trigger phrases, each corresponding to a specific workflow skill.`,
65
+ ``,
66
+ `## Invocation`,
67
+ ``,
68
+ `Use \`$rrr-{skill-name}\` syntax to invoke a skill. Examples:`,
69
+ ``,
70
+ `- \`$rrr-plan-phase\` — create a detailed execution plan for a phase`,
71
+ `- \`$rrr-execute-phase\` — run all plans in a phase with wave-based parallelization`,
72
+ `- \`$rrr-next\` — see the recommended next action`,
73
+ `- \`$rrr-help\` — show full skill catalogue`,
74
+ ``,
75
+ `Trigger phrases are case-sensitive: use \`$rrr-plan-phase\` not \`$rrr-PlanPhase\`.`,
76
+ ``,
77
+ `## Core Workflow`,
78
+ ``,
79
+ `The RRR loop follows five steps:`,
80
+ ``,
81
+ `1. **Discuss** — Align on goals, scope, and approach`,
82
+ `2. **Plan** — Generate structured PLAN.md files with tasks and verification`,
83
+ `3. **Execute** — Run plans atomically, one task per commit`,
84
+ `4. **Verify** — Confirm output matches success criteria`,
85
+ `5. **Complete** — Archive milestone, update STATE.md, tag release`,
86
+ ``,
87
+ `## Available Skills`,
88
+ ``,
89
+ `| Trigger | Description |`,
90
+ `| ------- | ----------- |`,
91
+ tableRows,
92
+ ``,
93
+ `## Key Conventions`,
94
+ ``,
95
+ `- Always check \`.planning/STATE.md\` for current project position before invoking a workflow skill`,
96
+ `- Use \`$rrr-next\` to see recommended next action`,
97
+ `- Use \`$rrr-help\` for full skill catalogue`,
98
+ `- Trigger phrases are case-sensitive: \`$rrr-plan-phase\` not \`$rrr-PlanPhase\``,
99
+ ``,
100
+ `<!-- generated: ${timestamp} | source: commands/rrr/*.md | count: ${skillCount} skills -->`,
101
+ ].join('\n');
102
+
103
+ return { content, skillCount };
104
+ }
105
+
106
+ /**
107
+ * Generate AGENTS.md and write it to outputPath.
108
+ *
109
+ * @param {object} options
110
+ * @param {string} options.commandsDir - Absolute path to commands/rrr/ directory
111
+ * @param {string} options.outputPath - Absolute path to output AGENTS.md
112
+ * @returns {{ written: string, skillCount: number }}
113
+ */
114
+ function generateAgentsMd({ commandsDir, outputPath }) {
115
+ const { content, skillCount } = buildContent(commandsDir);
116
+ fs.writeFileSync(outputPath, content, 'utf8');
117
+ return { written: content, skillCount };
118
+ }
119
+
120
+ /**
121
+ * Return expected AGENTS.md content WITHOUT writing to disk.
122
+ * Used by prepublish-check.js to verify the committed file is in sync.
123
+ *
124
+ * Note: The timestamp in the footer will differ between calls. For sync
125
+ * checking, compare everything except the generated: timestamp line.
126
+ *
127
+ * @param {object} options
128
+ * @param {string} options.commandsDir - Absolute path to commands/rrr/ directory
129
+ * @returns {string} Expected content string
130
+ */
131
+ function getExpectedContent({ commandsDir }) {
132
+ const { content } = buildContent(commandsDir);
133
+ return content;
134
+ }
135
+
136
+ module.exports = { generateAgentsMd, getExpectedContent };
137
+
138
+ // CLI entry point
139
+ if (require.main === module) {
140
+ const root = path.join(__dirname, '..');
141
+ generateAgentsMd({
142
+ commandsDir: path.join(root, 'commands', 'rrr'),
143
+ outputPath: path.join(root, 'AGENTS.md'),
144
+ });
145
+ console.log('AGENTS.md generated at project root');
146
+ }
@@ -346,6 +346,33 @@ try {
346
346
  check('Install smoke v1.20 parity', false, `Could not run: ${e.message}`);
347
347
  }
348
348
 
349
+ // 16. AGENTS.md sync check (SYNC-02, Phase 89)
350
+ console.log(`\n${cyan}16. AGENTS.md Sync${reset}`);
351
+ try {
352
+ const genAgentsMd = require('./gen-agents-md.js');
353
+ const root = path.join(__dirname, '..');
354
+ const commandsDir = path.join(root, 'commands', 'rrr');
355
+ const agentsMdPath = path.join(root, 'AGENTS.md');
356
+
357
+ if (!fs.existsSync(agentsMdPath)) {
358
+ check('AGENTS.md exists', false, 'Run: node scripts/gen-agents-md.js');
359
+ } else {
360
+ const onDisk = fs.readFileSync(agentsMdPath, 'utf8');
361
+ const expected = genAgentsMd.getExpectedContent({ commandsDir });
362
+
363
+ // Compare by normalizing the timestamp footer line (it changes on every run)
364
+ // Strip the <!-- generated: ... --> line before comparing
365
+ const normalize = s => s.replace(/<!--\s*generated:.*?-->/g, '').trim();
366
+ const diskNorm = normalize(onDisk);
367
+ const expectedNorm = normalize(expected);
368
+
369
+ check('AGENTS.md in sync with commands/rrr/*.md', diskNorm === expectedNorm,
370
+ 'AGENTS.md is out of sync. Run: node scripts/gen-agents-md.js');
371
+ }
372
+ } catch (e) {
373
+ check('AGENTS.md sync check', false, `Could not run: ${e.message}`);
374
+ }
375
+
349
376
  // Summary
350
377
  console.log(`\n${cyan}━━━ Summary ━━━${reset}\n`);
351
378
  if (hasErrors) {