gm-skill 2.0.1558 → 2.0.1560
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/AGENTS.md +6 -6
- package/gm-plugkit/package.json +1 -1
- package/gm.json +1 -1
- package/lib/skill-bootstrap.js +13 -4
- package/package.json +1 -1
- package/skills/gm-skill/SKILL.md +1 -1
- package/bin/plugkit-supervisor.js +0 -360
package/AGENTS.md
CHANGED
|
@@ -120,17 +120,17 @@ Every skill's `allowed-tools:` is reduced to `Skill, Read, Write` (plus the SKIL
|
|
|
120
120
|
|
|
121
121
|
**Behavioral discipline lives in plugkit's `instruction` verb**: dispatch `instruction` for the live phase-specific prose (Three-Layer Admission Filter, maturity-first emit, closure anti-shapes, code invariants); do not duplicate it here. Enumeration in rs-learn (`recall: instruction-verb behavioral discipline invariants`).
|
|
122
122
|
|
|
123
|
-
**The agent IS the LLM rs-learn calls**: rs-learn never reaches a separate judge model for a quality score, relevance
|
|
123
|
+
**The agent IS the LLM rs-learn calls**: rs-learn never reaches a separate judge model for a quality score, relevance, prune, route, or loss signal -- plugkit IS the harness and the agent IS the model, each an inline decision reported through the spool. Per-core internals in rs-learn (`recall: rs-learn self-report core internals`).
|
|
124
124
|
|
|
125
|
-
**host_exec_js is synchronous**: pass a real per-call `timeoutMs` (zero/missing is a hard error)
|
|
125
|
+
**host_exec_js is synchronous**: pass a real per-call `timeoutMs` (zero/missing is a hard error). Detail in rs-learn (`recall: host_exec_js synchronous`).
|
|
126
126
|
|
|
127
|
-
**Sync-before-emit (codeinsight + search)**:
|
|
127
|
+
**Sync-before-emit (codeinsight + search)**: output must come from a freshly-synced index this invocation (cache serves only on digest match). Mechanics in rs-learn (`recall: sync-before-emit codeinsight search`).
|
|
128
128
|
|
|
129
|
-
**Auto-recall on turn entry**:
|
|
129
|
+
**Auto-recall on turn entry**: `instruction` attaches an `auto_recall` pack on the first dispatch after a >30s idle gap or session-start. Detail in rs-learn (`recall: auto-recall on turn entry`).
|
|
130
130
|
|
|
131
131
|
**Skill SKILL.md frontmatter `allowed-tools:` is harness-enforced**: a skill must list `Skill` (and `Read`/`Write`, Write only for spool dispatch) or it loses downstream-skill invocation that turn. Detail in rs-learn (`recall: SKILL.md frontmatter allowed-tools`).
|
|
132
132
|
|
|
133
|
-
**rs-learn observability**:
|
|
133
|
+
**rs-learn observability**: learning-pipeline state changes emit `evt:` lines to `.gm/exec-spool/.watcher.log` + gm-log; recall replies carry per-hit scoring fields. Surface + taxonomy + flags in rs-learn (`recall: rs-learn observability taxonomy`).
|
|
134
134
|
|
|
135
135
|
**Bootstrap contract (skill-init + SKILL.md auto-refresh + project wiring)**: `bootstrapPlugkit`/`ensureReady` initialize wasm hook-free (failures non-fatal), sha256-rewrite stale installed SKILL.md, and seed per-project `CLAUDE.md` (`@AGENTS.md`) + `.gm/next-step.md`; the wiring lives in `gm-plugkit/bootstrap.js::ensureReady` (the consumer-project watcher boot path), not only repo-root `bin`/`lib`. Detail in rs-learn (`recall: skill-initiated bootstrap contract`, `recall: SKILL.md auto-refresh`).
|
|
136
136
|
|
|
@@ -168,7 +168,7 @@ One-shot system-state probe: dispatch `plugkit health` via the file-spool before
|
|
|
168
168
|
|
|
169
169
|
## Site Build & Documentation
|
|
170
170
|
|
|
171
|
-
Site build is single-surface detail
|
|
171
|
+
Site build + landing render is single-surface detail, fully drained to rs-learn (`recall: gm site build details`).
|
|
172
172
|
|
|
173
173
|
|
|
174
174
|
@.gm/next-step.md
|
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.1560",
|
|
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": {
|
package/gm.json
CHANGED
package/lib/skill-bootstrap.js
CHANGED
|
@@ -503,10 +503,19 @@ function openWatcherLog(projectDir) {
|
|
|
503
503
|
|
|
504
504
|
function ensureSupervisorInstalled() {
|
|
505
505
|
try {
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
506
|
+
let src = null;
|
|
507
|
+
try {
|
|
508
|
+
const gmPlugkit = require('gm-plugkit');
|
|
509
|
+
const base = path.dirname(gmPlugkit.getPath ? gmPlugkit.getPath() : require.resolve('gm-plugkit'));
|
|
510
|
+
const cand = path.join(base, 'supervisor.js');
|
|
511
|
+
if (fs.existsSync(cand)) src = cand;
|
|
512
|
+
} catch (_) {}
|
|
513
|
+
if (!src) {
|
|
514
|
+
src = resolveFromCandidates([
|
|
515
|
+
path.join(__dirname, '..', 'gm-plugkit', 'supervisor.js'),
|
|
516
|
+
path.join(__dirname, '..', '..', 'gm-plugkit', 'supervisor.js'),
|
|
517
|
+
], 'gm-skill/gm-plugkit/supervisor.js');
|
|
518
|
+
}
|
|
510
519
|
if (!src || !fs.existsSync(src)) {
|
|
511
520
|
emitBootstrapEvent('warn', 'bundled plugkit-supervisor.js not found; supervisor unavailable');
|
|
512
521
|
return null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gm-skill",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1560",
|
|
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",
|
package/skills/gm-skill/SKILL.md
CHANGED
|
@@ -48,7 +48,7 @@ bun x gm-plugkit@latest spool > /dev/null 2>&1 &
|
|
|
48
48
|
|
|
49
49
|
**Dead-watcher recovery is mandatory.** Two consecutive missing re-Reads AND stale `ts` (>15s) AND no future `busy_until` = dead: `bun x gm-plugkit@latest spool` to boot a fresh watcher, then re-dispatch the original verb. Never substitute an alternative tool (puppeteer-core, WebFetch, raw chrome) for the `browser` verb -- reaching outside plugkit orphans state and bypasses the witness gates. Recovery is always notice-dead -> boot -> re-dispatch.
|
|
50
50
|
|
|
51
|
-
From PowerShell, write spool input as UTF-8 no-BOM (`-Encoding utf8` or `[System.IO.File]::WriteAllText`); the 5.1 default UTF-16+BOM trips `spool.body-encoding-recoded`. Prefer the `Write` tool for JSON bodies. First-turn body is `{"prompt":"<user request>"}` (derives orient_nouns + recall_hits); later same-conversation turns may use `{}`.
|
|
51
|
+
From PowerShell, write spool input as UTF-8 no-BOM (`-Encoding utf8` or `[System.IO.File]::WriteAllText`); the 5.1 default UTF-16+BOM trips `spool.body-encoding-recoded`. Prefer the `Write` tool for JSON bodies. First-turn body is `{"prompt":"<user request>"}` (derives orient_nouns + recall_hits); later same-conversation turns may use `{}`. A `Write` to `in/<verb>/` that errors `ENOENT` (a fast watcher consumed and unlinked the file before the tool's post-write stat) has STILL dispatched -- confirm via the `out/` response, never blind-retry (a non-idempotent verb like `git_finalize` would double-fire); a Bash heredoc `cat > in/<verb>/<N>.txt` has no post-write stat and never surfaces this.
|
|
52
52
|
|
|
53
53
|
**Batch writes and reads together.** Write request + Read response is one logical step -- issue both in one block, not three turns. Fan-out is the same: N independent verbs = N Writes in one block then N Reads in one block. Only a real data dependency (verb B needs A's response) forces separate turns.
|
|
54
54
|
|
|
@@ -1,360 +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 { spawn, spawnSync } = require('child_process');
|
|
8
|
-
const crypto = require('crypto');
|
|
9
|
-
|
|
10
|
-
function wrapperSha12OnDisk() {
|
|
11
|
-
try {
|
|
12
|
-
return crypto.createHash('sha256').update(fs.readFileSync(resolveWrapper())).digest('hex').slice(0, 12);
|
|
13
|
-
} catch (_) { return null; }
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
17
|
-
const spoolDir = path.join(projectDir, '.gm', 'exec-spool');
|
|
18
|
-
fs.mkdirSync(spoolDir, { recursive: true });
|
|
19
|
-
|
|
20
|
-
const STATUS_PATH = path.join(spoolDir, '.status.json');
|
|
21
|
-
const SHUTDOWN_REASON_PATH = path.join(spoolDir, '.shutdown-reason.json');
|
|
22
|
-
const SUPERVISOR_STATUS_PATH = path.join(spoolDir, '.supervisor-status.json');
|
|
23
|
-
const SUPERVISOR_PID_PATH = path.join(spoolDir, '.supervisor.pid');
|
|
24
|
-
const LOG_PATH = path.join(spoolDir, '.watcher.log');
|
|
25
|
-
const GM_LOG_ROOT = process.env.GM_LOG_DIR || path.join(os.homedir(), '.claude', 'gm-log');
|
|
26
|
-
|
|
27
|
-
const HEARTBEAT_STALE_MS = 60_000;
|
|
28
|
-
const HEALTH_POLL_MS = 5_000;
|
|
29
|
-
const SUPERVISOR_HEARTBEAT_MS = 5_000;
|
|
30
|
-
const SIGTERM_GRACE_MS = 5_000;
|
|
31
|
-
const BACKOFF_BASE_MS = 2_000;
|
|
32
|
-
const BACKOFF_CAP_MS = 30_000;
|
|
33
|
-
|
|
34
|
-
function logEvent(event, fields) {
|
|
35
|
-
try {
|
|
36
|
-
const day = new Date().toISOString().slice(0, 10);
|
|
37
|
-
const dir = path.join(GM_LOG_ROOT, day);
|
|
38
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
39
|
-
const line = JSON.stringify({
|
|
40
|
-
ts: new Date().toISOString(),
|
|
41
|
-
sub: 'plugkit',
|
|
42
|
-
event,
|
|
43
|
-
pid: process.pid,
|
|
44
|
-
sess: process.env.CLAUDE_SESSION_ID || '',
|
|
45
|
-
cwd: projectDir,
|
|
46
|
-
role: 'supervisor',
|
|
47
|
-
...fields,
|
|
48
|
-
}) + '\n';
|
|
49
|
-
fs.appendFileSync(path.join(dir, 'plugkit.jsonl'), line);
|
|
50
|
-
} catch (_) {}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function writeSupervisorStatus(state, extra) {
|
|
54
|
-
try {
|
|
55
|
-
fs.writeFileSync(SUPERVISOR_STATUS_PATH, JSON.stringify({
|
|
56
|
-
pid: process.pid,
|
|
57
|
-
ts: Date.now(),
|
|
58
|
-
iso: new Date().toISOString(),
|
|
59
|
-
state,
|
|
60
|
-
watcher_pid: currentChildPid,
|
|
61
|
-
...(extra || {}),
|
|
62
|
-
}));
|
|
63
|
-
} catch (_) {}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function writeShutdownReason(reason, extra) {
|
|
67
|
-
try {
|
|
68
|
-
fs.writeFileSync(SHUTDOWN_REASON_PATH, JSON.stringify({
|
|
69
|
-
ts: new Date().toISOString(),
|
|
70
|
-
reason,
|
|
71
|
-
written_by: 'supervisor',
|
|
72
|
-
supervisor_pid: process.pid,
|
|
73
|
-
watcher_pid: currentChildPid,
|
|
74
|
-
...(extra || {}),
|
|
75
|
-
}));
|
|
76
|
-
} catch (_) {}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function pidAlive(pid) {
|
|
80
|
-
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
81
|
-
try { process.kill(pid, 0); return true; } catch (_) { return false; }
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function readStatus() {
|
|
85
|
-
try { return JSON.parse(fs.readFileSync(STATUS_PATH, 'utf-8')); } catch (_) { return null; }
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function statusMtime() {
|
|
89
|
-
try { return fs.statSync(STATUS_PATH).mtimeMs; } catch (_) { return 0; }
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function acquireSingleInstance() {
|
|
93
|
-
// Atomic via O_EXCL ('wx'): exclusive-create fails if the file exists, so when N supervisors
|
|
94
|
-
// race to start in the same instant exactly one wins. A plain existsSync->write is TOCTOU and
|
|
95
|
-
// lets a concurrent burst all pass, which is the duplicate-supervisor churn this guards against.
|
|
96
|
-
for (let attempt = 0; attempt < 2; attempt++) {
|
|
97
|
-
try {
|
|
98
|
-
const fd = fs.openSync(SUPERVISOR_PID_PATH, 'wx');
|
|
99
|
-
try { fs.writeSync(fd, String(process.pid)); } finally { fs.closeSync(fd); }
|
|
100
|
-
return true;
|
|
101
|
-
} catch (e) {
|
|
102
|
-
if (e && e.code === 'EEXIST') {
|
|
103
|
-
let other = NaN;
|
|
104
|
-
try { other = parseInt(fs.readFileSync(SUPERVISOR_PID_PATH, 'utf-8').trim(), 10); } catch (_) {}
|
|
105
|
-
if (Number.isFinite(other) && other !== process.pid && pidAlive(other)) {
|
|
106
|
-
logEvent('supervisor.refused-duplicate', { existing_pid: other, severity: 'warn' });
|
|
107
|
-
process.stderr.write(`[plugkit-supervisor] another supervisor is alive (pid=${other}); exiting\n`);
|
|
108
|
-
return false;
|
|
109
|
-
}
|
|
110
|
-
try { fs.unlinkSync(SUPERVISOR_PID_PATH); } catch (_) {}
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
logEvent('supervisor.pid-write-failed', { error: e && e.message, severity: 'warn' });
|
|
114
|
-
return true;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return true;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function releaseSingleInstance() {
|
|
121
|
-
try {
|
|
122
|
-
if (fs.existsSync(SUPERVISOR_PID_PATH)) {
|
|
123
|
-
const raw = fs.readFileSync(SUPERVISOR_PID_PATH, 'utf-8').trim();
|
|
124
|
-
if (parseInt(raw, 10) === process.pid) fs.unlinkSync(SUPERVISOR_PID_PATH);
|
|
125
|
-
}
|
|
126
|
-
} catch (_) {}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
let currentChildPid = null;
|
|
130
|
-
let currentChild = null;
|
|
131
|
-
let restartCount = 0;
|
|
132
|
-
let lastSpawnedAt = 0;
|
|
133
|
-
let shuttingDown = false;
|
|
134
|
-
let killingForHeartbeat = false;
|
|
135
|
-
|
|
136
|
-
function nextBackoffMs() {
|
|
137
|
-
const ms = Math.min(BACKOFF_CAP_MS, BACKOFF_BASE_MS * Math.pow(2, restartCount));
|
|
138
|
-
return ms;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function resolveWrapper() {
|
|
142
|
-
const primary = path.join(os.homedir(), '.gm-tools', 'plugkit-wasm-wrapper.js');
|
|
143
|
-
const fallback = path.join(os.homedir(), '.claude', 'gm-tools', 'plugkit-wasm-wrapper.js');
|
|
144
|
-
if (fs.existsSync(primary)) return primary;
|
|
145
|
-
if (fs.existsSync(fallback)) return fallback;
|
|
146
|
-
return primary;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function resolveRuntime() {
|
|
150
|
-
const preferred = process.env.PLUGKIT_RUNTIME || 'bun';
|
|
151
|
-
try {
|
|
152
|
-
const r = spawnSync(preferred, ['--version'], { stdio: 'ignore', windowsHide: true, timeout: 1500 });
|
|
153
|
-
if (r.status === 0) return preferred;
|
|
154
|
-
} catch (_) {}
|
|
155
|
-
return process.execPath;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function spawnWatcher(bootReason) {
|
|
159
|
-
if (shuttingDown) return;
|
|
160
|
-
const wrapper = resolveWrapper();
|
|
161
|
-
if (!fs.existsSync(wrapper)) {
|
|
162
|
-
logEvent('supervisor.wrapper-missing', { wrapper, severity: 'critical' });
|
|
163
|
-
writeSupervisorStatus('error', { error: 'wrapper-missing', wrapper });
|
|
164
|
-
setTimeout(() => spawnWatcher(bootReason), Math.min(BACKOFF_CAP_MS, nextBackoffMs()));
|
|
165
|
-
restartCount += 1;
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
const runtime = resolveRuntime();
|
|
169
|
-
let logFd = null;
|
|
170
|
-
try { logFd = fs.openSync(LOG_PATH, 'a'); } catch (_) {}
|
|
171
|
-
try {
|
|
172
|
-
if (logFd !== null) fs.writeSync(logFd, `\n--- watcher spawn ${new Date().toISOString()} supervisor=${process.pid} reason=${bootReason} ---\n`);
|
|
173
|
-
} catch (_) {}
|
|
174
|
-
|
|
175
|
-
const child = spawn(runtime, [wrapper, 'spool'], {
|
|
176
|
-
detached: false,
|
|
177
|
-
stdio: ['ignore', logFd || 'ignore', logFd || 'ignore'],
|
|
178
|
-
windowsHide: true,
|
|
179
|
-
env: {
|
|
180
|
-
...process.env,
|
|
181
|
-
CLAUDE_PROJECT_DIR: projectDir,
|
|
182
|
-
PLUGKIT_BOOT_REASON: bootReason,
|
|
183
|
-
PLUGKIT_SUPERVISOR_PID: String(process.pid),
|
|
184
|
-
},
|
|
185
|
-
...(process.platform === 'win32' ? { creationFlags: 0x08000000 | 0x00000008 } : {}),
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
try { if (logFd !== null) fs.closeSync(logFd); } catch (_) {}
|
|
189
|
-
currentChild = child;
|
|
190
|
-
currentChildPid = child.pid;
|
|
191
|
-
lastSpawnedAt = Date.now();
|
|
192
|
-
writeSupervisorStatus('watching', { boot_reason: bootReason, runtime });
|
|
193
|
-
logEvent('supervisor.spawned-watcher', { watcher_pid: child.pid, boot_reason: bootReason, runtime });
|
|
194
|
-
|
|
195
|
-
child.on('exit', (code, signal) => {
|
|
196
|
-
const wasKilled = killingForHeartbeat;
|
|
197
|
-
killingForHeartbeat = false;
|
|
198
|
-
const exitedPid = currentChildPid;
|
|
199
|
-
currentChild = null;
|
|
200
|
-
currentChildPid = null;
|
|
201
|
-
if (shuttingDown) return;
|
|
202
|
-
const uptimeMs = Date.now() - lastSpawnedAt;
|
|
203
|
-
const respawnReason = wasKilled ? 'supervisor-killed-stale-heartbeat' : (signal ? `signal-${signal}` : `exit-${code}`);
|
|
204
|
-
logEvent('supervisor.watcher-exited', {
|
|
205
|
-
watcher_pid: exitedPid,
|
|
206
|
-
exit_code: code,
|
|
207
|
-
signal,
|
|
208
|
-
uptime_ms: uptimeMs,
|
|
209
|
-
respawn_reason: respawnReason,
|
|
210
|
-
severity: code === 0 && !signal && !wasKilled ? 'info' : 'critical',
|
|
211
|
-
});
|
|
212
|
-
if (code === 0 && !signal && !wasKilled) {
|
|
213
|
-
restartCount = 0;
|
|
214
|
-
} else {
|
|
215
|
-
restartCount += 1;
|
|
216
|
-
}
|
|
217
|
-
const delay = nextBackoffMs();
|
|
218
|
-
writeSupervisorStatus('restarting', { prior_watcher_pid: exitedPid, prior_exit_code: code, prior_signal: signal, respawn_reason: respawnReason, backoff_ms: delay });
|
|
219
|
-
setTimeout(() => spawnWatcher(respawnReason), delay);
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
child.on('error', (err) => {
|
|
223
|
-
logEvent('supervisor.spawn-error', { error: err.message, severity: 'critical' });
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function killChild(reason) {
|
|
228
|
-
if (!currentChildPid || !pidAlive(currentChildPid)) return;
|
|
229
|
-
killingForHeartbeat = true;
|
|
230
|
-
writeShutdownReason(reason, { uptime_ms: Date.now() - lastSpawnedAt });
|
|
231
|
-
try { process.kill(currentChildPid, 'SIGTERM'); } catch (_) {}
|
|
232
|
-
const pidAtKill = currentChildPid;
|
|
233
|
-
setTimeout(() => {
|
|
234
|
-
if (pidAtKill && pidAlive(pidAtKill)) {
|
|
235
|
-
logEvent('supervisor.sigkill-after-grace', { watcher_pid: pidAtKill, grace_ms: SIGTERM_GRACE_MS, severity: 'warn' });
|
|
236
|
-
if (process.platform === 'win32') {
|
|
237
|
-
try { spawnSync('taskkill', ['/F', '/T', '/PID', String(pidAtKill)], { stdio: 'ignore', windowsHide: true, timeout: 3000 }); } catch (_) {}
|
|
238
|
-
} else {
|
|
239
|
-
try { process.kill(pidAtKill, 'SIGKILL'); } catch (_) {}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}, SIGTERM_GRACE_MS);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function checkWatcherHealth() {
|
|
246
|
-
if (shuttingDown) return;
|
|
247
|
-
if (!currentChildPid) return;
|
|
248
|
-
if (!pidAlive(currentChildPid)) return;
|
|
249
|
-
const mtime = statusMtime();
|
|
250
|
-
if (mtime === 0) {
|
|
251
|
-
const age = Date.now() - lastSpawnedAt;
|
|
252
|
-
if (age > HEARTBEAT_STALE_MS) {
|
|
253
|
-
logEvent('supervisor.no-heartbeat-file', { watcher_pid: currentChildPid, age_since_spawn_ms: age, severity: 'critical' });
|
|
254
|
-
killChild('supervisor-killed-no-heartbeat');
|
|
255
|
-
}
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
const age = Date.now() - mtime;
|
|
259
|
-
if (age > HEARTBEAT_STALE_MS) {
|
|
260
|
-
logEvent('supervisor.heartbeat-stale', {
|
|
261
|
-
watcher_pid: currentChildPid,
|
|
262
|
-
status_age_ms: age,
|
|
263
|
-
stale_limit_ms: HEARTBEAT_STALE_MS,
|
|
264
|
-
severity: 'critical',
|
|
265
|
-
});
|
|
266
|
-
killChild('supervisor-killed-stale-heartbeat');
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
// A published wrapper-only fix (no wasm version bump) is copied to ~/.gm-tools by the next
|
|
270
|
-
// bootstrap's ensureWrapperFresh, but a healthy running watcher keeps the old wrapper until it
|
|
271
|
-
// restarts. Compare the watcher's reported wrapper_sha against the on-disk wrapper; on drift,
|
|
272
|
-
// recycle so the fix goes live without a manual kill. Skip while busy (a long verb is running).
|
|
273
|
-
const status = readStatus();
|
|
274
|
-
if (status && !(status.busy_until && status.busy_until > Date.now())) {
|
|
275
|
-
const reported = status.wrapper_sha || null;
|
|
276
|
-
const onDisk = wrapperSha12OnDisk();
|
|
277
|
-
if (reported && onDisk && reported !== onDisk) {
|
|
278
|
-
logEvent('supervisor.wrapper-sha-drift', {
|
|
279
|
-
watcher_pid: currentChildPid,
|
|
280
|
-
reported_sha: reported,
|
|
281
|
-
on_disk_sha: onDisk,
|
|
282
|
-
severity: 'info',
|
|
283
|
-
});
|
|
284
|
-
killChild('supervisor-killed-wrapper-sha-drift');
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
// The watcher reads the wasm's embedded instance_version at load and compares it to the
|
|
288
|
-
// plugkit.version text file (file_version), exposing version_drifted when they disagree.
|
|
289
|
-
// This catches a bumped version text sitting next to a stale wasm build (text claims 635
|
|
290
|
-
// while the binary embeds 634), which ensureReady's text-only drift check never re-downloads.
|
|
291
|
-
// Evict the stale cached wasm so the next bootstrap fails isReady() and redownloads, then recycle.
|
|
292
|
-
if (status.version_drifted === true) {
|
|
293
|
-
logEvent('supervisor.version-drift', {
|
|
294
|
-
watcher_pid: currentChildPid,
|
|
295
|
-
instance_version: status.instance_version || null,
|
|
296
|
-
file_version: status.file_version || null,
|
|
297
|
-
severity: 'critical',
|
|
298
|
-
});
|
|
299
|
-
try {
|
|
300
|
-
const home = process.env.USERPROFILE || process.env.HOME || os.homedir();
|
|
301
|
-
const gmTools = fs.existsSync(path.join(home, '.gm-tools'))
|
|
302
|
-
? path.join(home, '.gm-tools')
|
|
303
|
-
: path.join(home, '.claude', 'gm-tools');
|
|
304
|
-
for (const f of ['plugkit.wasm', 'plugkit.version', 'plugkit.wasm.sha256']) {
|
|
305
|
-
try { fs.unlinkSync(path.join(gmTools, f)); } catch (_) {}
|
|
306
|
-
}
|
|
307
|
-
} catch (_) {}
|
|
308
|
-
killChild('supervisor-killed-version-drift');
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function shutdown(reason) {
|
|
314
|
-
if (shuttingDown) return;
|
|
315
|
-
shuttingDown = true;
|
|
316
|
-
logEvent('supervisor.shutdown', { reason });
|
|
317
|
-
writeSupervisorStatus('shutdown', { reason });
|
|
318
|
-
if (currentChildPid && pidAlive(currentChildPid)) {
|
|
319
|
-
writeShutdownReason('supervisor-graceful-shutdown', { trigger: reason, uptime_ms: Date.now() - lastSpawnedAt });
|
|
320
|
-
try { process.kill(currentChildPid, 'SIGTERM'); } catch (_) {}
|
|
321
|
-
const pidAtKill = currentChildPid;
|
|
322
|
-
const start = Date.now();
|
|
323
|
-
const waitInterval = setInterval(() => {
|
|
324
|
-
if (!pidAlive(pidAtKill)) {
|
|
325
|
-
clearInterval(waitInterval);
|
|
326
|
-
releaseSingleInstance();
|
|
327
|
-
process.exit(0);
|
|
328
|
-
} else if (Date.now() - start > SIGTERM_GRACE_MS) {
|
|
329
|
-
clearInterval(waitInterval);
|
|
330
|
-
if (process.platform === 'win32') {
|
|
331
|
-
try { spawnSync('taskkill', ['/F', '/T', '/PID', String(pidAtKill)], { stdio: 'ignore', windowsHide: true, timeout: 3000 }); } catch (_) {}
|
|
332
|
-
} else {
|
|
333
|
-
try { process.kill(pidAtKill, 'SIGKILL'); } catch (_) {}
|
|
334
|
-
}
|
|
335
|
-
releaseSingleInstance();
|
|
336
|
-
process.exit(0);
|
|
337
|
-
}
|
|
338
|
-
}, 200);
|
|
339
|
-
} else {
|
|
340
|
-
releaseSingleInstance();
|
|
341
|
-
process.exit(0);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
process.on('SIGINT', () => shutdown('sigint'));
|
|
346
|
-
process.on('SIGTERM', () => shutdown('sigterm'));
|
|
347
|
-
process.on('uncaughtException', (err) => {
|
|
348
|
-
logEvent('supervisor.uncaught', { error: err.message, stack: err.stack, severity: 'critical' });
|
|
349
|
-
shutdown('uncaught-exception');
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
if (!acquireSingleInstance()) {
|
|
353
|
-
process.exit(0);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
writeSupervisorStatus('starting', {});
|
|
357
|
-
logEvent('supervisor.starting', { spool_dir: spoolDir, heartbeat_stale_ms: HEARTBEAT_STALE_MS });
|
|
358
|
-
spawnWatcher('initial');
|
|
359
|
-
setInterval(checkWatcherHealth, HEALTH_POLL_MS);
|
|
360
|
-
setInterval(() => writeSupervisorStatus('watching', {}), SUPERVISOR_HEARTBEAT_MS);
|