gm-skill 2.0.1222 → 2.0.1223

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.1223` — 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,31 @@ 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
+
672
736
  function findFreePortSync() {
673
737
  const r = spawnSync(process.execPath, ['-e', `
674
738
  const net = require('net');
@@ -722,10 +786,31 @@ function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
722
786
  const ports = readJsonFile(portsFile, {});
723
787
  const sessions = readJsonFile(sessionsFile, {});
724
788
  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];
789
+ if (existing && existing.port) {
790
+ const wantProfile = path.join(cwd, '.gm', 'browser-profile');
791
+ const pidOk = existing.pid && isProcessAliveSync(existing.pid);
792
+ const profileOk = !existing.profileDir || existing.profileDir === wantProfile || existing.profileDir.startsWith(path.join(cwd, '.gm', 'browser-profile'));
793
+ const portOk = isPortAliveSync(existing.port);
794
+ if (pidOk && profileOk && portOk) {
795
+ const pwIds = sessions[claudeSessionId] || [];
796
+ if (pwIds.length > 0) return pwIds[0];
797
+ } else {
798
+ const reason = !pidOk ? 'pid-dead' : !profileOk ? 'profile-drift' : 'port-dead';
799
+ logEvent('hook', 'deviation.browser-profile-collision', {
800
+ sid: claudeSessionId,
801
+ stale_pid: existing.pid || null,
802
+ stale_port: existing.port || null,
803
+ stale_profile: existing.profileDir || null,
804
+ want_profile: wantProfile,
805
+ reason,
806
+ });
807
+ delete ports[claudeSessionId];
808
+ delete sessions[claudeSessionId];
809
+ try { writeJsonFile(portsFile, ports); } catch (_) {}
810
+ try { writeJsonFile(sessionsFile, sessions); } catch (_) {}
811
+ }
728
812
  }
813
+ cleanDeadProfileFragments(cwd);
729
814
  const chrome = findChrome();
730
815
  if (!chrome) throw new Error('Chrome not found. Please install Google Chrome.');
731
816
  const profileDir = acquireProfileDir(cwd);
@@ -1385,7 +1470,12 @@ function makeHostFunctions(instanceRef) {
1385
1470
  });
1386
1471
  }
1387
1472
  const pwSessionId = getOrCreateBrowserSession(cwd, sessionId, pw);
1388
- const r = runPlaywriter(pw, ['-s', pwSessionId, '--timeout', '14000', '-e', body], 60000);
1473
+ const portsAfter = readJsonFile(browserPortsFile(cwd), {});
1474
+ const livePort = portsAfter[sessionId] && portsAfter[sessionId].port;
1475
+ const directArgs = (livePort && isPortAliveSync(livePort))
1476
+ ? [`--direct=localhost:${livePort}`]
1477
+ : [];
1478
+ const r = runPlaywriter(pw, ['-s', pwSessionId, '--timeout', '14000', ...directArgs, '-e', body], 60000);
1389
1479
  return writeWasmJson(instanceRef.value, {
1390
1480
  ok: r.status === 0,
1391
1481
  stdout: scrubBrowserRunnerText(r.stdout || ''),
package/gm.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm",
3
- "version": "2.0.1222",
3
+ "version": "2.0.1223",
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.1223",
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.1223"
43
43
  },
44
44
  "engines": {
45
45
  "node": ">=16.0.0"