gm-skill 2.0.1159 → 2.0.1160
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 +1 -1
- package/gm-plugkit/bootstrap.js +58 -17
- package/gm-plugkit/plugkit-wasm-wrapper.js +50 -2
- package/gm-plugkit/supervisor.js +13 -3
- package/gm.json +1 -1
- package/package.json +2 -2
- package/skills/gm-skill/SKILL.md +3 -1
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.
|
|
38
|
+
`2.0.1160` — 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
|
|
package/gm-plugkit/bootstrap.js
CHANGED
|
@@ -153,6 +153,23 @@ function sha256OfFile(filePath) {
|
|
|
153
153
|
});
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
function resolveNpxJsCli() {
|
|
157
|
+
if (process.platform !== 'win32') return null;
|
|
158
|
+
const candidates = [];
|
|
159
|
+
if (process.env.npm_config_prefix) {
|
|
160
|
+
candidates.push(path.join(process.env.npm_config_prefix, 'node_modules', 'npm', 'bin', 'npx-cli.js'));
|
|
161
|
+
}
|
|
162
|
+
const programFiles = process.env.ProgramFiles || 'C:\\Program Files';
|
|
163
|
+
candidates.push(path.join(programFiles, 'nodejs', 'node_modules', 'npm', 'bin', 'npx-cli.js'));
|
|
164
|
+
candidates.push(path.join(path.dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npx-cli.js'));
|
|
165
|
+
const appdata = process.env.APPDATA;
|
|
166
|
+
if (appdata) candidates.push(path.join(appdata, 'npm', 'node_modules', 'npm', 'bin', 'npx-cli.js'));
|
|
167
|
+
for (const c of candidates) {
|
|
168
|
+
try { if (fs.existsSync(c)) return c; } catch (_) {}
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
156
173
|
async function extractNpmPackageWasm(destPath, version) {
|
|
157
174
|
const tempDir = path.join(path.dirname(destPath), '.npm-extract-' + Date.now());
|
|
158
175
|
try {
|
|
@@ -161,16 +178,28 @@ async function extractNpmPackageWasm(destPath, version) {
|
|
|
161
178
|
log(`extracting npm package ${NPM_PACKAGE}@${version} to ${tempDir}`);
|
|
162
179
|
obsEvent('bootstrap', 'npm.extract.start', { package: NPM_PACKAGE, version });
|
|
163
180
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
{
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
181
|
+
let cmd, args;
|
|
182
|
+
if (process.platform === 'win32') {
|
|
183
|
+
const npxCli = resolveNpxJsCli();
|
|
184
|
+
if (npxCli) {
|
|
185
|
+
cmd = process.execPath;
|
|
186
|
+
args = [npxCli, NPM_PACKAGE + '@' + version, '--prefix', tempDir];
|
|
187
|
+
} else {
|
|
188
|
+
cmd = 'npx.cmd';
|
|
189
|
+
args = [NPM_PACKAGE + '@' + version, '--prefix', tempDir];
|
|
172
190
|
}
|
|
173
|
-
|
|
191
|
+
} else {
|
|
192
|
+
cmd = 'npx';
|
|
193
|
+
args = [NPM_PACKAGE + '@' + version, '--prefix', tempDir];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const result = spawnSync(cmd, args, {
|
|
197
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
198
|
+
timeout: ATTEMPT_TIMEOUT_MS,
|
|
199
|
+
encoding: 'utf8',
|
|
200
|
+
windowsHide: true,
|
|
201
|
+
shell: process.platform === 'win32' && cmd === 'npx.cmd',
|
|
202
|
+
});
|
|
174
203
|
|
|
175
204
|
if (result.error) throw result.error;
|
|
176
205
|
if (result.status !== 0) {
|
|
@@ -192,25 +221,37 @@ async function extractNpmPackageWasm(destPath, version) {
|
|
|
192
221
|
|
|
193
222
|
function httpGetBuffer(url, timeoutMs) {
|
|
194
223
|
const https = require('https');
|
|
224
|
+
const idleTimeoutMs = timeoutMs || 30000;
|
|
225
|
+
const totalDeadlineMs = (timeoutMs || 30000) * 2;
|
|
195
226
|
return new Promise((resolve, reject) => {
|
|
196
|
-
|
|
227
|
+
let bytesReceived = 0;
|
|
228
|
+
let settled = false;
|
|
229
|
+
const settleReject = (err) => { if (!settled) { settled = true; reject(err); } };
|
|
230
|
+
const settleResolve = (v) => { if (!settled) { settled = true; resolve(v); } };
|
|
231
|
+
const absTimer = setTimeout(() => {
|
|
232
|
+
try { req.destroy(new Error(`abs-deadline ${totalDeadlineMs}ms ${url} after ${bytesReceived} bytes`)); } catch (_) {}
|
|
233
|
+
settleReject(new Error(`abs-deadline ${totalDeadlineMs}ms ${url} after ${bytesReceived} bytes`));
|
|
234
|
+
}, totalDeadlineMs);
|
|
235
|
+
const req = https.get(url, { timeout: idleTimeoutMs, headers: { 'user-agent': 'gm-plugkit-bootstrap' } }, (res) => {
|
|
197
236
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
198
237
|
res.resume();
|
|
199
|
-
|
|
238
|
+
clearTimeout(absTimer);
|
|
239
|
+
httpGetBuffer(res.headers.location, timeoutMs).then(settleResolve, settleReject);
|
|
200
240
|
return;
|
|
201
241
|
}
|
|
202
242
|
if (res.statusCode !== 200) {
|
|
203
243
|
res.resume();
|
|
204
|
-
|
|
244
|
+
clearTimeout(absTimer);
|
|
245
|
+
settleReject(new Error(`HTTP ${res.statusCode} ${url}`));
|
|
205
246
|
return;
|
|
206
247
|
}
|
|
207
248
|
const chunks = [];
|
|
208
|
-
res.on('data', c => chunks.push(c));
|
|
209
|
-
res.on('end', () =>
|
|
210
|
-
res.on('error',
|
|
249
|
+
res.on('data', c => { chunks.push(c); bytesReceived += c.length; });
|
|
250
|
+
res.on('end', () => { clearTimeout(absTimer); settleResolve(Buffer.concat(chunks)); });
|
|
251
|
+
res.on('error', (e) => { clearTimeout(absTimer); settleReject(e); });
|
|
211
252
|
});
|
|
212
|
-
req.on('timeout', () => req.destroy(new Error(`timeout ${url}`)));
|
|
213
|
-
req.on('error',
|
|
253
|
+
req.on('timeout', () => { try { req.destroy(new Error(`idle-timeout ${idleTimeoutMs}ms ${url}`)); } catch (_) {} settleReject(new Error(`idle-timeout ${idleTimeoutMs}ms ${url}`)); });
|
|
254
|
+
req.on('error', (e) => { clearTimeout(absTimer); settleReject(e); });
|
|
214
255
|
});
|
|
215
256
|
}
|
|
216
257
|
|
|
@@ -932,6 +932,22 @@ function resolveVersion(instance) {
|
|
|
932
932
|
return 'unknown';
|
|
933
933
|
}
|
|
934
934
|
|
|
935
|
+
function readFileVersionOnly() {
|
|
936
|
+
try { return fs.readFileSync(path.join(os.homedir(), '.claude', 'gm-tools', 'plugkit.version'), 'utf8').trim(); } catch (_) { return null; }
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function readInstanceVersion(instance) {
|
|
940
|
+
try {
|
|
941
|
+
const fn = instance && instance.exports && instance.exports.plugkit_version;
|
|
942
|
+
if (typeof fn !== 'function') return null;
|
|
943
|
+
const result = fn();
|
|
944
|
+
const ptr = Number(result & 0xffffffffn);
|
|
945
|
+
const len = Number(result >> 32n);
|
|
946
|
+
const bytes = new Uint8Array(instance.exports.memory.buffer, ptr, len);
|
|
947
|
+
return new TextDecoder().decode(bytes).trim();
|
|
948
|
+
} catch (_) { return null; }
|
|
949
|
+
}
|
|
950
|
+
|
|
935
951
|
async function runSpoolWatcher(instance, spoolDir) {
|
|
936
952
|
const inDir = path.join(spoolDir, 'in');
|
|
937
953
|
const outDir = path.join(spoolDir, 'out');
|
|
@@ -947,8 +963,11 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
947
963
|
const lockTs = parseInt(tsStr, 10);
|
|
948
964
|
const age = Date.now() - lockTs;
|
|
949
965
|
if (age < 15000) {
|
|
950
|
-
|
|
951
|
-
|
|
966
|
+
const msg = JSON.stringify({ ok: false, reason: 'another-watcher-active', pid: pidStr, age_ms: age });
|
|
967
|
+
console.error(`[plugkit-wasm] ${msg}; refusing to start`);
|
|
968
|
+
try { fs.writeFileSync(path.join(spoolDir, '.lock-rejection.json'), msg); } catch (_) {}
|
|
969
|
+
try { logEvent('plugkit', 'watcher.lock-rejected', { holder_pid: pidStr, lock_age_ms: age }); } catch (_) {}
|
|
970
|
+
process.exit(75);
|
|
952
971
|
}
|
|
953
972
|
console.error(`[plugkit-wasm] stale lock (age=${age}ms); taking over`);
|
|
954
973
|
}
|
|
@@ -1035,6 +1054,35 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
1035
1054
|
try { reapTimedOutTasks(); } catch (_) {}
|
|
1036
1055
|
}, 5000);
|
|
1037
1056
|
|
|
1057
|
+
const _instanceVersionAtBoot = readInstanceVersion(instance);
|
|
1058
|
+
setInterval(() => {
|
|
1059
|
+
try {
|
|
1060
|
+
const fileV = readFileVersionOnly();
|
|
1061
|
+
const instV = _instanceVersionAtBoot;
|
|
1062
|
+
if (!fileV || !instV || fileV === instV) return;
|
|
1063
|
+
logEvent('plugkit', 'version.drift', {
|
|
1064
|
+
instance_version: instV,
|
|
1065
|
+
file_version: fileV,
|
|
1066
|
+
action: 'exit-for-respawn',
|
|
1067
|
+
});
|
|
1068
|
+
console.error(`[plugkit-wasm] version drift detected: instance=${instV} file=${fileV} → exiting so supervisor reloads fresh wasm`);
|
|
1069
|
+
try {
|
|
1070
|
+
fs.writeFileSync(path.join(spoolDir, '.shutdown-reason.json'), JSON.stringify({
|
|
1071
|
+
reason: 'version-change',
|
|
1072
|
+
ts: Date.now(),
|
|
1073
|
+
pid: process.pid,
|
|
1074
|
+
instance_version: instV,
|
|
1075
|
+
file_version: fileV,
|
|
1076
|
+
}));
|
|
1077
|
+
} catch (_) {}
|
|
1078
|
+
try { releaseLock(); } catch (_) {}
|
|
1079
|
+
try { fs.unlinkSync(STATUS_PATH_FOR_TEARDOWN); } catch (_) {}
|
|
1080
|
+
process.exit(0);
|
|
1081
|
+
} catch (e) {
|
|
1082
|
+
console.error(`[version-drift-check] error: ${e.message}`);
|
|
1083
|
+
}
|
|
1084
|
+
}, 60_000);
|
|
1085
|
+
|
|
1038
1086
|
setInterval(() => {
|
|
1039
1087
|
try {
|
|
1040
1088
|
const idleMs = Date.now() - lastActivityMs;
|
package/gm-plugkit/supervisor.js
CHANGED
|
@@ -130,27 +130,37 @@ function spawnWatcher(bootReason) {
|
|
|
130
130
|
const shutdownReason = readShutdownReason();
|
|
131
131
|
const reason = shutdownReason && shutdownReason.reason;
|
|
132
132
|
const idleClean = reason === 'idle';
|
|
133
|
-
|
|
133
|
+
const plannedReasons = new Set(['idle', 'sigterm', 'version-change']);
|
|
134
|
+
const isPlanned = plannedReasons.has(reason);
|
|
135
|
+
const eventName = idleClean
|
|
136
|
+
? 'supervisor.watcher-exited-idle'
|
|
137
|
+
: reason === 'version-change'
|
|
138
|
+
? 'supervisor.watcher-exited-for-update'
|
|
139
|
+
: 'supervisor.watcher-exited-unexpectedly';
|
|
140
|
+
logEvent(eventName, {
|
|
134
141
|
watcher_pid: currentChildPid,
|
|
135
142
|
exit_code: code,
|
|
136
143
|
signal,
|
|
137
144
|
shutdown_reason: reason || null,
|
|
138
145
|
had_shutdown_reason_file: shutdownReason !== null,
|
|
139
|
-
severity:
|
|
146
|
+
severity: isPlanned ? 'info' : 'critical',
|
|
140
147
|
uptime_ms: Date.now() - lastSpawnedAt,
|
|
148
|
+
...(shutdownReason || {}),
|
|
141
149
|
});
|
|
142
150
|
if (idleClean) {
|
|
143
151
|
writeSupervisorStatus('exited-idle', { watcher_pid: currentChildPid });
|
|
144
152
|
try { fs.unlinkSync(SUPERVISOR_PATH); } catch (_) {}
|
|
145
153
|
process.exit(0);
|
|
146
154
|
}
|
|
155
|
+
const respawnReason = reason === 'version-change' ? 'planned-restart-version-change' : 'unplanned-restart-after-exit';
|
|
147
156
|
writeSupervisorStatus('restarting', {
|
|
148
157
|
prior_watcher_pid: currentChildPid,
|
|
149
158
|
prior_exit_code: code,
|
|
150
159
|
prior_signal: signal,
|
|
151
160
|
prior_shutdown_reason: reason || null,
|
|
161
|
+
respawn_reason: respawnReason,
|
|
152
162
|
});
|
|
153
|
-
setTimeout(() => spawnWatcher(
|
|
163
|
+
setTimeout(() => spawnWatcher(respawnReason), 1500);
|
|
154
164
|
});
|
|
155
165
|
|
|
156
166
|
child.on('error', (err) => {
|
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.1160",
|
|
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.
|
|
42
|
+
"gm-plugkit": "^2.0.1160"
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|
|
45
45
|
"node": ">=16.0.0"
|
package/skills/gm-skill/SKILL.md
CHANGED
|
@@ -18,6 +18,8 @@ Before any apparent stop, dispatch `residual-scan`. If it returns work that fits
|
|
|
18
18
|
|
|
19
19
|
If the `instruction` response carries a non-null `update_available`, plugkit drift has been detected on disk and the running watcher is behind. Rebootstrap before continuing — newer fixes are sitting on disk unused, and every dispatch is wasted on stale code. `bun x gm-plugkit@latest` (or `npx -y gm-plugkit@latest`) is the one-shot: it fetches the new wasm, replaces the artifact, and the watcher reloads. Drift past one version is a deviation.
|
|
20
20
|
|
|
21
|
+
If `running_tasks` is non-empty, you own them — every entry is a subprocess you started that's still consuming CPU/memory. Stop ones that have outlived their purpose with `task-stop` (write `.gm/exec-spool/in/task-stop/<N>.txt` with `{id: "t<n>"}`). The 15-min idle reaper is the last resort, not a substitute for hygiene. If `stuck_spool` is non-empty, dispatches are wedged — the watcher's host_exec_js is synchronous and a stuck body blocks every other verb until it returns. Diagnose via `.watcher.log` and consider rebootstrapping if it doesn't clear; spool bodies are Turing-complete and can loop forever. Long-running work goes through `task-spawn` (returns a `task_id` immediately, body runs detached), not through the sync `nodejs`/`bash`/`python` language verbs.
|
|
22
|
+
|
|
21
23
|
The wasm artifact lives at `~/.claude/gm-tools/plugkit.wasm`; the spool watcher runs it. The watcher's own stdout/stderr is appended to `.gm/exec-spool/.watcher.log` — Read it to see plugkit's internal trace, dispatch timings, sweep actions, errors.
|
|
22
24
|
|
|
23
25
|
The watcher self-shuts-down after 15 minutes idle (no spool I/O, no live browser session) and is restarted on next agent activity by a detached supervisor. `.gm/exec-spool/.unplanned-restart.json` is a critical-failure marker — present means a prior watcher died without a planned shutdown. Treat as a PRD-worthy incident on sight: diagnose via `.watcher.log` and `gm-log/<day>/plugkit.jsonl` events `supervisor.watcher-exited-unexpectedly` and `supervisor.heartbeat-stale` around the prior_status.ts timestamp, then delete the marker once root cause is named.
|
|
@@ -106,7 +108,7 @@ Stop only when `phase` is `COMPLETE` AND `residual-scan` returns empty AND the w
|
|
|
106
108
|
|
|
107
109
|
## Orchestrator verbs
|
|
108
110
|
|
|
109
|
-
`instruction`, `transition`, `phase-status`, `prd-add`, `prd-resolve`, `prd-list`, `mutable-add`, `mutable-resolve`, `mutable-list`, `memorize-fire`, `residual-scan`, `auto-recall`.
|
|
111
|
+
`instruction`, `transition`, `phase-status`, `prd-add`, `prd-resolve`, `prd-list`, `mutable-add`, `mutable-resolve`, `mutable-list`, `memorize-fire`, `residual-scan`, `auto-recall`, `task-spawn`, `task-list`, `task-stop`, `task-output`.
|
|
110
112
|
|
|
111
113
|
## Host verbs
|
|
112
114
|
|