gm-skill 2.0.1540 → 2.0.1542

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
@@ -28,7 +28,7 @@ This repo IS the published `gm-skill` npm package. The repo root is the package
28
28
 
29
29
  The plugkit stack runs as a wasm cdylib loaded by `plugkit-wasm-wrapper.js` under Node/bun. No native binaries are built, downloaded, or published. The shipped `plugkit.wasm` (~149MB, embeds bge-small-en-v1.5 for offline in-wasm embeddings) is fetched at bootstrap from `plugkit-wasm` npm / `plugkit-bin` gh-releases, sha256-pinned, not bundled in `gm-skill`. Full size/embedding mechanics in rs-learn (`recall: WASM-only plugkit size mechanics`).
30
30
 
31
- **Every wasm host-import `extern "C"` block carries `#[link(wasm_import_module = "env")]`.** Holds in rs-plugkit AND every dep crate linked into the cdylib (rs-learn) AND any sibling that builds wasm (rs-exec, rs-search) -- add a host import anywhere and the block carries the attribute or the cascade goes dark. The trap (bare block links on non-Linux hosts so `cargo check`/`build --release` pass while strict Linux `rust-lld` in CI fails `undefined symbol: host_*`; only a Linux release link reproduces) + the full host-fn `env` enumeration live in rs-learn (`recall: cascade outage wasm import module link`).
31
+ **Every wasm host-import `extern "C"` block carries `#[link(wasm_import_module = "env")]`.** Holds in rs-plugkit AND every dep crate linked into the cdylib (rs-learn) AND any sibling that builds wasm (rs-exec, rs-search) -- add a host import anywhere and the block carries the attribute or the cascade goes dark. The silent-pass trap (local builds stay green, only Linux CI link fails), recovery, and the full host-fn `env` enumeration live in rs-learn (`recall: cascade outage wasm import module link`, `recall: wasm host-import link-module trap`).
32
32
 
33
33
  **`plugkit-wasm-wrapper.js` is ESM; import node builtins at module scope, never inline `require()` (throws silently under bun's ESM inside `catch(_){}`).** Full incident in rs-learn (`recall: wrapper require not defined under bun`).
34
34
 
@@ -1 +1 @@
1
- 0.1.638
1
+ 0.1.639
@@ -1 +1 @@
1
- 4446475d6343de16cbb4bcf7352daede709192c86877378b2b56e4475094b83c plugkit.wasm
1
+ d2b32fb436ed9b4a70e0816b0492596209bf5fe3e7dc4e163380e6974490be09 plugkit.wasm
@@ -606,7 +606,25 @@ function copyWasmToGmTools(wasmPath, version) {
606
606
  if (cur === src) wasmFresh = true;
607
607
  } catch (_) {}
608
608
  }
609
- if (!wasmFresh) fs.copyFileSync(wasmPath, target);
609
+ if (!wasmFresh) {
610
+ // copyFileSync truncates the target before streaming ~149MB, leaving a window where
611
+ // a crash or a concurrent watcher load sees a truncated/absent wasm (the
612
+ // "self-heal: wasm not installed" crash-loop during an upgrade). Copy to a
613
+ // pid-suffixed temp and rename over the target: same-volume rename is atomic,
614
+ // with the Windows EEXIST/EPERM unlink+retry.
615
+ const tmp = `${target}.partial-${process.pid}`;
616
+ fs.copyFileSync(wasmPath, tmp);
617
+ try { fs.renameSync(tmp, target); }
618
+ catch (err) {
619
+ if (err.code === 'EEXIST' || err.code === 'EPERM') {
620
+ try { fs.unlinkSync(target); } catch (_) {}
621
+ fs.renameSync(tmp, target);
622
+ } else {
623
+ try { fs.unlinkSync(tmp); } catch (_) {}
624
+ throw err;
625
+ }
626
+ }
627
+ }
610
628
  fs.writeFileSync(path.join(dst, 'plugkit.version'), version);
611
629
 
612
630
  try {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-plugkit",
3
- "version": "2.0.1540",
3
+ "version": "2.0.1542",
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": {
@@ -2212,6 +2212,32 @@ async function runSpoolWatcher(instance, spoolDir) {
2212
2212
  detected_at: Date.now(),
2213
2213
  });
2214
2214
  try { console.error(`[plugkit-wasm] VERB ABORT detected: prior watcher pid=${priorVerb.pid} died inside verb=${priorVerb.verb} task=${priorVerb.task}`); } catch (_) {}
2215
+ // The aborted dispatch otherwise gets NO response file: the in-file was consumed,
2216
+ // the prior watcher died before writing out/, and the agent waits forever on a
2217
+ // Read that never lands (then must git-archaeology whether the verb's side effects
2218
+ // happened). Write a definite failure response so the agent's Read returns
2219
+ // immediately and it re-dispatches. Both out-name shapes are written because
2220
+ // .verb-active.json does not record whether the dispatch was root or nested;
2221
+ // the agent reads whichever its dispatch shape expects, the other is swept.
2222
+ if (priorVerb.verb && priorVerb.task) {
2223
+ try {
2224
+ const abortBody = JSON.stringify({
2225
+ ok: false,
2226
+ error: `verb aborted: watcher pid=${priorVerb.pid} died mid-verb; side effects may be partial -- verify state, then re-dispatch`,
2227
+ verb_aborted: true,
2228
+ verb: priorVerb.verb,
2229
+ task: priorVerb.task,
2230
+ });
2231
+ const abortOutDir = path.join(path.dirname(STATUS_PATH_FOR_VERB_ABORT), 'out');
2232
+ fs.mkdirSync(abortOutDir, { recursive: true });
2233
+ const nestedName = path.join(abortOutDir, `${priorVerb.verb}-${priorVerb.task}.json`);
2234
+ if (!fs.existsSync(nestedName)) fs.writeFileSync(nestedName, abortBody);
2235
+ if (priorVerb.verb !== priorVerb.task) {
2236
+ const rootName = path.join(abortOutDir, `${priorVerb.task}.json`);
2237
+ if (!fs.existsSync(rootName)) fs.writeFileSync(rootName, abortBody);
2238
+ }
2239
+ } catch (_) {}
2240
+ }
2215
2241
  }
2216
2242
  try { fs.unlinkSync(VERB_ACTIVE_PATH); } catch (_) {}
2217
2243
  }
@@ -3628,7 +3654,24 @@ async function selfHealFromGithubReleases() {
3628
3654
  }
3629
3655
  const toolsDir = GM_TOOLS_ROOT;
3630
3656
  fs.mkdirSync(toolsDir, { recursive: true });
3631
- fs.writeFileSync(path.join(toolsDir, 'plugkit.wasm'), wasm);
3657
+ // Replace the live wasm atomically. A direct writeFileSync truncates the target
3658
+ // before streaming ~149MB, so a crash mid-write or a concurrent watcher load in
3659
+ // that window sees a truncated or absent wasm ("self-heal: wasm not installed"
3660
+ // crash-loop). Write to a pid-suffixed temp and rename over the target; rename
3661
+ // on the same volume is atomic, with the Windows EEXIST/EPERM unlink+retry.
3662
+ const wasmTarget = path.join(toolsDir, 'plugkit.wasm');
3663
+ const wasmTmp = `${wasmTarget}.partial-${process.pid}`;
3664
+ fs.writeFileSync(wasmTmp, wasm);
3665
+ try { fs.renameSync(wasmTmp, wasmTarget); }
3666
+ catch (renameErr) {
3667
+ if (renameErr.code === 'EEXIST' || renameErr.code === 'EPERM') {
3668
+ try { fs.unlinkSync(wasmTarget); } catch (_) {}
3669
+ fs.renameSync(wasmTmp, wasmTarget);
3670
+ } else {
3671
+ try { fs.unlinkSync(wasmTmp); } catch (_) {}
3672
+ throw renameErr;
3673
+ }
3674
+ }
3632
3675
  fs.writeFileSync(path.join(toolsDir, 'plugkit.version'), version);
3633
3676
  const wrapperSrc = __filename;
3634
3677
  const wrapperDst = path.join(toolsDir, 'plugkit-wasm-wrapper.js');
@@ -85,10 +85,39 @@ function acquireSingleInstance() {
85
85
  let other = NaN;
86
86
  try { other = parseInt(fs.readFileSync(SUPERVISOR_PID_PATH, 'utf-8').trim(), 10); } catch (_) {}
87
87
  if (Number.isFinite(other) && other !== process.pid && pidAlive(other)) {
88
- logEvent('supervisor.refused-duplicate', { existing_pid: other, severity: 'warn' });
89
- return false;
88
+ // An alive holder pid is not the same as a working holder: a wedged supervisor
89
+ // (event loop stuck, watcher dead, neither .supervisor.json nor .status.json
90
+ // advancing) blocks every newcomer forever under a pidAlive-only check, forcing
91
+ // manual process kills to recover the spool. Discriminate by progress, not
92
+ // liveness: holder is wedged only when its own status heartbeat AND the spool
93
+ // status are both stale past the takeover window, honoring a future busy_until
94
+ // exactly as checkWatcherHealth does.
95
+ const TAKEOVER_STALE_MS = 45_000;
96
+ const now = Date.now();
97
+ let supTs = 0;
98
+ try { supTs = (JSON.parse(fs.readFileSync(SUPERVISOR_PATH, 'utf-8')).ts) || 0; } catch (_) {}
99
+ const spool = readStatus();
100
+ const spoolBusy = spool && spool.busy_until && spool.busy_until > now;
101
+ const spoolTs = (spool && spool.ts) || 0;
102
+ const holderWedged = !spoolBusy
103
+ && (now - supTs) > TAKEOVER_STALE_MS
104
+ && (now - spoolTs) > TAKEOVER_STALE_MS;
105
+ if (!holderWedged) {
106
+ logEvent('supervisor.refused-duplicate', { existing_pid: other, severity: 'warn' });
107
+ return false;
108
+ }
109
+ logEvent('supervisor.takeover-wedged', {
110
+ existing_pid: other,
111
+ supervisor_status_age_ms: now - supTs,
112
+ spool_status_age_ms: now - spoolTs,
113
+ severity: 'critical',
114
+ });
115
+ try { process.kill(other, 'SIGTERM'); } catch (_) {}
116
+ if (process.platform === 'win32') {
117
+ try { spawnSync('taskkill', ['/F', '/T', '/PID', String(other)], { stdio: 'ignore', windowsHide: true, timeout: 3000 }); } catch (_) {}
118
+ }
90
119
  }
91
- // Holder is dead/stale: remove and retry the exclusive create once.
120
+ // Holder is dead/stale/wedged: remove and retry the exclusive create once.
92
121
  try { fs.unlinkSync(SUPERVISOR_PID_PATH); } catch (_) {}
93
122
  continue;
94
123
  }
package/gm.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm",
3
- "version": "2.0.1540",
3
+ "version": "2.0.1542",
4
4
  "description": "Spool-dispatch orchestration engine with unified state machine, skills, and automated git enforcement",
5
5
  "author": "AnEntrypoint",
6
6
  "license": "MIT",
@@ -17,5 +17,5 @@
17
17
  "publishConfig": {
18
18
  "access": "public"
19
19
  },
20
- "plugkitVersion": "0.1.638"
20
+ "plugkitVersion": "0.1.639"
21
21
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-skill",
3
- "version": "2.0.1540",
3
+ "version": "2.0.1542",
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",