gm-skill 2.0.1291 → 2.0.1292

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
@@ -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.1291` — auto-bumped from the canonical `gm` repo. Every push to `AnEntrypoint/gm` (or any cascading sibling crate) republishes this package.
38
+ `2.0.1292` — 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/bin/ccsniff.js ADDED
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env node
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const os = require('os');
5
+
6
+ const C = { r:'\x1b[31m', g:'\x1b[32m', y:'\x1b[33m', b:'\x1b[34m', m:'\x1b[35m', c:'\x1b[36m', d:'\x1b[2m', x:'\x1b[0m', bold:'\x1b[1m' };
7
+ const useColor = process.stdout.isTTY;
8
+ const col = (k, s) => useColor ? C[k] + s + C.x : s;
9
+
10
+ function parseDuration(s) {
11
+ if (!s) return null;
12
+ const m = String(s).match(/^(\d+)([smhd])$/);
13
+ if (!m) return null;
14
+ return parseInt(m[1],10) * ({s:1000,m:60000,h:3600000,d:86400000}[m[2]]);
15
+ }
16
+
17
+ function parseArgs(argv) {
18
+ const flags = new Set();
19
+ let since = null, project = null;
20
+ for (let i = 2; i < argv.length; i++) {
21
+ const a = argv[i];
22
+ if (a === '--since') { since = parseDuration(argv[++i]); continue; }
23
+ if (a === '--project') { project = argv[++i]; continue; }
24
+ if (a.startsWith('--')) flags.add(a.slice(2));
25
+ }
26
+ return { flags, since, project };
27
+ }
28
+
29
+ function encodeCwd(cwd) {
30
+ return cwd.replace(/[\\/:]+/g, '-').replace(/^-+/, '');
31
+ }
32
+
33
+ function findTranscripts(project) {
34
+ const root = path.join(os.homedir(), '.claude', 'projects');
35
+ if (!fs.existsSync(root)) { console.log(col('d', `no log at ${root}`)); return []; }
36
+ const out = [];
37
+ const targetEnc = project ? encodeCwd(project) : null;
38
+ for (const dir of fs.readdirSync(root)) {
39
+ if (targetEnc && !dir.includes(targetEnc)) continue;
40
+ const full = path.join(root, dir);
41
+ if (!fs.statSync(full).isDirectory()) continue;
42
+ for (const f of fs.readdirSync(full)) {
43
+ if (f.endsWith('.jsonl')) out.push({ project: dir, file: path.join(full, f) });
44
+ }
45
+ }
46
+ return out;
47
+ }
48
+
49
+ function* iterTurns(file) {
50
+ let lines;
51
+ try { lines = fs.readFileSync(file, 'utf8').split(/\r?\n/); }
52
+ catch { return; }
53
+ for (const line of lines) {
54
+ if (!line) continue;
55
+ try { yield JSON.parse(line); } catch {}
56
+ }
57
+ }
58
+
59
+ function extractBashCommands(turn) {
60
+ const out = [];
61
+ const msg = turn.message;
62
+ if (!msg || !Array.isArray(msg.content)) return out;
63
+ for (const c of msg.content) {
64
+ if (c.type === 'tool_use' && (c.name === 'Bash' || c.name === 'PowerShell')) {
65
+ out.push({ tool: c.name, cmd: c.input?.command || '', id: c.id });
66
+ }
67
+ }
68
+ return out;
69
+ }
70
+
71
+ function turnTs(turn) {
72
+ return Date.parse(turn.timestamp || '') || 0;
73
+ }
74
+
75
+ function gitDisciplineScan(transcripts, cutoff) {
76
+ const findings = [];
77
+ for (const { project, file } of transcripts) {
78
+ let lastCommitTs = null, lastCommitSess = null, sawPushAfterCommit = false;
79
+ for (const turn of iterTurns(file)) {
80
+ const ts = turnTs(turn);
81
+ if (cutoff && ts && ts < cutoff) continue;
82
+ const cmds = extractBashCommands(turn);
83
+ for (const { tool, cmd } of cmds) {
84
+ if (!cmd) continue;
85
+ if (/\bgit\s+push\b/.test(cmd) && !/exec-spool|spool-dispatch|git_push/.test(cmd)) {
86
+ findings.push({ kind: 'raw-push', project, file: path.basename(file), ts, tool, cmd: cmd.slice(0, 120), sess: turn.sessionId });
87
+ sawPushAfterCommit = true;
88
+ }
89
+ if (/\bgit\s+commit\b/.test(cmd)) {
90
+ if (lastCommitTs && !sawPushAfterCommit) {
91
+ findings.push({ kind: 'commit-without-push', project, file: path.basename(file), ts: lastCommitTs, sess: lastCommitSess, cmd: '(prior commit had no push)' });
92
+ }
93
+ lastCommitTs = ts; lastCommitSess = turn.sessionId; sawPushAfterCommit = false;
94
+ }
95
+ if (/git\s+push.*--force\b/.test(cmd)) {
96
+ findings.push({ kind: 'force-push', project, file: path.basename(file), ts, tool, cmd: cmd.slice(0,120), sess: turn.sessionId });
97
+ }
98
+ }
99
+ }
100
+ }
101
+ return findings;
102
+ }
103
+
104
+ function collectPlugkitEvents(cutoff) {
105
+ const root = path.join(os.homedir(), '.claude', 'gm-log');
106
+ const events = [];
107
+ if (!fs.existsSync(root)) { console.log(col('d', `no log at ${root}`)); return events; }
108
+ for (const date of fs.readdirSync(root)) {
109
+ const pj = path.join(root, date, 'plugkit.jsonl');
110
+ if (!fs.existsSync(pj)) continue;
111
+ let lines; try { lines = fs.readFileSync(pj, 'utf8').split(/\r?\n/); } catch { continue; }
112
+ for (const line of lines) {
113
+ if (!line) continue;
114
+ try {
115
+ const ev = JSON.parse(line);
116
+ const ts = Date.parse(ev.ts || '') || 0;
117
+ if (cutoff && ts && ts < cutoff) continue;
118
+ ev._ts = ts;
119
+ events.push(ev);
120
+ } catch {}
121
+ }
122
+ }
123
+ return events;
124
+ }
125
+
126
+ function learningXref(transcripts, cutoff) {
127
+ const events = collectPlugkitEvents(cutoff);
128
+ const bySess = new Map();
129
+ for (const ev of events) {
130
+ const s = ev.sess || '';
131
+ if (!s) continue;
132
+ if (!bySess.has(s)) bySess.set(s, []);
133
+ bySess.get(s).push(ev);
134
+ }
135
+ const xrefs = [];
136
+ for (const { project, file } of transcripts) {
137
+ for (const turn of iterTurns(file)) {
138
+ const sess = turn.sessionId;
139
+ const ts = turnTs(turn);
140
+ if (cutoff && ts && ts < cutoff) continue;
141
+ if (!sess || !bySess.has(sess)) continue;
142
+ const candidates = bySess.get(sess).filter(ev => Math.abs((ev._ts||0) - ts) < 60000);
143
+ for (const ev of candidates) {
144
+ xrefs.push({ project, sess, turnTs: ts, evTs: ev._ts, event: ev.event, verb: ev.verb, dur: ev.dur_ms });
145
+ }
146
+ }
147
+ }
148
+ return xrefs;
149
+ }
150
+
151
+ function fmtTs(ts) { return ts ? new Date(ts).toISOString().slice(0,19).replace('T',' ') : '----------'; }
152
+
153
+ function main() {
154
+ const { flags, since, project } = parseArgs(process.argv);
155
+ const cutoff = since ? Date.now() - since : 0;
156
+ const transcripts = findTranscripts(project);
157
+ console.log(col('bold', `ccsniff — ${transcripts.length} transcript(s)${since ? `, since ${since/1000}s` : ''}${project ? `, project=${project}` : ''}`));
158
+ console.log(col('d', '-'.repeat(80)));
159
+
160
+ if (flags.has('git-discipline') || flags.size === 0) {
161
+ const findings = gitDisciplineScan(transcripts, cutoff);
162
+ console.log(col('bold', `git-discipline: ${findings.length} finding(s)`));
163
+ const byKind = {};
164
+ for (const f of findings) byKind[f.kind] = (byKind[f.kind]||0)+1;
165
+ console.log(col('d', ' by kind: ') + Object.entries(byKind).map(([k,v]) => `${col('y',k)}=${v}`).join(' '));
166
+ for (const f of findings.slice(-200)) {
167
+ const tag = f.kind === 'raw-push' ? col('r','RAW-PUSH ')
168
+ : f.kind === 'commit-without-push' ? col('y','COMMIT-NO-PUSH ')
169
+ : col('r','FORCE-PUSH ');
170
+ console.log(`${col('d', fmtTs(f.ts))} ${tag} ${col('m','['+String(f.sess||'').slice(0,8)+']')} ${col('c', f.project.slice(0,40))} ${f.cmd||''}`);
171
+ }
172
+ console.log();
173
+ }
174
+
175
+ if (flags.has('learning-xref')) {
176
+ const xrefs = learningXref(transcripts, cutoff);
177
+ console.log(col('bold', `learning-xref: ${xrefs.length} joined turn/event pair(s)`));
178
+ for (const x of xrefs.slice(-200)) {
179
+ console.log(`${col('d', fmtTs(x.turnTs))} ${col('m','['+x.sess.slice(0,8)+']')} ${col('c', x.event||'').padEnd(24)} verb=${x.verb||''} dur=${x.dur||''}ms proj=${x.project.slice(0,30)}`);
180
+ }
181
+ }
182
+ }
183
+
184
+ main();
package/bin/gmsniff.js ADDED
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const os = require('os');
5
+
6
+ const C = { r:'\x1b[31m', g:'\x1b[32m', y:'\x1b[33m', b:'\x1b[34m', m:'\x1b[35m', c:'\x1b[36m', d:'\x1b[2m', x:'\x1b[0m', bold:'\x1b[1m' };
7
+ const useColor = process.stdout.isTTY;
8
+ const col = (k, s) => useColor ? C[k] + s + C.x : s;
9
+
10
+ function parseDuration(s) {
11
+ if (!s) return null;
12
+ const m = String(s).match(/^(\d+)([smhd])$/);
13
+ if (!m) return null;
14
+ const n = parseInt(m[1], 10);
15
+ const mult = { s:1000, m:60000, h:3600000, d:86400000 }[m[2]];
16
+ return n * mult;
17
+ }
18
+
19
+ function parseArgs(argv) {
20
+ const flags = new Set();
21
+ let since = null;
22
+ for (let i = 2; i < argv.length; i++) {
23
+ const a = argv[i];
24
+ if (a === '--since') { since = parseDuration(argv[++i]); continue; }
25
+ if (a.startsWith('--')) flags.add(a.slice(2));
26
+ }
27
+ return { flags, since };
28
+ }
29
+
30
+ function readLines(p) {
31
+ if (!fs.existsSync(p)) { console.log(col('d', `no log at ${p}`)); return []; }
32
+ try { return fs.readFileSync(p, 'utf8').split(/\r?\n/).filter(Boolean); }
33
+ catch (e) { console.log(col('r', `read err ${p}: ${e.message}`)); return []; }
34
+ }
35
+
36
+ function collectEvents(sinceMs) {
37
+ const events = [];
38
+ const cutoff = sinceMs ? Date.now() - sinceMs : 0;
39
+ const cwd = process.cwd();
40
+ const wlog = path.join(cwd, '.gm', 'exec-spool', '.watcher.log');
41
+ for (const line of readLines(wlog)) {
42
+ const m = line.match(/evt:\s*(\{.*\})\s*$/);
43
+ if (!m) continue;
44
+ try {
45
+ const ev = JSON.parse(m[1]);
46
+ const ts = typeof ev.ts === 'number' ? ev.ts : Date.parse(ev.ts || '');
47
+ if (ts && ts < cutoff) continue;
48
+ ev._ts = ts || 0; ev._src = 'watcher.log';
49
+ events.push(ev);
50
+ } catch {}
51
+ }
52
+ const gmLogRoot = path.join(os.homedir(), '.claude', 'gm-log');
53
+ if (fs.existsSync(gmLogRoot)) {
54
+ for (const date of fs.readdirSync(gmLogRoot)) {
55
+ const pj = path.join(gmLogRoot, date, 'plugkit.jsonl');
56
+ if (!fs.existsSync(pj)) continue;
57
+ for (const line of readLines(pj)) {
58
+ try {
59
+ const ev = JSON.parse(line);
60
+ const ts = Date.parse(ev.ts || '') || ev.ts || 0;
61
+ if (ts && ts < cutoff) continue;
62
+ ev._ts = ts; ev._src = `gm-log/${date}/plugkit.jsonl`;
63
+ events.push(ev);
64
+ } catch {}
65
+ }
66
+ }
67
+ }
68
+ events.sort((a,b) => (a._ts||0) - (b._ts||0));
69
+ return events;
70
+ }
71
+
72
+ function fmtTs(ts) {
73
+ if (!ts) return '----------';
74
+ return new Date(ts).toISOString().slice(11, 19);
75
+ }
76
+
77
+ function evName(ev) { return ev.event || ''; }
78
+
79
+ const FILTERS = {
80
+ 'embed-failures': ev => /^embed_(fail|init_fail|cached_fail|init_cached_fail)$/.test(evName(ev)),
81
+ 'recall-misses': ev => evName(ev) === 'recall' && (ev.hits === 0 || (Array.isArray(ev.results) && ev.results.length === 0)),
82
+ 'recall-scores': ev => evName(ev) === 'recall' || evName(ev) === 'recall_score_unavailable',
83
+ 'classifier-rejects': ev => evName(ev) === 'memorize_reject',
84
+ 'memory-leverage': ev => evName(ev) === 'recall' && (ev.hits > 0 || (Array.isArray(ev.results) && ev.results.length > 0)),
85
+ 'recall-modes': ev => evName(ev) === 'recall' && ev.mode,
86
+ 'table-drops': ev => evName(ev) === 'table_dropped',
87
+ 'discipline-sigil-ignored': ev => evName(ev) === 'discipline_sigil_ignored',
88
+ };
89
+
90
+ function renderEvent(ev, kinds) {
91
+ const t = col('d', fmtTs(ev._ts));
92
+ const name = col('c', evName(ev).padEnd(28));
93
+ const sess = ev.sess ? col('m', `[${String(ev.sess).slice(0,16)}]`) : '';
94
+ const extras = [];
95
+ if (kinds.has('embed-failures') && ev.step) extras.push(`step=${ev.step}`);
96
+ if (ev.error) extras.push(col('r', `err=${String(ev.error).slice(0,80)}`));
97
+ if (ev.reason) extras.push(`reason=${ev.reason}`);
98
+ if (ev.text_prefix) extras.push(col('y', `text="${String(ev.text_prefix).slice(0,40)}"`));
99
+ if (ev.namespace) extras.push(`ns=${ev.namespace}`);
100
+ if (ev.mode) extras.push(col('g', `mode=${ev.mode}`));
101
+ if (ev.derived_query) extras.push(`q="${String(ev.derived_query).slice(0,40)}"`);
102
+ if (typeof ev.hits === 'number') extras.push(`hits=${ev.hits}`);
103
+ if (typeof ev.score === 'number') extras.push(`score=${ev.score.toFixed(3)}`);
104
+ if (ev.sigil) extras.push(col('y', `sigil=@${ev.sigil}`));
105
+ if (ev.key && evName(ev) === 'table_dropped') extras.push(`key=${ev.key}`);
106
+ return `${t} ${name} ${sess} ${extras.join(' ')}`;
107
+ }
108
+
109
+ function summarize(events) {
110
+ const counts = {};
111
+ for (const ev of events) {
112
+ const n = evName(ev) || '(unknown)';
113
+ counts[n] = (counts[n] || 0) + 1;
114
+ }
115
+ return counts;
116
+ }
117
+
118
+ function main() {
119
+ const { flags, since } = parseArgs(process.argv);
120
+ const allEvents = collectEvents(since);
121
+ const activeFilters = [...flags].filter(f => FILTERS[f]);
122
+ const useAll = activeFilters.length === 0;
123
+ const kinds = new Set(activeFilters);
124
+ const matched = useAll ? allEvents : allEvents.filter(ev => activeFilters.some(f => FILTERS[f](ev)));
125
+
126
+ console.log(col('bold', `gmsniff — ${matched.length} event(s)${since ? ` in last ${since/1000}s` : ''}${activeFilters.length ? `, filters: ${activeFilters.join(',')}` : ' (all)'}`));
127
+ const counts = summarize(matched);
128
+ const summary = Object.entries(counts).sort((a,b) => b[1]-a[1]).map(([k,v]) => `${col('c',k)}=${v}`).join(' ');
129
+ if (summary) console.log(col('d', 'by event: ') + summary);
130
+ console.log(col('d', '-'.repeat(80)));
131
+
132
+ if (kinds.has('recall-modes')) {
133
+ const modeCounts = {};
134
+ for (const ev of matched) if (ev.mode) modeCounts[ev.mode] = (modeCounts[ev.mode] || 0) + 1;
135
+ console.log(col('bold','recall mode distribution:'));
136
+ for (const [m,n] of Object.entries(modeCounts)) console.log(` ${col('g',m.padEnd(20))} ${n}`);
137
+ console.log();
138
+ }
139
+
140
+ for (const ev of matched) console.log(renderEvent(ev, kinds));
141
+ }
142
+
143
+ main();
@@ -0,0 +1,297 @@
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_STATUS_PATH = path.join(spoolDir, '.supervisor-status.json');
16
+ const SUPERVISOR_PID_PATH = path.join(spoolDir, '.supervisor.pid');
17
+ const LOG_PATH = path.join(spoolDir, '.watcher.log');
18
+ const GM_LOG_ROOT = process.env.GM_LOG_DIR || path.join(os.homedir(), '.claude', 'gm-log');
19
+
20
+ const HEARTBEAT_STALE_MS = 60_000;
21
+ const HEALTH_POLL_MS = 5_000;
22
+ const SUPERVISOR_HEARTBEAT_MS = 5_000;
23
+ const SIGTERM_GRACE_MS = 5_000;
24
+ const BACKOFF_BASE_MS = 2_000;
25
+ const BACKOFF_CAP_MS = 30_000;
26
+
27
+ function logEvent(event, fields) {
28
+ try {
29
+ const day = new Date().toISOString().slice(0, 10);
30
+ const dir = path.join(GM_LOG_ROOT, day);
31
+ fs.mkdirSync(dir, { recursive: true });
32
+ const line = JSON.stringify({
33
+ ts: new Date().toISOString(),
34
+ sub: 'plugkit',
35
+ event,
36
+ pid: process.pid,
37
+ sess: process.env.CLAUDE_SESSION_ID || '',
38
+ cwd: projectDir,
39
+ role: 'supervisor',
40
+ ...fields,
41
+ }) + '\n';
42
+ fs.appendFileSync(path.join(dir, 'plugkit.jsonl'), line);
43
+ } catch (_) {}
44
+ }
45
+
46
+ function writeSupervisorStatus(state, extra) {
47
+ try {
48
+ fs.writeFileSync(SUPERVISOR_STATUS_PATH, JSON.stringify({
49
+ pid: process.pid,
50
+ ts: Date.now(),
51
+ iso: new Date().toISOString(),
52
+ state,
53
+ watcher_pid: currentChildPid,
54
+ ...(extra || {}),
55
+ }));
56
+ } catch (_) {}
57
+ }
58
+
59
+ function writeShutdownReason(reason, extra) {
60
+ try {
61
+ fs.writeFileSync(SHUTDOWN_REASON_PATH, JSON.stringify({
62
+ ts: new Date().toISOString(),
63
+ reason,
64
+ written_by: 'supervisor',
65
+ supervisor_pid: process.pid,
66
+ watcher_pid: currentChildPid,
67
+ ...(extra || {}),
68
+ }));
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 readStatus() {
78
+ try { return JSON.parse(fs.readFileSync(STATUS_PATH, 'utf-8')); } catch (_) { return null; }
79
+ }
80
+
81
+ function statusMtime() {
82
+ try { return fs.statSync(STATUS_PATH).mtimeMs; } catch (_) { return 0; }
83
+ }
84
+
85
+ function acquireSingleInstance() {
86
+ try {
87
+ if (fs.existsSync(SUPERVISOR_PID_PATH)) {
88
+ const raw = fs.readFileSync(SUPERVISOR_PID_PATH, 'utf-8').trim();
89
+ const other = parseInt(raw, 10);
90
+ if (Number.isFinite(other) && other !== process.pid && pidAlive(other)) {
91
+ logEvent('supervisor.refused-duplicate', { existing_pid: other, severity: 'warn' });
92
+ process.stderr.write(`[plugkit-supervisor] another supervisor is alive (pid=${other}); exiting\n`);
93
+ return false;
94
+ }
95
+ }
96
+ fs.writeFileSync(SUPERVISOR_PID_PATH, String(process.pid));
97
+ return true;
98
+ } catch (e) {
99
+ logEvent('supervisor.pid-write-failed', { error: e.message, severity: 'warn' });
100
+ return true;
101
+ }
102
+ }
103
+
104
+ function releaseSingleInstance() {
105
+ try {
106
+ if (fs.existsSync(SUPERVISOR_PID_PATH)) {
107
+ const raw = fs.readFileSync(SUPERVISOR_PID_PATH, 'utf-8').trim();
108
+ if (parseInt(raw, 10) === process.pid) fs.unlinkSync(SUPERVISOR_PID_PATH);
109
+ }
110
+ } catch (_) {}
111
+ }
112
+
113
+ let currentChildPid = null;
114
+ let currentChild = null;
115
+ let restartCount = 0;
116
+ let lastSpawnedAt = 0;
117
+ let shuttingDown = false;
118
+ let killingForHeartbeat = false;
119
+
120
+ function nextBackoffMs() {
121
+ const ms = Math.min(BACKOFF_CAP_MS, BACKOFF_BASE_MS * Math.pow(2, restartCount));
122
+ return ms;
123
+ }
124
+
125
+ function resolveWrapper() {
126
+ return path.join(os.homedir(), '.claude', 'gm-tools', 'plugkit-wasm-wrapper.js');
127
+ }
128
+
129
+ function resolveRuntime() {
130
+ const preferred = process.env.PLUGKIT_RUNTIME || 'bun';
131
+ try {
132
+ const r = spawnSync(preferred, ['--version'], { stdio: 'ignore', windowsHide: true, timeout: 1500 });
133
+ if (r.status === 0) return preferred;
134
+ } catch (_) {}
135
+ return process.execPath;
136
+ }
137
+
138
+ function spawnWatcher(bootReason) {
139
+ if (shuttingDown) return;
140
+ const wrapper = resolveWrapper();
141
+ if (!fs.existsSync(wrapper)) {
142
+ logEvent('supervisor.wrapper-missing', { wrapper, severity: 'critical' });
143
+ writeSupervisorStatus('error', { error: 'wrapper-missing', wrapper });
144
+ setTimeout(() => spawnWatcher(bootReason), Math.min(BACKOFF_CAP_MS, nextBackoffMs()));
145
+ restartCount += 1;
146
+ return;
147
+ }
148
+ const runtime = resolveRuntime();
149
+ let logFd = null;
150
+ try { logFd = fs.openSync(LOG_PATH, 'a'); } catch (_) {}
151
+ try {
152
+ if (logFd !== null) fs.writeSync(logFd, `\n--- watcher spawn ${new Date().toISOString()} supervisor=${process.pid} reason=${bootReason} ---\n`);
153
+ } catch (_) {}
154
+
155
+ const child = spawn(runtime, [wrapper, 'spool'], {
156
+ detached: false,
157
+ stdio: ['ignore', logFd || 'ignore', logFd || 'ignore'],
158
+ windowsHide: true,
159
+ env: {
160
+ ...process.env,
161
+ CLAUDE_PROJECT_DIR: projectDir,
162
+ PLUGKIT_BOOT_REASON: bootReason,
163
+ PLUGKIT_SUPERVISOR_PID: String(process.pid),
164
+ },
165
+ ...(process.platform === 'win32' ? { creationFlags: 0x08000000 | 0x00000008 } : {}),
166
+ });
167
+
168
+ try { if (logFd !== null) fs.closeSync(logFd); } catch (_) {}
169
+ currentChild = child;
170
+ currentChildPid = child.pid;
171
+ lastSpawnedAt = Date.now();
172
+ writeSupervisorStatus('watching', { boot_reason: bootReason, runtime });
173
+ logEvent('supervisor.spawned-watcher', { watcher_pid: child.pid, boot_reason: bootReason, runtime });
174
+
175
+ child.on('exit', (code, signal) => {
176
+ const wasKilled = killingForHeartbeat;
177
+ killingForHeartbeat = false;
178
+ const exitedPid = currentChildPid;
179
+ currentChild = null;
180
+ currentChildPid = null;
181
+ if (shuttingDown) return;
182
+ const uptimeMs = Date.now() - lastSpawnedAt;
183
+ const respawnReason = wasKilled ? 'supervisor-killed-stale-heartbeat' : (signal ? `signal-${signal}` : `exit-${code}`);
184
+ logEvent('supervisor.watcher-exited', {
185
+ watcher_pid: exitedPid,
186
+ exit_code: code,
187
+ signal,
188
+ uptime_ms: uptimeMs,
189
+ respawn_reason: respawnReason,
190
+ severity: code === 0 && !signal && !wasKilled ? 'info' : 'critical',
191
+ });
192
+ if (code === 0 && !signal && !wasKilled) {
193
+ restartCount = 0;
194
+ } else {
195
+ restartCount += 1;
196
+ }
197
+ const delay = nextBackoffMs();
198
+ writeSupervisorStatus('restarting', { prior_watcher_pid: exitedPid, prior_exit_code: code, prior_signal: signal, respawn_reason: respawnReason, backoff_ms: delay });
199
+ setTimeout(() => spawnWatcher(respawnReason), delay);
200
+ });
201
+
202
+ child.on('error', (err) => {
203
+ logEvent('supervisor.spawn-error', { error: err.message, severity: 'critical' });
204
+ });
205
+ }
206
+
207
+ function killChild(reason) {
208
+ if (!currentChildPid || !pidAlive(currentChildPid)) return;
209
+ killingForHeartbeat = true;
210
+ writeShutdownReason(reason, { uptime_ms: Date.now() - lastSpawnedAt });
211
+ try { process.kill(currentChildPid, 'SIGTERM'); } catch (_) {}
212
+ const pidAtKill = currentChildPid;
213
+ setTimeout(() => {
214
+ if (pidAtKill && pidAlive(pidAtKill)) {
215
+ logEvent('supervisor.sigkill-after-grace', { watcher_pid: pidAtKill, grace_ms: SIGTERM_GRACE_MS, severity: 'warn' });
216
+ if (process.platform === 'win32') {
217
+ try { spawnSync('taskkill', ['/F', '/T', '/PID', String(pidAtKill)], { stdio: 'ignore', windowsHide: true, timeout: 3000 }); } catch (_) {}
218
+ } else {
219
+ try { process.kill(pidAtKill, 'SIGKILL'); } catch (_) {}
220
+ }
221
+ }
222
+ }, SIGTERM_GRACE_MS);
223
+ }
224
+
225
+ function checkWatcherHealth() {
226
+ if (shuttingDown) return;
227
+ if (!currentChildPid) return;
228
+ if (!pidAlive(currentChildPid)) return;
229
+ const mtime = statusMtime();
230
+ if (mtime === 0) {
231
+ const age = Date.now() - lastSpawnedAt;
232
+ if (age > HEARTBEAT_STALE_MS) {
233
+ logEvent('supervisor.no-heartbeat-file', { watcher_pid: currentChildPid, age_since_spawn_ms: age, severity: 'critical' });
234
+ killChild('supervisor-killed-no-heartbeat');
235
+ }
236
+ return;
237
+ }
238
+ const age = Date.now() - mtime;
239
+ if (age > HEARTBEAT_STALE_MS) {
240
+ logEvent('supervisor.heartbeat-stale', {
241
+ watcher_pid: currentChildPid,
242
+ status_age_ms: age,
243
+ stale_limit_ms: HEARTBEAT_STALE_MS,
244
+ severity: 'critical',
245
+ });
246
+ killChild('supervisor-killed-stale-heartbeat');
247
+ }
248
+ }
249
+
250
+ function shutdown(reason) {
251
+ if (shuttingDown) return;
252
+ shuttingDown = true;
253
+ logEvent('supervisor.shutdown', { reason });
254
+ writeSupervisorStatus('shutdown', { reason });
255
+ if (currentChildPid && pidAlive(currentChildPid)) {
256
+ writeShutdownReason('supervisor-graceful-shutdown', { trigger: reason, uptime_ms: Date.now() - lastSpawnedAt });
257
+ try { process.kill(currentChildPid, 'SIGTERM'); } catch (_) {}
258
+ const pidAtKill = currentChildPid;
259
+ const start = Date.now();
260
+ const waitInterval = setInterval(() => {
261
+ if (!pidAlive(pidAtKill)) {
262
+ clearInterval(waitInterval);
263
+ releaseSingleInstance();
264
+ process.exit(0);
265
+ } else if (Date.now() - start > SIGTERM_GRACE_MS) {
266
+ clearInterval(waitInterval);
267
+ if (process.platform === 'win32') {
268
+ try { spawnSync('taskkill', ['/F', '/T', '/PID', String(pidAtKill)], { stdio: 'ignore', windowsHide: true, timeout: 3000 }); } catch (_) {}
269
+ } else {
270
+ try { process.kill(pidAtKill, 'SIGKILL'); } catch (_) {}
271
+ }
272
+ releaseSingleInstance();
273
+ process.exit(0);
274
+ }
275
+ }, 200);
276
+ } else {
277
+ releaseSingleInstance();
278
+ process.exit(0);
279
+ }
280
+ }
281
+
282
+ process.on('SIGINT', () => shutdown('sigint'));
283
+ process.on('SIGTERM', () => shutdown('sigterm'));
284
+ process.on('uncaughtException', (err) => {
285
+ logEvent('supervisor.uncaught', { error: err.message, stack: err.stack, severity: 'critical' });
286
+ shutdown('uncaught-exception');
287
+ });
288
+
289
+ if (!acquireSingleInstance()) {
290
+ process.exit(0);
291
+ }
292
+
293
+ writeSupervisorStatus('starting', {});
294
+ logEvent('supervisor.starting', { spool_dir: spoolDir, heartbeat_stale_ms: HEARTBEAT_STALE_MS });
295
+ spawnWatcher('initial');
296
+ setInterval(checkWatcherHealth, HEALTH_POLL_MS);
297
+ setInterval(() => writeSupervisorStatus('watching', {}), SUPERVISOR_HEARTBEAT_MS);
@@ -461,15 +461,17 @@ function writeJsonFile(fp, value) {
461
461
  try { fs.writeFileSync(fp, JSON.stringify(value, null, 2)); } catch (_) {}
462
462
  }
463
463
 
464
- function findPlaywriter() {
464
+ const BROWSER_RUNNER_BIN = process.env.GM_BROWSER_RUNNER_BIN || 'playwriter';
465
+
466
+ function findBrowserRunner() {
465
467
  const npmR = spawnSync('npm', ['root', '-g'], { encoding: 'utf-8', shell: true });
466
468
  if (npmR.status === 0 && npmR.stdout.trim()) {
467
469
  const root = npmR.stdout.trim().split(/\r?\n/).pop();
468
- const binJs = path.join(root, 'playwriter', 'bin.js');
470
+ const binJs = path.join(root, BROWSER_RUNNER_BIN, 'bin.js');
469
471
  if (fs.existsSync(binJs)) return { cmd: process.execPath, baseArgs: [binJs], shell: false };
470
472
  }
471
473
  const whichCmd = process.platform === 'win32' ? 'where' : 'which';
472
- const r = spawnSync(whichCmd, ['playwriter'], { encoding: 'utf-8', shell: true });
474
+ const r = spawnSync(whichCmd, [BROWSER_RUNNER_BIN], { encoding: 'utf-8', shell: true });
473
475
  if (r.status === 0 && r.stdout.trim()) {
474
476
  const candidates = r.stdout.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
475
477
  const cmd = candidates.find(c => c.toLowerCase().endsWith('.cmd')) || candidates.find(c => !c.toLowerCase().endsWith('.ps1')) || candidates[0];
@@ -477,11 +479,11 @@ function findPlaywriter() {
477
479
  }
478
480
  const bunR = spawnSync(whichCmd, ['bun'], { encoding: 'utf-8', shell: true });
479
481
  if (bunR.status === 0 && bunR.stdout.trim()) {
480
- return { cmd: 'bun', baseArgs: ['x', 'playwriter@latest'], shell: true };
482
+ return { cmd: 'bun', baseArgs: ['x', `${BROWSER_RUNNER_BIN}@latest`], shell: true };
481
483
  }
482
484
  const npxR = spawnSync(whichCmd, ['npx'], { encoding: 'utf-8', shell: true });
483
485
  if (npxR.status === 0 && npxR.stdout.trim()) {
484
- return { cmd: 'npx', baseArgs: ['-y', 'playwriter'], shell: true };
486
+ return { cmd: 'npx', baseArgs: ['-y', BROWSER_RUNNER_BIN], shell: true };
485
487
  }
486
488
  return null;
487
489
  }
@@ -637,7 +639,7 @@ function sleepSync(ms) {
637
639
  spawnSync(process.execPath, ['-e', `setTimeout(()=>{}, ${ms})`], { timeout: ms + 2000 });
638
640
  }
639
641
 
640
- function runPlaywriter(pw, args, timeoutMs) {
642
+ function runBrowserRunner(pw, args, timeoutMs) {
641
643
  const allArgs = [...pw.baseArgs, ...args];
642
644
  const useShell = !!pw.shell;
643
645
  const spawnCmd = useShell && /\s/.test(pw.cmd) ? `"${pw.cmd}"` : pw.cmd;
@@ -654,7 +656,7 @@ function runPlaywriter(pw, args, timeoutMs) {
654
656
  function scrubBrowserRunnerText(s) {
655
657
  if (!s || typeof s !== 'string') return s;
656
658
  let t = s;
657
- t = t.replace(/(^|[^A-Za-z0-9_\\/.-])playwriter(?![A-Za-z0-9_\\/.-])/gi, (m, pre) => `${pre}managed browser session`);
659
+ t = t.replace(/(^|[^A-Za-z0-9_\\/.-])(playwriter|playwright|puppeteer)(?![A-Za-z0-9_\\/.-])/gi, (m, pre) => `${pre}managed browser session`);
658
660
  t = t.replace(/Click the[^.\n]*?extension[^.\n]*?icon[^.\n]*?\.?/gi, '');
659
661
  t = t.replace(/(connected\s+)?browser\s+extension(\s+is)?\s+not\s+connected\b[^.\n]*\.?/gi, '');
660
662
  t = t.replace(/no\s+connected\s+browsers?\b[^.\n]*\.?/gi, '');
@@ -664,18 +666,20 @@ function scrubBrowserRunnerText(s) {
664
666
 
665
667
  function findInstalledChromiumBinary() {
666
668
  try {
667
- if (process.env.PLAYWRITER_BROWSER_PATH && fs.existsSync(process.env.PLAYWRITER_BROWSER_PATH)) {
668
- return process.env.PLAYWRITER_BROWSER_PATH;
669
+ const explicit = process.env.GM_BROWSER_RUNNER_PATH || process.env.PLAYWRITER_BROWSER_PATH;
670
+ if (explicit && fs.existsSync(explicit)) {
671
+ return explicit;
669
672
  }
670
673
  const roots = [];
674
+ const cacheDir = process.env.GM_BROWSER_RUNNER_CACHE_DIR || 'ms-playwright';
671
675
  if (process.platform === 'win32') {
672
676
  const lad = process.env.LOCALAPPDATA;
673
- if (lad) roots.push(path.join(lad, 'ms-playwright'));
677
+ if (lad) roots.push(path.join(lad, cacheDir));
674
678
  } else {
675
679
  const home = process.env.HOME || '';
676
680
  if (home) {
677
- roots.push(path.join(home, '.cache', 'ms-playwright'));
678
- roots.push(path.join(home, 'Library', 'Caches', 'ms-playwright'));
681
+ roots.push(path.join(home, '.cache', cacheDir));
682
+ roots.push(path.join(home, 'Library', 'Caches', cacheDir));
679
683
  }
680
684
  }
681
685
  const exeName = process.platform === 'win32' ? 'chrome.exe' : (process.platform === 'darwin' ? 'Chromium.app/Contents/MacOS/Chromium' : 'chrome');
@@ -707,9 +711,10 @@ function findInstalledChromiumBinary() {
707
711
  function startManagedBrowser(pw, profileDir) {
708
712
  const args = [...pw.baseArgs, 'browser', 'start', '--user-data-dir', profileDir, '--headless'];
709
713
  const env = { ...process.env };
710
- if (!env.PLAYWRITER_BROWSER_PATH) {
714
+ if (!env.GM_BROWSER_RUNNER_PATH && !env.PLAYWRITER_BROWSER_PATH) {
711
715
  const browserBin = findInstalledChromiumBinary();
712
716
  if (browserBin) {
717
+ env.GM_BROWSER_RUNNER_PATH = browserBin;
713
718
  env.PLAYWRITER_BROWSER_PATH = browserBin;
714
719
  logEvent('plugkit', 'browser.binary-resolved', { path: browserBin });
715
720
  } else {
@@ -742,24 +747,24 @@ function isColdRunProfile(profileDir) {
742
747
  }
743
748
 
744
749
  function resolveManagedBrowserKey(pw) {
745
- if (process.env.PLAYWRITER_BROWSER_KEY) {
746
- const k = process.env.PLAYWRITER_BROWSER_KEY;
747
- if (/^profile:/.test(k)) {
748
- throw new Error(`PLAYWRITER_BROWSER_KEY=${k} points at a user OS browser profile; refusing — managed sessions must use install:Chromium:*`);
750
+ const explicitKey = process.env.GM_BROWSER_RUNNER_KEY || process.env.PLAYWRITER_BROWSER_KEY;
751
+ if (explicitKey) {
752
+ if (/^profile:/.test(explicitKey)) {
753
+ throw new Error(`GM_BROWSER_RUNNER_KEY=${explicitKey} points at a user OS browser profile; refusing — managed sessions must use install:Chromium:*`);
749
754
  }
750
- return k;
755
+ return explicitKey;
751
756
  }
752
757
  let text = '';
753
758
  try {
754
- const r = runPlaywriter(pw, ['browser', 'list'], 8000);
759
+ const r = runBrowserRunner(pw, ['browser', 'list'], 8000);
755
760
  text = (r && (r.stdout || r.stderr)) || '';
756
761
  } catch (e) {
757
- throw new Error(`playwriter browser list failed: ${e.message}`);
762
+ throw new Error(`managed browser session list failed: ${e.message}`);
758
763
  }
759
764
  const lines = text.split(/\r?\n/);
760
765
  const installChromium = lines.map(l => (l.match(/\b(install:Chromium:[A-Za-z0-9_-]+)\b/) || [])[1]).find(Boolean);
761
766
  if (installChromium) return installChromium;
762
- throw new Error(`no managed Chromium install detected; playwriter browser list returned: ${scrubBrowserRunnerText(text).trim()}`);
767
+ throw new Error(`no managed Chromium install detected; browser runner returned: ${scrubBrowserRunnerText(text).trim()}`);
763
768
  }
764
769
 
765
770
  function waitForExtensionReady(pw, profileDir, opts) {
@@ -775,7 +780,7 @@ function waitForExtensionReady(pw, profileDir, opts) {
775
780
  while (Date.now() < deadline) {
776
781
  const remaining = deadline - Date.now();
777
782
  const innerTimeout = Math.max(28000, Math.min(remaining, 30000));
778
- const r = runPlaywriter(pw, ['session', 'new', '--browser', browserKey], innerTimeout);
783
+ const r = runBrowserRunner(pw, ['session', 'new', '--browser', browserKey], innerTimeout);
779
784
  if (r && r.status === 0) return r;
780
785
  lastErr = scrubBrowserRunnerText((r && (r.stderr || r.stdout)) || '');
781
786
  const now = Date.now();
@@ -1461,11 +1466,11 @@ function makeHostFunctions(instanceRef) {
1461
1466
  const body = readWasmStr(instanceRef.value, bodyPtr, bodyLen);
1462
1467
  const cwd = readWasmStr(instanceRef.value, cwdPtr, cwdLen) || process.cwd();
1463
1468
  const sessionId = readWasmStr(instanceRef.value, sidPtr, sidLen) || 'default';
1464
- const pw = findPlaywriter();
1469
+ const pw = findBrowserRunner();
1465
1470
  if (!pw) return writeWasmJson(instanceRef.value, { ok: false, error: 'managed browser session runner not available' });
1466
1471
  if (body.startsWith('session ')) {
1467
1472
  const parts = body.slice(8).trim().split(/\s+/);
1468
- const r = runPlaywriter(pw, ['session', ...parts], 30000);
1473
+ const r = runBrowserRunner(pw, ['session', ...parts], 30000);
1469
1474
  return writeWasmJson(instanceRef.value, {
1470
1475
  ok: r.status === 0,
1471
1476
  stdout: scrubBrowserRunnerText(r.stdout || ''),
@@ -1474,7 +1479,7 @@ function makeHostFunctions(instanceRef) {
1474
1479
  });
1475
1480
  }
1476
1481
  const pwSessionId = getOrCreateBrowserSession(cwd, sessionId, pw);
1477
- const r = runPlaywriter(pw, ['-s', pwSessionId, '--timeout', '14000', '-e', body], 60000);
1482
+ const r = runBrowserRunner(pw, ['-s', pwSessionId, '--timeout', '14000', '-e', body], 60000);
1478
1483
  return writeWasmJson(instanceRef.value, {
1479
1484
  ok: r.status === 0,
1480
1485
  stdout: scrubBrowserRunnerText(r.stdout || ''),
@@ -1659,7 +1664,13 @@ async function runSpoolWatcher(instance, spoolDir) {
1659
1664
  const ageMs = Date.now() - priorBoot.ts;
1660
1665
  const shutdownIsNewer = priorShutdownForAbort && Number.isFinite(priorShutdownForAbort.ts) && priorShutdownForAbort.ts >= priorBoot.ts;
1661
1666
  if (ageMs > 30_000 && !shutdownIsNewer) {
1662
- logEvent('plugkit', 'watcher.silent-abort', {
1667
+ let priorVerbSnap = null;
1668
+ let priorStatusSnap = null;
1669
+ let priorPidAlive = null;
1670
+ try { priorVerbSnap = JSON.parse(fs.readFileSync(VERB_ACTIVE_PATH, 'utf-8')); } catch (_) {}
1671
+ try { priorStatusSnap = JSON.parse(fs.readFileSync(path.join(path.dirname(BOOT_ACTIVE_PATH), '.status.json'), 'utf-8')); } catch (_) {}
1672
+ try { process.kill(priorBoot.pid, 0); priorPidAlive = true; } catch (e) { priorPidAlive = e.code === 'EPERM'; }
1673
+ const forensics = {
1663
1674
  prior_pid: priorBoot.pid,
1664
1675
  prior_ts: priorBoot.ts,
1665
1676
  prior_sha: priorBoot.wrapper_sha || null,
@@ -1668,8 +1679,15 @@ async function runSpoolWatcher(instance, spoolDir) {
1668
1679
  age_ms: ageMs,
1669
1680
  shutdown_reason_present: !!priorShutdownForAbort,
1670
1681
  shutdown_reason_ts: priorShutdownForAbort ? priorShutdownForAbort.ts : null,
1671
- });
1672
- try { console.error(`[plugkit-wasm] SILENT ABORT detected: prior watcher pid=${priorBoot.pid} sha=${priorBoot.wrapper_sha} died without writing .shutdown-reason.json (age=${ageMs}ms)`); } catch (_) {}
1682
+ prior_verb_active: priorVerbSnap,
1683
+ prior_status: priorStatusSnap,
1684
+ prior_pid_alive: priorPidAlive,
1685
+ };
1686
+ logEvent('plugkit', 'watcher.silent-abort', forensics);
1687
+ try {
1688
+ fs.writeFileSync(path.join(path.dirname(BOOT_ACTIVE_PATH), '.silent-abort-forensics.json'), JSON.stringify(forensics, null, 2));
1689
+ } catch (_) {}
1690
+ try { console.error(`[plugkit-wasm] SILENT ABORT detected: prior watcher pid=${priorBoot.pid} sha=${priorBoot.wrapper_sha} died without writing .shutdown-reason.json (age=${ageMs}ms, prior_pid_alive=${priorPidAlive})`); } catch (_) {}
1673
1691
  }
1674
1692
  }
1675
1693
  } catch (_) {}
package/gm.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm",
3
- "version": "2.0.1291",
3
+ "version": "2.0.1292",
4
4
  "description": "Spool-dispatch orchestration engine with unified state machine, skills, and automated git enforcement",
5
5
  "author": "AnEntrypoint",
6
6
  "license": "MIT",
@@ -10,6 +10,7 @@ const PLUGKIT_TOOLS_DIR = path.join(os.homedir(), '.claude', 'gm-tools');
10
10
  const PLUGKIT_VERSION_FILE = path.join(PLUGKIT_TOOLS_DIR, 'plugkit.version');
11
11
  const PLUGKIT_WASM_PATH = path.join(PLUGKIT_TOOLS_DIR, 'plugkit.wasm');
12
12
  const PLUGKIT_WASM_WRAPPER = path.join(PLUGKIT_TOOLS_DIR, 'plugkit-wasm-wrapper.js');
13
+ const PLUGKIT_SUPERVISOR = path.join(PLUGKIT_TOOLS_DIR, 'plugkit-supervisor.js');
13
14
  const BOOTSTRAP_STATUS_FILE = path.join(os.homedir(), '.gm', 'bootstrap-status.json');
14
15
  const BOOTSTRAP_ERROR_FILE = path.join(os.homedir(), '.gm', 'bootstrap-error.json');
15
16
  const LOG_DIR = path.join(os.homedir(), '.claude', 'gm-log');
@@ -385,12 +386,28 @@ function findPlugkitWasmPids() {
385
386
  }
386
387
 
387
388
  function isProcessRunning() {
388
- return findPlugkitWasmPids().length > 0;
389
+ if (findPlugkitWasmPids().length > 0) return true;
390
+ try {
391
+ const supervisorPidFile = path.join(process.cwd(), '.gm', 'exec-spool', '.supervisor.pid');
392
+ if (!fs.existsSync(supervisorPidFile)) return false;
393
+ const pid = parseInt(fs.readFileSync(supervisorPidFile, 'utf8').trim(), 10);
394
+ if (!Number.isFinite(pid) || pid <= 0) return false;
395
+ try { process.kill(pid, 0); return true; } catch (_) { return false; }
396
+ } catch (_) { return false; }
389
397
  }
390
398
 
391
399
  function killExistingPlugkit() {
392
400
  try {
393
401
  const pids = findPlugkitWasmPids();
402
+ try {
403
+ const supervisorPidFile = path.join(process.cwd(), '.gm', 'exec-spool', '.supervisor.pid');
404
+ if (fs.existsSync(supervisorPidFile)) {
405
+ const sp = parseInt(fs.readFileSync(supervisorPidFile, 'utf8').trim(), 10);
406
+ if (Number.isFinite(sp) && sp > 0) {
407
+ try { process.kill(sp, 0); pids.unshift(String(sp)); } catch (_) {}
408
+ }
409
+ }
410
+ } catch (_) {}
394
411
  if (pids.length === 0) {
395
412
  emitBootstrapEvent('info', 'No existing plugkit WASM watcher to kill');
396
413
  return;
@@ -477,9 +494,51 @@ function openWatcherLog(projectDir) {
477
494
  return fd;
478
495
  }
479
496
 
497
+ function ensureSupervisorInstalled() {
498
+ try {
499
+ const src = resolveFromCandidates([
500
+ path.join(__dirname, '..', 'bin', 'plugkit-supervisor.js'),
501
+ path.join(__dirname, '..', '..', 'bin', 'plugkit-supervisor.js'),
502
+ ], 'gm-skill/bin/plugkit-supervisor.js');
503
+ if (!src || !fs.existsSync(src)) {
504
+ emitBootstrapEvent('warn', 'bundled plugkit-supervisor.js not found; supervisor unavailable');
505
+ return null;
506
+ }
507
+ fs.mkdirSync(PLUGKIT_TOOLS_DIR, { recursive: true });
508
+ let needsWrite = true;
509
+ if (fs.existsSync(PLUGKIT_SUPERVISOR)) {
510
+ try {
511
+ const a = crypto.createHash('sha256').update(fs.readFileSync(src)).digest('hex');
512
+ const b = crypto.createHash('sha256').update(fs.readFileSync(PLUGKIT_SUPERVISOR)).digest('hex');
513
+ if (a === b) needsWrite = false;
514
+ } catch (_) {}
515
+ }
516
+ if (needsWrite) {
517
+ const tmp = PLUGKIT_SUPERVISOR + '.tmp';
518
+ fs.copyFileSync(src, tmp);
519
+ fs.renameSync(tmp, PLUGKIT_SUPERVISOR);
520
+ emitBootstrapEvent('info', 'plugkit-supervisor.js installed', { target: PLUGKIT_SUPERVISOR });
521
+ }
522
+ return PLUGKIT_SUPERVISOR;
523
+ } catch (e) {
524
+ emitBootstrapEvent('warn', 'ensureSupervisorInstalled failed', { error: e.message });
525
+ return null;
526
+ }
527
+ }
528
+
529
+ function findSupervisorPid() {
530
+ try {
531
+ const supervisorPidFile = path.join(process.cwd(), '.gm', 'exec-spool', '.supervisor.pid');
532
+ if (!fs.existsSync(supervisorPidFile)) return null;
533
+ const pid = parseInt(fs.readFileSync(supervisorPidFile, 'utf8').trim(), 10);
534
+ if (!Number.isFinite(pid) || pid <= 0) return null;
535
+ try { process.kill(pid, 0); return pid; } catch (_) { return null; }
536
+ } catch (_) { return null; }
537
+ }
538
+
480
539
  async function spawnPlugkitWatcher(wasmPath) {
481
540
  try {
482
- emitBootstrapEvent('info', 'Spawning plugkit WASM watcher daemon');
541
+ emitBootstrapEvent('info', 'Spawning plugkit supervisor');
483
542
 
484
543
  let wrapperPath;
485
544
  try {
@@ -494,16 +553,37 @@ async function spawnPlugkitWatcher(wasmPath) {
494
553
  throw new Error(`WASM wrapper not found at ${wrapperPath}`);
495
554
  }
496
555
 
556
+ const supervisorPath = ensureSupervisorInstalled();
497
557
  const projectDir = process.cwd();
558
+
559
+ const existingSupervisor = findSupervisorPid();
560
+ if (existingSupervisor) {
561
+ emitBootstrapEvent('info', 'Plugkit supervisor already running', { pid: existingSupervisor });
562
+ return existingSupervisor;
563
+ }
564
+
565
+ if (!supervisorPath) {
566
+ emitBootstrapEvent('warn', 'falling back to direct watcher spawn (no supervisor)');
567
+ const logFd = openWatcherLog(projectDir);
568
+ const runtime = process.platform === 'win32' ? 'bun.exe' : 'bun';
569
+ const proc = spawn(runtime, [wrapperPath, 'spool'], {
570
+ detached: true,
571
+ stdio: ['ignore', logFd, logFd],
572
+ windowsHide: true,
573
+ env: { ...process.env, CLAUDE_PROJECT_DIR: projectDir },
574
+ ...(process.platform === 'win32' ? { creationFlags: 0x08000000 | 0x00000008 } : {}),
575
+ });
576
+ try { fs.closeSync(logFd); } catch (_) {}
577
+ const pid = proc.pid;
578
+ proc.unref();
579
+ emitBootstrapEvent('info', 'Plugkit watcher spawned (unsupervised)', { pid });
580
+ return pid;
581
+ }
582
+
498
583
  const logFd = openWatcherLog(projectDir);
584
+ try { fs.writeSync(logFd, `\n--- supervisor spawn ${new Date().toISOString()} parent=${process.pid} ---\n`); } catch (_) {}
499
585
 
500
- const runtime = process.platform === 'win32' ? 'bun.exe' : 'bun';
501
- // CREATE_NO_WINDOW (0x08000000) | DETACHED_PROCESS (0x00000008) —
502
- // inherited by all descendants so bun.exe → spool watcher → any
503
- // downstream spawn never allocates a console window. Without this,
504
- // windowsHide:true only hides the immediate bun.exe child while
505
- // .cmd shims it later spawns each pop their own conhost.
506
- const proc = spawn(runtime, [wrapperPath, 'spool'], {
586
+ const proc = spawn(process.execPath, [supervisorPath], {
507
587
  detached: true,
508
588
  stdio: ['ignore', logFd, logFd],
509
589
  windowsHide: true,
@@ -516,14 +596,53 @@ async function spawnPlugkitWatcher(wasmPath) {
516
596
  const pid = proc.pid;
517
597
  proc.unref();
518
598
 
519
- emitBootstrapEvent('info', 'Plugkit WASM watcher spawned', { pid, logPath: path.join(projectDir, '.gm', 'exec-spool', '.watcher.log') });
599
+ emitBootstrapEvent('info', 'Plugkit supervisor spawned', { pid, supervisor: supervisorPath, logPath: path.join(projectDir, '.gm', 'exec-spool', '.watcher.log') });
520
600
  return pid;
521
601
  } catch (e) {
522
- emitBootstrapEvent('error', 'Failed to spawn plugkit WASM watcher', { error: e.message });
602
+ emitBootstrapEvent('error', 'Failed to spawn plugkit supervisor', { error: e.message });
523
603
  throw e;
524
604
  }
525
605
  }
526
606
 
607
+ function ensureNextStepWiring(cwd) {
608
+ const gmDir = path.join(cwd, '.gm');
609
+ try { fs.mkdirSync(gmDir, { recursive: true }); } catch (_) {}
610
+
611
+ const nextStepPath = path.join(gmDir, 'next-step.md');
612
+ const nextStepBody = '# Next step\n\nDispatch the `instruction` verb to plugkit by writing `.gm/exec-spool/in/instruction/<N>.txt` (any unique N) with body `{}` (or `{"prompt":"<user request>"}` on the first dispatch of the turn). Read the response from `.gm/exec-spool/out/<N>.json` and follow the imperative in the `instruction` field.\n\nThis file is auto-rewritten by plugkit on every instruction dispatch.\n';
613
+ try {
614
+ if (!fs.existsSync(nextStepPath)) {
615
+ fs.writeFileSync(nextStepPath, nextStepBody);
616
+ emitBootstrapEvent('info', 'Seeded .gm/next-step.md', { path: nextStepPath });
617
+ }
618
+ } catch (e) {
619
+ emitBootstrapEvent('warn', 'Failed to seed .gm/next-step.md', { error: e.message });
620
+ }
621
+
622
+ const claudeMdPath = path.join(cwd, 'CLAUDE.md');
623
+ try {
624
+ if (!fs.existsSync(claudeMdPath)) {
625
+ fs.writeFileSync(claudeMdPath, '@AGENTS.md\n');
626
+ } else {
627
+ const cur = fs.readFileSync(claudeMdPath, 'utf8');
628
+ const hasLine = cur.split(/\r?\n/).some(l => l.trim() === '@AGENTS.md');
629
+ if (!hasLine) fs.writeFileSync(claudeMdPath, '@AGENTS.md\n' + cur);
630
+ }
631
+ } catch (_) {}
632
+
633
+ const agentsMdPath = path.join(cwd, 'AGENTS.md');
634
+ try {
635
+ if (fs.existsSync(agentsMdPath)) {
636
+ const cur = fs.readFileSync(agentsMdPath, 'utf8');
637
+ const hasLine = cur.split(/\r?\n/).some(l => l.trim() === '@.gm/next-step.md');
638
+ if (!hasLine) {
639
+ const sep = cur.endsWith('\n') ? '' : '\n';
640
+ fs.writeFileSync(agentsMdPath, cur + sep + '\n@.gm/next-step.md\n');
641
+ }
642
+ }
643
+ } catch (_) {}
644
+ }
645
+
527
646
  async function bootstrapPlugkit(sessionId, options) {
528
647
  const startTime = Date.now();
529
648
  const opts = options || {};
@@ -534,6 +653,7 @@ async function bootstrapPlugkit(sessionId, options) {
534
653
 
535
654
  writeSessionSidecar(sessionId);
536
655
  ensureBuildToolIgnores(process.cwd());
656
+ ensureNextStepWiring(process.cwd());
537
657
  ensureSkillMdCurrent();
538
658
 
539
659
  const manifest = readManifest();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-skill",
3
- "version": "2.0.1291",
3
+ "version": "2.0.1292",
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",
@@ -23,7 +23,9 @@
23
23
  },
24
24
  "main": "bin/bootstrap.js",
25
25
  "bin": {
26
- "gm-skill-bootstrap": "./bin/bootstrap.js"
26
+ "gm-skill-bootstrap": "./bin/bootstrap.js",
27
+ "gmsniff": "./bin/gmsniff.js",
28
+ "ccsniff": "./bin/ccsniff.js"
27
29
  },
28
30
  "files": [
29
31
  "skills/",
@@ -39,7 +41,7 @@
39
41
  "gm.json"
40
42
  ],
41
43
  "dependencies": {
42
- "gm-plugkit": "^2.0.1291"
44
+ "gm-plugkit": "^2.0.1292"
43
45
  },
44
46
  "engines": {
45
47
  "node": ">=16.0.0"