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 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 -- no npx `skills` library, no marketplace. It copies `skills/gm` into `<home>/.claude/skills/gm/` (personal) or `.claude/skills/gm/` (`--project`); the dir name IS the `/command`. Non-interactive (`-y`/`--yes` or non-TTY) SETS four Claude Code settings (`autoCompactEnabled:true`, `autoCompactWindow:380000` -- an ABSOLUTE token count = 38% of 1M, not a percentage -- `effortLevel:"low"`, `alwaysThinkingEnabled:false`) and explains the revert; interactive OFFERS them. The reasoning-in-code framing it prints is load-bearing: the LLM still thinks, it tests its thoughts in code (execution as reasoning). `test.js checkRenameAndInstaller()` is the structural guard (asserts no `skills/gm-skill`, package id stays `gm-skill`, installer lands the skill + writes the four keys into an isolated temp HOME).
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
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-plugkit",
3
- "version": "2.0.1620",
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
- const verbPtr = instance.exports.plugkit_alloc(verbBytes.length);
167
- const bodyPtr = instance.exports.plugkit_alloc(bodyBytes.length);
168
- if ((verbBytes.length > 0 && verbPtr === 0) || (bodyBytes.length > 0 && bodyPtr === 0)) {
169
- try { if (verbPtr !== 0) instance.exports.plugkit_free(verbPtr, verbBytes.length); } catch (_) {}
170
- try { if (bodyPtr !== 0) instance.exports.plugkit_free(bodyPtr, bodyBytes.length); } catch (_) {}
171
- throw new Error(`wasm-alloc-failed for dispatch_verb(${verb}): plugkit_alloc returned 0 (wasm OOM); refusing to write to a null offset and corrupt the heap`);
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
- const ptr = Number(result & 0xffffffffn);
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
- ptr = Number(result & 0xffffffffn);
2254
- len = Number(result >> 32n);
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) & 0xffffffff;
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
- const verbPtr = instance.exports.plugkit_alloc(verbBytes.length);
3251
- const bodyPtr = instance.exports.plugkit_alloc(bodyBytes.length);
3252
- new Uint8Array(instance.exports.memory.buffer, verbPtr, verbBytes.length).set(verbBytes);
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
- const ptr = Number(result & 0xffffffffn);
3260
- const len = Number(result >> 32n);
3261
- guardWasmRange(instance.exports.memory.buffer, ptr, len, `spool-dispatch:${verb}`);
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
- try { instance.exports.plugkit_free(ptr, len); } catch (_) {}
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.exports.plugkit_alloc(verbBytes.length);
3965
- const bodyPtr = instance.exports.plugkit_alloc(bodyBytes.length);
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 ptr = Number(result & 0xffffffffn);
3970
- const len = Number(result >> 32n);
3971
- guardWasmRange(instance.exports.memory.buffer, ptr, len, `cli-dispatch:${verb}`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm",
3
- "version": "2.0.1620",
3
+ "version": "2.0.1622",
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.1620",
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",
@@ -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 -- issue both in one block, not three turns. Fan-out is the same: N independent verbs = N Writes in one block then N Reads in one block. Only a real data dependency (verb B needs A's response) forces separate turns.
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