@hanzlaa/rcode 4.4.3 → 4.5.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.
package/AGENTS.md CHANGED
@@ -68,8 +68,8 @@ If a user says "just keep going" or "don't stop until done", that authorization
68
68
 
69
69
  ## Dashboard Server Rules
70
70
 
71
- - Keep `server/dashboard.js` dependency-free (pure Node stdlib)
72
- - Single-filedo not introduce a framework
71
+ - `server/dashboard.js` is the entry point; server-side logic lives in `server/lib/` (`api.js`, `scanner.js`, `html/`) and `server/orchestrator.js` — split across files, not single-file
72
+ - No external framework dependency on the server or client the client is a Preact SPA built on a vendored copy (`server/lib/html/client/vendor/preact.js`), not an npm package; keep it that way
73
73
  - View-only — NEVER add write endpoints, POST handlers, or database code
74
74
  - Test after every change: `node server/dashboard.js` must start cleanly
75
75
 
@@ -84,9 +84,16 @@ If a user says "just keep going" or "don't stop until done", that authorization
84
84
  node cli/doctor.js # full preflight + compliance
85
85
  node --test test/artifact-schema.test.cjs # schema validators only
86
86
  ```
87
- - Run grep checks before committing renames or refactors:
87
+ - Run grep checks before committing renames or refactors. Scope to `TODO:`
88
+ and code files only — `rcode/`, `docs/`, and skill/workflow markdown
89
+ legitimately discuss the concept of TODOs (stub-detection guides, the
90
+ `add-todo`/`check-todos` commands, checklists); an unscoped `-i "TODO"`
91
+ grep will always be red on this repo:
88
92
  ```bash
89
- grep -rn -i "TODO" rcode docs examples README.md server # should be empty
93
+ grep -rn "TODO:" rcode docs examples README.md server \
94
+ --include="*.js" --include="*.cjs" --include="*.mjs" --include="*.ts"
95
+ # should be empty, or only match deliberate string/regex literals
96
+ # (e.g. rcode/bin/rcode-hooks.cjs's hedging-language detector)
90
97
  ```
91
98
  - Test the dashboard server boots without errors before committing dashboard changes
92
99
 
package/CLAUDE.md CHANGED
@@ -58,8 +58,8 @@ If a user says "just keep going" or "don't stop until done", that authorization
58
58
 
59
59
  ## Dashboard Server Rules
60
60
 
61
- - Keep `server/dashboard.js` dependency-free (pure Node stdlib)
62
- - Single-filedo not introduce a framework
61
+ - `server/dashboard.js` is the entry point; server-side logic lives in `server/lib/` (`api.js`, `scanner.js`, `html/`) and `server/orchestrator.js` — split across files, not single-file
62
+ - No external framework dependency on the server or client the client is a Preact SPA built on a vendored copy (`server/lib/html/client/vendor/preact.js`), not an npm package; keep it that way
63
63
  - View-only — NEVER add write endpoints, POST handlers, or database code
64
64
  - Test after every change: `node server/dashboard.js` must start cleanly
65
65
 
@@ -74,9 +74,16 @@ If a user says "just keep going" or "don't stop until done", that authorization
74
74
  grep -q "^## Examples" "$f" || echo "MISSING: $f"
75
75
  done
76
76
  ```
77
- - Run grep checks before committing renames or refactors:
77
+ - Run grep checks before committing renames or refactors. Scope to `TODO:`
78
+ and code files only — `rcode/`, `docs/`, and skill/workflow markdown
79
+ legitimately discuss the concept of TODOs (stub-detection guides, the
80
+ `add-todo`/`check-todos` commands, checklists); an unscoped `-i "TODO"`
81
+ grep will always be red on this repo:
78
82
  ```bash
79
- grep -rn -i "TODO" rcode docs examples README.md server # should be empty
83
+ grep -rn "TODO:" rcode docs examples README.md server \
84
+ --include="*.js" --include="*.cjs" --include="*.mjs" --include="*.ts"
85
+ # should be empty, or only match deliberate string/regex literals
86
+ # (e.g. rcode/bin/rcode-hooks.cjs's hedging-language detector)
80
87
  ```
81
88
  - Test the dashboard server boots without errors before committing dashboard changes
82
89
 
package/CONTRIBUTING.md CHANGED
@@ -399,7 +399,12 @@ chore(github): import Siraaj commit and PR rules
399
399
  ```
400
400
  4. **Grep for regressions:**
401
401
  ```bash
402
- grep -rn -i "TODO" rcode docs examples README.md server # should be empty
402
+ # Scope to "TODO:" and code files rcode/, docs/, and skill/workflow
403
+ # markdown legitimately discuss TODOs (stub-detection guides, the
404
+ # add-todo/check-todos commands); an unscoped -i "TODO" grep is always red.
405
+ grep -rn "TODO:" rcode docs examples README.md server \
406
+ --include="*.js" --include="*.cjs" --include="*.mjs" --include="*.ts"
407
+ # should be empty, or only match deliberate string/regex literals
403
408
  grep -rn "ahmed" rcode docs README.md server | grep -v "ahmed-hassani\|Ahmed Al Hassani" # should be empty
404
409
  ```
405
410
 
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.4.3 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.4.4 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.
27
27
 
28
28
  ---
29
29
 
package/cli/doctor.js CHANGED
@@ -17,6 +17,8 @@ const path = require('path');
17
17
  const { spawnSync } = require('child_process');
18
18
  const { verifyInstall, formatReport } = require('./lib/manifest.cjs');
19
19
  const { checkStaleness } = require('./lib/memory-bank.cjs');
20
+ const { homedir } = require('./lib/homedir.cjs');
21
+ const { scanNamespaceDuplication } = require('./lib/namespace-migrate.cjs');
20
22
  const {
21
23
  parseFrontmatter,
22
24
  validateSkillFrontmatter,
@@ -102,6 +104,29 @@ function commandAvailable(cmd) {
102
104
  return result.status === 0;
103
105
  }
104
106
 
107
+ /**
108
+ * Detect phases stuck at status 'executing' while a numerically later phase
109
+ * has already reached 'complete'. That combination only happens when an
110
+ * executor crashed/was killed without updating state.json — a real phase
111
+ * would never be marked complete while an earlier one is still mid-run.
112
+ * Read-only; used by `rcode doctor` to flag stale state for manual repair.
113
+ */
114
+ function findStuckExecutingPhases(state) {
115
+ const phases = Array.isArray(state?.phases) ? state.phases : [];
116
+ const executing = phases.filter((p) => p && p.status === 'executing');
117
+ if (executing.length === 0) return [];
118
+ const maxCompleteNumber = phases
119
+ .filter((p) => p && p.status === 'complete')
120
+ .map((p) => parseFloat(p.number))
121
+ .filter((n) => !Number.isNaN(n))
122
+ .reduce((max, n) => Math.max(max, n), -Infinity);
123
+ if (maxCompleteNumber === -Infinity) return [];
124
+ return executing.filter((p) => {
125
+ const n = parseFloat(p.number);
126
+ return !Number.isNaN(n) && n < maxCompleteNumber;
127
+ });
128
+ }
129
+
105
130
  // ---------- Preflight checks ----------
106
131
 
107
132
  /**
@@ -192,6 +217,36 @@ function runPreflight(cwd, packageRoot) {
192
217
  : 'not found (github-sync unavailable)',
193
218
  });
194
219
 
220
+ // 5b. Namespace duplication (#954) — legacy rihal-* entries whose rcode-*
221
+ // twin already exists, plus unprefixed/cross-scope command dupes. Doesn't
222
+ // require .rcode/ init since it inspects .claude/ directly. Read-only —
223
+ // fix path is `rcode update` or `rcode migrate-namespace --yes`.
224
+ try {
225
+ const nsDup = scanNamespaceDuplication(cwd, homedir());
226
+ if (nsDup.totalCount > 0) {
227
+ checks.push({
228
+ label: 'Namespace duplication',
229
+ status: 'warn',
230
+ message:
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\`)`,
234
+ });
235
+ } else {
236
+ checks.push({
237
+ label: 'Namespace duplication',
238
+ status: 'ok',
239
+ message: 'no duplicate rihal-*/rcode-* registrations found',
240
+ });
241
+ }
242
+ } catch (e) {
243
+ checks.push({
244
+ label: 'Namespace duplication',
245
+ status: 'warn',
246
+ message: `could not scan: ${e.message}`,
247
+ });
248
+ }
249
+
195
250
  // 6. Agent manifest drift (only if .rcode/ is initialized — indicates installed editors)
196
251
  if (fs.existsSync(rcodeDir)) {
197
252
  const editors = [];
@@ -254,6 +309,69 @@ function runPreflight(cwd, packageRoot) {
254
309
  message: `STALE — ${staleness.reasons[0]}${staleness.reasons.length > 1 ? ` (+${staleness.reasons.length - 1} more)` : ''}`,
255
310
  });
256
311
  }
312
+
313
+ // 8. .rcode/data/intent-table.json presence (#952/#954). Installer ships
314
+ // rcode/data/ into consumer projects as of #952; if it's missing, the
315
+ // prompt-router degrades to a no-op with no visible error. Warn-only —
316
+ // the fail-open behavior in rcode-hooks.cjs keeps hooks working either way.
317
+ const intentTablePath = path.join(rcodeDir, 'data', 'intent-table.json');
318
+ checks.push({
319
+ label: 'intent-table.json',
320
+ status: fs.existsSync(intentTablePath) ? 'ok' : 'warn',
321
+ message: fs.existsSync(intentTablePath)
322
+ ? path.relative(cwd, intentTablePath)
323
+ : 'missing — run `npx @hanzlaa/rcode update` to sync .rcode/data/',
324
+ });
325
+
326
+ // 9. Stale phase state — a phase stuck 'executing' while a numerically
327
+ // later phase has already reached 'complete' means an executor died
328
+ // without updating state.json. Warn-only; fix is manual state repair.
329
+ const statePath = path.join(rcodeDir, 'state.json');
330
+ if (fs.existsSync(statePath)) {
331
+ try {
332
+ const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
333
+ const stuck = findStuckExecutingPhases(state);
334
+ checks.push({
335
+ label: 'Phase state',
336
+ status: stuck.length > 0 ? 'warn' : 'ok',
337
+ message:
338
+ stuck.length > 0
339
+ ? `${stuck.length} phase(s) stuck 'executing' while a later phase is complete: ${stuck.map((p) => p.number).join(', ')}`
340
+ : 'no stuck phases',
341
+ });
342
+ } catch (e) {
343
+ checks.push({
344
+ label: 'Phase state',
345
+ status: 'warn',
346
+ message: `state.json unreadable: ${e.message}`,
347
+ });
348
+ }
349
+ }
350
+
351
+ // 10. Memory INDEX.md staleness — separate from the fingerprint-based
352
+ // "Memory bank" check above (#7), which tracks .rcode/context/. This is
353
+ // a simple wall-clock check on .rcode/memory/INDEX.md, the distilled
354
+ // memory index every workflow reads.
355
+ const memoryIndexPath = path.join(rcodeDir, 'memory', 'INDEX.md');
356
+ if (fs.existsSync(memoryIndexPath)) {
357
+ const ageDays = Math.floor(
358
+ (Date.now() - fs.statSync(memoryIndexPath).mtimeMs) / (24 * 60 * 60 * 1000),
359
+ );
360
+ checks.push({
361
+ label: 'Memory INDEX.md',
362
+ status: ageDays > 30 ? 'warn' : 'ok',
363
+ message:
364
+ ageDays > 30
365
+ ? `STALE — last updated ${ageDays}d ago (run /rcode-memory-update)`
366
+ : `fresh — updated ${ageDays}d ago`,
367
+ });
368
+ } else {
369
+ checks.push({
370
+ label: 'Memory INDEX.md',
371
+ status: 'warn',
372
+ message: 'missing (run /rcode-memory-init)',
373
+ });
374
+ }
257
375
  }
258
376
 
259
377
  return checks;
@@ -479,3 +597,7 @@ module.exports = function doctor(args, { packageRoot }) {
479
597
  console.log();
480
598
  process.exit(totalFailures > 0 ? 1 : 0);
481
599
  };
600
+
601
+ // Re-exports for unit tests.
602
+ module.exports.findStuckExecutingPhases = findStuckExecutingPhases;
603
+ module.exports.runPreflight = runPreflight;
package/cli/index.js CHANGED
@@ -27,6 +27,7 @@ const COMMANDS = {
27
27
  uninstall: require('./uninstall'),
28
28
  remove: require('./uninstall'), // alias
29
29
  nuke: require('./nuke'), // full cleanup across all package managers + global state
30
+ 'migrate-namespace': require('./migrate-namespace'), // clean up legacy rihal-* dupes (#954)
30
31
  dashboard: require('./dashboard'),
31
32
  serve: require('./dashboard'),
32
33
  digest: require('./digest'),
@@ -73,6 +74,9 @@ Usage:
73
74
  binaries, ~/.claude/* rcode artifacts, ~/.rcode/, project artifacts)
74
75
  Default = dry-run. Pass --yes to remove. Pass --include-planning
75
76
  to also remove .planning/ in CWD.
77
+ migrate-namespace Back up + remove legacy rihal-* skills/commands whose
78
+ rcode-* twin exists, plus unprefixed/cross-scope command dupes.
79
+ Default = dry-run. Pass --yes to remove. Idempotent.
76
80
  config Get/set project configuration (project_name, user_name, etc.)
77
81
  context Memory bank freshness (--check | --refresh | --install-hook)
78
82
  github-sync Sync .rcode/ phases/epics/stories to GitHub (dry-run default)
package/cli/install.js CHANGED
@@ -888,6 +888,7 @@ function ensureRcodeGitignore(target, options = {}) {
888
888
  '# Installed methodology files (regenerate with: npx @hanzlaa/rcode install)',
889
889
  '.claude/',
890
890
  '.rcode/bin/',
891
+ '.rcode/data/',
891
892
  '.rcode/workflows/',
892
893
  '.rcode/references/',
893
894
  '.rcode/commands/',
@@ -1310,6 +1311,12 @@ function buildInstallPlan(ide = 'claude', target = process.cwd()) {
1310
1311
  plan.push({ src: f, rel: path.join(relBin, rel), executable: f.endsWith('.cjs') });
1311
1312
  }
1312
1313
 
1314
+ // .rcode/data/
1315
+ for (const f of walkFiles(path.join(SOURCE_ROOT, 'data'))) {
1316
+ const rel = path.relative(path.join(SOURCE_ROOT, 'data'), f);
1317
+ plan.push({ src: f, rel: path.join('.rcode', 'data', rel) });
1318
+ }
1319
+
1313
1320
  // .rcode/templates/projects/ — starter templates consumed by /rcode-from-template
1314
1321
  const projectTemplatesSrc = path.join(SOURCE_ROOT, 'templates', 'projects');
1315
1322
  const relProjectTemplates = path.relative(target, path.join(target, '.rcode', 'templates', 'projects'));
@@ -1418,6 +1425,7 @@ function filterPlanByModules(plan, moduleNames) {
1418
1425
  // Always include bin/ (shared infrastructure, not module-specific)
1419
1426
  return plan.filter((entry) => {
1420
1427
  if (entry.rel.startsWith(path.join('.rcode', 'bin'))) return true;
1428
+ if (entry.rel.startsWith(path.join('.rcode', 'data'))) return true;
1421
1429
  return allowed.has(entry.rel);
1422
1430
  });
1423
1431
  }
@@ -0,0 +1,216 @@
1
+ 'use strict';
2
+ /**
3
+ * Legacy rihal-* → rcode-* namespace cleanup (#954).
4
+ *
5
+ * The rcode↔rihal rename left rihal-* skills/commands installed alongside
6
+ * their rcode-* twins everywhere — every session loaded each command 2-4x.
7
+ * This module scans for safe-to-remove legacy artifacts (a rihal-* entry is
8
+ * only ever flagged when its rcode-* twin already exists — never delete
9
+ * something with no replacement) and, on request, backs them up under
10
+ * ~/.claude/.rcode-backup/<timestamp>/ before deleting.
11
+ *
12
+ * Read-only scan functions (findLegacyRihalArtifacts, findUnprefixedTwinDupes,
13
+ * findCrossScopeDupes, scanNamespaceDuplication) are reused by `rcode doctor`
14
+ * for report-only checks. The destructive migrateNamespace() is reused by
15
+ * `rcode update` and `rcode migrate-namespace`.
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+
21
+ function listDirSafe(dir) {
22
+ if (!fs.existsSync(dir)) return [];
23
+ try {
24
+ return fs.readdirSync(dir, { withFileTypes: true });
25
+ } catch {
26
+ return [];
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Legacy rihal-* skill dirs / command files whose rcode-* twin exists in
32
+ * the SAME .claude dir. Read-only.
33
+ */
34
+ function findLegacyRihalArtifacts(claudeDir) {
35
+ const skills = [];
36
+ const skillsDir = path.join(claudeDir, 'skills');
37
+ for (const entry of listDirSafe(skillsDir)) {
38
+ if (!entry.isDirectory() || !entry.name.startsWith('rihal-')) continue;
39
+ const twinName = 'rcode-' + entry.name.slice('rihal-'.length);
40
+ if (fs.existsSync(path.join(skillsDir, twinName))) {
41
+ skills.push({
42
+ name: entry.name,
43
+ twin: twinName,
44
+ srcPath: path.join(skillsDir, entry.name),
45
+ kind: 'skill',
46
+ });
47
+ }
48
+ }
49
+
50
+ const commands = [];
51
+ const commandsDir = path.join(claudeDir, 'commands');
52
+ for (const entry of listDirSafe(commandsDir)) {
53
+ if (!entry.isFile() || !entry.name.startsWith('rihal-') || !entry.name.endsWith('.md')) continue;
54
+ const twinName = 'rcode-' + entry.name.slice('rihal-'.length);
55
+ if (fs.existsSync(path.join(commandsDir, twinName))) {
56
+ commands.push({
57
+ name: entry.name,
58
+ twin: twinName,
59
+ srcPath: path.join(commandsDir, entry.name),
60
+ kind: 'command',
61
+ });
62
+ }
63
+ }
64
+
65
+ return { skills, commands };
66
+ }
67
+
68
+ /**
69
+ * Unprefixed command files (e.g. `do.md`) that duplicate an already-installed
70
+ * `rcode-do.md` twin in the SAME .claude/commands dir. Read-only.
71
+ */
72
+ function findUnprefixedTwinDupes(claudeDir) {
73
+ const dupes = [];
74
+ const commandsDir = path.join(claudeDir, 'commands');
75
+ for (const entry of listDirSafe(commandsDir)) {
76
+ if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
77
+ if (entry.name.startsWith('rcode-') || entry.name.startsWith('rihal-')) continue;
78
+ const twinName = 'rcode-' + entry.name;
79
+ if (fs.existsSync(path.join(commandsDir, twinName))) {
80
+ dupes.push({
81
+ name: entry.name,
82
+ twin: twinName,
83
+ srcPath: path.join(commandsDir, entry.name),
84
+ kind: 'command',
85
+ });
86
+ }
87
+ }
88
+ return dupes;
89
+ }
90
+
91
+ /**
92
+ * Unprefixed command basenames registered in BOTH project and global scope.
93
+ * Project always wins — the returned entries point at the GLOBAL copy,
94
+ * which is the one safe to remove (the project copy stays canonical).
95
+ */
96
+ function findCrossScopeDupes(projectClaudeDir, globalClaudeDir) {
97
+ const dupes = [];
98
+ if (path.resolve(projectClaudeDir) === path.resolve(globalClaudeDir)) return dupes;
99
+ const projectCommandsDir = path.join(projectClaudeDir, 'commands');
100
+ const globalCommandsDir = path.join(globalClaudeDir, 'commands');
101
+ const projectNames = new Set(
102
+ listDirSafe(projectCommandsDir)
103
+ .filter((e) => e.isFile() && e.name.endsWith('.md'))
104
+ .map((e) => e.name),
105
+ );
106
+ for (const entry of listDirSafe(globalCommandsDir)) {
107
+ if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
108
+ if (projectNames.has(entry.name)) {
109
+ dupes.push({
110
+ name: entry.name,
111
+ srcPath: path.join(globalCommandsDir, entry.name),
112
+ keptPath: path.join(projectCommandsDir, entry.name),
113
+ kind: 'command',
114
+ });
115
+ }
116
+ }
117
+ return dupes;
118
+ }
119
+
120
+ /**
121
+ * Full duplication report across project + global scopes. Pure read — safe
122
+ * to call from `rcode doctor` on every run.
123
+ */
124
+ function scanNamespaceDuplication(projectDir, homeDir) {
125
+ const projectClaudeDir = path.join(projectDir, '.claude');
126
+ const globalClaudeDir = path.join(homeDir, '.claude');
127
+
128
+ const projectLegacy = findLegacyRihalArtifacts(projectClaudeDir);
129
+ const globalLegacy = findLegacyRihalArtifacts(globalClaudeDir);
130
+ const projectUnprefixed = findUnprefixedTwinDupes(projectClaudeDir);
131
+ const globalUnprefixed = findUnprefixedTwinDupes(globalClaudeDir);
132
+ const crossScope = findCrossScopeDupes(projectClaudeDir, globalClaudeDir);
133
+
134
+ const legacySkillCount = projectLegacy.skills.length + globalLegacy.skills.length;
135
+ const legacyCommandCount = projectLegacy.commands.length + globalLegacy.commands.length;
136
+ const unprefixedCount = projectUnprefixed.length + globalUnprefixed.length;
137
+ const crossScopeCount = crossScope.length;
138
+
139
+ return {
140
+ legacySkillCount,
141
+ legacyCommandCount,
142
+ unprefixedCount,
143
+ crossScopeCount,
144
+ totalCount: legacySkillCount + legacyCommandCount + unprefixedCount + crossScopeCount,
145
+ detail: { projectLegacy, globalLegacy, projectUnprefixed, globalUnprefixed, crossScope },
146
+ };
147
+ }
148
+
149
+ /**
150
+ * Move `srcPath` under `backupRoot/<scope>/<skills|commands>/`, then delete
151
+ * the original. Idempotent: if srcPath no longer exists (already migrated
152
+ * in an earlier pass, or removed by an earlier step in this same pass),
153
+ * this is a no-op that returns false.
154
+ */
155
+ function backupAndRemove(srcPath, backupRoot, scope, kind) {
156
+ if (!fs.existsSync(srcPath)) return false;
157
+ const destDir = path.join(backupRoot, scope, kind === 'skill' ? 'skills' : 'commands');
158
+ fs.mkdirSync(destDir, { recursive: true });
159
+ const dest = path.join(destDir, path.basename(srcPath));
160
+ fs.cpSync(srcPath, dest, { recursive: true });
161
+ fs.rmSync(srcPath, { recursive: true, force: true });
162
+ return true;
163
+ }
164
+
165
+ /**
166
+ * Execute the cleanup: back up + remove every legacy rihal-* artifact and
167
+ * unprefixed/cross-scope duplicate found by scanNamespaceDuplication(), for
168
+ * real. Idempotent — running twice in a row removes 0 the second time
169
+ * because the scan re-runs fresh each call and finds nothing left to flag.
170
+ *
171
+ * Backups always land under ~/.claude/.rcode-backup/<timestamp>/ regardless
172
+ * of whether the artifact came from the project or global scope, so a
173
+ * single rollback location covers everything from one migrate run.
174
+ */
175
+ function migrateNamespace(projectDir, homeDir) {
176
+ const scan = scanNamespaceDuplication(projectDir, homeDir);
177
+ const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
178
+ const backupRoot = path.join(homeDir, '.claude', '.rcode-backup', ts);
179
+
180
+ const summary = {
181
+ backupDir: null,
182
+ removed: { legacySkills: 0, legacyCommands: 0, unprefixedDupes: 0, crossScopeDupes: 0 },
183
+ };
184
+
185
+ const removeAll = (items, scope, kind) => {
186
+ let count = 0;
187
+ for (const item of items) {
188
+ if (backupAndRemove(item.srcPath, backupRoot, scope, kind)) count++;
189
+ }
190
+ return count;
191
+ };
192
+
193
+ summary.removed.legacySkills =
194
+ removeAll(scan.detail.projectLegacy.skills, 'project', 'skill') +
195
+ removeAll(scan.detail.globalLegacy.skills, 'global', 'skill');
196
+ summary.removed.legacyCommands =
197
+ removeAll(scan.detail.projectLegacy.commands, 'project', 'command') +
198
+ removeAll(scan.detail.globalLegacy.commands, 'global', 'command');
199
+ summary.removed.unprefixedDupes =
200
+ removeAll(scan.detail.projectUnprefixed, 'project', 'command') +
201
+ removeAll(scan.detail.globalUnprefixed, 'global', 'command');
202
+ summary.removed.crossScopeDupes = removeAll(scan.detail.crossScope, 'global', 'command');
203
+
204
+ const totalRemoved = Object.values(summary.removed).reduce((a, b) => a + b, 0);
205
+ if (totalRemoved > 0) summary.backupDir = backupRoot;
206
+
207
+ return summary;
208
+ }
209
+
210
+ module.exports = {
211
+ findLegacyRihalArtifacts,
212
+ findUnprefixedTwinDupes,
213
+ findCrossScopeDupes,
214
+ scanNamespaceDuplication,
215
+ migrateNamespace,
216
+ };
@@ -0,0 +1,61 @@
1
+ /**
2
+ * rcode migrate-namespace — clean up legacy rihal-* skills/commands whose
3
+ * rcode-* twin already exists (#954), plus unprefixed/cross-scope command
4
+ * duplicates. Backs everything up under ~/.claude/.rcode-backup/<ts>/
5
+ * before removing. Idempotent — running twice removes nothing the second
6
+ * time.
7
+ *
8
+ * Default is dry-run (prints what WOULD be removed). Pass --yes to execute.
9
+ * `rcode update` also runs this automatically after refreshing files.
10
+ */
11
+
12
+ const path = require('path');
13
+ const { homedir } = require('./lib/homedir.cjs');
14
+ const { scanNamespaceDuplication, migrateNamespace } = require('./lib/namespace-migrate.cjs');
15
+
16
+ function parseArgs(args) {
17
+ return { yes: args.includes('--yes') || args.includes('-y') };
18
+ }
19
+
20
+ function printScanSummary(scan) {
21
+ console.log(`\n🕌 rcode — Namespace Migration\n`);
22
+ if (scan.totalCount === 0) {
23
+ console.log(` ✓ No legacy rihal-* or duplicate command registrations found.\n`);
24
+ return;
25
+ }
26
+ console.log(` Found ${scan.totalCount} artifact(s) to migrate:`);
27
+ console.log(` legacy rihal-* skills: ${scan.legacySkillCount}`);
28
+ console.log(` legacy rihal-* commands: ${scan.legacyCommandCount}`);
29
+ console.log(` unprefixed dupes: ${scan.unprefixedCount}`);
30
+ console.log(` cross-scope dupes: ${scan.crossScopeCount} (global copy shadowed by project)`);
31
+ console.log();
32
+ }
33
+
34
+ module.exports = function migrateNamespaceCommand(args = []) {
35
+ const opts = parseArgs(args);
36
+ const cwd = process.cwd();
37
+ const home = homedir();
38
+
39
+ const scan = scanNamespaceDuplication(cwd, home);
40
+ printScanSummary(scan);
41
+ if (scan.totalCount === 0) return;
42
+
43
+ if (!opts.yes) {
44
+ console.log(` [DRY RUN] Nothing removed. Pass --yes to back up and remove these files.\n`);
45
+ return;
46
+ }
47
+
48
+ const summary = migrateNamespace(cwd, home);
49
+ const total = Object.values(summary.removed).reduce((a, b) => a + b, 0);
50
+ console.log(` ✓ Removed ${total} artifact(s):`);
51
+ console.log(` legacy skills: ${summary.removed.legacySkills}`);
52
+ console.log(` legacy commands: ${summary.removed.legacyCommands}`);
53
+ console.log(` unprefixed: ${summary.removed.unprefixedDupes}`);
54
+ console.log(` cross-scope: ${summary.removed.crossScopeDupes}`);
55
+ if (summary.backupDir) {
56
+ console.log(` 💾 backup: ${path.relative(home, summary.backupDir)} (under ~/.claude/.rcode-backup/)`);
57
+ }
58
+ console.log();
59
+ };
60
+
61
+ module.exports.parseArgs = parseArgs;
package/cli/update.js CHANGED
@@ -446,6 +446,25 @@ async function runUpdate(args, { packageRoot, packageJson }) {
446
446
  }
447
447
  console.log(` ✓ ${path.relative(cwd, configPath)} → installed_version: ${packageVersion}`);
448
448
 
449
+ // ------ Legacy namespace cleanup (#954) ------
450
+ // Backs up + removes legacy rihal-* skills/commands whose rcode-* twin
451
+ // already exists, plus unprefixed/cross-scope command dupes. Idempotent —
452
+ // safe to run on every update even when there's nothing to migrate.
453
+ try {
454
+ const { migrateNamespace } = require('./lib/namespace-migrate.cjs');
455
+ const { homedir: getHomedir } = require('./lib/homedir.cjs');
456
+ const nsResult = migrateNamespace(cwd, getHomedir());
457
+ const nsTotal = Object.values(nsResult.removed).reduce((a, b) => a + b, 0);
458
+ if (nsTotal > 0) {
459
+ console.log(` 🧹 migrated ${nsTotal} legacy rihal-*/duplicate command registration(s)`);
460
+ if (nsResult.backupDir) {
461
+ console.log(` backup: ${nsResult.backupDir}`);
462
+ }
463
+ }
464
+ } catch (err) {
465
+ console.log(` ⚠ namespace migration skipped: ${err.message}`);
466
+ }
467
+
449
468
  // ------ Verify manifest ------
450
469
  console.log();
451
470
  const { reports, hasDrift } = verifyInstall(cwd, packageRoot, editors);