@lifeaitools/rdc-skills 0.9.30 → 0.9.31

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rdc",
3
- "version": "0.9.28",
3
+ "version": "0.9.31",
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifeaitools/rdc-skills",
3
- "version": "0.9.30",
3
+ "version": "0.9.31",
4
4
  "description": "RDC typed-agent dispatch skill suite for Claude Code — plan, build, review, overnight builds",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -140,30 +140,53 @@ function buildPluginCache(cacheDir, version, gitSha) {
140
140
  // Claude Code loads that directory AND the plugin cache, so any rdc skills left
141
141
  // there produce duplicate registrations and break the resolver.
142
142
  // This function nukes any entry whose frontmatter name starts with "rdc:".
143
+ // Scans BOTH the immediate dir and nested .md files (e.g. `user/skill.md`,
144
+ // `user/rdc-build/SKILL.md`) so pre-plugin orphans are caught regardless of
145
+ // naming convention.
143
146
  function cleanUserSkills(userSkillsDir) {
144
147
  if (!fs.existsSync(userSkillsDir)) return 0;
145
148
  let removed = 0;
146
149
  for (const entry of fs.readdirSync(userSkillsDir, { withFileTypes: true })) {
147
150
  const candidate = path.join(userSkillsDir, entry.name);
148
- let skillFile = null;
149
151
  if (entry.isDirectory()) {
150
- // New format: <name>/SKILL.md or <name>/skill.md
152
+ // Subdir form: <name>/SKILL.md or <name>/skill.md
153
+ let skillFile = null;
151
154
  for (const sf of ['SKILL.md', 'skill.md']) {
152
155
  const p = path.join(candidate, sf);
153
156
  if (fs.existsSync(p)) { skillFile = p; break; }
154
157
  }
155
- } else if (entry.isFile() && entry.name.endsWith('.md') && entry.name !== 'skill.md' && entry.name !== 'SKILL.md' && entry.name !== 'README.md') {
156
- // Old format: rdc-deploy.md flat file
157
- skillFile = candidate;
158
+ if (!skillFile) continue;
159
+ const fm = readFrontmatter(skillFile);
160
+ if (fm.name && fm.name.startsWith('rdc:')) {
161
+ try { fs.rmSync(candidate, { recursive: true, force: true }); removed++; } catch {}
162
+ }
163
+ } else if (entry.isFile() && entry.name.endsWith('.md')) {
164
+ // ANY .md file at this level — including skill.md / SKILL.md / README.md
165
+ // if their frontmatter declares an rdc:* skill. A previous version skipped
166
+ // those names; that left an orphan rdc:build copy at user/skill.md which
167
+ // registered as a duplicate "user" skill.
168
+ const fm = readFrontmatter(candidate);
169
+ if (fm.name && fm.name.startsWith('rdc:')) {
170
+ try { fs.unlinkSync(candidate); removed++; } catch {}
171
+ }
158
172
  }
159
- if (!skillFile) continue;
160
- const fm = readFrontmatter(skillFile);
161
- if (fm.name && fm.name.startsWith('rdc:')) {
162
- try {
163
- if (entry.isDirectory()) fs.rmSync(candidate, { recursive: true, force: true });
164
- else fs.unlinkSync(candidate);
165
- removed++;
166
- } catch {}
173
+ }
174
+ return removed;
175
+ }
176
+
177
+ // Scrub legacy rdc orphans from ~/.claude/skills/ (top-level), not just user/.
178
+ // Some older installs landed flat skill files alongside the plugin tree.
179
+ function cleanGlobalSkillsRoot(skillsDir) {
180
+ if (!fs.existsSync(skillsDir)) return 0;
181
+ let removed = 0;
182
+ for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
183
+ if (entry.name === 'user') continue; // handled separately
184
+ const candidate = path.join(skillsDir, entry.name);
185
+ if (entry.isFile() && entry.name.endsWith('.md')) {
186
+ const fm = readFrontmatter(candidate);
187
+ if (fm.name && fm.name.startsWith('rdc:')) {
188
+ try { fs.unlinkSync(candidate); removed++; } catch {}
189
+ }
167
190
  }
168
191
  }
169
192
  return removed;
@@ -192,11 +215,14 @@ function cleanStaleHooks(hooksDstDir) {
192
215
  }
193
216
 
194
217
  // ── Cache flush helper ────────────────────────────────────────────────────────
195
- function flushOldCaches(cacheBase, keepVersion) {
218
+ // Only `latest/` is ever kept. Earlier versions wrote BOTH `<version>/` and
219
+ // `latest/`, which caused the plugin loader to scan and register every rdc:*
220
+ // skill twice (once per directory). The fix is permanent single-dir layout.
221
+ function flushOldCaches(cacheBase /* keepVersion intentionally unused */) {
196
222
  if (!fs.existsSync(cacheBase)) return 0;
197
223
  let flushed = 0;
198
224
  for (const entry of fs.readdirSync(cacheBase)) {
199
- if (entry === keepVersion || entry === 'latest') continue;
225
+ if (entry === 'latest') continue;
200
226
  try {
201
227
  fs.rmSync(path.join(cacheBase, entry), { recursive: true, force: true });
202
228
  flushed++;
@@ -211,7 +237,6 @@ function registerCLI(version, gitSha) {
211
237
  const mktDir = path.join(pluginDir, 'marketplaces', MARKETPLACE);
212
238
  const mktPlugDir = path.join(mktDir, '.claude-plugin');
213
239
  const cacheBase = path.join(pluginDir, 'cache', MARKETPLACE, 'rdc-skills');
214
- const cacheDir = path.join(cacheBase, version);
215
240
  const latestDir = path.join(cacheBase, 'latest');
216
241
 
217
242
  // 1. Marketplace manifest
@@ -224,12 +249,11 @@ function registerCLI(version, gitSha) {
224
249
  knownMp[MARKETPLACE] = { source: { source: 'github', repo: 'LIFEAI/rdc-skills' }, installLocation: mktDir, lastUpdated: new Date().toISOString() };
225
250
  writeJson(kmpPath, knownMp, 4);
226
251
 
227
- // 3. Flush stale version caches, then write versioned + stable latest
228
- const flushed = flushOldCaches(cacheBase, version);
252
+ // 3. Flush every cache dir except `latest/`, then rewrite `latest/`. We
253
+ // intentionally do NOT keep a versioned dir — the plugin loader registers
254
+ // every dir it finds, so two dirs = duplicate skills.
255
+ const flushed = flushOldCaches(cacheBase);
229
256
  if (flushed > 0) info(` flushed : ${flushed} stale cache dir(s)`);
230
- buildPluginCache(cacheDir, version, gitSha);
231
- // Write to stable 'latest/' so open terminals can pick up changes if they
232
- // re-read installed_plugins.json between skill invocations.
233
257
  if (fs.existsSync(latestDir)) fs.rmSync(latestDir, { recursive: true, force: true });
234
258
  buildPluginCache(latestDir, version, gitSha);
235
259
 
@@ -312,7 +336,6 @@ function registerCowork(version, gitSha) {
312
336
  for (const { dir, settingsFile } of bases) {
313
337
  const pluginsDir = path.join(dir, 'cowork_plugins');
314
338
  const cacheBase = path.join(pluginsDir, 'cache', MARKETPLACE, 'rdc-skills');
315
- const cacheDir = path.join(cacheBase, version);
316
339
  const latestDir = path.join(cacheBase, 'latest');
317
340
  const mktDir = path.join(pluginsDir, 'marketplaces', MARKETPLACE);
318
341
  const mktPlugDir = path.join(mktDir, '.claude-plugin');
@@ -327,9 +350,8 @@ function registerCowork(version, gitSha) {
327
350
  knownMp[MARKETPLACE] = { source: { source: 'github', repo: 'LIFEAI/rdc-skills' }, installLocation: mktDir, lastUpdated: new Date().toISOString() };
328
351
  writeJson(kmpPath, knownMp, 4);
329
352
 
330
- // Flush stale caches, write versioned + stable latest
331
- flushOldCaches(cacheBase, version);
332
- buildPluginCache(cacheDir, version, gitSha);
353
+ // Flush all non-latest caches and rewrite `latest/` only — single-dir layout
354
+ flushOldCaches(cacheBase);
333
355
  if (fs.existsSync(latestDir)) fs.rmSync(latestDir, { recursive: true, force: true });
334
356
  buildPluginCache(latestDir, version, gitSha);
335
357
 
@@ -622,6 +644,10 @@ async function main() {
622
644
  const userSkillsDir = path.join(claudeHome, 'skills', 'user');
623
645
  const purged = cleanUserSkills(userSkillsDir);
624
646
  if (purged > 0) ok(`[0.5a] Skills cleanup — removed ${purged} stale rdc: skill(s) from skills/user/`);
647
+ // Also scan the parent ~/.claude/skills/ for any flat-file rdc orphans.
648
+ const skillsRoot = path.join(claudeHome, 'skills');
649
+ const rootPurged = cleanGlobalSkillsRoot(skillsRoot);
650
+ if (rootPurged > 0) ok(`[0.5a] Skills cleanup — removed ${rootPurged} stale rdc: file(s) from skills/`);
625
651
  }
626
652
 
627
653
  // 0.5b. Stale hook cleanup — remove hooks we no longer ship
@@ -742,6 +768,53 @@ async function main() {
742
768
  console.log(' \x1b[36mPreflight:\x1b[0m');
743
769
  runPreflight();
744
770
 
771
+ // 6.5 Post-install verification — duplication guard
772
+ console.log('');
773
+ console.log(' \x1b[36mPost-install verification:\x1b[0m');
774
+ let verifyFailed = false;
775
+ {
776
+ const cacheBase = path.join(claudeHome, 'plugins', 'cache', MARKETPLACE, 'rdc-skills');
777
+ const cacheDirs = fs.existsSync(cacheBase) ? fs.readdirSync(cacheBase) : [];
778
+ if (cacheDirs.length === 1 && cacheDirs[0] === 'latest') {
779
+ ok(`plugin cache : 1 dir (latest/)`);
780
+ } else {
781
+ fail(`plugin cache : expected exactly [latest/], found [${cacheDirs.join(', ')}]`);
782
+ verifyFailed = true;
783
+ }
784
+ const ipPath = path.join(claudeHome, 'plugins', 'installed_plugins.json');
785
+ const installed = readJson(ipPath, { plugins: {} });
786
+ const rdcEntries = installed.plugins[PLUGIN_KEY] || [];
787
+ if (rdcEntries.length === 1) {
788
+ ok(`installed_plugins: 1 entry for ${PLUGIN_KEY}`);
789
+ } else {
790
+ fail(`installed_plugins: expected 1 entry, found ${rdcEntries.length}`);
791
+ verifyFailed = true;
792
+ }
793
+ const userSkillsDir = path.join(claudeHome, 'skills', 'user');
794
+ const stillThere = fs.existsSync(userSkillsDir)
795
+ ? fs.readdirSync(userSkillsDir).filter(f => {
796
+ const p = path.join(userSkillsDir, f);
797
+ const skillFile = fs.statSync(p).isDirectory()
798
+ ? ['SKILL.md','skill.md'].map(s => path.join(p, s)).find(fs.existsSync)
799
+ : (f.endsWith('.md') ? p : null);
800
+ if (!skillFile) return false;
801
+ const fm = readFrontmatter(skillFile);
802
+ return fm.name && fm.name.startsWith('rdc:');
803
+ })
804
+ : [];
805
+ if (stillThere.length === 0) {
806
+ ok(`skills/user/ : no rdc: orphans`);
807
+ } else {
808
+ fail(`skills/user/ : still has rdc: orphans: ${stillThere.join(', ')}`);
809
+ verifyFailed = true;
810
+ }
811
+ }
812
+ if (verifyFailed) {
813
+ console.log('');
814
+ fail('Post-install verification FAILED — duplicates or orphans remain. Investigate.');
815
+ process.exit(2);
816
+ }
817
+
745
818
  // Done
746
819
  console.log('');
747
820
  console.log(' \x1b[32mDone!\x1b[0m');