@lifeaitools/rdc-skills 0.25.4 → 0.25.9

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 (71) hide show
  1. package/.claude-plugin/plugin.json +1574 -1528
  2. package/.github/workflows/self-test.yml +34 -34
  3. package/CHANGELOG.md +326 -313
  4. package/MANIFEST.md +224 -224
  5. package/README.md +372 -369
  6. package/RELEASE.md +26 -2
  7. package/commands/build.md +181 -181
  8. package/commands/collab.md +180 -180
  9. package/commands/deploy.md +148 -148
  10. package/commands/fixit.md +150 -150
  11. package/commands/handoff.md +173 -173
  12. package/commands/overnight.md +220 -220
  13. package/commands/plan.md +158 -158
  14. package/commands/preplan.md +131 -131
  15. package/commands/prototype.md +145 -145
  16. package/commands/report.md +99 -99
  17. package/commands/review.md +120 -120
  18. package/commands/status.md +86 -86
  19. package/commands/workitems.md +127 -127
  20. package/git-sha.json +1 -1
  21. package/guides/agent-bootstrap.md +195 -195
  22. package/guides/agents/backend.md +102 -102
  23. package/guides/agents/content.md +94 -94
  24. package/guides/agents/cs2.md +56 -56
  25. package/guides/agents/data.md +86 -86
  26. package/guides/agents/design.md +77 -77
  27. package/guides/agents/frontend.md +91 -91
  28. package/guides/agents/infrastructure.md +81 -81
  29. package/guides/agents/setup.md +272 -272
  30. package/guides/agents/verify.md +119 -119
  31. package/guides/agents/viz.md +106 -106
  32. package/hooks/check-rdc-environment.js +318 -164
  33. package/hooks/lib/box-lock.js +186 -0
  34. package/package.json +1 -1
  35. package/scripts/install-rdc-skills.js +1474 -1400
  36. package/scripts/local-install-with-stop.sh +41 -0
  37. package/scripts/probe-box-lock.mjs +179 -0
  38. package/scripts/probe-installed-hooks.mjs +60 -0
  39. package/scripts/probe-lock-holders.mjs +36 -0
  40. package/scripts/self-test.mjs +1459 -1459
  41. package/scripts/validate-publish-manifests.js +502 -502
  42. package/skills/build/SKILL.md +578 -578
  43. package/skills/channel-formatter/SKILL.md +538 -538
  44. package/skills/collab/SKILL.md +239 -239
  45. package/skills/convert/SKILL.md +150 -161
  46. package/skills/deploy/SKILL.md +541 -541
  47. package/skills/design/SKILL.md +205 -205
  48. package/skills/env/SKILL.md +139 -139
  49. package/skills/fixit/SKILL.md +203 -203
  50. package/skills/handoff/SKILL.md +236 -236
  51. package/skills/housekeeping/SKILL.md +189 -189
  52. package/skills/new-model/SKILL.md +14 -3
  53. package/skills/onramp/SKILL.md +1459 -1459
  54. package/skills/overnight/SKILL.md +251 -251
  55. package/skills/plan/SKILL.md +345 -345
  56. package/skills/preplan/SKILL.md +90 -90
  57. package/skills/prototype/SKILL.md +150 -150
  58. package/skills/regen-media/SKILL.md +94 -94
  59. package/skills/release/SKILL.md +140 -140
  60. package/skills/report/SKILL.md +100 -100
  61. package/skills/review/SKILL.md +151 -151
  62. package/skills/self-test/SKILL.md +108 -108
  63. package/skills/status/SKILL.md +99 -99
  64. package/skills/tests/MATRIX.md +55 -55
  65. package/skills/tests/onramp.test.json +87 -87
  66. package/skills/tests/rdc-regen-media.test.json +29 -29
  67. package/skills/watch/SKILL.md +84 -84
  68. package/skills/workitems/SKILL.md +151 -151
  69. package/tests/curl-surface.test.mjs +4 -2
  70. package/tests/help-surface.test.mjs +1 -1
  71. package/tests/install-rdc-skills.test.mjs +1 -1
@@ -0,0 +1,186 @@
1
+ 'use strict';
2
+ /**
3
+ * Box-wide single-flight lock — ONE home for the rule.
4
+ *
5
+ * The global rdc-skills package and rdc-skills-mcp are BOX-WIDE (one per machine),
6
+ * but the SessionStart hook runs per session and per worktree. Without
7
+ * coordination, N sessions each conclude "unhealthy" and each run
8
+ * `npm install -g` against the same directory — and on Windows, against a
9
+ * directory a live process is sitting in.
10
+ *
11
+ * This lives in its own module because the verification probe MUST exercise the
12
+ * real implementation. It previously kept a private copy of these functions, and
13
+ * that copy silently drifted to the old algorithm — so the probe reported PASS for
14
+ * code that was not shipping, and later FAIL for code that was already fixed. A
15
+ * test that duplicates its subject tests nothing.
16
+ */
17
+ const fs = require('node:fs');
18
+ const os = require('node:os');
19
+ const path = require('node:path');
20
+
21
+ const LOCK_PATH = path.join(os.tmpdir(), 'rdc-skills-environment-repair.lock');
22
+ const LOCK_STALE_MS = 5 * 60 * 1000;
23
+
24
+ let lockHeld = false;
25
+
26
+ // How long to let a fresh acquisition settle before trusting it. Long enough to
27
+ // cover the remove-then-create gap of a concurrent reclaim, short enough to be
28
+ // invisible on a SessionStart path that already budgets minutes for repair.
29
+ const SETTLE_MS = 60;
30
+
31
+ /** Synchronous sleep — this module runs in a hook with no event loop to yield to. */
32
+ function settle(ms) {
33
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
34
+ }
35
+
36
+ /**
37
+ * Is `pid` a live process?
38
+ *
39
+ * PID 0 is NOT a probe: process.kill(0, 0) signals the CURRENT PROCESS GROUP and
40
+ * succeeds, so a truncated lock written as {"pid":0} would read as alive forever.
41
+ * Reject non-positive/non-integer pids before asking the OS.
42
+ */
43
+ function processAlive(pid) {
44
+ if (!Number.isInteger(pid) || pid <= 0) return false;
45
+ try {
46
+ process.kill(pid, 0);
47
+ return true;
48
+ } catch (err) {
49
+ return Boolean(err && err.code === 'EPERM'); // exists, not ours to signal
50
+ }
51
+ }
52
+
53
+ /**
54
+ * A lock we cannot reason about is stale.
55
+ *
56
+ * A parseable-but-corrupt body ({"pid":0,"ts":"garbage"}) threw nothing, so
57
+ * `NaN > LOCK_STALE_MS` was false and the lock was judged live FOREVER — wedging
58
+ * every session onto the follower path until the tmpdir file was deleted by hand.
59
+ */
60
+ function lockIsStale(held) {
61
+ const pid = Number(held && held.pid);
62
+ const age = Date.now() - Date.parse(held && held.ts);
63
+ if (!Number.isInteger(pid) || pid <= 0 || !Number.isFinite(age)) return true;
64
+ return !processAlive(pid) || age > LOCK_STALE_MS;
65
+ }
66
+
67
+ /**
68
+ * Become the box's updater, or report that someone else already is.
69
+ *
70
+ * Stale locks are reclaimed by RENAME, never unlink-then-create. The naive
71
+ * sequence (read → judge stale → unlink → open 'wx') is a TOCTOU: two sessions can
72
+ * both read the same stale lock, both judge it stale, and the second's unlink can
73
+ * delete the FIRST's freshly created lock — leaving both convinced they own the
74
+ * box-wide update, which is the exact race this module exists to prevent.
75
+ * rename() is atomic, so only one racer can win, and only the winner proceeds.
76
+ */
77
+ function acquireBoxLock() {
78
+ for (let attempt = 0; attempt < 4; attempt++) {
79
+ try {
80
+ const body = JSON.stringify({ pid: process.pid, ts: new Date().toISOString() });
81
+ const fd = fs.openSync(LOCK_PATH, 'wx');
82
+ fs.writeSync(fd, body);
83
+ fs.closeSync(fd);
84
+
85
+ // POST-ACQUIRE VERIFICATION — the part that actually makes this single-flight.
86
+ //
87
+ // O_EXCL guarantees only one process can CREATE the file, but not one OWNER
88
+ // across a delete-and-recreate: reclaiming a stale lock necessarily leaves the
89
+ // path empty for an instant, and a racer arriving in that gap creates it
90
+ // legitimately. Both then believe they lead. No amount of care in the reclaim
91
+ // path closes that window — it is inherent to remove-then-create.
92
+ //
93
+ // So settle briefly and re-read: whoever's bytes are still there owns the box,
94
+ // and everyone else stands down. This converges on exactly one leader instead
95
+ // of trying to make the gap not exist. Without it the race was intermittent —
96
+ // 0/12, 1/12, 2/12 double-leader rounds across runs, which is the worst kind
97
+ // of bug to ship: it passes CI and fails on a busy machine.
98
+ settle(SETTLE_MS);
99
+ let observed = null;
100
+ try { observed = fs.readFileSync(LOCK_PATH, 'utf8'); } catch { observed = null; }
101
+ if (observed !== body) return false; // someone reclaimed it; they lead
102
+
103
+ lockHeld = true;
104
+ return true;
105
+ } catch (err) {
106
+ if (err.code !== 'EEXIST') return false;
107
+
108
+ let heldRaw = null;
109
+ let held = null;
110
+ try {
111
+ heldRaw = fs.readFileSync(LOCK_PATH, 'utf8');
112
+ held = JSON.parse(heldRaw);
113
+ } catch {
114
+ held = null; // unreadable/corrupt → stale
115
+ }
116
+ if (!lockIsStale(held)) return false;
117
+
118
+ // Could not READ the file we are about to judge — it vanished (the winner is
119
+ // mid-reclaim, between its rename and its re-create) or is unreadable. We
120
+ // have no identity to verify against, so reclaiming here would rename away
121
+ // the winner's brand-new lock unverified and elect a second leader. Measured:
122
+ // this window alone produced 2/12 double-leader rounds. Retry the create
123
+ // instead; if the winner has re-created by then we will see a fresh, live
124
+ // lock and correctly stand down.
125
+ //
126
+ // A corrupt-but-readable body (e.g. "not json") still reclaims normally —
127
+ // heldRaw is non-null there, so this does not wedge a genuinely bad lock.
128
+ if (heldRaw === null) continue;
129
+
130
+ // Claim the RIGHT to reclaim. Losing this rename means another session got
131
+ // there first and now owns the update, so we are a follower.
132
+ const claim = `${LOCK_PATH}.${process.pid}.${attempt}.reclaim`;
133
+ try {
134
+ fs.renameSync(LOCK_PATH, claim);
135
+ } catch {
136
+ return false;
137
+ }
138
+
139
+ // VERIFY WE TOOK THE FILE WE JUDGED. rename() moves whatever is at the path,
140
+ // not the bytes we inspected. Without this check a slow racer renames away
141
+ // the WINNER'S BRAND-NEW lock — it judged the old stale body, then moved the
142
+ // fresh one — and both processes become leader. Measured: 12/12 rounds
143
+ // elected two leaders before this check existed (scripts/probe-box-lock.mjs).
144
+ if (heldRaw !== null) {
145
+ let claimRaw = null;
146
+ try { claimRaw = fs.readFileSync(claim, 'utf8'); } catch { claimRaw = null; }
147
+ if (claimRaw !== heldRaw) {
148
+ // Not ours to take — put it back and stand down.
149
+ try { fs.renameSync(claim, LOCK_PATH); } catch { /* owner will re-create */ }
150
+ return false;
151
+ }
152
+ }
153
+ try { fs.unlinkSync(claim); } catch { /* already gone */ }
154
+ }
155
+ }
156
+ return false;
157
+ }
158
+
159
+ function releaseBoxLock() {
160
+ if (!lockHeld) return;
161
+ lockHeld = false;
162
+ try { fs.unlinkSync(LOCK_PATH); } catch { /* best effort */ }
163
+ }
164
+
165
+ function holdsBoxLock() {
166
+ return lockHeld;
167
+ }
168
+
169
+ // Safety net. The hook's block() calls process.exit(1), which terminates
170
+ // IMMEDIATELY and does NOT unwind pending finally blocks — so the failure path (the
171
+ // one most likely to run) leaked the lock. A leaked lock usually self-heals via the
172
+ // dead-PID check, but Windows recycles PIDs aggressively: if a live process inherits
173
+ // the dead owner's PID inside the 5-minute window, the lock reads as live and every
174
+ // concurrent session waits then hard-blocks. 'exit' handlers DO run on
175
+ // process.exit(), so this covers exit paths we did not anticipate.
176
+ process.on('exit', releaseBoxLock);
177
+
178
+ module.exports = {
179
+ LOCK_PATH,
180
+ LOCK_STALE_MS,
181
+ acquireBoxLock,
182
+ releaseBoxLock,
183
+ holdsBoxLock,
184
+ lockIsStale,
185
+ processAlive,
186
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifeaitools/rdc-skills",
3
- "version": "0.25.4",
3
+ "version": "0.25.9",
4
4
  "description": "RDC typed-agent dispatch skill suite for Claude Code - plan, build, review, overnight builds",
5
5
  "keywords": [
6
6
  "claude-code",