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 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 ships the wasm-wrapper + supervisor
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
- acd7bc0e2ee1d9dc3311ee37e74e2cb5b59f15559a1d29aacc703ddacf323199 plugkit-slim.wasm
1
+ fc7364d869507c548db7846b67e71084c8d9c0c5a18c39a09afaf3f28155d4cd plugkit-slim.wasm
@@ -1 +1 @@
1
- 0.1.935
1
+ 0.1.936
@@ -1 +1 @@
1
- 89e0937c8879886b66420c807e7decc29d6723df91300c63028de6c6cefeed1b plugkit.wasm
1
+ 6492759f1cf535d6b899f22d1a31c00296397ae493aa755a6c735940128d9b18 plugkit.wasm
@@ -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 supervisor = path.join(__dirname, 'supervisor.js');
1351
- if (process.env.PLUGKIT_SKIP_SUPERVISOR === '1' || !fs.existsSync(supervisor)) {
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--- supervisor spawn ${new Date().toISOString()} parent=${process.pid} ---\n`); } catch (_) {}
1370
- const child = require('child_process').spawn(runtime, [supervisor], {
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, PLUGKIT_RUNTIME: runtime },
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, supervisor, runtime, logPath, supervised: true };
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
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-plugkit",
3
- "version": "2.0.2023",
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
- acd7bc0e2ee1d9dc3311ee37e74e2cb5b59f15559a1d29aacc703ddacf323199 plugkit-slim.wasm
1
+ fc7364d869507c548db7846b67e71084c8d9c0c5a18c39a09afaf3f28155d4cd plugkit-slim.wasm
@@ -1 +1 @@
1
- 0.1.935
1
+ 0.1.936
package/gm.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm",
3
- "version": "2.0.2023",
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.935"
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.2023",
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",
@@ -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);