gm-skill 2.0.2023 → 2.0.2025
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/README.md +1 -1
- package/bin/plugkit-slim.wasm.sha256 +1 -1
- package/bin/plugkit.version +1 -1
- package/bin/plugkit.wasm.sha256 +1 -1
- package/gm-plugkit/bootstrap.js +6 -54
- package/gm-plugkit/package.json +1 -2
- package/gm-plugkit/plugkit-slim.wasm.sha256 +1 -1
- package/gm-plugkit/plugkit.version +1 -1
- package/gm.json +2 -2
- package/package.json +1 -2
- package/gm-plugkit/supervisor.js +0 -402
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ gm/
|
|
|
60
60
|
|-- skills/gm/ <- the skill (SKILL.md), installed as /gm
|
|
61
61
|
|-- bin/ <- bootstrap + installer + plugkit wasm pins (gmsniff / ccsniff are separate npm packages, `bun x gmsniff`, `bun x ccsniff`)
|
|
62
62
|
|-- scripts/ <- publish-time helper scripts
|
|
63
|
-
|-- gm-plugkit/ <- separate npm package that
|
|
63
|
+
|-- gm-plugkit/ <- separate npm package: thin launcher + bootstrap that delegates to the native agentplug-runner, with the JS wasm-wrapper kept only as a fallback host
|
|
64
64
|
|-- gm.json <- version + plugkit pin
|
|
65
65
|
|-- package.json <- npm publish manifest
|
|
66
66
|
|-- AGENTS.md <- architectural rules (present-tense, no history)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
fc7364d869507c548db7846b67e71084c8d9c0c5a18c39a09afaf3f28155d4cd plugkit-slim.wasm
|
package/bin/plugkit.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.1.
|
|
1
|
+
0.1.936
|
package/bin/plugkit.wasm.sha256
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
6492759f1cf535d6b899f22d1a31c00296397ae493aa755a6c735940128d9b18 plugkit.wasm
|
package/gm-plugkit/bootstrap.js
CHANGED
|
@@ -1263,37 +1263,6 @@ function getBinaryPath() {
|
|
|
1263
1263
|
return getWasmPath();
|
|
1264
1264
|
}
|
|
1265
1265
|
|
|
1266
|
-
function probeUnsupervisedWatcher(spoolDir) {
|
|
1267
|
-
try {
|
|
1268
|
-
const statusPath = path.join(spoolDir, '.status.json');
|
|
1269
|
-
const supervisorPath = path.join(spoolDir, '.supervisor.json');
|
|
1270
|
-
const markerPath = path.join(spoolDir, '.pre-supervised-watcher.json');
|
|
1271
|
-
if (!fs.existsSync(statusPath)) {
|
|
1272
|
-
try { fs.unlinkSync(markerPath); } catch (_) {}
|
|
1273
|
-
return;
|
|
1274
|
-
}
|
|
1275
|
-
const status = JSON.parse(fs.readFileSync(statusPath, 'utf-8'));
|
|
1276
|
-
const age = Date.now() - (status && status.ts || 0);
|
|
1277
|
-
if (age > 30_000) {
|
|
1278
|
-
try { fs.unlinkSync(markerPath); } catch (_) {}
|
|
1279
|
-
return;
|
|
1280
|
-
}
|
|
1281
|
-
if (fs.existsSync(supervisorPath)) {
|
|
1282
|
-
try { fs.unlinkSync(markerPath); } catch (_) {}
|
|
1283
|
-
return;
|
|
1284
|
-
}
|
|
1285
|
-
const marker = {
|
|
1286
|
-
ts: Date.now(),
|
|
1287
|
-
reason: 'running-watcher-has-no-supervisor',
|
|
1288
|
-
watcher_pid: status.pid,
|
|
1289
|
-
watcher_version: status.version,
|
|
1290
|
-
severity: 'warn',
|
|
1291
|
-
instruction: 'A running watcher was started under an older bootstrap that did not spawn a supervisor. Unplanned-restart recovery and idle-teardown coordination are dormant. To migrate, stop the current watcher (taskkill /F /T /PID <watcher_pid> on Windows or kill <watcher_pid> on POSIX) and let the next bootstrap re-spawn it under supervisor.js.',
|
|
1292
|
-
};
|
|
1293
|
-
fs.writeFileSync(markerPath, JSON.stringify(marker, null, 2));
|
|
1294
|
-
} catch (_) {}
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
1266
|
function resolveNodeRuntime() {
|
|
1298
1267
|
const candidates = [];
|
|
1299
1268
|
if (process.env.PLUGKIT_RUNTIME) candidates.push(process.env.PLUGKIT_RUNTIME);
|
|
@@ -1337,7 +1306,6 @@ function startSpoolDaemon() {
|
|
|
1337
1306
|
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
1338
1307
|
const spoolDir = path.join(projectDir, '.gm', 'exec-spool');
|
|
1339
1308
|
fs.mkdirSync(spoolDir, { recursive: true });
|
|
1340
|
-
probeUnsupervisedWatcher(spoolDir);
|
|
1341
1309
|
const logPath = path.join(spoolDir, '.watcher.log');
|
|
1342
1310
|
try {
|
|
1343
1311
|
const stat = fs.statSync(logPath);
|
|
@@ -1347,36 +1315,20 @@ function startSpoolDaemon() {
|
|
|
1347
1315
|
}
|
|
1348
1316
|
} catch (_) {}
|
|
1349
1317
|
|
|
1350
|
-
const
|
|
1351
|
-
|
|
1352
|
-
const cmd = runtime;
|
|
1353
|
-
const args = [wrapper, 'spool'];
|
|
1354
|
-
const logFd = fs.openSync(logPath, 'a');
|
|
1355
|
-
try { fs.writeSync(logFd, `\n--- daemon spawn ${new Date().toISOString()} parent=${process.pid} (no supervisor) ---\n`); } catch (_) {}
|
|
1356
|
-
const child = require('child_process').spawn(cmd, args, {
|
|
1357
|
-
detached: true,
|
|
1358
|
-
stdio: ['ignore', logFd, logFd],
|
|
1359
|
-
windowsHide: true,
|
|
1360
|
-
env: { ...process.env, CLAUDE_PROJECT_DIR: projectDir, PLUGKIT_BOOT_REASON: 'direct-no-supervisor' },
|
|
1361
|
-
});
|
|
1362
|
-
try { fs.closeSync(logFd); } catch (_) {}
|
|
1363
|
-
const pid = child.pid;
|
|
1364
|
-
child.unref();
|
|
1365
|
-
return { ok: true, pid, wrapper, runtime: cmd, logPath, supervised: false };
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1318
|
+
const cmd = runtime;
|
|
1319
|
+
const args = [wrapper, 'spool'];
|
|
1368
1320
|
const logFd = fs.openSync(logPath, 'a');
|
|
1369
|
-
try { fs.writeSync(logFd, `\n---
|
|
1370
|
-
const child = require('child_process').spawn(
|
|
1321
|
+
try { fs.writeSync(logFd, `\n--- daemon spawn ${new Date().toISOString()} parent=${process.pid} (js-host fallback, native runner absent) ---\n`); } catch (_) {}
|
|
1322
|
+
const child = require('child_process').spawn(cmd, args, {
|
|
1371
1323
|
detached: true,
|
|
1372
1324
|
stdio: ['ignore', logFd, logFd],
|
|
1373
1325
|
windowsHide: true,
|
|
1374
|
-
env: { ...process.env, CLAUDE_PROJECT_DIR: projectDir,
|
|
1326
|
+
env: { ...process.env, CLAUDE_PROJECT_DIR: projectDir, PLUGKIT_BOOT_REASON: 'js-host-fallback' },
|
|
1375
1327
|
});
|
|
1376
1328
|
try { fs.closeSync(logFd); } catch (_) {}
|
|
1377
1329
|
const pid = child.pid;
|
|
1378
1330
|
child.unref();
|
|
1379
|
-
return { ok: true, pid, wrapper,
|
|
1331
|
+
return { ok: true, pid, wrapper, runtime: cmd, logPath, supervised: false };
|
|
1380
1332
|
} catch (e) {
|
|
1381
1333
|
return { ok: false, error: e.message };
|
|
1382
1334
|
}
|
package/gm-plugkit/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gm-plugkit",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2025",
|
|
4
4
|
"description": "Bootstrap and daemon-spawn tool for gm plugkit binary. Downloads the correct platform binary, verifies SHA256, and starts the spool watcher daemon. Includes plugkit-wasm-wrapper for WASM-based spool watching.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
"cli.js",
|
|
12
12
|
"index.js",
|
|
13
13
|
"bootstrap.js",
|
|
14
|
-
"supervisor.js",
|
|
15
14
|
"plugkit-wasm-wrapper.js",
|
|
16
15
|
"gm-log.js",
|
|
17
16
|
"gm-process.js",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
fc7364d869507c548db7846b67e71084c8d9c0c5a18c39a09afaf3f28155d4cd plugkit-slim.wasm
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.1.
|
|
1
|
+
0.1.936
|
package/gm.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gm",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2025",
|
|
4
4
|
"description": "Spool-dispatch orchestration engine with unified state machine, skills, and automated git enforcement",
|
|
5
5
|
"author": "AnEntrypoint",
|
|
6
6
|
"license": "MIT",
|
|
@@ -17,5 +17,5 @@
|
|
|
17
17
|
"publishConfig": {
|
|
18
18
|
"access": "public"
|
|
19
19
|
},
|
|
20
|
-
"plugkitVersion": "0.1.
|
|
20
|
+
"plugkitVersion": "0.1.936"
|
|
21
21
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gm-skill",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2025",
|
|
4
4
|
"description": "Canonical universal harness — AI-native software engineering via skill-driven orchestration; bootstraps plugkit for task execution and session isolation. Install in any AI coding agent host.",
|
|
5
5
|
"author": "AnEntrypoint",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,7 +42,6 @@
|
|
|
42
42
|
"gm-plugkit/index.js",
|
|
43
43
|
"gm-plugkit/package.json",
|
|
44
44
|
"gm-plugkit/plugkit-wasm-wrapper.js",
|
|
45
|
-
"gm-plugkit/supervisor.js",
|
|
46
45
|
"gm-plugkit/gm-log.js",
|
|
47
46
|
"gm-plugkit/gm-process.js",
|
|
48
47
|
"gm-plugkit/plugkit.version",
|
package/gm-plugkit/supervisor.js
DELETED
|
@@ -1,402 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const os = require('os');
|
|
7
|
-
const crypto = require('crypto');
|
|
8
|
-
const { spawn, spawnSync } = require('child_process');
|
|
9
|
-
const { gmToolsDir, resolveProjectRoot } = require('./bootstrap');
|
|
10
|
-
const { logEvent: _sharedLogEvent, GM_LOG_ROOT: _sharedGmLogRoot } = require('./gm-log');
|
|
11
|
-
const { pidCommandLineForKillGuard: _sharedPidCommandLine, pidAliveSync, waitForPidDeath } = require('./gm-process');
|
|
12
|
-
|
|
13
|
-
function wrapperSha12OnDisk() {
|
|
14
|
-
try {
|
|
15
|
-
const wp = path.join(gmToolsDir(), 'plugkit-wasm-wrapper.js');
|
|
16
|
-
return crypto.createHash('sha256').update(fs.readFileSync(wp)).digest('hex').slice(0, 12);
|
|
17
|
-
} catch (_) { return null; }
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Root the spool/lock state at the git common dir (see resolveProjectRoot's
|
|
21
|
-
// own doc comment in bootstrap.js for the full worktree-dedup rationale) so
|
|
22
|
-
// a worktree-spawned supervisor shares its lock/spoolDir with the main-repo
|
|
23
|
-
// checkout instead of always cold-booting its own independent watcher.
|
|
24
|
-
const projectDir = resolveProjectRoot(process.env.CLAUDE_PROJECT_DIR || process.cwd());
|
|
25
|
-
const spoolDir = path.join(projectDir, '.gm', 'exec-spool');
|
|
26
|
-
fs.mkdirSync(spoolDir, { recursive: true });
|
|
27
|
-
|
|
28
|
-
const STATUS_PATH = path.join(spoolDir, '.status.json');
|
|
29
|
-
const SHUTDOWN_REASON_PATH = path.join(spoolDir, '.shutdown-reason.json');
|
|
30
|
-
const SUPERVISOR_PATH = path.join(spoolDir, '.supervisor.json');
|
|
31
|
-
const SUPERVISOR_PID_PATH = path.join(spoolDir, '.supervisor.pid');
|
|
32
|
-
const LOG_PATH = path.join(spoolDir, '.watcher.log');
|
|
33
|
-
const GM_LOG_ROOT = _sharedGmLogRoot;
|
|
34
|
-
|
|
35
|
-
const POLL_INTERVAL_MS = 10_000;
|
|
36
|
-
const STATUS_STALE_MS = 30_000;
|
|
37
|
-
const MAX_RESTART_BURST = 5;
|
|
38
|
-
const RESTART_WINDOW_MS = 60_000;
|
|
39
|
-
const BURST_BACKOFF_MS = 60_000;
|
|
40
|
-
const VERSION_DRIFT_COOLDOWN_MS = 60_000;
|
|
41
|
-
|
|
42
|
-
function logEvent(event, fields) {
|
|
43
|
-
_sharedLogEvent('plugkit', event, fields, { sess: process.env.CLAUDE_SESSION_ID || '', role: 'supervisor' });
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function writeSupervisorStatus(state, extra) {
|
|
47
|
-
try {
|
|
48
|
-
fs.writeFileSync(SUPERVISOR_PATH, JSON.stringify({
|
|
49
|
-
pid: process.pid,
|
|
50
|
-
ts: Date.now(),
|
|
51
|
-
state,
|
|
52
|
-
...(extra || {}),
|
|
53
|
-
}));
|
|
54
|
-
} catch (_) {}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function pidCommandLineForKillGuard(pid) {
|
|
58
|
-
return _sharedPidCommandLine(pid);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function pidIsPlugkitProcess(pid) {
|
|
62
|
-
return /plugkit-wasm-wrapper\.js|plugkit-supervisor\.js|gm-plugkit[\\\/]supervisor\.js/i.test(pidCommandLineForKillGuard(pid));
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function writeKillAttribution(targetSpoolDir, info) {
|
|
66
|
-
try {
|
|
67
|
-
fs.mkdirSync(targetSpoolDir, { recursive: true });
|
|
68
|
-
fs.writeFileSync(path.join(targetSpoolDir, '.kill-attribution.json'), JSON.stringify({ killer_pid: process.pid, killer_cwd: process.cwd(), killer_script: __filename, ts: Date.now(), ...info }, null, 2));
|
|
69
|
-
} catch (_) {}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function pidAlive(pid) {
|
|
73
|
-
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
74
|
-
try { process.kill(pid, 0); return true; } catch (_) { return false; }
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function acquireSingleInstance() {
|
|
78
|
-
for (let attempt = 0; attempt < 2; attempt++) {
|
|
79
|
-
try {
|
|
80
|
-
const fd = fs.openSync(SUPERVISOR_PID_PATH, 'wx');
|
|
81
|
-
try { fs.writeSync(fd, String(process.pid)); } finally { fs.closeSync(fd); }
|
|
82
|
-
return true;
|
|
83
|
-
} catch (e) {
|
|
84
|
-
if (e && e.code === 'EEXIST') {
|
|
85
|
-
let other = NaN;
|
|
86
|
-
try { other = parseInt(fs.readFileSync(SUPERVISOR_PID_PATH, 'utf-8').trim(), 10); } catch (_) {}
|
|
87
|
-
if (Number.isFinite(other) && other !== process.pid && pidAlive(other)) {
|
|
88
|
-
const TAKEOVER_STALE_MS = 45_000;
|
|
89
|
-
const now = Date.now();
|
|
90
|
-
let supTs = 0;
|
|
91
|
-
try { supTs = (JSON.parse(fs.readFileSync(SUPERVISOR_PATH, 'utf-8')).ts) || 0; } catch (_) {}
|
|
92
|
-
const spool = readStatus();
|
|
93
|
-
const spoolBusy = spool && spool.busy_until && spool.busy_until > now;
|
|
94
|
-
const spoolTs = (spool && spool.ts) || 0;
|
|
95
|
-
const holderWedged = !spoolBusy
|
|
96
|
-
&& (now - supTs) > TAKEOVER_STALE_MS
|
|
97
|
-
&& (now - spoolTs) > TAKEOVER_STALE_MS;
|
|
98
|
-
if (!holderWedged) {
|
|
99
|
-
logEvent('supervisor.refused-duplicate', { existing_pid: other, severity: 'warn' });
|
|
100
|
-
return false;
|
|
101
|
-
}
|
|
102
|
-
logEvent('supervisor.takeover-wedged', {
|
|
103
|
-
existing_pid: other,
|
|
104
|
-
supervisor_status_age_ms: now - supTs,
|
|
105
|
-
spool_status_age_ms: now - spoolTs,
|
|
106
|
-
severity: 'critical',
|
|
107
|
-
});
|
|
108
|
-
if (pidIsPlugkitProcess(other)) {
|
|
109
|
-
writeKillAttribution(spoolDir, { reason: 'supervisor-takeover-wedged', target_pid: other, via: 'supervisor-duel' });
|
|
110
|
-
try { process.kill(other, 'SIGTERM'); } catch (_) {}
|
|
111
|
-
if (process.platform === 'win32') {
|
|
112
|
-
try { spawnSync('taskkill', ['/F', '/T', '/PID', String(other)], { stdio: 'ignore', windowsHide: true, timeout: 3000 }); } catch (_) {}
|
|
113
|
-
}
|
|
114
|
-
} else {
|
|
115
|
-
logEvent('supervisor.takeover-kill-skipped-pid-reused', { existing_pid: other, severity: 'warn' });
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
try { fs.unlinkSync(SUPERVISOR_PID_PATH); } catch (_) {}
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
logEvent('supervisor.pid-write-failed', { error: e && e.message, severity: 'warn' });
|
|
122
|
-
return true;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return true;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function releaseSingleInstance() {
|
|
129
|
-
try {
|
|
130
|
-
if (fs.existsSync(SUPERVISOR_PID_PATH)) {
|
|
131
|
-
const raw = fs.readFileSync(SUPERVISOR_PID_PATH, 'utf-8').trim();
|
|
132
|
-
if (parseInt(raw, 10) === process.pid) fs.unlinkSync(SUPERVISOR_PID_PATH);
|
|
133
|
-
}
|
|
134
|
-
} catch (_) {}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function readStatus() {
|
|
138
|
-
try { return JSON.parse(fs.readFileSync(STATUS_PATH, 'utf-8')); } catch (_) { return null; }
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function readShutdownReason() {
|
|
142
|
-
try { return JSON.parse(fs.readFileSync(SHUTDOWN_REASON_PATH, 'utf-8')); } catch (_) { return null; }
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// A supervisor-initiated kill is PLANNED (version/wrapper drift, heartbeat-stale),
|
|
146
|
-
// but on Windows process.kill(pid,'SIGTERM') terminates the child immediately with
|
|
147
|
-
// no catchable signal, so the child never writes its own .shutdown-reason.json and
|
|
148
|
-
// the taskkill /F that follows leaves none either -- the NEXT boot then mislabels a
|
|
149
|
-
// deliberate upgrade recycle as a SILENT ABORT / unplanned-restart critical. Write
|
|
150
|
-
// the planned reason on the child's behalf BEFORE killing, so the next boot reads a
|
|
151
|
-
// fresh planned shutdown reason and classifies the restart correctly.
|
|
152
|
-
function killChild(reason) {
|
|
153
|
-
if (!currentChildPid) return;
|
|
154
|
-
const pid = currentChildPid;
|
|
155
|
-
try { fs.writeFileSync(SHUTDOWN_REASON_PATH, JSON.stringify({ reason, ts: Date.now(), pid, killed_by_supervisor: true })); } catch (_) {}
|
|
156
|
-
try { process.kill(pid, 'SIGTERM'); } catch (_) {}
|
|
157
|
-
if (process.platform === 'win32') {
|
|
158
|
-
try { spawnSync('taskkill', ['/F', '/T', '/PID', String(pid)], { stdio: 'ignore', windowsHide: true, timeout: 3000 }); } catch (_) {}
|
|
159
|
-
}
|
|
160
|
-
waitForPidDeath(pid, 5000);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
let lastSpawnedAt = 0;
|
|
164
|
-
let lastVersionDriftActionAt = 0;
|
|
165
|
-
let restartTimestamps = [];
|
|
166
|
-
let currentChildPid = null;
|
|
167
|
-
let currentBootReason = 'initial';
|
|
168
|
-
|
|
169
|
-
function spawnWatcher(bootReason) {
|
|
170
|
-
lastSpawnedAt = Date.now();
|
|
171
|
-
restartTimestamps.push(Date.now());
|
|
172
|
-
restartTimestamps = restartTimestamps.filter(t => Date.now() - t < RESTART_WINDOW_MS);
|
|
173
|
-
if (restartTimestamps.length > MAX_RESTART_BURST) {
|
|
174
|
-
logEvent('supervisor.restart-burst-backoff', {
|
|
175
|
-
reason: 'restart-burst-exceeded',
|
|
176
|
-
restarts_in_window: restartTimestamps.length,
|
|
177
|
-
window_ms: RESTART_WINDOW_MS,
|
|
178
|
-
max: MAX_RESTART_BURST,
|
|
179
|
-
backoff_ms: BURST_BACKOFF_MS,
|
|
180
|
-
severity: 'warn',
|
|
181
|
-
});
|
|
182
|
-
writeSupervisorStatus('backoff', { reason: 'restart-burst-exceeded', backoff_ms: BURST_BACKOFF_MS });
|
|
183
|
-
restartTimestamps = [];
|
|
184
|
-
setTimeout(() => spawnWatcher('post-burst-backoff'), BURST_BACKOFF_MS);
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const wrapper = path.join(gmToolsDir(), 'plugkit-wasm-wrapper.js');
|
|
189
|
-
if (!fs.existsSync(wrapper)) {
|
|
190
|
-
logEvent('supervisor.wrapper-missing', { wrapper, severity: 'critical' });
|
|
191
|
-
writeSupervisorStatus('error', { error: 'wrapper-missing' });
|
|
192
|
-
process.exit(3);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const isNodeExe = (p) => /(^|[\\/])node(\.exe)?$/i.test(String(p || ''));
|
|
196
|
-
const resolveRuntime = () => {
|
|
197
|
-
const candidates = [];
|
|
198
|
-
if (process.env.PLUGKIT_RUNTIME) candidates.push(process.env.PLUGKIT_RUNTIME);
|
|
199
|
-
candidates.push('bun');
|
|
200
|
-
try {
|
|
201
|
-
const which = process.platform === 'win32' ? 'where' : 'which';
|
|
202
|
-
const out = spawnSync(which, ['bun'], { encoding: 'utf8', windowsHide: true });
|
|
203
|
-
if (out && out.stdout) {
|
|
204
|
-
const first = out.stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean)[0];
|
|
205
|
-
if (first) candidates.push(first);
|
|
206
|
-
}
|
|
207
|
-
} catch (_) {}
|
|
208
|
-
for (const c of candidates) {
|
|
209
|
-
try { const r = spawnSync(c, ['--version'], { stdio: 'ignore', windowsHide: true }); if (r && r.status === 0) return c; } catch (_) {}
|
|
210
|
-
}
|
|
211
|
-
const nodeCandidates = [];
|
|
212
|
-
if (isNodeExe(process.execPath)) nodeCandidates.push(process.execPath);
|
|
213
|
-
if (process.env.GM_NODE_PATH) nodeCandidates.push(process.env.GM_NODE_PATH);
|
|
214
|
-
try {
|
|
215
|
-
const which = process.platform === 'win32' ? 'where' : 'which';
|
|
216
|
-
const out = spawnSync(which, ['node'], { encoding: 'utf8', windowsHide: true });
|
|
217
|
-
if (out && out.stdout) {
|
|
218
|
-
const first = out.stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean)[0];
|
|
219
|
-
if (first) nodeCandidates.push(first);
|
|
220
|
-
}
|
|
221
|
-
} catch (_) {}
|
|
222
|
-
for (const c of nodeCandidates) {
|
|
223
|
-
try { const r = spawnSync(c, ['--version'], { stdio: 'ignore', windowsHide: true }); if (r && r.status === 0) return c; } catch (_) {}
|
|
224
|
-
}
|
|
225
|
-
return process.execPath;
|
|
226
|
-
};
|
|
227
|
-
let cmd = resolveRuntime();
|
|
228
|
-
let args = [wrapper, 'spool'];
|
|
229
|
-
|
|
230
|
-
let logFd = null;
|
|
231
|
-
try { logFd = fs.openSync(LOG_PATH, 'a'); } catch (_) {}
|
|
232
|
-
try {
|
|
233
|
-
if (logFd !== null) fs.writeSync(logFd, `\n--- watcher spawn ${new Date().toISOString()} supervisor=${process.pid} reason=${bootReason} ---\n`);
|
|
234
|
-
} catch (_) {}
|
|
235
|
-
|
|
236
|
-
const child = spawn(cmd, args, {
|
|
237
|
-
detached: false,
|
|
238
|
-
stdio: ['ignore', logFd || 'ignore', logFd || 'ignore'],
|
|
239
|
-
windowsHide: true,
|
|
240
|
-
env: {
|
|
241
|
-
...process.env,
|
|
242
|
-
CLAUDE_PROJECT_DIR: projectDir,
|
|
243
|
-
PLUGKIT_BOOT_REASON: bootReason,
|
|
244
|
-
PLUGKIT_SUPERVISOR_PID: String(process.pid),
|
|
245
|
-
},
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
try { if (logFd !== null) fs.closeSync(logFd); } catch (_) {}
|
|
249
|
-
currentChildPid = child.pid;
|
|
250
|
-
currentBootReason = bootReason;
|
|
251
|
-
writeSupervisorStatus('watching', { watcher_pid: child.pid, boot_reason: bootReason });
|
|
252
|
-
logEvent('supervisor.spawned-watcher', { watcher_pid: child.pid, boot_reason: bootReason, runtime: cmd });
|
|
253
|
-
|
|
254
|
-
child.on('exit', (code, signal) => {
|
|
255
|
-
const shutdownReason = readShutdownReason();
|
|
256
|
-
const reason = shutdownReason && shutdownReason.reason;
|
|
257
|
-
const idleClean = reason === 'idle';
|
|
258
|
-
const lockRejected = code === 75;
|
|
259
|
-
const cleanExit = code === 0;
|
|
260
|
-
const plannedReasons = new Set(['idle', 'sigterm', 'version-change', 'wrapper-change', 'heartbeat-stale', 'peer-stale-takeover', 'external-planned', 'process-exit']);
|
|
261
|
-
const isPlanned = plannedReasons.has(reason) || lockRejected || cleanExit;
|
|
262
|
-
const eventName = idleClean
|
|
263
|
-
? 'supervisor.watcher-exited-idle'
|
|
264
|
-
: reason === 'version-change'
|
|
265
|
-
? 'supervisor.watcher-exited-for-update'
|
|
266
|
-
: lockRejected
|
|
267
|
-
? 'supervisor.watcher-exited-lock-rejected'
|
|
268
|
-
: cleanExit
|
|
269
|
-
? 'supervisor.watcher-exited-clean'
|
|
270
|
-
: 'supervisor.watcher-exited-unexpectedly';
|
|
271
|
-
logEvent(eventName, {
|
|
272
|
-
watcher_pid: currentChildPid,
|
|
273
|
-
exit_code: code,
|
|
274
|
-
signal,
|
|
275
|
-
shutdown_reason: reason || null,
|
|
276
|
-
had_shutdown_reason_file: shutdownReason !== null,
|
|
277
|
-
severity: isPlanned ? 'info' : 'critical',
|
|
278
|
-
uptime_ms: Date.now() - lastSpawnedAt,
|
|
279
|
-
...(shutdownReason || {}),
|
|
280
|
-
});
|
|
281
|
-
if (idleClean) {
|
|
282
|
-
writeSupervisorStatus('exited-idle', { watcher_pid: currentChildPid });
|
|
283
|
-
try { fs.unlinkSync(SUPERVISOR_PATH); } catch (_) {}
|
|
284
|
-
process.exit(0);
|
|
285
|
-
}
|
|
286
|
-
if (lockRejected) {
|
|
287
|
-
writeSupervisorStatus('exited-lock-rejected', { watcher_pid: currentChildPid });
|
|
288
|
-
try { fs.unlinkSync(SUPERVISOR_PATH); } catch (_) {}
|
|
289
|
-
process.exit(0);
|
|
290
|
-
}
|
|
291
|
-
const respawnReason = reason === 'version-change'
|
|
292
|
-
? 'planned-restart-version-change'
|
|
293
|
-
: isPlanned
|
|
294
|
-
? `planned-restart-after-${reason || (cleanExit ? 'clean-exit' : 'exit')}`
|
|
295
|
-
: 'unplanned-restart-after-exit';
|
|
296
|
-
writeSupervisorStatus('restarting', {
|
|
297
|
-
prior_watcher_pid: currentChildPid,
|
|
298
|
-
prior_exit_code: code,
|
|
299
|
-
prior_signal: signal,
|
|
300
|
-
prior_shutdown_reason: reason || null,
|
|
301
|
-
respawn_reason: respawnReason,
|
|
302
|
-
});
|
|
303
|
-
setTimeout(() => spawnWatcher(respawnReason), 1500);
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
child.on('error', (err) => {
|
|
307
|
-
logEvent('supervisor.spawn-error', { error: err.message, severity: 'critical' });
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function checkWatcherHealth() {
|
|
312
|
-
if (!currentChildPid) return;
|
|
313
|
-
if (!pidAlive(currentChildPid)) {
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
const status = readStatus();
|
|
317
|
-
if (!status) {
|
|
318
|
-
logEvent('supervisor.status-missing', {
|
|
319
|
-
watcher_pid: currentChildPid,
|
|
320
|
-
severity: 'warn',
|
|
321
|
-
});
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
const now = Date.now();
|
|
325
|
-
if (status.busy_until && status.busy_until > now) {
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
const age = now - (status.ts || 0);
|
|
329
|
-
if (age > STATUS_STALE_MS) {
|
|
330
|
-
logEvent('supervisor.heartbeat-stale', {
|
|
331
|
-
watcher_pid: currentChildPid,
|
|
332
|
-
status_pid: status.pid,
|
|
333
|
-
status_age_ms: age,
|
|
334
|
-
stale_limit_ms: STATUS_STALE_MS,
|
|
335
|
-
severity: 'critical',
|
|
336
|
-
});
|
|
337
|
-
killChild('heartbeat-stale');
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
const reported = status.wrapper_sha || null;
|
|
341
|
-
const onDisk = wrapperSha12OnDisk();
|
|
342
|
-
if (reported && onDisk && reported !== onDisk) {
|
|
343
|
-
logEvent('supervisor.wrapper-sha-drift', {
|
|
344
|
-
watcher_pid: currentChildPid,
|
|
345
|
-
reported_sha: reported,
|
|
346
|
-
on_disk_sha: onDisk,
|
|
347
|
-
severity: 'info',
|
|
348
|
-
});
|
|
349
|
-
killChild('wrapper-change');
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
if (status.version_drifted === true) {
|
|
353
|
-
if (now - lastVersionDriftActionAt < VERSION_DRIFT_COOLDOWN_MS) {
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
lastVersionDriftActionAt = now;
|
|
357
|
-
logEvent('supervisor.version-drift', {
|
|
358
|
-
watcher_pid: currentChildPid,
|
|
359
|
-
instance_version: status.instance_version || null,
|
|
360
|
-
file_version: status.file_version || null,
|
|
361
|
-
cooldown_ms: VERSION_DRIFT_COOLDOWN_MS,
|
|
362
|
-
severity: 'critical',
|
|
363
|
-
});
|
|
364
|
-
try {
|
|
365
|
-
const gmTools = gmToolsDir();
|
|
366
|
-
for (const f of ['plugkit.wasm', 'plugkit.version', 'plugkit.wasm.sha256']) {
|
|
367
|
-
try { fs.unlinkSync(path.join(gmTools, f)); } catch (_) {}
|
|
368
|
-
}
|
|
369
|
-
} catch (_) {}
|
|
370
|
-
killChild('version-change');
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
process.on('SIGINT', () => {
|
|
375
|
-
logEvent('supervisor.shutdown', { reason: 'sigint' });
|
|
376
|
-
writeSupervisorStatus('shutdown', { reason: 'sigint' });
|
|
377
|
-
if (currentChildPid && pidAlive(currentChildPid)) {
|
|
378
|
-
try { process.kill(currentChildPid, 'SIGTERM'); } catch (_) {}
|
|
379
|
-
}
|
|
380
|
-
process.exit(0);
|
|
381
|
-
});
|
|
382
|
-
process.on('SIGTERM', () => {
|
|
383
|
-
logEvent('supervisor.shutdown', { reason: 'sigterm' });
|
|
384
|
-
writeSupervisorStatus('shutdown', { reason: 'sigterm' });
|
|
385
|
-
if (currentChildPid && pidAlive(currentChildPid)) {
|
|
386
|
-
try { process.kill(currentChildPid, 'SIGTERM'); } catch (_) {}
|
|
387
|
-
}
|
|
388
|
-
releaseSingleInstance();
|
|
389
|
-
process.exit(0);
|
|
390
|
-
});
|
|
391
|
-
process.on('exit', () => { releaseSingleInstance(); });
|
|
392
|
-
|
|
393
|
-
if (!acquireSingleInstance()) {
|
|
394
|
-
process.stderr.write('[plugkit-supervisor] another supervisor is alive; exiting\n');
|
|
395
|
-
process.exit(0);
|
|
396
|
-
}
|
|
397
|
-
writeSupervisorStatus('starting', {});
|
|
398
|
-
logEvent('supervisor.starting', { spool_dir: spoolDir });
|
|
399
|
-
try { fs.unlinkSync(path.join(spoolDir, '.pre-supervised-watcher.json')); } catch (_) {}
|
|
400
|
-
spawnWatcher('initial');
|
|
401
|
-
setInterval(checkWatcherHealth, POLL_INTERVAL_MS);
|
|
402
|
-
setInterval(() => writeSupervisorStatus('watching', { watcher_pid: currentChildPid, boot_reason: currentBootReason }), 10_000);
|