gm-skill 2.0.2059 → 2.0.2060

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
@@ -105,7 +105,7 @@ A task that reduces to read/investigate/report, or a change confined to files th
105
105
 
106
106
  **Disciplines are isolated knowledge stores**, tracked, `@<name>` sigil-scoped, enabled list at `.gm/disciplines/enabled.txt` (one name per line; `discipline_note.rs::active_policies()` auto-surfaces each enabled discipline's `.gm/disciplines/<name>/policy.md` on every `instruction`). Detail: the recall store (`recall: gm disciplines mechanics`).
107
107
 
108
- **Every runtime config surface, indexed**: `.gm/instructions/<key>.md` + `.gm/instructions/source.json` (prose vendoring, see above); `.gm/instructions/fsm/graph.json` (phase-graph override, write path is the `fsm-vendor` verb, see SKILL.md); `.gm/browser-config.json` (`BrowserConfig::load`, per-project -- `cdp_poll_timeout_ms` default 1000, `cdp_poll_interval_ms` default 250, `chrome_ready_deadline_ms` default 30000, `eval_timeout_grace_ms` default 6000, `headless` default false, `session_idle_timeout_ms` default 1800000); `.gm/daemon-project-config.json` (`gm_concurrency_limit`, per-project fairness cap on the shared pool); `~/.agentplug/daemon-config.json` (`DaemonConfig::load`, machine-wide daemon lifecycle timing, including the `side_plugin_concurrency` pool-size knob for the shared bert/treesitter/libsql instances); `.gm/disciplines/enabled.txt` + `.gm/disciplines/<name>/policy.md` (above); `AGENTPLUG_NO_DAEMON`/`CLAUDE_PROJECT_DIR`/`GM_PLUGKIT_SKIP_SELF_STALE_CHECK` (env toggles, SKILL.md documents the third). Daemon-scoped fields (`daemon-project-config.json`, `~/.agentplug/daemon-config.json`) fully detailed in `.gm/daemon-config-reference.md`; this line is the top-level index into that doc and the others.
108
+ **Every runtime config surface, indexed**: `.gm/instructions/<key>.md` + `.gm/instructions/source.json` (prose vendoring, see above); `.gm/instructions/fsm/graph.json` (phase-graph override, write path is the `fsm-vendor` verb, see SKILL.md); `.gm/instructions/hooks/<name>.js` (jit-hook: a real `exec_js` script the orchestrator runs automatically at a gate's evaluation, wired in via a `GateDef`'s `hook` field + `hook_mode` -- `"hook-only"` replaces the compiled predicate, `"both"` requires both, default `"predicate-only"` ignores the hook; an explicit `return true` passes, anything else -- `false`, a thrown error, non-boolean, missing `return`, missing/unreadable file -- fails CLOSED); `.gm/browser-config.json` (`BrowserConfig::load`, per-project -- `cdp_poll_timeout_ms` default 1000, `cdp_poll_interval_ms` default 250, `chrome_ready_deadline_ms` default 30000, `eval_timeout_grace_ms` default 6000, `headless` default false, `session_idle_timeout_ms` default 1800000); `.gm/daemon-project-config.json` (`gm_concurrency_limit`, per-project fairness cap on the shared pool); `~/.agentplug/daemon-config.json` (`DaemonConfig::load`, machine-wide daemon lifecycle timing, including the `side_plugin_concurrency` pool-size knob for the shared bert/treesitter/libsql instances); `.gm/disciplines/enabled.txt` + `.gm/disciplines/<name>/policy.md` (above); `AGENTPLUG_NO_DAEMON`/`CLAUDE_PROJECT_DIR`/`GM_PLUGKIT_SKIP_SELF_STALE_CHECK` (env toggles, SKILL.md documents the third). Daemon-scoped fields (`daemon-project-config.json`, `~/.agentplug/daemon-config.json`) fully detailed in `.gm/daemon-config-reference.md`; this line is the top-level index into that doc and the others.
109
109
 
110
110
  **Nothing fake in source the user runs**: stub/mock/placeholder-return/fixture-only-path/demo-mode-short-circuit/"TODO: implement" forbidden in shipped code. Scaffolds/shims permitted only delegating to real behavior (upstream API, subprocess, disk); check for an existing library before adding a shim. Detection = behavioral: always-succeeds, input-invariant, or type-signature-satisfying short-circuit = stub. Acceptance = real input through real code into real output, witnessed. `Mock*`/`Fake*`/`Stub*` names checked at VERIFY explicitly, not left prose-only (`recall: nevil false-completion mock-network incident`). A row marked `completed` by documenting it (a README "Future Optimizations" section, a design note) instead of implementing it is the same violation -- `prd-resolve` structurally refuses two PRD rows sharing byte-identical `witness_evidence` text (`deviation.prd-resolve-duplicate-witness`), the copy-paste tell of rubber-stamped completion.
111
111
 
package/bin/bootstrap.js CHANGED
@@ -6,7 +6,30 @@ const path = require('path');
6
6
  const os = require('os');
7
7
  const crypto = require('crypto');
8
8
  const { spawnSync } = require('child_process');
9
- const { ensureDir, pidAlive, sha256OfFile, sha256OfFileSync } = require('../gm-plugkit/gm-process');
9
+ const { pidAlive, sha256OfFile, sha256OfFileSync } = require('../gm-plugkit/gm-process');
10
+ const shared = require('../gm-plugkit/bootstrap-shared');
11
+ const {
12
+ obsEvent,
13
+ cacheRoot,
14
+ fallbackCacheRoot,
15
+ gmToolsDir,
16
+ ensureDir,
17
+ acquireLock,
18
+ releaseLock,
19
+ isLockStale,
20
+ pruneOldVersions,
21
+ healIfShaMatches,
22
+ daemonVersionSentinel,
23
+ readDaemonVersion,
24
+ writeDaemonVersion,
25
+ pidCommandLineForKillGuard,
26
+ pidIsPlugkitProcess,
27
+ writeKillAttribution,
28
+ killPid,
29
+ killSpoolWatcherInCwd,
30
+ proactiveKillForNewInstall,
31
+ ensureNextStepWiring,
32
+ } = shared;
10
33
 
11
34
  const NPM_PACKAGE = 'plugkit-wasm';
12
35
  const ATTEMPT_TIMEOUT_MS = 10 * 60 * 1000;
@@ -90,55 +113,6 @@ function ensureSkillMdCurrent(wrapperDir) {
90
113
  return { refreshed: allRefreshed };
91
114
  }
92
115
 
93
- function ensureNextStepWiring(cwd) {
94
- const changes = [];
95
- const gmDir = path.join(cwd, '.gm');
96
- try { fs.mkdirSync(gmDir, { recursive: true }); }
97
- catch (e) { obsEvent('bootstrap', 'next-step.wiring.target-failed', { target: gmDir, error: e.message }); }
98
-
99
- const nextStepPath = path.join(gmDir, 'next-step.md');
100
- const nextStepBody = '# Next step\n\nDispatch the `instruction` verb to plugkit by writing `.gm/exec-spool/in/instruction/<N>.txt` (any unique N) with body `{}` (or `{"prompt":"<user request>"}` on the first dispatch of the turn). Read the response from `.gm/exec-spool/out/<N>.json` and follow the imperative in the `instruction` field.\n\nThis file is auto-rewritten by plugkit on every instruction dispatch.\n';
101
- try {
102
- if (!fs.existsSync(nextStepPath)) {
103
- fs.writeFileSync(nextStepPath, nextStepBody);
104
- changes.push('seeded .gm/next-step.md');
105
- }
106
- } catch (e) { obsEvent('bootstrap', 'next-step.wiring.target-failed', { target: nextStepPath, error: e.message }); }
107
-
108
- const claudeMdPath = path.join(cwd, 'CLAUDE.md');
109
- try {
110
- if (!fs.existsSync(claudeMdPath)) {
111
- fs.writeFileSync(claudeMdPath, '@AGENTS.md\n');
112
- changes.push('created CLAUDE.md');
113
- } else {
114
- const cur = fs.readFileSync(claudeMdPath, 'utf8');
115
- const hasLine = cur.split(/\r?\n/).some(l => l.trim() === '@AGENTS.md');
116
- if (!hasLine) {
117
- fs.writeFileSync(claudeMdPath, '@AGENTS.md\n' + cur);
118
- changes.push('prepended @AGENTS.md to CLAUDE.md');
119
- }
120
- }
121
- } catch (e) { obsEvent('bootstrap', 'next-step.wiring.target-failed', { target: claudeMdPath, error: e.message }); }
122
-
123
- const agentsMdPath = path.join(cwd, 'AGENTS.md');
124
- try {
125
- if (fs.existsSync(agentsMdPath)) {
126
- const cur = fs.readFileSync(agentsMdPath, 'utf8');
127
- const hasLine = cur.split(/\r?\n/).some(l => l.trim() === '@.gm/next-step.md');
128
- if (!hasLine) {
129
- const sep = cur.endsWith('\n') ? '' : '\n';
130
- fs.writeFileSync(agentsMdPath, cur + sep + '\n@.gm/next-step.md\n');
131
- changes.push('appended @.gm/next-step.md to AGENTS.md');
132
- }
133
- }
134
- } catch (e) { obsEvent('bootstrap', 'next-step.wiring.target-failed', { target: agentsMdPath, error: e.message }); }
135
-
136
- if (changes.length > 0) {
137
- log(`next-step wiring: ${changes.join(', ')}`);
138
- obsEvent('bootstrap', 'next-step.wiring.applied', { changes });
139
- }
140
- }
141
-
142
116
  function resolveWindowsExe(cmd) {
143
117
  if (process.platform !== 'win32') return cmd;
144
118
  try {
@@ -180,6 +154,7 @@ function writeBootstrapError(spec) {
180
154
  }
181
155
 
182
156
  function clearBootstrapError() {
157
+ // best-effort, missing file is fine
183
158
  try {
184
159
  const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
185
160
  const out = path.join(projectDir, '.gm', 'exec-spool', '.bootstrap-error.json');
@@ -187,52 +162,6 @@ function clearBootstrapError() {
187
162
  } catch (_) {}
188
163
  }
189
164
 
190
- function obsEvent(subsystem, event, fields) {
191
- if (process.env.GM_LOG_DISABLE) return;
192
- try {
193
- const root = process.env.GM_LOG_DIR
194
- || path.join(os.homedir(), '.claude', 'gm-log');
195
- const day = new Date().toISOString().slice(0, 10);
196
- const dir = path.join(root, day);
197
- fs.mkdirSync(dir, { recursive: true });
198
- const line = JSON.stringify({
199
- ts: new Date().toISOString(),
200
- sub: subsystem,
201
- event,
202
- pid: process.pid,
203
- sess: process.env.CLAUDE_SESSION_ID || process.env.GM_SESSION_ID || '',
204
- ...fields,
205
- });
206
- fs.appendFileSync(path.join(dir, `${subsystem}.jsonl`), line + '\n');
207
- } catch (_) {}
208
- }
209
-
210
-
211
- function cacheRoot() {
212
- const home = os.homedir();
213
- if (process.env.PLUGKIT_CACHE_DIR) return process.env.PLUGKIT_CACHE_DIR;
214
- if (os.platform() === 'win32') {
215
- const base = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
216
- return path.join(base, 'plugkit', 'bin');
217
- }
218
- if (os.platform() === 'darwin') return path.join(home, 'Library', 'Caches', 'plugkit', 'bin');
219
- const xdg = process.env.XDG_CACHE_HOME || path.join(home, '.cache');
220
- return path.join(xdg, 'plugkit', 'bin');
221
- }
222
-
223
- function fallbackCacheRoot() {
224
- return path.join(os.tmpdir(), 'plugkit-cache', 'bin');
225
- }
226
-
227
- function gmToolsDir() {
228
- const home = process.env.USERPROFILE || process.env.HOME || os.homedir();
229
- const primary = path.join(home, '.gm-tools');
230
- const fallback = path.join(home, '.claude', 'gm-tools');
231
- if (fs.existsSync(primary)) return primary;
232
- if (fs.existsSync(fallback)) return fallback;
233
- return primary;
234
- }
235
-
236
165
  function copyWasmToGmTools(wasmPath, wrapperDir, version) {
237
166
  const dst = gmToolsDir();
238
167
  fs.mkdirSync(dst, { recursive: true });
@@ -262,22 +191,6 @@ function readVersionFile(wrapperDir) {
262
191
  }
263
192
 
264
193
 
265
- function healIfShaMatches(binPath, expectedSha, sentinelPath, partialPath, kind) {
266
- if (!fs.existsSync(binPath)) return false;
267
- if (partialPath) { try { if (fs.existsSync(partialPath)) fs.unlinkSync(partialPath); } catch (_) {} }
268
- if (!expectedSha) return false;
269
- let got;
270
- try { got = sha256OfFileSync(binPath); }
271
- catch (_) { return false; }
272
- if (got !== expectedSha) {
273
- try { fs.unlinkSync(binPath); } catch (_) {}
274
- return false;
275
- }
276
- try { fs.writeFileSync(sentinelPath, new Date().toISOString()); } catch (_) { return false; }
277
- obsEvent('bootstrap', 'cache.heal', { path: binPath, kind });
278
- return true;
279
- }
280
-
281
194
  function readShaManifest(wrapperDir, manifestName) {
282
195
  const p = path.join(wrapperDir, manifestName || 'plugkit.sha256');
283
196
  if (!fs.existsSync(p)) return null;
@@ -289,37 +202,6 @@ function readShaManifest(wrapperDir, manifestName) {
289
202
  return out;
290
203
  }
291
204
 
292
- function acquireLock(lockPath) {
293
- const start = Date.now();
294
- for (;;) {
295
- try {
296
- const fd = fs.openSync(lockPath, 'wx');
297
- fs.writeSync(fd, String(process.pid));
298
- fs.closeSync(fd);
299
- return true;
300
- } catch (err) {
301
- if (err.code !== 'EEXIST') throw err;
302
- let stale = false;
303
- try {
304
- const st = fs.statSync(lockPath);
305
- if (Date.now() - st.mtimeMs > LOCK_STALE_MS) stale = true;
306
- const owner = parseInt(fs.readFileSync(lockPath, 'utf8').trim(), 10);
307
- if (Number.isFinite(owner) && owner !== process.pid && !pidAlive(owner)) stale = true;
308
- } catch (_) { stale = true; }
309
- if (stale) {
310
- try { fs.unlinkSync(lockPath); } catch (_) {}
311
- continue;
312
- }
313
- if (Date.now() - start > ATTEMPT_TIMEOUT_MS) throw new Error(`lock wait timeout: ${lockPath}`);
314
- try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2000); } catch (_) {}
315
- }
316
- }
317
- }
318
-
319
- function releaseLock(lockPath) {
320
- try { fs.unlinkSync(lockPath); } catch (_) {}
321
- }
322
-
323
205
  async function extractNpmPackageWasm(destPath, version) {
324
206
  const tempDir = path.join(path.dirname(destPath), '.npm-extract-' + Date.now());
325
207
  try {
@@ -342,7 +224,6 @@ async function extractNpmPackageWasm(destPath, version) {
342
224
  encoding: 'utf8',
343
225
  windowsHide: true,
344
226
  ...(isCmdShim ? { shell: true } : {}),
345
- ...(process.platform === 'win32' ? { creationFlags: 0x08000000 } : {}),
346
227
  }
347
228
  );
348
229
 
@@ -393,34 +274,6 @@ async function extractNpmPackageWithRetry(destPath, version) {
393
274
  throw lastErr;
394
275
  }
395
276
 
396
- function isLockStale(lockPath) {
397
- try {
398
- const st = fs.statSync(lockPath);
399
- if (Date.now() - st.mtimeMs > LOCK_STALE_MS) return true;
400
- const owner = parseInt(fs.readFileSync(lockPath, 'utf8').trim(), 10);
401
- if (Number.isFinite(owner) && !pidAlive(owner)) return true;
402
- } catch (_) { return true; }
403
- return false;
404
- }
405
-
406
- function pruneOldVersions(root, keepVersion) {
407
- try {
408
- const entries = fs.readdirSync(root);
409
- for (const e of entries) {
410
- if (!e.startsWith('v')) continue;
411
- if (e === `v${keepVersion}`) continue;
412
- const dir = path.join(root, e);
413
- const lock = path.join(dir, '.lock');
414
- if (fs.existsSync(lock) && !isLockStale(lock)) continue;
415
- if (fs.existsSync(lock)) { try { fs.unlinkSync(lock); } catch (_) {} }
416
- try {
417
- fs.rmSync(dir, { recursive: true, force: true, maxRetries: 1, retryDelay: 50 });
418
- log(`pruned ${dir}`);
419
- } catch (err) { log(`prune skip ${dir}: ${err.message}`); }
420
- }
421
- } catch (_) {}
422
- }
423
-
424
277
  async function bootstrap(opts) {
425
278
  opts = opts || {};
426
279
  const wrapperDir = opts.wrapperDir || __dirname;
@@ -565,91 +418,13 @@ function getWasmPath(opts) {
565
418
  return null;
566
419
  }
567
420
 
568
- function daemonVersionSentinel() {
569
- const root = (() => {
570
- try { const r = cacheRoot(); ensureDir(r); return r; }
571
- catch (_) { const r = fallbackCacheRoot(); ensureDir(r); return r; }
572
- })();
573
- return path.join(root, '.daemon-version');
574
- }
575
-
576
- function readDaemonVersion() {
577
- try { return fs.readFileSync(daemonVersionSentinel(), 'utf8').trim(); }
578
- catch (_) { return null; }
579
- }
580
-
581
- function writeDaemonVersion(v) {
582
- try { fs.writeFileSync(daemonVersionSentinel(), String(v)); } catch (_) {}
583
- }
584
-
585
- function pidCommandLineForKillGuard(pid) {
586
- try {
587
- const { spawnSync } = require('child_process');
588
- if (process.platform === 'win32') {
589
- const r = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', `(Get-CimInstance Win32_Process -Filter "ProcessId=${Number(pid)}").CommandLine`], { encoding: 'utf8', windowsHide: true, timeout: 5000 });
590
- return String((r && r.stdout) || '');
591
- }
592
- const r = spawnSync('ps', ['-p', String(pid), '-o', 'args='], { encoding: 'utf8', timeout: 5000 });
593
- return String((r && r.stdout) || '');
594
- } catch (_) { return ''; }
595
- }
596
-
597
- function pidIsPlugkitProcess(pid) {
598
- return /agentplug-runner(\.exe)?/i.test(pidCommandLineForKillGuard(pid));
599
- }
600
-
601
- function writeKillAttribution(targetSpoolDir, info) {
602
- try {
603
- fs.mkdirSync(targetSpoolDir, { recursive: true });
604
- fs.writeFileSync(path.join(targetSpoolDir, '.kill-attribution.json'), JSON.stringify({ killer_pid: process.pid, killer_cwd: process.cwd(), killer_script: __filename, ts: Date.now(), ...info }, null, 2));
605
- } catch (_) {}
606
- }
607
-
608
- function killPid(pid) {
609
- if (!Number.isFinite(pid) || pid === process.pid || !pidAlive(pid)) return false;
610
- try { process.kill(pid, 'SIGTERM'); }
611
- catch (_) { try { process.kill(pid); } catch (_) {} }
612
- if (os.platform() === 'win32' && pidAlive(pid)) {
613
- try {
614
- const { spawnSync } = require('child_process');
615
- spawnSync('taskkill', ['/F', '/PID', String(pid)], { stdio: 'ignore', windowsHide: true, timeout: 3000, killSignal: 'SIGKILL' });
616
- } catch (_) {}
617
- }
618
- return true;
619
- }
620
-
621
- function killSpoolWatcherInCwd(reason) {
622
- try {
623
- const pidPath = path.join(process.cwd(), '.gm', 'exec-spool', '.watcher.pid');
624
- if (!fs.existsSync(pidPath)) return null;
625
- const pid = parseInt(fs.readFileSync(pidPath, 'utf8').trim(), 10);
626
- if (pidAlive(pid) && !pidIsPlugkitProcess(pid)) {
627
- obsEvent('bootstrap', 'watcher.kill-skipped-pid-reused', { pid, reason });
628
- try { fs.unlinkSync(pidPath); } catch (_) {}
629
- return null;
630
- }
631
- writeKillAttribution(path.join(process.cwd(), '.gm', 'exec-spool'), { reason, target_pid: pid, via: 'killSpoolWatcherInCwd' });
632
- if (killPid(pid)) {
633
- obsEvent('bootstrap', 'watcher.killed', { pid, reason });
634
- try { fs.unlinkSync(pidPath); } catch (_) {}
635
- return pid;
636
- }
637
- try { fs.unlinkSync(pidPath); } catch (_) {}
638
- } catch (_) {}
639
- return null;
640
- }
641
-
642
- function proactiveKillForNewInstall(installedVersion) {
643
- try {
644
- const reason = `install:v${installedVersion}`;
645
- killSpoolWatcherInCwd(reason);
646
- writeDaemonVersion(installedVersion);
647
- } catch (_) {}
648
- }
649
-
650
421
  function killStaleDaemonIfVersionChanged(wrapperDir) {
651
422
  let currentVersion;
652
- try { currentVersion = readVersionFile(wrapperDir); } catch (_) { return; }
423
+ try { currentVersion = readVersionFile(wrapperDir); }
424
+ catch (e) {
425
+ obsEvent('bootstrap', 'kill-stale-daemon.version-read-failed', { error: e.message });
426
+ return;
427
+ }
653
428
  const cached = getWasmPath({ wrapperDir, version: currentVersion });
654
429
  if (cached) {
655
430
  proactiveKillForNewInstall(currentVersion);
@@ -6,8 +6,35 @@ const path = require('path');
6
6
  const os = require('os');
7
7
  const crypto = require('crypto');
8
8
  const { spawn, spawnSync } = require('child_process');
9
- const { logEvent: _sharedLogEvent } = require('./gm-log');
10
- const { pidCommandLineForKillGuard: _sharedPidCommandLine, ensureDir, pidAlive, sha256OfFile, sha256OfFileSync } = require('./gm-process');
9
+ const { pidAlive, sha256OfFile, sha256OfFileSync } = require('./gm-process');
10
+ const shared = require('./bootstrap-shared');
11
+ const {
12
+ obsEvent,
13
+ cacheRoot,
14
+ fallbackCacheRoot,
15
+ gmToolsDir,
16
+ ensureDir,
17
+ acquireLock,
18
+ releaseLock,
19
+ isLockStale,
20
+ pruneOldVersions,
21
+ healIfShaMatches,
22
+ daemonVersionSentinel,
23
+ readDaemonVersion,
24
+ writeDaemonVersion,
25
+ pidCommandLineForKillGuard,
26
+ pidIsPlugkitProcess,
27
+ writeKillAttribution,
28
+ killPid,
29
+ killSpoolWatcherInCwd,
30
+ proactiveKillForNewInstall,
31
+ } = shared;
32
+
33
+ // ensureNextStepWiring here is intentionally NOT imported from
34
+ // bootstrap-shared -- this file's version does strictly more (CLAUDE.md
35
+ // prepend, AGENTS.md append, managed .npmignore block) than bin/bootstrap.js's
36
+ // leaner one, so it is genuinely-different logic, not accidental drift, and
37
+ // stays local to this file.
11
38
 
12
39
  function resolveWindowsExe(cmd) {
13
40
  if (process.platform !== 'win32') return cmd;
@@ -40,10 +67,6 @@ function log(msg) {
40
67
  try { process.stderr.write(`[gm-plugkit] ${msg}\n`); } catch (_) {}
41
68
  }
42
69
 
43
- function obsEvent(subsystem, event, fields) {
44
- _sharedLogEvent(subsystem, event, fields, { cwd: false });
45
- }
46
-
47
70
  function writeBootstrapError(spec) {
48
71
  try {
49
72
  const projectDir = resolveProjectRoot(process.env.CLAUDE_PROJECT_DIR || process.cwd());
@@ -54,6 +77,7 @@ function writeBootstrapError(spec) {
54
77
  }
55
78
 
56
79
  function clearBootstrapError() {
80
+ // best-effort, missing file is fine
57
81
  try {
58
82
  const projectDir = resolveProjectRoot(process.env.CLAUDE_PROJECT_DIR || process.cwd());
59
83
  fs.unlinkSync(path.join(projectDir, '.gm', 'exec-spool', '.bootstrap-error.json'));
@@ -79,7 +103,12 @@ function instructionsManifestPath(cwd) {
79
103
 
80
104
  function readInstructionsManifest(cwd) {
81
105
  try { return JSON.parse(fs.readFileSync(instructionsManifestPath(cwd), 'utf-8')); }
82
- catch (_) { return {}; }
106
+ catch (e) {
107
+ if (e && e.code !== 'ENOENT') {
108
+ obsEvent('bootstrap', 'instructions-bundle.manifest-read-failed', { error: e.message });
109
+ }
110
+ return {};
111
+ }
83
112
  }
84
113
 
85
114
  function writeInstructionsManifest(cwd, manifest) {
@@ -229,31 +258,6 @@ function ensureNextStepWiring(cwd) {
229
258
  }
230
259
 
231
260
 
232
- function cacheRoot() {
233
- const home = os.homedir();
234
- if (process.env.PLUGKIT_CACHE_DIR) return process.env.PLUGKIT_CACHE_DIR;
235
- if (os.platform() === 'win32') {
236
- const base = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
237
- return path.join(base, 'plugkit', 'bin');
238
- }
239
- if (os.platform() === 'darwin') return path.join(home, 'Library', 'Caches', 'plugkit', 'bin');
240
- const xdg = process.env.XDG_CACHE_HOME || path.join(home, '.cache');
241
- return path.join(xdg, 'plugkit', 'bin');
242
- }
243
-
244
- function fallbackCacheRoot() {
245
- return path.join(os.tmpdir(), 'plugkit-cache', 'bin');
246
- }
247
-
248
- function gmToolsDir() {
249
- const home = process.env.USERPROFILE || process.env.HOME || os.homedir();
250
- const primary = path.join(home, '.gm-tools');
251
- const fallback = path.join(home, '.claude', 'gm-tools');
252
- if (fs.existsSync(primary)) return primary;
253
- if (fs.existsSync(fallback)) return fallback;
254
- return primary;
255
- }
256
-
257
261
  // Slim-artifact eligibility: plugkit-core's embed.rs probes host_vec_embed
258
262
  // before ever loading its wasm-embedded safetensors fallback (see
259
263
  // rs-plugkit/crates/plugkit-core/src/embed.rs::init_ctx) -- a slim build
@@ -311,6 +315,13 @@ function readVersionFile() {
311
315
  return fs.readFileSync(p, 'utf8').trim();
312
316
  }
313
317
 
318
+ // readVersionFile() throws loudly on its own (missing/unreadable file is a
319
+ // real, surfaced error) -- callers below that swallow it are choosing to
320
+ // no-op rather than propagate, so they log what they swallowed instead of
321
+ // going silent. This is the actual behavior at every call site already
322
+ // (killStaleDaemonIfVersionChanged just returns, ensureReady leaves
323
+ // pinnedVersion null) -- only the missing observability was the gap.
324
+
314
325
  function readShaManifest() {
315
326
  const p = path.join(wrapperDir, 'plugkit.sha256');
316
327
  if (!fs.existsSync(p)) return null;
@@ -340,37 +351,6 @@ function readShaManifest() {
340
351
  return out;
341
352
  }
342
353
 
343
- function acquireLock(lockPath) {
344
- const start = Date.now();
345
- for (;;) {
346
- try {
347
- const fd = fs.openSync(lockPath, 'wx');
348
- fs.writeSync(fd, String(process.pid));
349
- fs.closeSync(fd);
350
- return true;
351
- } catch (err) {
352
- if (err.code !== 'EEXIST') throw err;
353
- let stale = false;
354
- try {
355
- const st = fs.statSync(lockPath);
356
- if (Date.now() - st.mtimeMs > LOCK_STALE_MS) stale = true;
357
- const owner = parseInt(fs.readFileSync(lockPath, 'utf8').trim(), 10);
358
- if (Number.isFinite(owner) && owner !== process.pid && !pidAlive(owner)) stale = true;
359
- } catch (_) { stale = true; }
360
- if (stale) {
361
- try { fs.unlinkSync(lockPath); } catch (_) {}
362
- continue;
363
- }
364
- if (Date.now() - start > ATTEMPT_TIMEOUT_MS) throw new Error(`lock wait timeout: ${lockPath}`);
365
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2000);
366
- }
367
- }
368
- }
369
-
370
- function releaseLock(lockPath) {
371
- try { fs.unlinkSync(lockPath); } catch (_) {}
372
- }
373
-
374
354
  async function extractNpmPackageWasm(destPath, version) {
375
355
  const tempDir = path.join(path.dirname(destPath), '.npm-extract-' + Date.now());
376
356
  try {
@@ -395,7 +375,6 @@ async function extractNpmPackageWasm(destPath, version) {
395
375
  encoding: 'utf8',
396
376
  windowsHide: true,
397
377
  ...(isCmdShim ? { shell: true } : {}),
398
- ...(process.platform === 'win32' ? { creationFlags: 0x08000000 } : {}),
399
378
  });
400
379
 
401
380
  if (result.error) throw result.error;
@@ -533,125 +512,13 @@ async function extractNpmPackageWithRetry(destPath, version) {
533
512
  }
534
513
 
535
514
 
536
- function healIfShaMatches(binPath, expectedSha, sentinelPath, partialPath, kind) {
537
- if (!fs.existsSync(binPath)) return false;
538
- if (partialPath) { try { if (fs.existsSync(partialPath)) fs.unlinkSync(partialPath); } catch (_) {} }
539
- if (!expectedSha) return false;
540
- let got;
541
- try { got = sha256OfFileSync(binPath); }
542
- catch (_) { return false; }
543
- if (got !== expectedSha) {
544
- try { fs.unlinkSync(binPath); } catch (_) {}
545
- return false;
546
- }
547
- try { fs.writeFileSync(sentinelPath, new Date().toISOString()); } catch (_) { return false; }
548
- obsEvent('bootstrap', 'cache.heal', { path: binPath, kind });
549
- return true;
550
- }
551
-
552
- function daemonVersionSentinel() {
553
- const root = (() => {
554
- try { const r = cacheRoot(); ensureDir(r); return r; }
555
- catch (_) { const r = fallbackCacheRoot(); ensureDir(r); return r; }
556
- })();
557
- return path.join(root, '.daemon-version');
558
- }
559
-
560
- function readDaemonVersion() {
561
- try { return fs.readFileSync(daemonVersionSentinel(), 'utf8').trim(); }
562
- catch (_) { return null; }
563
- }
564
-
565
- function writeDaemonVersion(v) {
566
- try { fs.writeFileSync(daemonVersionSentinel(), String(v)); } catch (_) {}
567
- }
568
-
569
- function pidCommandLineForKillGuard(pid) {
570
- return _sharedPidCommandLine(pid);
571
- }
572
-
573
- function pidIsPlugkitProcess(pid) {
574
- return /agentplug-runner(\.exe)?/i.test(pidCommandLineForKillGuard(pid));
575
- }
576
-
577
- function writeKillAttribution(targetSpoolDir, info) {
578
- try {
579
- fs.mkdirSync(targetSpoolDir, { recursive: true });
580
- fs.writeFileSync(path.join(targetSpoolDir, '.kill-attribution.json'), JSON.stringify({ killer_pid: process.pid, killer_cwd: process.cwd(), killer_script: __filename, ts: Date.now(), ...info }, null, 2));
581
- } catch (_) {}
582
- }
583
-
584
- function killPid(pid) {
585
- if (!Number.isFinite(pid) || pid === process.pid || !pidAlive(pid)) return false;
586
- try { process.kill(pid, 'SIGTERM'); }
587
- catch (_) { try { process.kill(pid); } catch (_) {} }
588
- if (os.platform() === 'win32' && pidAlive(pid)) {
589
- try { spawnSync('taskkill', ['/F', '/PID', String(pid)], { stdio: 'ignore', windowsHide: true, timeout: 3000, killSignal: 'SIGKILL' }); } catch (_) {}
590
- }
591
- return true;
592
- }
593
-
594
- function killSpoolWatcherInCwd(reason) {
595
- try {
596
- const pidPath = path.join(process.cwd(), '.gm', 'exec-spool', '.watcher.pid');
597
- if (!fs.existsSync(pidPath)) return null;
598
- const pid = parseInt(fs.readFileSync(pidPath, 'utf8').trim(), 10);
599
- if (pidAlive(pid) && !pidIsPlugkitProcess(pid)) {
600
- obsEvent('bootstrap', 'watcher.kill-skipped-pid-reused', { pid, reason });
601
- try { fs.unlinkSync(pidPath); } catch (_) {}
602
- return null;
603
- }
604
- writeKillAttribution(path.join(process.cwd(), '.gm', 'exec-spool'), { reason, target_pid: pid, via: 'killSpoolWatcherInCwd' });
605
- if (killPid(pid)) {
606
- obsEvent('bootstrap', 'watcher.killed', { pid, reason });
607
- try { fs.unlinkSync(pidPath); } catch (_) {}
608
- return pid;
609
- }
610
- try { fs.unlinkSync(pidPath); } catch (_) {}
611
- } catch (_) {}
612
- return null;
613
- }
614
-
615
-
616
- function isLockStale(lockPath) {
617
- try {
618
- const st = fs.statSync(lockPath);
619
- if (Date.now() - st.mtimeMs > LOCK_STALE_MS) return true;
620
- const owner = parseInt(fs.readFileSync(lockPath, 'utf8').trim(), 10);
621
- if (Number.isFinite(owner) && !pidAlive(owner)) return true;
622
- } catch (_) { return true; }
623
- return false;
624
- }
625
-
626
- function pruneOldVersions(root, keepVersion) {
627
- try {
628
- const entries = fs.readdirSync(root);
629
- for (const e of entries) {
630
- if (!e.startsWith('v')) continue;
631
- if (e === `v${keepVersion}`) continue;
632
- const dir = path.join(root, e);
633
- const lock = path.join(dir, '.lock');
634
- if (fs.existsSync(lock) && !isLockStale(lock)) continue;
635
- if (fs.existsSync(lock)) { try { fs.unlinkSync(lock); } catch (_) {} }
636
- try {
637
- fs.rmSync(dir, { recursive: true, force: true, maxRetries: 1, retryDelay: 50 });
638
- log(`pruned ${dir}`);
639
- } catch (err) { log(`prune skip ${dir}: ${err.message}`); }
640
- }
641
- } catch (_) {}
642
- }
643
-
644
- function proactiveKillForNewInstall(installedVersion) {
645
- try {
646
- const reason = `install:v${installedVersion}`;
647
- killSpoolWatcherInCwd(reason);
648
- writeDaemonVersion(installedVersion);
649
- } catch (_) {}
650
- }
651
-
652
515
  function killStaleDaemonIfVersionChanged() {
653
516
  let currentVersion;
654
- try { currentVersion = readVersionFile(); } catch (_) { return; }
517
+ try { currentVersion = readVersionFile(); }
518
+ catch (e) {
519
+ obsEvent('bootstrap', 'kill-stale-daemon.version-read-failed', { error: e.message });
520
+ return;
521
+ }
655
522
  const cached = resolveCachedBinary({ version: currentVersion });
656
523
  if (cached) {
657
524
  proactiveKillForNewInstall(currentVersion, cached);
@@ -976,7 +843,9 @@ function discoverBundledSkillsAndSources() {
976
843
  const p = path.join(root, e.name, 'SKILL.md');
977
844
  if (fs.existsSync(p) && !found.has(e.name)) found.set(e.name, p);
978
845
  }
979
- } catch (_) {}
846
+ } catch (e) {
847
+ obsEvent('bootstrap', 'discover-bundled-skills.dir-read-failed', { root, error: e.message });
848
+ }
980
849
  }
981
850
  return found;
982
851
  }
@@ -1060,7 +929,9 @@ async function resolveLatestRemoteVersion(timeoutMs) {
1060
929
  const hasPlugkitWasm = Array.isArray(rel.assets) && rel.assets.some(a => a && a.name === 'plugkit.wasm');
1061
930
  if (hasPlugkitWasm) return m[1];
1062
931
  }
1063
- } catch (_) {}
932
+ } catch (e) {
933
+ obsEvent('bootstrap', 'resolve-latest-remote-version.failed', { error: e.message });
934
+ }
1064
935
  return null;
1065
936
  }
1066
937
 
@@ -1069,7 +940,9 @@ async function resolveLatestGmPlugkitNpmVersion(timeoutMs) {
1069
940
  const buf = await httpGetBuffer('https://registry.npmjs.org/gm-plugkit/latest', timeoutMs || 3000);
1070
941
  const meta = JSON.parse(buf.toString('utf-8'));
1071
942
  if (meta && typeof meta.version === 'string') return meta.version;
1072
- } catch (_) {}
943
+ } catch (e) {
944
+ obsEvent('bootstrap', 'resolve-latest-npm-version.failed', { error: e.message });
945
+ }
1073
946
  return null;
1074
947
  }
1075
948
 
@@ -1120,7 +993,9 @@ async function ensureReady(opts) {
1120
993
  if (fs.existsSync(stalePath)) fs.unlinkSync(stalePath);
1121
994
  } catch (_) {}
1122
995
  }
1123
- } catch (_) {}
996
+ } catch (e) {
997
+ obsEvent('bootstrap', 'self-stale-check.failed', { error: e.message });
998
+ }
1124
999
  }
1125
1000
 
1126
1001
  let pinnedVersion = null;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-plugkit",
3
- "version": "2.0.2059",
3
+ "version": "2.0.2060",
4
4
  "description": "Bootstrap and daemon-spawn tool for gm plugkit binary. Downloads the correct platform wasm, verifies SHA256, and launches agentplug-runner (the native wasm host) as the spool watcher daemon.",
5
5
  "main": "index.js",
6
6
  "bin": {
package/gm.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm",
3
- "version": "2.0.2059",
3
+ "version": "2.0.2060",
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.2059",
3
+ "version": "2.0.2060",
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",