gm-skill 2.0.1159 → 2.0.1161

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/README.md CHANGED
@@ -35,7 +35,7 @@ An earlier generation fanned out fifteen per-platform downstream repos (gm-cc, g
35
35
 
36
36
  ## Version
37
37
 
38
- `2.0.1159` — auto-bumped from the canonical `gm` repo. Every push to `AnEntrypoint/gm` (or any cascading sibling crate) republishes this package.
38
+ `2.0.1161` — auto-bumped from the canonical `gm` repo. Every push to `AnEntrypoint/gm` (or any cascading sibling crate) republishes this package.
39
39
 
40
40
  ## Source of truth
41
41
 
@@ -1 +1 @@
1
- 0.1.418
1
+ 0.1.419
package/bin/plugkit.wasm CHANGED
Binary file
@@ -1 +1 @@
1
- ffb68924055bffd275db6a174fb5d286c1fcaf5a135f968345bf221b9976544f plugkit.wasm
1
+ 623152434c8b0f75b20cb89f48551e3a1677f132275e0fc43e261db7a69afeed plugkit.wasm
@@ -153,6 +153,23 @@ function sha256OfFile(filePath) {
153
153
  });
154
154
  }
155
155
 
156
+ function resolveNpxJsCli() {
157
+ if (process.platform !== 'win32') return null;
158
+ const candidates = [];
159
+ if (process.env.npm_config_prefix) {
160
+ candidates.push(path.join(process.env.npm_config_prefix, 'node_modules', 'npm', 'bin', 'npx-cli.js'));
161
+ }
162
+ const programFiles = process.env.ProgramFiles || 'C:\\Program Files';
163
+ candidates.push(path.join(programFiles, 'nodejs', 'node_modules', 'npm', 'bin', 'npx-cli.js'));
164
+ candidates.push(path.join(path.dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npx-cli.js'));
165
+ const appdata = process.env.APPDATA;
166
+ if (appdata) candidates.push(path.join(appdata, 'npm', 'node_modules', 'npm', 'bin', 'npx-cli.js'));
167
+ for (const c of candidates) {
168
+ try { if (fs.existsSync(c)) return c; } catch (_) {}
169
+ }
170
+ return null;
171
+ }
172
+
156
173
  async function extractNpmPackageWasm(destPath, version) {
157
174
  const tempDir = path.join(path.dirname(destPath), '.npm-extract-' + Date.now());
158
175
  try {
@@ -161,16 +178,28 @@ async function extractNpmPackageWasm(destPath, version) {
161
178
  log(`extracting npm package ${NPM_PACKAGE}@${version} to ${tempDir}`);
162
179
  obsEvent('bootstrap', 'npm.extract.start', { package: NPM_PACKAGE, version });
163
180
 
164
- const result = spawnSync(
165
- process.platform === 'win32' ? 'npx.cmd' : 'npx',
166
- [NPM_PACKAGE + '@' + version, '--prefix', tempDir],
167
- {
168
- stdio: ['ignore', 'pipe', 'pipe'],
169
- timeout: ATTEMPT_TIMEOUT_MS,
170
- encoding: 'utf8',
171
- windowsHide: true,
181
+ let cmd, args;
182
+ if (process.platform === 'win32') {
183
+ const npxCli = resolveNpxJsCli();
184
+ if (npxCli) {
185
+ cmd = process.execPath;
186
+ args = [npxCli, NPM_PACKAGE + '@' + version, '--prefix', tempDir];
187
+ } else {
188
+ cmd = 'npx.cmd';
189
+ args = [NPM_PACKAGE + '@' + version, '--prefix', tempDir];
172
190
  }
173
- );
191
+ } else {
192
+ cmd = 'npx';
193
+ args = [NPM_PACKAGE + '@' + version, '--prefix', tempDir];
194
+ }
195
+
196
+ const result = spawnSync(cmd, args, {
197
+ stdio: ['ignore', 'pipe', 'pipe'],
198
+ timeout: ATTEMPT_TIMEOUT_MS,
199
+ encoding: 'utf8',
200
+ windowsHide: true,
201
+ shell: process.platform === 'win32' && cmd === 'npx.cmd',
202
+ });
174
203
 
175
204
  if (result.error) throw result.error;
176
205
  if (result.status !== 0) {
@@ -192,25 +221,37 @@ async function extractNpmPackageWasm(destPath, version) {
192
221
 
193
222
  function httpGetBuffer(url, timeoutMs) {
194
223
  const https = require('https');
224
+ const idleTimeoutMs = timeoutMs || 30000;
225
+ const totalDeadlineMs = (timeoutMs || 30000) * 2;
195
226
  return new Promise((resolve, reject) => {
196
- const req = https.get(url, { timeout: timeoutMs || 30000, headers: { 'user-agent': 'gm-plugkit-bootstrap' } }, (res) => {
227
+ let bytesReceived = 0;
228
+ let settled = false;
229
+ const settleReject = (err) => { if (!settled) { settled = true; reject(err); } };
230
+ const settleResolve = (v) => { if (!settled) { settled = true; resolve(v); } };
231
+ const absTimer = setTimeout(() => {
232
+ try { req.destroy(new Error(`abs-deadline ${totalDeadlineMs}ms ${url} after ${bytesReceived} bytes`)); } catch (_) {}
233
+ settleReject(new Error(`abs-deadline ${totalDeadlineMs}ms ${url} after ${bytesReceived} bytes`));
234
+ }, totalDeadlineMs);
235
+ const req = https.get(url, { timeout: idleTimeoutMs, headers: { 'user-agent': 'gm-plugkit-bootstrap' } }, (res) => {
197
236
  if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
198
237
  res.resume();
199
- httpGetBuffer(res.headers.location, timeoutMs).then(resolve, reject);
238
+ clearTimeout(absTimer);
239
+ httpGetBuffer(res.headers.location, timeoutMs).then(settleResolve, settleReject);
200
240
  return;
201
241
  }
202
242
  if (res.statusCode !== 200) {
203
243
  res.resume();
204
- reject(new Error(`HTTP ${res.statusCode} ${url}`));
244
+ clearTimeout(absTimer);
245
+ settleReject(new Error(`HTTP ${res.statusCode} ${url}`));
205
246
  return;
206
247
  }
207
248
  const chunks = [];
208
- res.on('data', c => chunks.push(c));
209
- res.on('end', () => resolve(Buffer.concat(chunks)));
210
- res.on('error', reject);
249
+ res.on('data', c => { chunks.push(c); bytesReceived += c.length; });
250
+ res.on('end', () => { clearTimeout(absTimer); settleResolve(Buffer.concat(chunks)); });
251
+ res.on('error', (e) => { clearTimeout(absTimer); settleReject(e); });
211
252
  });
212
- req.on('timeout', () => req.destroy(new Error(`timeout ${url}`)));
213
- req.on('error', reject);
253
+ req.on('timeout', () => { try { req.destroy(new Error(`idle-timeout ${idleTimeoutMs}ms ${url}`)); } catch (_) {} settleReject(new Error(`idle-timeout ${idleTimeoutMs}ms ${url}`)); });
254
+ req.on('error', (e) => { clearTimeout(absTimer); settleReject(e); });
214
255
  });
215
256
  }
216
257
 
@@ -932,6 +932,22 @@ function resolveVersion(instance) {
932
932
  return 'unknown';
933
933
  }
934
934
 
935
+ function readFileVersionOnly() {
936
+ try { return fs.readFileSync(path.join(os.homedir(), '.claude', 'gm-tools', 'plugkit.version'), 'utf8').trim(); } catch (_) { return null; }
937
+ }
938
+
939
+ function readInstanceVersion(instance) {
940
+ try {
941
+ const fn = instance && instance.exports && instance.exports.plugkit_version;
942
+ if (typeof fn !== 'function') return null;
943
+ const result = fn();
944
+ const ptr = Number(result & 0xffffffffn);
945
+ const len = Number(result >> 32n);
946
+ const bytes = new Uint8Array(instance.exports.memory.buffer, ptr, len);
947
+ return new TextDecoder().decode(bytes).trim();
948
+ } catch (_) { return null; }
949
+ }
950
+
935
951
  async function runSpoolWatcher(instance, spoolDir) {
936
952
  const inDir = path.join(spoolDir, 'in');
937
953
  const outDir = path.join(spoolDir, 'out');
@@ -947,8 +963,11 @@ async function runSpoolWatcher(instance, spoolDir) {
947
963
  const lockTs = parseInt(tsStr, 10);
948
964
  const age = Date.now() - lockTs;
949
965
  if (age < 15000) {
950
- console.error(`[plugkit-wasm] another watcher active (pid=${pidStr}, age=${age}ms); refusing to start`);
951
- process.exit(1);
966
+ const msg = JSON.stringify({ ok: false, reason: 'another-watcher-active', pid: pidStr, age_ms: age });
967
+ console.error(`[plugkit-wasm] ${msg}; refusing to start`);
968
+ try { fs.writeFileSync(path.join(spoolDir, '.lock-rejection.json'), msg); } catch (_) {}
969
+ try { logEvent('plugkit', 'watcher.lock-rejected', { holder_pid: pidStr, lock_age_ms: age }); } catch (_) {}
970
+ process.exit(75);
952
971
  }
953
972
  console.error(`[plugkit-wasm] stale lock (age=${age}ms); taking over`);
954
973
  }
@@ -1035,6 +1054,35 @@ async function runSpoolWatcher(instance, spoolDir) {
1035
1054
  try { reapTimedOutTasks(); } catch (_) {}
1036
1055
  }, 5000);
1037
1056
 
1057
+ const _instanceVersionAtBoot = readInstanceVersion(instance);
1058
+ setInterval(() => {
1059
+ try {
1060
+ const fileV = readFileVersionOnly();
1061
+ const instV = _instanceVersionAtBoot;
1062
+ if (!fileV || !instV || fileV === instV) return;
1063
+ logEvent('plugkit', 'version.drift', {
1064
+ instance_version: instV,
1065
+ file_version: fileV,
1066
+ action: 'exit-for-respawn',
1067
+ });
1068
+ console.error(`[plugkit-wasm] version drift detected: instance=${instV} file=${fileV} → exiting so supervisor reloads fresh wasm`);
1069
+ try {
1070
+ fs.writeFileSync(path.join(spoolDir, '.shutdown-reason.json'), JSON.stringify({
1071
+ reason: 'version-change',
1072
+ ts: Date.now(),
1073
+ pid: process.pid,
1074
+ instance_version: instV,
1075
+ file_version: fileV,
1076
+ }));
1077
+ } catch (_) {}
1078
+ try { releaseLock(); } catch (_) {}
1079
+ try { fs.unlinkSync(STATUS_PATH_FOR_TEARDOWN); } catch (_) {}
1080
+ process.exit(0);
1081
+ } catch (e) {
1082
+ console.error(`[version-drift-check] error: ${e.message}`);
1083
+ }
1084
+ }, 60_000);
1085
+
1038
1086
  setInterval(() => {
1039
1087
  try {
1040
1088
  const idleMs = Date.now() - lastActivityMs;
@@ -130,27 +130,37 @@ function spawnWatcher(bootReason) {
130
130
  const shutdownReason = readShutdownReason();
131
131
  const reason = shutdownReason && shutdownReason.reason;
132
132
  const idleClean = reason === 'idle';
133
- logEvent(idleClean ? 'supervisor.watcher-exited-idle' : 'supervisor.watcher-exited-unexpectedly', {
133
+ const plannedReasons = new Set(['idle', 'sigterm', 'version-change']);
134
+ const isPlanned = plannedReasons.has(reason);
135
+ const eventName = idleClean
136
+ ? 'supervisor.watcher-exited-idle'
137
+ : reason === 'version-change'
138
+ ? 'supervisor.watcher-exited-for-update'
139
+ : 'supervisor.watcher-exited-unexpectedly';
140
+ logEvent(eventName, {
134
141
  watcher_pid: currentChildPid,
135
142
  exit_code: code,
136
143
  signal,
137
144
  shutdown_reason: reason || null,
138
145
  had_shutdown_reason_file: shutdownReason !== null,
139
- severity: idleClean ? 'info' : 'critical',
146
+ severity: isPlanned ? 'info' : 'critical',
140
147
  uptime_ms: Date.now() - lastSpawnedAt,
148
+ ...(shutdownReason || {}),
141
149
  });
142
150
  if (idleClean) {
143
151
  writeSupervisorStatus('exited-idle', { watcher_pid: currentChildPid });
144
152
  try { fs.unlinkSync(SUPERVISOR_PATH); } catch (_) {}
145
153
  process.exit(0);
146
154
  }
155
+ const respawnReason = reason === 'version-change' ? 'planned-restart-version-change' : 'unplanned-restart-after-exit';
147
156
  writeSupervisorStatus('restarting', {
148
157
  prior_watcher_pid: currentChildPid,
149
158
  prior_exit_code: code,
150
159
  prior_signal: signal,
151
160
  prior_shutdown_reason: reason || null,
161
+ respawn_reason: respawnReason,
152
162
  });
153
- setTimeout(() => spawnWatcher('unplanned-restart-after-exit'), 1500);
163
+ setTimeout(() => spawnWatcher(respawnReason), 1500);
154
164
  });
155
165
 
156
166
  child.on('error', (err) => {
package/gm.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm",
3
- "version": "2.0.1159",
3
+ "version": "2.0.1161",
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.418"
20
+ "plugkitVersion": "0.1.419"
21
21
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-skill",
3
- "version": "2.0.1159",
3
+ "version": "2.0.1161",
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",
@@ -39,7 +39,7 @@
39
39
  "gm.json"
40
40
  ],
41
41
  "dependencies": {
42
- "gm-plugkit": "^2.0.1159"
42
+ "gm-plugkit": "^2.0.1161"
43
43
  },
44
44
  "engines": {
45
45
  "node": ">=16.0.0"
@@ -18,6 +18,8 @@ Before any apparent stop, dispatch `residual-scan`. If it returns work that fits
18
18
 
19
19
  If the `instruction` response carries a non-null `update_available`, plugkit drift has been detected on disk and the running watcher is behind. Rebootstrap before continuing — newer fixes are sitting on disk unused, and every dispatch is wasted on stale code. `bun x gm-plugkit@latest` (or `npx -y gm-plugkit@latest`) is the one-shot: it fetches the new wasm, replaces the artifact, and the watcher reloads. Drift past one version is a deviation.
20
20
 
21
+ If `running_tasks` is non-empty, you own them — every entry is a subprocess you started that's still consuming CPU/memory. Stop ones that have outlived their purpose with `task-stop` (write `.gm/exec-spool/in/task-stop/<N>.txt` with `{id: "t<n>"}`). The 15-min idle reaper is the last resort, not a substitute for hygiene. If `stuck_spool` is non-empty, dispatches are wedged — the watcher's host_exec_js is synchronous and a stuck body blocks every other verb until it returns. Diagnose via `.watcher.log` and consider rebootstrapping if it doesn't clear; spool bodies are Turing-complete and can loop forever. Long-running work goes through `task-spawn` (returns a `task_id` immediately, body runs detached), not through the sync `nodejs`/`bash`/`python` language verbs.
22
+
21
23
  The wasm artifact lives at `~/.claude/gm-tools/plugkit.wasm`; the spool watcher runs it. The watcher's own stdout/stderr is appended to `.gm/exec-spool/.watcher.log` — Read it to see plugkit's internal trace, dispatch timings, sweep actions, errors.
22
24
 
23
25
  The watcher self-shuts-down after 15 minutes idle (no spool I/O, no live browser session) and is restarted on next agent activity by a detached supervisor. `.gm/exec-spool/.unplanned-restart.json` is a critical-failure marker — present means a prior watcher died without a planned shutdown. Treat as a PRD-worthy incident on sight: diagnose via `.watcher.log` and `gm-log/<day>/plugkit.jsonl` events `supervisor.watcher-exited-unexpectedly` and `supervisor.heartbeat-stale` around the prior_status.ts timestamp, then delete the marker once root cause is named.
@@ -106,7 +108,7 @@ Stop only when `phase` is `COMPLETE` AND `residual-scan` returns empty AND the w
106
108
 
107
109
  ## Orchestrator verbs
108
110
 
109
- `instruction`, `transition`, `phase-status`, `prd-add`, `prd-resolve`, `prd-list`, `mutable-add`, `mutable-resolve`, `mutable-list`, `memorize-fire`, `residual-scan`, `auto-recall`.
111
+ `instruction`, `transition`, `phase-status`, `prd-add`, `prd-resolve`, `prd-list`, `mutable-add`, `mutable-resolve`, `mutable-list`, `memorize-fire`, `residual-scan`, `auto-recall`, `task-spawn`, `task-list`, `task-stop`, `task-output`.
110
112
 
111
113
  ## Host verbs
112
114