gm-skill 2.0.1630 → 2.0.1631
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 +2 -0
- package/gm-plugkit/package.json +1 -1
- package/gm-plugkit/plugkit-wasm-wrapper.js +61 -6
- package/gm.json +1 -1
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -174,6 +174,8 @@ Orchestration state is tracked via `.gm/` marker files, not hook events; the CLI
|
|
|
174
174
|
|
|
175
175
|
**Session lifecycle**: background tasks + browser sessions persist across turn-stops; cleanup fires only on real-exit reasons; residual-scan fires when PRD empty AND no open browser sessions AND no running tasks. Detail in rs-learn (`recall: session lifecycle killSessionTasks residual-scan`).
|
|
176
176
|
|
|
177
|
+
**Browser session state is rooted at the git common dir, never `process.cwd()`**: a workflow worktree fan-out runs each parallel agent in its own worktree (distinct cwd); keying the browser ports-registry + profile dir on cwd opens one chromium per worktree (the "meant one, got N browsers" defect). `browserRootDir(cwd)` resolves the worktree to its main repo via `git rev-parse --git-common-dir`, and `browserStateDir`/`sessionProfileDir`/`acquireProfileDir` route through it, so all worktrees of one workflow share ONE browser while separate repos stay isolated. The cross-agent spawn is guarded by an atomic O_EXCL single-flight lock (loser attaches to the winner's chromium). Detail in rs-learn (`recall: browser session state worktree common-dir rooting`).
|
|
178
|
+
|
|
177
179
|
## Spool observability surface
|
|
178
180
|
|
|
179
181
|
One-shot system-state probe: dispatch `plugkit health` via the file-spool before assuming any component is broken; the runtime diagnostic files at `.gm/exec-spool/` root are readable directly via Read (runtime-data exception). File list + health fields in rs-learn (`recall: spool runtime diagnostic files`, `recall: plugkit health verb fields`).
|
package/gm-plugkit/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gm-plugkit",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1631",
|
|
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": {
|
|
@@ -549,8 +549,26 @@ const TMP_DIR = os.tmpdir();
|
|
|
549
549
|
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
|
+
const __browserRootCache = new Map();
|
|
553
|
+
function browserRootDir(cwd) {
|
|
554
|
+
const start = path.resolve(cwd || process.cwd());
|
|
555
|
+
if (__browserRootCache.has(start)) return __browserRootCache.get(start);
|
|
556
|
+
let root = start;
|
|
557
|
+
try {
|
|
558
|
+
const r = spawnSync('git', ['rev-parse', '--git-common-dir'], { cwd: start, encoding: 'utf-8', windowsHide: true, timeout: 1500 });
|
|
559
|
+
if (r.status === 0 && r.stdout && r.stdout.trim()) {
|
|
560
|
+
let commonDir = r.stdout.trim();
|
|
561
|
+
if (!path.isAbsolute(commonDir)) commonDir = path.resolve(start, commonDir);
|
|
562
|
+
if (/(^|[\\/])\.git$/.test(commonDir)) root = path.dirname(commonDir);
|
|
563
|
+
}
|
|
564
|
+
} catch (_) {}
|
|
565
|
+
root = path.resolve(root);
|
|
566
|
+
__browserRootCache.set(start, root);
|
|
567
|
+
return root;
|
|
568
|
+
}
|
|
569
|
+
|
|
552
570
|
function browserStateDir(cwd) {
|
|
553
|
-
const dir = path.join(cwd
|
|
571
|
+
const dir = path.join(browserRootDir(cwd), '.gm', 'exec-spool');
|
|
554
572
|
try { fs.mkdirSync(dir, { recursive: true }); } catch (_) {}
|
|
555
573
|
return dir;
|
|
556
574
|
}
|
|
@@ -752,14 +770,15 @@ function sessionProfileSlug(claudeSessionId) {
|
|
|
752
770
|
}
|
|
753
771
|
|
|
754
772
|
function sessionProfileDir(cwd, claudeSessionId) {
|
|
755
|
-
return path.join(cwd, '.gm', `browser-profile-${sessionProfileSlug(claudeSessionId)}`);
|
|
773
|
+
return path.join(browserRootDir(cwd), '.gm', `browser-profile-${sessionProfileSlug(claudeSessionId)}`);
|
|
756
774
|
}
|
|
757
775
|
|
|
758
776
|
function acquireProfileDir(cwd, claudeSessionId) {
|
|
759
|
-
const
|
|
777
|
+
const root = browserRootDir(cwd);
|
|
778
|
+
const gmDir = path.join(root, '.gm');
|
|
760
779
|
try { fs.mkdirSync(gmDir, { recursive: true }); } catch (_) {}
|
|
761
|
-
ensureGitignored(
|
|
762
|
-
ensureGitignored(
|
|
780
|
+
ensureGitignored(root, '.gm/browser-profile/');
|
|
781
|
+
ensureGitignored(root, '.gm/browser-profile-*/');
|
|
763
782
|
const primary = sessionProfileDir(cwd, claudeSessionId);
|
|
764
783
|
try { fs.mkdirSync(primary, { recursive: true }); } catch (_) {}
|
|
765
784
|
if (!isProfileLocked(primary)) return primary;
|
|
@@ -770,7 +789,7 @@ function acquireProfileDir(cwd, claudeSessionId) {
|
|
|
770
789
|
|
|
771
790
|
function cleanDeadProfileFragments(cwd) {
|
|
772
791
|
try {
|
|
773
|
-
const gmDir = path.join(cwd, '.gm');
|
|
792
|
+
const gmDir = path.join(browserRootDir(cwd), '.gm');
|
|
774
793
|
if (!fs.existsSync(gmDir)) return { cleaned: 0 };
|
|
775
794
|
let cleaned = 0;
|
|
776
795
|
for (const name of fs.readdirSync(gmDir)) {
|
|
@@ -1202,6 +1221,41 @@ function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
|
|
|
1202
1221
|
try { writeJsonFile(sessionsFile, sessions); } catch (_) {}
|
|
1203
1222
|
}
|
|
1204
1223
|
}
|
|
1224
|
+
const spawnLock = path.join(browserStateDir(cwd), `.browser-spawn-${sessionProfileSlug(claudeSessionId)}.lock`);
|
|
1225
|
+
let lockFd = null;
|
|
1226
|
+
const spawnDeadline = Date.now() + 35000;
|
|
1227
|
+
for (;;) {
|
|
1228
|
+
try { lockFd = fs.openSync(spawnLock, 'wx'); break; }
|
|
1229
|
+
catch (e) {
|
|
1230
|
+
if (e.code !== 'EEXIST') break;
|
|
1231
|
+
let stale = false;
|
|
1232
|
+
try {
|
|
1233
|
+
const owner = parseInt(String(fs.readFileSync(spawnLock, 'utf-8')).split('|')[0], 10);
|
|
1234
|
+
const ageOk = (Date.now() - fs.statSync(spawnLock).mtimeMs) < 40000;
|
|
1235
|
+
if (!ageOk || !(Number.isFinite(owner) && isProcessAliveSync(owner))) stale = true;
|
|
1236
|
+
} catch (_) { stale = true; }
|
|
1237
|
+
if (stale) { try { fs.unlinkSync(spawnLock); } catch (_) {} continue; }
|
|
1238
|
+
const winner = readJsonFile(portsFile, {})[claudeSessionId];
|
|
1239
|
+
if (winner && winner.pid && winner.wsEndpoint && isProcessAliveSync(winner.pid)
|
|
1240
|
+
&& fetchJsonSync(`http://127.0.0.1:${winner.port}/json/version`, 1000)) {
|
|
1241
|
+
const a = runBrowserRunner(pw, ['session', 'new', '--direct', winner.wsEndpoint], 30000, cwd, claudeSessionId);
|
|
1242
|
+
const sid = a && a.status === 0 ? parseSessionId(a.stdout || '') : null;
|
|
1243
|
+
if (sid) { logEvent('plugkit', 'browser.attached', { pwSessionId: sid, reused: true, via: 'spawn-lock-wait' }); return sid; }
|
|
1244
|
+
}
|
|
1245
|
+
if (Date.now() > spawnDeadline) break;
|
|
1246
|
+
sleepSyncMs(300);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
try { if (lockFd !== null) { fs.writeSync(lockFd, `${process.pid}|${Date.now()}`); fs.closeSync(lockFd); } } catch (_) {}
|
|
1250
|
+
const releaseSpawnLock = () => { try { const o = parseInt(String(fs.readFileSync(spawnLock, 'utf-8')).split('|')[0], 10); if (o === process.pid) fs.unlinkSync(spawnLock); } catch (_) {} };
|
|
1251
|
+
try {
|
|
1252
|
+
const winner2 = readJsonFile(portsFile, {})[claudeSessionId];
|
|
1253
|
+
if (winner2 && winner2.pid && winner2.wsEndpoint && isProcessAliveSync(winner2.pid)
|
|
1254
|
+
&& fetchJsonSync(`http://127.0.0.1:${winner2.port}/json/version`, 1000)) {
|
|
1255
|
+
const a = runBrowserRunner(pw, ['session', 'new', '--direct', winner2.wsEndpoint], 30000, cwd, claudeSessionId);
|
|
1256
|
+
const sid = a && a.status === 0 ? parseSessionId(a.stdout || '') : null;
|
|
1257
|
+
if (sid) { logEvent('plugkit', 'browser.attached', { pwSessionId: sid, reused: true, via: 'spawn-lock-recheck' }); return sid; }
|
|
1258
|
+
}
|
|
1205
1259
|
cleanDeadProfileFragments(cwd);
|
|
1206
1260
|
reapOrphanBrowserSessions(pw, cwd, claudeSessionId, 'pre-spawn');
|
|
1207
1261
|
const profileDir = acquireProfileDir(cwd, claudeSessionId);
|
|
@@ -1243,6 +1297,7 @@ function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
|
|
|
1243
1297
|
writeJsonFile(sessionsFile, sessions);
|
|
1244
1298
|
logEvent('plugkit', 'browser.attached', { pwSessionId, pid: browserPid, port });
|
|
1245
1299
|
return pwSessionId;
|
|
1300
|
+
} finally { releaseSpawnLock(); }
|
|
1246
1301
|
}
|
|
1247
1302
|
|
|
1248
1303
|
function parseSessionId(rawOut) {
|
package/gm.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gm-skill",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1631",
|
|
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",
|