@lifeaitools/rdc-skills 0.25.5 → 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.
- package/.claude-plugin/plugin.json +1550 -1550
- package/.github/workflows/self-test.yml +34 -34
- package/CHANGELOG.md +319 -319
- package/MANIFEST.md +224 -224
- package/README.md +367 -367
- package/commands/build.md +181 -181
- package/commands/collab.md +180 -180
- package/commands/deploy.md +148 -148
- package/commands/fixit.md +150 -150
- package/commands/handoff.md +173 -173
- package/commands/overnight.md +220 -220
- package/commands/plan.md +158 -158
- package/commands/preplan.md +131 -131
- package/commands/prototype.md +145 -145
- package/commands/report.md +99 -99
- package/commands/review.md +120 -120
- package/commands/status.md +86 -86
- package/commands/workitems.md +127 -127
- package/git-sha.json +1 -1
- package/guides/agent-bootstrap.md +195 -195
- package/guides/agents/backend.md +102 -102
- package/guides/agents/content.md +94 -94
- package/guides/agents/cs2.md +56 -56
- package/guides/agents/data.md +86 -86
- package/guides/agents/design.md +77 -77
- package/guides/agents/frontend.md +91 -91
- package/guides/agents/infrastructure.md +81 -81
- package/guides/agents/setup.md +272 -272
- package/guides/agents/verify.md +119 -119
- package/guides/agents/viz.md +106 -106
- package/hooks/check-rdc-environment.js +318 -164
- package/hooks/lib/box-lock.js +186 -0
- package/package.json +1 -1
- package/scripts/install-rdc-skills.js +1474 -1400
- package/scripts/local-install-with-stop.sh +41 -0
- package/scripts/probe-box-lock.mjs +179 -0
- package/scripts/probe-installed-hooks.mjs +60 -0
- package/scripts/probe-lock-holders.mjs +36 -0
- package/scripts/self-test.mjs +1459 -1459
- package/scripts/validate-publish-manifests.js +502 -502
- package/skills/build/SKILL.md +578 -578
- package/skills/channel-formatter/SKILL.md +538 -538
- package/skills/collab/SKILL.md +239 -239
- package/skills/convert/SKILL.md +138 -138
- package/skills/deploy/SKILL.md +541 -541
- package/skills/design/SKILL.md +205 -205
- package/skills/env/SKILL.md +139 -139
- package/skills/fixit/SKILL.md +203 -203
- package/skills/handoff/SKILL.md +236 -236
- package/skills/housekeeping/SKILL.md +189 -189
- package/skills/onramp/SKILL.md +1459 -1459
- package/skills/overnight/SKILL.md +251 -251
- package/skills/plan/SKILL.md +345 -345
- package/skills/preplan/SKILL.md +90 -90
- package/skills/prototype/SKILL.md +150 -150
- package/skills/regen-media/SKILL.md +94 -94
- package/skills/release/SKILL.md +140 -140
- package/skills/report/SKILL.md +100 -100
- package/skills/review/SKILL.md +151 -151
- package/skills/self-test/SKILL.md +108 -108
- package/skills/status/SKILL.md +99 -99
- package/skills/tests/MATRIX.md +55 -55
- package/skills/tests/onramp.test.json +87 -87
- package/skills/tests/rdc-regen-media.test.json +29 -29
- package/skills/watch/SKILL.md +84 -84
- package/skills/workitems/SKILL.md +151 -151
|
@@ -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
|
+
};
|