@lifeaitools/rdc-skills 0.31.2 → 0.33.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 (47) hide show
  1. package/.claude-plugin/plugin.json +8 -8
  2. package/.github/workflows/publish.yml +35 -3
  3. package/README.md +2 -2
  4. package/deploy/install-systemd.sh +0 -0
  5. package/guides/lessons-learned-spec.md +1 -1
  6. package/package.json +2 -2
  7. package/scripts/clean-malformed-registrations.js +257 -0
  8. package/skills/behavior-audit/SKILL.md +1 -1
  9. package/skills/brochure/SKILL.md +1 -1
  10. package/skills/{rdc-brochurify → brochurify}/SKILL.md +1 -1
  11. package/skills/build/SKILL.md +1 -1
  12. package/skills/channel-formatter/SKILL.md +1 -1
  13. package/skills/co-develop/SKILL.md +1 -1
  14. package/skills/collab/SKILL.md +1 -1
  15. package/skills/convert/SKILL.md +1 -1
  16. package/skills/deploy/SKILL.md +1 -1
  17. package/skills/design/SKILL.md +1 -1
  18. package/skills/edit/SKILL.md +1 -1
  19. package/skills/env/SKILL.md +1 -1
  20. package/skills/{rdc-extract-verifier-rules → extract-verifier-rules}/SKILL.md +1 -1
  21. package/skills/fixit/SKILL.md +1 -1
  22. package/skills/fs-mcp/SKILL.md +1 -1
  23. package/skills/handoff/SKILL.md +1 -1
  24. package/skills/help/SKILL.md +1 -1
  25. package/skills/housekeeping/SKILL.md +1 -1
  26. package/skills/onramp/SKILL.md +1 -1
  27. package/skills/overnight/SKILL.md +1 -1
  28. package/skills/plan/SKILL.md +1 -1
  29. package/skills/preplan/SKILL.md +1 -1
  30. package/skills/prototype/SKILL.md +1 -1
  31. package/skills/refactor/SKILL.md +1 -1
  32. package/skills/regen-media/SKILL.md +1 -1
  33. package/skills/release/SKILL.md +1 -1
  34. package/skills/report/SKILL.md +1 -1
  35. package/skills/review/SKILL.md +1 -1
  36. package/skills/self-test/SKILL.md +1 -1
  37. package/skills/status/SKILL.md +1 -1
  38. package/skills/terminal-config/SKILL.md +1 -1
  39. package/skills/tests/MATRIX.md +2 -2
  40. package/skills/watch/SKILL.md +1 -1
  41. package/skills/workitems/SKILL.md +1 -1
  42. package/tests/channel-formatter.contract.test.mjs +2 -2
  43. package/tests/mcp.test.mjs +1 -1
  44. package/tests/plugin-cache-clean-install.test.mjs +76 -0
  45. package/tests/plugin-namespace-names.test.mjs +125 -0
  46. /package/skills/tests/{rdc-rdc-brochurify.test.json → rdc-brochurify.test.json} +0 -0
  47. /package/skills/tests/{rdc-rdc-extract-verifier-rules.test.json → rdc-extract-verifier-rules.test.json} +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rdc",
3
- "version": "0.31.2",
3
+ "version": "0.33.0",
4
4
  "description": "RDC typed-agent dispatch skill suite for Claude Code — plan, build, review, overnight unattended builds with work-item tracking and TDD enforcement.",
5
5
  "author": {
6
6
  "name": "LIFEAI",
@@ -78,7 +78,7 @@
78
78
  "leads_to": [
79
79
  "convert",
80
80
  "brochure",
81
- "rdc-brochurify",
81
+ "brochurify",
82
82
  "lifeai-brochure-author"
83
83
  ],
84
84
  "sandbox_aware": true,
@@ -158,8 +158,8 @@
158
158
  "enabled_default": true,
159
159
  "codeflow_required": false
160
160
  },
161
- "rdc-brochurify": {
162
- "name": "rdc-brochurify",
161
+ "brochurify": {
162
+ "name": "brochurify",
163
163
  "slash": "rdc:brochurify",
164
164
  "category": "tooling",
165
165
  "usage": "rdc:brochurify <source|job-payload>",
@@ -234,7 +234,7 @@
234
234
  ],
235
235
  "follows": [],
236
236
  "leads_to": [
237
- "rdc-brochurify"
237
+ "brochurify"
238
238
  ],
239
239
  "sandbox_aware": false,
240
240
  "output_contract": "guides/output-contract.md",
@@ -999,8 +999,8 @@
999
999
  "enabled_default": true,
1000
1000
  "codeflow_required": true
1001
1001
  },
1002
- "rdc-extract-verifier-rules": {
1003
- "name": "rdc-extract-verifier-rules",
1002
+ "extract-verifier-rules": {
1003
+ "name": "extract-verifier-rules",
1004
1004
  "slash": "rdc:extract-verifier-rules",
1005
1005
  "category": "tooling",
1006
1006
  "usage": "rdc:extract-verifier-rules [enhancement-log]",
@@ -1032,7 +1032,7 @@
1032
1032
  "nightly cron at 3:00 AM PT"
1033
1033
  ],
1034
1034
  "follows": [
1035
- "rdc-brochurify"
1035
+ "brochurify"
1036
1036
  ],
1037
1037
  "leads_to": [
1038
1038
  "review"
@@ -1,5 +1,32 @@
1
1
  name: Publish to npm
2
2
 
3
+ # TRUSTED PUBLISHING (OIDC) — no stored npm credential.
4
+ #
5
+ # WHY THIS CHANGED. npm retired the token model this fleet was built on:
6
+ # 2025-12-09 every classic token permanently revoked
7
+ # 2025-11 granular tokens capped at 90 days; any expiring after 2026-02-03
8
+ # were forcibly moved to that date
9
+ # 2026-08 tokens with Bypass-2FA stop working entirely
10
+ # So `secrets.NPM_TOKEN` is a credential that expires on npm's schedule regardless of
11
+ # how carefully it is managed, and a release pipeline that depends on one is a pipeline
12
+ # that silently stops working. This job proves its identity to npm per-run instead, and
13
+ # there is nothing to rotate.
14
+ #
15
+ # It also publishes provenance automatically — cryptographic proof of the commit and
16
+ # workflow that built the tarball — which a token publish does not.
17
+ #
18
+ # ONE-TIME SETUP ON NPMJS, per package (this cannot be done from CI):
19
+ # npmjs.com -> package -> Settings -> Trusted Publisher
20
+ # organization/user : LIFEAI
21
+ # repository : rdc-skills
22
+ # workflow filename : publish.yml (filename only, not a path)
23
+ # allowed actions : must select at least one (required for configs created
24
+ # after 2026-05-20)
25
+ #
26
+ # A package that does not exist yet cannot be configured this way — npm requires the
27
+ # package to exist before its settings page does. Publish the first version with a
28
+ # short-lived token, then switch here.
29
+
3
30
  on:
4
31
  push:
5
32
  tags:
@@ -7,6 +34,7 @@ on:
7
34
 
8
35
  permissions:
9
36
  contents: read
37
+ id-token: write # REQUIRED — without it npm gets no OIDC identity to trust
10
38
 
11
39
  jobs:
12
40
  publish:
@@ -19,10 +47,14 @@ jobs:
19
47
  node-version: '24'
20
48
  registry-url: 'https://registry.npmjs.org'
21
49
 
50
+ # npm >= 11.5.1 is the first release that understands trusted publishing. The
51
+ # node-24 image ships an older npm, and the failure mode without this is an
52
+ # ordinary-looking auth error rather than anything naming OIDC.
53
+ - name: Ensure npm supports trusted publishing
54
+ run: npm install -g npm@latest && npm --version
55
+
22
56
  - name: Publish
23
57
  run: npm publish --access public
24
- env:
25
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
26
58
 
27
59
  - name: Notify clauth channel
28
60
  if: always()
@@ -31,4 +63,4 @@ jobs:
31
63
  STATUS="${{ job.status }}"
32
64
  curl -s -X POST "${{ secrets.CLAUTH_CHANNEL_URL }}" \
33
65
  -H "Content-Type: application/json" \
34
- -d "{\"event\":\"npm-publish\",\"package\":\"rdc-skills\",\"version\":\"${VERSION}\",\"status\":\"${STATUS}\",\"source\":\"github-actions\"}"
66
+ -d "{\"event\":\"npm-publish\",\"package\":\"rdc-skills\",\"version\":\"${VERSION}\",\"status\":\"${STATUS}\",\"source\":\"github-actions-oidc\"}"
package/README.md CHANGED
@@ -335,8 +335,8 @@ skills/
335
335
  plan/SKILL.md (create architecture + tasks)
336
336
  preplan/SKILL.md (research before planning)
337
337
  prototype/SKILL.md (build JSX prototype)
338
- rdc-brochurify/SKILL.md (Brochurify orchestration)
339
- rdc-extract-verifier-rules/SKILL.md (verifier rule extraction)
338
+ brochurify/SKILL.md (Brochurify orchestration)
339
+ extract-verifier-rules/SKILL.md (verifier rule extraction)
340
340
  refactor/SKILL.md (governed corrective takeover and consolidation)
341
341
  release/SKILL.md (atomic package/app release)
342
342
  report/SKILL.md (nightly report)
File without changes
@@ -30,7 +30,7 @@ gate, a tooling gotcha) used to survive only if someone hand-wrote a memory. Thi
30
30
  makes capture a **routine exit step** of every long skill, and triage a **routine phase**
31
31
  of the weekly housekeeping. Capture is cheap and append-only; triage is where fixes happen.
32
32
 
33
- Precedent: brochurify's `rdc-extract-verifier-rules` already does read-log → cluster →
33
+ Precedent: brochurify's `extract-verifier-rules` already does read-log → cluster →
34
34
  propose-rule for one domain. This generalizes that pattern fleet-wide.
35
35
 
36
36
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifeaitools/rdc-skills",
3
- "version": "0.31.2",
3
+ "version": "0.33.0",
4
4
  "description": "RDC typed-agent dispatch skill suite for Claude Code - plan, build, review, overnight builds",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -35,7 +35,7 @@
35
35
  "rdc-design": "node scripts/rdc-design-cli.mjs",
36
36
  "test:hooks": "node scripts/test-rdc-hooks.mjs",
37
37
  "test:truth-gate": "node tests/run-evidence-gate.test.mjs && node tests/work-item-exit-gate-l2.test.mjs && node tests/work-item-exit-gate-l3.test.mjs && node tests/require-work-item-on-commit.test.mjs && node tests/harness-gates.test.mjs",
38
- "test:acceptance": "node tests/acceptance.test.mjs && node tests/install-rdc-skills.test.mjs && node tests/help-surface.test.mjs && node tests/manifest-contract-fields.test.mjs && node tests/skill-test-matrix.test.mjs && node tests/completion-gate-supervisor-admission.test.mjs && node tests/curl-surface.test.mjs",
38
+ "test:acceptance": "node tests/acceptance.test.mjs && node tests/install-rdc-skills.test.mjs && node tests/help-surface.test.mjs && node tests/manifest-contract-fields.test.mjs && node tests/plugin-namespace-names.test.mjs && node tests/skill-test-matrix.test.mjs && node tests/completion-gate-supervisor-admission.test.mjs && node tests/curl-surface.test.mjs",
39
39
  "acceptance": "node scripts/acceptance.mjs --changed",
40
40
  "test:mcp": "node tests/mcp.test.mjs",
41
41
  "test:mcp:remote": "node tests/mcp.test.mjs --remote",
@@ -0,0 +1,257 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * clean-malformed-registrations — find (and optionally remove) rdc-skills
4
+ * registrations that are malformed or duplicated on an already-installed box.
5
+ *
6
+ * DRY RUN BY DEFAULT. Nothing is deleted unless you pass --apply.
7
+ *
8
+ * node scripts/clean-malformed-registrations.mjs # report only
9
+ * node scripts/clean-malformed-registrations.mjs --apply # remove
10
+ * node scripts/clean-malformed-registrations.mjs --project-root C:/Dev/regen-root
11
+ *
12
+ * What "malformed" means here
13
+ * ---------------------------
14
+ * `.claude-plugin/plugin.json` declares `name: "rdc"`, and Claude Code registers
15
+ * a plugin component as `<plugin>:<declared frontmatter name>`. A component whose
16
+ * own `name:` already spells the namespace gets it twice — `name: rdc:plan`
17
+ * renders `/rdc:rdc:plan`, and a directory called `rdc-brochurify` renders
18
+ * `rdc:rdc-brochurify`.
19
+ *
20
+ * Two independent defects produce what the operator sees, and this script
21
+ * separates them because the remedies differ:
22
+ *
23
+ * PREFIXING — a component declares the namespace itself. Fixed at the
24
+ * source (this repo). An installed copy carrying it is simply
25
+ * an old build: re-run the installer.
26
+ * MULTIPLICATION— the same component is registered from more than one live
27
+ * source at once (extra plugin-cache dirs, a stale marketplace
28
+ * clone that is a full second copy of commands/ + skills/,
29
+ * leftover skills/user orphans). Re-running the installer does
30
+ * not always clear these, which is what this script is for.
31
+ *
32
+ * Safety: only regenerable install artifacts are ever removed. The marketplace
33
+ * directory is a git clone — it is REPORTED with the exact command, never
34
+ * deleted, because destroying it loses nothing but confuses Claude Code's own
35
+ * marketplace sync.
36
+ */
37
+ 'use strict';
38
+ const fs = require('fs');
39
+ const path = require('path');
40
+ const os = require('os');
41
+
42
+ const args = process.argv.slice(2);
43
+ const APPLY = args.includes('--apply');
44
+ const argOf = (flag, fallback) => {
45
+ const i = args.indexOf(flag);
46
+ return i >= 0 && args[i + 1] ? args[i + 1] : fallback;
47
+ };
48
+ const CLAUDE_HOME = path.resolve(argOf('--claude-home', path.join(os.homedir(), '.claude')));
49
+ const PROJECT_ROOT = argOf('--project-root', null);
50
+ const MARKETPLACE = 'rdc-skills';
51
+
52
+ // The namespace is read from the installed plugin manifest when available, so
53
+ // this script stays correct if the plugin is ever renamed.
54
+ function pluginNamespace() {
55
+ const candidates = [
56
+ path.join(CLAUDE_HOME, 'plugins', 'cache', MARKETPLACE, 'rdc-skills', 'latest', '.claude-plugin', 'plugin.json'),
57
+ path.resolve(__dirname, '..', '.claude-plugin', 'plugin.json'),
58
+ ];
59
+ for (const p of candidates) {
60
+ try { return JSON.parse(fs.readFileSync(p, 'utf8')).name; } catch {}
61
+ }
62
+ return 'rdc';
63
+ }
64
+ const NS = pluginNamespace();
65
+
66
+ const findings = { prefixing: [], multiplication: [], report: [] };
67
+ const removals = []; // { path, why, kind: 'dir' | 'file' }
68
+
69
+ function declaredName(file) {
70
+ try {
71
+ const raw = fs.readFileSync(file, 'utf8').replace(/\r\n/g, '\n');
72
+ const block = raw.match(/^---\n([\s\S]*?)\n---/);
73
+ if (!block) return null;
74
+ const line = block[1].split('\n').find((l) => /^name:\s*/.test(l));
75
+ return line ? line.replace(/^name:\s*/, '').trim().replace(/^["']|["']$/g, '') : null;
76
+ } catch { return null; }
77
+ }
78
+
79
+ function isDoubled(effective) {
80
+ return effective === NS || effective.startsWith(`${NS}:`) || effective.startsWith(`${NS}-`);
81
+ }
82
+
83
+ /** Scan one installed plugin tree (commands/ + skills/) for doubled names. */
84
+ function scanTree(label, treeDir) {
85
+ if (!fs.existsSync(treeDir)) return;
86
+
87
+ const cmdDir = path.join(treeDir, 'commands');
88
+ if (fs.existsSync(cmdDir)) {
89
+ for (const f of fs.readdirSync(cmdDir).filter((f) => f.endsWith('.md')).sort()) {
90
+ const eff = declaredName(path.join(cmdDir, f)) ?? f.replace(/\.md$/, '');
91
+ if (isDoubled(eff)) {
92
+ findings.prefixing.push({ source: label, file: path.join(cmdDir, f), declared: eff, renders: `${NS}:${eff}` });
93
+ }
94
+ }
95
+ }
96
+
97
+ const skillsDir = path.join(treeDir, 'skills');
98
+ if (fs.existsSync(skillsDir)) {
99
+ for (const e of fs.readdirSync(skillsDir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
100
+ if (!e.isDirectory()) continue;
101
+ const md = path.join(skillsDir, e.name, 'SKILL.md');
102
+ if (!fs.existsSync(md)) continue;
103
+ const eff = declaredName(md) ?? e.name;
104
+ if (isDoubled(eff)) {
105
+ findings.prefixing.push({ source: label, file: md, declared: eff, renders: `${NS}:${eff}` });
106
+ }
107
+ }
108
+ }
109
+ }
110
+
111
+ // ── 1. Every live plugin source ───────────────────────────────────────────────
112
+ const pluginsDir = path.join(CLAUDE_HOME, 'plugins');
113
+ const cacheBase = path.join(pluginsDir, 'cache', MARKETPLACE, 'rdc-skills');
114
+ const mktDir = path.join(pluginsDir, 'marketplaces', MARKETPLACE);
115
+
116
+ if (fs.existsSync(cacheBase)) {
117
+ for (const entry of fs.readdirSync(cacheBase)) {
118
+ scanTree(`cache/${entry}`, path.join(cacheBase, entry));
119
+ if (entry !== 'latest') {
120
+ findings.multiplication.push(`extra plugin cache dir registers a second full copy: cache/${entry}`);
121
+ removals.push({ path: path.join(cacheBase, entry), why: 'non-latest plugin cache dir (duplicate source)', kind: 'dir' });
122
+ }
123
+ }
124
+ }
125
+
126
+ // The marketplace directory is a git CLONE of the whole repo, not just a
127
+ // manifest. When it lags, it is a second, older copy of every command and skill,
128
+ // live in the menu beside the current one. Never deleted here.
129
+ if (fs.existsSync(mktDir)) {
130
+ scanTree('marketplaces/rdc-skills', mktDir);
131
+ const hasTree = fs.existsSync(path.join(mktDir, 'commands')) || fs.existsSync(path.join(mktDir, 'skills'));
132
+ if (hasTree) {
133
+ findings.multiplication.push('marketplace dir contains a full commands/ + skills/ tree — a second live source');
134
+ findings.report.push(
135
+ `Marketplace clone is a second source. Bring it to the shipping ref (do NOT delete it):\n` +
136
+ ` git -C "${mktDir}" fetch origin master\n` +
137
+ ` git -C "${mktDir}" status --porcelain # must be empty first\n` +
138
+ ` git -C "${mktDir}" reset --hard origin/master`
139
+ );
140
+ }
141
+ }
142
+
143
+ // ── 2. skills/ and skills/user/ orphans ───────────────────────────────────────
144
+ for (const rel of ['skills', path.join('skills', 'user')]) {
145
+ const dir = path.join(CLAUDE_HOME, rel);
146
+ if (!fs.existsSync(dir)) continue;
147
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
148
+ const p = path.join(dir, e.name);
149
+ let md = null;
150
+ if (e.isDirectory()) {
151
+ md = ['SKILL.md', 'skill.md'].map((n) => path.join(p, n)).find((f) => fs.existsSync(f)) || null;
152
+ } else if (e.name.endsWith('.md')) {
153
+ md = p;
154
+ }
155
+ if (!md) continue;
156
+ const name = declaredName(md);
157
+ if (name && (name === NS || name.startsWith(`${NS}:`) || name.startsWith(`${NS}-`))) {
158
+ findings.multiplication.push(`loose skill in ${rel}/ shadows the plugin copy: ${e.name} (declares "${name}")`);
159
+ removals.push({ path: p, why: `loose rdc skill under ${rel}/ (plugin cache is authoritative)`, kind: e.isDirectory() ? 'dir' : 'file' });
160
+ }
161
+ }
162
+ }
163
+
164
+ // ── 3. Codex / .agents skill dirs — stale rdc-* copies ────────────────────────
165
+ const codexTargets = [
166
+ path.join(os.homedir(), '.codex', 'skills'),
167
+ path.join(os.homedir(), '.agents', 'skills'),
168
+ PROJECT_ROOT ? path.join(path.resolve(PROJECT_ROOT), '.agents', 'skills', 'user') : null,
169
+ ].filter(Boolean);
170
+
171
+ for (const target of codexTargets) {
172
+ if (!fs.existsSync(target)) continue;
173
+ for (const e of fs.readdirSync(target, { withFileTypes: true })) {
174
+ if (!e.isDirectory() || !new RegExp(`^${NS}-`).test(e.name)) continue;
175
+ const p = path.join(target, e.name);
176
+ findings.multiplication.push(`stale codex skill copy: ${p}`);
177
+ removals.push({ path: p, why: 'legacy rdc-<name>/ codex skill copy (superseded by MCP)', kind: 'dir' });
178
+ }
179
+ }
180
+
181
+ // ── 4. Duplicate plugin registration rows ─────────────────────────────────────
182
+ function jsonKeysStartingWithRdc(file, pick) {
183
+ if (!fs.existsSync(file)) return [];
184
+ try {
185
+ const data = JSON.parse(fs.readFileSync(file, 'utf8'));
186
+ const bag = pick(data) || {};
187
+ return Object.keys(bag).filter((k) => k.startsWith('rdc-skills@'));
188
+ } catch { return []; }
189
+ }
190
+ const ipKeys = jsonKeysStartingWithRdc(path.join(pluginsDir, 'installed_plugins.json'), (d) => d.plugins);
191
+ if (ipKeys.length > 1) {
192
+ findings.multiplication.push(`installed_plugins.json has ${ipKeys.length} rdc-skills keys: ${ipKeys.join(', ')}`);
193
+ findings.report.push('Re-run `node scripts/install-rdc-skills.js` — it collapses these to one row.');
194
+ }
195
+ const spKeys = jsonKeysStartingWithRdc(path.join(CLAUDE_HOME, 'settings.json'), (d) => d.enabledPlugins);
196
+ if (spKeys.length > 1) {
197
+ findings.multiplication.push(`settings.json enabledPlugins has ${spKeys.length} rdc-skills keys: ${spKeys.join(', ')}`);
198
+ findings.report.push('Re-run `node scripts/install-rdc-skills.js` — it collapses these to one key.');
199
+ }
200
+
201
+ // ── Output ────────────────────────────────────────────────────────────────────
202
+ const B = (s) => `\x1b[1m${s}\x1b[0m`;
203
+ console.log('');
204
+ console.log(B(`rdc-skills malformed-registration scan namespace="${NS}" home=${CLAUDE_HOME}`));
205
+ console.log(APPLY ? ' MODE: APPLY (will remove)' : ' MODE: DRY RUN (nothing will be removed — pass --apply to act)');
206
+ console.log('');
207
+
208
+ console.log(B(`A. PREFIXING — components that spell "${NS}" twice (${findings.prefixing.length})`));
209
+ if (!findings.prefixing.length) console.log(' none');
210
+ for (const f of findings.prefixing) {
211
+ console.log(` [${f.source}] ${f.declared} -> registers as ${f.renders}`);
212
+ console.log(` ${f.file}`);
213
+ }
214
+ console.log('');
215
+ console.log(' Remedy: these are stale BUILDS, not stray files — fixing them by hand');
216
+ console.log(' would be overwritten on the next install. Update the source package and');
217
+ console.log(' re-run the installer: npm i -g @lifeaitools/rdc-skills && rdc-skills-install');
218
+ console.log('');
219
+
220
+ console.log(B(`B. MULTIPLICATION — the same component registered from >1 source (${findings.multiplication.length})`));
221
+ if (!findings.multiplication.length) console.log(' none');
222
+ for (const m of findings.multiplication) console.log(` ${m}`);
223
+ console.log('');
224
+
225
+ console.log(B(`REMOVABLE ARTIFACTS (${removals.length})`));
226
+ if (!removals.length) console.log(' none');
227
+ for (const r of removals) console.log(` ${r.kind.padEnd(4)} ${r.path}\n why: ${r.why}`);
228
+ console.log('');
229
+
230
+ if (findings.report.length) {
231
+ console.log(B('MANUAL STEPS (not removable by this script)'));
232
+ for (const r of findings.report) console.log(` - ${r}`);
233
+ console.log('');
234
+ }
235
+
236
+ if (!APPLY) {
237
+ console.log(B('DRY RUN — nothing was removed.'));
238
+ console.log(` Re-run with --apply to remove the ${removals.length} artifact(s) listed above.`);
239
+ console.log('');
240
+ process.exit(0);
241
+ }
242
+
243
+ let removed = 0;
244
+ for (const r of removals) {
245
+ try {
246
+ fs.rmSync(r.path, { recursive: r.kind === 'dir', force: true });
247
+ console.log(` removed ${r.path}`);
248
+ removed++;
249
+ } catch (e) {
250
+ console.log(` FAILED ${r.path} — ${e.message}`);
251
+ }
252
+ }
253
+ console.log('');
254
+ console.log(` removed ${removed}/${removals.length} artifact(s).`);
255
+ console.log(' Now re-run the installer so the single authoritative copy is rebuilt:');
256
+ console.log(' node scripts/install-rdc-skills.js');
257
+ console.log('');
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: rdc:behavior-audit
2
+ name: behavior-audit
3
3
  description: "Usage `rdc:behavior-audit <report-dir> [--since-days N] [--latest N] [--reprocess]` — produces a bounded, redacted Claude/Codex transcript evidence bundle, incrementally skips completed transcript hashes, and aligns candidate behavior problems to shared truth-governance rules."
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: rdc:brochure
2
+ name: brochure
3
3
  description: "Usage `rdc:brochure <input> [--out <path>] [--template <name>] [--format Letter|A4]` — Turn a zip, folder, HTML file, URL, or markdown folder into a print-quality PDF brochure via Puppeteer. Auto-detects print-variant HTML, honors @page CSS, falls back to a Studio-token-aware template when no HTML exists."
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: rdc-brochurify
2
+ name: brochurify
3
3
  version: 0.1.0
4
4
  description: |
5
5
  Orchestrate a Brochurify job from source ingest through delivered PDF, using six parallel-dispatched typed sub-agents and the convergence loop. Use this skill EVERY TIME the user invokes Brochurify directly via "brochurify this", "make a brochure from", "convert this to a brochure PDF", or "rdc:brochurify". Also runs automatically when a job arrives from the broker via monkey_dispatch. The skill enforces D-001 through D-016 from the brochurify DECISIONS-LOG.
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: rdc:build
2
+ name: build
3
3
  description: "Usage `rdc:build <epic-id>` — You have a planned epic with tasks ready to execute. Dispatches parallel typed agents, each commits atomically to develop, runs a mandatory per-wave code-review gate (pr-review-toolkit:code-reviewer), closes work items, and runs the validator gate. Call after rdc:plan or when told to build."
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: rdc:channel-formatter
2
+ name: channel-formatter
3
3
  description: "Usage `rdc:channel-formatter <channel|pack> [content]` — Apply precise, channel-native formatting and content repurposing to any output: LinkedIn, Twitter/X, Slack/Teams, Email (external/internal), Pitch Deck slides, Word/DOCX, PDF Report, Web/Landing Page, and multi-output content packs. Use EVERY TIME the user names an output channel/platform/document type, asks to 'write a post', 'draft an email', 'format this for', 'make this a slide', 'send to LinkedIn', 'write a tweet', 'social media post', 'turn this article into posts', 'make a content pack', 'make a social pack', or 'repurpose this article'. Each channel has its own Unicode strategy, emphasis system, length limits, and structure — never apply generic markdown to channel-specific output. This skill FORMATS, STRUCTURES, and REPURPOSES text only — for actual .docx/.pptx ↔ Markdown FILE conversion (either direction) use `rdc:convert` (build-corpus), not this skill. Self-contained: all channel and pack rules are inlined below (no external reference files)."
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: rdc:co-develop
2
+ name: co-develop
3
3
  description: "Usage `rdc:co-develop <ask|reply|inbox|start|resume|status>` — peer-aware Claude/Codex co-development over clauth, with ask --wait as the default delegation pattern."
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: rdc:collab
2
+ name: collab
3
3
  description: "Usage `rdc:collab <collaborator> [inbound] <topic>` — Structured agent-to-agent CONVERSATION with Codex, a local LLM, a Claude agent, or a claude.ai session. Converges on a decision and never writes to the repo — work that needs writing becomes a work item for rdc:build. Run `rdc:collab status <peer>` first — both sides emit probed capability state. Every dispatch carries a response contract."
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: rdc:convert
2
+ name: convert
3
3
  description: "Usage `rdc:convert <input> [--out <dir>] [--to markdown|word] [--images assets|base64|s3]` — Convert .docx/.pptx/.ppt → Markdown (Word OMML equations as KaTeX TeX, tables, images) or Markdown → Word via the build-corpus CLI from npm `regen.mde`. Portable: runs in any session that can reach npm — Claude Code CLI, Codex, and claude.ai can fetch + run it. Use whenever the user asks to convert an Office document, build a Markdown corpus from .docx/.pptx, turn Markdown into a .docx, or open the report in the regen-mde editor on Windows."
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: rdc:deploy
2
+ name: deploy
3
3
  description: "Usage `rdc:deploy <slug> [promote|diagnose|audit|convert]` — registry-resolved PM2 development deployment and Coolify production promotion through clauth's bearer-authenticated job control plane, with status receipts and mandatory content gates."
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: rdc:design
2
+ name: design
3
3
  description: "Usage `rdc:design <topic>` — RDC-owned design work for Studio, Palette Library, and token-aware UI systems. Use for Rampant color work or design-system-first builds owned by the design cell. Not for general UI polish — use impeccable for that."
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: rdc:edit
2
+ name: edit
3
3
  description: "Usage `rdc:edit <site|brand|route|file>` — open the local website editor host for a target site, brand, route, or file. Resolves the target, launches or reuses the local editor host on port 3015, and opens the target in the browser when not under `RDC_TEST=1`."
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: rdc:env
2
+ name: env
3
3
  description: "Usage `rdc:env [status|install|repair|update]` — Manage the LIFEAI environment harness: check status, install on a fresh box, repair broken services, or pull updates. Reads $LIFEAI_ENV/manifest.json as the source of truth. Use when: 'check the environment', 'install environment', 'repair environment', 'update environment', 'setup env', 'fix env', 'env status', or after a reboot/GPU crash."
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: rdc-extract-verifier-rules
2
+ name: extract-verifier-rules
3
3
  version: 0.1.0
4
4
  description: |
5
5
  Read recent enhancement-log entries, cluster failures by pattern, generate candidate verifier rules, test them against the known-good corpus and the failure corpus, and propose pull requests adding the highest-confidence rules to forbidden-patterns.json. Use this skill on a nightly cadence (3 AM PT), or manually when the user says "extract verifier rules", "promote enhancement log", "what new rules should we add", or after a significant brochure run produced many failures.
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: rdc:fixit
2
+ name: fixit
3
3
  description: "Usage `rdc:fixit <description>` — Quick fix under 5 files / 30 min that does not warrant a full plan→build cycle. Creates a minimal work item, makes the change, commits, runs a mandatory code-review pass (pr-review-toolkit:code-reviewer), DELIVERS the change to where it is consumed (publish/deploy/land) and verifies it, then closes. The only sanctioned bypass of rdc:build."
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: rdc:fs-mcp
2
+ name: fs-mcp
3
3
  description: "Usage `rdc:fs-mcp <task>` — Use the File System MCP bridge for live repo reads, safe writes, cloud-to-local ingest, and GitHub-branch imports into a dirty local monorepo. Use when Claude.ai, Cowork, or CLI agents need fs_read/fs_write/fs_import_git_files guidance."
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: rdc:handoff
2
+ name: handoff
3
3
  description: "Usage `rdc:handoff [--from-prototype <id>]` — Convert a finalized plan or prototype into CLI-ready artifacts: writes .rdc/plans/, creates Supabase work items with DoD checklists, registers prototype if present. Bridge between planning session and rdc:build."
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: rdc:help
2
+ name: help
3
3
  description: "Usage `rdc:help` — Show all MCP skills with usage, requirements, slash forms, and codeflow status. Reads `.claude-plugin/plugin.json` skills_meta (single source of truth). Call when unsure which skill to use or what args it takes."
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: rdc:housekeeping
2
+ name: housekeeping
3
3
  description: "Usage `rdc:housekeeping [--fix]` — Weekly maintenance audit: directory structure verification, PUBLISH.md URL validation, CLAUDE.md freshness, orphan detection, places compliance, and stale version scan. Produces `.rdc/reports/YYYY-MM-DD-housekeeping.md`. With `--fix`, auto-remediate safe issues."
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: rdc:onramp
2
+ name: onramp
3
3
  description: "Usage `rdc:onramp <slug> --name \"<Display Name>\"` — Full-pipeline enrollment orchestrator for a new Place Fund project. Runs all 7 phases: enroll → research + document integration → conflict resolution → brand book + imagery → Regen Score gate → site build → deploy dev. Each phase is idempotent and resumable."
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: rdc:overnight
2
+ name: overnight
3
3
  description: "Usage `rdc:overnight [epic-id|label=<label>]` — Unattended end-to-end: drain the entire work queue autonomously (preplan → plan → build → review → report). Inherits mandatory per-wave code-review (pr-review-toolkit:code-reviewer) from rdc:build and rdc:review. Use when leaving Claude to run unsupervised for an extended session."
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: rdc:plan
2
+ name: plan
3
3
  description: "Usage `rdc:plan <topic>` — No epic exists and you need architecture + task breakdown. Produces design decisions, tradeoffs, and Supabase epics/tasks with DoD checklists that feed rdc:build. Use after rdc:preplan or when given clear architectural direction."
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: rdc:preplan
2
+ name: preplan
3
3
  description: "Usage `rdc:preplan <topic>` — Before committing to an architecture, research unknowns first. Codebase scan + web search, no code written, no work items created. Output feeds rdc:plan. Use for open-ended approach questions."
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: rdc:prototype
2
+ name: prototype
3
3
  description: "Usage `rdc:prototype <description>` — Build a JSX/TSX mockup for visual review before implementation. Saves to docs/source/, registers in prototype_registry. Use when asked to mock something up or show what it looks like. Not production code."
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: rdc:refactor
2
+ name: refactor
3
3
  description: "Usage `rdc:refactor <epic-id|topic> --takeover <reason>` — Governed consolidation and implementation of a cross-cutting refactor. Authorizes an explicit takeover, preserves evidence, re-parents relevant work through the consolidation RPC, isolates every writer, and uses the same review, validator, landing, and delivery gates as rdc:build."
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: rdc:regen-media
2
+ name: regen-media
3
3
  description: "Usage `rdc:regen-media <generate|edit|upscale|upload> <brief-or-path>` - Primary image-generation and Regen Media asset workflow. Use for image generation, image editing/upscaling, GPT Image/gpt-image-2 requests, Codex built-in image_gen, and uploading finished images to regen-media/R2. The default path is keyless local Codex gpt-image-2 via the built-in image_gen tool; server-side regen-media MCP/API generation is fallback only."
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: rdc:release
2
+ name: release
3
3
  description: "Usage `rdc:release <repo> [version|--patch|--minor|--major|--dry-run]` — bump, commit, tag, push, wait for CI/publish, install, and verify a package or project using repo-local release metadata."
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: rdc:report
2
+ name: report
3
3
  description: "Usage `rdc:report` — Write the nightly session summary to .rdc/reports/YYYY-MM-DD.md covering completed work, open items, per-project progress, and infra status. Call at session end."
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: rdc:review
2
+ name: review
3
3
  description: "Usage `rdc:review [--unattended]` — Post-build quality gate: tsc, tests, stale docs, export conflicts, and a mandatory pr-review-toolkit:code-reviewer pass across modified packages. Fixes what it can automatically, escalates the rest. Call after rdc:build and before merging to main."
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: rdc:self-test
2
+ name: self-test
3
3
  description: "Usage `rdc:self-test [--strict]` — Validate all rdc:* skills, plugin manifest, and tooling consistency. Use after editing skills, upgrading the plugin, or when a skill behaves unexpectedly."
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: rdc:status
2
+ name: status
3
3
  description: "Usage `rdc:status` — Read-only snapshot of open epics, work items, Coolify health, blockers, and next recommended action. Safe to call anytime. Start here when disoriented or resuming a session."
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: rdc:terminal-config
2
+ name: terminal-config
3
3
  description: "Usage `rdc:terminal-config <task>` — read and safely modify Windows Terminal settings, shell profiles, and agent startup sequencing without relying on machine-specific paths."
4
4
  ---
5
5
 
@@ -31,8 +31,8 @@ The manifest layer verifies each skill can be started from a realistic caller pr
31
31
  | `rdc:plan` | `rdc-plan.test.json` | Health-endpoint planning prompt | `exit_code`, `stdout_contains` | Design-decision, decomposition-matrix, work-package, and coarse-check negative checks |
32
32
  | `rdc:preplan` | `rdc-preplan.test.json` | Rate-limiter research prompt | `commits_made`, `exit_code`, `stdout_contains` | Research artifact, comparison, unknowns, recommendation, and research-tool routing checks |
33
33
  | `rdc:prototype` | `rdc-prototype.test.json` | Tiny component prototype prompt | `exit_code`, `stdout_contains` | Local TSX prototype artifact, registry, test-mode, and production-write negative checks |
34
- | `rdc:brochurify` | `rdc-rdc-brochurify.test.json` | Read-only markdown Brochurify fixture | `exit_code`, `stdout_contains` | Sandbox output and no-follow-up negative checks |
35
- | `rdc:extract-verifier-rules` | `rdc-rdc-extract-verifier-rules.test.json` | Enhancement-log verifier fixture | `exit_code`, `stdout_contains` | Candidate, false-positive, known-good, recall, and no-auto-merge/PR negative checks |
34
+ | `rdc:brochurify` | `rdc-brochurify.test.json` | Read-only markdown Brochurify fixture | `exit_code`, `stdout_contains` | Sandbox output and no-follow-up negative checks |
35
+ | `rdc:extract-verifier-rules` | `rdc-extract-verifier-rules.test.json` | Enhancement-log verifier fixture | `exit_code`, `stdout_contains` | Candidate, false-positive, known-good, recall, and no-auto-merge/PR negative checks |
36
36
  | `rdc:release` | `rdc-release.test.json` | Dry-run package release | `commits_made`, `exit_code`, `stdout_contains` | Dry-run release checklist and force/bypass negative checks |
37
37
  | `rdc:regen-media` | `rdc-regen-media.test.json` | Image generation path selection | `commits_made`, `exit_code`, `stdout_contains` | Local keyless Codex gpt-image-2 default, test-mode no-generation, and server-side fallback negative checks |
38
38
  | `rdc:report` | `rdc-report.test.json` | Unattended report generation | `exit_code`, `stdout_contains` | Report status block, counts, next recommendation, and dump/push negative checks |
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: rdc:watch
2
+ name: watch
3
3
  description: "Usage `rdc:watch` — Initialize a live session-log viewer and report its path; open it only during attended, non-test use. Zero infrastructure — pure filesystem + static HTML."
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: rdc:workitems
2
+ name: workitems
3
3
  description: "Usage `rdc:workitems [add|done|status|list|query]` — Manual work item CRUD outside the normal build flow: add to backlog, mark done, update status, list open epics, query by label."
4
4
  ---
5
5
 
@@ -152,7 +152,7 @@ function inspectFormatterBody(body, prefix) {
152
152
  function unitChecks() {
153
153
  const names = listSkills().map((skill) => skill.name);
154
154
  check('unit: channel-formatter in local catalog', names.includes('channel-formatter'));
155
- for (const specialist of ['convert', 'brochure', 'rdc-brochurify', 'lifeai-brochure-author']) {
155
+ for (const specialist of ['convert', 'brochure', 'brochurify', 'lifeai-brochure-author']) {
156
156
  check(`unit: specialist present ${specialist}`, names.includes(specialist));
157
157
  }
158
158
  const results = searchSkills('turn this article into social posts');
@@ -181,7 +181,7 @@ async function endpointChecks(url, label) {
181
181
  const catalog = JSON.parse(list.text || '{"skills":[]}');
182
182
  const names = catalog.skills.map((skill) => skill.name);
183
183
  check(`${label}: list includes channel-formatter`, names.includes('channel-formatter'));
184
- for (const specialist of ['convert', 'brochure', 'rdc-brochurify', 'lifeai-brochure-author']) {
184
+ for (const specialist of ['convert', 'brochure', 'brochurify', 'lifeai-brochure-author']) {
185
185
  check(`${label}: list includes ${specialist}`, names.includes(specialist));
186
186
  }
187
187
 
@@ -115,7 +115,7 @@ function unitTests() {
115
115
  check('unit: resolves bare skill name', resolveSkillName('build') === 'build');
116
116
  check('unit: resolves rdc slash skill name', resolveSkillName('rdc:build') === 'build');
117
117
  check('unit: resolves leading slash command form', resolveSkillName('/rdc:build') === 'build');
118
- check('unit: resolves specialist slash alias', resolveSkillName('rdc:brochurify') === 'rdc-brochurify');
118
+ check('unit: resolves specialist slash alias', resolveSkillName('rdc:brochurify') === 'brochurify');
119
119
  check('unit: resolves non-rdc specialist name', resolveSkillName('lifeai-brochure-author') === 'lifeai-brochure-author');
120
120
 
121
121
  // Searchability gate — the dimension the first "100% coverage" missed. Every
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Installing REBUILDS the plugin cache from scratch — a skill removed or renamed
3
+ * upstream must not survive in it.
4
+ *
5
+ * Claude Code registers whatever sits in the plugin cache, so anything left behind
6
+ * there keeps registering. A rename like `skills/rdc-brochurify/` ->
7
+ * `skills/brochurify/` would otherwise leave the old folder in place and the box
8
+ * would register BOTH `rdc:brochurify` and the double-namespaced
9
+ * `rdc:rdc-brochurify` — the shape of the malformed registrations this branch fixes.
10
+ *
11
+ * The installer already gets this right: both call sites `fs.rmSync(latestDir, ...)`
12
+ * immediately before `buildPluginCache(latestDir, ...)`. This test exists to keep it
13
+ * that way, because the guarantee lives in two separate places and neither states
14
+ * that it is load-bearing.
15
+ *
16
+ * WORTH RECORDING, because it nearly went the other way: the first version of this
17
+ * file shipped alongside an "obvious" fix to buildPluginCache — clear each payload
18
+ * directory before copying into it — on the theory that copy-over-merge was leaving
19
+ * the stale folders. Reverting that fix and re-running left the test GREEN, which is
20
+ * what exposed the diagnosis as wrong: the wipe already happens one level up, at the
21
+ * caller. Without that control run, a redundant change and a test that could not
22
+ * detect its own subject would both have shipped, and the real source of the stale
23
+ * registrations would have stayed hidden behind an apparent fix.
24
+ */
25
+ import { test } from 'node:test';
26
+ import assert from 'node:assert/strict';
27
+ import { execFileSync } from 'node:child_process';
28
+ import fs from 'node:fs';
29
+ import os from 'node:os';
30
+ import path from 'node:path';
31
+ import { fileURLToPath } from 'node:url';
32
+
33
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
34
+ const INSTALLER = path.join(ROOT, 'scripts', 'install-rdc-skills.js');
35
+
36
+ function runInstaller(claudeHome) {
37
+ try {
38
+ return execFileSync(process.execPath, [INSTALLER, '--claude-home', claudeHome, '--skip-hooks'], {
39
+ encoding: 'utf8', timeout: 300_000, cwd: ROOT,
40
+ });
41
+ } catch (err) {
42
+ return `${err.stdout || ''}${err.stderr || ''}`;
43
+ }
44
+ }
45
+
46
+ test('a stale skill directory in the plugin cache does not survive an install', () => {
47
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'rdcskills-home-'));
48
+
49
+ const first = runInstaller(home);
50
+ // The cache carries a version segment: cache/<marketplace>/<plugin>/<version>/skills
51
+ const cache = path.join(home, 'plugins', 'cache', 'rdc-skills', 'rdc-skills', 'latest', 'skills');
52
+ assert.ok(fs.existsSync(cache), `expected a plugin skills cache — installer said:\n${first.slice(-800)}`);
53
+
54
+ // Plant exactly the shape the defect produces: a skill folder that no longer exists
55
+ // upstream, carrying the double-namespaced name.
56
+ const stale = path.join(cache, 'rdc-brochurify');
57
+ fs.mkdirSync(stale, { recursive: true });
58
+ fs.writeFileSync(path.join(stale, 'SKILL.md'), '---\nname: rdc-brochurify\n---\nstale\n');
59
+ assert.ok(fs.existsSync(stale), 'precondition: decoy planted');
60
+
61
+ runInstaller(home);
62
+ assert.ok(!fs.existsSync(stale), 'a skill removed upstream must not survive in the cache');
63
+ assert.ok(
64
+ fs.readdirSync(cache).length > 0,
65
+ 'control: the cache must still hold real skills — an empty cache would satisfy the assertion above for the wrong reason',
66
+ );
67
+ });
68
+
69
+ test('no rdc-prefixed skill directory ships in the repo', () => {
70
+ // The plugin is already named `rdc`, so a directory named `rdc-x` registers as
71
+ // `rdc:rdc-x`. This is the source-side half of the same defect, and it is the half
72
+ // that actually produced the malformed names.
73
+ const skills = path.join(ROOT, 'skills');
74
+ const bad = fs.readdirSync(skills).filter((d) => /^rdc-/.test(d));
75
+ assert.deepEqual(bad, [], `these register as rdc:rdc-*: ${bad.join(', ')}`);
76
+ });
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Regression guard: the plugin namespace is applied ONCE.
4
+ *
5
+ * `.claude-plugin/plugin.json` declares `"name": "rdc"`. Claude Code registers a
6
+ * plugin component as `<plugin>:<declared frontmatter name>`. So any component
7
+ * that ALSO spells the namespace into its own `name:` gets it twice, and the
8
+ * menu renders `/rdc:rdc:plan` beside `/rdc:plan` — one entry per source.
9
+ *
10
+ * That defect was fixed for `commands/` in 643e198 ("the plugin namespace
11
+ * already says rdc — stop saying it twice") and `skills/` was never migrated,
12
+ * so 31 skills kept declaring `name: rdc:<x>` and two more carried the
13
+ * namespace in their DIRECTORY name (`skills/rdc-brochurify/`), composing to
14
+ * `rdc:rdc-brochurify`.
15
+ *
16
+ * Why the existing guards missed it: `tests/manifest-contract-fields.test.mjs`
17
+ * and `tests/mcp.test.mjs` both assert `!/^rdc:rdc-/` against
18
+ * `plugin.json.skills_meta[].slash` — a HAND-AUTHORED field that was already
19
+ * spelled correctly (`rdc:brochurify`). Nothing asserted anything about the
20
+ * frontmatter `name:` or the directory name, which is what the loader actually
21
+ * composes from. This test checks the registering artifact, not the aspiration.
22
+ */
23
+ import assert from 'node:assert/strict';
24
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
25
+ import { dirname, join, resolve } from 'node:path';
26
+ import { fileURLToPath } from 'node:url';
27
+
28
+ const __dirname = dirname(fileURLToPath(import.meta.url));
29
+ const root = resolve(__dirname, '..');
30
+
31
+ const plugin = JSON.parse(readFileSync(join(root, '.claude-plugin', 'plugin.json'), 'utf8'));
32
+ const PLUGIN = plugin.name;
33
+ assert.ok(PLUGIN, '.claude-plugin/plugin.json must declare a plugin name');
34
+
35
+ /** Declared `name:` from a markdown frontmatter block, or null when absent. */
36
+ function declaredName(file) {
37
+ const raw = readFileSync(file, 'utf8').replace(/\r\n/g, '\n');
38
+ const block = raw.match(/^---\n([\s\S]*?)\n---/);
39
+ if (!block) return null;
40
+ const line = block[1].split('\n').find((l) => /^name:\s*/.test(l));
41
+ return line ? line.replace(/^name:\s*/, '').trim().replace(/^["']|["']$/g, '') : null;
42
+ }
43
+
44
+ /** Every registering component: its file, the id it composes to, and its source. */
45
+ function components() {
46
+ const out = [];
47
+
48
+ const cmdDir = join(root, 'commands');
49
+ if (existsSync(cmdDir)) {
50
+ for (const f of readdirSync(cmdDir).filter((f) => f.endsWith('.md')).sort()) {
51
+ const file = join(cmdDir, f);
52
+ out.push({
53
+ kind: 'command',
54
+ file: `commands/${f}`,
55
+ dir: f.replace(/\.md$/, ''),
56
+ declared: declaredName(file),
57
+ });
58
+ }
59
+ }
60
+
61
+ const skillsDir = join(root, 'skills');
62
+ for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
63
+ if (!entry.isDirectory() || entry.name === 'tests') continue;
64
+ const file = join(skillsDir, entry.name, 'SKILL.md');
65
+ if (!existsSync(file)) continue;
66
+ out.push({
67
+ kind: 'skill',
68
+ file: `skills/${entry.name}/SKILL.md`,
69
+ dir: entry.name,
70
+ declared: declaredName(file),
71
+ });
72
+ }
73
+
74
+ return out;
75
+ }
76
+
77
+ const all = components();
78
+ assert.ok(all.length > 0, 'expected at least one plugin component to check');
79
+
80
+ const offenders = { doublePrefixed: [], dirPrefixed: [] };
81
+
82
+ for (const c of all) {
83
+ // The loader falls back to the directory / file stem when `name:` is absent.
84
+ const effective = c.declared ?? c.dir;
85
+
86
+ // 1. The declared name must not repeat the plugin namespace.
87
+ // `name: rdc:plan` under plugin `rdc` composes to `rdc:rdc:plan`.
88
+ if (effective === PLUGIN || effective.startsWith(`${PLUGIN}:`)) {
89
+ offenders.doublePrefixed.push(`${c.file} name: ${effective} -> ${PLUGIN}:${effective}`);
90
+ }
91
+
92
+ // 2. Same defect, hyphen spelling: a directory named `rdc-brochurify`
93
+ // composes to `rdc:rdc-brochurify`.
94
+ if (effective.startsWith(`${PLUGIN}-`)) {
95
+ offenders.dirPrefixed.push(`${c.file} name: ${effective} -> ${PLUGIN}:${effective}`);
96
+ }
97
+ }
98
+
99
+ assert.deepEqual(
100
+ offenders.doublePrefixed,
101
+ [],
102
+ `these components spell the "${PLUGIN}" namespace twice — the plugin supplies it:\n ` +
103
+ offenders.doublePrefixed.join('\n '),
104
+ );
105
+
106
+ assert.deepEqual(
107
+ offenders.dirPrefixed,
108
+ [],
109
+ `these components carry the "${PLUGIN}-" namespace in their own name — the plugin supplies it:\n ` +
110
+ offenders.dirPrefixed.join('\n '),
111
+ );
112
+
113
+ // 3. No two components of the same kind may compose to the same registered id.
114
+ for (const kind of ['command', 'skill']) {
115
+ const seen = new Map();
116
+ for (const c of all.filter((c) => c.kind === kind)) {
117
+ const id = `${PLUGIN}:${c.declared ?? c.dir}`;
118
+ if (seen.has(id)) {
119
+ assert.fail(`duplicate ${kind} registration ${id}: ${seen.get(id)} and ${c.file}`);
120
+ }
121
+ seen.set(id, c.file);
122
+ }
123
+ }
124
+
125
+ console.log(`plugin namespace name tests — PASS (${all.length} components, namespace "${PLUGIN}")`);