gm-skill 2.0.1620 → 2.0.1622
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/package.json +1 -1
- package/gm-plugkit/plugkit-wasm-wrapper.js +62 -40
- 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
|
|
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.1622",
|
|
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;
|
|
@@ -2250,13 +2277,14 @@ function readInstanceVersion(instance) {
|
|
|
2250
2277
|
const result = fn();
|
|
2251
2278
|
let ptr, len;
|
|
2252
2279
|
if (typeof result === 'bigint') {
|
|
2253
|
-
|
|
2254
|
-
|
|
2280
|
+
const u = BigInt.asUintN(64, result); // normalize the i64 to unsigned before splitting (signed-ptr fix)
|
|
2281
|
+
ptr = Number(u & 0xffffffffn);
|
|
2282
|
+
len = Number(u >> 32n);
|
|
2255
2283
|
} else {
|
|
2256
|
-
ptr = Number(result)
|
|
2284
|
+
ptr = Number(result) >>> 0; // unsigned 32-bit
|
|
2257
2285
|
len = 0;
|
|
2258
2286
|
}
|
|
2259
|
-
const buf = new Uint8Array(instance.exports.memory.buffer, ptr, 64);
|
|
2287
|
+
const buf = new Uint8Array(instance.exports.memory.buffer, ptr, 64); // fresh buffer (post fn() grow)
|
|
2260
2288
|
if (len === 0) {
|
|
2261
2289
|
let end = 0;
|
|
2262
2290
|
while (end < buf.length && buf[end] !== 0) end++;
|
|
@@ -3247,20 +3275,17 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
3247
3275
|
}
|
|
3248
3276
|
}
|
|
3249
3277
|
|
|
3250
|
-
|
|
3251
|
-
const
|
|
3252
|
-
|
|
3253
|
-
new Uint8Array(instance.exports.memory.buffer, bodyPtr, bodyBytes.length).set(bodyBytes);
|
|
3278
|
+
// writeWasmInput re-reads memory.buffer fresh after each alloc (detached-buffer write fix).
|
|
3279
|
+
const verbPtr = writeWasmInput(instance, verbBytes, `spool-dispatch:${verb}.verb`);
|
|
3280
|
+
const bodyPtr = writeWasmInput(instance, bodyBytes, `spool-dispatch:${verb}.body`);
|
|
3254
3281
|
|
|
3255
3282
|
writeVerbActive(verb, taskBase);
|
|
3256
3283
|
const result = dispatch(verbPtr, verbBytes.length, bodyPtr, bodyBytes.length);
|
|
3257
3284
|
clearVerbActive();
|
|
3258
3285
|
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
const resultBytes = new Uint8Array(instance.exports.memory.buffer, ptr, len);
|
|
3263
|
-
let resultStr = new TextDecoder().decode(resultBytes);
|
|
3286
|
+
// decodeWasmResult normalizes the i64 (BigInt.asUintN), re-reads the buffer FRESH (post-grow),
|
|
3287
|
+
// guards the range, AND frees the result ptr -- so the (ptr,len) free below is dropped.
|
|
3288
|
+
let resultStr = decodeWasmResult(instance, result, `spool-dispatch:${verb}`);
|
|
3264
3289
|
|
|
3265
3290
|
if (autoRecallPayload) {
|
|
3266
3291
|
resultStr = mergeAutoRecallIntoInstructionResponse(resultStr, autoRecallPayload);
|
|
@@ -3313,7 +3338,7 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
3313
3338
|
|
|
3314
3339
|
try { instance.exports.plugkit_free(verbPtr, verbBytes.length); } catch (_) {}
|
|
3315
3340
|
try { instance.exports.plugkit_free(bodyPtr, bodyBytes.length); } catch (_) {}
|
|
3316
|
-
|
|
3341
|
+
// (the result ptr is freed inside decodeWasmResult above)
|
|
3317
3342
|
|
|
3318
3343
|
try { if (fs.existsSync(filePath)) fs.unlinkSync(filePath); } catch (_) {}
|
|
3319
3344
|
unmarkProcessed(key);
|
|
@@ -3961,15 +3986,12 @@ if (_isCliEntry) (async () => {
|
|
|
3961
3986
|
const dispatch = instance.exports.dispatch_verb;
|
|
3962
3987
|
const verbBytes = new TextEncoder().encode(verb);
|
|
3963
3988
|
const bodyBytes = new TextEncoder().encode(body);
|
|
3964
|
-
const verbPtr = instance
|
|
3965
|
-
const bodyPtr = instance
|
|
3966
|
-
new Uint8Array(instance.exports.memory.buffer, verbPtr, verbBytes.length).set(verbBytes);
|
|
3967
|
-
new Uint8Array(instance.exports.memory.buffer, bodyPtr, bodyBytes.length).set(bodyBytes);
|
|
3989
|
+
const verbPtr = writeWasmInput(instance, verbBytes, `cli-dispatch:${verb}.verb`);
|
|
3990
|
+
const bodyPtr = writeWasmInput(instance, bodyBytes, `cli-dispatch:${verb}.body`);
|
|
3968
3991
|
const result = dispatch(verbPtr, verbBytes.length, bodyPtr, bodyBytes.length);
|
|
3969
|
-
const
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
const out = new TextDecoder().decode(new Uint8Array(instance.exports.memory.buffer, ptr, len));
|
|
3992
|
+
const out = decodeWasmResult(instance, result, `cli-dispatch:${verb}`); // normalized i64 + fresh buffer
|
|
3993
|
+
try { instance.exports.plugkit_free(verbPtr, verbBytes.length); } catch (_) {}
|
|
3994
|
+
try { instance.exports.plugkit_free(bodyPtr, bodyBytes.length); } catch (_) {}
|
|
3973
3995
|
process.stdout.write(out);
|
|
3974
3996
|
let parsed;
|
|
3975
3997
|
try { parsed = JSON.parse(out); } catch (_) { parsed = null; }
|
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.1622",
|
|
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
|
|