gm-skill 2.0.1291 → 2.0.1293
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/bin/ccsniff.js +184 -0
- package/bin/gmsniff.js +143 -0
- package/bin/plugkit-supervisor.js +297 -0
- package/gm-plugkit/plugkit-wasm-wrapper.js +49 -29
- package/gm.json +1 -1
- package/lib/skill-bootstrap.js +131 -11
- package/package.json +5 -3
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.1293` — 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
|
-
|
|
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,
|
|
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, [
|
|
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',
|
|
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',
|
|
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
|
|
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
|
-
|
|
668
|
-
|
|
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,
|
|
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',
|
|
678
|
-
roots.push(path.join(home, 'Library', 'Caches',
|
|
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');
|
|
@@ -705,11 +709,14 @@ function findInstalledChromiumBinary() {
|
|
|
705
709
|
}
|
|
706
710
|
|
|
707
711
|
function startManagedBrowser(pw, profileDir) {
|
|
708
|
-
const
|
|
712
|
+
const headless = process.env.GM_BROWSER_HEADLESS === '1';
|
|
713
|
+
const args = [...pw.baseArgs, 'browser', 'start', '--user-data-dir', profileDir];
|
|
714
|
+
if (headless) args.push('--headless');
|
|
709
715
|
const env = { ...process.env };
|
|
710
|
-
if (!env.PLAYWRITER_BROWSER_PATH) {
|
|
716
|
+
if (!env.GM_BROWSER_RUNNER_PATH && !env.PLAYWRITER_BROWSER_PATH) {
|
|
711
717
|
const browserBin = findInstalledChromiumBinary();
|
|
712
718
|
if (browserBin) {
|
|
719
|
+
env.GM_BROWSER_RUNNER_PATH = browserBin;
|
|
713
720
|
env.PLAYWRITER_BROWSER_PATH = browserBin;
|
|
714
721
|
logEvent('plugkit', 'browser.binary-resolved', { path: browserBin });
|
|
715
722
|
} else {
|
|
@@ -742,24 +749,24 @@ function isColdRunProfile(profileDir) {
|
|
|
742
749
|
}
|
|
743
750
|
|
|
744
751
|
function resolveManagedBrowserKey(pw) {
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
if (/^profile:/.test(
|
|
748
|
-
throw new Error(`
|
|
752
|
+
const explicitKey = process.env.GM_BROWSER_RUNNER_KEY || process.env.PLAYWRITER_BROWSER_KEY;
|
|
753
|
+
if (explicitKey) {
|
|
754
|
+
if (/^profile:/.test(explicitKey)) {
|
|
755
|
+
throw new Error(`GM_BROWSER_RUNNER_KEY=${explicitKey} points at a user OS browser profile; refusing — managed sessions must use install:Chromium:*`);
|
|
749
756
|
}
|
|
750
|
-
return
|
|
757
|
+
return explicitKey;
|
|
751
758
|
}
|
|
752
759
|
let text = '';
|
|
753
760
|
try {
|
|
754
|
-
const r =
|
|
761
|
+
const r = runBrowserRunner(pw, ['browser', 'list'], 8000);
|
|
755
762
|
text = (r && (r.stdout || r.stderr)) || '';
|
|
756
763
|
} catch (e) {
|
|
757
|
-
throw new Error(`
|
|
764
|
+
throw new Error(`managed browser session list failed: ${e.message}`);
|
|
758
765
|
}
|
|
759
766
|
const lines = text.split(/\r?\n/);
|
|
760
767
|
const installChromium = lines.map(l => (l.match(/\b(install:Chromium:[A-Za-z0-9_-]+)\b/) || [])[1]).find(Boolean);
|
|
761
768
|
if (installChromium) return installChromium;
|
|
762
|
-
throw new Error(`no managed Chromium install detected;
|
|
769
|
+
throw new Error(`no managed Chromium install detected; browser runner returned: ${scrubBrowserRunnerText(text).trim()}`);
|
|
763
770
|
}
|
|
764
771
|
|
|
765
772
|
function waitForExtensionReady(pw, profileDir, opts) {
|
|
@@ -775,7 +782,7 @@ function waitForExtensionReady(pw, profileDir, opts) {
|
|
|
775
782
|
while (Date.now() < deadline) {
|
|
776
783
|
const remaining = deadline - Date.now();
|
|
777
784
|
const innerTimeout = Math.max(28000, Math.min(remaining, 30000));
|
|
778
|
-
const r =
|
|
785
|
+
const r = runBrowserRunner(pw, ['session', 'new', '--browser', browserKey], innerTimeout);
|
|
779
786
|
if (r && r.status === 0) return r;
|
|
780
787
|
lastErr = scrubBrowserRunnerText((r && (r.stderr || r.stdout)) || '');
|
|
781
788
|
const now = Date.now();
|
|
@@ -1461,11 +1468,11 @@ function makeHostFunctions(instanceRef) {
|
|
|
1461
1468
|
const body = readWasmStr(instanceRef.value, bodyPtr, bodyLen);
|
|
1462
1469
|
const cwd = readWasmStr(instanceRef.value, cwdPtr, cwdLen) || process.cwd();
|
|
1463
1470
|
const sessionId = readWasmStr(instanceRef.value, sidPtr, sidLen) || 'default';
|
|
1464
|
-
const pw =
|
|
1471
|
+
const pw = findBrowserRunner();
|
|
1465
1472
|
if (!pw) return writeWasmJson(instanceRef.value, { ok: false, error: 'managed browser session runner not available' });
|
|
1466
1473
|
if (body.startsWith('session ')) {
|
|
1467
1474
|
const parts = body.slice(8).trim().split(/\s+/);
|
|
1468
|
-
const r =
|
|
1475
|
+
const r = runBrowserRunner(pw, ['session', ...parts], 30000);
|
|
1469
1476
|
return writeWasmJson(instanceRef.value, {
|
|
1470
1477
|
ok: r.status === 0,
|
|
1471
1478
|
stdout: scrubBrowserRunnerText(r.stdout || ''),
|
|
@@ -1474,7 +1481,7 @@ function makeHostFunctions(instanceRef) {
|
|
|
1474
1481
|
});
|
|
1475
1482
|
}
|
|
1476
1483
|
const pwSessionId = getOrCreateBrowserSession(cwd, sessionId, pw);
|
|
1477
|
-
const r =
|
|
1484
|
+
const r = runBrowserRunner(pw, ['-s', pwSessionId, '--timeout', '14000', '-e', body], 60000);
|
|
1478
1485
|
return writeWasmJson(instanceRef.value, {
|
|
1479
1486
|
ok: r.status === 0,
|
|
1480
1487
|
stdout: scrubBrowserRunnerText(r.stdout || ''),
|
|
@@ -1659,7 +1666,13 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
1659
1666
|
const ageMs = Date.now() - priorBoot.ts;
|
|
1660
1667
|
const shutdownIsNewer = priorShutdownForAbort && Number.isFinite(priorShutdownForAbort.ts) && priorShutdownForAbort.ts >= priorBoot.ts;
|
|
1661
1668
|
if (ageMs > 30_000 && !shutdownIsNewer) {
|
|
1662
|
-
|
|
1669
|
+
let priorVerbSnap = null;
|
|
1670
|
+
let priorStatusSnap = null;
|
|
1671
|
+
let priorPidAlive = null;
|
|
1672
|
+
try { priorVerbSnap = JSON.parse(fs.readFileSync(VERB_ACTIVE_PATH, 'utf-8')); } catch (_) {}
|
|
1673
|
+
try { priorStatusSnap = JSON.parse(fs.readFileSync(path.join(path.dirname(BOOT_ACTIVE_PATH), '.status.json'), 'utf-8')); } catch (_) {}
|
|
1674
|
+
try { process.kill(priorBoot.pid, 0); priorPidAlive = true; } catch (e) { priorPidAlive = e.code === 'EPERM'; }
|
|
1675
|
+
const forensics = {
|
|
1663
1676
|
prior_pid: priorBoot.pid,
|
|
1664
1677
|
prior_ts: priorBoot.ts,
|
|
1665
1678
|
prior_sha: priorBoot.wrapper_sha || null,
|
|
@@ -1668,8 +1681,15 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
1668
1681
|
age_ms: ageMs,
|
|
1669
1682
|
shutdown_reason_present: !!priorShutdownForAbort,
|
|
1670
1683
|
shutdown_reason_ts: priorShutdownForAbort ? priorShutdownForAbort.ts : null,
|
|
1671
|
-
|
|
1672
|
-
|
|
1684
|
+
prior_verb_active: priorVerbSnap,
|
|
1685
|
+
prior_status: priorStatusSnap,
|
|
1686
|
+
prior_pid_alive: priorPidAlive,
|
|
1687
|
+
};
|
|
1688
|
+
logEvent('plugkit', 'watcher.silent-abort', forensics);
|
|
1689
|
+
try {
|
|
1690
|
+
fs.writeFileSync(path.join(path.dirname(BOOT_ACTIVE_PATH), '.silent-abort-forensics.json'), JSON.stringify(forensics, null, 2));
|
|
1691
|
+
} catch (_) {}
|
|
1692
|
+
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
1693
|
}
|
|
1674
1694
|
}
|
|
1675
1695
|
} catch (_) {}
|
package/gm.json
CHANGED
package/lib/skill-bootstrap.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
3
|
+
"version": "2.0.1293",
|
|
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.
|
|
44
|
+
"gm-plugkit": "^2.0.1293"
|
|
43
45
|
},
|
|
44
46
|
"engines": {
|
|
45
47
|
"node": ">=16.0.0"
|