@hanzlaa/rcode 4.8.0 โ†’ 4.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/AGENTS.md +1 -1
  2. package/CONTRIBUTING.md +3 -0
  3. package/README.md +7 -5
  4. package/cli/doctor.js +4 -2
  5. package/cli/github-sync.js +26 -17
  6. package/cli/install.js +59 -34
  7. package/cli/lib/namespace-migrate.cjs +62 -4
  8. package/cli/migrate-namespace.js +4 -0
  9. package/dist/rcode.js +207 -207
  10. package/package.json +1 -1
  11. package/rcode/agents/rcode-code-reviewer.md +1 -1
  12. package/rcode/agents/rcode-docs-auditor.md +1 -1
  13. package/rcode/agents/rcode-edge-case-hunter.md +1 -1
  14. package/rcode/agents/rcode-security-adversary.md +1 -1
  15. package/rcode/agents/rcode-security-auditor.md +1 -1
  16. package/rcode/agents/rcode-sprint-checker.md +1 -1
  17. package/rcode/agents/rcode-verifier.md +1 -1
  18. package/rcode/bin/lib/brain.cjs +16 -1
  19. package/rcode/bin/lib/gitignore.cjs +3 -5
  20. package/rcode/bin/rcode-hooks.cjs +22 -5
  21. package/rcode/bin/rcode-tools.cjs +166 -14
  22. package/rcode/data/intent-table.json +1 -1
  23. package/rcode/references/git-preflight.md +5 -2
  24. package/rcode/references/output-format.md +5 -5
  25. package/rcode/workflows/add-phase.md +33 -14
  26. package/rcode/workflows/do.md +33 -1
  27. package/rcode/workflows/execute-sprint.md +3 -4
  28. package/rcode/workflows/execute-waves.md +25 -32
  29. package/rcode/workflows/execute.md +80 -21
  30. package/rcode/workflows/init.md +28 -6
  31. package/rcode/workflows/plan-research-validation.md +10 -5
  32. package/rcode/workflows/plan-spawn-planner.md +9 -13
  33. package/rcode/workflows/plan.md +2 -2
  34. package/rcode/workflows/scaffold-skill.md +19 -1
  35. package/rcode/workflows/scan.md +22 -1
  36. package/rcode/workflows/secure-phase.md +7 -1
  37. package/rcode/workflows/validate-phase.md +7 -1
  38. package/server/lib/html/client/components/Sidebar.js +8 -5
  39. package/server/lib/html/client/views/PhasesView.js +1 -1
  40. package/server/lib/html/css.js +4 -0
  41. package/server/lib/scanner.js +4 -1
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`, `github-sync`, `commands`, `memory`, `brand`, `cli`, `ci`, `release`, `meta`, `tasks`, `migrations`, `refs`, `state`, `hooks`, `init`, `install`, `parity`, `triggers`, `dogfood`, `namespace`, `planning`, `insights`, `help`, `roadmap`, `session`, `audits`, `execute`, `executor`, `plan`, `planner`, `readme`, `rcode`, `review`, `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`, `getting-started`, `do-router`, `milestone-health`, `modules`, `project-types`, `roadmapper`, `token`, plus numeric phase/sprint scopes (e.g. `docs(15)`, `feat(8.3)`)
27
+ - Scopes allowed: `agents`, `skills`, `workflows`, `templates`, `dashboard`, `docs`, `config`, `github`, `github-sync`, `commands`, `memory`, `brand`, `cli`, `ci`, `release`, `meta`, `tasks`, `migrations`, `refs`, `state`, `hooks`, `init`, `install`, `parity`, `triggers`, `dogfood`, `namespace`, `planning`, `insights`, `help`, `roadmap`, `session`, `audits`, `execute`, `executor`, `plan`, `planner`, `readme`, `rcode`, `review`, `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`, `getting-started`, `do-router`, `milestone-health`, `modules`, `project-types`, `roadmapper`, `token`, `benchmarks`, `eval`, `scan`, 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
@@ -363,6 +363,9 @@ We use [Conventional Commits](https://www.conventionalcommits.org/) format. The
363
363
  - `project-types` โ€” project-type templates and detection
364
364
  - `roadmapper` โ€” roadmap generation agent/workflow
365
365
  - `token` โ€” token-budget and cost-tracking logic
366
+ - `benchmarks` โ€” `benchmarks/` scripts and benchmark reports
367
+ - `eval` โ€” `test/eval/` behavioral drift baselines and eval harness
368
+ - `scan` โ€” `/rcode-scan` workflow and existing-codebase analysis
366
369
  - `<phase-id>` โ€” numeric phase scope when committing inside a phase (e.g. `docs(15)`, `feat(8.3)`)
367
370
  - `<sprint-id>` โ€” numeric sprint scope inside a phase (e.g. `feat(15.1)`)
368
371
 
package/README.md CHANGED
@@ -23,7 +23,7 @@ pnpm dlx @hanzlaa/rcode install
23
23
  [![CI](https://github.com/hanzlahabib/rcode/actions/workflows/test.yml/badge.svg)](https://github.com/hanzlahabib/rcode/actions/workflows/test.yml)
24
24
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
25
25
 
26
- Status: `@hanzlaa/rcode` v4.7.0 on npm. 45 agents ยท 117 commands ยท 129 workflows ยท **1 runtime dependency**. Test status tracked by CI badge above. Actively dogfooded on real projects every week.
26
+ Status: `@hanzlaa/rcode` v4.9.0 on npm. 45 agents ยท 117 commands ยท 130 workflows ยท **1 runtime dependency**. Test status tracked by CI badge above. Actively dogfooded on real projects every week.
27
27
 
28
28
  ---
29
29
 
@@ -154,10 +154,10 @@ The point isn't "I beat LangChain." The point is **you don't need LangChain for
154
154
  | Metric | Value | Why it's not a vanity number |
155
155
  |---|---|---|
156
156
  | **Portable methodology corpus** | **~105,000 lines** of markdown | A 10-person eng org's playbooks (agents + commands + workflows + skills + references) โ€” as files you own and grep, not a SaaS you rent. |
157
- | **Automated tests** | **495** across 62 files | The methodology is *guarded*, not vibes. Live pass/fail status is on the CI badge above; `node --test` (or `node benchmarks/facts.cjs`) reproduces locally in seconds. |
157
+ | **Automated tests** | **598** across 74 files | The methodology is *guarded*, not vibes. Live pass/fail status is on the CI badge above; `node --test` (or `node benchmarks/facts.cjs`) reproduces locally in seconds. |
158
158
  | **Tested CLI engine** | **~9,700 lines** (`rcode-tools.cjs` + `lib/`) | The deterministic brain โ€” routing, state, planning math โ€” is real code under test, not prompt soup. |
159
159
  | **Runtime dependencies** | **1** (`ws`, for the dashboard socket) | The dashboard is otherwise pure Node stdlib. Almost nothing to audit, nothing to CVE-scan, nothing to break on `npm install`. |
160
- | **Specialist agents / commands / workflows / skills** | **45 / 117 / 129 / 96** | An entire engineering org, phrase-activated, that travels with you across Claude Code, Cursor, Codex, VS Code, Antigravity, Windsurf. |
160
+ | **Specialist agents / commands / workflows / skills** | **45 / 117 / 130 / 96** | An entire engineering org, phrase-activated, that travels with you across Claude Code, Cursor, Codex, VS Code, Antigravity, Windsurf. |
161
161
  | **Core-op latency** | **~60 ms**, **0 LLM tokens** (best-of-7) | Intent routing, state reads, and milestone-health are deterministic *local* compute โ€” a few ms over Node's own cold-start floor. The orchestration layer doesn't burn API tokens on bookkeeping the way pure-LLM agent frameworks do. |
162
162
 
163
163
  > Counts above are produced by `node benchmarks/facts.cjs` and may drift slightly between releases โ€” run it for the exact current figures.
@@ -181,6 +181,8 @@ pnpm dlx @hanzlaa/rcode install
181
181
 
182
182
  `/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`.
183
183
 
184
+ > **Want a status primer at the start of every session?** Run `/rcode-enable-hooks` to turn on a one-line project status readout (phase, plan progress, blockers) each time Claude Code starts in this project, plus 9 other opt-in guardrails (read-before-edit checks, dangerous-command blocking, auto-formatting). All off by default โ€” a fresh install never surprises you.
185
+
184
186
  ### The full loop
185
187
 
186
188
  Four commands cover most of a real week of work โ€” decide, plan, build, check in:
@@ -227,9 +229,9 @@ The non-goal: building yet another agent framework. There are enough. rcode stay
227
229
 
228
230
  ## Honest state of things
229
231
 
230
- - **v4.3.2** is the current release โ€” Memory Bank now ships populated by default, brain-pull is live, and 50+ bug fixes landed from the most recent 3-project dogfeed run.
232
+ - **v4.9.0** is the current release โ€” Memory Bank now ships populated by default, brain-pull is live, and 50+ bug fixes landed from the most recent 3-project dogfeed run.
231
233
  - **Open issues**: ~50 โ€” half are feature requests, the rest are backlog bugs ranked by severity.
232
- - **Test suite**: 497 automated tests across 61 files, 100% pass on every release (run `node --test`). Coverage is structural (compliance + artifact schema + workflow behavioral), not line-coverage.
234
+ - **Test suite**: 598 automated tests across 74 files (run `node --test`). Live pass/fail status is on the CI badge above. Coverage is structural (compliance + artifact schema + workflow behavioral), not line-coverage.
233
235
  - **Real users**: I run it on 4 projects daily. A handful of others run it on theirs. If you find a bug, file it โ€” most P1s ship within 48 hours.
234
236
  - **Funding**: none. This is solo work. If your company wants commercial support, [email me](mailto:hanzla.dev@gmail.com).
235
237
 
package/cli/doctor.js CHANGED
@@ -229,8 +229,10 @@ function runPreflight(cwd, packageRoot) {
229
229
  status: 'warn',
230
230
  message:
231
231
  `${nsDup.totalCount} duplicate registration(s) โ€” legacy skills: ${nsDup.legacySkillCount}, ` +
232
- `legacy commands: ${nsDup.legacyCommandCount}, unprefixed: ${nsDup.unprefixedCount}, ` +
233
- `cross-scope: ${nsDup.crossScopeCount} (run \`rcode migrate-namespace --yes\` or \`rcode update\`)`,
232
+ `legacy commands: ${nsDup.legacyCommandCount}, legacy agents: ${nsDup.legacyAgentCount}, ` +
233
+ `unprefixed: ${nsDup.unprefixedCount}, cross-scope: ${nsDup.crossScopeCount}, ` +
234
+ `legacy Codex commands: ${nsDup.legacyCodexCommandCount} ` +
235
+ `(run \`rcode migrate-namespace --yes\` or \`rcode update\`)`,
234
236
  });
235
237
  } else {
236
238
  checks.push({
@@ -174,6 +174,15 @@ function saveSyncMap(cwd, map) {
174
174
  writeJsonAtomic(mapPath, map);
175
175
  }
176
176
 
177
+ // Namespace syncMap.epics/stories keys by phase/track. Sprint-track task ids
178
+ // (from `<task id="">`, no format enforcement) and epic-track story ids
179
+ // (`{epicNum}.{storyNum}`) can collapse to the same bare id across tracks โ€”
180
+ // without a phase prefix they'd silently clobber each other's issue mapping.
181
+ // See #1002.
182
+ function syncKey(phaseId, id) {
183
+ return `${phaseId}:${id}`;
184
+ }
185
+
177
186
  // ---------- Main sync flow ----------
178
187
 
179
188
  /**
@@ -369,33 +378,33 @@ async function main(args) {
369
378
  ] : [],
370
379
  milestones: phases.filter((p) => !p.noMilestone && !syncMap.phases[p.id]),
371
380
  epics: phases.flatMap((p) =>
372
- p.epics.filter((e) => !syncMap.epics[e.id]).map((e) => ({ ...e, phase: p.id })),
381
+ p.epics.filter((e) => !syncMap.epics[syncKey(p.id, e.id)]).map((e) => ({ ...e, phase: p.id })),
373
382
  ),
374
383
  stories: phases.flatMap((p) =>
375
- p.stories.filter((s) => !syncMap.stories[s.id]).map((s) => ({ ...s, phase: p.id })),
384
+ p.stories.filter((s) => !syncMap.stories[syncKey(p.id, s.id)]).map((s) => ({ ...s, phase: p.id })),
376
385
  ),
377
386
 
378
387
  // Items that already exist on GitHub โ€” candidates for update.
379
388
  // Only populated when updateEnabled is true.
380
389
  updateEpics: opts.updateEnabled
381
390
  ? phases.flatMap((p) =>
382
- p.epics.filter((e) => syncMap.epics[e.id]).map((e) => ({
391
+ p.epics.filter((e) => syncMap.epics[syncKey(p.id, e.id)]).map((e) => ({
383
392
  ...e,
384
393
  phase: p.id,
385
- issueNumber: syncMap.epics[e.id].issue_number,
386
- lastSyncedAt: syncMap.epics[e.id].synced_at,
387
- lastSyncedContentHash: syncMap.epics[e.id].content_hash,
394
+ issueNumber: syncMap.epics[syncKey(p.id, e.id)].issue_number,
395
+ lastSyncedAt: syncMap.epics[syncKey(p.id, e.id)].synced_at,
396
+ lastSyncedContentHash: syncMap.epics[syncKey(p.id, e.id)].content_hash,
388
397
  })),
389
398
  )
390
399
  : [],
391
400
  updateStories: opts.updateEnabled
392
401
  ? phases.flatMap((p) =>
393
- p.stories.filter((s) => syncMap.stories[s.id]).map((s) => ({
402
+ p.stories.filter((s) => syncMap.stories[syncKey(p.id, s.id)]).map((s) => ({
394
403
  ...s,
395
404
  phase: p.id,
396
- issueNumber: syncMap.stories[s.id].issue_number,
397
- lastSyncedAt: syncMap.stories[s.id].synced_at,
398
- lastSyncedContentHash: syncMap.stories[s.id].content_hash,
405
+ issueNumber: syncMap.stories[syncKey(p.id, s.id)].issue_number,
406
+ lastSyncedAt: syncMap.stories[syncKey(p.id, s.id)].synced_at,
407
+ lastSyncedContentHash: syncMap.stories[syncKey(p.id, s.id)].content_hash,
399
408
  })),
400
409
  )
401
410
  : [],
@@ -544,7 +553,7 @@ async function main(args) {
544
553
  if (result.error) {
545
554
  results.errors.push(`epic ${epic.id}: ${result.error}`);
546
555
  } else if (!result.dryRun) {
547
- syncMap.epics[epic.id] = {
556
+ syncMap.epics[syncKey(epic.phase, epic.id)] = {
548
557
  issue_number: result.number,
549
558
  url: result.url,
550
559
  phase: epic.phase,
@@ -568,7 +577,7 @@ async function main(args) {
568
577
  // convention), not "any epic in this phase" โ€” that bug led to all
569
578
  // stories pointing at the same epic previously.
570
579
  const parentEpicEntry = story.parentEpic
571
- ? syncMap.epics[story.parentEpic]
580
+ ? syncMap.epics[syncKey(story.phase, story.parentEpic)]
572
581
  : null;
573
582
  const parentRefLine = parentEpicEntry
574
583
  ? `- **Parent Epic:** #${parentEpicEntry.issue_number} (Part of this epic)`
@@ -624,7 +633,7 @@ async function main(args) {
624
633
  if (result.error) {
625
634
  results.errors.push(`story ${story.id}: ${result.error}`);
626
635
  } else if (!result.dryRun) {
627
- syncMap.stories[story.id] = {
636
+ syncMap.stories[syncKey(story.phase, story.id)] = {
628
637
  issue_number: result.number,
629
638
  url: result.url,
630
639
  phase: story.phase,
@@ -734,8 +743,8 @@ async function main(args) {
734
743
  if (result.error) {
735
744
  results.errors.push(`update epic #${epic.issueNumber}: ${result.error}`);
736
745
  } else if (!result.dryRun) {
737
- syncMap.epics[epic.id].content_hash = newHash;
738
- syncMap.epics[epic.id].updated_at = new Date().toISOString();
746
+ syncMap.epics[syncKey(epic.phase, epic.id)].content_hash = newHash;
747
+ syncMap.epics[syncKey(epic.phase, epic.id)].updated_at = new Date().toISOString();
739
748
  console.log(` โœ“ updated: #${epic.issueNumber} (${epic.id})`);
740
749
  }
741
750
  }
@@ -779,8 +788,8 @@ async function main(args) {
779
788
  if (result.error) {
780
789
  results.errors.push(`update story #${story.issueNumber}: ${result.error}`);
781
790
  } else if (!result.dryRun) {
782
- syncMap.stories[story.id].content_hash = newHash;
783
- syncMap.stories[story.id].updated_at = new Date().toISOString();
791
+ syncMap.stories[syncKey(story.phase, story.id)].content_hash = newHash;
792
+ syncMap.stories[syncKey(story.phase, story.id)].updated_at = new Date().toISOString();
784
793
  console.log(` โœ“ updated: #${story.issueNumber} (${story.id})`);
785
794
  }
786
795
  }
package/cli/install.js CHANGED
@@ -2151,11 +2151,15 @@ async function installInner(opts) {
2151
2151
 
2152
2152
  // Antigravity install is experimental โ€” best-effort path, user may need to adjust
2153
2153
  if (opts.ides.includes('antigravity')) {
2154
- console.log(' ' + warn('Antigravity install is experimental. Files land at .antigravity/rcode/{agents,commands}/.'));
2155
- console.log(' ' + dim('If Antigravity expects a different path, adjust .rcode/config.yaml and re-run.'));
2156
- // #908: same as Codex โ€” the UserPrompt hook is only wired on a global install.
2154
+ // #908/#1028: the UserPrompt hook that makes .antigravity/ files functional
2155
+ // is only wired on a global install. A project-local install would write
2156
+ // guaranteed-inert files, so skip them entirely instead of installing dead
2157
+ // weight โ€” see planIdes filtering below.
2157
2158
  if (!opts.global) {
2158
- console.log(' ' + warn('Antigravity /rcode-* slash commands need a GLOBAL install โ€” re-run with `--global`. This project-local install does NOT wire the hook.'));
2159
+ console.log(' ' + warn('Antigravity /rcode-* slash commands need a GLOBAL install โ€” re-run with `--global`. Skipping .antigravity/ files on this project-local install (they would be inert).'));
2160
+ } else {
2161
+ console.log(' ' + warn('Antigravity install is experimental. Files land at .antigravity/rcode/{agents,commands}/.'));
2162
+ console.log(' ' + dim('If Antigravity expects a different path, adjust .rcode/config.yaml and re-run.'));
2159
2163
  }
2160
2164
  }
2161
2165
 
@@ -2179,9 +2183,23 @@ async function installInner(opts) {
2179
2183
  }
2180
2184
  }
2181
2185
 
2182
- const fullPlan = buildInstallPlan(opts.ides, opts.target);
2186
+ // #1028: .antigravity/ is only wired up on a GLOBAL install (the hook that
2187
+ // makes /rcode-* slash commands work lives in ~/.gemini/antigravity/, written
2188
+ // by installAntigravitySlashRouterHook() only when opts.global is set โ€” see
2189
+ // the warning above). A project-local install writes guaranteed-inert files;
2190
+ // skip them entirely unless --global was actually passed.
2191
+ const planIdes = (!opts.global && Array.isArray(opts.ides))
2192
+ ? opts.ides.filter(i => i !== 'antigravity')
2193
+ : opts.ides;
2194
+
2195
+ const fullPlan = buildInstallPlan(planIdes, opts.target);
2183
2196
  const plan = filterPlanByModules(fullPlan, opts.modules);
2184
2197
  if (plan.length === 0) {
2198
+ if (Array.isArray(opts.ides) && opts.ides.includes('antigravity') && !opts.global) {
2199
+ console.error('โœ– Nothing to install โ€” Antigravity was the only target IDE, and its files need a GLOBAL install.');
2200
+ console.error(' Re-run with --global, or pick another --ide.');
2201
+ return 1;
2202
+ }
2185
2203
  console.error('โœ– Nothing to install โ€” install plan is empty.');
2186
2204
  if (opts.modules.length > 0) console.error(` Modules requested: ${opts.modules.join(', ')}`);
2187
2205
  return 1;
@@ -2502,21 +2520,32 @@ async function installInner(opts) {
2502
2520
  fs.existsSync(path.join(projectClaudeCommands, 'rcode'))
2503
2521
  );
2504
2522
  if (globalHasrcode && !projectHasrcode) {
2505
- // Global commands exist, project has none yet โ€” filter them out of the plan
2506
- // so we don't create duplicates. Project gets .rcode/ state only.
2523
+ // Global commands exist, project has none yet โ€” filter commands out of the
2524
+ // plan so we don't create duplicates. Project gets .rcode/ state only.
2525
+ //
2526
+ // Issue #1022: agents are NOT deferrable to global the way commands/skills
2527
+ // are โ€” they are first-class, project-local files by design (see the
2528
+ // createInstallBackup comment above: "closes #381 โ€” without this,
2529
+ // customized .claude/agents/rcode-*.md ... were silently lost"). Lumping
2530
+ // `.claude/agents/` into this commands-dedup filter meant a project with
2531
+ // global rcode commands installed but no project-level commands yet would
2532
+ // never get its .claude/agents/*.md files written at all, leaving only
2533
+ // whatever pre-existing subdirectories (e.g. rules/) survived untouched.
2507
2534
  const before = plan.length;
2508
2535
  const filtered = plan.filter(e => {
2509
2536
  const rel = e.rel.split(path.sep).join('/');
2510
- return !rel.startsWith('.claude/commands/') && !rel.startsWith('.claude/agents/');
2537
+ return !rel.startsWith('.claude/commands/');
2511
2538
  });
2512
2539
  if (filtered.length < before) {
2513
2540
  plan.length = 0;
2514
2541
  filtered.forEach(e => plan.push(e));
2515
- console.log(' ' + dim('Global rcode commands detected in ~/.claude/ โ€” skipping project-level agent/command install to avoid duplicates.'));
2542
+ console.log(' ' + dim('Global rcode commands detected in ~/.claude/ โ€” skipping project-level command install to avoid duplicates.'));
2516
2543
  console.log(' ' + dim('Use --force-overwrite to install locally anyway.'));
2517
2544
  }
2518
2545
  } else if (globalHasrcode && projectHasrcode) {
2519
2546
  // Both exist โ€” project commands are duplicates. Remove project-level ones.
2547
+ // Agents are left untouched (#1022) โ€” they are project-local by design,
2548
+ // never deduped against the global commands install.
2520
2549
  try {
2521
2550
  // Remove root-level rcode-*.md files
2522
2551
  const projectCommandFiles = fs.readdirSync(projectClaudeCommands)
@@ -2530,19 +2559,11 @@ async function installInner(opts) {
2530
2559
  if (fs.existsSync(rcodeSubdir)) {
2531
2560
  safeRmSync(rcodeSubdir, opts.target);
2532
2561
  }
2533
- const projectAgentsDir = path.join(opts.target, '.claude', 'agents');
2534
- if (fs.existsSync(projectAgentsDir)) {
2535
- const agentFiles = fs.readdirSync(projectAgentsDir)
2536
- .filter(f => f.startsWith('rcode-') && f.endsWith('.md'));
2537
- for (const f of agentFiles) {
2538
- fs.unlinkSync(path.join(projectAgentsDir, f));
2539
- }
2540
- }
2541
2562
  console.log(' ' + dim('Removed duplicate project-level rcode commands (global ones in ~/.claude/ take precedence).'));
2542
2563
  } catch { /* non-fatal */ }
2543
2564
  const filtered = plan.filter(e => {
2544
2565
  const rel = e.rel.split(path.sep).join('/');
2545
- return !rel.startsWith('.claude/commands/') && !rel.startsWith('.claude/agents/');
2566
+ return !rel.startsWith('.claude/commands/');
2546
2567
  });
2547
2568
  plan.length = 0;
2548
2569
  filtered.forEach(e => plan.push(e));
@@ -2751,23 +2772,30 @@ async function installInner(opts) {
2751
2772
  const hookReport = ensureRcodePreCommitHook(opts.target, { gitHooks: opts.gitHooks });
2752
2773
 
2753
2774
  // Pull rcode brain content (v2.0 โ€” issue #158).
2754
- // Runs rcode-tools brain pull as a child process. Placeholder URLs
2755
- // are skipped gracefully so this does not fail a fresh install.
2775
+ // Runs rcode-tools brain pull as a detached background process. Placeholder
2776
+ // URLs are skipped gracefully so this does not fail a fresh install.
2777
+ //
2778
+ // Issue #1030: a cold pull (cache miss) clones + sparse-checks-out a real
2779
+ // upstream repo and live-measured ~58s for just 2 small files โ€” dangerously
2780
+ // close to the previous 60s execFileSync timeout (#706) and ~6x over the
2781
+ // 10s kill criterion issue #162 itself specified. Since brain pull is
2782
+ // already best-effort and never fails install (see catch below, historically
2783
+ // a timeout was just treated as a pull failure), there is no reason to block
2784
+ // install on it at all. Spawn it detached and let install finish immediately;
2785
+ // the child keeps running and warms the cache/writes content on its own.
2756
2786
  let brainReport = null;
2787
+ let brainBackgrounded = false;
2757
2788
  try {
2758
- const { execFileSync } = require('child_process');
2789
+ const { spawn } = require('child_process');
2759
2790
  const toolsPath = path.join(opts.target, '.rcode', 'bin', 'rcode-tools.cjs');
2760
2791
  if (fs.existsSync(toolsPath)) {
2761
- // Issue #706: 60s timeout โ€” without it, a slow upstream URL hangs the
2762
- // entire install indefinitely. Brain pull is best-effort, so a timeout
2763
- // failure is treated identically to any other pull failure (caught below).
2764
- const out = execFileSync('node', [toolsPath, 'brain', 'pull'], {
2792
+ const child = spawn('node', [toolsPath, 'brain', 'pull'], {
2765
2793
  cwd: opts.target,
2766
- encoding: 'utf8',
2767
- stdio: ['ignore', 'pipe', 'pipe'],
2768
- timeout: 60_000,
2794
+ stdio: 'ignore',
2795
+ detached: true,
2769
2796
  });
2770
- try { brainReport = JSON.parse(out); } catch {}
2797
+ child.unref();
2798
+ brainBackgrounded = true;
2771
2799
  }
2772
2800
  } catch (e) {
2773
2801
  // brain pull is best-effort on install โ€” do not fail the whole install
@@ -2780,11 +2808,8 @@ async function installInner(opts) {
2780
2808
  if (opts.force && existedBefore) {
2781
2809
  console.log(' ' + warn('config.yaml and state.json preserved (pass --reset to wipe)'));
2782
2810
  }
2783
- if (brainReport && brainReport.ok) {
2784
- const pulledCount = (brainReport.pulled || []).length;
2785
- const skippedCount = (brainReport.skipped || []).length;
2786
- console.log(' ' + ok(`Brain: ${pulledCount} source${pulledCount === 1 ? '' : 's'} pulled` +
2787
- (skippedCount ? `, ${skippedCount} skipped (placeholder URLs)` : '')));
2811
+ if (brainBackgrounded) {
2812
+ console.log(' ' + dim('Brain: pulling in background (may take up to a minute on a cold cache; run `rcode brain status` to check)'));
2788
2813
  } else if (brainReport && brainReport.error) {
2789
2814
  console.log(' ' + dim(`Brain: skipped (${brainReport.error})`));
2790
2815
  }
@@ -132,6 +132,40 @@ function findCrossScopeDupes(projectClaudeDir, globalClaudeDir) {
132
132
  return dupes;
133
133
  }
134
134
 
135
+ /**
136
+ * Legacy rihal-* flat command files under ~/.rcode/slash-commands/, whose
137
+ * rcode-* twin already exists in the SAME dir. Read-only.
138
+ *
139
+ * This is the ONLY per-file location that's actually Codex-specific (#1024):
140
+ * `cli/install.js`'s `--global` install writes every command body here (a
141
+ * single flat dir shared by the Codex + Antigravity prompt-submit hook
142
+ * router โ€” see `installSlashRouterCommands()`), and this is where a stale
143
+ * rihal-* copy from before the rename would still live. The comment in
144
+ * install.js claiming Codex reads `~/.codex/prompts/*.md` is inaccurate โ€”
145
+ * that path is never written by this codebase; verified by reading
146
+ * install.js's actual writer functions rather than trusting the comment.
147
+ * Project-level Codex installs (agentsDir/commandsDir under `.claude/`) are
148
+ * already covered by findLegacyRihalArtifacts() since Codex shares that
149
+ * layout with claude/grok.
150
+ */
151
+ function findLegacyCodexSlashCommands(homeDir) {
152
+ const dupes = [];
153
+ const dir = path.join(homeDir, '.rcode', 'slash-commands');
154
+ for (const entry of listDirSafe(dir)) {
155
+ if (!entry.isFile() || !entry.name.startsWith('rihal-') || !entry.name.endsWith('.md')) continue;
156
+ const twinName = 'rcode-' + entry.name.slice('rihal-'.length);
157
+ if (fs.existsSync(path.join(dir, twinName))) {
158
+ dupes.push({
159
+ name: entry.name,
160
+ twin: twinName,
161
+ srcPath: path.join(dir, entry.name),
162
+ kind: 'command',
163
+ });
164
+ }
165
+ }
166
+ return dupes;
167
+ }
168
+
135
169
  /**
136
170
  * Full duplication report across project + global scopes. Pure read โ€” safe
137
171
  * to call from `rcode doctor` on every run.
@@ -145,19 +179,30 @@ function scanNamespaceDuplication(projectDir, homeDir) {
145
179
  const projectUnprefixed = findUnprefixedTwinDupes(projectClaudeDir);
146
180
  const globalUnprefixed = findUnprefixedTwinDupes(globalClaudeDir);
147
181
  const crossScope = findCrossScopeDupes(projectClaudeDir, globalClaudeDir);
182
+ const codexSlashCommands = findLegacyCodexSlashCommands(homeDir);
148
183
 
149
184
  const legacySkillCount = projectLegacy.skills.length + globalLegacy.skills.length;
150
185
  const legacyCommandCount = projectLegacy.commands.length + globalLegacy.commands.length;
186
+ const legacyAgentCount = projectLegacy.agents.length + globalLegacy.agents.length;
151
187
  const unprefixedCount = projectUnprefixed.length + globalUnprefixed.length;
152
188
  const crossScopeCount = crossScope.length;
189
+ const legacyCodexCommandCount = codexSlashCommands.length;
153
190
 
154
191
  return {
155
192
  legacySkillCount,
156
193
  legacyCommandCount,
194
+ legacyAgentCount,
157
195
  unprefixedCount,
158
196
  crossScopeCount,
159
- totalCount: legacySkillCount + legacyCommandCount + unprefixedCount + crossScopeCount,
160
- detail: { projectLegacy, globalLegacy, projectUnprefixed, globalUnprefixed, crossScope },
197
+ legacyCodexCommandCount,
198
+ totalCount:
199
+ legacySkillCount +
200
+ legacyCommandCount +
201
+ legacyAgentCount +
202
+ unprefixedCount +
203
+ crossScopeCount +
204
+ legacyCodexCommandCount,
205
+ detail: { projectLegacy, globalLegacy, projectUnprefixed, globalUnprefixed, crossScope, codexSlashCommands },
161
206
  };
162
207
  }
163
208
 
@@ -169,7 +214,8 @@ function scanNamespaceDuplication(projectDir, homeDir) {
169
214
  */
170
215
  function backupAndRemove(srcPath, backupRoot, scope, kind) {
171
216
  if (!fs.existsSync(srcPath)) return false;
172
- const destDir = path.join(backupRoot, scope, kind === 'skill' ? 'skills' : 'commands');
217
+ const destDirName = kind === 'skill' ? 'skills' : kind === 'agent' ? 'agents' : 'commands';
218
+ const destDir = path.join(backupRoot, scope, destDirName);
173
219
  fs.mkdirSync(destDir, { recursive: true });
174
220
  const dest = path.join(destDir, path.basename(srcPath));
175
221
  fs.cpSync(srcPath, dest, { recursive: true });
@@ -194,7 +240,14 @@ function migrateNamespace(projectDir, homeDir) {
194
240
 
195
241
  const summary = {
196
242
  backupDir: null,
197
- removed: { legacySkills: 0, legacyCommands: 0, unprefixedDupes: 0, crossScopeDupes: 0 },
243
+ removed: {
244
+ legacySkills: 0,
245
+ legacyCommands: 0,
246
+ legacyAgents: 0,
247
+ unprefixedDupes: 0,
248
+ crossScopeDupes: 0,
249
+ legacyCodexCommands: 0,
250
+ },
198
251
  };
199
252
 
200
253
  const removeAll = (items, scope, kind) => {
@@ -211,10 +264,14 @@ function migrateNamespace(projectDir, homeDir) {
211
264
  summary.removed.legacyCommands =
212
265
  removeAll(scan.detail.projectLegacy.commands, 'project', 'command') +
213
266
  removeAll(scan.detail.globalLegacy.commands, 'global', 'command');
267
+ summary.removed.legacyAgents =
268
+ removeAll(scan.detail.projectLegacy.agents, 'project', 'agent') +
269
+ removeAll(scan.detail.globalLegacy.agents, 'global', 'agent');
214
270
  summary.removed.unprefixedDupes =
215
271
  removeAll(scan.detail.projectUnprefixed, 'project', 'command') +
216
272
  removeAll(scan.detail.globalUnprefixed, 'global', 'command');
217
273
  summary.removed.crossScopeDupes = removeAll(scan.detail.crossScope, 'global', 'command');
274
+ summary.removed.legacyCodexCommands = removeAll(scan.detail.codexSlashCommands, 'global', 'command');
218
275
 
219
276
  const totalRemoved = Object.values(summary.removed).reduce((a, b) => a + b, 0);
220
277
  if (totalRemoved > 0) summary.backupDir = backupRoot;
@@ -226,6 +283,7 @@ module.exports = {
226
283
  findLegacyRihalArtifacts,
227
284
  findUnprefixedTwinDupes,
228
285
  findCrossScopeDupes,
286
+ findLegacyCodexSlashCommands,
229
287
  scanNamespaceDuplication,
230
288
  migrateNamespace,
231
289
  };
@@ -26,8 +26,10 @@ function printScanSummary(scan) {
26
26
  console.log(` Found ${scan.totalCount} artifact(s) to migrate:`);
27
27
  console.log(` legacy rihal-* skills: ${scan.legacySkillCount}`);
28
28
  console.log(` legacy rihal-* commands: ${scan.legacyCommandCount}`);
29
+ console.log(` legacy rihal-* agents: ${scan.legacyAgentCount}`);
29
30
  console.log(` unprefixed dupes: ${scan.unprefixedCount}`);
30
31
  console.log(` cross-scope dupes: ${scan.crossScopeCount} (global copy shadowed by project)`);
32
+ console.log(` legacy Codex commands: ${scan.legacyCodexCommandCount} (~/.rcode/slash-commands/)`);
31
33
  console.log();
32
34
  }
33
35
 
@@ -50,8 +52,10 @@ module.exports = function migrateNamespaceCommand(args = []) {
50
52
  console.log(` โœ“ Removed ${total} artifact(s):`);
51
53
  console.log(` legacy skills: ${summary.removed.legacySkills}`);
52
54
  console.log(` legacy commands: ${summary.removed.legacyCommands}`);
55
+ console.log(` legacy agents: ${summary.removed.legacyAgents}`);
53
56
  console.log(` unprefixed: ${summary.removed.unprefixedDupes}`);
54
57
  console.log(` cross-scope: ${summary.removed.crossScopeDupes}`);
58
+ console.log(` codex commands: ${summary.removed.legacyCodexCommands}`);
55
59
  if (summary.backupDir) {
56
60
  console.log(` ๐Ÿ’พ backup: ${path.relative(home, summary.backupDir)} (under ~/.claude/.rcode-backup/)`);
57
61
  }