gm-skill 2.0.2025 → 2.0.2026
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +7 -11
- package/bin/bootstrap.js +1 -1
- package/bin/install.js +31 -9
- package/gm-plugkit/bootstrap.js +35 -152
- package/gm-plugkit/cli.js +31 -168
- package/gm-plugkit/package.json +3 -5
- package/gm.json +1 -1
- package/package.json +1 -3
- package/skills/gm/SKILL.md +4 -4
- package/gm-plugkit/plugkit-wasm-wrapper.js +0 -4939
- package/gm-plugkit/wrapper/cdp-eval.js +0 -123
- package/gm-plugkit/wrapper/fs-atomic.js +0 -26
- package/gm-plugkit/wrapper/kv-store.js +0 -81
- package/gm-plugkit/wrapper/task-manager.js +0 -272
- package/gm-plugkit/wrapper/wasi-shim.js +0 -377
- package/gm-plugkit/wrapper/wasm-bridge.js +0 -71
package/gm-plugkit/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ const fs = require('fs');
|
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const cp = require('child_process');
|
|
8
|
-
const { ensureReady, startSpoolDaemon, gmToolsDir, readVersionFile, ensureGmPlugkitVersionFresh, ensureSkillMdFresh,
|
|
8
|
+
const { ensureReady, startSpoolDaemon, gmToolsDir, readVersionFile, ensureGmPlugkitVersionFresh, ensureSkillMdFresh, isReady, getWasmPath, readPinnedGmPlugkitVersion, spawnPinnedBoot, resolveProjectRoot } = require('./bootstrap');
|
|
9
9
|
const { pidAliveSync, waitForPidDeath } = require('./gm-process');
|
|
10
10
|
|
|
11
11
|
function getWasmPathSafe() {
|
|
@@ -60,10 +60,6 @@ Usage:
|
|
|
60
60
|
bun x gm-plugkit@latest --daemon Same as default
|
|
61
61
|
bun x gm-plugkit@latest --binary Print binary path only
|
|
62
62
|
bun x gm-plugkit@latest --status JSON status check
|
|
63
|
-
bun x gm-plugkit@latest --kill-stale-watchers
|
|
64
|
-
Kill plugkit watchers whose in-memory
|
|
65
|
-
wrapper sha differs from on-disk
|
|
66
|
-
(lets new wrapper code load on next bootstrap)
|
|
67
63
|
bun x gm-plugkit@latest --help Show this help
|
|
68
64
|
|
|
69
65
|
Opt-in pinned fast-path (skips bunx's own @latest npm-registry resolution):
|
|
@@ -83,111 +79,6 @@ Opt-in pinned fast-path (skips bunx's own @latest npm-registry resolution):
|
|
|
83
79
|
unaffected.
|
|
84
80
|
`;
|
|
85
81
|
|
|
86
|
-
function readDiskWasmVersion() {
|
|
87
|
-
try {
|
|
88
|
-
const versionFile = path.join(gmToolsDir(), 'plugkit.version');
|
|
89
|
-
return fs.readFileSync(versionFile, 'utf-8').trim() || null;
|
|
90
|
-
} catch (_) { return null; }
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function readWatcherInstanceVersion(pid) {
|
|
94
|
-
try {
|
|
95
|
-
const ps = process.platform === 'win32'
|
|
96
|
-
? `(Get-WmiObject Win32_Process -Filter "ProcessId=${pid}").CommandLine`
|
|
97
|
-
: null;
|
|
98
|
-
if (!ps) return null;
|
|
99
|
-
const out = cp.execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', ps], { encoding: 'utf-8', windowsHide: true });
|
|
100
|
-
const m = out.match(/([A-Z]:\\[^"\s]+\.gm[\\/]exec-spool)/i);
|
|
101
|
-
if (!m) return null;
|
|
102
|
-
const statusPath = path.join(m[1].replace(/[\\/]exec-spool.*$/, ''), 'exec-spool', '.status.json');
|
|
103
|
-
if (!fs.existsSync(statusPath)) return null;
|
|
104
|
-
const status = JSON.parse(fs.readFileSync(statusPath, 'utf-8'));
|
|
105
|
-
return status && status.instance_version ? status.instance_version : null;
|
|
106
|
-
} catch (_) { return null; }
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function killStaleWatchers() {
|
|
110
|
-
try {
|
|
111
|
-
const wrapperPath = path.join(gmToolsDir(), 'plugkit-wasm-wrapper.js');
|
|
112
|
-
if (!fs.existsSync(wrapperPath)) {
|
|
113
|
-
console.log(JSON.stringify({ ok: false, error: `wrapper not installed at ${wrapperPath}` }));
|
|
114
|
-
return 1;
|
|
115
|
-
}
|
|
116
|
-
const diskMtime = fs.statSync(wrapperPath).mtimeMs;
|
|
117
|
-
const diskWasmVersion = readDiskWasmVersion();
|
|
118
|
-
let localStatus = null;
|
|
119
|
-
try {
|
|
120
|
-
const localStatusPath = path.join(process.cwd(), '.gm', 'exec-spool', '.status.json');
|
|
121
|
-
if (fs.existsSync(localStatusPath)) localStatus = JSON.parse(fs.readFileSync(localStatusPath, 'utf-8'));
|
|
122
|
-
} catch (_) { localStatus = null; }
|
|
123
|
-
const stale = [];
|
|
124
|
-
const fresh = [];
|
|
125
|
-
function consider(pid, startedMs) {
|
|
126
|
-
const reasons = [];
|
|
127
|
-
if (startedMs < diskMtime) reasons.push('wrapper-mtime');
|
|
128
|
-
let instV = readWatcherInstanceVersion(pid);
|
|
129
|
-
if (!instV && localStatus && localStatus.pid === pid) {
|
|
130
|
-
instV = localStatus.instance_version || localStatus.version || null;
|
|
131
|
-
}
|
|
132
|
-
if (diskWasmVersion && instV && instV !== diskWasmVersion) reasons.push(`wasm-drift:${instV}->${diskWasmVersion}`);
|
|
133
|
-
if (reasons.length > 0) stale.push({ pid, started_ms: startedMs, instance_version: instV, reasons });
|
|
134
|
-
else fresh.push({ pid, started_ms: startedMs, instance_version: instV });
|
|
135
|
-
}
|
|
136
|
-
if (process.platform === 'win32') {
|
|
137
|
-
const ps = `Get-WmiObject Win32_Process -Filter "name='node.exe' OR name='bun.exe'" | Where-Object { $_.CommandLine -match 'plugkit-wasm-wrapper' } | ForEach-Object { $_.ProcessId.ToString() + '|' + $_.CreationDate }`;
|
|
138
|
-
const out = cp.execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', ps], { encoding: 'utf-8', windowsHide: true });
|
|
139
|
-
for (const line of out.split(/\r?\n/).filter(Boolean)) {
|
|
140
|
-
const [pidStr, creation] = line.split('|');
|
|
141
|
-
const pid = parseInt(pidStr, 10);
|
|
142
|
-
if (!Number.isFinite(pid)) continue;
|
|
143
|
-
const m = creation && creation.match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(?:\.(\d+))?(?:([+-])(\d+))?/);
|
|
144
|
-
if (!m) continue;
|
|
145
|
-
const localMs = new Date(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6], m[7] ? Math.round(+('0.' + m[7]) * 1000) : 0).getTime();
|
|
146
|
-
consider(pid, localMs);
|
|
147
|
-
}
|
|
148
|
-
} else {
|
|
149
|
-
const out = cp.execFileSync('ps', ['-eo', 'pid,lstart,command'], { encoding: 'utf-8' });
|
|
150
|
-
for (const line of out.split('\n').slice(1)) {
|
|
151
|
-
if (!line.includes('plugkit-wasm-wrapper')) continue;
|
|
152
|
-
const m = line.match(/^\s*(\d+)\s+(.+?\d{4})\s+/);
|
|
153
|
-
if (!m) continue;
|
|
154
|
-
const pid = parseInt(m[1], 10);
|
|
155
|
-
const start = Date.parse(m[2]);
|
|
156
|
-
if (!Number.isFinite(pid) || !Number.isFinite(start)) continue;
|
|
157
|
-
consider(pid, start);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
const killed = [];
|
|
161
|
-
const failed = [];
|
|
162
|
-
for (const s of stale) {
|
|
163
|
-
try {
|
|
164
|
-
if (process.platform === 'win32') {
|
|
165
|
-
cp.execFileSync('taskkill', ['/F', '/T', '/PID', String(s.pid)], { stdio: 'ignore', windowsHide: true });
|
|
166
|
-
} else {
|
|
167
|
-
process.kill(s.pid, 'SIGTERM');
|
|
168
|
-
}
|
|
169
|
-
killed.push(s.pid);
|
|
170
|
-
} catch (e) {
|
|
171
|
-
failed.push({ pid: s.pid, error: e.message });
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
console.log(JSON.stringify({
|
|
175
|
-
ok: true,
|
|
176
|
-
disk_wrapper_mtime_ms: diskMtime,
|
|
177
|
-
disk_wasm_version: diskWasmVersion,
|
|
178
|
-
stale_found: stale.length,
|
|
179
|
-
fresh_found: fresh.length,
|
|
180
|
-
stale_detail: stale,
|
|
181
|
-
killed,
|
|
182
|
-
failed,
|
|
183
|
-
}, null, 2));
|
|
184
|
-
return 0;
|
|
185
|
-
} catch (e) {
|
|
186
|
-
console.log(JSON.stringify({ ok: false, error: e.message }));
|
|
187
|
-
return 1;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
82
|
function spoolDir() {
|
|
192
83
|
const projectDir = resolveProjectRoot(process.env.CLAUDE_PROJECT_DIR || process.cwd());
|
|
193
84
|
return path.join(projectDir, '.gm', 'exec-spool');
|
|
@@ -230,38 +121,28 @@ function writeCliError(phase, err) {
|
|
|
230
121
|
} catch (_) {}
|
|
231
122
|
}
|
|
232
123
|
|
|
233
|
-
// agentplug-runner is the
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
// 6-platform parity.)
|
|
124
|
+
// agentplug-runner is the SOLE spool loader -- a native wasmtime binary that
|
|
125
|
+
// loads gm.wasm as one plugin alongside its shared libsql/bert/treesitter
|
|
126
|
+
// plugins and serves the full spool ABI (in/out layout, verb names, browser
|
|
127
|
+
// via direct CDP, task via a native registry). When it is installed, delegate
|
|
128
|
+
// to it immediately and exit before any bun/node bootstrap runs. The JS
|
|
129
|
+
// wasm-host was retired; there is no pure-JS fallback anymore. If no runner is
|
|
130
|
+
// installed, the bootstrap path below downloads the wasm and startSpoolDaemon()
|
|
131
|
+
// either launches the runner or fails loudly with an actionable message
|
|
132
|
+
// (there is no silent no-loader state).
|
|
243
133
|
function tryDelegateToRunner(args) {
|
|
244
134
|
if (process.env.GM_PLUGKIT_NO_RUNNER_DELEGATE === '1') return false;
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (!fs.existsSync(runnerPath)) continue;
|
|
255
|
-
try {
|
|
256
|
-
const result = cp.spawnSync(runnerPath, args, { stdio: 'inherit', windowsHide: true });
|
|
257
|
-
if (result.error) continue; // this candidate genuinely failed to start -- try the next one
|
|
258
|
-
process.exit(typeof result.status === 'number' ? result.status : 0);
|
|
259
|
-
} catch (_) {
|
|
260
|
-
continue;
|
|
261
|
-
}
|
|
262
|
-
return true;
|
|
135
|
+
const exeName = process.platform === 'win32' ? 'agentplug-runner.exe' : 'agentplug-runner';
|
|
136
|
+
const runnerPath = path.join(gmToolsDir(), exeName);
|
|
137
|
+
if (!fs.existsSync(runnerPath)) return false;
|
|
138
|
+
try {
|
|
139
|
+
const result = cp.spawnSync(runnerPath, args, { stdio: 'inherit', windowsHide: true });
|
|
140
|
+
if (result.error) return false; // genuinely failed to start -- fall through to bootstrap+relaunch
|
|
141
|
+
process.exit(typeof result.status === 'number' ? result.status : 0);
|
|
142
|
+
} catch (_) {
|
|
143
|
+
return false;
|
|
263
144
|
}
|
|
264
|
-
return
|
|
145
|
+
return true;
|
|
265
146
|
}
|
|
266
147
|
|
|
267
148
|
(async () => {
|
|
@@ -274,10 +155,6 @@ function tryDelegateToRunner(args) {
|
|
|
274
155
|
|
|
275
156
|
tryDelegateToRunner(args);
|
|
276
157
|
|
|
277
|
-
if (args.includes('--kill-stale-watchers')) {
|
|
278
|
-
process.exit(killStaleWatchers());
|
|
279
|
-
}
|
|
280
|
-
|
|
281
158
|
const wantsPinned = process.env.GM_PLUGKIT_PREFER_PINNED === '1' || args.includes('--pinned');
|
|
282
159
|
const alreadyReexecedPinned = process.env.GM_PLUGKIT_PINNED_REEXEC === '1';
|
|
283
160
|
if (wantsPinned && !alreadyReexecedPinned) {
|
|
@@ -311,29 +188,16 @@ function tryDelegateToRunner(args) {
|
|
|
311
188
|
try { ensureGmPlugkitVersionFresh(); } catch (_) {}
|
|
312
189
|
let skillRefresh = null;
|
|
313
190
|
try { skillRefresh = ensureSkillMdFresh(); } catch (_) {}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
} else {
|
|
325
|
-
writeCliStatus({ phase: 'ready', already_serving: true, watcher_pid: already.pid });
|
|
326
|
-
console.log(JSON.stringify({
|
|
327
|
-
ok: true,
|
|
328
|
-
already_serving: true,
|
|
329
|
-
watcher_pid: already.pid,
|
|
330
|
-
version: already.version,
|
|
331
|
-
skills_refreshed: skillRefresh && skillRefresh.refreshed || [],
|
|
332
|
-
wrapper_refreshed: false,
|
|
333
|
-
message: 'plugkit already serving, no bootstrap/spawn needed',
|
|
334
|
-
}));
|
|
335
|
-
process.exit(0);
|
|
336
|
-
}
|
|
191
|
+
writeCliStatus({ phase: 'ready', already_serving: true, watcher_pid: already.pid });
|
|
192
|
+
console.log(JSON.stringify({
|
|
193
|
+
ok: true,
|
|
194
|
+
already_serving: true,
|
|
195
|
+
watcher_pid: already.pid,
|
|
196
|
+
version: already.version,
|
|
197
|
+
skills_refreshed: skillRefresh && skillRefresh.refreshed || [],
|
|
198
|
+
message: 'plugkit already serving, no bootstrap/spawn needed',
|
|
199
|
+
}));
|
|
200
|
+
process.exit(0);
|
|
337
201
|
}
|
|
338
202
|
if (versionDrifted) {
|
|
339
203
|
const targetVersion = remoteVersionDrifted ? remoteUpdate.latest : onDiskVersion;
|
|
@@ -347,8 +211,7 @@ function tryDelegateToRunner(args) {
|
|
|
347
211
|
waitForPidDeath(already.pid, 5000);
|
|
348
212
|
}
|
|
349
213
|
|
|
350
|
-
|
|
351
|
-
if (isReady() && fs.existsSync(wrapperPath)) {
|
|
214
|
+
if (isReady()) {
|
|
352
215
|
let installedVersion = null;
|
|
353
216
|
try { installedVersion = readVersionFile(); } catch (_) { installedVersion = null; }
|
|
354
217
|
writeCliStatus({ phase: 'bootstrapped', version: installedVersion, binary: getWasmPathSafe() });
|
package/gm-plugkit/package.json
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gm-plugkit",
|
|
3
|
-
"version": "2.0.
|
|
4
|
-
"description": "Bootstrap and daemon-spawn tool for gm plugkit binary. Downloads the correct platform
|
|
3
|
+
"version": "2.0.2026",
|
|
4
|
+
"description": "Bootstrap and daemon-spawn tool for gm plugkit binary. Downloads the correct platform wasm, verifies SHA256, and launches agentplug-runner (the native wasm host) as the spool watcher daemon.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"gm-plugkit": "./cli.js"
|
|
8
|
-
"plugkit-wasm-wrapper": "./plugkit-wasm-wrapper.js"
|
|
7
|
+
"gm-plugkit": "./cli.js"
|
|
9
8
|
},
|
|
10
9
|
"files": [
|
|
11
10
|
"cli.js",
|
|
12
11
|
"index.js",
|
|
13
12
|
"bootstrap.js",
|
|
14
|
-
"plugkit-wasm-wrapper.js",
|
|
15
13
|
"gm-log.js",
|
|
16
14
|
"gm-process.js",
|
|
17
15
|
"plugkit.version",
|
package/gm.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gm-skill",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2026",
|
|
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",
|
|
@@ -41,14 +41,12 @@
|
|
|
41
41
|
"gm-plugkit/cli.js",
|
|
42
42
|
"gm-plugkit/index.js",
|
|
43
43
|
"gm-plugkit/package.json",
|
|
44
|
-
"gm-plugkit/plugkit-wasm-wrapper.js",
|
|
45
44
|
"gm-plugkit/gm-log.js",
|
|
46
45
|
"gm-plugkit/gm-process.js",
|
|
47
46
|
"gm-plugkit/plugkit.version",
|
|
48
47
|
"gm-plugkit/plugkit.sha256",
|
|
49
48
|
"gm-plugkit/plugkit-slim.wasm.sha256",
|
|
50
49
|
"gm-plugkit/instructions/",
|
|
51
|
-
"gm-plugkit/wrapper/",
|
|
52
50
|
"AGENTS.md",
|
|
53
51
|
"README.md",
|
|
54
52
|
"gm.json"
|
package/skills/gm/SKILL.md
CHANGED
|
@@ -62,7 +62,7 @@ bun x gm-plugkit@latest spool
|
|
|
62
62
|
|
|
63
63
|
(`npx -y gm-plugkit@latest spool` if no `bun`.) Fire-and-forget: spawns the detached daemon and returns immediately (already-alive watcher also returns at once, unchanged) -- it does NOT wait for the watcher to confirm serving. No `&`, no `sleep`, no re-`cat`; write your first verb to `in/` right after it returns. A first-read "file does not exist" on that verb is normal (the just-spawned watcher hasn't noticed the file yet) -- re-Read next message, same as any dead-watcher-adjacent recheck. If you need to actively confirm serving before dispatching (rare), read `.gm/exec-spool/.status.json` yourself and check `ts` freshness.
|
|
64
64
|
|
|
65
|
-
**The boot line resolves to
|
|
65
|
+
**The boot line resolves to agentplug-runner, the sole spool loader; `bun x gm-plugkit@latest spool` is the thin launcher that re-execs into it.** `gm-plugkit/cli.js::tryDelegateToRunner` execs `~/.gm-tools/agentplug-runner` -- a native wasmtime binary that loads gm.wasm alongside shared `bert`/`libsql`/`treesitter` plugins (so gm runs `plugkit-slim.wasm`, browser via direct CDP, task via a native registry); `.status.json` `runtime` reads `agentplug` when it serves. The JS wasm-host (`plugkit-wasm-wrapper.js`) and the redundant `gm-runner` native host are retired -- there is no fallback loader. `bin/install.js` hard-requires agentplug-runner: it downloads the sha256-verified binary from `AnEntrypoint/agentplug-bin` for the host platform and fails loudly if none is published (or the download fails) rather than leaving no loader. agentplug-runner auto-updates only its served `plugkit.wasm` (600s poll), not its own executable (that self-updates on the next `bin/install.js` re-run, and it adopts a staged `.new` on next start).
|
|
66
66
|
|
|
67
67
|
The `Resolving dependencies` / `Saved lockfile` chatter before the JSON payload is `bunx` re-resolving the `@latest` tag against the registry, not gm-plugkit hanging -- the daemon already spawns detached+unref'd and the CLI itself exits the instant that happens; the visible delay is entirely bunx's own network round-trip, unavoidable on `@latest` (a pinned exact version, once bunx-cached, skips it). `GM_PLUGKIT_SKIP_SELF_STALE_CHECK=1` skips the CLI's own redundant npm-registry version probe (already covered by `@latest`'s resolution) for a faster boot on repeat same-session invocations. In PowerShell 5.1, never `2>&1`-redirect this command into another cmdlet (e.g. `| Select-Object`) -- PowerShell wraps every stderr line from a native exe in a `NativeCommandError` record and reports failure even on exit 0, turning bun's routine stderr progress output into a misleading red error block; run it bare or capture stdout only.
|
|
68
68
|
|
|
@@ -72,7 +72,7 @@ The `Resolving dependencies` / `Saved lockfile` chatter before the JSON payload
|
|
|
72
72
|
|
|
73
73
|
**Waiting out a real `busy_until` window (a long `browser`/`exec_js` dispatch genuinely still running) is not the same thing as the banned bare-`sleep`-then-`ls` poll above -- the ban is on blind, unconditional sleeping when nothing indicates work is actually in flight; a live `busy_until` in the future is exactly the condition that licenses a bounded wait for it.** On a host with a background-monitor primitive (a tool that runs a shell condition and notifies on completion rather than blocking the turn), the correct shape is a condition loop over the OUT file's existence, not a fixed-duration sleep: `until [ -f .gm/exec-spool/out/<verb>-<N>.json ]; do sleep 2; done` handed to that primitive, so the wait happens off-turn and a completion notification (not a guessed delay) resumes work. On a host with no such primitive, a single bounded `sleep` no longer than the dispatch's own declared `busy_until` remaining-window (never an arbitrary guess, never chained/repeated past that one wait) is the fallback -- re-check `.status.json`'s `ts`/`busy_until` once after it elapses, do not loop blind sleeps. Either way: never poll faster than realistic verb latency (sub-second loops are themselves a form of the banned pattern), and never fall back to declaring the watcher dead just because a wait felt long -- dead is defined structurally (stale `ts` AND no future `busy_until`), not by elapsed wall-clock alone.
|
|
74
74
|
|
|
75
|
-
**Reboot-loop escape (watcher dies ~30-90s after every boot).** If a fresh `bun x gm-plugkit@latest spool` boots but the watcher dies again shortly after (heartbeat `ts` goes stale >30s with no future `busy_until`, then a new pid appears, repeatedly), the on-disk index has not finished embedding and each boot re-triggers the same synchronous code-index embed that can block the heartbeat past the supervisor's 30s stale limit (`STATUS_STALE_MS`). Confirm by reading `.gm/exec-spool/.watcher.log` for repeated `codeinsight_rebuild` + `partial pass (wall budget) ... deferred_files=N` lines whose `deferred_files` never reaches 0. The
|
|
75
|
+
**Reboot-loop escape (watcher dies ~30-90s after every boot).** If a fresh `bun x gm-plugkit@latest spool` boots but the watcher dies again shortly after (heartbeat `ts` goes stale >30s with no future `busy_until`, then a new pid appears, repeatedly), the on-disk index has not finished embedding and each boot re-triggers the same synchronous code-index embed that can block the heartbeat past the supervisor's 30s stale limit (`STATUS_STALE_MS`). Confirm by reading `.gm/exec-spool/.watcher.log` for repeated `codeinsight_rebuild` + `partial pass (wall budget) ... deferred_files=N` lines whose `deferred_files` never reaches 0. The embed is genuinely converging in that case: do NOT immediately re-boot on the first stale reading -- read `.watcher.log`, and as long as `deferred_files` is strictly decreasing across `codeinsight_index_partial` events the index is converging (each accepted verb advances it one wall-budget); give it repeated single verbs until a `code_index: done` / `deferred_files=0` line appears, then normal dispatch resumes. Rebooting mid-convergence resets this progress -- the loop is the reboot, not the embed.
|
|
76
76
|
|
|
77
77
|
**Apparent tooling failure is never grounds to ask the user, never a blind restart.** "Spooler not working" / missing response / stale watcher / `gm_plugkit_stale` flagged in a response = your own mechanical self-recovery: honor a future `busy_until` (wait), else boot + re-dispatch. You have boot authority; asking the user to do what a verb can do is a deviation. Staleness of any kind (stale watcher version, stale served prose vs published source) is itself a deviation to resolve immediately, the same turn it's noticed -- `bun x gm-plugkit@latest spool` first, before any other work.
|
|
78
78
|
|
|
@@ -90,11 +90,11 @@ The `Resolving dependencies` / `Saved lockfile` chatter before the JSON payload
|
|
|
90
90
|
|
|
91
91
|
**Historical note, resolved (2026-07-17, gm-plugkit source fix committed on `AnEntrypoint/gm` main):** the GL-error dedup/stack-trace/`gpu_hint` improvements described in the paragraph above were themselves discovered as real, live-hit debugging-productivity gaps -- not designed speculatively -- while root-causing a game-engine FPS regression: the pre-2026-07-17 `window.__gmGlErrors` cap counted raw OCCURRENCES (first 40, ever, per page load), so a GL error firing every single frame filled the array within the first second and every subsequent `browser` dispatch for the rest of a many-minutes debugging session read back the exact same frozen entries, making a still-firing error look capped/stale/resolved. Fixed to the per-signature dedup table described above. If a future session again observes `debug.gl.errors` looking suspiciously static across several dispatches spanning real wall-clock time, that is the SAME class of bug recurring somewhere else in the capture pipeline (not user error) -- check `debug.gl.errorTotalCount` first (it is never capped): a growing total against a static `errors` array length means a NEW dedup-adjacent bug, not a fixed one regressing.
|
|
92
92
|
|
|
93
|
-
If similar symptoms recur (stub-like responses, silent truncation, debug fields always empty), do NOT re-add a stale-bug workaround section here -- instead root-cause in the real
|
|
93
|
+
If similar symptoms recur (stub-like responses, silent truncation, debug fields always empty), do NOT re-add a stale-bug workaround section here -- instead root-cause in the real browser-host source, which is now native in agentplug (clone `AnEntrypoint/agentplug`, edit `crates/agentplug-host/src/browser.rs` and its embedded `cdp_eval.js`, rebuild agentplug-runner, verify live against the locally-built `~/.gm-tools/agentplug-runner`, then commit+push to `AnEntrypoint/agentplug` main so the fix ships through the agentplug-bin release path) and update this section with the real fix, the same discipline used for the bugs above.
|
|
94
94
|
|
|
95
95
|
**Windows-specific transient flakiness that is NOT a plugkit/wrapper bug, just retry it:** `bun x <pkg>@<version>` occasionally hangs indefinitely at "Resolving dependencies" with zero further output even with a pinned exact cached version (a known intermittent Bun/Windows dependency-resolution stall, reproducible with plain `bun x playwriter@<version> session list` outside any wrapper). A bare re-dispatch of the exact same body, or a fresh direct CLI retry, has resolved it every time observed. Do not chase this as a code bug -- 2-3 retries is the correct response, same bound as the raw-CLI fragility note below.
|
|
96
96
|
|
|
97
|
-
**The browser verb drives Chrome directly over CDP -- the playwriter relay is
|
|
97
|
+
**The browser verb drives Chrome directly over CDP, native in agentplug-host -- the playwriter relay is not on the eval path.** agentplug-runner launches Chrome with `--remote-debugging-port`, obtains a live DevTools `webSocketDebuggerUrl`, and runs the script in-page via `Runtime.evaluate` over that websocket directly (`crates/agentplug-host/src/browser.rs` + its embedded `cdp_eval.js`), so no relay attach+eval is ever spawned. This is the standing browser eval path. A flaky browser witness is therefore a real bug to root-cause in the CDP path or the Chrome launch, never a `blockedBy: external` tooling gap -- everything is fixable: if a browser dispatch fails, read the CDP endpoint (`http://127.0.0.1:<port>/json/version`), check the Chrome launch args, and fix the actual failure rather than parking it. There is no accepted "external tooling gap" resolution for the browser verb.
|
|
98
98
|
|
|
99
99
|
Spool input from PowerShell must be UTF-8 no-BOM (`-Encoding utf8` or `[System.IO.File]::WriteAllText`); UTF-16+BOM causes `spool.body-encoding-recoded`. First-turn body is `{"prompt":"<user request>"}` (derives orient_nouns + recall_hits); later turns may use `{}`. Batch independent dispatches: multiple `prd-add`, `prd-resolve`, `mutable-add`, `recall`+`codesearch`, or inspection `Read` calls in a single tool block. Avoid editing the same file twice in one block; collapse changes into a single Edit.
|
|
100
100
|
|