gm-skill 2.0.1619 → 2.0.1621
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 +1 -1
- package/gm-plugkit/instructions/browser.md +2 -2
- package/gm-plugkit/package.json +1 -1
- package/gm-plugkit/plugkit-wasm-wrapper.js +61 -23
- package/gm.json +1 -1
- package/package.json +1 -1
- package/skills/gm/SKILL.md +1 -1
package/AGENTS.md
CHANGED
|
@@ -72,7 +72,7 @@ Record only non-obvious technical caveats that cost multiple runs to discover; r
|
|
|
72
72
|
|
|
73
73
|
No build step; the repo root is the published artifact. `npm publish` from root publishes `gm-skill` (npm package id is permanent; only the skill DIRECTORY is `skills/gm`, so the command is `/gm`). `package.json` `files:` pins the shipped paths. `AnEntrypoint/gm-skill` is a back-compat mirror receiving only `skills/gm/SKILL.md` per release.
|
|
74
74
|
|
|
75
|
-
`bin/install.js` is the canonical installer
|
|
75
|
+
`bin/install.js` is the canonical installer (no npx `skills` library, no marketplace); the dir name it lands IS the `/command`, and `test.js checkRenameAndInstaller()` is the structural guard. Copy-target, the four Claude Code settings it sets non-interactively, the reasoning-in-code framing, and the guard assertions in rs-learn (`recall: gm installer detail`).
|
|
76
76
|
|
|
77
77
|
## The agent is the orchestrator; plugkit is the brain it drives
|
|
78
78
|
|
|
@@ -26,7 +26,7 @@ capture\n<expression>
|
|
|
26
26
|
profile\n<expression>
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
**Open on the page you want to test, not a blank one.** A bare `https://...` URL body navigates the session straight to that page and returns `{url, title}` -- the simplest "show me this page." `url=<url>\n<expression>` navigates first, then runs your expression on the loaded page, so the global/DOM you assert is already there in one dispatch instead of a blank surface you must `page.goto` yourself. `url=` composes with `timeout=` and `capture` -- stack the prefix lines in order `timeout=`, then `url=`, then `capture`, the expression last; the prepended `page.goto` rides inside the capture so its navigation console/network is captured too. A bare expression with no URL prefix
|
|
29
|
+
**Open on the page you want to test, not a blank one.** A bare `https://...` URL body navigates the session straight to that page and returns `{url, title}` -- the simplest "show me this page." `url=<url>\n<expression>` navigates first, then runs your expression on the loaded page, so the global/DOM you assert is already there in one dispatch instead of a blank surface you must `page.goto` yourself. `url=` composes with `timeout=` and `capture` -- stack the prefix lines in order `timeout=`, then `url=`, then `capture`, the expression last; the prepended `page.goto` rides inside the capture so its navigation console/network is captured too. A bare expression with no `url=`/bare-URL prefix runs against whatever the session is already on -- a never-navigated session is on `about:blank`, so the expression evaluates an empty page and the envelope comes back with `landed_on_blank: true` and a `hint` telling you to add `url=`; navigate first and the surprise never happens. `session new` returns the id you carry. (`session close` and `session kill` are aliases.) Default per-eval timeout 120000ms; operations that legitimately exceed it prefix `timeout=<ms>\n` (wrapper clamps to 120000ms). The response carries `timeout_ms_used`; `browser.runner-timeout` fires at the cap -- read `stderr`, narrow or raise, never retry blind at the same budget.
|
|
30
30
|
|
|
31
31
|
**`capture\n<expression>` is the zero-boilerplate debug path -- prefer it.** Prefix your script with `capture` (or `profile`) on its own line and the wrapper auto-attaches `page.on('console'|'pageerror'|'requestfinished')` before your code runs, runs your script in an async wrapper (your top-level `await`/`return` work unchanged), and returns `{result: <your return>, debug: {console, pageErrors, network, performance}}` -- page console logs, uncaught errors, per-request network timing, and navigation performance, captured for free. Combine with timeout via `timeout=<ms>\ncapture\n<expr>`. Use the bare expression only when you do not want the capture overhead.
|
|
32
32
|
|
|
@@ -34,7 +34,7 @@ profile\n<expression>
|
|
|
34
34
|
|
|
35
35
|
## Envelope
|
|
36
36
|
|
|
37
|
-
`{ok, stdout, stderr, exit_code, session_id?}`. `stdout` = stringified eval result; `stderr` = page errors + launch diagnostics; `exit_code` non-zero = the dispatch did not land -- read `stderr` and re-dispatch, never blind.
|
|
37
|
+
`{ok, stdout, stderr, exit_code, session_id?, navigation_requested, landed_on_blank?, hint?}`. `stdout` = stringified eval result; `stderr` = page errors + launch diagnostics; `exit_code` non-zero = the dispatch did not land -- read `stderr` and re-dispatch, never blind. `navigation_requested` reflects whether the dispatch carried a `url=`/bare-URL navigation; `landed_on_blank: true` with a `hint` means the expression ran against `about:blank` -- prefix `url=<target>` and re-dispatch.
|
|
38
38
|
|
|
39
39
|
## Headed by default
|
|
40
40
|
|
package/gm-plugkit/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gm-plugkit",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1621",
|
|
4
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.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -163,27 +163,19 @@ function dispatchVerbToWasmInternal(instance, verb, body) {
|
|
|
163
163
|
if (!dispatch) return null;
|
|
164
164
|
const verbBytes = new TextEncoder().encode(verb);
|
|
165
165
|
const bodyBytes = new TextEncoder().encode(body || '');
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
166
|
+
// writeWasmInput re-reads memory.buffer fresh after each alloc (avoids the detached-buffer write bug).
|
|
167
|
+
let verbPtr = 0, bodyPtr = 0;
|
|
168
|
+
try { verbPtr = writeWasmInput(instance, verbBytes, `dispatch_verb(${verb}).verb`); }
|
|
169
|
+
catch (e) { throw new Error(`wasm-alloc-failed for dispatch_verb(${verb}): ${e.message}`); }
|
|
170
|
+
try { bodyPtr = writeWasmInput(instance, bodyBytes, `dispatch_verb(${verb}).body`); }
|
|
171
|
+
catch (e) { try { if (verbPtr) instance.exports.plugkit_free(verbPtr, verbBytes.length); } catch (_) {}
|
|
172
|
+
throw new Error(`wasm-alloc-failed for dispatch_verb(${verb}): ${e.message}`); }
|
|
173
173
|
try {
|
|
174
|
-
new Uint8Array(instance.exports.memory.buffer, verbPtr, verbBytes.length).set(verbBytes);
|
|
175
|
-
new Uint8Array(instance.exports.memory.buffer, bodyPtr, bodyBytes.length).set(bodyBytes);
|
|
176
174
|
const result = dispatch(verbPtr, verbBytes.length, bodyPtr, bodyBytes.length);
|
|
177
|
-
|
|
178
|
-
const len = Number(result >> 32n);
|
|
179
|
-
const buffer = instance.exports.memory.buffer;
|
|
180
|
-
guardWasmRange(buffer, ptr, len, `dispatch_verb(${verb})`);
|
|
181
|
-
const out = new TextDecoder().decode(new Uint8Array(buffer, ptr, len));
|
|
182
|
-
try { instance.exports.plugkit_free(ptr, len); } catch (_) {}
|
|
183
|
-
return out;
|
|
175
|
+
return decodeWasmResult(instance, result, `dispatch_verb(${verb})`); // normalized i64 + fresh buffer
|
|
184
176
|
} finally {
|
|
185
|
-
try { instance.exports.plugkit_free(verbPtr, verbBytes.length); } catch (_) {}
|
|
186
|
-
try { instance.exports.plugkit_free(bodyPtr, bodyBytes.length); } catch (_) {}
|
|
177
|
+
try { if (verbPtr) instance.exports.plugkit_free(verbPtr, verbBytes.length); } catch (_) {}
|
|
178
|
+
try { if (bodyPtr) instance.exports.plugkit_free(bodyPtr, bodyBytes.length); } catch (_) {}
|
|
187
179
|
}
|
|
188
180
|
}
|
|
189
181
|
|
|
@@ -1383,6 +1375,41 @@ function guardWasmRange(buffer, ptr, len, where) {
|
|
|
1383
1375
|
}
|
|
1384
1376
|
}
|
|
1385
1377
|
|
|
1378
|
+
// Decode a packed (ptr,len) i64 dispatch result into a JS string, the ONE correct way.
|
|
1379
|
+
// Two bugs this consolidates (they only surface once the wasm memory grows past a threshold --
|
|
1380
|
+
// e.g. a large .gm state file -> a big plugkit_alloc -> the memory grows past ~2GB / the linear
|
|
1381
|
+
// memory is re-grown mid-dispatch):
|
|
1382
|
+
// 1. SIGNED i64 result. dispatch_verb returns an i64; a high bit set (large ptr or a packed
|
|
1383
|
+
// len in the top 32 bits) makes `result` a NEGATIVE BigInt. `result >> 32n` on a negative
|
|
1384
|
+
// BigInt arithmetic-shifts in sign bits -> a garbage/negative len, and the low-word mask can
|
|
1385
|
+
// misread too. Normalize to unsigned 64-bit FIRST: BigInt.asUintN(64, result).
|
|
1386
|
+
// 2. DETACHED buffer. `instance.exports.memory.buffer` captured before plugkit_alloc/dispatch is
|
|
1387
|
+
// a STALE ArrayBuffer once the wasm linear memory grows (the old buffer detaches). Reading the
|
|
1388
|
+
// result against it throws 'Start offset N is outside the bounds of the buffer'. Always re-read
|
|
1389
|
+
// instance.exports.memory.buffer FRESH at the moment of the view, never reuse a captured one.
|
|
1390
|
+
function decodeWasmResult(instance, result, where) {
|
|
1391
|
+
const u = BigInt.asUintN(64, BigInt(result)); // (1) normalize the i64 to unsigned before splitting
|
|
1392
|
+
const ptr = Number(u & 0xffffffffn);
|
|
1393
|
+
const len = Number(u >> 32n);
|
|
1394
|
+
if (ptr === 0 || len === 0) return '';
|
|
1395
|
+
const buffer = instance.exports.memory.buffer; // (2) FRESH buffer (post-grow), never a stale capture
|
|
1396
|
+
guardWasmRange(buffer, ptr, len, where);
|
|
1397
|
+
const out = new TextDecoder().decode(new Uint8Array(buffer, ptr, len));
|
|
1398
|
+
try { instance.exports.plugkit_free(ptr, len); } catch (_) {}
|
|
1399
|
+
return out;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
// Write input bytes into wasm memory, re-reading memory.buffer FRESH after the alloc so a memory
|
|
1403
|
+
// grow during plugkit_alloc never leaves us writing into a detached buffer (the write-side half of
|
|
1404
|
+
// the detached-buffer bug). Returns the ptr (caller frees) or throws on alloc failure.
|
|
1405
|
+
function writeWasmInput(instance, bytes, where) {
|
|
1406
|
+
if (bytes.length === 0) return 0;
|
|
1407
|
+
const ptr = instance.exports.plugkit_alloc(bytes.length);
|
|
1408
|
+
if (ptr === 0) throw new Error(`wasm-alloc-failed at ${where}: plugkit_alloc returned 0 (wasm OOM)`);
|
|
1409
|
+
new Uint8Array(instance.exports.memory.buffer, ptr, bytes.length).set(bytes); // fresh buffer post-alloc
|
|
1410
|
+
return ptr;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1386
1413
|
function readWasmBytes(instance, ptr, len) {
|
|
1387
1414
|
if (ptr === 0 || len === 0) return new Uint8Array(0);
|
|
1388
1415
|
const buffer = instance.exports.memory.buffer;
|
|
@@ -2127,6 +2154,7 @@ function makeHostFunctions(instanceRef) {
|
|
|
2127
2154
|
+ `page.on('pageerror',e=>{try{__errs.push(String(e&&e.message||e));}catch(_){}});`
|
|
2128
2155
|
+ `page.on('requestfinished',r=>{try{const t=r.timing();__net.push({url:String(r.url()).slice(0,120),dur_ms:Math.round(t.responseEnd),ttfb_ms:Math.round(t.responseStart)});}catch(_){}});}catch(_){}\n`;
|
|
2129
2156
|
const perfRead = `let __perf=null;try{__perf=await page.evaluate(()=>{const n=performance.getEntriesByType('navigation')[0];return n?{load_ms:Math.round(n.loadEventEnd||0),dcl_ms:Math.round(n.domContentLoadedEventEnd||0),resources:performance.getEntriesByType('resource').length,now:Math.round(performance.now())}:null;});}catch(_){}\n`;
|
|
2157
|
+
const blankProbe = startUrl ? '' : `try{const __u=page.url();if(__u==='about:blank'||__u===''){console.error('__GM_BLANK__');}}catch(_){}\n`;
|
|
2130
2158
|
if (modeMatch && modeMatch[1] === 'profile') {
|
|
2131
2159
|
const userScript = modeMatch[2];
|
|
2132
2160
|
const intervalUs = 100;
|
|
@@ -2134,7 +2162,7 @@ function makeHostFunctions(instanceRef) {
|
|
|
2134
2162
|
+ `let __profile=null,__profileError=null;\n`
|
|
2135
2163
|
+ `let __cdp=null;\n`
|
|
2136
2164
|
+ `try{__cdp=await page.context().newCDPSession(page);await __cdp.send('Profiler.enable');await __cdp.send('Profiler.setSamplingInterval',{interval:${intervalUs}});await __cdp.send('Profiler.start');}catch(e){__profileError=String(e&&e.message||e);__cdp=null;}\n`
|
|
2137
|
-
+ `const __result = await (async () => {\n${gotoPrefix}${userScript}\n})();\n`
|
|
2165
|
+
+ `const __result = await (async () => {\n${blankProbe}${gotoPrefix}${userScript}\n})();\n`
|
|
2138
2166
|
+ `if(__cdp){try{const __r=await __cdp.send('Profiler.stop');__profile=__r&&__r.profile||null;}catch(e){__profileError=String(e&&e.message||e);}}\n`
|
|
2139
2167
|
+ perfRead
|
|
2140
2168
|
+ AGGREGATE_CPU_PROFILE_SRC + `\n`
|
|
@@ -2143,11 +2171,13 @@ function makeHostFunctions(instanceRef) {
|
|
|
2143
2171
|
} else if (modeMatch && modeMatch[1] === 'capture') {
|
|
2144
2172
|
const userScript = modeMatch[2];
|
|
2145
2173
|
evalBody = debugSetup
|
|
2146
|
-
+ `const __result = await (async () => {\n${gotoPrefix}${userScript}\n})();\n`
|
|
2174
|
+
+ `const __result = await (async () => {\n${blankProbe}${gotoPrefix}${userScript}\n})();\n`
|
|
2147
2175
|
+ perfRead
|
|
2148
2176
|
+ `return {result:__result,debug:{console:__logs,pageErrors:__errs,network:__net.slice(0,30),performance:__perf}};`;
|
|
2149
2177
|
} else if (startUrl) {
|
|
2150
2178
|
evalBody = `${gotoPrefix}${evalBody}`;
|
|
2179
|
+
} else if (blankProbe) {
|
|
2180
|
+
evalBody = `${blankProbe}${evalBody}`;
|
|
2151
2181
|
}
|
|
2152
2182
|
const outerTimeoutMs = Math.min(timeoutMs + 6000, 126000);
|
|
2153
2183
|
const r = runBrowserRunner(pw, ['-s', pwSessionId, '--timeout', String(timeoutMs), '-e', evalBody], outerTimeoutMs, cwd, sessionId);
|
|
@@ -2155,14 +2185,22 @@ function makeHostFunctions(instanceRef) {
|
|
|
2155
2185
|
if (!ok && r.status === null) {
|
|
2156
2186
|
logEvent('plugkit', 'browser.runner-timeout', { session_id: pwSessionId, timeout_ms: timeoutMs, body_bytes: evalBody.length });
|
|
2157
2187
|
}
|
|
2158
|
-
|
|
2188
|
+
const rawStderr = r.stderr || '';
|
|
2189
|
+
const landedOnBlank = !startUrl && rawStderr.includes('__GM_BLANK__');
|
|
2190
|
+
const envelope = {
|
|
2159
2191
|
ok,
|
|
2160
2192
|
stdout: scrubBrowserRunnerText(r.stdout || ''),
|
|
2161
|
-
stderr: scrubBrowserRunnerText(r
|
|
2193
|
+
stderr: scrubBrowserRunnerText(rawStderr.replace(/^__GM_BLANK__\r?\n?/gm, '')),
|
|
2162
2194
|
exit_code: r.status === null ? -1 : r.status,
|
|
2163
2195
|
session_id: pwSessionId,
|
|
2164
2196
|
timeout_ms_used: timeoutMs,
|
|
2165
|
-
}
|
|
2197
|
+
};
|
|
2198
|
+
envelope.navigation_requested = !!startUrl;
|
|
2199
|
+
if (landedOnBlank) {
|
|
2200
|
+
envelope.landed_on_blank = true;
|
|
2201
|
+
envelope.hint = "page is about:blank: this dispatch did not navigate, so the expression evaluated against an empty page. Prefix the body with 'url=<target>' (or send a bare 'https://...' URL) to open the page you want before evaluating.";
|
|
2202
|
+
}
|
|
2203
|
+
return writeWasmJson(instanceRef.value, envelope);
|
|
2166
2204
|
} catch (e) {
|
|
2167
2205
|
return writeWasmJson(instanceRef.value, { ok: false, error: scrubBrowserRunnerText(e.message) });
|
|
2168
2206
|
}
|
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.1621",
|
|
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",
|
package/skills/gm/SKILL.md
CHANGED
|
@@ -56,7 +56,7 @@ bun x gm-plugkit@latest spool > /dev/null 2>&1 &
|
|
|
56
56
|
|
|
57
57
|
From PowerShell, write spool input as UTF-8 no-BOM (`-Encoding utf8` or `[System.IO.File]::WriteAllText`); the 5.1 default UTF-16+BOM trips `spool.body-encoding-recoded`. Prefer the `Write` tool for JSON bodies. First-turn body is `{"prompt":"<user request>"}` (derives orient_nouns + recall_hits); later same-conversation turns may use `{}`. A `Write` to `in/<verb>/` that errors `ENOENT` (a fast watcher consumed and unlinked the file before the tool's post-write stat) has STILL dispatched -- confirm via the `out/` response, never blind-retry (a non-idempotent verb like `git_finalize` would double-fire); a Bash heredoc `cat > in/<verb>/<N>.txt` has no post-write stat and never surfaces this.
|
|
58
58
|
|
|
59
|
-
**Batch writes and reads together.** Write request + Read response is one logical step
|
|
59
|
+
**Batch writes and reads together -- one block is the default, the serial single dispatch is the drift.** Write request + Read response is one logical step; issue both in one block, never across turns. Independent dispatches batch as a class -- N `prd-add`, N `prd-resolve`, N `mutable-add`, the orient `recall`+`codesearch`, several inspection `Read`/`codesearch` -- as N Writes in one block then N Reads in one block. A turn that issues one independent verb while three were ready is the miss to correct; the only thing that forces separate turns is a true data dependency, verb B reading verb A's response. Two edges bound the rule. Same-file batching inverts it: two Edits to the SAME file in one block is not fan-out -- the first invalidates the file's read-state and the rest fail `File has been modified since read`, so collapse same-file changes into one Edit (or `replace_all`, or one Write of the whole file) and reserve in-block batching for Edits across DIFFERENT files. And a long verb (browser, an `exec_js` build, `git_finalize`) whose response is not ready on the Write+Read block: the recovery is one block carrying both the wait probe and the re-Read (the `until [ -f .gm/exec-spool/out/<verb>-<N>.json ]; do sleep N; done` and the `Read` together, or honoring an advertised `busy_until` the same way), never a bare wait turn followed by a separate Read turn. Reading a homogeneous fan-out's responses is itself batched: Read all N in one block, or spot-check first and last -- they carry no ordering dependency.
|
|
60
60
|
|
|
61
61
|
The chain is not COMPLETE until changes are on origin. Commit and push at the end of every session that touched tracked files; do not ask -- the push IS the validation dispatch (`verify.rs`). Only the porcelain check holds it back, and a dirty tree is fixed by stage-commit or revert, not by asking.
|
|
62
62
|
|