gm-skill 2.0.1517 → 2.0.1519
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 +4 -0
- package/bin/plugkit-supervisor.js +22 -13
- package/gm-plugkit/bootstrap.js +16 -1
- package/gm-plugkit/child-script-alias.test.js +25 -0
- package/gm-plugkit/package.json +1 -1
- package/gm-plugkit/plugkit-wasm-wrapper.js +56 -8
- package/gm-plugkit/supervisor.js +22 -11
- package/gm.json +1 -1
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -32,6 +32,10 @@ The plugkit stack runs as a wasm cdylib loaded by `plugkit-wasm-wrapper.js` unde
|
|
|
32
32
|
|
|
33
33
|
**`plugkit-wasm-wrapper.js` is ESM; never use inline `require()` for a node builtin — import it at module scope.** The wrapper runs under both node and bun, and the supervisor's `resolveRuntime()` prefers bun. Under bun's ESM, `require` is not a global, so an inline `const x = require('crypto'|'net'|'http'|'https'|'child_process')` throws `require is not defined` — and because those calls sit in `catch(_){}` blocks, the failure is silent: it broke `_ownWrapperSha12` (status.wrapper_sha stayed null, leaving the supervisor wrapper-sha-drift recycle inert), `_wrapperShaAtBoot` and its self-drift-restart, the synthetic-session cwd-hash, and the file-index sha — all only under the bun watcher, which is why it hid for so long (node-run watchers have `require` via CJS interop). Every node builtin is imported once at the top (`import crypto from 'crypto'`, etc.); inline `require` of a builtin is forbidden. Full incident in rs-learn (`recall: wrapper require not defined under bun`).
|
|
34
34
|
|
|
35
|
+
**Every single-instance / lock guard is atomic, never check-then-act.** A guard that does `existsSync` -> read -> decide -> `writeFileSync` is TOCTOU: under a concurrent burst (the bootstrap spawns several supervisors in the same millisecond per skill-load) every caller passes the check before any writes, so all proceed and the duplicate it was meant to prevent happens anyway. The supervisor single-instance guard, the `.watcher.lock`, and any future pid/lock file all acquire via an atomic primitive — `fs.openSync(path, 'wx')` (O_EXCL exclusive-create, succeeds for exactly one racer) or atomic-rename — then on `EEXIST` read the holder and refuse-if-alive / take-over-if-stale. The trap: a check-then-write guard passes sequential testing (a later boot sees the prior holder) and silently fails only under concurrency, so when a guard is in place and the duplicate STILL occurs, suspect non-atomicity before suspecting absence. Full incident (three mis-diagnoses) in rs-learn (`recall: supervisor churn TOCTOU atomic guard`).
|
|
36
|
+
|
|
37
|
+
**Count plugkit processes by executable Name, never by command-line substring.** A `Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like '*plugkit-wasm-wrapper*' }` (or `-match 'plugkit-supervisor'`) also matches the bash/powershell command running the query itself — its eval string contains those literals — so it fabricates phantom processes and inflates the count (it reported `8 watchers + 4 supervisors` when the truth was `4 watchers, 0 supervisors`). Always constrain to the real runtime: `Where-Object { ($_.Name -eq 'node.exe' -or $_.Name -eq 'bun.exe') -and $_.CommandLine -match 'plugkit-wasm-wrapper\.js' }`. The phantom count made a working atomic-guard fix look unconverged across two fires; a wrong measurement is as costly as a wrong diagnosis. Full incident in rs-learn (`recall: supervisor churn TOCTOU atomic guard`).
|
|
38
|
+
|
|
35
39
|
## Spool dispatch ABI
|
|
36
40
|
|
|
37
41
|
Agents dispatch verbs by writing to `.gm/exec-spool/in/<verb>/<N>.txt` (request body) and reading the response from `.gm/exec-spool/out/<verb>-<N>.json` (nested verbs) or `.gm/exec-spool/out/<N>.json` (root verbs). The wasm orchestrator services every possible verb; the harness never executes side effects directly.
|
|
@@ -90,22 +90,31 @@ function statusMtime() {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
function acquireSingleInstance() {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
93
|
+
// Atomic via O_EXCL ('wx'): exclusive-create fails if the file exists, so when N supervisors
|
|
94
|
+
// race to start in the same instant exactly one wins. A plain existsSync->write is TOCTOU and
|
|
95
|
+
// lets a concurrent burst all pass, which is the duplicate-supervisor churn this guards against.
|
|
96
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
97
|
+
try {
|
|
98
|
+
const fd = fs.openSync(SUPERVISOR_PID_PATH, 'wx');
|
|
99
|
+
try { fs.writeSync(fd, String(process.pid)); } finally { fs.closeSync(fd); }
|
|
100
|
+
return true;
|
|
101
|
+
} catch (e) {
|
|
102
|
+
if (e && e.code === 'EEXIST') {
|
|
103
|
+
let other = NaN;
|
|
104
|
+
try { other = parseInt(fs.readFileSync(SUPERVISOR_PID_PATH, 'utf-8').trim(), 10); } catch (_) {}
|
|
105
|
+
if (Number.isFinite(other) && other !== process.pid && pidAlive(other)) {
|
|
106
|
+
logEvent('supervisor.refused-duplicate', { existing_pid: other, severity: 'warn' });
|
|
107
|
+
process.stderr.write(`[plugkit-supervisor] another supervisor is alive (pid=${other}); exiting\n`);
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
try { fs.unlinkSync(SUPERVISOR_PID_PATH); } catch (_) {}
|
|
111
|
+
continue;
|
|
101
112
|
}
|
|
113
|
+
logEvent('supervisor.pid-write-failed', { error: e && e.message, severity: 'warn' });
|
|
114
|
+
return true;
|
|
102
115
|
}
|
|
103
|
-
fs.writeFileSync(SUPERVISOR_PID_PATH, String(process.pid));
|
|
104
|
-
return true;
|
|
105
|
-
} catch (e) {
|
|
106
|
-
logEvent('supervisor.pid-write-failed', { error: e.message, severity: 'warn' });
|
|
107
|
-
return true;
|
|
108
116
|
}
|
|
117
|
+
return true;
|
|
109
118
|
}
|
|
110
119
|
|
|
111
120
|
function releaseSingleInstance() {
|
package/gm-plugkit/bootstrap.js
CHANGED
|
@@ -665,6 +665,20 @@ function ensureWrapperFresh() {
|
|
|
665
665
|
} catch (_) { return false; }
|
|
666
666
|
}
|
|
667
667
|
|
|
668
|
+
function ensureGmPlugkitVersionFresh() {
|
|
669
|
+
try {
|
|
670
|
+
const ownPkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'));
|
|
671
|
+
if (!ownPkg || !ownPkg.version) return false;
|
|
672
|
+
const dst = path.join(gmToolsDir(), 'gm-plugkit.version');
|
|
673
|
+
let cur = null;
|
|
674
|
+
try { cur = fs.readFileSync(dst, 'utf-8').trim(); } catch (_) {}
|
|
675
|
+
if (cur === ownPkg.version) return false;
|
|
676
|
+
fs.mkdirSync(gmToolsDir(), { recursive: true });
|
|
677
|
+
fs.writeFileSync(dst, ownPkg.version);
|
|
678
|
+
return true;
|
|
679
|
+
} catch (_) { return false; }
|
|
680
|
+
}
|
|
681
|
+
|
|
668
682
|
function ensureSkillMdFresh() {
|
|
669
683
|
try {
|
|
670
684
|
const candidates = [
|
|
@@ -820,8 +834,9 @@ async function ensureReady(opts) {
|
|
|
820
834
|
if (isReady() && !versionDrift) {
|
|
821
835
|
const wasmPath = getWasmPath();
|
|
822
836
|
const wrapperUpdated = ensureWrapperFresh();
|
|
837
|
+
const versionMarkerUpdated = ensureGmPlugkitVersionFresh();
|
|
823
838
|
ensureSkillMdFresh();
|
|
824
|
-
return { ok: true, wasmPath, binaryPath: wasmPath, status: wrapperUpdated ? 'wrapper-refreshed' : 'already-ready', version: installed };
|
|
839
|
+
return { ok: true, wasmPath, binaryPath: wasmPath, status: (wrapperUpdated || versionMarkerUpdated) ? 'wrapper-refreshed' : 'already-ready', version: installed };
|
|
825
840
|
}
|
|
826
841
|
if (versionDrift) {
|
|
827
842
|
try { killRunningDaemons(`version_drift:${installed}->${targetVersion}`); } catch (_) {}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const wrapper = fs.readFileSync(path.join(__dirname, 'plugkit-wasm-wrapper.js'), 'utf-8');
|
|
6
|
+
|
|
7
|
+
const aliasInChildScript = /\bspawnSync\s*\(\s*process\.execPath\s*,\s*\[\s*['"]-e['"]\s*,\s*`[^`]*\b_(?:net|http|https|crypto|childProcess)Module\b[^`]*`/;
|
|
8
|
+
|
|
9
|
+
(function noParentAliasInChildEvalTemplates() {
|
|
10
|
+
assert.strictEqual(
|
|
11
|
+
aliasInChildScript.test(wrapper),
|
|
12
|
+
false,
|
|
13
|
+
'no spawnSync(process.execPath, ["-e", `...`]) child-script template may reference a parent-scope _*Module alias; the spawned child has no such binding (use require() inside the template). This regression broke findFreePortSync/isPort*/fetchJsonSync and surfaced only as "could not allocate free port" at browser-spawn time.'
|
|
14
|
+
);
|
|
15
|
+
})();
|
|
16
|
+
|
|
17
|
+
(function childTemplatesUseRequire() {
|
|
18
|
+
const childEvalBlocks = wrapper.match(/spawnSync\s*\(\s*process\.execPath\s*,\s*\[\s*['"]-e['"]\s*,\s*`[^`]*`/g) || [];
|
|
19
|
+
for (const block of childEvalBlocks) {
|
|
20
|
+
if (/\brequire\s*\(\s*['"](?:net|http|https)['"]\s*\)/.test(block) || !/\b(?:net|http|https)\b/.test(block)) continue;
|
|
21
|
+
assert.fail(`child -e template uses a node builtin without require(): ${block.slice(0, 80)}...`);
|
|
22
|
+
}
|
|
23
|
+
})();
|
|
24
|
+
|
|
25
|
+
console.log('child-script-alias.test.js: all assertions passed');
|
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.1519",
|
|
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": {
|
|
@@ -828,7 +828,7 @@ function resolveWindowsExeLocal(cmd) {
|
|
|
828
828
|
|
|
829
829
|
function isPortReachableSync(host, port, timeoutMs) {
|
|
830
830
|
const r = spawnSync(process.execPath, ['-e', `
|
|
831
|
-
const net =
|
|
831
|
+
const net = require('net');
|
|
832
832
|
const s = net.connect({ port: ${port}, host: ${JSON.stringify(host)} });
|
|
833
833
|
let done = false;
|
|
834
834
|
s.on('connect', () => { done = true; s.destroy(); process.exit(0); });
|
|
@@ -840,7 +840,7 @@ function isPortReachableSync(host, port, timeoutMs) {
|
|
|
840
840
|
|
|
841
841
|
function findFreePortSync() {
|
|
842
842
|
const r = spawnSync(process.execPath, ['-e', `
|
|
843
|
-
const net =
|
|
843
|
+
const net = require('net');
|
|
844
844
|
const srv = net.createServer();
|
|
845
845
|
srv.listen(0, '127.0.0.1', () => { const p = srv.address().port; srv.close(() => { process.stdout.write(String(p)); }); });
|
|
846
846
|
srv.on('error', e => { process.stderr.write(e.message); process.exit(1); });
|
|
@@ -851,7 +851,7 @@ function findFreePortSync() {
|
|
|
851
851
|
|
|
852
852
|
function isPortAliveSync(port) {
|
|
853
853
|
const r = spawnSync(process.execPath, ['-e', `
|
|
854
|
-
const net =
|
|
854
|
+
const net = require('net');
|
|
855
855
|
const s = net.connect({ port: ${port}, host: '127.0.0.1' });
|
|
856
856
|
s.on('connect', () => { s.destroy(); process.exit(0); });
|
|
857
857
|
s.on('error', () => process.exit(1));
|
|
@@ -949,7 +949,7 @@ function findInstalledChromiumBinary() {
|
|
|
949
949
|
|
|
950
950
|
function fetchJsonSync(url, timeoutMs) {
|
|
951
951
|
const r = spawnSync(process.execPath, ['-e', `
|
|
952
|
-
const http =
|
|
952
|
+
const http = require('http');
|
|
953
953
|
const req = http.get(${JSON.stringify(url)}, (res) => {
|
|
954
954
|
let buf = '';
|
|
955
955
|
res.on('data', d => buf += d);
|
|
@@ -1057,7 +1057,7 @@ function gracefulCloseBrowser(entry, reason) {
|
|
|
1057
1057
|
const info = fetchJsonSync(`http://127.0.0.1:${port}/json/version`, 600);
|
|
1058
1058
|
if (info && info.webSocketDebuggerUrl) {
|
|
1059
1059
|
spawnSync(process.execPath, ['-e', `
|
|
1060
|
-
const http =
|
|
1060
|
+
const http = require('http');
|
|
1061
1061
|
const req = http.request({host:'127.0.0.1',port:${port},path:'/json/close/browser',method:'GET',timeout:1500},
|
|
1062
1062
|
res => { res.resume(); res.on('end', () => process.exit(0)); });
|
|
1063
1063
|
req.on('error', () => process.exit(1));
|
|
@@ -2369,8 +2369,33 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
2369
2369
|
}
|
|
2370
2370
|
const latest = JSON.parse(body).version;
|
|
2371
2371
|
const stalePath = path.join(spoolDir, '.gm-plugkit-stale.json');
|
|
2372
|
+
const respawnGuardPath = path.join(spoolDir, '.gm-plugkit-respawn-guard.json');
|
|
2372
2373
|
if (!latest || latest === own) {
|
|
2373
2374
|
if (fs.existsSync(stalePath)) { try { fs.unlinkSync(stalePath); } catch (_) {} }
|
|
2375
|
+
if (fs.existsSync(respawnGuardPath)) { try { fs.unlinkSync(respawnGuardPath); } catch (_) {} }
|
|
2376
|
+
return;
|
|
2377
|
+
}
|
|
2378
|
+
let respawnGuard = { attempts: 0, last_own: null, last_latest: null, first_ts: Date.now() };
|
|
2379
|
+
try {
|
|
2380
|
+
if (fs.existsSync(respawnGuardPath)) respawnGuard = JSON.parse(fs.readFileSync(respawnGuardPath, 'utf8'));
|
|
2381
|
+
} catch (_) {}
|
|
2382
|
+
const sameStaleAsBefore = respawnGuard.last_own === own && respawnGuard.last_latest === latest;
|
|
2383
|
+
const cameFromSelfRespawn = process.env.PLUGKIT_BOOT_REASON === 'self-respawn-from-self-stale';
|
|
2384
|
+
if (sameStaleAsBefore && respawnGuard.attempts >= 3) {
|
|
2385
|
+
try { fs.writeFileSync(stalePath, JSON.stringify({
|
|
2386
|
+
ts: new Date().toISOString(),
|
|
2387
|
+
reason: 'gm-plugkit-self-stale-respawn-exhausted',
|
|
2388
|
+
running_version: own,
|
|
2389
|
+
latest_version: latest,
|
|
2390
|
+
respawn_attempts: respawnGuard.attempts,
|
|
2391
|
+
instruction: `gm-plugkit ${own} cannot self-upgrade to ${latest}: ${respawnGuard.attempts} respawns all came up ${own} (bun/npx cache is serving the stale tarball). Respawn loop halted to keep this watcher alive and serving verbs. Fix manually: bun pm cache rm; npm cache clean --force; rm -rf ~/AppData/Local/npm-cache/_npx ~/.bun/install/cache; then bun x gm-plugkit@latest --kill-stale-watchers; bun x gm-plugkit@latest spool`,
|
|
2392
|
+
detected_by: 'watcher-periodic-probe',
|
|
2393
|
+
}, null, 2)); } catch (_) {}
|
|
2394
|
+
if (!_selfStaleLoggedOnce) {
|
|
2395
|
+
_selfStaleLoggedOnce = true;
|
|
2396
|
+
try { logEvent('plugkit', 'gm-plugkit.self-stale-respawn-exhausted', { running_version: own, latest_version: latest, attempts: respawnGuard.attempts }); } catch (_) {}
|
|
2397
|
+
console.error(`[plugkit-wasm] gm-plugkit self-stale respawn EXHAUSTED after ${respawnGuard.attempts} attempts (cache serving stale ${own} for latest ${latest}); halting respawn loop and staying alive to serve verbs`);
|
|
2398
|
+
}
|
|
2374
2399
|
return;
|
|
2375
2400
|
}
|
|
2376
2401
|
const marker = {
|
|
@@ -2389,6 +2414,25 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
2389
2414
|
try {
|
|
2390
2415
|
const cp = _childProcess;
|
|
2391
2416
|
const bunPath = process.env.GM_BUN_PATH || 'bun';
|
|
2417
|
+
const bustCache = sameStaleAsBefore || cameFromSelfRespawn;
|
|
2418
|
+
if (bustCache) {
|
|
2419
|
+
try { cp.execFileSync(bunPath, ['pm', 'cache', 'rm'], { stdio: 'ignore', timeout: 30000, windowsHide: true }); } catch (_) {}
|
|
2420
|
+
try {
|
|
2421
|
+
const home = process.env.USERPROFILE || process.env.HOME || '';
|
|
2422
|
+
for (const rel of ['AppData/Local/npm-cache/_npx', '.npm/_npx', '.bun/install/cache']) {
|
|
2423
|
+
try { fs.rmSync(path.join(home, rel), { recursive: true, force: true }); } catch (_) {}
|
|
2424
|
+
}
|
|
2425
|
+
} catch (_) {}
|
|
2426
|
+
try { logEvent('plugkit', 'gm-plugkit.self-stale-cache-busted', { running_version: own, latest_version: latest, attempt: (respawnGuard.attempts || 0) + 1 }); } catch (_) {}
|
|
2427
|
+
}
|
|
2428
|
+
try { fs.writeFileSync(respawnGuardPath, JSON.stringify({
|
|
2429
|
+
attempts: (sameStaleAsBefore ? (respawnGuard.attempts || 0) : 0) + 1,
|
|
2430
|
+
last_own: own,
|
|
2431
|
+
last_latest: latest,
|
|
2432
|
+
first_ts: respawnGuard.first_ts || Date.now(),
|
|
2433
|
+
last_ts: Date.now(),
|
|
2434
|
+
cache_busted: bustCache,
|
|
2435
|
+
}, null, 2)); } catch (_) {}
|
|
2392
2436
|
const child = cp.spawn(bunPath, ['x', `gm-plugkit@${latest}`, 'spool'], {
|
|
2393
2437
|
cwd: process.cwd(),
|
|
2394
2438
|
detached: true,
|
|
@@ -2397,7 +2441,7 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
2397
2441
|
env: { ...process.env, PLUGKIT_BOOT_REASON: 'self-respawn-from-self-stale' },
|
|
2398
2442
|
});
|
|
2399
2443
|
child.unref();
|
|
2400
|
-
try { logEvent('plugkit', 'gm-plugkit.self-stale-respawn', { running_version: own, latest_version: latest }); } catch (_) {}
|
|
2444
|
+
try { logEvent('plugkit', 'gm-plugkit.self-stale-respawn', { running_version: own, latest_version: latest, cache_busted: bustCache, attempt: (respawnGuard.attempts || 0) + 1 }); } catch (_) {}
|
|
2401
2445
|
try { fs.writeFileSync(path.join(spoolDir, '.shutdown-reason.json'), JSON.stringify({ reason: 'gm-plugkit-self-stale', ts: Date.now(), pid: process.pid, running_version: own, latest_version: latest })); } catch (_) {}
|
|
2402
2446
|
// Wait for the replacement's fresh heartbeat before exiting (mirror the
|
|
2403
2447
|
// version-drift path) instead of a blind 2s exit: the gm-plugkit download can
|
|
@@ -2406,13 +2450,17 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
2406
2450
|
const myPid = process.pid;
|
|
2407
2451
|
const respawnDeadline = Date.now() + 90000;
|
|
2408
2452
|
const exitSelfStale = () => { try { process.exit(0); } catch (_) {} };
|
|
2453
|
+
const ownVersionFile = path.join(GM_TOOLS_ROOT, 'gm-plugkit.version');
|
|
2409
2454
|
const pollSelfStaleReplacement = () => {
|
|
2410
2455
|
try {
|
|
2411
2456
|
const st = JSON.parse(fs.readFileSync(STATUS_PATH_FOR_TEARDOWN, 'utf8'));
|
|
2412
2457
|
const freshHeartbeat = st && st.ts && (Date.now() - st.ts) < 15000;
|
|
2413
2458
|
const differentProc = st && st.pid && st.pid !== myPid;
|
|
2414
|
-
|
|
2415
|
-
|
|
2459
|
+
let replacementOnLatest = false;
|
|
2460
|
+
try { replacementOnLatest = fs.readFileSync(ownVersionFile, 'utf-8').trim() === latest; } catch (_) {}
|
|
2461
|
+
if (freshHeartbeat && differentProc && replacementOnLatest) {
|
|
2462
|
+
try { fs.unlinkSync(respawnGuardPath); } catch (_) {}
|
|
2463
|
+
try { logEvent('plugkit', 'gm-plugkit.self-stale-respawn-confirmed', { old_pid: myPid, new_pid: st.pid, new_version: st.version, latest_version: latest, replacement_gm_plugkit: latest }); } catch (_) {}
|
|
2416
2464
|
return exitSelfStale();
|
|
2417
2465
|
}
|
|
2418
2466
|
} catch (_) {}
|
package/gm-plugkit/supervisor.js
CHANGED
|
@@ -72,20 +72,31 @@ function pidAlive(pid) {
|
|
|
72
72
|
// and duplicates spawn duplicate watchers that lock-fight in an endless spawn-reject churn. Write the
|
|
73
73
|
// pid file on startup and refuse to start if a live peer already holds it.
|
|
74
74
|
function acquireSingleInstance() {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
75
|
+
// Atomic via O_EXCL ('wx'): exclusive-create fails if the file exists, so when N supervisors
|
|
76
|
+
// race to start in the same instant exactly one wins. A plain existsSync->write is TOCTOU and
|
|
77
|
+
// lets a concurrent burst all pass, which is the duplicate-supervisor churn this guards against.
|
|
78
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
79
|
+
try {
|
|
80
|
+
const fd = fs.openSync(SUPERVISOR_PID_PATH, 'wx');
|
|
81
|
+
try { fs.writeSync(fd, String(process.pid)); } finally { fs.closeSync(fd); }
|
|
82
|
+
return true;
|
|
83
|
+
} catch (e) {
|
|
84
|
+
if (e && e.code === 'EEXIST') {
|
|
85
|
+
let other = NaN;
|
|
86
|
+
try { other = parseInt(fs.readFileSync(SUPERVISOR_PID_PATH, 'utf-8').trim(), 10); } catch (_) {}
|
|
87
|
+
if (Number.isFinite(other) && other !== process.pid && pidAlive(other)) {
|
|
88
|
+
logEvent('supervisor.refused-duplicate', { existing_pid: other, severity: 'warn' });
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
// Holder is dead/stale: remove and retry the exclusive create once.
|
|
92
|
+
try { fs.unlinkSync(SUPERVISOR_PID_PATH); } catch (_) {}
|
|
93
|
+
continue;
|
|
81
94
|
}
|
|
95
|
+
logEvent('supervisor.pid-write-failed', { error: e && e.message, severity: 'warn' });
|
|
96
|
+
return true;
|
|
82
97
|
}
|
|
83
|
-
fs.writeFileSync(SUPERVISOR_PID_PATH, String(process.pid));
|
|
84
|
-
return true;
|
|
85
|
-
} catch (e) {
|
|
86
|
-
logEvent('supervisor.pid-write-failed', { error: e.message, severity: 'warn' });
|
|
87
|
-
return true;
|
|
88
98
|
}
|
|
99
|
+
return true;
|
|
89
100
|
}
|
|
90
101
|
|
|
91
102
|
function releaseSingleInstance() {
|
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.1519",
|
|
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",
|