gm-skill 2.0.2059 → 2.0.2060
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 +1 -1
- package/bin/bootstrap.js +30 -255
- package/gm-plugkit/bootstrap.js +60 -185
- package/gm-plugkit/package.json +1 -1
- package/gm.json +1 -1
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -105,7 +105,7 @@ A task that reduces to read/investigate/report, or a change confined to files th
|
|
|
105
105
|
|
|
106
106
|
**Disciplines are isolated knowledge stores**, tracked, `@<name>` sigil-scoped, enabled list at `.gm/disciplines/enabled.txt` (one name per line; `discipline_note.rs::active_policies()` auto-surfaces each enabled discipline's `.gm/disciplines/<name>/policy.md` on every `instruction`). Detail: the recall store (`recall: gm disciplines mechanics`).
|
|
107
107
|
|
|
108
|
-
**Every runtime config surface, indexed**: `.gm/instructions/<key>.md` + `.gm/instructions/source.json` (prose vendoring, see above); `.gm/instructions/fsm/graph.json` (phase-graph override, write path is the `fsm-vendor` verb, see SKILL.md); `.gm/browser-config.json` (`BrowserConfig::load`, per-project -- `cdp_poll_timeout_ms` default 1000, `cdp_poll_interval_ms` default 250, `chrome_ready_deadline_ms` default 30000, `eval_timeout_grace_ms` default 6000, `headless` default false, `session_idle_timeout_ms` default 1800000); `.gm/daemon-project-config.json` (`gm_concurrency_limit`, per-project fairness cap on the shared pool); `~/.agentplug/daemon-config.json` (`DaemonConfig::load`, machine-wide daemon lifecycle timing, including the `side_plugin_concurrency` pool-size knob for the shared bert/treesitter/libsql instances); `.gm/disciplines/enabled.txt` + `.gm/disciplines/<name>/policy.md` (above); `AGENTPLUG_NO_DAEMON`/`CLAUDE_PROJECT_DIR`/`GM_PLUGKIT_SKIP_SELF_STALE_CHECK` (env toggles, SKILL.md documents the third). Daemon-scoped fields (`daemon-project-config.json`, `~/.agentplug/daemon-config.json`) fully detailed in `.gm/daemon-config-reference.md`; this line is the top-level index into that doc and the others.
|
|
108
|
+
**Every runtime config surface, indexed**: `.gm/instructions/<key>.md` + `.gm/instructions/source.json` (prose vendoring, see above); `.gm/instructions/fsm/graph.json` (phase-graph override, write path is the `fsm-vendor` verb, see SKILL.md); `.gm/instructions/hooks/<name>.js` (jit-hook: a real `exec_js` script the orchestrator runs automatically at a gate's evaluation, wired in via a `GateDef`'s `hook` field + `hook_mode` -- `"hook-only"` replaces the compiled predicate, `"both"` requires both, default `"predicate-only"` ignores the hook; an explicit `return true` passes, anything else -- `false`, a thrown error, non-boolean, missing `return`, missing/unreadable file -- fails CLOSED); `.gm/browser-config.json` (`BrowserConfig::load`, per-project -- `cdp_poll_timeout_ms` default 1000, `cdp_poll_interval_ms` default 250, `chrome_ready_deadline_ms` default 30000, `eval_timeout_grace_ms` default 6000, `headless` default false, `session_idle_timeout_ms` default 1800000); `.gm/daemon-project-config.json` (`gm_concurrency_limit`, per-project fairness cap on the shared pool); `~/.agentplug/daemon-config.json` (`DaemonConfig::load`, machine-wide daemon lifecycle timing, including the `side_plugin_concurrency` pool-size knob for the shared bert/treesitter/libsql instances); `.gm/disciplines/enabled.txt` + `.gm/disciplines/<name>/policy.md` (above); `AGENTPLUG_NO_DAEMON`/`CLAUDE_PROJECT_DIR`/`GM_PLUGKIT_SKIP_SELF_STALE_CHECK` (env toggles, SKILL.md documents the third). Daemon-scoped fields (`daemon-project-config.json`, `~/.agentplug/daemon-config.json`) fully detailed in `.gm/daemon-config-reference.md`; this line is the top-level index into that doc and the others.
|
|
109
109
|
|
|
110
110
|
**Nothing fake in source the user runs**: stub/mock/placeholder-return/fixture-only-path/demo-mode-short-circuit/"TODO: implement" forbidden in shipped code. Scaffolds/shims permitted only delegating to real behavior (upstream API, subprocess, disk); check for an existing library before adding a shim. Detection = behavioral: always-succeeds, input-invariant, or type-signature-satisfying short-circuit = stub. Acceptance = real input through real code into real output, witnessed. `Mock*`/`Fake*`/`Stub*` names checked at VERIFY explicitly, not left prose-only (`recall: nevil false-completion mock-network incident`). A row marked `completed` by documenting it (a README "Future Optimizations" section, a design note) instead of implementing it is the same violation -- `prd-resolve` structurally refuses two PRD rows sharing byte-identical `witness_evidence` text (`deviation.prd-resolve-duplicate-witness`), the copy-paste tell of rubber-stamped completion.
|
|
111
111
|
|
package/bin/bootstrap.js
CHANGED
|
@@ -6,7 +6,30 @@ const path = require('path');
|
|
|
6
6
|
const os = require('os');
|
|
7
7
|
const crypto = require('crypto');
|
|
8
8
|
const { spawnSync } = require('child_process');
|
|
9
|
-
const {
|
|
9
|
+
const { pidAlive, sha256OfFile, sha256OfFileSync } = require('../gm-plugkit/gm-process');
|
|
10
|
+
const shared = require('../gm-plugkit/bootstrap-shared');
|
|
11
|
+
const {
|
|
12
|
+
obsEvent,
|
|
13
|
+
cacheRoot,
|
|
14
|
+
fallbackCacheRoot,
|
|
15
|
+
gmToolsDir,
|
|
16
|
+
ensureDir,
|
|
17
|
+
acquireLock,
|
|
18
|
+
releaseLock,
|
|
19
|
+
isLockStale,
|
|
20
|
+
pruneOldVersions,
|
|
21
|
+
healIfShaMatches,
|
|
22
|
+
daemonVersionSentinel,
|
|
23
|
+
readDaemonVersion,
|
|
24
|
+
writeDaemonVersion,
|
|
25
|
+
pidCommandLineForKillGuard,
|
|
26
|
+
pidIsPlugkitProcess,
|
|
27
|
+
writeKillAttribution,
|
|
28
|
+
killPid,
|
|
29
|
+
killSpoolWatcherInCwd,
|
|
30
|
+
proactiveKillForNewInstall,
|
|
31
|
+
ensureNextStepWiring,
|
|
32
|
+
} = shared;
|
|
10
33
|
|
|
11
34
|
const NPM_PACKAGE = 'plugkit-wasm';
|
|
12
35
|
const ATTEMPT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
@@ -90,55 +113,6 @@ function ensureSkillMdCurrent(wrapperDir) {
|
|
|
90
113
|
return { refreshed: allRefreshed };
|
|
91
114
|
}
|
|
92
115
|
|
|
93
|
-
function ensureNextStepWiring(cwd) {
|
|
94
|
-
const changes = [];
|
|
95
|
-
const gmDir = path.join(cwd, '.gm');
|
|
96
|
-
try { fs.mkdirSync(gmDir, { recursive: true }); }
|
|
97
|
-
catch (e) { obsEvent('bootstrap', 'next-step.wiring.target-failed', { target: gmDir, error: e.message }); }
|
|
98
|
-
|
|
99
|
-
const nextStepPath = path.join(gmDir, 'next-step.md');
|
|
100
|
-
const nextStepBody = '# Next step\n\nDispatch the `instruction` verb to plugkit by writing `.gm/exec-spool/in/instruction/<N>.txt` (any unique N) with body `{}` (or `{"prompt":"<user request>"}` on the first dispatch of the turn). Read the response from `.gm/exec-spool/out/<N>.json` and follow the imperative in the `instruction` field.\n\nThis file is auto-rewritten by plugkit on every instruction dispatch.\n';
|
|
101
|
-
try {
|
|
102
|
-
if (!fs.existsSync(nextStepPath)) {
|
|
103
|
-
fs.writeFileSync(nextStepPath, nextStepBody);
|
|
104
|
-
changes.push('seeded .gm/next-step.md');
|
|
105
|
-
}
|
|
106
|
-
} catch (e) { obsEvent('bootstrap', 'next-step.wiring.target-failed', { target: nextStepPath, error: e.message }); }
|
|
107
|
-
|
|
108
|
-
const claudeMdPath = path.join(cwd, 'CLAUDE.md');
|
|
109
|
-
try {
|
|
110
|
-
if (!fs.existsSync(claudeMdPath)) {
|
|
111
|
-
fs.writeFileSync(claudeMdPath, '@AGENTS.md\n');
|
|
112
|
-
changes.push('created CLAUDE.md');
|
|
113
|
-
} else {
|
|
114
|
-
const cur = fs.readFileSync(claudeMdPath, 'utf8');
|
|
115
|
-
const hasLine = cur.split(/\r?\n/).some(l => l.trim() === '@AGENTS.md');
|
|
116
|
-
if (!hasLine) {
|
|
117
|
-
fs.writeFileSync(claudeMdPath, '@AGENTS.md\n' + cur);
|
|
118
|
-
changes.push('prepended @AGENTS.md to CLAUDE.md');
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
} catch (e) { obsEvent('bootstrap', 'next-step.wiring.target-failed', { target: claudeMdPath, error: e.message }); }
|
|
122
|
-
|
|
123
|
-
const agentsMdPath = path.join(cwd, 'AGENTS.md');
|
|
124
|
-
try {
|
|
125
|
-
if (fs.existsSync(agentsMdPath)) {
|
|
126
|
-
const cur = fs.readFileSync(agentsMdPath, 'utf8');
|
|
127
|
-
const hasLine = cur.split(/\r?\n/).some(l => l.trim() === '@.gm/next-step.md');
|
|
128
|
-
if (!hasLine) {
|
|
129
|
-
const sep = cur.endsWith('\n') ? '' : '\n';
|
|
130
|
-
fs.writeFileSync(agentsMdPath, cur + sep + '\n@.gm/next-step.md\n');
|
|
131
|
-
changes.push('appended @.gm/next-step.md to AGENTS.md');
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
} catch (e) { obsEvent('bootstrap', 'next-step.wiring.target-failed', { target: agentsMdPath, error: e.message }); }
|
|
135
|
-
|
|
136
|
-
if (changes.length > 0) {
|
|
137
|
-
log(`next-step wiring: ${changes.join(', ')}`);
|
|
138
|
-
obsEvent('bootstrap', 'next-step.wiring.applied', { changes });
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
116
|
function resolveWindowsExe(cmd) {
|
|
143
117
|
if (process.platform !== 'win32') return cmd;
|
|
144
118
|
try {
|
|
@@ -180,6 +154,7 @@ function writeBootstrapError(spec) {
|
|
|
180
154
|
}
|
|
181
155
|
|
|
182
156
|
function clearBootstrapError() {
|
|
157
|
+
// best-effort, missing file is fine
|
|
183
158
|
try {
|
|
184
159
|
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
185
160
|
const out = path.join(projectDir, '.gm', 'exec-spool', '.bootstrap-error.json');
|
|
@@ -187,52 +162,6 @@ function clearBootstrapError() {
|
|
|
187
162
|
} catch (_) {}
|
|
188
163
|
}
|
|
189
164
|
|
|
190
|
-
function obsEvent(subsystem, event, fields) {
|
|
191
|
-
if (process.env.GM_LOG_DISABLE) return;
|
|
192
|
-
try {
|
|
193
|
-
const root = process.env.GM_LOG_DIR
|
|
194
|
-
|| path.join(os.homedir(), '.claude', 'gm-log');
|
|
195
|
-
const day = new Date().toISOString().slice(0, 10);
|
|
196
|
-
const dir = path.join(root, day);
|
|
197
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
198
|
-
const line = JSON.stringify({
|
|
199
|
-
ts: new Date().toISOString(),
|
|
200
|
-
sub: subsystem,
|
|
201
|
-
event,
|
|
202
|
-
pid: process.pid,
|
|
203
|
-
sess: process.env.CLAUDE_SESSION_ID || process.env.GM_SESSION_ID || '',
|
|
204
|
-
...fields,
|
|
205
|
-
});
|
|
206
|
-
fs.appendFileSync(path.join(dir, `${subsystem}.jsonl`), line + '\n');
|
|
207
|
-
} catch (_) {}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
function cacheRoot() {
|
|
212
|
-
const home = os.homedir();
|
|
213
|
-
if (process.env.PLUGKIT_CACHE_DIR) return process.env.PLUGKIT_CACHE_DIR;
|
|
214
|
-
if (os.platform() === 'win32') {
|
|
215
|
-
const base = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
|
216
|
-
return path.join(base, 'plugkit', 'bin');
|
|
217
|
-
}
|
|
218
|
-
if (os.platform() === 'darwin') return path.join(home, 'Library', 'Caches', 'plugkit', 'bin');
|
|
219
|
-
const xdg = process.env.XDG_CACHE_HOME || path.join(home, '.cache');
|
|
220
|
-
return path.join(xdg, 'plugkit', 'bin');
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function fallbackCacheRoot() {
|
|
224
|
-
return path.join(os.tmpdir(), 'plugkit-cache', 'bin');
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function gmToolsDir() {
|
|
228
|
-
const home = process.env.USERPROFILE || process.env.HOME || os.homedir();
|
|
229
|
-
const primary = path.join(home, '.gm-tools');
|
|
230
|
-
const fallback = path.join(home, '.claude', 'gm-tools');
|
|
231
|
-
if (fs.existsSync(primary)) return primary;
|
|
232
|
-
if (fs.existsSync(fallback)) return fallback;
|
|
233
|
-
return primary;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
165
|
function copyWasmToGmTools(wasmPath, wrapperDir, version) {
|
|
237
166
|
const dst = gmToolsDir();
|
|
238
167
|
fs.mkdirSync(dst, { recursive: true });
|
|
@@ -262,22 +191,6 @@ function readVersionFile(wrapperDir) {
|
|
|
262
191
|
}
|
|
263
192
|
|
|
264
193
|
|
|
265
|
-
function healIfShaMatches(binPath, expectedSha, sentinelPath, partialPath, kind) {
|
|
266
|
-
if (!fs.existsSync(binPath)) return false;
|
|
267
|
-
if (partialPath) { try { if (fs.existsSync(partialPath)) fs.unlinkSync(partialPath); } catch (_) {} }
|
|
268
|
-
if (!expectedSha) return false;
|
|
269
|
-
let got;
|
|
270
|
-
try { got = sha256OfFileSync(binPath); }
|
|
271
|
-
catch (_) { return false; }
|
|
272
|
-
if (got !== expectedSha) {
|
|
273
|
-
try { fs.unlinkSync(binPath); } catch (_) {}
|
|
274
|
-
return false;
|
|
275
|
-
}
|
|
276
|
-
try { fs.writeFileSync(sentinelPath, new Date().toISOString()); } catch (_) { return false; }
|
|
277
|
-
obsEvent('bootstrap', 'cache.heal', { path: binPath, kind });
|
|
278
|
-
return true;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
194
|
function readShaManifest(wrapperDir, manifestName) {
|
|
282
195
|
const p = path.join(wrapperDir, manifestName || 'plugkit.sha256');
|
|
283
196
|
if (!fs.existsSync(p)) return null;
|
|
@@ -289,37 +202,6 @@ function readShaManifest(wrapperDir, manifestName) {
|
|
|
289
202
|
return out;
|
|
290
203
|
}
|
|
291
204
|
|
|
292
|
-
function acquireLock(lockPath) {
|
|
293
|
-
const start = Date.now();
|
|
294
|
-
for (;;) {
|
|
295
|
-
try {
|
|
296
|
-
const fd = fs.openSync(lockPath, 'wx');
|
|
297
|
-
fs.writeSync(fd, String(process.pid));
|
|
298
|
-
fs.closeSync(fd);
|
|
299
|
-
return true;
|
|
300
|
-
} catch (err) {
|
|
301
|
-
if (err.code !== 'EEXIST') throw err;
|
|
302
|
-
let stale = false;
|
|
303
|
-
try {
|
|
304
|
-
const st = fs.statSync(lockPath);
|
|
305
|
-
if (Date.now() - st.mtimeMs > LOCK_STALE_MS) stale = true;
|
|
306
|
-
const owner = parseInt(fs.readFileSync(lockPath, 'utf8').trim(), 10);
|
|
307
|
-
if (Number.isFinite(owner) && owner !== process.pid && !pidAlive(owner)) stale = true;
|
|
308
|
-
} catch (_) { stale = true; }
|
|
309
|
-
if (stale) {
|
|
310
|
-
try { fs.unlinkSync(lockPath); } catch (_) {}
|
|
311
|
-
continue;
|
|
312
|
-
}
|
|
313
|
-
if (Date.now() - start > ATTEMPT_TIMEOUT_MS) throw new Error(`lock wait timeout: ${lockPath}`);
|
|
314
|
-
try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2000); } catch (_) {}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function releaseLock(lockPath) {
|
|
320
|
-
try { fs.unlinkSync(lockPath); } catch (_) {}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
205
|
async function extractNpmPackageWasm(destPath, version) {
|
|
324
206
|
const tempDir = path.join(path.dirname(destPath), '.npm-extract-' + Date.now());
|
|
325
207
|
try {
|
|
@@ -342,7 +224,6 @@ async function extractNpmPackageWasm(destPath, version) {
|
|
|
342
224
|
encoding: 'utf8',
|
|
343
225
|
windowsHide: true,
|
|
344
226
|
...(isCmdShim ? { shell: true } : {}),
|
|
345
|
-
...(process.platform === 'win32' ? { creationFlags: 0x08000000 } : {}),
|
|
346
227
|
}
|
|
347
228
|
);
|
|
348
229
|
|
|
@@ -393,34 +274,6 @@ async function extractNpmPackageWithRetry(destPath, version) {
|
|
|
393
274
|
throw lastErr;
|
|
394
275
|
}
|
|
395
276
|
|
|
396
|
-
function isLockStale(lockPath) {
|
|
397
|
-
try {
|
|
398
|
-
const st = fs.statSync(lockPath);
|
|
399
|
-
if (Date.now() - st.mtimeMs > LOCK_STALE_MS) return true;
|
|
400
|
-
const owner = parseInt(fs.readFileSync(lockPath, 'utf8').trim(), 10);
|
|
401
|
-
if (Number.isFinite(owner) && !pidAlive(owner)) return true;
|
|
402
|
-
} catch (_) { return true; }
|
|
403
|
-
return false;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function pruneOldVersions(root, keepVersion) {
|
|
407
|
-
try {
|
|
408
|
-
const entries = fs.readdirSync(root);
|
|
409
|
-
for (const e of entries) {
|
|
410
|
-
if (!e.startsWith('v')) continue;
|
|
411
|
-
if (e === `v${keepVersion}`) continue;
|
|
412
|
-
const dir = path.join(root, e);
|
|
413
|
-
const lock = path.join(dir, '.lock');
|
|
414
|
-
if (fs.existsSync(lock) && !isLockStale(lock)) continue;
|
|
415
|
-
if (fs.existsSync(lock)) { try { fs.unlinkSync(lock); } catch (_) {} }
|
|
416
|
-
try {
|
|
417
|
-
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 1, retryDelay: 50 });
|
|
418
|
-
log(`pruned ${dir}`);
|
|
419
|
-
} catch (err) { log(`prune skip ${dir}: ${err.message}`); }
|
|
420
|
-
}
|
|
421
|
-
} catch (_) {}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
277
|
async function bootstrap(opts) {
|
|
425
278
|
opts = opts || {};
|
|
426
279
|
const wrapperDir = opts.wrapperDir || __dirname;
|
|
@@ -565,91 +418,13 @@ function getWasmPath(opts) {
|
|
|
565
418
|
return null;
|
|
566
419
|
}
|
|
567
420
|
|
|
568
|
-
function daemonVersionSentinel() {
|
|
569
|
-
const root = (() => {
|
|
570
|
-
try { const r = cacheRoot(); ensureDir(r); return r; }
|
|
571
|
-
catch (_) { const r = fallbackCacheRoot(); ensureDir(r); return r; }
|
|
572
|
-
})();
|
|
573
|
-
return path.join(root, '.daemon-version');
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
function readDaemonVersion() {
|
|
577
|
-
try { return fs.readFileSync(daemonVersionSentinel(), 'utf8').trim(); }
|
|
578
|
-
catch (_) { return null; }
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
function writeDaemonVersion(v) {
|
|
582
|
-
try { fs.writeFileSync(daemonVersionSentinel(), String(v)); } catch (_) {}
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
function pidCommandLineForKillGuard(pid) {
|
|
586
|
-
try {
|
|
587
|
-
const { spawnSync } = require('child_process');
|
|
588
|
-
if (process.platform === 'win32') {
|
|
589
|
-
const r = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', `(Get-CimInstance Win32_Process -Filter "ProcessId=${Number(pid)}").CommandLine`], { encoding: 'utf8', windowsHide: true, timeout: 5000 });
|
|
590
|
-
return String((r && r.stdout) || '');
|
|
591
|
-
}
|
|
592
|
-
const r = spawnSync('ps', ['-p', String(pid), '-o', 'args='], { encoding: 'utf8', timeout: 5000 });
|
|
593
|
-
return String((r && r.stdout) || '');
|
|
594
|
-
} catch (_) { return ''; }
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
function pidIsPlugkitProcess(pid) {
|
|
598
|
-
return /agentplug-runner(\.exe)?/i.test(pidCommandLineForKillGuard(pid));
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
function writeKillAttribution(targetSpoolDir, info) {
|
|
602
|
-
try {
|
|
603
|
-
fs.mkdirSync(targetSpoolDir, { recursive: true });
|
|
604
|
-
fs.writeFileSync(path.join(targetSpoolDir, '.kill-attribution.json'), JSON.stringify({ killer_pid: process.pid, killer_cwd: process.cwd(), killer_script: __filename, ts: Date.now(), ...info }, null, 2));
|
|
605
|
-
} catch (_) {}
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
function killPid(pid) {
|
|
609
|
-
if (!Number.isFinite(pid) || pid === process.pid || !pidAlive(pid)) return false;
|
|
610
|
-
try { process.kill(pid, 'SIGTERM'); }
|
|
611
|
-
catch (_) { try { process.kill(pid); } catch (_) {} }
|
|
612
|
-
if (os.platform() === 'win32' && pidAlive(pid)) {
|
|
613
|
-
try {
|
|
614
|
-
const { spawnSync } = require('child_process');
|
|
615
|
-
spawnSync('taskkill', ['/F', '/PID', String(pid)], { stdio: 'ignore', windowsHide: true, timeout: 3000, killSignal: 'SIGKILL' });
|
|
616
|
-
} catch (_) {}
|
|
617
|
-
}
|
|
618
|
-
return true;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
function killSpoolWatcherInCwd(reason) {
|
|
622
|
-
try {
|
|
623
|
-
const pidPath = path.join(process.cwd(), '.gm', 'exec-spool', '.watcher.pid');
|
|
624
|
-
if (!fs.existsSync(pidPath)) return null;
|
|
625
|
-
const pid = parseInt(fs.readFileSync(pidPath, 'utf8').trim(), 10);
|
|
626
|
-
if (pidAlive(pid) && !pidIsPlugkitProcess(pid)) {
|
|
627
|
-
obsEvent('bootstrap', 'watcher.kill-skipped-pid-reused', { pid, reason });
|
|
628
|
-
try { fs.unlinkSync(pidPath); } catch (_) {}
|
|
629
|
-
return null;
|
|
630
|
-
}
|
|
631
|
-
writeKillAttribution(path.join(process.cwd(), '.gm', 'exec-spool'), { reason, target_pid: pid, via: 'killSpoolWatcherInCwd' });
|
|
632
|
-
if (killPid(pid)) {
|
|
633
|
-
obsEvent('bootstrap', 'watcher.killed', { pid, reason });
|
|
634
|
-
try { fs.unlinkSync(pidPath); } catch (_) {}
|
|
635
|
-
return pid;
|
|
636
|
-
}
|
|
637
|
-
try { fs.unlinkSync(pidPath); } catch (_) {}
|
|
638
|
-
} catch (_) {}
|
|
639
|
-
return null;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
function proactiveKillForNewInstall(installedVersion) {
|
|
643
|
-
try {
|
|
644
|
-
const reason = `install:v${installedVersion}`;
|
|
645
|
-
killSpoolWatcherInCwd(reason);
|
|
646
|
-
writeDaemonVersion(installedVersion);
|
|
647
|
-
} catch (_) {}
|
|
648
|
-
}
|
|
649
|
-
|
|
650
421
|
function killStaleDaemonIfVersionChanged(wrapperDir) {
|
|
651
422
|
let currentVersion;
|
|
652
|
-
try { currentVersion = readVersionFile(wrapperDir); }
|
|
423
|
+
try { currentVersion = readVersionFile(wrapperDir); }
|
|
424
|
+
catch (e) {
|
|
425
|
+
obsEvent('bootstrap', 'kill-stale-daemon.version-read-failed', { error: e.message });
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
653
428
|
const cached = getWasmPath({ wrapperDir, version: currentVersion });
|
|
654
429
|
if (cached) {
|
|
655
430
|
proactiveKillForNewInstall(currentVersion);
|
package/gm-plugkit/bootstrap.js
CHANGED
|
@@ -6,8 +6,35 @@ const path = require('path');
|
|
|
6
6
|
const os = require('os');
|
|
7
7
|
const crypto = require('crypto');
|
|
8
8
|
const { spawn, spawnSync } = require('child_process');
|
|
9
|
-
const {
|
|
10
|
-
const
|
|
9
|
+
const { pidAlive, sha256OfFile, sha256OfFileSync } = require('./gm-process');
|
|
10
|
+
const shared = require('./bootstrap-shared');
|
|
11
|
+
const {
|
|
12
|
+
obsEvent,
|
|
13
|
+
cacheRoot,
|
|
14
|
+
fallbackCacheRoot,
|
|
15
|
+
gmToolsDir,
|
|
16
|
+
ensureDir,
|
|
17
|
+
acquireLock,
|
|
18
|
+
releaseLock,
|
|
19
|
+
isLockStale,
|
|
20
|
+
pruneOldVersions,
|
|
21
|
+
healIfShaMatches,
|
|
22
|
+
daemonVersionSentinel,
|
|
23
|
+
readDaemonVersion,
|
|
24
|
+
writeDaemonVersion,
|
|
25
|
+
pidCommandLineForKillGuard,
|
|
26
|
+
pidIsPlugkitProcess,
|
|
27
|
+
writeKillAttribution,
|
|
28
|
+
killPid,
|
|
29
|
+
killSpoolWatcherInCwd,
|
|
30
|
+
proactiveKillForNewInstall,
|
|
31
|
+
} = shared;
|
|
32
|
+
|
|
33
|
+
// ensureNextStepWiring here is intentionally NOT imported from
|
|
34
|
+
// bootstrap-shared -- this file's version does strictly more (CLAUDE.md
|
|
35
|
+
// prepend, AGENTS.md append, managed .npmignore block) than bin/bootstrap.js's
|
|
36
|
+
// leaner one, so it is genuinely-different logic, not accidental drift, and
|
|
37
|
+
// stays local to this file.
|
|
11
38
|
|
|
12
39
|
function resolveWindowsExe(cmd) {
|
|
13
40
|
if (process.platform !== 'win32') return cmd;
|
|
@@ -40,10 +67,6 @@ function log(msg) {
|
|
|
40
67
|
try { process.stderr.write(`[gm-plugkit] ${msg}\n`); } catch (_) {}
|
|
41
68
|
}
|
|
42
69
|
|
|
43
|
-
function obsEvent(subsystem, event, fields) {
|
|
44
|
-
_sharedLogEvent(subsystem, event, fields, { cwd: false });
|
|
45
|
-
}
|
|
46
|
-
|
|
47
70
|
function writeBootstrapError(spec) {
|
|
48
71
|
try {
|
|
49
72
|
const projectDir = resolveProjectRoot(process.env.CLAUDE_PROJECT_DIR || process.cwd());
|
|
@@ -54,6 +77,7 @@ function writeBootstrapError(spec) {
|
|
|
54
77
|
}
|
|
55
78
|
|
|
56
79
|
function clearBootstrapError() {
|
|
80
|
+
// best-effort, missing file is fine
|
|
57
81
|
try {
|
|
58
82
|
const projectDir = resolveProjectRoot(process.env.CLAUDE_PROJECT_DIR || process.cwd());
|
|
59
83
|
fs.unlinkSync(path.join(projectDir, '.gm', 'exec-spool', '.bootstrap-error.json'));
|
|
@@ -79,7 +103,12 @@ function instructionsManifestPath(cwd) {
|
|
|
79
103
|
|
|
80
104
|
function readInstructionsManifest(cwd) {
|
|
81
105
|
try { return JSON.parse(fs.readFileSync(instructionsManifestPath(cwd), 'utf-8')); }
|
|
82
|
-
catch (
|
|
106
|
+
catch (e) {
|
|
107
|
+
if (e && e.code !== 'ENOENT') {
|
|
108
|
+
obsEvent('bootstrap', 'instructions-bundle.manifest-read-failed', { error: e.message });
|
|
109
|
+
}
|
|
110
|
+
return {};
|
|
111
|
+
}
|
|
83
112
|
}
|
|
84
113
|
|
|
85
114
|
function writeInstructionsManifest(cwd, manifest) {
|
|
@@ -229,31 +258,6 @@ function ensureNextStepWiring(cwd) {
|
|
|
229
258
|
}
|
|
230
259
|
|
|
231
260
|
|
|
232
|
-
function cacheRoot() {
|
|
233
|
-
const home = os.homedir();
|
|
234
|
-
if (process.env.PLUGKIT_CACHE_DIR) return process.env.PLUGKIT_CACHE_DIR;
|
|
235
|
-
if (os.platform() === 'win32') {
|
|
236
|
-
const base = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
|
237
|
-
return path.join(base, 'plugkit', 'bin');
|
|
238
|
-
}
|
|
239
|
-
if (os.platform() === 'darwin') return path.join(home, 'Library', 'Caches', 'plugkit', 'bin');
|
|
240
|
-
const xdg = process.env.XDG_CACHE_HOME || path.join(home, '.cache');
|
|
241
|
-
return path.join(xdg, 'plugkit', 'bin');
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function fallbackCacheRoot() {
|
|
245
|
-
return path.join(os.tmpdir(), 'plugkit-cache', 'bin');
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function gmToolsDir() {
|
|
249
|
-
const home = process.env.USERPROFILE || process.env.HOME || os.homedir();
|
|
250
|
-
const primary = path.join(home, '.gm-tools');
|
|
251
|
-
const fallback = path.join(home, '.claude', 'gm-tools');
|
|
252
|
-
if (fs.existsSync(primary)) return primary;
|
|
253
|
-
if (fs.existsSync(fallback)) return fallback;
|
|
254
|
-
return primary;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
261
|
// Slim-artifact eligibility: plugkit-core's embed.rs probes host_vec_embed
|
|
258
262
|
// before ever loading its wasm-embedded safetensors fallback (see
|
|
259
263
|
// rs-plugkit/crates/plugkit-core/src/embed.rs::init_ctx) -- a slim build
|
|
@@ -311,6 +315,13 @@ function readVersionFile() {
|
|
|
311
315
|
return fs.readFileSync(p, 'utf8').trim();
|
|
312
316
|
}
|
|
313
317
|
|
|
318
|
+
// readVersionFile() throws loudly on its own (missing/unreadable file is a
|
|
319
|
+
// real, surfaced error) -- callers below that swallow it are choosing to
|
|
320
|
+
// no-op rather than propagate, so they log what they swallowed instead of
|
|
321
|
+
// going silent. This is the actual behavior at every call site already
|
|
322
|
+
// (killStaleDaemonIfVersionChanged just returns, ensureReady leaves
|
|
323
|
+
// pinnedVersion null) -- only the missing observability was the gap.
|
|
324
|
+
|
|
314
325
|
function readShaManifest() {
|
|
315
326
|
const p = path.join(wrapperDir, 'plugkit.sha256');
|
|
316
327
|
if (!fs.existsSync(p)) return null;
|
|
@@ -340,37 +351,6 @@ function readShaManifest() {
|
|
|
340
351
|
return out;
|
|
341
352
|
}
|
|
342
353
|
|
|
343
|
-
function acquireLock(lockPath) {
|
|
344
|
-
const start = Date.now();
|
|
345
|
-
for (;;) {
|
|
346
|
-
try {
|
|
347
|
-
const fd = fs.openSync(lockPath, 'wx');
|
|
348
|
-
fs.writeSync(fd, String(process.pid));
|
|
349
|
-
fs.closeSync(fd);
|
|
350
|
-
return true;
|
|
351
|
-
} catch (err) {
|
|
352
|
-
if (err.code !== 'EEXIST') throw err;
|
|
353
|
-
let stale = false;
|
|
354
|
-
try {
|
|
355
|
-
const st = fs.statSync(lockPath);
|
|
356
|
-
if (Date.now() - st.mtimeMs > LOCK_STALE_MS) stale = true;
|
|
357
|
-
const owner = parseInt(fs.readFileSync(lockPath, 'utf8').trim(), 10);
|
|
358
|
-
if (Number.isFinite(owner) && owner !== process.pid && !pidAlive(owner)) stale = true;
|
|
359
|
-
} catch (_) { stale = true; }
|
|
360
|
-
if (stale) {
|
|
361
|
-
try { fs.unlinkSync(lockPath); } catch (_) {}
|
|
362
|
-
continue;
|
|
363
|
-
}
|
|
364
|
-
if (Date.now() - start > ATTEMPT_TIMEOUT_MS) throw new Error(`lock wait timeout: ${lockPath}`);
|
|
365
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2000);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function releaseLock(lockPath) {
|
|
371
|
-
try { fs.unlinkSync(lockPath); } catch (_) {}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
354
|
async function extractNpmPackageWasm(destPath, version) {
|
|
375
355
|
const tempDir = path.join(path.dirname(destPath), '.npm-extract-' + Date.now());
|
|
376
356
|
try {
|
|
@@ -395,7 +375,6 @@ async function extractNpmPackageWasm(destPath, version) {
|
|
|
395
375
|
encoding: 'utf8',
|
|
396
376
|
windowsHide: true,
|
|
397
377
|
...(isCmdShim ? { shell: true } : {}),
|
|
398
|
-
...(process.platform === 'win32' ? { creationFlags: 0x08000000 } : {}),
|
|
399
378
|
});
|
|
400
379
|
|
|
401
380
|
if (result.error) throw result.error;
|
|
@@ -533,125 +512,13 @@ async function extractNpmPackageWithRetry(destPath, version) {
|
|
|
533
512
|
}
|
|
534
513
|
|
|
535
514
|
|
|
536
|
-
function healIfShaMatches(binPath, expectedSha, sentinelPath, partialPath, kind) {
|
|
537
|
-
if (!fs.existsSync(binPath)) return false;
|
|
538
|
-
if (partialPath) { try { if (fs.existsSync(partialPath)) fs.unlinkSync(partialPath); } catch (_) {} }
|
|
539
|
-
if (!expectedSha) return false;
|
|
540
|
-
let got;
|
|
541
|
-
try { got = sha256OfFileSync(binPath); }
|
|
542
|
-
catch (_) { return false; }
|
|
543
|
-
if (got !== expectedSha) {
|
|
544
|
-
try { fs.unlinkSync(binPath); } catch (_) {}
|
|
545
|
-
return false;
|
|
546
|
-
}
|
|
547
|
-
try { fs.writeFileSync(sentinelPath, new Date().toISOString()); } catch (_) { return false; }
|
|
548
|
-
obsEvent('bootstrap', 'cache.heal', { path: binPath, kind });
|
|
549
|
-
return true;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
function daemonVersionSentinel() {
|
|
553
|
-
const root = (() => {
|
|
554
|
-
try { const r = cacheRoot(); ensureDir(r); return r; }
|
|
555
|
-
catch (_) { const r = fallbackCacheRoot(); ensureDir(r); return r; }
|
|
556
|
-
})();
|
|
557
|
-
return path.join(root, '.daemon-version');
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
function readDaemonVersion() {
|
|
561
|
-
try { return fs.readFileSync(daemonVersionSentinel(), 'utf8').trim(); }
|
|
562
|
-
catch (_) { return null; }
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
function writeDaemonVersion(v) {
|
|
566
|
-
try { fs.writeFileSync(daemonVersionSentinel(), String(v)); } catch (_) {}
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
function pidCommandLineForKillGuard(pid) {
|
|
570
|
-
return _sharedPidCommandLine(pid);
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
function pidIsPlugkitProcess(pid) {
|
|
574
|
-
return /agentplug-runner(\.exe)?/i.test(pidCommandLineForKillGuard(pid));
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
function writeKillAttribution(targetSpoolDir, info) {
|
|
578
|
-
try {
|
|
579
|
-
fs.mkdirSync(targetSpoolDir, { recursive: true });
|
|
580
|
-
fs.writeFileSync(path.join(targetSpoolDir, '.kill-attribution.json'), JSON.stringify({ killer_pid: process.pid, killer_cwd: process.cwd(), killer_script: __filename, ts: Date.now(), ...info }, null, 2));
|
|
581
|
-
} catch (_) {}
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
function killPid(pid) {
|
|
585
|
-
if (!Number.isFinite(pid) || pid === process.pid || !pidAlive(pid)) return false;
|
|
586
|
-
try { process.kill(pid, 'SIGTERM'); }
|
|
587
|
-
catch (_) { try { process.kill(pid); } catch (_) {} }
|
|
588
|
-
if (os.platform() === 'win32' && pidAlive(pid)) {
|
|
589
|
-
try { spawnSync('taskkill', ['/F', '/PID', String(pid)], { stdio: 'ignore', windowsHide: true, timeout: 3000, killSignal: 'SIGKILL' }); } catch (_) {}
|
|
590
|
-
}
|
|
591
|
-
return true;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
function killSpoolWatcherInCwd(reason) {
|
|
595
|
-
try {
|
|
596
|
-
const pidPath = path.join(process.cwd(), '.gm', 'exec-spool', '.watcher.pid');
|
|
597
|
-
if (!fs.existsSync(pidPath)) return null;
|
|
598
|
-
const pid = parseInt(fs.readFileSync(pidPath, 'utf8').trim(), 10);
|
|
599
|
-
if (pidAlive(pid) && !pidIsPlugkitProcess(pid)) {
|
|
600
|
-
obsEvent('bootstrap', 'watcher.kill-skipped-pid-reused', { pid, reason });
|
|
601
|
-
try { fs.unlinkSync(pidPath); } catch (_) {}
|
|
602
|
-
return null;
|
|
603
|
-
}
|
|
604
|
-
writeKillAttribution(path.join(process.cwd(), '.gm', 'exec-spool'), { reason, target_pid: pid, via: 'killSpoolWatcherInCwd' });
|
|
605
|
-
if (killPid(pid)) {
|
|
606
|
-
obsEvent('bootstrap', 'watcher.killed', { pid, reason });
|
|
607
|
-
try { fs.unlinkSync(pidPath); } catch (_) {}
|
|
608
|
-
return pid;
|
|
609
|
-
}
|
|
610
|
-
try { fs.unlinkSync(pidPath); } catch (_) {}
|
|
611
|
-
} catch (_) {}
|
|
612
|
-
return null;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
function isLockStale(lockPath) {
|
|
617
|
-
try {
|
|
618
|
-
const st = fs.statSync(lockPath);
|
|
619
|
-
if (Date.now() - st.mtimeMs > LOCK_STALE_MS) return true;
|
|
620
|
-
const owner = parseInt(fs.readFileSync(lockPath, 'utf8').trim(), 10);
|
|
621
|
-
if (Number.isFinite(owner) && !pidAlive(owner)) return true;
|
|
622
|
-
} catch (_) { return true; }
|
|
623
|
-
return false;
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
function pruneOldVersions(root, keepVersion) {
|
|
627
|
-
try {
|
|
628
|
-
const entries = fs.readdirSync(root);
|
|
629
|
-
for (const e of entries) {
|
|
630
|
-
if (!e.startsWith('v')) continue;
|
|
631
|
-
if (e === `v${keepVersion}`) continue;
|
|
632
|
-
const dir = path.join(root, e);
|
|
633
|
-
const lock = path.join(dir, '.lock');
|
|
634
|
-
if (fs.existsSync(lock) && !isLockStale(lock)) continue;
|
|
635
|
-
if (fs.existsSync(lock)) { try { fs.unlinkSync(lock); } catch (_) {} }
|
|
636
|
-
try {
|
|
637
|
-
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 1, retryDelay: 50 });
|
|
638
|
-
log(`pruned ${dir}`);
|
|
639
|
-
} catch (err) { log(`prune skip ${dir}: ${err.message}`); }
|
|
640
|
-
}
|
|
641
|
-
} catch (_) {}
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
function proactiveKillForNewInstall(installedVersion) {
|
|
645
|
-
try {
|
|
646
|
-
const reason = `install:v${installedVersion}`;
|
|
647
|
-
killSpoolWatcherInCwd(reason);
|
|
648
|
-
writeDaemonVersion(installedVersion);
|
|
649
|
-
} catch (_) {}
|
|
650
|
-
}
|
|
651
|
-
|
|
652
515
|
function killStaleDaemonIfVersionChanged() {
|
|
653
516
|
let currentVersion;
|
|
654
|
-
try { currentVersion = readVersionFile(); }
|
|
517
|
+
try { currentVersion = readVersionFile(); }
|
|
518
|
+
catch (e) {
|
|
519
|
+
obsEvent('bootstrap', 'kill-stale-daemon.version-read-failed', { error: e.message });
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
655
522
|
const cached = resolveCachedBinary({ version: currentVersion });
|
|
656
523
|
if (cached) {
|
|
657
524
|
proactiveKillForNewInstall(currentVersion, cached);
|
|
@@ -976,7 +843,9 @@ function discoverBundledSkillsAndSources() {
|
|
|
976
843
|
const p = path.join(root, e.name, 'SKILL.md');
|
|
977
844
|
if (fs.existsSync(p) && !found.has(e.name)) found.set(e.name, p);
|
|
978
845
|
}
|
|
979
|
-
} catch (
|
|
846
|
+
} catch (e) {
|
|
847
|
+
obsEvent('bootstrap', 'discover-bundled-skills.dir-read-failed', { root, error: e.message });
|
|
848
|
+
}
|
|
980
849
|
}
|
|
981
850
|
return found;
|
|
982
851
|
}
|
|
@@ -1060,7 +929,9 @@ async function resolveLatestRemoteVersion(timeoutMs) {
|
|
|
1060
929
|
const hasPlugkitWasm = Array.isArray(rel.assets) && rel.assets.some(a => a && a.name === 'plugkit.wasm');
|
|
1061
930
|
if (hasPlugkitWasm) return m[1];
|
|
1062
931
|
}
|
|
1063
|
-
} catch (
|
|
932
|
+
} catch (e) {
|
|
933
|
+
obsEvent('bootstrap', 'resolve-latest-remote-version.failed', { error: e.message });
|
|
934
|
+
}
|
|
1064
935
|
return null;
|
|
1065
936
|
}
|
|
1066
937
|
|
|
@@ -1069,7 +940,9 @@ async function resolveLatestGmPlugkitNpmVersion(timeoutMs) {
|
|
|
1069
940
|
const buf = await httpGetBuffer('https://registry.npmjs.org/gm-plugkit/latest', timeoutMs || 3000);
|
|
1070
941
|
const meta = JSON.parse(buf.toString('utf-8'));
|
|
1071
942
|
if (meta && typeof meta.version === 'string') return meta.version;
|
|
1072
|
-
} catch (
|
|
943
|
+
} catch (e) {
|
|
944
|
+
obsEvent('bootstrap', 'resolve-latest-npm-version.failed', { error: e.message });
|
|
945
|
+
}
|
|
1073
946
|
return null;
|
|
1074
947
|
}
|
|
1075
948
|
|
|
@@ -1120,7 +993,9 @@ async function ensureReady(opts) {
|
|
|
1120
993
|
if (fs.existsSync(stalePath)) fs.unlinkSync(stalePath);
|
|
1121
994
|
} catch (_) {}
|
|
1122
995
|
}
|
|
1123
|
-
} catch (
|
|
996
|
+
} catch (e) {
|
|
997
|
+
obsEvent('bootstrap', 'self-stale-check.failed', { error: e.message });
|
|
998
|
+
}
|
|
1124
999
|
}
|
|
1125
1000
|
|
|
1126
1001
|
let pinnedVersion = null;
|
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.2060",
|
|
4
4
|
"description": "Bootstrap and daemon-spawn tool for gm plugkit binary. Downloads the correct platform wasm, verifies SHA256, and launches agentplug-runner (the native wasm host) as the spool watcher daemon.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
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.2060",
|
|
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",
|