gm-skill 2.0.1222 → 2.0.1224

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.1222` — auto-bumped from the canonical `gm` repo. Every push to `AnEntrypoint/gm` (or any cascading sibling crate) republishes this package.
38
+ `2.0.1224` — 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
 
@@ -651,9 +651,48 @@ function ensureGitignored(cwd, entry) {
651
651
  } catch (_) {}
652
652
  }
653
653
 
654
+ function isProcessAliveSync(pid) {
655
+ if (!pid || typeof pid !== 'number' || pid <= 0) return false;
656
+ try {
657
+ process.kill(pid, 0);
658
+ return true;
659
+ } catch (e) {
660
+ return e && e.code === 'EPERM';
661
+ }
662
+ }
663
+
664
+ function readSingletonLockPid(profileDir) {
665
+ const lock = path.join(profileDir, 'SingletonLock');
666
+ try {
667
+ let target;
668
+ try {
669
+ target = fs.readlinkSync(lock);
670
+ } catch (_) {
671
+ try { target = fs.readFileSync(lock, 'utf-8'); } catch (__) { return null; }
672
+ }
673
+ if (!target) return null;
674
+ const m = String(target).match(/-(\d+)\s*$/);
675
+ if (m) return parseInt(m[1], 10);
676
+ const m2 = String(target).match(/(\d+)/);
677
+ if (m2) return parseInt(m2[1], 10);
678
+ } catch (_) {}
679
+ return null;
680
+ }
681
+
654
682
  function isProfileLocked(profileDir) {
655
683
  const lock = path.join(profileDir, 'SingletonLock');
656
- return fs.existsSync(lock);
684
+ if (!fs.existsSync(lock)) return false;
685
+ const holderPid = readSingletonLockPid(profileDir);
686
+ if (holderPid != null && !isProcessAliveSync(holderPid)) {
687
+ try { fs.unlinkSync(lock); } catch (_) {}
688
+ try { fs.unlinkSync(path.join(profileDir, 'SingletonCookie')); } catch (_) {}
689
+ try { fs.unlinkSync(path.join(profileDir, 'SingletonSocket')); } catch (_) {}
690
+ logEvent('bootstrap', 'browser-profile.lock-cleared', {
691
+ profileDir, dead_pid: holderPid,
692
+ });
693
+ return false;
694
+ }
695
+ return true;
657
696
  }
658
697
 
659
698
  function acquireProfileDir(cwd) {
@@ -669,6 +708,71 @@ function acquireProfileDir(cwd) {
669
708
  return fallback;
670
709
  }
671
710
 
711
+ function cleanDeadProfileFragments(cwd) {
712
+ try {
713
+ const gmDir = path.join(cwd, '.gm');
714
+ if (!fs.existsSync(gmDir)) return { cleaned: 0 };
715
+ let cleaned = 0;
716
+ for (const name of fs.readdirSync(gmDir)) {
717
+ const m = name.match(/^browser-profile-(\d+)$/);
718
+ if (!m) continue;
719
+ const pid = parseInt(m[1], 10);
720
+ if (!isProcessAliveSync(pid)) {
721
+ try {
722
+ fs.rmSync(path.join(gmDir, name), { recursive: true, force: true });
723
+ cleaned++;
724
+ } catch (_) {}
725
+ }
726
+ }
727
+ if (cleaned > 0) {
728
+ logEvent('bootstrap', 'browser-profile.hygiene', { cwd, cleaned });
729
+ }
730
+ return { cleaned };
731
+ } catch (_) {
732
+ return { cleaned: 0 };
733
+ }
734
+ }
735
+
736
+ function isPortReachableSync(host, port, timeoutMs) {
737
+ const r = spawnSync(process.execPath, ['-e', `
738
+ const net = require('net');
739
+ const s = net.connect({ port: ${port}, host: ${JSON.stringify(host)} });
740
+ let done = false;
741
+ s.on('connect', () => { done = true; s.destroy(); process.exit(0); });
742
+ s.on('error', () => { if (!done) process.exit(1); });
743
+ setTimeout(() => { if (!done) { s.destroy(); process.exit(1); } }, ${timeoutMs || 800});
744
+ `], { timeout: (timeoutMs || 800) + 2000 });
745
+ return r.status === 0;
746
+ }
747
+
748
+ let _acptoapiBoot = { spawned_at: 0, pid: null };
749
+ function ensureAcptoapi() {
750
+ const host = '127.0.0.1';
751
+ const port = 4800;
752
+ try {
753
+ if (isPortReachableSync(host, port, 500)) {
754
+ _acptoapiBoot = { spawned_at: 0, pid: null, status: 'already-running' };
755
+ return;
756
+ }
757
+ if (_acptoapiBoot.spawned_at && Date.now() - _acptoapiBoot.spawned_at < 30000) {
758
+ return;
759
+ }
760
+ const isWindows = process.platform === 'win32';
761
+ const cmd = isWindows ? 'bun.exe' : 'bun';
762
+ const child = spawn(cmd, ['x', 'acptoapi@latest'], {
763
+ detached: true,
764
+ stdio: 'ignore',
765
+ windowsHide: true,
766
+ shell: false,
767
+ });
768
+ child.unref();
769
+ _acptoapiBoot = { spawned_at: Date.now(), pid: child.pid, status: 'spawned' };
770
+ logEvent('bootstrap', 'acptoapi.spawned', { pid: child.pid, port });
771
+ } catch (e) {
772
+ logEvent('bootstrap', 'acptoapi.spawn-failed', { error: e && e.message });
773
+ }
774
+ }
775
+
672
776
  function findFreePortSync() {
673
777
  const r = spawnSync(process.execPath, ['-e', `
674
778
  const net = require('net');
@@ -722,10 +826,31 @@ function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
722
826
  const ports = readJsonFile(portsFile, {});
723
827
  const sessions = readJsonFile(sessionsFile, {});
724
828
  const existing = ports[claudeSessionId];
725
- if (existing && existing.port && isPortAliveSync(existing.port)) {
726
- const pwIds = sessions[claudeSessionId] || [];
727
- if (pwIds.length > 0) return pwIds[0];
829
+ if (existing && existing.port) {
830
+ const wantProfile = path.join(cwd, '.gm', 'browser-profile');
831
+ const pidOk = existing.pid && isProcessAliveSync(existing.pid);
832
+ const profileOk = !existing.profileDir || existing.profileDir === wantProfile || existing.profileDir.startsWith(path.join(cwd, '.gm', 'browser-profile'));
833
+ const portOk = isPortAliveSync(existing.port);
834
+ if (pidOk && profileOk && portOk) {
835
+ const pwIds = sessions[claudeSessionId] || [];
836
+ if (pwIds.length > 0) return pwIds[0];
837
+ } else {
838
+ const reason = !pidOk ? 'pid-dead' : !profileOk ? 'profile-drift' : 'port-dead';
839
+ logEvent('hook', 'deviation.browser-profile-collision', {
840
+ sid: claudeSessionId,
841
+ stale_pid: existing.pid || null,
842
+ stale_port: existing.port || null,
843
+ stale_profile: existing.profileDir || null,
844
+ want_profile: wantProfile,
845
+ reason,
846
+ });
847
+ delete ports[claudeSessionId];
848
+ delete sessions[claudeSessionId];
849
+ try { writeJsonFile(portsFile, ports); } catch (_) {}
850
+ try { writeJsonFile(sessionsFile, sessions); } catch (_) {}
851
+ }
728
852
  }
853
+ cleanDeadProfileFragments(cwd);
729
854
  const chrome = findChrome();
730
855
  if (!chrome) throw new Error('Chrome not found. Please install Google Chrome.');
731
856
  const profileDir = acquireProfileDir(cwd);
@@ -1385,7 +1510,12 @@ function makeHostFunctions(instanceRef) {
1385
1510
  });
1386
1511
  }
1387
1512
  const pwSessionId = getOrCreateBrowserSession(cwd, sessionId, pw);
1388
- const r = runPlaywriter(pw, ['-s', pwSessionId, '--timeout', '14000', '-e', body], 60000);
1513
+ const portsAfter = readJsonFile(browserPortsFile(cwd), {});
1514
+ const livePort = portsAfter[sessionId] && portsAfter[sessionId].port;
1515
+ const directArgs = (livePort && isPortAliveSync(livePort))
1516
+ ? [`--direct=localhost:${livePort}`]
1517
+ : [];
1518
+ const r = runPlaywriter(pw, ['-s', pwSessionId, '--timeout', '14000', ...directArgs, '-e', body], 60000);
1389
1519
  return writeWasmJson(instanceRef.value, {
1390
1520
  ok: r.status === 0,
1391
1521
  stdout: scrubBrowserRunnerText(r.stdout || ''),
@@ -1465,6 +1595,7 @@ async function runSpoolWatcher(instance, spoolDir) {
1465
1595
  fs.mkdirSync(outDir, { recursive: true });
1466
1596
 
1467
1597
  try { ensureSpoolPollGate(process.env.CLAUDE_PROJECT_DIR || process.cwd()); } catch (_) {}
1598
+ try { ensureAcptoapi(); } catch (_) {}
1468
1599
 
1469
1600
  const LOCK_PATH = path.join(spoolDir, '.watcher.lock');
1470
1601
  let _ownWrapperSha12 = '';
@@ -1971,6 +2102,7 @@ async function runSpoolWatcher(instance, spoolDir) {
1971
2102
  }
1972
2103
  setInterval(writeStatus, 5000);
1973
2104
  writeStatus();
2105
+ setInterval(() => { try { ensureAcptoapi(); } catch (_) {} }, 60000);
1974
2106
 
1975
2107
  const UPDATE_AVAILABLE_PATH = path.join(spoolDir, '.update-available.json');
1976
2108
  const UPDATE_CHECK_INTERVAL_MS = 5 * 60 * 1000;
package/gm.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm",
3
- "version": "2.0.1222",
3
+ "version": "2.0.1224",
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.1222",
3
+ "version": "2.0.1224",
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.1222"
42
+ "gm-plugkit": "^2.0.1224"
43
43
  },
44
44
  "engines": {
45
45
  "node": ">=16.0.0"