gm-skill 2.0.1269 → 2.0.1271

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.1269` — auto-bumped from the canonical `gm` repo. Every push to `AnEntrypoint/gm` (or any cascading sibling crate) republishes this package.
38
+ `2.0.1271` — 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
 
@@ -420,106 +420,6 @@ function writeJsonFile(fp, value) {
420
420
  try { fs.writeFileSync(fp, JSON.stringify(value, null, 2)); } catch (_) {}
421
421
  }
422
422
 
423
- function bundledBrowserCacheRoots() {
424
- const roots = [];
425
- const envOverride = process.env.PLAYWRIGHT_BROWSERS_PATH;
426
- if (envOverride) roots.push(envOverride);
427
- if (process.platform === 'win32') {
428
- if (process.env.LOCALAPPDATA) roots.push(path.join(process.env.LOCALAPPDATA, 'ms-playwright'));
429
- } else if (process.platform === 'darwin') {
430
- if (process.env.HOME) roots.push(path.join(process.env.HOME, 'Library', 'Caches', 'ms-playwright'));
431
- } else {
432
- if (process.env.HOME) roots.push(path.join(process.env.HOME, '.cache', 'ms-playwright'));
433
- }
434
- return roots.filter(r => r && fs.existsSync(r));
435
- }
436
-
437
- function chromiumExeFromCacheRoot(root) {
438
- try {
439
- const entries = fs.readdirSync(root)
440
- .filter(n => /^chromium-\d+$/.test(n))
441
- .sort((a, b) => parseInt(b.split('-')[1], 10) - parseInt(a.split('-')[1], 10));
442
- const subdirs = process.platform === 'win32'
443
- ? ['chrome-win64', 'chrome-win']
444
- : process.platform === 'darwin'
445
- ? ['chrome-mac/Chromium.app/Contents/MacOS/Chromium']
446
- : ['chrome-linux'];
447
- const exeName = process.platform === 'win32' ? 'chrome.exe'
448
- : process.platform === 'darwin' ? null
449
- : 'chrome';
450
- for (const e of entries) {
451
- for (const sub of subdirs) {
452
- const p = exeName ? path.join(root, e, sub, exeName) : path.join(root, e, sub);
453
- if (fs.existsSync(p)) return p;
454
- }
455
- }
456
- } catch (_) {}
457
- return null;
458
- }
459
-
460
- function findBundledChromium() {
461
- for (const root of bundledBrowserCacheRoots()) {
462
- const exe = chromiumExeFromCacheRoot(root);
463
- if (exe) return exe;
464
- }
465
- try {
466
- const npmR = spawnSync('npm', ['root', '-g'], { encoding: 'utf-8', shell: true, windowsHide: true, timeout: 5000 });
467
- if (npmR.status === 0 && npmR.stdout.trim()) {
468
- const root = npmR.stdout.trim().split(/\r?\n/).pop();
469
- const localBrowsers = path.join(root, 'playwriter', 'node_modules', '@xmorse', 'playwright-core', '.local-browsers');
470
- if (fs.existsSync(localBrowsers)) {
471
- const exe = chromiumExeFromCacheRoot(localBrowsers);
472
- if (exe) return exe;
473
- }
474
- }
475
- } catch (_) {}
476
- return null;
477
- }
478
-
479
- function ensureBundledChromium(pw) {
480
- const existing = findBundledChromium();
481
- if (existing) return { exe: existing, installed: false };
482
- if (!pw || !pw.cmd) {
483
- return { exe: null, installed: false, error: 'playwriter not available to install browser' };
484
- }
485
- const installer = { cmd: pw.cmd, args: [...pw.baseArgs, 'install', 'chromium'], shell: pw.shell };
486
- logEvent('bootstrap', 'browser.bundled-install.start', { via: 'playwriter' });
487
- const r = spawnSync(installer.cmd, installer.args, {
488
- encoding: 'utf-8',
489
- timeout: 600000,
490
- shell: installer.shell,
491
- windowsHide: true,
492
- });
493
- logEvent('bootstrap', 'browser.bundled-install.done', { status: r.status });
494
- const after = findBundledChromium();
495
- if (after) return { exe: after, installed: true };
496
- return { exe: null, installed: false, error: r.stderr || r.stdout || 'install failed' };
497
- }
498
-
499
- function findChrome() {
500
- const bundled = findBundledChromium();
501
- if (bundled) return bundled;
502
- if (process.platform === 'win32') {
503
- const candidates = [
504
- path.join(process.env.PROGRAMFILES || 'C:\\Program Files', 'Google', 'Chrome', 'Application', 'chrome.exe'),
505
- path.join(process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)', 'Google', 'Chrome', 'Application', 'chrome.exe'),
506
- path.join(process.env.LOCALAPPDATA || '', 'Google', 'Chrome', 'Application', 'chrome.exe'),
507
- ];
508
- for (const c of candidates) { if (c && fs.existsSync(c)) return c; }
509
- return null;
510
- }
511
- if (process.platform === 'darwin') {
512
- const mac = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
513
- if (fs.existsSync(mac)) return mac;
514
- return null;
515
- }
516
- for (const bin of ['google-chrome', 'chromium', 'chromium-browser']) {
517
- const r = spawnSync('which', [bin], { encoding: 'utf-8' });
518
- if (r.status === 0 && r.stdout.trim()) return r.stdout.trim();
519
- }
520
- return null;
521
- }
522
-
523
423
  function findPlaywriter() {
524
424
  const npmR = spawnSync('npm', ['root', '-g'], { encoding: 'utf-8', shell: true });
525
425
  if (npmR.status === 0 && npmR.stdout.trim()) {
@@ -716,6 +616,124 @@ function scrubBrowserRunnerText(s) {
716
616
  return t;
717
617
  }
718
618
 
619
+ function findInstalledChromiumBinary() {
620
+ try {
621
+ if (process.env.PLAYWRITER_BROWSER_PATH && fs.existsSync(process.env.PLAYWRITER_BROWSER_PATH)) {
622
+ return process.env.PLAYWRITER_BROWSER_PATH;
623
+ }
624
+ const roots = [];
625
+ if (process.platform === 'win32') {
626
+ const lad = process.env.LOCALAPPDATA;
627
+ if (lad) roots.push(path.join(lad, 'ms-playwright'));
628
+ } else {
629
+ const home = process.env.HOME || '';
630
+ if (home) {
631
+ roots.push(path.join(home, '.cache', 'ms-playwright'));
632
+ roots.push(path.join(home, 'Library', 'Caches', 'ms-playwright'));
633
+ }
634
+ }
635
+ const exeName = process.platform === 'win32' ? 'chrome.exe' : (process.platform === 'darwin' ? 'Chromium.app/Contents/MacOS/Chromium' : 'chrome');
636
+ const subdirs = process.platform === 'win32'
637
+ ? ['chrome-win64', 'chrome-win']
638
+ : process.platform === 'darwin' ? ['chrome-mac'] : ['chrome-linux'];
639
+ const found = [];
640
+ for (const root of roots) {
641
+ if (!fs.existsSync(root)) continue;
642
+ for (const name of fs.readdirSync(root)) {
643
+ if (!/^chromium-\d+$/.test(name)) continue;
644
+ for (const sub of subdirs) {
645
+ const candidate = path.join(root, name, sub, exeName);
646
+ if (fs.existsSync(candidate)) {
647
+ const ver = parseInt(name.split('-')[1], 10) || 0;
648
+ found.push({ ver, candidate });
649
+ }
650
+ }
651
+ }
652
+ }
653
+ if (found.length === 0) return null;
654
+ found.sort((a, b) => b.ver - a.ver);
655
+ return found[0].candidate;
656
+ } catch (_) {
657
+ return null;
658
+ }
659
+ }
660
+
661
+ function startManagedBrowser(pw, profileDir) {
662
+ const args = [...pw.baseArgs, 'browser', 'start', '--user-data-dir', profileDir, '--headless'];
663
+ const env = { ...process.env };
664
+ if (!env.PLAYWRITER_BROWSER_PATH) {
665
+ const browserBin = findInstalledChromiumBinary();
666
+ if (browserBin) {
667
+ env.PLAYWRITER_BROWSER_PATH = browserBin;
668
+ logEvent('plugkit', 'browser.binary-resolved', { path: browserBin });
669
+ } else {
670
+ logEvent('plugkit', 'browser.binary-missing', {});
671
+ }
672
+ }
673
+ const child = spawn(pw.cmd, args, {
674
+ detached: true,
675
+ stdio: 'ignore',
676
+ shell: pw.shell,
677
+ windowsHide: true,
678
+ env,
679
+ ...(process.platform === 'win32' ? { creationFlags: 0x08000000 | 0x00000008 } : {}),
680
+ });
681
+ const pid = child.pid;
682
+ child.unref();
683
+ return pid;
684
+ }
685
+
686
+ function isColdRunProfile(profileDir) {
687
+ try {
688
+ if (!fs.existsSync(profileDir)) return true;
689
+ const entries = fs.readdirSync(profileDir);
690
+ if (entries.length === 0) return true;
691
+ if (!entries.some(n => n === 'Default' || n === 'Local State')) return true;
692
+ return false;
693
+ } catch (_) {
694
+ return true;
695
+ }
696
+ }
697
+
698
+ function waitForExtensionReady(pw, profileDir, opts) {
699
+ const cold = (opts && typeof opts.cold === 'boolean') ? opts.cold : isColdRunProfile(profileDir);
700
+ const timeoutMs = (opts && opts.timeoutMs) || (cold ? 180000 : 30000);
701
+ const start = Date.now();
702
+ const deadline = start + timeoutMs;
703
+ const backoff = [2000, 4000, 8000];
704
+ let attempt = 0;
705
+ let lastErr = '';
706
+ let lastProgressAt = start;
707
+ while (Date.now() < deadline) {
708
+ const remaining = deadline - Date.now();
709
+ const innerTimeout = Math.max(28000, Math.min(remaining, 30000));
710
+ const r = runPlaywriter(pw, ['session', 'new'], innerTimeout);
711
+ if (r && r.status === 0) return r;
712
+ lastErr = scrubBrowserRunnerText((r && (r.stderr || r.stdout)) || '');
713
+ const now = Date.now();
714
+ if (now - lastProgressAt >= 10000) {
715
+ logEvent('plugkit', 'browser.extension-wait', {
716
+ elapsed_ms: now - start,
717
+ cold_run: cold,
718
+ profileDir,
719
+ attempt,
720
+ });
721
+ lastProgressAt = now;
722
+ }
723
+ const sleepMs = backoff[Math.min(attempt, backoff.length - 1)];
724
+ attempt++;
725
+ if (Date.now() + sleepMs >= deadline) break;
726
+ sleepSync(sleepMs);
727
+ }
728
+ const flavor = cold
729
+ ? `cold-run timeout after ${Math.round((Date.now() - start) / 1000)}s waiting for managed browser extension to connect (first run downloads chromium ~150MB and installs the extension; if this persists the extension never registered with the relay server)`
730
+ : `warm-run timeout after ${Math.round((Date.now() - start) / 1000)}s waiting for managed browser extension to reconnect (profile exists but extension is not registering; relay server may be wedged)`;
731
+ const err = new Error(`managed browser session start failed: ${flavor}${lastErr ? ` :: ${lastErr}` : ''}`);
732
+ err._lastErr = lastErr;
733
+ err._coldRun = cold;
734
+ throw err;
735
+ }
736
+
719
737
  function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
720
738
  migrateLegacyBrowserState(cwd);
721
739
  const portsFile = browserPortsFile(cwd);
@@ -723,20 +741,18 @@ function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
723
741
  const ports = readJsonFile(portsFile, {});
724
742
  const sessions = readJsonFile(sessionsFile, {});
725
743
  const existing = ports[claudeSessionId];
726
- if (existing && existing.port) {
744
+ if (existing && existing.pid) {
727
745
  const wantProfile = path.join(cwd, '.gm', 'browser-profile');
728
- const pidOk = existing.pid && isProcessAliveSync(existing.pid);
746
+ const pidOk = isProcessAliveSync(existing.pid);
729
747
  const profileOk = !existing.profileDir || existing.profileDir === wantProfile || existing.profileDir.startsWith(path.join(cwd, '.gm', 'browser-profile'));
730
- const portOk = isPortAliveSync(existing.port);
731
- if (pidOk && profileOk && portOk) {
748
+ if (pidOk && profileOk) {
732
749
  const pwIds = sessions[claudeSessionId] || [];
733
750
  if (pwIds.length > 0) return pwIds[0];
734
751
  } else {
735
- const reason = !pidOk ? 'pid-dead' : !profileOk ? 'profile-drift' : 'port-dead';
752
+ const reason = !pidOk ? 'pid-dead' : 'profile-drift';
736
753
  logEvent('hook', 'deviation.browser-profile-collision', {
737
754
  sid: claudeSessionId,
738
755
  stale_pid: existing.pid || null,
739
- stale_port: existing.port || null,
740
756
  stale_profile: existing.profileDir || null,
741
757
  want_profile: wantProfile,
742
758
  reason,
@@ -748,44 +764,12 @@ function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
748
764
  }
749
765
  }
750
766
  cleanDeadProfileFragments(cwd);
751
- let chrome = findBundledChromium();
752
- if (!chrome) {
753
- const ensured = ensureBundledChromium(pw);
754
- if (ensured.exe) chrome = ensured.exe;
755
- }
756
- if (!chrome) chrome = findChrome();
757
- if (!chrome) throw new Error('No chromium binary available. Run: bun x playwriter@latest install chromium');
767
+ const probedProfile = path.join(cwd, '.gm', 'browser-profile');
768
+ const coldRun = isColdRunProfile(probedProfile);
758
769
  const profileDir = acquireProfileDir(cwd);
759
- const port = findFreePortSync();
760
- const chromeArgs = [
761
- `--remote-debugging-port=${port}`,
762
- `--user-data-dir=${profileDir}`,
763
- '--no-first-run',
764
- '--no-default-browser-check',
765
- '--disable-features=Translate',
766
- ];
767
- // Chrome itself is GUI but its remote-debugging port creates conhost
768
- // when launched from a console-attached parent. CREATE_NO_WINDOW
769
- // (0x08000000) | DETACHED_PROCESS (0x00000008) prevents the helper
770
- // processes (sandbox, GPU, network service) from allocating consoles.
771
- // Inherited by the entire chrome subprocess tree on Windows.
772
- const child = spawn(chrome, chromeArgs, {
773
- detached: true,
774
- stdio: 'ignore',
775
- windowsHide: true,
776
- ...(process.platform === 'win32' ? { creationFlags: 0x08000000 | 0x00000008 } : {}),
777
- });
778
- const chromePid = child.pid;
779
- child.unref();
780
- const deadline = Date.now() + 10000;
781
- let alive = false;
782
- while (Date.now() < deadline) {
783
- if (isPortAliveSync(port)) { alive = true; break; }
784
- sleepSync(300);
785
- }
786
- if (!alive) throw new Error(`Chrome failed to open debug port ${port}`);
787
- const newR = runPlaywriter(pw, ['session', 'new', '--direct', `localhost:${port}`], 30000);
788
- if (newR.status !== 0) throw new Error(`managed browser session start failed: ${scrubBrowserRunnerText(newR.stderr || newR.stdout || 'unknown')}`);
770
+ logEvent('plugkit', 'browser.start', { profileDir, cold_run: coldRun });
771
+ const browserPid = startManagedBrowser(pw, profileDir);
772
+ const newR = waitForExtensionReady(pw, profileDir, { cold: coldRun });
789
773
  const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
790
774
  const out = stripAnsi(newR.stdout || '').trim();
791
775
  let pwSessionId = null;
@@ -799,7 +783,7 @@ function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
799
783
  try { const j = JSON.parse(out); pwSessionId = j.id || j.session_id || j.session; } catch (_) {}
800
784
  }
801
785
  if (!pwSessionId) throw new Error(`could not parse managed browser session id from: ${scrubBrowserRunnerText(out)}`);
802
- ports[claudeSessionId] = { port, profileDir, pid: chromePid };
786
+ ports[claudeSessionId] = { profileDir, pid: browserPid };
803
787
  sessions[claudeSessionId] = [pwSessionId];
804
788
  writeJsonFile(portsFile, ports);
805
789
  writeJsonFile(sessionsFile, sessions);
@@ -1413,12 +1397,7 @@ function makeHostFunctions(instanceRef) {
1413
1397
  if (!pw) return writeWasmJson(instanceRef.value, { ok: false, error: 'managed browser session runner not available' });
1414
1398
  if (body.startsWith('session ')) {
1415
1399
  const parts = body.slice(8).trim().split(/\s+/);
1416
- const ports = readJsonFile(browserPortsFile(cwd), {});
1417
- const existing = ports[sessionId];
1418
- const directArgs = (existing && existing.port && isPortAliveSync(existing.port))
1419
- ? [`--direct=localhost:${existing.port}`]
1420
- : [];
1421
- const r = runPlaywriter(pw, ['session', ...parts, ...directArgs], 30000);
1400
+ const r = runPlaywriter(pw, ['session', ...parts], 30000);
1422
1401
  return writeWasmJson(instanceRef.value, {
1423
1402
  ok: r.status === 0,
1424
1403
  stdout: scrubBrowserRunnerText(r.stdout || ''),
@@ -1427,12 +1406,7 @@ function makeHostFunctions(instanceRef) {
1427
1406
  });
1428
1407
  }
1429
1408
  const pwSessionId = getOrCreateBrowserSession(cwd, sessionId, pw);
1430
- const portsAfter = readJsonFile(browserPortsFile(cwd), {});
1431
- const livePort = portsAfter[sessionId] && portsAfter[sessionId].port;
1432
- const directArgs = (livePort && isPortAliveSync(livePort))
1433
- ? [`--direct=localhost:${livePort}`]
1434
- : [];
1435
- const r = runPlaywriter(pw, ['-s', pwSessionId, '--timeout', '14000', ...directArgs, '-e', body], 60000);
1409
+ const r = runPlaywriter(pw, ['-s', pwSessionId, '--timeout', '14000', '-e', body], 60000);
1436
1410
  return writeWasmJson(instanceRef.value, {
1437
1411
  ok: r.status === 0,
1438
1412
  stdout: scrubBrowserRunnerText(r.stdout || ''),
@@ -1801,9 +1775,9 @@ async function runSpoolWatcher(instance, spoolDir) {
1801
1775
  const ports = readJsonFile(browserPortsFile(process.cwd()), {});
1802
1776
  let browserAlive = false;
1803
1777
  for (const entry of Object.values(ports)) {
1804
- if (entry && Number.isFinite(entry.port) && isPortAliveSync(entry.port)) { browserAlive = true; break; }
1778
+ if (entry && Number.isFinite(entry.pid) && isProcessAliveSync(entry.pid)) { browserAlive = true; break; }
1805
1779
  }
1806
- if (browserAlive) { markActivity('browser-port-alive'); return; }
1780
+ if (browserAlive) { markActivity('browser-pid-alive'); return; }
1807
1781
  } catch (_) {}
1808
1782
  try {
1809
1783
  let anyRunning = false;
package/gm.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm",
3
- "version": "2.0.1269",
3
+ "version": "2.0.1271",
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.1269",
3
+ "version": "2.0.1271",
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.1269"
42
+ "gm-plugkit": "^2.0.1271"
43
43
  },
44
44
  "engines": {
45
45
  "node": ">=16.0.0"