gm-skill 2.0.1156 → 2.0.1158
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/gm-plugkit/bootstrap.js +29 -12
- package/gm-plugkit/plugkit-wasm-wrapper.js +370 -8
- package/gm-plugkit/supervisor.js +211 -0
- package/gm.json +1 -1
- package/lib/daemon-bootstrap.js +4 -3
- package/package.json +2 -2
- package/skills/gm-skill/SKILL.md +2 -0
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ An earlier generation fanned out fifteen per-platform downstream repos (gm-cc, g
|
|
|
35
35
|
|
|
36
36
|
## Version
|
|
37
37
|
|
|
38
|
-
`2.0.
|
|
38
|
+
`2.0.1158` — auto-bumped from the canonical `gm` repo. Every push to `AnEntrypoint/gm` (or any cascading sibling crate) republishes this package.
|
|
39
39
|
|
|
40
40
|
## Source of truth
|
|
41
41
|
|
package/gm-plugkit/bootstrap.js
CHANGED
|
@@ -777,14 +777,6 @@ function startSpoolDaemon() {
|
|
|
777
777
|
return { ok: false, error: `wrapper not at ${wrapper} — ensureReady() must run first` };
|
|
778
778
|
}
|
|
779
779
|
const runtime = process.platform === 'win32' ? 'bun.exe' : 'bun';
|
|
780
|
-
let cmd = runtime;
|
|
781
|
-
let args = [wrapper, 'spool'];
|
|
782
|
-
try {
|
|
783
|
-
require('child_process').execFileSync(runtime, ['--version'], { stdio: 'ignore' });
|
|
784
|
-
} catch (_) {
|
|
785
|
-
cmd = process.execPath;
|
|
786
|
-
args = [wrapper, 'spool'];
|
|
787
|
-
}
|
|
788
780
|
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
789
781
|
const spoolDir = path.join(projectDir, '.gm', 'exec-spool');
|
|
790
782
|
fs.mkdirSync(spoolDir, { recursive: true });
|
|
@@ -796,18 +788,43 @@ function startSpoolDaemon() {
|
|
|
796
788
|
fs.renameSync(logPath, path.join(spoolDir, '.watcher.log.1'));
|
|
797
789
|
}
|
|
798
790
|
} catch (_) {}
|
|
791
|
+
|
|
792
|
+
const supervisor = path.join(__dirname, 'supervisor.js');
|
|
793
|
+
if (process.env.PLUGKIT_SKIP_SUPERVISOR === '1' || !fs.existsSync(supervisor)) {
|
|
794
|
+
let cmd = runtime;
|
|
795
|
+
let args = [wrapper, 'spool'];
|
|
796
|
+
try {
|
|
797
|
+
require('child_process').execFileSync(runtime, ['--version'], { stdio: 'ignore' });
|
|
798
|
+
} catch (_) {
|
|
799
|
+
cmd = process.execPath;
|
|
800
|
+
args = [wrapper, 'spool'];
|
|
801
|
+
}
|
|
802
|
+
const logFd = fs.openSync(logPath, 'a');
|
|
803
|
+
try { fs.writeSync(logFd, `\n--- daemon spawn ${new Date().toISOString()} parent=${process.pid} (no supervisor) ---\n`); } catch (_) {}
|
|
804
|
+
const child = require('child_process').spawn(cmd, args, {
|
|
805
|
+
detached: true,
|
|
806
|
+
stdio: ['ignore', logFd, logFd],
|
|
807
|
+
windowsHide: true,
|
|
808
|
+
env: { ...process.env, CLAUDE_PROJECT_DIR: projectDir, PLUGKIT_BOOT_REASON: 'direct-no-supervisor' },
|
|
809
|
+
});
|
|
810
|
+
try { fs.closeSync(logFd); } catch (_) {}
|
|
811
|
+
const pid = child.pid;
|
|
812
|
+
child.unref();
|
|
813
|
+
return { ok: true, pid, wrapper, runtime: cmd, logPath, supervised: false };
|
|
814
|
+
}
|
|
815
|
+
|
|
799
816
|
const logFd = fs.openSync(logPath, 'a');
|
|
800
|
-
try { fs.writeSync(logFd, `\n---
|
|
801
|
-
const child = require('child_process').spawn(
|
|
817
|
+
try { fs.writeSync(logFd, `\n--- supervisor spawn ${new Date().toISOString()} parent=${process.pid} ---\n`); } catch (_) {}
|
|
818
|
+
const child = require('child_process').spawn(process.execPath, [supervisor], {
|
|
802
819
|
detached: true,
|
|
803
820
|
stdio: ['ignore', logFd, logFd],
|
|
804
821
|
windowsHide: true,
|
|
805
|
-
env: { ...process.env, CLAUDE_PROJECT_DIR: projectDir },
|
|
822
|
+
env: { ...process.env, CLAUDE_PROJECT_DIR: projectDir, PLUGKIT_RUNTIME: runtime },
|
|
806
823
|
});
|
|
807
824
|
try { fs.closeSync(logFd); } catch (_) {}
|
|
808
825
|
const pid = child.pid;
|
|
809
826
|
child.unref();
|
|
810
|
-
return { ok: true, pid, wrapper, runtime
|
|
827
|
+
return { ok: true, pid, wrapper, supervisor, runtime, logPath, supervised: true };
|
|
811
828
|
} catch (e) {
|
|
812
829
|
return { ok: false, error: e.message };
|
|
813
830
|
}
|
|
@@ -123,8 +123,33 @@ function emitOrchestratorEvents(verb, taskBase, resultStr) {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
const TMP_DIR = os.tmpdir();
|
|
126
|
-
const
|
|
127
|
-
const
|
|
126
|
+
const LEGACY_BROWSER_PORTS_FILE = path.join(TMP_DIR, 'plugkit-browser-ports.json');
|
|
127
|
+
const LEGACY_BROWSER_SESSIONS_FILE = path.join(TMP_DIR, 'plugkit-browser-sessions.json');
|
|
128
|
+
|
|
129
|
+
function browserStateDir(cwd) {
|
|
130
|
+
const dir = path.join(cwd || process.cwd(), '.gm', 'exec-spool');
|
|
131
|
+
try { fs.mkdirSync(dir, { recursive: true }); } catch (_) {}
|
|
132
|
+
return dir;
|
|
133
|
+
}
|
|
134
|
+
function browserPortsFile(cwd) { return path.join(browserStateDir(cwd), 'browser-ports.json'); }
|
|
135
|
+
function browserSessionsFile(cwd) { return path.join(browserStateDir(cwd), 'browser-sessions.json'); }
|
|
136
|
+
|
|
137
|
+
function migrateLegacyBrowserState(cwd) {
|
|
138
|
+
const dst1 = browserPortsFile(cwd);
|
|
139
|
+
const dst2 = browserSessionsFile(cwd);
|
|
140
|
+
try {
|
|
141
|
+
if (!fs.existsSync(dst1) && fs.existsSync(LEGACY_BROWSER_PORTS_FILE)) {
|
|
142
|
+
const legacy = JSON.parse(fs.readFileSync(LEGACY_BROWSER_PORTS_FILE, 'utf-8'));
|
|
143
|
+
if (legacy && typeof legacy === 'object') fs.writeFileSync(dst1, JSON.stringify(legacy, null, 2));
|
|
144
|
+
}
|
|
145
|
+
} catch (_) {}
|
|
146
|
+
try {
|
|
147
|
+
if (!fs.existsSync(dst2) && fs.existsSync(LEGACY_BROWSER_SESSIONS_FILE)) {
|
|
148
|
+
const legacy = JSON.parse(fs.readFileSync(LEGACY_BROWSER_SESSIONS_FILE, 'utf-8'));
|
|
149
|
+
if (legacy && typeof legacy === 'object') fs.writeFileSync(dst2, JSON.stringify(legacy, null, 2));
|
|
150
|
+
}
|
|
151
|
+
} catch (_) {}
|
|
152
|
+
}
|
|
128
153
|
|
|
129
154
|
function readJsonFile(fp, fallback) {
|
|
130
155
|
try { return JSON.parse(fs.readFileSync(fp, 'utf-8')); } catch (_) { return fallback; }
|
|
@@ -246,8 +271,11 @@ function runPlaywriter(pw, args, timeoutMs) {
|
|
|
246
271
|
}
|
|
247
272
|
|
|
248
273
|
function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
|
|
249
|
-
|
|
250
|
-
const
|
|
274
|
+
migrateLegacyBrowserState(cwd);
|
|
275
|
+
const portsFile = browserPortsFile(cwd);
|
|
276
|
+
const sessionsFile = browserSessionsFile(cwd);
|
|
277
|
+
const ports = readJsonFile(portsFile, {});
|
|
278
|
+
const sessions = readJsonFile(sessionsFile, {});
|
|
251
279
|
const existing = ports[claudeSessionId];
|
|
252
280
|
if (existing && existing.port && isPortAliveSync(existing.port)) {
|
|
253
281
|
const pwIds = sessions[claudeSessionId] || [];
|
|
@@ -265,6 +293,7 @@ function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
|
|
|
265
293
|
'--disable-features=Translate',
|
|
266
294
|
];
|
|
267
295
|
const child = spawn(chrome, chromeArgs, { detached: true, stdio: 'ignore' });
|
|
296
|
+
const chromePid = child.pid;
|
|
268
297
|
child.unref();
|
|
269
298
|
const deadline = Date.now() + 10000;
|
|
270
299
|
let alive = false;
|
|
@@ -288,10 +317,10 @@ function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
|
|
|
288
317
|
try { const j = JSON.parse(out); pwSessionId = j.id || j.session_id || j.session; } catch (_) {}
|
|
289
318
|
}
|
|
290
319
|
if (!pwSessionId) throw new Error(`could not parse playwriter session id from: ${out}`);
|
|
291
|
-
ports[claudeSessionId] = { port, profileDir };
|
|
320
|
+
ports[claudeSessionId] = { port, profileDir, pid: chromePid };
|
|
292
321
|
sessions[claudeSessionId] = [pwSessionId];
|
|
293
|
-
writeJsonFile(
|
|
294
|
-
writeJsonFile(
|
|
322
|
+
writeJsonFile(portsFile, ports);
|
|
323
|
+
writeJsonFile(sessionsFile, sessions);
|
|
295
324
|
return pwSessionId;
|
|
296
325
|
}
|
|
297
326
|
|
|
@@ -406,6 +435,192 @@ function kvFilePath(ns, key) {
|
|
|
406
435
|
return path.join(dir, safeKey + '.json');
|
|
407
436
|
}
|
|
408
437
|
|
|
438
|
+
const __tasks = new Map();
|
|
439
|
+
|
|
440
|
+
function tasksDir(cwd) {
|
|
441
|
+
const d = path.join(cwd || process.cwd(), '.gm', 'exec-spool', 'tasks');
|
|
442
|
+
try { fs.mkdirSync(d, { recursive: true }); } catch (_) {}
|
|
443
|
+
return d;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function taskMetaPath(cwd, id) { return path.join(tasksDir(cwd), `${id}.json`); }
|
|
447
|
+
function taskOutPath(cwd, id, which) { return path.join(tasksDir(cwd), `${id}.${which}.log`); }
|
|
448
|
+
|
|
449
|
+
function writeTaskMeta(cwd, id, meta) {
|
|
450
|
+
try { fs.writeFileSync(taskMetaPath(cwd, id), JSON.stringify(meta, null, 2)); } catch (_) {}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function nextTaskId(cwd) {
|
|
454
|
+
const counterPath = path.join(tasksDir(cwd), '.counter');
|
|
455
|
+
let n = 0;
|
|
456
|
+
try { n = parseInt(fs.readFileSync(counterPath, 'utf-8'), 10) || 0; } catch (_) {}
|
|
457
|
+
n += 1;
|
|
458
|
+
try { fs.writeFileSync(counterPath, String(n)); } catch (_) {}
|
|
459
|
+
return `t${n}`;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function langToCmd(lang, code) {
|
|
463
|
+
if (lang === 'nodejs' || lang === 'js' || lang === 'javascript' || lang === 'node') return { cmd: process.execPath, args: ['-e', code], stdinCode: null };
|
|
464
|
+
if (lang === 'python' || lang === 'py') return { cmd: 'python', args: ['-c', code], stdinCode: null };
|
|
465
|
+
if (lang === 'bash' || lang === 'sh' || lang === 'shell' || lang === 'zsh') return { cmd: 'bash', args: ['-c', code], stdinCode: null };
|
|
466
|
+
if (lang === 'powershell' || lang === 'ps1') return { cmd: 'powershell', args: ['-NoProfile', '-NonInteractive', '-Command', code], stdinCode: null };
|
|
467
|
+
if (lang === 'deno') return { cmd: 'deno', args: ['eval', code], stdinCode: null };
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function spawnTask({ cwd, lang, code, timeoutMs }) {
|
|
472
|
+
const id = nextTaskId(cwd);
|
|
473
|
+
const built = langToCmd(lang, code);
|
|
474
|
+
if (!built) return { ok: false, error: `unsupported lang: ${lang}` };
|
|
475
|
+
const outLog = taskOutPath(cwd, id, 'stdout');
|
|
476
|
+
const errLog = taskOutPath(cwd, id, 'stderr');
|
|
477
|
+
let outFd = null, errFd = null;
|
|
478
|
+
try { outFd = fs.openSync(outLog, 'a'); } catch (_) {}
|
|
479
|
+
try { errFd = fs.openSync(errLog, 'a'); } catch (_) {}
|
|
480
|
+
const startedMs = Date.now();
|
|
481
|
+
const isPosix = process.platform !== 'win32';
|
|
482
|
+
const child = spawn(built.cmd, built.args, {
|
|
483
|
+
cwd: cwd || process.cwd(),
|
|
484
|
+
detached: isPosix,
|
|
485
|
+
stdio: ['ignore', outFd || 'ignore', errFd || 'ignore'],
|
|
486
|
+
windowsHide: true,
|
|
487
|
+
env: process.env,
|
|
488
|
+
});
|
|
489
|
+
try { if (outFd !== null) fs.closeSync(outFd); } catch (_) {}
|
|
490
|
+
try { if (errFd !== null) fs.closeSync(errFd); } catch (_) {}
|
|
491
|
+
const meta = {
|
|
492
|
+
id,
|
|
493
|
+
pid: child.pid,
|
|
494
|
+
pgid: isPosix ? child.pid : null,
|
|
495
|
+
lang,
|
|
496
|
+
cmd: built.cmd,
|
|
497
|
+
cwd: cwd || process.cwd(),
|
|
498
|
+
started_ms: startedMs,
|
|
499
|
+
timeout_ms: timeoutMs,
|
|
500
|
+
deadline_ms: startedMs + timeoutMs,
|
|
501
|
+
status: 'running',
|
|
502
|
+
exit_code: null,
|
|
503
|
+
stdout_log: outLog,
|
|
504
|
+
stderr_log: errLog,
|
|
505
|
+
};
|
|
506
|
+
__tasks.set(id, { child, meta });
|
|
507
|
+
writeTaskMeta(cwd, id, meta);
|
|
508
|
+
child.on('exit', (code, signal) => {
|
|
509
|
+
meta.status = signal ? 'killed' : (code === 0 ? 'completed' : 'failed');
|
|
510
|
+
meta.exit_code = code;
|
|
511
|
+
meta.signal = signal;
|
|
512
|
+
meta.ended_ms = Date.now();
|
|
513
|
+
writeTaskMeta(meta.cwd, id, meta);
|
|
514
|
+
});
|
|
515
|
+
child.on('error', (err) => {
|
|
516
|
+
meta.status = 'error';
|
|
517
|
+
meta.error = err.message;
|
|
518
|
+
meta.ended_ms = Date.now();
|
|
519
|
+
writeTaskMeta(meta.cwd, id, meta);
|
|
520
|
+
});
|
|
521
|
+
logEvent('plugkit', 'task.spawn', { task_id: id, pid: child.pid, lang, timeout_ms: timeoutMs });
|
|
522
|
+
return { ok: true, task_id: id, pid: child.pid, started_ms: startedMs };
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function stopTaskById(id) {
|
|
526
|
+
const entry = __tasks.get(id);
|
|
527
|
+
if (!entry) {
|
|
528
|
+
return { ok: false, error: 'unknown task_id', task_id: id };
|
|
529
|
+
}
|
|
530
|
+
const { child, meta } = entry;
|
|
531
|
+
if (meta.status !== 'running') return { ok: true, already: meta.status, task_id: id };
|
|
532
|
+
const pid = meta.pid;
|
|
533
|
+
const isPosix = process.platform !== 'win32';
|
|
534
|
+
try {
|
|
535
|
+
if (isPosix && meta.pgid) {
|
|
536
|
+
try { process.kill(-meta.pgid, 'SIGTERM'); } catch (_) {}
|
|
537
|
+
} else {
|
|
538
|
+
try { child.kill('SIGTERM'); } catch (_) {}
|
|
539
|
+
}
|
|
540
|
+
} catch (_) {}
|
|
541
|
+
const graceTimer = setTimeout(() => {
|
|
542
|
+
if (meta.status !== 'running') return;
|
|
543
|
+
if (isPosix && meta.pgid) {
|
|
544
|
+
try { process.kill(-meta.pgid, 'SIGKILL'); } catch (_) {}
|
|
545
|
+
} else if (process.platform === 'win32') {
|
|
546
|
+
try { spawnSync('taskkill', ['/F', '/T', '/PID', String(pid)], { stdio: 'ignore', timeout: 3000 }); } catch (_) {}
|
|
547
|
+
} else {
|
|
548
|
+
try { child.kill('SIGKILL'); } catch (_) {}
|
|
549
|
+
}
|
|
550
|
+
}, 2000);
|
|
551
|
+
graceTimer.unref && graceTimer.unref();
|
|
552
|
+
logEvent('plugkit', 'task.stop', { task_id: id, pid });
|
|
553
|
+
return { ok: true, task_id: id, pid };
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function tailFile(filePath, maxBytes) {
|
|
557
|
+
try {
|
|
558
|
+
const stat = fs.statSync(filePath);
|
|
559
|
+
if (stat.size <= maxBytes) return fs.readFileSync(filePath, 'utf-8');
|
|
560
|
+
const fd = fs.openSync(filePath, 'r');
|
|
561
|
+
try {
|
|
562
|
+
const buf = Buffer.alloc(maxBytes);
|
|
563
|
+
fs.readSync(fd, buf, 0, maxBytes, stat.size - maxBytes);
|
|
564
|
+
return buf.toString('utf-8');
|
|
565
|
+
} finally { try { fs.closeSync(fd); } catch (_) {} }
|
|
566
|
+
} catch (_) { return ''; }
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function listTasks(cwd) {
|
|
570
|
+
const d = tasksDir(cwd);
|
|
571
|
+
const out = [];
|
|
572
|
+
try {
|
|
573
|
+
for (const entry of fs.readdirSync(d)) {
|
|
574
|
+
if (!entry.endsWith('.json') || entry.startsWith('.')) continue;
|
|
575
|
+
try {
|
|
576
|
+
const meta = JSON.parse(fs.readFileSync(path.join(d, entry), 'utf-8'));
|
|
577
|
+
out.push(meta);
|
|
578
|
+
} catch (_) {}
|
|
579
|
+
}
|
|
580
|
+
} catch (_) {}
|
|
581
|
+
return out;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function reapTimedOutTasks() {
|
|
585
|
+
const now = Date.now();
|
|
586
|
+
for (const [id, entry] of __tasks) {
|
|
587
|
+
const m = entry.meta;
|
|
588
|
+
if (m.status === 'running' && m.deadline_ms && now > m.deadline_ms) {
|
|
589
|
+
logEvent('plugkit', 'task.timeout', { task_id: id, pid: m.pid, deadline_ms: m.deadline_ms, now_ms: now });
|
|
590
|
+
stopTaskById(id);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function killAllTasks(reason) {
|
|
596
|
+
let killed = 0;
|
|
597
|
+
for (const [id, entry] of __tasks) {
|
|
598
|
+
if (entry.meta.status === 'running') {
|
|
599
|
+
stopTaskById(id);
|
|
600
|
+
killed += 1;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
if (killed > 0) logEvent('plugkit', 'task.killAll', { reason, count: killed });
|
|
604
|
+
return killed;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function hostTaskProc(action, params) {
|
|
608
|
+
switch (action) {
|
|
609
|
+
case 'spawn': return spawnTask(params);
|
|
610
|
+
case 'stop': return stopTaskById(params.id || params.task_id);
|
|
611
|
+
case 'list': return { ok: true, tasks: listTasks(params.cwd) };
|
|
612
|
+
case 'output': return {
|
|
613
|
+
ok: true,
|
|
614
|
+
task_id: params.id || params.task_id,
|
|
615
|
+
stdout: tailFile(taskOutPath(params.cwd, params.id || params.task_id, 'stdout'), params.max_bytes || 65536),
|
|
616
|
+
stderr: tailFile(taskOutPath(params.cwd, params.id || params.task_id, 'stderr'), params.max_bytes || 65536),
|
|
617
|
+
};
|
|
618
|
+
case 'reap': { reapTimedOutTasks(); return { ok: true }; }
|
|
619
|
+
case 'killAll': { const n = killAllTasks(params.reason || 'host_task_proc'); return { ok: true, killed: n }; }
|
|
620
|
+
default: return { ok: false, error: `unknown action: ${action}` };
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
409
624
|
function makeHostFunctions(instanceRef) {
|
|
410
625
|
return {
|
|
411
626
|
host_fs_read: (pathPtr, pathLen) => {
|
|
@@ -684,6 +899,19 @@ function makeHostFunctions(instanceRef) {
|
|
|
684
899
|
return 0n;
|
|
685
900
|
}
|
|
686
901
|
},
|
|
902
|
+
|
|
903
|
+
host_task_proc: (actionPtr, actionLen, paramsPtr, paramsLen) => {
|
|
904
|
+
try {
|
|
905
|
+
const action = readWasmStr(instanceRef.value, actionPtr, actionLen);
|
|
906
|
+
const paramsStr = readWasmStr(instanceRef.value, paramsPtr, paramsLen);
|
|
907
|
+
const params = paramsStr ? JSON.parse(paramsStr) : {};
|
|
908
|
+
if (!params.cwd) params.cwd = process.cwd();
|
|
909
|
+
const result = hostTaskProc(action, params);
|
|
910
|
+
return writeWasmJson(instanceRef.value, result);
|
|
911
|
+
} catch (e) {
|
|
912
|
+
return writeWasmJson(instanceRef.value, { ok: false, error: e.message });
|
|
913
|
+
}
|
|
914
|
+
},
|
|
687
915
|
};
|
|
688
916
|
}
|
|
689
917
|
|
|
@@ -742,6 +970,96 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
742
970
|
}
|
|
743
971
|
acquireLock();
|
|
744
972
|
setInterval(refreshLock, 5000);
|
|
973
|
+
|
|
974
|
+
const IDLE_LIMIT_MS = parseInt(process.env.PLUGKIT_IDLE_LIMIT_MS, 10) || 15 * 60 * 1000;
|
|
975
|
+
const IDLE_CHECK_MS = 60_000;
|
|
976
|
+
const SHUTDOWN_REASON_PATH = path.join(spoolDir, '.shutdown-reason.json');
|
|
977
|
+
const STATUS_PATH_FOR_TEARDOWN = path.join(spoolDir, '.status.json');
|
|
978
|
+
const ACPTOAPI_STATUS_PATH = path.join(process.cwd(), '.gm', 'acptoapi-status.json');
|
|
979
|
+
let lastActivityMs = Date.now();
|
|
980
|
+
function markActivity(source) {
|
|
981
|
+
lastActivityMs = Date.now();
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function killPidQuiet(pid) {
|
|
985
|
+
if (!Number.isFinite(pid) || pid <= 0 || pid === process.pid) return false;
|
|
986
|
+
try { process.kill(pid, 'SIGTERM'); } catch (_) {}
|
|
987
|
+
if (process.platform === 'win32') {
|
|
988
|
+
try { spawnSync('taskkill', ['/F', '/T', '/PID', String(pid)], { stdio: 'ignore', timeout: 3000 }); } catch (_) {}
|
|
989
|
+
}
|
|
990
|
+
return true;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function teardownAll(reason) {
|
|
994
|
+
try {
|
|
995
|
+
logEvent('plugkit', 'watcher.teardown', { reason, idle_ms: Date.now() - lastActivityMs });
|
|
996
|
+
console.log(`[plugkit-wasm] teardown reason=${reason}`);
|
|
997
|
+
} catch (_) {}
|
|
998
|
+
|
|
999
|
+
try { killAllTasks(`teardown:${reason}`); } catch (_) {}
|
|
1000
|
+
|
|
1001
|
+
try {
|
|
1002
|
+
if (fs.existsSync(ACPTOAPI_STATUS_PATH)) {
|
|
1003
|
+
const status = JSON.parse(fs.readFileSync(ACPTOAPI_STATUS_PATH, 'utf-8'));
|
|
1004
|
+
if (status && Number.isFinite(status.pid)) killPidQuiet(status.pid);
|
|
1005
|
+
try { fs.unlinkSync(ACPTOAPI_STATUS_PATH); } catch (_) {}
|
|
1006
|
+
}
|
|
1007
|
+
} catch (_) {}
|
|
1008
|
+
|
|
1009
|
+
try {
|
|
1010
|
+
const portsFile = browserPortsFile(process.cwd());
|
|
1011
|
+
const sessionsFile = browserSessionsFile(process.cwd());
|
|
1012
|
+
const ports = readJsonFile(portsFile, {});
|
|
1013
|
+
for (const [sid, entry] of Object.entries(ports)) {
|
|
1014
|
+
if (entry && Number.isFinite(entry.pid)) killPidQuiet(entry.pid);
|
|
1015
|
+
}
|
|
1016
|
+
try { fs.unlinkSync(portsFile); } catch (_) {}
|
|
1017
|
+
try { fs.unlinkSync(sessionsFile); } catch (_) {}
|
|
1018
|
+
} catch (_) {}
|
|
1019
|
+
|
|
1020
|
+
try {
|
|
1021
|
+
fs.writeFileSync(SHUTDOWN_REASON_PATH, JSON.stringify({
|
|
1022
|
+
reason,
|
|
1023
|
+
ts: Date.now(),
|
|
1024
|
+
pid: process.pid,
|
|
1025
|
+
idle_ms: Date.now() - lastActivityMs,
|
|
1026
|
+
}));
|
|
1027
|
+
} catch (_) {}
|
|
1028
|
+
|
|
1029
|
+
try { fs.unlinkSync(STATUS_PATH_FOR_TEARDOWN); } catch (_) {}
|
|
1030
|
+
try { releaseLock(); } catch (_) {}
|
|
1031
|
+
process.exit(0);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
setInterval(() => {
|
|
1035
|
+
try { reapTimedOutTasks(); } catch (_) {}
|
|
1036
|
+
}, 5000);
|
|
1037
|
+
|
|
1038
|
+
setInterval(() => {
|
|
1039
|
+
try {
|
|
1040
|
+
const idleMs = Date.now() - lastActivityMs;
|
|
1041
|
+
if (idleMs < IDLE_LIMIT_MS) return;
|
|
1042
|
+
try {
|
|
1043
|
+
const ports = readJsonFile(browserPortsFile(process.cwd()), {});
|
|
1044
|
+
let browserAlive = false;
|
|
1045
|
+
for (const entry of Object.values(ports)) {
|
|
1046
|
+
if (entry && Number.isFinite(entry.port) && isPortAliveSync(entry.port)) { browserAlive = true; break; }
|
|
1047
|
+
}
|
|
1048
|
+
if (browserAlive) { markActivity('browser-port-alive'); return; }
|
|
1049
|
+
} catch (_) {}
|
|
1050
|
+
try {
|
|
1051
|
+
let anyRunning = false;
|
|
1052
|
+
for (const entry of __tasks.values()) {
|
|
1053
|
+
if (entry.meta.status === 'running') { anyRunning = true; break; }
|
|
1054
|
+
}
|
|
1055
|
+
if (anyRunning) { markActivity('task-running'); return; }
|
|
1056
|
+
} catch (_) {}
|
|
1057
|
+
teardownAll('idle');
|
|
1058
|
+
} catch (e) {
|
|
1059
|
+
console.error(`[idle-check] error: ${e.message}`);
|
|
1060
|
+
}
|
|
1061
|
+
}, IDLE_CHECK_MS);
|
|
1062
|
+
|
|
745
1063
|
process.on('SIGINT', () => { releaseLock(); process.exit(0); });
|
|
746
1064
|
process.on('SIGTERM', () => { releaseLock(); process.exit(0); });
|
|
747
1065
|
process.on('exit', releaseLock);
|
|
@@ -767,7 +1085,49 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
767
1085
|
const _bootVersion = resolveVersion(instance);
|
|
768
1086
|
console.log(`[plugkit-wasm] plugkit v${_bootVersion} (wasm)`);
|
|
769
1087
|
console.log(`[plugkit-wasm] watching ${inDir}`);
|
|
770
|
-
|
|
1088
|
+
|
|
1089
|
+
let _priorShutdown = null;
|
|
1090
|
+
let _priorStatus = null;
|
|
1091
|
+
try { _priorShutdown = JSON.parse(fs.readFileSync(SHUTDOWN_REASON_PATH, 'utf-8')); } catch (_) {}
|
|
1092
|
+
try { _priorStatus = JSON.parse(fs.readFileSync(STATUS_PATH_FOR_TEARDOWN, 'utf-8')); } catch (_) {}
|
|
1093
|
+
const _bootReason = process.env.PLUGKIT_BOOT_REASON || 'unknown';
|
|
1094
|
+
const _supervisorPid = parseInt(process.env.PLUGKIT_SUPERVISOR_PID, 10) || null;
|
|
1095
|
+
const restartContext = {
|
|
1096
|
+
boot_reason: _bootReason,
|
|
1097
|
+
supervisor_pid: _supervisorPid,
|
|
1098
|
+
prior_shutdown: _priorShutdown,
|
|
1099
|
+
prior_status: _priorStatus,
|
|
1100
|
+
prior_status_age_ms: _priorStatus && Number.isFinite(_priorStatus.ts) ? Date.now() - _priorStatus.ts : null,
|
|
1101
|
+
};
|
|
1102
|
+
const _isPlannedBoot = _priorShutdown && (_priorShutdown.reason === 'idle' || _priorShutdown.reason === 'sigterm' || _priorShutdown.reason === 'version-change');
|
|
1103
|
+
const _isFirstBoot = !_priorShutdown && !_priorStatus;
|
|
1104
|
+
const UNPLANNED_RESTART_MARKER = path.join(spoolDir, '.unplanned-restart.json');
|
|
1105
|
+
if (!_isPlannedBoot && !_isFirstBoot) {
|
|
1106
|
+
const incidentPayload = {
|
|
1107
|
+
ts: Date.now(),
|
|
1108
|
+
version: _bootVersion,
|
|
1109
|
+
severity: 'critical',
|
|
1110
|
+
...restartContext,
|
|
1111
|
+
log_tail_path: path.join(spoolDir, '.watcher.log'),
|
|
1112
|
+
gm_log_dir: GM_LOG_ROOT,
|
|
1113
|
+
instruction: 'Prior watcher died without a planned shutdown. This is treated as a critical failure. Inspect .watcher.log and gm-log/<day>/plugkit.jsonl events supervisor.watcher-exited-unexpectedly + supervisor.heartbeat-stale around the prior_status.ts timestamp to diagnose root cause.',
|
|
1114
|
+
};
|
|
1115
|
+
logEvent('plugkit', 'watcher.unplanned-restart', incidentPayload);
|
|
1116
|
+
try {
|
|
1117
|
+
let history = [];
|
|
1118
|
+
try { history = JSON.parse(fs.readFileSync(UNPLANNED_RESTART_MARKER, 'utf-8')).history || []; } catch (_) {}
|
|
1119
|
+
history.push(incidentPayload);
|
|
1120
|
+
if (history.length > 20) history = history.slice(-20);
|
|
1121
|
+
fs.writeFileSync(UNPLANNED_RESTART_MARKER, JSON.stringify({
|
|
1122
|
+
latest: incidentPayload,
|
|
1123
|
+
count: history.length,
|
|
1124
|
+
history,
|
|
1125
|
+
}, null, 2));
|
|
1126
|
+
} catch (_) {}
|
|
1127
|
+
console.error(`[plugkit-wasm] UNPLANNED RESTART detected — prior watcher died without writing .shutdown-reason.json. prior_status_age_ms=${restartContext.prior_status_age_ms} boot_reason=${_bootReason}`);
|
|
1128
|
+
}
|
|
1129
|
+
try { fs.unlinkSync(SHUTDOWN_REASON_PATH); } catch (_) {}
|
|
1130
|
+
logEvent('plugkit', 'watcher.boot', { version: _bootVersion, in_dir: inDir, out_dir: outDir, spool_dir: spoolDir, ...restartContext });
|
|
771
1131
|
|
|
772
1132
|
const PROCESSED_MAX = 10000;
|
|
773
1133
|
const processed = new Map();
|
|
@@ -934,6 +1294,7 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
934
1294
|
|
|
935
1295
|
const pollInterval = setInterval(async () => {
|
|
936
1296
|
const existing = walkDir(inDir);
|
|
1297
|
+
if (existing.length > 0) markActivity('poll');
|
|
937
1298
|
for (const fullPath of existing) {
|
|
938
1299
|
await processFile(fullPath);
|
|
939
1300
|
}
|
|
@@ -1004,6 +1365,7 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
1004
1365
|
watch(inDir, { recursive: true }, (eventType, filename) => {
|
|
1005
1366
|
if (!filename) return;
|
|
1006
1367
|
const fullPath = path.join(inDir, filename);
|
|
1368
|
+
markActivity('watch');
|
|
1007
1369
|
|
|
1008
1370
|
clearTimeout(debounce[fullPath]);
|
|
1009
1371
|
debounce[fullPath] = setTimeout(async () => {
|
|
@@ -0,0 +1,211 @@
|
|
|
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
|
+
|
|
9
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
10
|
+
const spoolDir = path.join(projectDir, '.gm', 'exec-spool');
|
|
11
|
+
fs.mkdirSync(spoolDir, { recursive: true });
|
|
12
|
+
|
|
13
|
+
const STATUS_PATH = path.join(spoolDir, '.status.json');
|
|
14
|
+
const SHUTDOWN_REASON_PATH = path.join(spoolDir, '.shutdown-reason.json');
|
|
15
|
+
const SUPERVISOR_PATH = path.join(spoolDir, '.supervisor.json');
|
|
16
|
+
const LOG_PATH = path.join(spoolDir, '.watcher.log');
|
|
17
|
+
const GM_LOG_ROOT = process.env.GM_LOG_DIR || path.join(os.homedir(), '.claude', 'gm-log');
|
|
18
|
+
|
|
19
|
+
const POLL_INTERVAL_MS = 10_000;
|
|
20
|
+
const STATUS_STALE_MS = 30_000;
|
|
21
|
+
const MAX_RESTART_BURST = 5;
|
|
22
|
+
const RESTART_WINDOW_MS = 60_000;
|
|
23
|
+
|
|
24
|
+
function logEvent(event, fields) {
|
|
25
|
+
try {
|
|
26
|
+
const day = new Date().toISOString().slice(0, 10);
|
|
27
|
+
const dir = path.join(GM_LOG_ROOT, day);
|
|
28
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
29
|
+
const line = JSON.stringify({
|
|
30
|
+
ts: Date.now(),
|
|
31
|
+
sub: 'plugkit',
|
|
32
|
+
event,
|
|
33
|
+
pid: process.pid,
|
|
34
|
+
sess: process.env.CLAUDE_SESSION_ID || '',
|
|
35
|
+
cwd: process.cwd(),
|
|
36
|
+
role: 'supervisor',
|
|
37
|
+
...fields,
|
|
38
|
+
}) + '\n';
|
|
39
|
+
fs.appendFileSync(path.join(dir, 'plugkit.jsonl'), line);
|
|
40
|
+
} catch (_) {}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeSupervisorStatus(state, extra) {
|
|
44
|
+
try {
|
|
45
|
+
fs.writeFileSync(SUPERVISOR_PATH, JSON.stringify({
|
|
46
|
+
pid: process.pid,
|
|
47
|
+
ts: Date.now(),
|
|
48
|
+
state,
|
|
49
|
+
...(extra || {}),
|
|
50
|
+
}));
|
|
51
|
+
} catch (_) {}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function pidAlive(pid) {
|
|
55
|
+
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
56
|
+
try { process.kill(pid, 0); return true; } catch (_) { return false; }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readStatus() {
|
|
60
|
+
try { return JSON.parse(fs.readFileSync(STATUS_PATH, 'utf-8')); } catch (_) { return null; }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function readShutdownReason() {
|
|
64
|
+
try { return JSON.parse(fs.readFileSync(SHUTDOWN_REASON_PATH, 'utf-8')); } catch (_) { return null; }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let lastSpawnedAt = 0;
|
|
68
|
+
let restartTimestamps = [];
|
|
69
|
+
let currentChildPid = null;
|
|
70
|
+
let currentBootReason = 'initial';
|
|
71
|
+
|
|
72
|
+
function spawnWatcher(bootReason) {
|
|
73
|
+
lastSpawnedAt = Date.now();
|
|
74
|
+
restartTimestamps.push(Date.now());
|
|
75
|
+
restartTimestamps = restartTimestamps.filter(t => Date.now() - t < RESTART_WINDOW_MS);
|
|
76
|
+
if (restartTimestamps.length > MAX_RESTART_BURST) {
|
|
77
|
+
logEvent('supervisor.giving-up', {
|
|
78
|
+
reason: 'restart-burst-exceeded',
|
|
79
|
+
restarts_in_window: restartTimestamps.length,
|
|
80
|
+
window_ms: RESTART_WINDOW_MS,
|
|
81
|
+
max: MAX_RESTART_BURST,
|
|
82
|
+
severity: 'critical',
|
|
83
|
+
});
|
|
84
|
+
writeSupervisorStatus('giving-up', { reason: 'restart-burst-exceeded' });
|
|
85
|
+
process.exit(2);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const wrapper = path.join(os.homedir(), '.claude', 'gm-tools', 'plugkit-wasm-wrapper.js');
|
|
89
|
+
if (!fs.existsSync(wrapper)) {
|
|
90
|
+
logEvent('supervisor.wrapper-missing', { wrapper, severity: 'critical' });
|
|
91
|
+
writeSupervisorStatus('error', { error: 'wrapper-missing' });
|
|
92
|
+
process.exit(3);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let runtime = process.env.PLUGKIT_RUNTIME || 'bun';
|
|
96
|
+
let cmd = runtime;
|
|
97
|
+
let args = [wrapper, 'spool'];
|
|
98
|
+
try {
|
|
99
|
+
spawnSync(runtime, ['--version'], { stdio: 'ignore', windowsHide: true });
|
|
100
|
+
} catch (_) {
|
|
101
|
+
cmd = process.execPath;
|
|
102
|
+
args = [wrapper, 'spool'];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let logFd = null;
|
|
106
|
+
try { logFd = fs.openSync(LOG_PATH, 'a'); } catch (_) {}
|
|
107
|
+
try {
|
|
108
|
+
if (logFd !== null) fs.writeSync(logFd, `\n--- watcher spawn ${new Date().toISOString()} supervisor=${process.pid} reason=${bootReason} ---\n`);
|
|
109
|
+
} catch (_) {}
|
|
110
|
+
|
|
111
|
+
const child = spawn(cmd, args, {
|
|
112
|
+
detached: false,
|
|
113
|
+
stdio: ['ignore', logFd || 'ignore', logFd || 'ignore'],
|
|
114
|
+
windowsHide: true,
|
|
115
|
+
env: {
|
|
116
|
+
...process.env,
|
|
117
|
+
CLAUDE_PROJECT_DIR: projectDir,
|
|
118
|
+
PLUGKIT_BOOT_REASON: bootReason,
|
|
119
|
+
PLUGKIT_SUPERVISOR_PID: String(process.pid),
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
try { if (logFd !== null) fs.closeSync(logFd); } catch (_) {}
|
|
124
|
+
currentChildPid = child.pid;
|
|
125
|
+
currentBootReason = bootReason;
|
|
126
|
+
writeSupervisorStatus('watching', { watcher_pid: child.pid, boot_reason: bootReason });
|
|
127
|
+
logEvent('supervisor.spawned-watcher', { watcher_pid: child.pid, boot_reason: bootReason, runtime: cmd });
|
|
128
|
+
|
|
129
|
+
child.on('exit', (code, signal) => {
|
|
130
|
+
const shutdownReason = readShutdownReason();
|
|
131
|
+
const reason = shutdownReason && shutdownReason.reason;
|
|
132
|
+
const idleClean = reason === 'idle';
|
|
133
|
+
logEvent(idleClean ? 'supervisor.watcher-exited-idle' : 'supervisor.watcher-exited-unexpectedly', {
|
|
134
|
+
watcher_pid: currentChildPid,
|
|
135
|
+
exit_code: code,
|
|
136
|
+
signal,
|
|
137
|
+
shutdown_reason: reason || null,
|
|
138
|
+
had_shutdown_reason_file: shutdownReason !== null,
|
|
139
|
+
severity: idleClean ? 'info' : 'critical',
|
|
140
|
+
uptime_ms: Date.now() - lastSpawnedAt,
|
|
141
|
+
});
|
|
142
|
+
if (idleClean) {
|
|
143
|
+
writeSupervisorStatus('exited-idle', { watcher_pid: currentChildPid });
|
|
144
|
+
try { fs.unlinkSync(SUPERVISOR_PATH); } catch (_) {}
|
|
145
|
+
process.exit(0);
|
|
146
|
+
}
|
|
147
|
+
writeSupervisorStatus('restarting', {
|
|
148
|
+
prior_watcher_pid: currentChildPid,
|
|
149
|
+
prior_exit_code: code,
|
|
150
|
+
prior_signal: signal,
|
|
151
|
+
prior_shutdown_reason: reason || null,
|
|
152
|
+
});
|
|
153
|
+
setTimeout(() => spawnWatcher('unplanned-restart-after-exit'), 1500);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
child.on('error', (err) => {
|
|
157
|
+
logEvent('supervisor.spawn-error', { error: err.message, severity: 'critical' });
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function checkWatcherHealth() {
|
|
162
|
+
if (!currentChildPid) return;
|
|
163
|
+
if (!pidAlive(currentChildPid)) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const status = readStatus();
|
|
167
|
+
if (!status) {
|
|
168
|
+
logEvent('supervisor.status-missing', {
|
|
169
|
+
watcher_pid: currentChildPid,
|
|
170
|
+
severity: 'warn',
|
|
171
|
+
});
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const age = Date.now() - (status.ts || 0);
|
|
175
|
+
if (age > STATUS_STALE_MS) {
|
|
176
|
+
logEvent('supervisor.heartbeat-stale', {
|
|
177
|
+
watcher_pid: currentChildPid,
|
|
178
|
+
status_pid: status.pid,
|
|
179
|
+
status_age_ms: age,
|
|
180
|
+
stale_limit_ms: STATUS_STALE_MS,
|
|
181
|
+
severity: 'critical',
|
|
182
|
+
});
|
|
183
|
+
try { process.kill(currentChildPid, 'SIGTERM'); } catch (_) {}
|
|
184
|
+
if (process.platform === 'win32') {
|
|
185
|
+
try { spawnSync('taskkill', ['/F', '/T', '/PID', String(currentChildPid)], { stdio: 'ignore', windowsHide: true, timeout: 3000 }); } catch (_) {}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
process.on('SIGINT', () => {
|
|
191
|
+
logEvent('supervisor.shutdown', { reason: 'sigint' });
|
|
192
|
+
writeSupervisorStatus('shutdown', { reason: 'sigint' });
|
|
193
|
+
if (currentChildPid && pidAlive(currentChildPid)) {
|
|
194
|
+
try { process.kill(currentChildPid, 'SIGTERM'); } catch (_) {}
|
|
195
|
+
}
|
|
196
|
+
process.exit(0);
|
|
197
|
+
});
|
|
198
|
+
process.on('SIGTERM', () => {
|
|
199
|
+
logEvent('supervisor.shutdown', { reason: 'sigterm' });
|
|
200
|
+
writeSupervisorStatus('shutdown', { reason: 'sigterm' });
|
|
201
|
+
if (currentChildPid && pidAlive(currentChildPid)) {
|
|
202
|
+
try { process.kill(currentChildPid, 'SIGTERM'); } catch (_) {}
|
|
203
|
+
}
|
|
204
|
+
process.exit(0);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
writeSupervisorStatus('starting', {});
|
|
208
|
+
logEvent('supervisor.starting', { spool_dir: spoolDir });
|
|
209
|
+
spawnWatcher('initial');
|
|
210
|
+
setInterval(checkWatcherHealth, POLL_INTERVAL_MS);
|
|
211
|
+
setInterval(() => writeSupervisorStatus('watching', { watcher_pid: currentChildPid, boot_reason: currentBootReason }), 10_000);
|
package/gm.json
CHANGED
package/lib/daemon-bootstrap.js
CHANGED
|
@@ -120,7 +120,7 @@ function computeIndexDigest(cwd = process.cwd()) {
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
function writeStatusFile(daemonName, status, sessionId) {
|
|
123
|
+
function writeStatusFile(daemonName, status, sessionId, childPid) {
|
|
124
124
|
try {
|
|
125
125
|
fs.mkdirSync(GM_STATE_DIR, { recursive: true });
|
|
126
126
|
const statusFile = path.join(GM_STATE_DIR, `${daemonName}-status.json`);
|
|
@@ -129,7 +129,8 @@ function writeStatusFile(daemonName, status, sessionId) {
|
|
|
129
129
|
status,
|
|
130
130
|
sessionId,
|
|
131
131
|
timestamp: new Date().toISOString(),
|
|
132
|
-
pid: process.pid,
|
|
132
|
+
pid: Number.isFinite(childPid) ? childPid : process.pid,
|
|
133
|
+
parent_pid: process.pid,
|
|
133
134
|
};
|
|
134
135
|
fs.writeFileSync(statusFile, JSON.stringify(payload, null, 2));
|
|
135
136
|
emitDaemonEvent(daemonName, 'info', 'Status written', { file: statusFile });
|
|
@@ -220,7 +221,7 @@ async function ensureAcptoapiRunning() {
|
|
|
220
221
|
});
|
|
221
222
|
child.unref();
|
|
222
223
|
emitDaemonEvent('acptoapi', 'info', 'Daemon spawned', { pid: child.pid, port, sessionId });
|
|
223
|
-
writeStatusFile('acptoapi', 'spawned', sessionId);
|
|
224
|
+
writeStatusFile('acptoapi', 'spawned', sessionId, child.pid);
|
|
224
225
|
return { ok: true, message: 'acptoapi spawned', pid: child.pid };
|
|
225
226
|
} catch (spawnErr) {
|
|
226
227
|
emitDaemonEvent('acptoapi', 'warn', 'Spawn failed, fallback to SDK', {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gm-skill",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1158",
|
|
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",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"gm.json"
|
|
40
40
|
],
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"gm-plugkit": "^2.0.
|
|
42
|
+
"gm-plugkit": "^2.0.1158"
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|
|
45
45
|
"node": ">=16.0.0"
|
package/skills/gm-skill/SKILL.md
CHANGED
|
@@ -20,6 +20,8 @@ If the `instruction` response carries a non-null `update_available`, plugkit dri
|
|
|
20
20
|
|
|
21
21
|
The wasm artifact lives at `~/.claude/gm-tools/plugkit.wasm`; the spool watcher runs it. The watcher's own stdout/stderr is appended to `.gm/exec-spool/.watcher.log` — Read it to see plugkit's internal trace, dispatch timings, sweep actions, errors.
|
|
22
22
|
|
|
23
|
+
The watcher self-shuts-down after 15 minutes idle (no spool I/O, no live browser session) and is restarted on next agent activity by a detached supervisor. `.gm/exec-spool/.unplanned-restart.json` is a critical-failure marker — present means a prior watcher died without a planned shutdown. Treat as a PRD-worthy incident on sight: diagnose via `.watcher.log` and `gm-log/<day>/plugkit.jsonl` events `supervisor.watcher-exited-unexpectedly` and `supervisor.heartbeat-stale` around the prior_status.ts timestamp, then delete the marker once root cause is named.
|
|
24
|
+
|
|
23
25
|
## Boot the spool watcher (first turn only)
|
|
24
26
|
|
|
25
27
|
Check `.gm/exec-spool/.status.json`. If absent or `ts` > 15s old, boot via the npm package — `bun x gm-plugkit@latest spool` fetches the freshest plugkit (wasm + wrapper), copies them into `~/.claude/gm-tools/`, then enters spool mode:
|