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/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, ensureWrapperFresh, isReady, getWasmPath, readPinnedGmPlugkitVersion, spawnPinnedBoot, resolveProjectRoot } = require('./bootstrap');
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 sole native host and a sha256-verified drop-in
234
- // replacement for this entire bun/node boot path -- when it's installed,
235
- // delegate to it immediately and exit, before any bun/node bootstrap runs.
236
- // It serves the identical spool ABI (same in/out layout, same verb names;
237
- // gm.wasm is one of its loadable plugins alongside libsql/bert/treesitter),
238
- // so bun/node are only ever exercised as the pure-JS fallback on a platform
239
- // agentplug-bin has no published binary for, or during the one-time install
240
- // before the runner lands on disk. (gm-runner, a strictly-inferior
241
- // single-module duplicate, was retired once agentplug-runner reached full
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
- // agentplug-runner is the sole native host now -- gm-runner (a strictly
246
- // inferior single-module duplicate) was retired once agentplug-runner had
247
- // full 6-platform parity published to agentplug-bin. The JS wasm-host below
248
- // is the only fallback, for a platform with no published native binary.
249
- const candidates = process.platform === 'win32'
250
- ? ['agentplug-runner.exe']
251
- : ['agentplug-runner'];
252
- for (const exeName of candidates) {
253
- const runnerPath = path.join(gmToolsDir(), exeName);
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 false;
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
- let wrapperRefreshed = false;
315
- try { wrapperRefreshed = ensureWrapperFresh(); } catch (_) {}
316
- if (wrapperRefreshed) {
317
- writeCliStatus({ phase: 'wrapper-drift-detected', reason: 'on-disk-wrapper-refreshed', running_pid: already.pid });
318
- console.error(`[gm-plugkit] running watcher (pid=${already.pid}) serves a stale wrapper; on-disk copy just refreshed -- forcing reboot`);
319
- try {
320
- if (process.platform === 'win32') cp.execFileSync('taskkill', ['/F', '/T', '/PID', String(already.pid)], { stdio: 'ignore', windowsHide: true });
321
- else process.kill(already.pid, 'SIGTERM');
322
- } catch (_) {}
323
- waitForPidDeath(already.pid, 5000);
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
- const wrapperPath = path.join(gmToolsDir(), 'plugkit-wasm-wrapper.js');
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() });
@@ -1,17 +1,15 @@
1
1
  {
2
2
  "name": "gm-plugkit",
3
- "version": "2.0.2025",
4
- "description": "Bootstrap and daemon-spawn tool for gm plugkit binary. Downloads the correct platform binary, verifies SHA256, and starts the spool watcher daemon. Includes plugkit-wasm-wrapper for WASM-based spool watching.",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm",
3
- "version": "2.0.2025",
3
+ "version": "2.0.2026",
4
4
  "description": "Spool-dispatch orchestration engine with unified state machine, skills, and automated git enforcement",
5
5
  "author": "AnEntrypoint",
6
6
  "license": "MIT",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-skill",
3
- "version": "2.0.2025",
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"
@@ -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 a native runner when one is installed; `bun x gm-plugkit@latest spool` is the thin launcher that re-execs into it.** `gm-plugkit/cli.js::tryDelegateToRunner` picks the host in this precedence: (1) **`agentplug-runner`** (`~/.gm-tools/agentplug-runner`) -- the LIVE, preferred host, 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. (2) **`gm-runner`** (`~/.gm-tools/gm-runner`) -- native fallback, a single-module subset of agentplug-runner. (3) the JS wasm-host in `plugkit-wasm-wrapper.js` -- last resort when no native runner is published for the platform. All three serve the identical spool ABI, so a session works the same regardless. `bin/install.js` best-effort-downloads both native runners (sha256-verified from `AnEntrypoint/agentplug-bin` / `gm-runner-bin`) and hard-requires neither; each native 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; agentplug-runner also adopts a staged `.new` on next start). You never pick the host by hand -- `bun x gm-plugkit@latest spool` always resolves to the best available one.
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 durable fix has shipped: every `bun x gm-plugkit@latest spool` boot calls `ensureWrapperFresh()` (`gm-plugkit/bootstrap.js`), which sha256-compares the installed `~/.gm-tools/plugkit-wasm-wrapper.js` against the bundled one and atomically force-replaces it on any mismatch (lock-guarded, tmp-write-then-rename, safe under concurrent per-project watchers) -- a stale installed wrapper self-heals on the very next boot, no manual reinstall needed. If the loop persists even on a freshly-synced wrapper, the embed itself is still genuinely converging: 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.
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 `gm-plugkit` source (clone `AnEntrypoint/gm`, edit `gm-plugkit/plugkit-wasm-wrapper.js`, verify live against a locally-copied `~/.gm-tools/plugkit-wasm-wrapper.js` + direct `bun ~/.gm-tools/plugkit-wasm-wrapper.js spool` invocation to bypass the npm-package re-fetch overwriting local edits, then commit+push to `AnEntrypoint/gm` main so the fix ships through the real CI/CD cascading-update path) and update this section with the real fix, the same discipline used for the bugs above.
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 no longer on the eval path.** The `UV_HANDLE_CLOSING` native crash in playwriter's own relay process on Windows ("Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\\win\\async.c, line 76") was resolved by removing the dependency on that relay: the wrapper already launches Chrome with `--remote-debugging-port` and obtains a live DevTools `webSocketDebuggerUrl` (playwriter-free), and `gm-plugkit/wrapper/cdp-eval.js` now runs the script in-page via `Runtime.evaluate` over that websocket directly, so the crashing relay attach+eval is never 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.
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