gm-skill 2.0.1557 → 2.0.1559

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-plugkit",
3
- "version": "2.0.1557",
3
+ "version": "2.0.1559",
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": {
@@ -264,7 +264,11 @@ function spawnWatcher(bootReason) {
264
264
  try { fs.unlinkSync(SUPERVISOR_PATH); } catch (_) {}
265
265
  process.exit(0);
266
266
  }
267
- const respawnReason = reason === 'version-change' ? 'planned-restart-version-change' : 'unplanned-restart-after-exit';
267
+ const respawnReason = reason === 'version-change'
268
+ ? 'planned-restart-version-change'
269
+ : isPlanned
270
+ ? `planned-restart-after-${reason || (cleanExit ? 'clean-exit' : 'exit')}`
271
+ : 'unplanned-restart-after-exit';
268
272
  writeSupervisorStatus('restarting', {
269
273
  prior_watcher_pid: currentChildPid,
270
274
  prior_exit_code: code,
package/gm.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm",
3
- "version": "2.0.1557",
3
+ "version": "2.0.1559",
4
4
  "description": "Spool-dispatch orchestration engine with unified state machine, skills, and automated git enforcement",
5
5
  "author": "AnEntrypoint",
6
6
  "license": "MIT",
@@ -503,10 +503,19 @@ function openWatcherLog(projectDir) {
503
503
 
504
504
  function ensureSupervisorInstalled() {
505
505
  try {
506
- const src = resolveFromCandidates([
507
- path.join(__dirname, '..', 'bin', 'plugkit-supervisor.js'),
508
- path.join(__dirname, '..', '..', 'bin', 'plugkit-supervisor.js'),
509
- ], 'gm-skill/bin/plugkit-supervisor.js');
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.1557",
3
+ "version": "2.0.1559",
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",
@@ -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);