@lifeaitools/rdc-skills 0.25.9 → 0.26.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/.claude-plugin/plugin.json +1608 -1551
- package/.github/workflows/self-test.yml +34 -34
- package/CHANGELOG.md +332 -319
- package/MANIFEST.md +224 -224
- package/README.md +368 -367
- package/RELEASE.md +50 -45
- package/bin/rdc-skills-mcp.mjs +1 -1
- package/commands/build.md +181 -181
- package/commands/collab.md +180 -180
- package/commands/deploy.md +173 -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/deploy/install-systemd.sh +16 -0
- package/deploy/systemd/rdc-skills-mcp.service +19 -0
- 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/guides/orchestration-epic.md +17 -0
- package/hooks/check-rdc-environment.js +172 -318
- package/hooks/lib/box-lock.js +28 -8
- package/package.json +1 -2
- package/scripts/install-rdc-skills.js +1244 -1474
- package/scripts/probe-installed-hooks.mjs +2 -2
- package/scripts/self-test.mjs +1459 -1459
- package/scripts/validate-publish-manifests.js +502 -502
- package/skills/build/SKILL.md +662 -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 +583 -541
- package/skills/design/SKILL.md +205 -205
- package/skills/env/SKILL.md +146 -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/refactor/SKILL.md +51 -0
- 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 +59 -55
- package/skills/tests/README.md +1 -1
- package/skills/tests/onramp.test.json +101 -87
- package/skills/tests/rdc-env.test.json +12 -0
- package/skills/tests/rdc-new-model.test.json +12 -0
- package/skills/tests/rdc-refactor.test.json +29 -0
- package/skills/tests/rdc-regen-media.test.json +29 -29
- package/skills/watch/SKILL.md +84 -84
- package/skills/workitems/SKILL.md +151 -151
- package/tests/no-local-pm2.test.mjs +45 -0
- package/scripts/local-install-with-stop.sh +0 -41
- package/scripts/probe-lock-holders.mjs +0 -36
- package/scripts/rebuild-mcp.mjs +0 -107
|
@@ -1,318 +1,172 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* SessionStart hook —
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function
|
|
37
|
-
return
|
|
38
|
-
encoding: 'utf8',
|
|
39
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
40
|
-
timeout: opts.timeout || 30000,
|
|
41
|
-
cwd: opts.cwd || process.cwd(),
|
|
42
|
-
env: { ...process.env, ...(opts.env || {}) },
|
|
43
|
-
}).trim();
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function
|
|
57
|
-
try {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
markRepaired(reason);
|
|
174
|
-
} finally {
|
|
175
|
-
// ALWAYS restart, even when the install threw. Leaving the MCP stopped would
|
|
176
|
-
// turn a failed repair into a worse outage than the one being repaired.
|
|
177
|
-
for (const name of holders) {
|
|
178
|
-
try {
|
|
179
|
-
shell(`pm2 restart ${q(name)}`, { timeout: 30000 });
|
|
180
|
-
} catch {
|
|
181
|
-
/* surfaced by the health re-check below */
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* ── The global package is BOX-WIDE; this hook is PER-SESSION ──────────────────
|
|
189
|
-
*
|
|
190
|
-
* Every session and every worktree runs this hook, but there is only ONE global
|
|
191
|
-
* npm package and ONE rdc-skills-mcp on the machine. Without coordination, N
|
|
192
|
-
* sessions starting together each independently conclude "unhealthy" and each run
|
|
193
|
-
* `npm install -g` against the same directory — they fight, and on Windows they
|
|
194
|
-
* fight over a directory a live process is sitting in.
|
|
195
|
-
*
|
|
196
|
-
* recentlyRepaired() did not prevent this: it gated the BLOCK path, not the REPAIR
|
|
197
|
-
* path, so concurrent starts all saw "not recently repaired" and all installed.
|
|
198
|
-
*
|
|
199
|
-
* A box-wide resource gets a box-wide update: exactly one session performs it, the
|
|
200
|
-
* rest wait for it and re-probe. Waiting is the correct behaviour for a follower —
|
|
201
|
-
* the leader is already fixing the thing they would have fixed.
|
|
202
|
-
*/
|
|
203
|
-
// Single home: hooks/lib/box-lock.js. Kept out of this file so the verification
|
|
204
|
-
// probe can exercise the REAL implementation instead of a copy that drifts.
|
|
205
|
-
const { acquireBoxLock, releaseBoxLock, LOCK_PATH: lockPath } = require('./lib/box-lock');
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Wait for the session that owns the update to finish, re-probing health.
|
|
211
|
-
*
|
|
212
|
-
* MUST stay under the SessionStart hook timeout (see HOOK_TIMEOUT_SEC in
|
|
213
|
-
* scripts/install-rdc-skills.js). At 90s against a 60s harness default the
|
|
214
|
-
* follower was killed BEFORE it could re-probe or emit a verdict, so a
|
|
215
|
-
* slow-but-succeeding repair looked like a hook crash to every follower.
|
|
216
|
-
*/
|
|
217
|
-
async function waitForBoxRepair(timeoutMs = 45000) {
|
|
218
|
-
const deadline = Date.now() + timeoutMs;
|
|
219
|
-
while (Date.now() < deadline) {
|
|
220
|
-
await new Promise((r) => setTimeout(r, 3000));
|
|
221
|
-
const h = await health();
|
|
222
|
-
if (h && h.status === 'ok' && Number(h.skills || 0) >= MIN_SKILLS) return h;
|
|
223
|
-
if (!fs.existsSync(lockPath)) break; // leader finished; take one last look
|
|
224
|
-
}
|
|
225
|
-
return health();
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function block(message, details = {}) {
|
|
229
|
-
hookLog('check-rdc-environment', 'SessionStart', 'block', { message, ...details });
|
|
230
|
-
process.stdout.write(JSON.stringify({
|
|
231
|
-
systemMessage:
|
|
232
|
-
`HARD BLOCK — RDC skills environment is not healthy.\n\n` +
|
|
233
|
-
`${message}\n\n` +
|
|
234
|
-
`Do not proceed with RDC work until the approved install path is repaired.\n\n` +
|
|
235
|
-
`STOP THE SERVER FIRST — npm upgrades by renaming the global package dir, and\n` +
|
|
236
|
-
`Windows cannot rename a running process's cwd. Skipping this step is what\n` +
|
|
237
|
-
`produces "EBUSY ... rename ... @lifeaitools/rdc-skills":\n\n` +
|
|
238
|
-
`pm2 stop rdc-skills-mcp\n` +
|
|
239
|
-
`npm install -g @lifeaitools/rdc-skills@latest\n` +
|
|
240
|
-
`rdc-skills-install --profile lifeai --project-root ${projectRoot()} --write-startup-blocks\n` +
|
|
241
|
-
`pm2 restart rdc-skills-mcp`
|
|
242
|
-
}));
|
|
243
|
-
process.exit(1);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
async function main() {
|
|
247
|
-
const initialPkg = globalPackageJson();
|
|
248
|
-
const initialHealth = await health();
|
|
249
|
-
const reasons = [];
|
|
250
|
-
|
|
251
|
-
if (!initialPkg) reasons.push('global package missing');
|
|
252
|
-
if (!commandExists('rdc-skills-install')) reasons.push('installer command missing');
|
|
253
|
-
if (!initialHealth || initialHealth.status !== 'ok' || Number(initialHealth.skills || 0) < MIN_SKILLS) {
|
|
254
|
-
reasons.push('local MCP health/catalog invalid');
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
if (reasons.length) {
|
|
258
|
-
if (recentlyRepaired()) {
|
|
259
|
-
block(`RDC skills still unhealthy after a recent repair attempt: ${reasons.join(', ')}`);
|
|
260
|
-
}
|
|
261
|
-
if (acquireBoxLock()) {
|
|
262
|
-
// This session is the box's updater. block() calls process.exit(1), which
|
|
263
|
-
// SKIPS finally — so the error is captured here and reported only AFTER the
|
|
264
|
-
// lock has been released. Calling block() from inside the try/catch leaked
|
|
265
|
-
// the lock on exactly the path most likely to run.
|
|
266
|
-
let repairError = null;
|
|
267
|
-
try {
|
|
268
|
-
repair(reasons.join(', '));
|
|
269
|
-
} catch (err) {
|
|
270
|
-
repairError = err;
|
|
271
|
-
} finally {
|
|
272
|
-
releaseBoxLock();
|
|
273
|
-
}
|
|
274
|
-
if (repairError) {
|
|
275
|
-
block(`Automatic RDC skills repair failed: ${repairError.message}`, { reasons });
|
|
276
|
-
}
|
|
277
|
-
} else {
|
|
278
|
-
// Another session owns the box-wide update. Racing it is what broke this.
|
|
279
|
-
hookLog('check-rdc-environment', 'SessionStart', 'await-box-repair', { reasons });
|
|
280
|
-
await waitForBoxRepair();
|
|
281
|
-
// Re-evaluate from scratch rather than trusting the pre-wait `reasons`. A
|
|
282
|
-
// follower's only complaint is often a transient blip caused by the LEADER's
|
|
283
|
-
// own `pm2 restart`; blocking on that stale list hard-blocked sessions the
|
|
284
|
-
// leader had already fixed.
|
|
285
|
-
const afterReasons = [];
|
|
286
|
-
if (!globalPackageJson()) afterReasons.push('global package missing');
|
|
287
|
-
if (!commandExists('rdc-skills-install')) afterReasons.push('installer command missing');
|
|
288
|
-
const h = await health();
|
|
289
|
-
if (!h || h.status !== 'ok' || Number(h.skills || 0) < MIN_SKILLS) {
|
|
290
|
-
afterReasons.push('local MCP health/catalog invalid');
|
|
291
|
-
}
|
|
292
|
-
if (afterReasons.length) {
|
|
293
|
-
block(
|
|
294
|
-
'Another session is updating the box-wide rdc-skills install and it did not '
|
|
295
|
-
+ `become healthy in time: ${afterReasons.join(', ')}`,
|
|
296
|
-
{ reasons, afterReasons, waited: true },
|
|
297
|
-
);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const finalPkg = globalPackageJson();
|
|
303
|
-
const finalHealth = await health();
|
|
304
|
-
if (!finalPkg) block('Global @lifeaitools/rdc-skills package is missing after repair.');
|
|
305
|
-
if (!commandExists('rdc-skills-install')) block('rdc-skills-install is missing after repair.');
|
|
306
|
-
if (!finalHealth || finalHealth.status !== 'ok' || Number(finalHealth.skills || 0) < MIN_SKILLS) {
|
|
307
|
-
block(`rdc-skills MCP is unhealthy after repair. Health: ${JSON.stringify(finalHealth)}`);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
hookLog('check-rdc-environment', 'SessionStart', 'pass', {
|
|
311
|
-
version: finalPkg.version,
|
|
312
|
-
mcpVersion: finalHealth.version,
|
|
313
|
-
skills: finalHealth.skills,
|
|
314
|
-
});
|
|
315
|
-
process.exit(0);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
main().catch((err) => block(`RDC skills environment check crashed: ${err.message}`));
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SessionStart hook — verifies the locally installed RDC skill package.
|
|
4
|
+
*
|
|
5
|
+
* The public rdc-skills MCP is an independently hosted, stateless connector for
|
|
6
|
+
* claude.ai and other MCP clients. It is not a Windows daemon and this hook
|
|
7
|
+
* never starts, stops, probes, or installs a process manager.
|
|
8
|
+
*/
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { execFileSync, execSync } = require('child_process');
|
|
15
|
+
const hookLog = require('./hook-logger');
|
|
16
|
+
const { acquireBoxLock, releaseBoxLock, LOCK_PATH: lockPath } = require('./lib/box-lock');
|
|
17
|
+
|
|
18
|
+
const PACKAGE = '@lifeaitools/rdc-skills';
|
|
19
|
+
const stampPath = path.join(os.tmpdir(), 'rdc-skills-environment-last-repair.json');
|
|
20
|
+
const SAFE_PATH = /^[A-Za-z0-9 :._\\/\-]+$/;
|
|
21
|
+
|
|
22
|
+
function q(value) {
|
|
23
|
+
return `"${String(value).replace(/"/g, '\\"')}"`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function run(command, args, opts = {}) {
|
|
27
|
+
return execFileSync(command, args, {
|
|
28
|
+
encoding: 'utf8',
|
|
29
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
30
|
+
timeout: opts.timeout || 30000,
|
|
31
|
+
cwd: opts.cwd || process.cwd(),
|
|
32
|
+
env: { ...process.env, ...(opts.env || {}) },
|
|
33
|
+
}).trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function shell(command, opts = {}) {
|
|
37
|
+
return execSync(command, {
|
|
38
|
+
encoding: 'utf8',
|
|
39
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
40
|
+
timeout: opts.timeout || 30000,
|
|
41
|
+
cwd: opts.cwd || process.cwd(),
|
|
42
|
+
env: { ...process.env, ...(opts.env || {}) },
|
|
43
|
+
}).trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function commandExists(name) {
|
|
47
|
+
try {
|
|
48
|
+
if (process.platform === 'win32') run('where.exe', [name], { timeout: 5000 });
|
|
49
|
+
else run('which', [name], { timeout: 5000 });
|
|
50
|
+
return true;
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function projectRoot() {
|
|
57
|
+
try {
|
|
58
|
+
return shell('git rev-parse --show-toplevel', { timeout: 5000 });
|
|
59
|
+
} catch {
|
|
60
|
+
return process.cwd();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function globalPackageJson() {
|
|
65
|
+
try {
|
|
66
|
+
const root = shell('npm root -g', { timeout: 10000 });
|
|
67
|
+
const pkg = path.join(root, '@lifeaitools', 'rdc-skills', 'package.json');
|
|
68
|
+
if (!fs.existsSync(pkg)) return null;
|
|
69
|
+
return JSON.parse(fs.readFileSync(pkg, 'utf8'));
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function recentlyRepaired() {
|
|
76
|
+
try {
|
|
77
|
+
const data = JSON.parse(fs.readFileSync(stampPath, 'utf8'));
|
|
78
|
+
return Date.now() - Date.parse(data.ts) < 10 * 60 * 1000;
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function markRepaired(reason) {
|
|
85
|
+
try {
|
|
86
|
+
fs.writeFileSync(stampPath, JSON.stringify({ ts: new Date().toISOString(), reason }, null, 2));
|
|
87
|
+
} catch {
|
|
88
|
+
/* best effort */
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function repair(reason) {
|
|
93
|
+
hookLog('check-rdc-environment', 'SessionStart', 'repair', { reason });
|
|
94
|
+
shell(`npm install -g ${q(`${PACKAGE}@latest`)}`, { timeout: 120000 });
|
|
95
|
+
const root = projectRoot();
|
|
96
|
+
if (!SAFE_PATH.test(root)) {
|
|
97
|
+
throw new Error(`refusing to shell out with an unsafe project root: ${root}`);
|
|
98
|
+
}
|
|
99
|
+
shell(`rdc-skills-install --profile lifeai --project-root ${q(root)} --write-startup-blocks`, {
|
|
100
|
+
timeout: 180000,
|
|
101
|
+
});
|
|
102
|
+
markRepaired(reason);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function localReasons() {
|
|
106
|
+
const reasons = [];
|
|
107
|
+
if (!globalPackageJson()) reasons.push('global package missing');
|
|
108
|
+
if (!commandExists('rdc-skills-install')) reasons.push('installer command missing');
|
|
109
|
+
return reasons;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function waitForBoxRepair(timeoutMs = 45000) {
|
|
113
|
+
const deadline = Date.now() + timeoutMs;
|
|
114
|
+
while (Date.now() < deadline) {
|
|
115
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
116
|
+
if (localReasons().length === 0) return;
|
|
117
|
+
if (!fs.existsSync(lockPath)) break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function block(message, details = {}) {
|
|
122
|
+
hookLog('check-rdc-environment', 'SessionStart', 'block', { message, ...details });
|
|
123
|
+
process.stdout.write(JSON.stringify({
|
|
124
|
+
systemMessage:
|
|
125
|
+
`HARD BLOCK — RDC skills package is not healthy.\n\n` +
|
|
126
|
+
`${message}\n\n` +
|
|
127
|
+
`Repair the approved local package/plugin installation:\n\n` +
|
|
128
|
+
`npm install -g @lifeaitools/rdc-skills@latest\n` +
|
|
129
|
+
`rdc-skills-install --profile lifeai --project-root ${projectRoot()} --write-startup-blocks`,
|
|
130
|
+
}));
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function main() {
|
|
135
|
+
const reasons = localReasons();
|
|
136
|
+
if (reasons.length) {
|
|
137
|
+
if (recentlyRepaired()) {
|
|
138
|
+
block(`RDC skills still unhealthy after a recent repair attempt: ${reasons.join(', ')}`);
|
|
139
|
+
}
|
|
140
|
+
if (acquireBoxLock()) {
|
|
141
|
+
let repairError = null;
|
|
142
|
+
try {
|
|
143
|
+
repair(reasons.join(', '));
|
|
144
|
+
} catch (error) {
|
|
145
|
+
repairError = error;
|
|
146
|
+
} finally {
|
|
147
|
+
releaseBoxLock();
|
|
148
|
+
}
|
|
149
|
+
if (repairError) {
|
|
150
|
+
block(`Automatic RDC skills repair failed: ${repairError.message}`, { reasons });
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
hookLog('check-rdc-environment', 'SessionStart', 'await-box-repair', { reasons });
|
|
154
|
+
await waitForBoxRepair();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const finalReasons = localReasons();
|
|
159
|
+
if (finalReasons.length) {
|
|
160
|
+
block(`RDC skills package is unhealthy after repair: ${finalReasons.join(', ')}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const pkg = globalPackageJson();
|
|
164
|
+
hookLog('check-rdc-environment', 'SessionStart', 'pass', { version: pkg.version });
|
|
165
|
+
process.exit(0);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (require.main === module) {
|
|
169
|
+
main().catch((error) => block(`RDC skills environment check crashed: ${error.message}`));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = { main, repair, localReasons };
|
package/hooks/lib/box-lock.js
CHANGED
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Box-wide single-flight lock — ONE home for the rule.
|
|
4
4
|
*
|
|
5
|
-
* The global rdc-skills package
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* directory a live process is sitting in.
|
|
5
|
+
* The global rdc-skills package is BOX-WIDE (one per machine), but the
|
|
6
|
+
* SessionStart hook runs per session and per worktree. Without coordination,
|
|
7
|
+
* N sessions can each conclude "missing" and run `npm install -g` against the
|
|
8
|
+
* same directory.
|
|
10
9
|
*
|
|
11
10
|
* This lives in its own module because the verification probe MUST exercise the
|
|
12
11
|
* real implementation. It previously kept a private copy of these functions, and
|
|
@@ -96,8 +95,16 @@ function acquireBoxLock() {
|
|
|
96
95
|
// 0/12, 1/12, 2/12 double-leader rounds across runs, which is the worst kind
|
|
97
96
|
// of bug to ship: it passes CI and fails on a busy machine.
|
|
98
97
|
settle(SETTLE_MS);
|
|
98
|
+
// Distinguish "someone else took it" from "the read failed". Standing down on
|
|
99
|
+
// a transient read error can elect ZERO leaders — nobody repairs the box while
|
|
100
|
+
// every session waits and then hard-blocks. Only a genuine byte MISMATCH means
|
|
101
|
+
// another session leads.
|
|
99
102
|
let observed = null;
|
|
100
|
-
|
|
103
|
+
let readOk = false;
|
|
104
|
+
for (let r = 0; r < 2 && !readOk; r++) {
|
|
105
|
+
try { observed = fs.readFileSync(LOCK_PATH, 'utf8'); readOk = true; } catch { settle(20); }
|
|
106
|
+
}
|
|
107
|
+
if (!readOk) continue; // could not verify — retry the create
|
|
101
108
|
if (observed !== body) return false; // someone reclaimed it; they lead
|
|
102
109
|
|
|
103
110
|
lockHeld = true;
|
|
@@ -145,8 +152,21 @@ function acquireBoxLock() {
|
|
|
145
152
|
let claimRaw = null;
|
|
146
153
|
try { claimRaw = fs.readFileSync(claim, 'utf8'); } catch { claimRaw = null; }
|
|
147
154
|
if (claimRaw !== heldRaw) {
|
|
148
|
-
// Not ours to take —
|
|
149
|
-
|
|
155
|
+
// Not ours to take — DISCARD the claim and stand down.
|
|
156
|
+
//
|
|
157
|
+
// Do NOT rename it back. renameSync silently OVERWRITES an existing
|
|
158
|
+
// destination, and the bytes we hold are the STALE ones (that is why we
|
|
159
|
+
// judged them reclaimable). Restoring them clobbers the live leader's
|
|
160
|
+
// fresh lock with a stale body, so the next session judges it
|
|
161
|
+
// reclaimable and becomes a SECOND LEADER while the true leader is
|
|
162
|
+
// mid `npm install -g` — the exact race this module exists to prevent,
|
|
163
|
+
// arriving through the restore path. Measured, and invisible to the
|
|
164
|
+
// concurrency probe because it seeds a dead-PID lock, so claimRaw
|
|
165
|
+
// always equals heldRaw and this branch never runs there.
|
|
166
|
+
//
|
|
167
|
+
// Whoever owns the path already owns the box. Losing the stale bytes
|
|
168
|
+
// costs nothing; putting them back can only mislead.
|
|
169
|
+
try { fs.unlinkSync(claim); } catch { /* already gone */ }
|
|
150
170
|
return false;
|
|
151
171
|
}
|
|
152
172
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lifeaitools/rdc-skills",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.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",
|
|
@@ -45,7 +45,6 @@
|
|
|
45
45
|
"test:channel-formatter:remote": "node tests/channel-formatter.contract.test.mjs --remote",
|
|
46
46
|
"test:brochure": "node tests/rdc-brochure.test.mjs",
|
|
47
47
|
"mcp": "node bin/rdc-skills-mcp.mjs",
|
|
48
|
-
"rebuild-mcp": "node scripts/rebuild-mcp.mjs",
|
|
49
48
|
"prepack": "node scripts/prepack.mjs"
|
|
50
49
|
},
|
|
51
50
|
"dependencies": {
|