gm-skill 2.0.1645 → 2.0.1647

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/bin/bootstrap.js CHANGED
@@ -619,25 +619,6 @@ function killPid(pid) {
619
619
  return true;
620
620
  }
621
621
 
622
- function killRunningDaemons(reason) {
623
- const tmp = os.tmpdir();
624
- const killedPids = [];
625
- for (const pidFile of ['glootie-runner.pid', 'plugkit-runner.pid']) {
626
- const pidPath = path.join(tmp, pidFile);
627
- if (!fs.existsSync(pidPath)) continue;
628
- try {
629
- const pid = parseInt(fs.readFileSync(pidPath, 'utf8').trim(), 10);
630
- if (killPid(pid)) {
631
- killedPids.push(pid);
632
- obsEvent('bootstrap', 'daemon.killed', { pid, pidFile, reason });
633
- }
634
- try { fs.unlinkSync(pidPath); } catch (_) {}
635
- } catch (_) {}
636
- }
637
- return killedPids;
638
- }
639
-
640
-
641
622
  function killSpoolWatcherInCwd(reason) {
642
623
  try {
643
624
  const pidPath = path.join(process.cwd(), '.gm', 'exec-spool', '.watcher.pid');
@@ -656,7 +637,6 @@ function killSpoolWatcherInCwd(reason) {
656
637
  function proactiveKillForNewInstall(installedVersion) {
657
638
  try {
658
639
  const reason = `install:v${installedVersion}`;
659
- killRunningDaemons(reason);
660
640
  killSpoolWatcherInCwd(reason);
661
641
  writeDaemonVersion(installedVersion);
662
642
  } catch (_) {}
@@ -672,11 +652,11 @@ function killStaleDaemonIfVersionChanged(wrapperDir) {
672
652
  }
673
653
  const recorded = readDaemonVersion();
674
654
  if (recorded === currentVersion) return;
675
- if (recorded) killRunningDaemons(`version_change:${recorded}->${currentVersion}`);
655
+ if (recorded) killSpoolWatcherInCwd(`version_change:${recorded}->${currentVersion}`);
676
656
  writeDaemonVersion(currentVersion);
677
657
  }
678
658
 
679
- module.exports = { bootstrap, getWasmPath, cacheRoot, obsEvent, killRunningDaemons, killStaleDaemonIfVersionChanged, killSpoolWatcherInCwd, proactiveKillForNewInstall };
659
+ module.exports = { bootstrap, getWasmPath, cacheRoot, obsEvent, killStaleDaemonIfVersionChanged, killSpoolWatcherInCwd, proactiveKillForNewInstall };
680
660
 
681
661
  if (require.main === module) {
682
662
  const argv = process.argv.slice(2);
package/bin/install.js CHANGED
@@ -99,7 +99,9 @@ function applyClaudeSettings(home) {
99
99
  const { obj, existed, corrupt } = readSettings(settingsPath);
100
100
  if (corrupt) {
101
101
  const backup = settingsPath + '.bak';
102
- try { fs.copyFileSync(settingsPath, backup); err(`existing settings.json was malformed; backed up to ${backup}`); } catch (_) {}
102
+ try { fs.copyFileSync(settingsPath, backup); } catch (_) {}
103
+ err(`existing settings.json was malformed; backed up to ${backup} and left untouched (not overwritten with defaults, to avoid discarding your other settings) -- fix the JSON manually, or delete it and re-run to get a fresh settings.json`);
104
+ return { settingsPath, existed, corrupt: true };
103
105
  }
104
106
  obj.effortLevel = 'low';
105
107
  obj.alwaysThinkingEnabled = false;
@@ -460,24 +460,6 @@ function killPid(pid) {
460
460
  return true;
461
461
  }
462
462
 
463
- function killRunningDaemons(reason) {
464
- const tmp = os.tmpdir();
465
- const killedPids = [];
466
- for (const pidFile of ['glootie-runner.pid', 'plugkit-runner.pid']) {
467
- const pidPath = path.join(tmp, pidFile);
468
- if (!fs.existsSync(pidPath)) continue;
469
- try {
470
- const pid = parseInt(fs.readFileSync(pidPath, 'utf8').trim(), 10);
471
- if (killPid(pid)) {
472
- killedPids.push(pid);
473
- obsEvent('bootstrap', 'daemon.killed', { pid, pidFile, reason });
474
- }
475
- try { fs.unlinkSync(pidPath); } catch (_) {}
476
- } catch (_) {}
477
- }
478
- return killedPids;
479
- }
480
-
481
463
  function killSpoolWatcherInCwd(reason) {
482
464
  try {
483
465
  const pidPath = path.join(process.cwd(), '.gm', 'exec-spool', '.watcher.pid');
@@ -525,7 +507,6 @@ function pruneOldVersions(root, keepVersion) {
525
507
  function proactiveKillForNewInstall(installedVersion) {
526
508
  try {
527
509
  const reason = `install:v${installedVersion}`;
528
- killRunningDaemons(reason);
529
510
  killSpoolWatcherInCwd(reason);
530
511
  writeDaemonVersion(installedVersion);
531
512
  } catch (_) {}
@@ -541,7 +522,7 @@ function killStaleDaemonIfVersionChanged() {
541
522
  }
542
523
  const recorded = readDaemonVersion();
543
524
  if (recorded === currentVersion) return;
544
- if (recorded) killRunningDaemons(`version_change:${recorded}->${currentVersion}`);
525
+ if (recorded) killSpoolWatcherInCwd(`version_change:${recorded}->${currentVersion}`);
545
526
  writeDaemonVersion(currentVersion);
546
527
  }
547
528
 
@@ -967,7 +948,7 @@ async function ensureReady(opts) {
967
948
  }
968
949
 
969
950
  if (versionDrift) {
970
- try { killRunningDaemons(`version_drift:${installed}->${targetVersion}`); } catch (_) {}
951
+ try { killSpoolWatcherInCwd(`version_drift:${installed}->${targetVersion}`); } catch (_) {}
971
952
  }
972
953
 
973
954
  ensureWrapperFresh();
@@ -1096,7 +1077,6 @@ module.exports = {
1096
1077
  isReady,
1097
1078
  cacheRoot,
1098
1079
  obsEvent,
1099
- killRunningDaemons,
1100
1080
  killStaleDaemonIfVersionChanged,
1101
1081
  killSpoolWatcherInCwd,
1102
1082
  proactiveKillForNewInstall,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-plugkit",
3
- "version": "2.0.1645",
3
+ "version": "2.0.1647",
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": {
@@ -550,9 +550,11 @@ const LEGACY_BROWSER_PORTS_FILE = path.join(TMP_DIR, 'plugkit-browser-ports.json
550
550
  const LEGACY_BROWSER_SESSIONS_FILE = path.join(TMP_DIR, 'plugkit-browser-sessions.json');
551
551
 
552
552
  const __browserRootCache = new Map();
553
+ const BROWSER_ROOT_CACHE_TTL_MS = 60_000;
553
554
  function browserRootDir(cwd) {
554
555
  const start = path.resolve(cwd || process.cwd());
555
- if (__browserRootCache.has(start)) return __browserRootCache.get(start);
556
+ const cached = __browserRootCache.get(start);
557
+ if (cached && Date.now() - cached.ts < BROWSER_ROOT_CACHE_TTL_MS) return cached.root;
556
558
  let root = start;
557
559
  try {
558
560
  const r = spawnSync('git', ['rev-parse', '--git-common-dir'], { cwd: start, encoding: 'utf-8', windowsHide: true, timeout: 1500 });
@@ -563,15 +565,18 @@ function browserRootDir(cwd) {
563
565
  }
564
566
  } catch (_) {}
565
567
  root = path.resolve(root);
566
- __browserRootCache.set(start, root);
568
+ __browserRootCache.set(start, { root, ts: Date.now() });
567
569
  return root;
568
570
  }
569
571
 
570
572
  function browserStateDir(cwd) {
571
- const dir = path.join(browserRootDir(cwd), '.gm', 'exec-spool');
573
+ const dir = path.join(browserRootDir(cwd), '.gm', 'browser-state');
572
574
  try { fs.mkdirSync(dir, { recursive: true }); } catch (_) {}
573
575
  return dir;
574
576
  }
577
+ function browserStateDirLegacyCoLocated(cwd) {
578
+ return path.join(browserRootDir(cwd), '.gm', 'exec-spool');
579
+ }
575
580
  function browserPortsFile(cwd) { return path.join(browserStateDir(cwd), 'browser-ports.json'); }
576
581
  function browserSessionsFile(cwd) { return path.join(browserStateDir(cwd), 'browser-sessions.json'); }
577
582
 
@@ -609,14 +614,23 @@ function atomicWriteJson(filePath, obj) {
609
614
  function migrateLegacyBrowserState(cwd) {
610
615
  const dst1 = browserPortsFile(cwd);
611
616
  const dst2 = browserSessionsFile(cwd);
617
+ const coLocated = browserStateDirLegacyCoLocated(cwd);
618
+ const coLocatedPorts = path.join(coLocated, 'browser-ports.json');
619
+ const coLocatedSessions = path.join(coLocated, 'browser-sessions.json');
612
620
  try {
613
- if (!fs.existsSync(dst1) && fs.existsSync(LEGACY_BROWSER_PORTS_FILE)) {
621
+ if (!fs.existsSync(dst1) && fs.existsSync(coLocatedPorts)) {
622
+ const legacy = JSON.parse(fs.readFileSync(coLocatedPorts, 'utf-8'));
623
+ if (legacy && typeof legacy === 'object') atomicWriteJson(dst1, legacy);
624
+ } else if (!fs.existsSync(dst1) && fs.existsSync(LEGACY_BROWSER_PORTS_FILE)) {
614
625
  const legacy = JSON.parse(fs.readFileSync(LEGACY_BROWSER_PORTS_FILE, 'utf-8'));
615
626
  if (legacy && typeof legacy === 'object') atomicWriteJson(dst1, legacy);
616
627
  }
617
628
  } catch (_) {}
618
629
  try {
619
- if (!fs.existsSync(dst2) && fs.existsSync(LEGACY_BROWSER_SESSIONS_FILE)) {
630
+ if (!fs.existsSync(dst2) && fs.existsSync(coLocatedSessions)) {
631
+ const legacy = JSON.parse(fs.readFileSync(coLocatedSessions, 'utf-8'));
632
+ if (legacy && typeof legacy === 'object') atomicWriteJson(dst2, legacy);
633
+ } else if (!fs.existsSync(dst2) && fs.existsSync(LEGACY_BROWSER_SESSIONS_FILE)) {
620
634
  const legacy = JSON.parse(fs.readFileSync(LEGACY_BROWSER_SESSIONS_FILE, 'utf-8'));
621
635
  if (legacy && typeof legacy === 'object') atomicWriteJson(dst2, legacy);
622
636
  }
@@ -2820,8 +2834,10 @@ async function runSpoolWatcher(instance, spoolDir) {
2820
2834
  if (!_ownWrapperSha12) return;
2821
2835
  let reg = {};
2822
2836
  try { reg = JSON.parse(fs.readFileSync(PEER_REGISTRY_PATH, 'utf-8')); } catch (_) { return; }
2837
+ const ownRoot = browserRootDir(process.cwd());
2823
2838
  for (const peerCwd of Object.keys(reg)) {
2824
2839
  if (peerCwd === process.cwd()) continue;
2840
+ if (browserRootDir(peerCwd) !== ownRoot) continue;
2825
2841
  const peerLock = path.join(peerCwd, '.gm', 'exec-spool', '.watcher.lock');
2826
2842
  let content = '';
2827
2843
  try { content = fs.readFileSync(peerLock, 'utf-8').trim(); } catch (_) { continue; }
package/gm.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm",
3
- "version": "2.0.1645",
3
+ "version": "2.0.1647",
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/lang/ssh.js CHANGED
@@ -11,6 +11,12 @@ function loadTarget(targetName) {
11
11
  ];
12
12
  const cfgPath = candidatesCfg.find(p => fsSync.existsSync(p));
13
13
  if (!cfgPath) throw new Error('No ssh-targets.json found at ' + candidatesCfg.join(' or '));
14
+ if (process.platform !== 'win32') {
15
+ try {
16
+ const mode = fsSync.statSync(cfgPath).mode & 0o777;
17
+ if (mode !== 0o600) fsSync.chmodSync(cfgPath, 0o600);
18
+ } catch (_) {}
19
+ }
14
20
  const cfg = JSON.parse(fsSync.readFileSync(cfgPath, 'utf8'));
15
21
  const name = targetName || 'default';
16
22
  if (!cfg[name]) throw new Error('No target \'' + name + '\' in ssh-targets.json. Available: ' + Object.keys(cfg).join(', '));
@@ -375,53 +375,40 @@ async function downloadPlugkitBinary(version) {
375
375
  }
376
376
  }
377
377
 
378
- function findPlugkitWasmPids() {
378
+ function repoStatePidsForCwd() {
379
379
  const pids = [];
380
+ const cwd = process.cwd();
381
+ for (const rel of [path.join('.gm', 'exec-spool', '.supervisor.pid'), path.join('.gm', 'exec-spool', '.watcher.pid')]) {
382
+ try {
383
+ const p = path.join(cwd, rel);
384
+ if (!fs.existsSync(p)) continue;
385
+ const pid = parseInt(fs.readFileSync(p, 'utf8').trim(), 10);
386
+ if (Number.isFinite(pid) && pid > 0) pids.push(String(pid));
387
+ } catch (_) {}
388
+ }
380
389
  try {
381
- if (process.platform === 'win32') {
382
- const ps = "Get-CimInstance Win32_Process -Filter \"Name='bun.exe' OR Name='node.exe'\" | Where-Object { $_.CommandLine -match 'plugkit-wasm-wrapper' } | Select-Object -ExpandProperty ProcessId";
383
- const output = execFileSync('powershell', ['-NoProfile', '-Command', ps], { encoding: 'utf8', windowsHide: true, timeout: 3000 });
384
- for (const line of output.split(/\r?\n/)) {
385
- const trimmed = line.trim();
386
- if (/^\d+$/.test(trimmed)) pids.push(trimmed);
387
- }
388
- } else {
389
- const output = execSync("ps -eo pid,args", { encoding: 'utf8', timeout: 3000 });
390
- const lines = output.split('\n').filter(Boolean);
391
- for (const line of lines) {
392
- if (!line.includes('plugkit-wasm-wrapper')) continue;
393
- const m = line.trim().match(/^(\d+)\s/);
394
- if (m) pids.push(m[1]);
395
- }
390
+ const statusPath = path.join(cwd, '.gm', 'exec-spool', '.status.json');
391
+ if (fs.existsSync(statusPath)) {
392
+ const status = JSON.parse(fs.readFileSync(statusPath, 'utf-8'));
393
+ if (Number.isFinite(status.pid) && status.pid > 0) pids.push(String(status.pid));
396
394
  }
397
- } catch (e) {
398
- }
399
- return pids;
395
+ } catch (_) {}
396
+ return Array.from(new Set(pids));
397
+ }
398
+
399
+ function findPlugkitWasmPids() {
400
+ return repoStatePidsForCwd().filter(pid => {
401
+ try { process.kill(parseInt(pid, 10), 0); return true; } catch (_) { return false; }
402
+ });
400
403
  }
401
404
 
402
405
  function isProcessRunning() {
403
- if (findPlugkitWasmPids().length > 0) return true;
404
- try {
405
- const supervisorPidFile = path.join(process.cwd(), '.gm', 'exec-spool', '.supervisor.pid');
406
- if (!fs.existsSync(supervisorPidFile)) return false;
407
- const pid = parseInt(fs.readFileSync(supervisorPidFile, 'utf8').trim(), 10);
408
- if (!Number.isFinite(pid) || pid <= 0) return false;
409
- try { process.kill(pid, 0); return true; } catch (_) { return false; }
410
- } catch (_) { return false; }
406
+ return findPlugkitWasmPids().length > 0;
411
407
  }
412
408
 
413
409
  function killExistingPlugkit() {
414
410
  try {
415
411
  const pids = findPlugkitWasmPids();
416
- try {
417
- const supervisorPidFile = path.join(process.cwd(), '.gm', 'exec-spool', '.supervisor.pid');
418
- if (fs.existsSync(supervisorPidFile)) {
419
- const sp = parseInt(fs.readFileSync(supervisorPidFile, 'utf8').trim(), 10);
420
- if (Number.isFinite(sp) && sp > 0) {
421
- try { process.kill(sp, 0); pids.unshift(String(sp)); } catch (_) {}
422
- }
423
- }
424
- } catch (_) {}
425
412
  if (pids.length === 0) {
426
413
  emitBootstrapEvent('info', 'No existing plugkit WASM watcher to kill');
427
414
  return;
@@ -429,9 +416,9 @@ function killExistingPlugkit() {
429
416
  for (const pid of pids) {
430
417
  try {
431
418
  if (process.platform === 'win32') {
432
- execSync(`taskkill /F /T /PID ${pid}`, { stdio: 'ignore' });
419
+ execFileSync('taskkill', ['/F', '/T', '/PID', String(pid)], { stdio: 'ignore', windowsHide: true });
433
420
  } else {
434
- execSync(`kill -9 ${pid}`, { stdio: 'ignore' });
421
+ execFileSync('kill', ['-9', String(pid)], { stdio: 'ignore' });
435
422
  }
436
423
  } catch (e) {
437
424
  }
@@ -157,8 +157,18 @@ function unsolicitedDocs(cwd) {
157
157
  if (r.status !== 0) return { count: 0, files: [], available: false };
158
158
  const flagged = [];
159
159
  for (const line of r.stdout.split('\n')) {
160
- if (!line.startsWith('?? ')) continue;
161
- const rel = line.slice(3).trim();
160
+ if (!line || line.length < 4) continue;
161
+ const code = line.slice(0, 2);
162
+ let rel;
163
+ if (code === '??') {
164
+ rel = line.slice(3).trim();
165
+ } else if (/^[MADRCU ][MADRCU ]$/.test(code)) {
166
+ const rest = line.slice(3).trim();
167
+ const arrowIdx = rest.indexOf(' -> ');
168
+ rel = arrowIdx >= 0 ? rest.slice(arrowIdx + 4).trim() : rest;
169
+ } else {
170
+ continue;
171
+ }
162
172
  if (!rel) continue;
163
173
  if (!/\.(md|txt)$/i.test(rel)) continue;
164
174
  const base = rel.includes('/') ? rel.slice(rel.lastIndexOf('/') + 1) : rel;
@@ -304,8 +314,21 @@ function checkDispatchGates(sessionId, operation, extra) {
304
314
 
305
315
  if (['bash', 'sh', 'shell', 'zsh', 'powershell', 'ps1'].includes(extra.verb)) {
306
316
  const cmd = String((extra.body && (extra.body.command || extra.body.code || extra.body.script)) || extra.command || '').trim();
307
- const first = cmd.split(/\s+/)[0] || '';
308
- const gitDominant = first === 'git' || /[\\/]git$/.test(first) || cmd.startsWith('git ');
317
+ const isGitToken = (tok) => tok === 'git' || /[\\/]git(\.exe)?$/i.test(tok);
318
+ const segments = cmd
319
+ .split(/&&|\|\||[;&|]|(?<=["'])\s*&\s*(?=["'])/)
320
+ .map((s) => s.trim())
321
+ .filter(Boolean);
322
+ const gitDominant = segments.some((seg) => {
323
+ const tokens = seg.replace(/^["']|["']$/g, '').split(/\s+/).filter(Boolean);
324
+ for (let i = 0; i < tokens.length; i++) {
325
+ const tok = tokens[i].replace(/^["']|["']$/g, '');
326
+ if (isGitToken(tok)) return true;
327
+ if (tok === 'env' || tok === 'exec' || tok === 'cd') continue;
328
+ break;
329
+ }
330
+ return false;
331
+ });
309
332
  if (gitDominant) {
310
333
  logDeviation('deviation.bash-git-bypass', { verb: extra.verb, cmd: cmd.slice(0, 80) });
311
334
  return {
package/lib/spool.js CHANGED
@@ -63,7 +63,8 @@ function writeSpool(body, lang = 'nodejs', options = {}) {
63
63
  code = prelude + body;
64
64
  }
65
65
 
66
- fs.writeFileSync(inFile, code, 'utf8');
66
+ const fd = fs.openSync(inFile, 'wx');
67
+ try { fs.writeSync(fd, code, null, 'utf8'); } finally { fs.closeSync(fd); }
67
68
 
68
69
  return {
69
70
  id: taskId,
@@ -80,7 +81,8 @@ function writeSpoolVerb(body, verb, options = {}) {
80
81
  const inDir = path.join(baseDir, 'in', validVerb);
81
82
  const inFile = path.join(inDir, `${taskId}.txt`);
82
83
  fs.mkdirSync(inDir, { recursive: true });
83
- fs.writeFileSync(inFile, body, 'utf8');
84
+ const fd = fs.openSync(inFile, 'wx');
85
+ try { fs.writeSync(fd, body, null, 'utf8'); } finally { fs.closeSync(fd); }
84
86
  return { id: taskId, path: inFile, verb: validVerb };
85
87
  }
86
88
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-skill",
3
- "version": "2.0.1645",
3
+ "version": "2.0.1647",
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",