@lifeaitools/rdc-skills 0.25.5 → 0.25.10
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 +378 -164
- package/hooks/lib/box-lock.js +207 -0
- package/package.json +1 -1
- package/scripts/install-rdc-skills.js +97 -8
- 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
|
@@ -1,164 +1,378 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* SessionStart hook — hard gate for the RDC skills runtime.
|
|
4
|
-
*
|
|
5
|
-
* This hook repairs the approved install path when it can do so safely:
|
|
6
|
-
* npm install -g @lifeaitools/rdc-skills@latest
|
|
7
|
-
* rdc-skills-install --profile lifeai --project-root <repo> --write-startup-blocks
|
|
8
|
-
*
|
|
9
|
-
* It then verifies that the local MCP server answers /health and sees a real
|
|
10
|
-
* skills catalog. If repair fails, startup is blocked before agents trust stale
|
|
11
|
-
* copied skill files.
|
|
12
|
-
*/
|
|
13
|
-
'use strict';
|
|
14
|
-
|
|
15
|
-
const fs = require('fs');
|
|
16
|
-
const os = require('os');
|
|
17
|
-
const path = require('path');
|
|
18
|
-
const { execFileSync, execSync } = require('child_process');
|
|
19
|
-
const hookLog = require('./hook-logger');
|
|
20
|
-
|
|
21
|
-
const MIN_SKILLS = 20;
|
|
22
|
-
const MCP_HEALTH = 'http://127.0.0.1:3110/health';
|
|
23
|
-
const PACKAGE = '@lifeaitools/rdc-skills';
|
|
24
|
-
const stampPath = path.join(os.tmpdir(), 'rdc-skills-environment-last-repair.json');
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SessionStart hook — hard gate for the RDC skills runtime.
|
|
4
|
+
*
|
|
5
|
+
* This hook repairs the approved install path when it can do so safely:
|
|
6
|
+
* npm install -g @lifeaitools/rdc-skills@latest
|
|
7
|
+
* rdc-skills-install --profile lifeai --project-root <repo> --write-startup-blocks
|
|
8
|
+
*
|
|
9
|
+
* It then verifies that the local MCP server answers /health and sees a real
|
|
10
|
+
* skills catalog. If repair fails, startup is blocked before agents trust stale
|
|
11
|
+
* copied skill files.
|
|
12
|
+
*/
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const os = require('os');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const { execFileSync, execSync } = require('child_process');
|
|
19
|
+
const hookLog = require('./hook-logger');
|
|
20
|
+
|
|
21
|
+
const MIN_SKILLS = 20;
|
|
22
|
+
const MCP_HEALTH = 'http://127.0.0.1:3110/health';
|
|
23
|
+
const PACKAGE = '@lifeaitools/rdc-skills';
|
|
24
|
+
const stampPath = path.join(os.tmpdir(), 'rdc-skills-environment-last-repair.json');
|
|
25
|
+
|
|
26
|
+
// Values interpolated into a shell command are VALIDATED, not quoted. cmd.exe
|
|
27
|
+
// expands %VAR% even inside double quotes, so no quoting function can make an
|
|
28
|
+
// arbitrary string safe there. A name/path that fails these is skipped, not escaped.
|
|
29
|
+
const SAFE_PM2_NAME = /^[A-Za-z0-9._@\-]+$/;
|
|
30
|
+
const SAFE_PATH = /^[A-Za-z0-9 :._\\/\-]+$/;
|
|
31
|
+
|
|
32
|
+
function q(value) {
|
|
33
|
+
return `"${String(value).replace(/"/g, '\\"')}"`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function run(command, args, opts = {}) {
|
|
37
|
+
return execFileSync(command, args, {
|
|
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 shell(command, opts = {}) {
|
|
47
|
+
return execSync(command, {
|
|
48
|
+
encoding: 'utf8',
|
|
49
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
50
|
+
timeout: opts.timeout || 30000,
|
|
51
|
+
cwd: opts.cwd || process.cwd(),
|
|
52
|
+
env: { ...process.env, ...(opts.env || {}) },
|
|
53
|
+
}).trim();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function commandExists(name) {
|
|
57
|
+
try {
|
|
58
|
+
if (process.platform === 'win32') run('where.exe', [name], { timeout: 5000 });
|
|
59
|
+
else run('which', [name], { timeout: 5000 });
|
|
60
|
+
return true;
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function projectRoot() {
|
|
67
|
+
try {
|
|
68
|
+
return shell('git rev-parse --show-toplevel', { timeout: 5000 });
|
|
69
|
+
} catch {
|
|
70
|
+
return process.cwd();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function globalPackageJson() {
|
|
75
|
+
try {
|
|
76
|
+
const root = shell('npm root -g', { timeout: 10000 });
|
|
77
|
+
const pkg = path.join(root, '@lifeaitools', 'rdc-skills', 'package.json');
|
|
78
|
+
if (!fs.existsSync(pkg)) return null;
|
|
79
|
+
return JSON.parse(fs.readFileSync(pkg, 'utf8'));
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function health() {
|
|
86
|
+
try {
|
|
87
|
+
const res = await fetch(MCP_HEALTH, { signal: AbortSignal.timeout(4000) });
|
|
88
|
+
if (!res.ok) return null;
|
|
89
|
+
return await res.json();
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function recentlyRepaired() {
|
|
96
|
+
try {
|
|
97
|
+
const data = JSON.parse(fs.readFileSync(stampPath, 'utf8'));
|
|
98
|
+
return Date.now() - Date.parse(data.ts) < 10 * 60 * 1000;
|
|
99
|
+
} catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function markRepaired(reason) {
|
|
105
|
+
try {
|
|
106
|
+
fs.writeFileSync(stampPath, JSON.stringify({ ts: new Date().toISOString(), reason }, null, 2));
|
|
107
|
+
} catch {
|
|
108
|
+
/* best effort */
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* PM2 processes running FROM the global package directory.
|
|
114
|
+
*
|
|
115
|
+
* `npm install -g` upgrades by RENAMING that directory. On Windows a directory
|
|
116
|
+
* that is a live process's cwd cannot be renamed, so the install dies EBUSY —
|
|
117
|
+
* which is exactly what happened here: `rdc-skills-mcp` runs with
|
|
118
|
+
* pm_cwd = <npm root>/@lifeaitools/rdc-skills, the very path npm moves.
|
|
119
|
+
*
|
|
120
|
+
* Matched by CWD rather than by name so a renamed or duplicated process is still
|
|
121
|
+
* found — the lock is held by whatever sits in that directory, not by a name.
|
|
122
|
+
*/
|
|
123
|
+
function processesHoldingPackage() {
|
|
124
|
+
if (!commandExists('pm2')) return [];
|
|
125
|
+
try {
|
|
126
|
+
const root = shell('npm root -g', { timeout: 10000 });
|
|
127
|
+
const pkgDir = path.join(root, '@lifeaitools', 'rdc-skills').toLowerCase().replace(/\\/g, '/');
|
|
128
|
+
return JSON.parse(shell('pm2 jlist', { timeout: 15000 }))
|
|
129
|
+
.filter((p) => {
|
|
130
|
+
const cwd = String(p?.pm2_env?.pm_cwd || '').toLowerCase().replace(/\\/g, '/');
|
|
131
|
+
return cwd && (cwd === pkgDir || cwd.startsWith(`${pkgDir}/`));
|
|
132
|
+
})
|
|
133
|
+
.map((p) => p.name)
|
|
134
|
+
.filter(Boolean);
|
|
135
|
+
} catch {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const stoppedStampPath = path.join(os.tmpdir(), 'rdc-skills-stopped-holders.json');
|
|
141
|
+
|
|
142
|
+
function writeStoppedStamp(names) {
|
|
143
|
+
try {
|
|
144
|
+
fs.writeFileSync(stoppedStampPath, JSON.stringify({ names, ts: new Date().toISOString() }), 'utf8');
|
|
145
|
+
} catch { /* best effort — the finally-restart still covers the normal path */ }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function clearStoppedStamp() {
|
|
149
|
+
try { fs.unlinkSync(stoppedStampPath); } catch { /* nothing to clear */ }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Restart anything a PREVIOUS run stopped but never restarted.
|
|
154
|
+
*
|
|
155
|
+
* The only way a holder stays stopped is if that run died between `pm2 stop` and
|
|
156
|
+
* `pm2 restart` — a harness timeout kill, which neither `finally` nor
|
|
157
|
+
* process.on('exit') catches. Leaving the MCP down is strictly worse than the
|
|
158
|
+
* unhealthy state being repaired, so every run repairs the previous run's crash
|
|
159
|
+
* before doing anything else.
|
|
160
|
+
*/
|
|
161
|
+
function restartStrandedHolders() {
|
|
162
|
+
let names = [];
|
|
163
|
+
try {
|
|
164
|
+
names = JSON.parse(fs.readFileSync(stoppedStampPath, 'utf8')).names || [];
|
|
165
|
+
} catch {
|
|
166
|
+
return; // no stamp — nothing was left stopped
|
|
167
|
+
}
|
|
168
|
+
for (const name of names) {
|
|
169
|
+
if (!SAFE_PM2_NAME.test(name)) continue;
|
|
170
|
+
try {
|
|
171
|
+
shell(`pm2 restart ${q(name)}`, { timeout: 30000 });
|
|
172
|
+
hookLog('check-rdc-environment', 'SessionStart', 'restarted-stranded-holder', { name });
|
|
173
|
+
} catch { /* reported by the health check below */ }
|
|
174
|
+
}
|
|
175
|
+
clearStoppedStamp();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function repair(reason) {
|
|
179
|
+
hookLog('check-rdc-environment', 'SessionStart', 'repair', { reason });
|
|
180
|
+
|
|
181
|
+
// Release the directory lock BEFORE npm touches it. Without this the repair
|
|
182
|
+
// cannot succeed while the MCP is running — it fails EBUSY, block() fires, and
|
|
183
|
+
// the session is hard-blocked by its own repair attempt.
|
|
184
|
+
// These MUST go through a shell: npm, pm2 and rdc-skills-install are .cmd shims
|
|
185
|
+
// on Windows, and execFileSync cannot launch a .cmd without one — measured, not
|
|
186
|
+
// assumed: execFileSync('npm', ['--version']) fails ENOENT here
|
|
187
|
+
// (scripts/probe-execfile-cmd.mjs). So the exposure is closed by VALIDATING the
|
|
188
|
+
// interpolated values instead of trying to quote them: q() escapes only double
|
|
189
|
+
// quotes, and cmd.exe still expands %VAR% inside them, which no quoting fixes.
|
|
190
|
+
const holders = processesHoldingPackage().filter((name) => {
|
|
191
|
+
if (SAFE_PM2_NAME.test(name)) return true;
|
|
192
|
+
hookLog('check-rdc-environment', 'SessionStart', 'skipped-unsafe-pm2-name', { name });
|
|
193
|
+
return false;
|
|
194
|
+
});
|
|
195
|
+
// Record what we are about to stop BEFORE stopping it. `finally` covers an
|
|
196
|
+
// exception but NOT the harness killing this hook at its timeout — and
|
|
197
|
+
// process.on('exit') does not fire for a SIGKILL either. Without this stamp a
|
|
198
|
+
// timeout kill between `pm2 stop` and `pm2 restart` leaves the MCP STOPPED with
|
|
199
|
+
// nothing that knows to bring it back. restartStrandedHolders() (called at hook
|
|
200
|
+
// entry) is the crash-safe half of this pair.
|
|
201
|
+
writeStoppedStamp(holders);
|
|
202
|
+
for (const name of holders) {
|
|
203
|
+
try {
|
|
204
|
+
shell(`pm2 stop ${q(name)}`, { timeout: 30000 });
|
|
205
|
+
hookLog('check-rdc-environment', 'SessionStart', 'stopped-for-repair', { name });
|
|
206
|
+
} catch {
|
|
207
|
+
/* already stopped, or pm2 unavailable — the install will report the truth */
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
shell(`npm install -g ${q(`${PACKAGE}@latest`)}`, { timeout: 120000 });
|
|
213
|
+
const root = projectRoot();
|
|
214
|
+
if (!SAFE_PATH.test(root)) {
|
|
215
|
+
throw new Error(`refusing to shell out with an unsafe project root: ${root}`);
|
|
216
|
+
}
|
|
217
|
+
shell(`rdc-skills-install --profile lifeai --project-root ${q(root)} --write-startup-blocks`, { timeout: 180000 });
|
|
218
|
+
markRepaired(reason);
|
|
219
|
+
} finally {
|
|
220
|
+
// ALWAYS restart, even when the install threw. Leaving the MCP stopped would
|
|
221
|
+
// turn a failed repair into a worse outage than the one being repaired.
|
|
222
|
+
for (const name of holders) {
|
|
223
|
+
try {
|
|
224
|
+
shell(`pm2 restart ${q(name)}`, { timeout: 30000 });
|
|
225
|
+
} catch {
|
|
226
|
+
/* surfaced by the health re-check below */
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// Restarted (or tried to) — the stamp has served its purpose. Clearing it here
|
|
230
|
+
// means a stamp that SURVIVES is unambiguous evidence of a killed run.
|
|
231
|
+
clearStoppedStamp();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* ── The global package is BOX-WIDE; this hook is PER-SESSION ──────────────────
|
|
237
|
+
*
|
|
238
|
+
* Every session and every worktree runs this hook, but there is only ONE global
|
|
239
|
+
* npm package and ONE rdc-skills-mcp on the machine. Without coordination, N
|
|
240
|
+
* sessions starting together each independently conclude "unhealthy" and each run
|
|
241
|
+
* `npm install -g` against the same directory — they fight, and on Windows they
|
|
242
|
+
* fight over a directory a live process is sitting in.
|
|
243
|
+
*
|
|
244
|
+
* recentlyRepaired() did not prevent this: it gated the BLOCK path, not the REPAIR
|
|
245
|
+
* path, so concurrent starts all saw "not recently repaired" and all installed.
|
|
246
|
+
*
|
|
247
|
+
* A box-wide resource gets a box-wide update: exactly one session performs it, the
|
|
248
|
+
* rest wait for it and re-probe. Waiting is the correct behaviour for a follower —
|
|
249
|
+
* the leader is already fixing the thing they would have fixed.
|
|
250
|
+
*/
|
|
251
|
+
// Single home: hooks/lib/box-lock.js. Kept out of this file so the verification
|
|
252
|
+
// probe can exercise the REAL implementation instead of a copy that drifts.
|
|
253
|
+
const { acquireBoxLock, releaseBoxLock, LOCK_PATH: lockPath } = require('./lib/box-lock');
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Wait for the session that owns the update to finish, re-probing health.
|
|
259
|
+
*
|
|
260
|
+
* MUST stay under the SessionStart hook timeout (see HOOK_TIMEOUT_SEC in
|
|
261
|
+
* scripts/install-rdc-skills.js). At 90s against a 60s harness default the
|
|
262
|
+
* follower was killed BEFORE it could re-probe or emit a verdict, so a
|
|
263
|
+
* slow-but-succeeding repair looked like a hook crash to every follower.
|
|
264
|
+
*/
|
|
265
|
+
async function waitForBoxRepair(timeoutMs = 45000) {
|
|
266
|
+
const deadline = Date.now() + timeoutMs;
|
|
267
|
+
while (Date.now() < deadline) {
|
|
268
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
269
|
+
const h = await health();
|
|
270
|
+
if (h && h.status === 'ok' && Number(h.skills || 0) >= MIN_SKILLS) return h;
|
|
271
|
+
if (!fs.existsSync(lockPath)) break; // leader finished; take one last look
|
|
272
|
+
}
|
|
273
|
+
return health();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function block(message, details = {}) {
|
|
277
|
+
hookLog('check-rdc-environment', 'SessionStart', 'block', { message, ...details });
|
|
278
|
+
process.stdout.write(JSON.stringify({
|
|
279
|
+
systemMessage:
|
|
280
|
+
`HARD BLOCK — RDC skills environment is not healthy.\n\n` +
|
|
281
|
+
`${message}\n\n` +
|
|
282
|
+
`Do not proceed with RDC work until the approved install path is repaired.\n\n` +
|
|
283
|
+
`STOP THE SERVER FIRST — npm upgrades by renaming the global package dir, and\n` +
|
|
284
|
+
`Windows cannot rename a running process's cwd. Skipping this step is what\n` +
|
|
285
|
+
`produces "EBUSY ... rename ... @lifeaitools/rdc-skills":\n\n` +
|
|
286
|
+
`pm2 stop rdc-skills-mcp\n` +
|
|
287
|
+
`npm install -g @lifeaitools/rdc-skills@latest\n` +
|
|
288
|
+
`rdc-skills-install --profile lifeai --project-root ${projectRoot()} --write-startup-blocks\n` +
|
|
289
|
+
`pm2 restart rdc-skills-mcp`
|
|
290
|
+
}));
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function main() {
|
|
295
|
+
// Repair a PREVIOUS run's crash first: if it was killed between `pm2 stop` and
|
|
296
|
+
// `pm2 restart`, the MCP is still down and no health check can fix that.
|
|
297
|
+
restartStrandedHolders();
|
|
298
|
+
|
|
299
|
+
const initialPkg = globalPackageJson();
|
|
300
|
+
const initialHealth = await health();
|
|
301
|
+
const reasons = [];
|
|
302
|
+
|
|
303
|
+
if (!initialPkg) reasons.push('global package missing');
|
|
304
|
+
if (!commandExists('rdc-skills-install')) reasons.push('installer command missing');
|
|
305
|
+
if (!initialHealth || initialHealth.status !== 'ok' || Number(initialHealth.skills || 0) < MIN_SKILLS) {
|
|
306
|
+
reasons.push('local MCP health/catalog invalid');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (reasons.length) {
|
|
310
|
+
if (recentlyRepaired()) {
|
|
311
|
+
block(`RDC skills still unhealthy after a recent repair attempt: ${reasons.join(', ')}`);
|
|
312
|
+
}
|
|
313
|
+
if (acquireBoxLock()) {
|
|
314
|
+
// This session is the box's updater. block() calls process.exit(1), which
|
|
315
|
+
// SKIPS finally — so the error is captured here and reported only AFTER the
|
|
316
|
+
// lock has been released. Calling block() from inside the try/catch leaked
|
|
317
|
+
// the lock on exactly the path most likely to run.
|
|
318
|
+
let repairError = null;
|
|
319
|
+
try {
|
|
320
|
+
repair(reasons.join(', '));
|
|
321
|
+
} catch (err) {
|
|
322
|
+
repairError = err;
|
|
323
|
+
} finally {
|
|
324
|
+
releaseBoxLock();
|
|
325
|
+
}
|
|
326
|
+
if (repairError) {
|
|
327
|
+
block(`Automatic RDC skills repair failed: ${repairError.message}`, { reasons });
|
|
328
|
+
}
|
|
329
|
+
} else {
|
|
330
|
+
// Another session owns the box-wide update. Racing it is what broke this.
|
|
331
|
+
hookLog('check-rdc-environment', 'SessionStart', 'await-box-repair', { reasons });
|
|
332
|
+
await waitForBoxRepair();
|
|
333
|
+
// Re-evaluate from scratch rather than trusting the pre-wait `reasons`. A
|
|
334
|
+
// follower's only complaint is often a transient blip caused by the LEADER's
|
|
335
|
+
// own `pm2 restart`; blocking on that stale list hard-blocked sessions the
|
|
336
|
+
// leader had already fixed.
|
|
337
|
+
const afterReasons = [];
|
|
338
|
+
if (!globalPackageJson()) afterReasons.push('global package missing');
|
|
339
|
+
if (!commandExists('rdc-skills-install')) afterReasons.push('installer command missing');
|
|
340
|
+
const h = await health();
|
|
341
|
+
if (!h || h.status !== 'ok' || Number(h.skills || 0) < MIN_SKILLS) {
|
|
342
|
+
afterReasons.push('local MCP health/catalog invalid');
|
|
343
|
+
}
|
|
344
|
+
if (afterReasons.length) {
|
|
345
|
+
block(
|
|
346
|
+
'Another session is updating the box-wide rdc-skills install and it did not '
|
|
347
|
+
+ `become healthy in time: ${afterReasons.join(', ')}`,
|
|
348
|
+
{ reasons, afterReasons, waited: true },
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const finalPkg = globalPackageJson();
|
|
355
|
+
const finalHealth = await health();
|
|
356
|
+
if (!finalPkg) block('Global @lifeaitools/rdc-skills package is missing after repair.');
|
|
357
|
+
if (!commandExists('rdc-skills-install')) block('rdc-skills-install is missing after repair.');
|
|
358
|
+
if (!finalHealth || finalHealth.status !== 'ok' || Number(finalHealth.skills || 0) < MIN_SKILLS) {
|
|
359
|
+
block(`rdc-skills MCP is unhealthy after repair. Health: ${JSON.stringify(finalHealth)}`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
hookLog('check-rdc-environment', 'SessionStart', 'pass', {
|
|
363
|
+
version: finalPkg.version,
|
|
364
|
+
mcpVersion: finalHealth.version,
|
|
365
|
+
skills: finalHealth.skills,
|
|
366
|
+
});
|
|
367
|
+
process.exit(0);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Run ONLY when git/Claude invokes this file directly. Without the guard, merely
|
|
371
|
+
// require()-ing it executes the whole hook — which is what the installer's own
|
|
372
|
+
// load-check did, triggering a REAL `npm install -g` + pm2 stop/restart during the
|
|
373
|
+
// install, and on an unhealthy box a recursive install→repair→install loop.
|
|
374
|
+
if (require.main === module) {
|
|
375
|
+
main().catch((err) => block(`RDC skills environment check crashed: ${err.message}`));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
module.exports = { main, repair, restartStrandedHolders };
|