@hanzlaa/rcode 4.1.1 → 4.3.0

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 (105) hide show
  1. package/AGENTS.md +1 -1
  2. package/CONTRIBUTING.md +3 -0
  3. package/README.md +3 -0
  4. package/cli/agent.js +3 -1
  5. package/cli/index.js +29 -0
  6. package/cli/install.js +233 -15
  7. package/cli/lib/config.cjs +4 -2
  8. package/cli/lib/fsutil.cjs +13 -2
  9. package/cli/lib/homedir.cjs +21 -0
  10. package/cli/lib/schemas.cjs +6 -1
  11. package/cli/nuke.js +13 -8
  12. package/cli/postinstall.js +14 -4
  13. package/cli/rcode-slash-router.cjs +118 -0
  14. package/cli/uninstall.js +59 -1
  15. package/cli/update.js +10 -5
  16. package/cli/workflow.js +3 -1
  17. package/dist/rcode.js +241 -227
  18. package/package.json +1 -1
  19. package/rcode/bin/rcode-tools.cjs +15 -6
  20. package/rcode/commands/scaffold-project.md +2 -2
  21. package/rcode/skills/actions/2-plan/rcode-create-epics-and-stories/steps/step-04-final-validation.md +1 -1
  22. package/rcode/skills/actions/2-plan/rcode-create-milestone/steps/README.md +2 -2
  23. package/rcode/skills/actions/2-plan/rcode-create-milestone/steps/step-09-state-sync.md +1 -1
  24. package/rcode/skills/actions/4-implementation/rcode-code-review/steps/step-02-review.md +1 -1
  25. package/rcode/skills/actions/4-implementation/rcode-git-flow/SKILL.md +1 -1
  26. package/rcode/skills/actions/4-implementation/rcode-scaffold-project/SKILL.md +39 -12
  27. package/rcode/skills/actions/4-implementation/rcode-scaffold-project/steps/step-01-target.md +18 -3
  28. package/rcode/skills/actions/4-implementation/rcode-scaffold-project/steps/step-02-safety.md +27 -3
  29. package/rcode/skills/actions/4-implementation/rcode-scaffold-project/steps/step-03-brownfield.md +57 -0
  30. package/rcode/skills/actions/4-implementation/rcode-scaffold-project/steps/step-03-clone.md +4 -1
  31. package/rcode/skills/actions/4-implementation/rcode-scaffold-project/steps/step-04-post-setup.md +15 -1
  32. package/rcode/skills/actions/4-implementation/rcode-trim/SKILL.md +1 -1
  33. package/rcode/workflows/audit-milestone.md +1 -1
  34. package/rcode/workflows/discuss-phase.md +1 -1
  35. package/rcode/workflows/execute-milestone.md +1 -1
  36. package/rcode/workflows/execute-regression-gates.md +3 -0
  37. package/rcode/workflows/execute-sprint.md +27 -1
  38. package/rcode/workflows/execute-waves.md +6 -0
  39. package/rcode/workflows/execute.md +13 -3
  40. package/rcode/workflows/new-milestone.md +2 -2
  41. package/rcode/workflows/new-project.md +4 -0
  42. package/rcode/workflows/plan-research-validation.md +1 -1
  43. package/rcode/workflows/plan-spawn-planner.md +2 -2
  44. package/rcode/workflows/plan.md +34 -15
  45. package/rcode/workflows/review.md +2 -0
  46. package/rcode/workflows/scaffold-project.md +5 -1
  47. package/rcode/workflows/session-report.md +1 -1
  48. package/rcode/workflows/ship.md +39 -0
  49. package/rcode/workflows/sprint-planning.md +27 -0
  50. package/rcode/workflows/status.md +3 -3
  51. package/server/dashboard.js +26 -7
  52. package/server/lib/api.js +62 -4
  53. package/server/lib/html/client/agents-data.js +22 -18
  54. package/server/lib/html/client/app.js +3 -0
  55. package/server/lib/html/client/components/AgentCard.js +127 -0
  56. package/server/lib/html/client/components/App.js +104 -39
  57. package/server/lib/html/client/components/CommandPalette.js +133 -0
  58. package/server/lib/html/client/components/FileReader.js +116 -0
  59. package/server/lib/html/client/components/FilterChips.js +94 -0
  60. package/server/lib/html/client/components/NotifyCenter.js +117 -0
  61. package/server/lib/html/client/components/OrchPanel.js +80 -52
  62. package/server/lib/html/client/components/PhaseGraph.js +300 -0
  63. package/server/lib/html/client/components/RejectDialog.js +78 -0
  64. package/server/lib/html/client/components/RunnerPicker.js +190 -0
  65. package/server/lib/html/client/components/Sidebar.js +106 -61
  66. package/server/lib/html/client/components/StatusSummaryBar.js +76 -0
  67. package/server/lib/html/client/components/TaskPipeline.js +83 -0
  68. package/server/lib/html/client/components/Topbar.js +86 -39
  69. package/server/lib/html/client/components/dashboard/Blockers.js +57 -0
  70. package/server/lib/html/client/components/dashboard/CompletedTasks.js +47 -0
  71. package/server/lib/html/client/components/dashboard/CurrentPhase.js +107 -0
  72. package/server/lib/html/client/components/dashboard/InProgress.js +72 -0
  73. package/server/lib/html/client/components/dashboard/ProgressDonut.js +101 -0
  74. package/server/lib/html/client/components/dashboard/ProgressTimeline.js +101 -0
  75. package/server/lib/html/client/components/dashboard/ProjectHealth.js +80 -0
  76. package/server/lib/html/client/components/dashboard/RecentDecisions.js +57 -0
  77. package/server/lib/html/client/components/dashboard/Timeline.js +143 -0
  78. package/server/lib/html/client/components/shared.js +47 -11
  79. package/server/lib/html/client/filter-state.js +72 -0
  80. package/server/lib/html/client/icons-client.js +7 -0
  81. package/server/lib/html/client/notify.js +75 -0
  82. package/server/lib/html/client/orchestrator.js +168 -41
  83. package/server/lib/html/client/preact.js +13 -8
  84. package/server/lib/html/client/store.js +70 -6
  85. package/server/lib/html/client/util.js +78 -0
  86. package/server/lib/html/client/vendor/htm.js +1 -0
  87. package/server/lib/html/client/vendor/preact-hooks.js +2 -0
  88. package/server/lib/html/client/vendor/preact.js +2 -0
  89. package/server/lib/html/client/views/AgentsView.js +144 -51
  90. package/server/lib/html/client/views/FilesView.js +20 -103
  91. package/server/lib/html/client/views/KanbanView.js +40 -21
  92. package/server/lib/html/client/views/MemoryView.js +26 -9
  93. package/server/lib/html/client/views/MilestonesView.js +4 -4
  94. package/server/lib/html/client/views/OrchestrationView.js +154 -19
  95. package/server/lib/html/client/views/OverviewView.js +47 -239
  96. package/server/lib/html/client/views/PhasesView.js +50 -6
  97. package/server/lib/html/client/views/RoadmapView.js +6 -3
  98. package/server/lib/html/client/views/SprintsView.js +50 -6
  99. package/server/lib/html/client/views/TasksView.js +4 -3
  100. package/server/lib/html/client.js +21 -4
  101. package/server/lib/html/css.js +2761 -8
  102. package/server/lib/html/icons.js +7 -0
  103. package/server/lib/html/shell.js +10 -3
  104. package/server/lib/scanner.js +376 -39
  105. package/server/orchestrator.js +329 -5
package/AGENTS.md CHANGED
@@ -24,7 +24,7 @@ If a user says "just keep going" or "don't stop until done", that authorization
24
24
 
25
25
  - Follow [Conventional Commits](https://www.conventionalcommits.org/) format: `type(scope): subject`
26
26
  - Types allowed: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`, `perf`, `revert`
27
- - Scopes allowed: `agents`, `skills`, `workflows`, `templates`, `dashboard`, `docs`, `config`, `github`, `commands`, `memory`, `brand`, `cli`, `ci`, `release`, `meta`, `tasks`, `migrations`, `refs`, `state`, `hooks`, `install`, `parity`, `triggers`, `dogfood`, `namespace`, `planning`, `insights`, `help`, `roadmap`, `session`, `audits`, `execute`, `executor`, `plan`, `planner`, `readme`, `sync`, `sprint`, `agent-exp`, `extensibility`, `lens-audit`, `tiers`, `build`, `council`, `doctor`, `postinstall`, `progress`, `security`, `tools`, `uninstall`, `update`, `test`, `changelog`, `scopes`, `phases`, `references`, `kanban`, `orchestrator`, `orchpanel`, `status`, `bin`, `brain`, `dogfeed`, `new-project`, `package`, `rcode-tools`, `rihal-tools`, `team`, `usp`, `v4`, `observability`, `audit`, `agent-rules`, `cursor`, `i18n`, `phase`, plus numeric phase/sprint scopes (e.g. `docs(15)`, `feat(8.3)`)
27
+ - Scopes allowed: `agents`, `skills`, `workflows`, `templates`, `dashboard`, `docs`, `config`, `github`, `commands`, `memory`, `brand`, `cli`, `ci`, `release`, `meta`, `tasks`, `migrations`, `refs`, `state`, `hooks`, `install`, `parity`, `triggers`, `dogfood`, `namespace`, `planning`, `insights`, `help`, `roadmap`, `session`, `audits`, `execute`, `executor`, `plan`, `planner`, `readme`, `sync`, `sprint`, `agent-exp`, `extensibility`, `lens-audit`, `tiers`, `build`, `council`, `doctor`, `postinstall`, `progress`, `security`, `tools`, `uninstall`, `update`, `test`, `changelog`, `scopes`, `phases`, `references`, `kanban`, `orchestrator`, `orchpanel`, `status`, `bin`, `brain`, `dogfeed`, `new-project`, `package`, `rcode-tools`, `rihal-tools`, `team`, `usp`, `v4`, `observability`, `audit`, `agent-rules`, `cursor`, `i18n`, `phase`, `scaffold`, `campaign`, `ship`, plus numeric phase/sprint scopes (e.g. `docs(15)`, `feat(8.3)`)
28
28
  - Subject: lowercase first letter, imperative mood, no trailing period, under 72 chars
29
29
  - **NEVER add Claude/AI attribution to commit messages.** No "Generated with Claude Code", no "Co-Authored-By: Claude", no "🤖 Generated". The user does not want this.
30
30
  - **NEVER use `--no-verify`** to bypass hooks. If hooks fail, fix the underlying issue.
package/CONTRIBUTING.md CHANGED
@@ -349,6 +349,9 @@ We use [Conventional Commits](https://www.conventionalcommits.org/) format. The
349
349
  - `cursor` — Cursor IDE rules under `.cursor/rules/rcode/`
350
350
  - `i18n` — internationalization, brand strings, and user-visible copy
351
351
  - `phase` — phase-lifecycle changes (variable renames, phase-loop fixes); use numeric `<phase-id>` for in-phase commits
352
+ - `scaffold` — scaffold-project workflow changes
353
+ - `campaign` — fix-campaign orchestration tracking files
354
+ - `ship` — ship workflow changes
352
355
  - `<phase-id>` — numeric phase scope when committing inside a phase (e.g. `docs(15)`, `feat(8.3)`)
353
356
  - `<sprint-id>` — numeric sprint scope inside a phase (e.g. `feat(15.1)`)
354
357
 
package/README.md CHANGED
@@ -117,6 +117,9 @@ pnpm dlx @hanzlaa/rcode install
117
117
  /rcode-init
118
118
  ```
119
119
 
120
+ > **Don't have pnpm?** On Node 18+ / npm 11.x we recommend pnpm to avoid `npx` cache issues:
121
+ > `npm install -g pnpm`
122
+
120
123
  `/rcode-init` detects your project state (fresh / existing / returning) and routes to the right first action. For a greenfield project it auto-routes to `/rcode-new-project`.
121
124
 
122
125
  ### The full loop
package/cli/agent.js CHANGED
@@ -24,7 +24,9 @@ module.exports = function agent(args, { packageRoot }) {
24
24
  }
25
25
 
26
26
  const name = args[0];
27
- const agentName = `rcode-${name}`;
27
+ // Strip rcode- prefix if already provided so rcode agent rcode-executor works (#882)
28
+ const bare = name.startsWith('rcode-') ? name.slice('rcode-'.length) : name;
29
+ const agentName = `rcode-${bare}`;
28
30
 
29
31
  // Validate agent file exists
30
32
  const agentFile = path.join(agentDir, `${agentName}.md`);
package/cli/index.js CHANGED
@@ -34,6 +34,10 @@ const COMMANDS = {
34
34
  agent: require('./agent'),
35
35
  doctor: require('./doctor'),
36
36
  workflow: require('./workflow'), // lifecycle bridge for non-Claude runtimes
37
+ // Thin lifecycle aliases — delegate to workflow show <name> (#883)
38
+ plan: (args, ctx) => lifecycleAlias('plan', args, ctx),
39
+ execute: (args, ctx) => lifecycleAlias('execute-sprint', args, ctx),
40
+ ship: (args, ctx) => lifecycleAlias('ship', args, ctx),
37
41
  'set-profile': require('./set-profile'),
38
42
  'set-mode': require('./set-mode'),
39
43
  config: require('./config'),
@@ -70,6 +74,9 @@ Usage:
70
74
  github-sync Sync .rcode/ phases/epics/stories to GitHub (dry-run default)
71
75
 
72
76
  🔄 LIFECYCLE (Codex / Copilot / Grok bridge)
77
+ plan Print the plan workflow (alias for workflow show plan)
78
+ execute Print the execute-sprint workflow
79
+ ship Print the ship workflow
73
80
  workflow list List all lifecycle workflow names
74
81
  workflow show <name> Print a workflow's full instructions to stdout
75
82
  workflow show new-project → project setup + ROADMAP
@@ -82,6 +89,7 @@ Usage:
82
89
  workflow show ship → deploy / release workflow
83
90
 
84
91
  Non-Claude agents: pipe to your agent instead of using slash commands.
92
+ Example: rcode plan | codex run -
85
93
  Example: rcode workflow show plan | codex run -
86
94
 
87
95
  👥 TEAM
@@ -91,6 +99,7 @@ Usage:
91
99
  rcode agent --list to see available agents
92
100
  show-model Show which model each agent uses in the current profile
93
101
  dashboard Start the Diwan view-only dashboard (port 7717)
102
+ Starts a view-only dashboard at http://localhost:7717. No write access.
94
103
  serve Alias for dashboard
95
104
 
96
105
  ⚙️ META
@@ -111,6 +120,26 @@ Documentation: https://github.com/hanzlahabib/rihal-code
111
120
  `.trim());
112
121
  }
113
122
 
123
+ /**
124
+ * Lifecycle aliases (plan/execute/ship): show the workflow then print actionable
125
+ * next-step guidance so the user knows how to actually run it.
126
+ */
127
+ function lifecycleAlias(workflowName, args, ctx) {
128
+ require('./workflow')(['show', workflowName, ...args], ctx);
129
+
130
+ const hasAuto = args.includes('--auto') || args.includes('--run');
131
+
132
+ console.log('\n─────────────────────────────────────────────');
133
+ console.log(`▶ To run: paste the above into Claude Code as /${workflowName === 'execute-sprint' ? 'rcode-execute-sprint' : `rcode-${workflowName}`}`);
134
+ console.log(' or pipe it directly: rcode ' + (workflowName === 'execute-sprint' ? 'execute' : workflowName) + ' | cld --model sonnet');
135
+
136
+ if (hasAuto) {
137
+ console.log('\n AUTO mode detected — in Claude Code run: /rcode-' +
138
+ (workflowName === 'execute-sprint' ? 'execute-sprint' : workflowName) +
139
+ ' --auto (applies yolo defaults, skips confirmation prompts)');
140
+ }
141
+ }
142
+
114
143
  /**
115
144
  * npm 10+ suppresses postinstall script output during global installs, so users
116
145
  * who run `npm install -g @hanzlaa/rcode` see only "added 1 package" with no
package/cli/install.js CHANGED
@@ -59,6 +59,11 @@ const os = require('os');
59
59
  // Ctrl+C mid-write and malicious symlink-traversal during dedup/cleanup.
60
60
  const { writeFileAtomic, safeRmSync } = require('./lib/fsutil.cjs');
61
61
 
62
+ // HOME-aware home resolution (#889) — os.homedir() ignores a stubbed HOME on
63
+ // Windows (it reads USERPROFILE), so HOME-isolated tests and CI leaked global
64
+ // installs (~/.rcode, ~/.codex, ~/.gemini) into the real profile dir there.
65
+ const { homedir } = require('./lib/homedir.cjs');
66
+
62
67
  // Bundled packages — devDeps inlined by esbuild, loaded from node_modules in dev.
63
68
  const pc = require('picocolors');
64
69
  const { createSpinner } = require('nanospinner');
@@ -89,7 +94,7 @@ const SOURCE_ROOT = path.join(PACKAGE_ROOT, 'rcode');
89
94
  * detectIdeSignals, plus a row to runInstallWizard's multiselect — three
90
95
  * sites instead of ten.
91
96
  */
92
- const SUPPORTED_IDES = Object.freeze(['claude', 'cursor', 'gemini', 'vscode', 'antigravity', 'windsurf']);
97
+ const SUPPORTED_IDES = Object.freeze(['claude', 'cursor', 'gemini', 'vscode', 'antigravity', 'windsurf', 'codex', 'grok']);
93
98
 
94
99
  /**
95
100
  * Resolve the stable on-disk location of this package so config.yaml
@@ -197,6 +202,9 @@ function parseArgs(argv) {
197
202
  silent: false,
198
203
  // noPrompt — skip all interactive prompts (used by postinstall auto-run)
199
204
  noPrompt: false,
205
+ // dry-run / list-files — preview paths that would be written, then exit
206
+ dryRun: false,
207
+ listFiles: false,
200
208
  };
201
209
  const positional = [];
202
210
  for (let i = 0; i < argv.length; i++) {
@@ -231,6 +239,8 @@ function parseArgs(argv) {
231
239
  else if (arg === '--global') opts.global = true;
232
240
  else if (arg === '--silent') opts.silent = true;
233
241
  else if (arg === '--no-prompt') opts.noPrompt = true;
242
+ else if (arg === '--dry-run') opts.dryRun = true;
243
+ else if (arg === '--list-files') opts.listFiles = true;
234
244
  else if (!arg.startsWith('--')) positional.push(arg);
235
245
  }
236
246
  if (positional[0]) {
@@ -242,7 +252,7 @@ function parseArgs(argv) {
242
252
  // project directory wrote rcode artifacts to that project, not to the user's
243
253
  // home where Claude Code reads global commands from.
244
254
  if (opts.global && !opts.targetProvided) {
245
- opts.target = os.homedir();
255
+ opts.target = homedir();
246
256
  }
247
257
  // Issue #821/#832: pnpm workspace anchor.
248
258
  // When `pnpm add -D @hanzlaa/rcode` runs inside a workspace member,
@@ -372,7 +382,7 @@ function printInstallHeader(targetVersion) {
372
382
  * Returns a set like { claude: true, cursor: false, gemini: false }.
373
383
  */
374
384
  function detectIdeSignals(target) {
375
- const signals = { claude: false, cursor: false, gemini: false, vscode: false, antigravity: false, windsurf: false };
385
+ const signals = { claude: false, cursor: false, gemini: false, vscode: false, antigravity: false, windsurf: false, codex: false };
376
386
  // 1. Project-local install dirs (strongest signal — they already use one)
377
387
  if (fs.existsSync(path.join(target, '.claude'))) signals.claude = true;
378
388
  if (fs.existsSync(path.join(target, '.cursor'))) signals.cursor = true;
@@ -381,7 +391,7 @@ function detectIdeSignals(target) {
381
391
  if (fs.existsSync(path.join(target, '.antigravity'))) signals.antigravity = true;
382
392
  if (fs.existsSync(path.join(target, '.windsurf'))) signals.windsurf = true;
383
393
  // 2. User-level config dirs
384
- const home = os.homedir();
394
+ const home = homedir();
385
395
  if (fs.existsSync(path.join(home, '.claude'))) signals.claude = true;
386
396
  if (fs.existsSync(path.join(home, '.cursor'))) signals.cursor = true;
387
397
  if (fs.existsSync(path.join(home, '.config', 'Cursor'))) signals.cursor = true;
@@ -396,6 +406,7 @@ function detectIdeSignals(target) {
396
406
  if (process.env.CLAUDECODE === '1' || process.env.CLAUDE_CODE_ENTRYPOINT) signals.claude = true;
397
407
  if (process.env.VSCODE_PID || /vscode/i.test(process.env.TERM_PROGRAM || '')) signals.vscode = true;
398
408
  if (/windsurf/i.test(process.env.TERM_PROGRAM || '')) signals.windsurf = true;
409
+ if (process.env.CODEX_ENV || /codex/i.test(process.env.TERM_PROGRAM || '')) signals.codex = true;
399
410
  return signals;
400
411
  }
401
412
 
@@ -442,6 +453,7 @@ async function resolveIde(opts) {
442
453
  options: [
443
454
  { value: 'claude', label: 'Claude Code', hint: signals.claude ? '(detected)' : undefined },
444
455
  { value: 'cursor', label: 'Cursor', hint: signals.cursor ? '(detected)' : undefined },
456
+ { value: 'codex', label: 'Codex (OpenAI CLI)', hint: signals.codex ? '(detected)' : '(uses AGENTS.md + workflow bridge)' },
445
457
  { value: 'gemini', label: 'Gemini CLI', hint: signals.gemini ? '(detected)' : '(beta — limited)' },
446
458
  { value: 'vscode', label: 'VS Code', hint: signals.vscode ? '(detected)' : '(via Continue / Copilot extensions)' },
447
459
  { value: 'antigravity', label: 'Antigravity', hint: '(experimental — installs to .antigravity/)' },
@@ -522,6 +534,8 @@ Options:
522
534
  --language <lang> set communication_language (default: English)
523
535
  --mode <guided|yolo> default mode (default: guided)
524
536
  --ide <name> target IDE (claude, cursor, gemini; default: claude)
537
+ --dry-run preview what would be written; exit without writing any files
538
+ --list-files alias for --dry-run
525
539
  --help this text
526
540
 
527
541
  Installs (IDE-specific):
@@ -601,6 +615,33 @@ function getPathsForIde(ide, target) {
601
615
  referencesDir: path.join(target, '.rcode', 'references'),
602
616
  binDir: path.join(target, '.rcode', 'bin'),
603
617
  };
618
+ case 'codex':
619
+ // OpenAI Codex CLI reads AGENTS.md from the project root (written by the
620
+ // claude/vscode install paths). We install agent + command files to .claude/
621
+ // so multi-IDE installs share files, and the rcode workflow bridge gives
622
+ // Codex access to lifecycle workflows via `rcode workflow show <name>` (#883).
623
+ // NOTE: Codex surfaces native /slash commands ONLY from ~/.codex/prompts/*.md
624
+ // (home, startup-loaded) — installed separately by installNativeHomeSlashCommands()
625
+ // under the opt-in --global flag, since .claude/commands is invisible to Codex.
626
+ return {
627
+ agentsDir: path.join(target, '.claude', 'agents'),
628
+ commandsDir: path.join(target, '.claude', 'commands'),
629
+ workflowsDir: path.join(target, '.rcode', 'workflows'),
630
+ referencesDir: path.join(target, '.rcode', 'references'),
631
+ binDir: path.join(target, '.rcode', 'bin'),
632
+ };
633
+ case 'grok':
634
+ // Grok Build (xAI CLI) is Claude-Code-compatible: it reads slash commands
635
+ // from .claude/commands/*.md (project) and ~/.claude/commands (global), same
636
+ // as Claude Code. So grok maps to the identical .claude/ layout — verified
637
+ // live: `/rcode-add-phase` surfaces in grok from these dirs.
638
+ return {
639
+ agentsDir: path.join(target, '.claude', 'agents'),
640
+ commandsDir: path.join(target, '.claude', 'commands'),
641
+ workflowsDir: path.join(target, '.rcode', 'workflows'),
642
+ referencesDir: path.join(target, '.rcode', 'references'),
643
+ binDir: path.join(target, '.rcode', 'bin'),
644
+ };
604
645
  default:
605
646
  throw new Error(`Unknown IDE: ${ide}. Supported: ${SUPPORTED_IDES.join(', ')}`);
606
647
  }
@@ -1058,7 +1099,7 @@ function installSkills(packageRoot, target, options = {}) {
1058
1099
  // prefix, Claude Code reads from BOTH global and project, showing every
1059
1100
  // /rcode-* twice in the slash picker. Skip the project copy for any rcode-*
1060
1101
  // skill that already lives in the global skills dir.
1061
- const globalSkillsDir = path.join(os.homedir(), '.claude', 'skills');
1102
+ const globalSkillsDir = path.join(homedir(), '.claude', 'skills');
1062
1103
  const globalRcodeSkills = (options.skipGlobalDuplicates && fs.existsSync(globalSkillsDir))
1063
1104
  ? new Set(fs.readdirSync(globalSkillsDir).filter(n => n.startsWith('rcode-')))
1064
1105
  : new Set();
@@ -1282,7 +1323,7 @@ function buildInstallPlan(ide = 'claude', target = process.cwd()) {
1282
1323
  const rel = path.relative(path.join(SOURCE_ROOT, 'commands'), f);
1283
1324
  const ext = ide === 'cursor' ? '.mdc' : '.md';
1284
1325
  const baseName = path.basename(f, '.md');
1285
- const outName = (ide === 'claude' || ide === 'vscode')
1326
+ const outName = (ide === 'claude' || ide === 'vscode' || ide === 'grok')
1286
1327
  ? `rcode-${baseName}${ext}`
1287
1328
  : baseName + ext;
1288
1329
  plan.push({ src: f, rel: path.join(relCommands, path.dirname(rel), outName), ide, cursor: ide === 'cursor' });
@@ -1431,8 +1472,8 @@ function generateAgentManifest(plan, target) {
1431
1472
  // Also scan rcode/agents/ in SOURCE_ROOT as a last-resort fallback so the
1432
1473
  // manifest is never empty when the package itself ships agent definitions.
1433
1474
  const extraScans = [
1434
- path.join(os.homedir(), '.claude', 'agents'),
1435
- path.join(os.homedir(), '.rcode', 'agents'),
1475
+ path.join(homedir(), '.claude', 'agents'),
1476
+ path.join(homedir(), '.rcode', 'agents'),
1436
1477
  ];
1437
1478
  // Final fallback: scan the package source itself.
1438
1479
  try {
@@ -1880,6 +1921,125 @@ function acquireInstallLock(target) {
1880
1921
  return { ok: false, pid: 0, lockPath };
1881
1922
  }
1882
1923
 
1924
+ // ─────────────────────────────────────────────────────────────────────────────
1925
+ // Native home-dir slash-command install.
1926
+ //
1927
+ // Some agentic CLIs surface their `/slash` command menu ONLY from a fixed
1928
+ // home directory (not from project dirs the way Claude Code / Grok do):
1929
+ // • Codex → ~/.codex/prompts/<name>.md (flat prompt files)
1930
+ // • Antigravity→ ~/.gemini/antigravity/skills/<name>/SKILL.md (skill dirs)
1931
+ // For those tools the normal project install writes files the CLI never reads,
1932
+ // so `/rcode-*` never appears. This installs the commands in each CLI's NATIVE
1933
+ // format into its NATIVE home dir, gated behind the opt-in `--global` flag.
1934
+ //
1935
+ // IDEs that read project dirs (claude, grok, cursor, vscode, windsurf) are a
1936
+ // no-op here — they already work via getPathsForIde().
1937
+ //
1938
+ // Each tool's writer lives in its own helper; the dispatcher routes by ide.
1939
+ // SOURCE_ROOT/commands/*.md is the canonical command source for all writers.
1940
+ // ─────────────────────────────────────────────────────────────────────────────
1941
+
1942
+ // Codex + Antigravity surface NO file-based slash commands (verified live),
1943
+ // but BOTH support a prompt-submit hook (UserPromptSubmit / UserPrompt) that
1944
+ // can inject context. We install a hook ROUTER (cli/rcode-slash-router.cjs)
1945
+ // into each, plus a home-dir copy of every command body the router reads.
1946
+ // See cli/rcode-slash-router.cjs for the runtime contract.
1947
+
1948
+ // Shared: copy every command body to ~/.rcode/slash-commands/<name>.md and the
1949
+ // router script to ~/.rcode/bin/. A fixed home-dir location lets the hook read
1950
+ // commands regardless of the user's cwd. Idempotent (plain overwrite).
1951
+ function installSlashRouterCommands(opts) {
1952
+ const home = homedir();
1953
+ const cmdDestDir = path.join(home, '.rcode', 'slash-commands');
1954
+ const binDestDir = path.join(home, '.rcode', 'bin');
1955
+ ensureDir(cmdDestDir);
1956
+ ensureDir(binDestDir);
1957
+
1958
+ const srcCmdDir = path.join(SOURCE_ROOT, 'commands');
1959
+ let copied = 0;
1960
+ for (const file of fs.readdirSync(srcCmdDir)) {
1961
+ if (!file.endsWith('.md')) continue;
1962
+ fs.copyFileSync(path.join(srcCmdDir, file), path.join(cmdDestDir, file));
1963
+ copied++;
1964
+ }
1965
+
1966
+ const routerSrc = path.join(PACKAGE_ROOT, 'cli', 'rcode-slash-router.cjs');
1967
+ const routerDest = path.join(binDestDir, 'rcode-slash-router.cjs');
1968
+ fs.copyFileSync(routerSrc, routerDest);
1969
+
1970
+ if (opts && opts.global !== 'silent') {
1971
+ console.log(' ' + ok(`Slash-router: ${copied} command bodies → ~/.rcode/slash-commands/ + router → ~/.rcode/bin/`));
1972
+ }
1973
+ return routerDest;
1974
+ }
1975
+
1976
+ // The absolute command a hook entry runs. Matched by substring for idempotency
1977
+ // and for removal on uninstall — keep the basename stable.
1978
+ function slashRouterHookCommand() {
1979
+ return `node "${path.join(homedir(), '.rcode', 'bin', 'rcode-slash-router.cjs')}"`;
1980
+ }
1981
+
1982
+ // Merge a prompt-submit hook entry into an existing CLI hooks JSON file without
1983
+ // disturbing any pre-existing entries (e.g. herdr's). `eventKey` is the hook
1984
+ // event name that CLI uses (codex: UserPromptSubmit, antigravity: UserPrompt).
1985
+ // Idempotent: re-running detects the router by command substring and no-ops.
1986
+ function mergeSlashRouterHook(jsonPath, eventKey, command, label) {
1987
+ let root = {};
1988
+ if (fs.existsSync(jsonPath)) {
1989
+ try {
1990
+ root = JSON.parse(fs.readFileSync(jsonPath, 'utf8')) || {};
1991
+ } catch {
1992
+ // Unparseable file — don't clobber the user's config; bail loudly.
1993
+ console.log(' ' + warn(`${label}: ${jsonPath} is not valid JSON — skipped slash-router wiring.`));
1994
+ return false;
1995
+ }
1996
+ }
1997
+ if (!root.hooks || typeof root.hooks !== 'object') root.hooks = {};
1998
+ if (!Array.isArray(root.hooks[eventKey])) root.hooks[eventKey] = [];
1999
+
2000
+ const already = root.hooks[eventKey].some(group =>
2001
+ Array.isArray(group?.hooks) &&
2002
+ group.hooks.some(h => typeof h?.command === 'string' && h.command.includes('rcode-slash-router.cjs')),
2003
+ );
2004
+ if (already) {
2005
+ console.log(' ' + ok(`${label}: slash-router hook already present (idempotent).`));
2006
+ return false;
2007
+ }
2008
+
2009
+ root.hooks[eventKey].push({ hooks: [{ type: 'command', command, timeout: 10 }] });
2010
+ ensureDir(path.dirname(jsonPath));
2011
+ fs.writeFileSync(jsonPath, JSON.stringify(root, null, 2) + '\n');
2012
+ console.log(' ' + ok(`${label}: wired slash-router into ${eventKey} hook (existing hooks preserved).`));
2013
+ return true;
2014
+ }
2015
+
2016
+ // Codex: ~/.codex/hooks.json, event UserPromptSubmit.
2017
+ function installCodexSlashRouterHook(opts) {
2018
+ installSlashRouterCommands(opts);
2019
+ const jsonPath = path.join(homedir(), '.codex', 'hooks.json');
2020
+ mergeSlashRouterHook(jsonPath, 'UserPromptSubmit', slashRouterHookCommand(), 'Codex');
2021
+ }
2022
+
2023
+ // Antigravity: ~/.gemini/antigravity/settings.json, event UserPrompt.
2024
+ function installAntigravitySlashRouterHook(opts) {
2025
+ installSlashRouterCommands(opts);
2026
+ const jsonPath = path.join(homedir(), '.gemini', 'antigravity', 'settings.json');
2027
+ mergeSlashRouterHook(jsonPath, 'UserPrompt', slashRouterHookCommand(), 'Antigravity');
2028
+ }
2029
+
2030
+ function installNativeHomeSlashCommands(opts) {
2031
+ if (!opts || !opts.global) return;
2032
+ const ides = Array.isArray(opts.ides) ? opts.ides : [opts.ide].filter(Boolean);
2033
+ for (const ide of ides) {
2034
+ switch (ide) {
2035
+ case 'codex': installCodexSlashRouterHook(opts); break;
2036
+ case 'antigravity': installAntigravitySlashRouterHook(opts); break;
2037
+ default:
2038
+ break;
2039
+ }
2040
+ }
2041
+ }
2042
+
1883
2043
  async function installInner(opts) {
1884
2044
  const pkgVersion = readPackageVersion();
1885
2045
 
@@ -1929,10 +2089,12 @@ async function installInner(opts) {
1929
2089
  console.error(' Currently supported:');
1930
2090
  console.error(' claude — Claude Code native (recommended)');
1931
2091
  console.error(' cursor — Cursor IDE');
1932
- console.error(' gemini Gemini CLI');
2092
+ console.error(' codex Codex CLI');
2093
+ console.error(' gemini — Gemini CLI (planned — not yet implemented)');
1933
2094
  console.error(' vscode — VS Code (with Claude Code / Continue / Copilot extension)');
1934
2095
  console.error(' windsurf — Windsurf (Codeium)');
1935
2096
  console.error(' antigravity — Antigravity (experimental)');
2097
+ console.error(' grok — Grok Build (xAI CLI, Claude-Code-compatible)');
1936
2098
  console.error('');
1937
2099
  console.error(' Tracked for future:');
1938
2100
  console.error(' jetbrains — IntelliJ / PyCharm');
@@ -1946,6 +2108,11 @@ async function installInner(opts) {
1946
2108
  console.log(' ' + dim('VS Code → installing to .claude/ paths (read by Claude Code / Continue / Copilot extensions).'));
1947
2109
  }
1948
2110
 
2111
+ // Codex installs to .claude/ and AGENTS.md; lifecycle via rcode workflow bridge.
2112
+ if (opts.ides.includes('codex')) {
2113
+ console.log(' ' + dim('Codex → installing to .claude/ paths + AGENTS.md. Use `rcode workflow show <name>` to feed workflows to Codex.'));
2114
+ }
2115
+
1949
2116
  // Gemini IDE support deferred
1950
2117
  if (opts.ides.includes('gemini')) {
1951
2118
  console.log(`\n⚠️ Gemini CLI install not yet implemented\n`);
@@ -1994,6 +2161,15 @@ async function installInner(opts) {
1994
2161
  console.log(` Modules: ${opts.modules.join(', ')}`);
1995
2162
  }
1996
2163
 
2164
+ // Dry run / list-files — list paths that would be written and exit without writing
2165
+ if (opts.dryRun || opts.listFiles) {
2166
+ console.log('DRY RUN: the following paths would be written:');
2167
+ for (const entry of plan) {
2168
+ console.log(' + ' + entry.rel);
2169
+ }
2170
+ return 0;
2171
+ }
2172
+
1997
2173
  // Force-overwrite backup — closes #381. Without this, customized
1998
2174
  // .claude/agents/rcode-*.md and similar package-managed files were silently
1999
2175
  // clobbered with no recovery path. Now every --force-overwrite run creates
@@ -2262,6 +2438,16 @@ async function installInner(opts) {
2262
2438
  } catch { /* non-fatal */ }
2263
2439
  console.log('');
2264
2440
  console.log(` ${dim(`${skillsInstalled} skills installed globally`)}`);
2441
+
2442
+ // Native home-dir slash commands for CLIs that can't surface file-based
2443
+ // /commands (codex, antigravity) but DO support a prompt-submit hook.
2444
+ // This MUST run inside the --global block: the global path returns here,
2445
+ // before the non-global call site below. Gated on opts.global inside.
2446
+ try {
2447
+ installNativeHomeSlashCommands(opts);
2448
+ } catch (err) {
2449
+ process.stderr.write(pc.yellow(`WARNING: native slash-command install skipped: ${err?.message || err}`) + '\n');
2450
+ }
2265
2451
  return 0;
2266
2452
  }
2267
2453
 
@@ -2269,9 +2455,9 @@ async function installInner(opts) {
2269
2455
  // skip writing agents/commands to the project's .claude/ directory. Without this,
2270
2456
  // running `npx rcode install` in the home dir AND then in a project creates two sets
2271
2457
  // of identical files — Claude Code shows both as duplicate slash commands.
2272
- const globalClaudeCommands = path.join(os.homedir(), '.claude', 'commands');
2458
+ const globalClaudeCommands = path.join(homedir(), '.claude', 'commands');
2273
2459
  const projectClaudeCommands = path.join(opts.target, '.claude', 'commands');
2274
- const isProjectInstall = opts.target !== os.homedir();
2460
+ const isProjectInstall = opts.target !== homedir();
2275
2461
  // Run dedup even when force:true — only forceOverwrite skips it.
2276
2462
  if (isProjectInstall && !opts.forceOverwrite) {
2277
2463
  try {
@@ -2453,7 +2639,7 @@ async function installInner(opts) {
2453
2639
  }
2454
2640
 
2455
2641
  // ~/.rcode/agents/ global agents directory
2456
- const globalAgentsDir = path.join(os.homedir(), '.rcode', 'agents');
2642
+ const globalAgentsDir = path.join(homedir(), '.rcode', 'agents');
2457
2643
  ensureDir(globalAgentsDir);
2458
2644
 
2459
2645
  // Issue #702: files-manifest.csv used to be written here, BEFORE
@@ -2638,9 +2824,9 @@ async function installInner(opts) {
2638
2824
  // the project skills folder may have only sidebar stubs while ~/.claude/
2639
2825
  // has the real skills — health check should see those.
2640
2826
  if (agentCount === 0 || commandCount === 0 || skillsInstalled < 20) {
2641
- const homeAgents = path.join(os.homedir(), '.claude/agents');
2642
- const homeCommands = path.join(os.homedir(), '.claude/commands');
2643
- const homeSkills = path.join(os.homedir(), '.claude/skills');
2827
+ const homeAgents = path.join(homedir(), '.claude/agents');
2828
+ const homeCommands = path.join(homedir(), '.claude/commands');
2829
+ const homeSkills = path.join(homedir(), '.claude/skills');
2644
2830
  if (agentCount === 0 && fs.existsSync(homeAgents)) {
2645
2831
  // #669 — count both rcode-* and rcode-* prefixes; missing rcode-
2646
2832
  // branch produced "Agents: 0" alongside "Skills: 120".
@@ -2667,6 +2853,32 @@ async function installInner(opts) {
2667
2853
  console.error('[install] installInner: failed to count installed agents/commands:', err?.message || err);
2668
2854
  }
2669
2855
 
2856
+ // Duplicate-namespace detection: warn when both rcode-* and rihal-* entries exist.
2857
+ // Having both doubles the roster size with near-identical content.
2858
+ try {
2859
+ const skillsDir = path.join(opts.target, '.rcode', 'skills');
2860
+ const claudeCommandsDir = path.join(opts.target, '.claude', 'commands');
2861
+ const dirsToCheck = [skillsDir, claudeCommandsDir];
2862
+ let hasRcode = false, hasRihal = false;
2863
+ for (const dir of dirsToCheck) {
2864
+ if (!fs.existsSync(dir)) continue;
2865
+ const entries = fs.readdirSync(dir);
2866
+ if (entries.some(e => e.startsWith('rcode-'))) hasRcode = true;
2867
+ if (entries.some(e => e.startsWith('rihal-'))) hasRihal = true;
2868
+ }
2869
+ if (hasRcode && hasRihal) {
2870
+ process.stderr.write(pc.yellow('WARNING: rcode-* and rihal-* namespaces both detected — consider removing one to reduce roster size.') + '\n');
2871
+ }
2872
+ } catch { /* non-fatal */ }
2873
+
2874
+ // Native home-dir slash commands for CLIs that ONLY surface /commands from
2875
+ // their own home dir (not project dirs). Opt-in via --global. See the fn def.
2876
+ try {
2877
+ installNativeHomeSlashCommands(opts);
2878
+ } catch (err) {
2879
+ process.stderr.write(pc.yellow(`WARNING: native slash-command install skipped: ${err?.message || err}`) + '\n');
2880
+ }
2881
+
2670
2882
  const version = readPackageVersion();
2671
2883
  console.log('');
2672
2884
  console.log(` ${bold('Version:')} ${pc.cyan('@hanzlaa/rcode@' + version)}`);
@@ -2915,6 +3127,7 @@ async function runInstallWizard(opts) {
2915
3127
  options: [
2916
3128
  { value: 'claude', label: 'Claude Code', hint: 'recommended' },
2917
3129
  { value: 'cursor', label: 'Cursor' },
3130
+ { value: 'codex', label: 'Codex (OpenAI CLI)', hint: 'AGENTS.md + workflow bridge' },
2918
3131
  { value: 'gemini', label: 'Gemini CLI', hint: 'coming soon' },
2919
3132
  { value: 'vscode', label: 'VS Code', hint: 'via Continue / Copilot extensions' },
2920
3133
  { value: 'antigravity', label: 'Antigravity', hint: 'experimental' },
@@ -3016,3 +3229,8 @@ module.exports.install = install;
3016
3229
  module.exports.SUPPORTED_IDES = SUPPORTED_IDES;
3017
3230
  module.exports.migrateVscodeCommandsLayout = migrateVscodeCommandsLayout;
3018
3231
  module.exports.getPathsForIde = getPathsForIde;
3232
+ // Slash-router (hook-based /rcode-* support for codex + antigravity).
3233
+ module.exports.installSlashRouterCommands = installSlashRouterCommands;
3234
+ module.exports.installCodexSlashRouterHook = installCodexSlashRouterHook;
3235
+ module.exports.installAntigravitySlashRouterHook = installAntigravitySlashRouterHook;
3236
+ module.exports.installNativeHomeSlashCommands = installNativeHomeSlashCommands;
@@ -18,9 +18,11 @@
18
18
  */
19
19
 
20
20
  const fs = require('fs');
21
- const os = require('os');
22
21
  const path = require('path');
23
22
  const { writeJsonAtomic } = require('./fsutil.cjs');
23
+ // HOME-aware home resolution (#889) — os.homedir() ignores a stubbed HOME
24
+ // on Windows, which broke user-level defaults isolation in tests.
25
+ const { homedir } = require('./homedir.cjs');
24
26
 
25
27
  // ---------- Schema ----------
26
28
 
@@ -102,7 +104,7 @@ const VALID_COMMUNICATION_MODES = new Set(['guided', 'yolo']);
102
104
  // ---------- Paths ----------
103
105
 
104
106
  function userLevelPath() {
105
- return path.join(os.homedir(), '.rcode', 'defaults.json');
107
+ return path.join(homedir(), '.rcode', 'defaults.json');
106
108
  }
107
109
 
108
110
  function projectLevelPath(cwd) {
@@ -115,8 +115,19 @@ function safeRmSync(targetPath, projectRoot) {
115
115
  }
116
116
  }
117
117
 
118
- // Real path must stay inside the project root.
119
- const root = path.resolve(projectRoot);
118
+ // Real path must stay inside the project root. The root must be
119
+ // realpathed too: on macOS os.tmpdir() lives behind a symlink
120
+ // (/tmp → /private/tmp, /var → /private/var), so comparing a realpathed
121
+ // target against a merely-resolved root misreports anything under /tmp
122
+ // as outside-root.
123
+ let root;
124
+ try {
125
+ root = fs.realpathSync(projectRoot);
126
+ } catch {
127
+ // Root missing/unreadable — fall back to a lexical resolve; the
128
+ // containment check below then fails closed for an existing target.
129
+ root = path.resolve(projectRoot);
130
+ }
120
131
  let resolved;
121
132
  try {
122
133
  resolved = fs.realpathSync(targetPath);
@@ -0,0 +1,21 @@
1
+ /**
2
+ * cli/lib/homedir.cjs — user home directory resolution (#889).
3
+ *
4
+ * process.env.HOME wins over os.homedir() so a single env var redirects
5
+ * every home-relative read/write on EVERY platform. os.homedir() ignores
6
+ * HOME on Windows (it reads USERPROFILE), which made HOME-stubbed tests
7
+ * silently escape to the real profile dir on Windows CI — installs leaked
8
+ * ~/.codex / ~/.gemini / ~/.rcode into the runner's real home and broke
9
+ * unrelated tests. Honoring HOME also matches git/npm behavior on Windows
10
+ * (git-bash sets HOME), so real users get consistent paths across shells.
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ const os = require('os');
16
+
17
+ function homedir() {
18
+ return process.env.HOME || os.homedir();
19
+ }
20
+
21
+ module.exports = { homedir };
@@ -35,9 +35,14 @@ const { z } = require('zod');
35
35
  * @returns {{ frontmatter: object, body: string }}
36
36
  */
37
37
  function parseFrontmatter(text) {
38
- if (typeof text !== 'string' || !text.startsWith('---\n')) {
38
+ if (typeof text !== 'string') {
39
39
  return { frontmatter: {}, body: text || '' };
40
40
  }
41
+ // CRLF tolerance (#889): Windows checkouts/user files may use \r\n.
42
+ text = text.replace(/\r\n/g, '\n');
43
+ if (!text.startsWith('---\n')) {
44
+ return { frontmatter: {}, body: text };
45
+ }
41
46
  const end = text.indexOf('\n---\n', 4);
42
47
  if (end === -1) return { frontmatter: {}, body: text };
43
48
  const block = text.slice(4, end);