gm-thebird 2.0.1069 → 2.0.1071
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/bin/bootstrap.js +143 -336
- package/bin/plugkit.js +29 -4
- package/gm.json +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/code-search/SKILL.md +1 -1
- package/skills/gm-complete/SKILL.md +2 -2
- package/skills/gm-emit/SKILL.md +1 -1
- package/skills/gm-execute/SKILL.md +5 -5
- package/skills/planning/SKILL.md +2 -2
package/bin/bootstrap.js
CHANGED
|
@@ -4,20 +4,13 @@
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const os = require('os');
|
|
7
|
-
const https = require('https');
|
|
8
7
|
const crypto = require('crypto');
|
|
9
|
-
const {
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
const ATTEMPT_TIMEOUT_MS =
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const BACKOFF_MS = [2000, 5000, 15000, 30000];
|
|
16
|
-
// Worst case: a slow link downloading 140MB at 1MB/s = ~140s. Allow 30 minutes
|
|
17
|
-
// before another bootstrap process treats this lock as abandoned. Below this,
|
|
18
|
-
// concurrent bootstrap calls would wipe an in-progress download mid-stream
|
|
19
|
-
// (see the v0.1.294 incident where a race between two wrappers blew away the
|
|
20
|
-
// .partial during a 10-minute fetch).
|
|
8
|
+
const { spawnSync } = require('child_process');
|
|
9
|
+
|
|
10
|
+
const NPM_PACKAGE = '@anentrypoint/plugkit-wasm';
|
|
11
|
+
const ATTEMPT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
12
|
+
const MAX_ATTEMPTS = 3;
|
|
13
|
+
const BACKOFF_MS = [5000, 15000];
|
|
21
14
|
const LOCK_STALE_MS = 30 * 60 * 1000;
|
|
22
15
|
|
|
23
16
|
function log(msg) {
|
|
@@ -73,18 +66,6 @@ function obsEvent(subsystem, event, fields) {
|
|
|
73
66
|
} catch (_) {}
|
|
74
67
|
}
|
|
75
68
|
|
|
76
|
-
function platformKey() {
|
|
77
|
-
const p = os.platform();
|
|
78
|
-
const a = os.arch();
|
|
79
|
-
if (p === 'win32') return a === 'arm64' ? 'win32-arm64' : 'win32-x64';
|
|
80
|
-
if (p === 'darwin') return a === 'arm64' ? 'darwin-arm64' : 'darwin-x64';
|
|
81
|
-
return (a === 'arm64' || a === 'aarch64') ? 'linux-arm64' : 'linux-x64';
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function binaryName() {
|
|
85
|
-
const key = platformKey();
|
|
86
|
-
return key.startsWith('win32') ? `plugkit-${key}.exe` : `plugkit-${key}`;
|
|
87
|
-
}
|
|
88
69
|
|
|
89
70
|
function rtkBinaryName() {
|
|
90
71
|
const key = platformKey();
|
|
@@ -117,98 +98,22 @@ function gmToolsDir() {
|
|
|
117
98
|
// through node. Self-update inside the Rust binary keeps gm-tools fresh from
|
|
118
99
|
// here on. Skipped silently on any error — the next session-start hook will
|
|
119
100
|
// retry via ensure_tools_current.
|
|
120
|
-
function killHoldersOfPath(targetPath) {
|
|
121
|
-
if (process.platform !== 'win32') return 0;
|
|
122
|
-
try {
|
|
123
|
-
const { spawnSync } = require('child_process');
|
|
124
|
-
const norm = path.resolve(targetPath).replace(/\//g, '\\');
|
|
125
|
-
const r = spawnSync('wmic', ['process', 'where', `ExecutablePath='${norm.replace(/\\/g, '\\\\')}'`, 'get', 'ProcessId', '/format:value'], { encoding: 'utf8', windowsHide: true, timeout: 5000 });
|
|
126
|
-
if (r.status !== 0 || !r.stdout) return 0;
|
|
127
|
-
const pids = [];
|
|
128
|
-
for (const line of r.stdout.split(/\r?\n/)) {
|
|
129
|
-
const m = line.match(/ProcessId=(\d+)/);
|
|
130
|
-
if (m) {
|
|
131
|
-
const pid = parseInt(m[1], 10);
|
|
132
|
-
if (Number.isFinite(pid) && pid !== process.pid) pids.push(pid);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
for (const pid of pids) {
|
|
136
|
-
try { spawnSync('taskkill', ['/F', '/PID', String(pid)], { windowsHide: true, timeout: 3000 }); } catch (_) {}
|
|
137
|
-
}
|
|
138
|
-
return pids.length;
|
|
139
|
-
} catch (_) { return 0; }
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function cleanOrphanNewFiles(dst, exeName) {
|
|
143
|
-
try {
|
|
144
|
-
for (const name of fs.readdirSync(dst)) {
|
|
145
|
-
if (name === exeName + '.new') continue;
|
|
146
|
-
if (/^plugkit(\.\d+\.\d+\.\d+)?\.new$/i.test(name)) {
|
|
147
|
-
try { fs.unlinkSync(path.join(dst, name)); } catch (_) {}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
} catch (_) {}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function renameWithRetry(src, dst, attempts) {
|
|
154
|
-
for (let i = 0; i < attempts; i++) {
|
|
155
|
-
try { fs.renameSync(src, dst); return true; }
|
|
156
|
-
catch (err) {
|
|
157
|
-
if (err.code !== 'EEXIST' && err.code !== 'EPERM' && err.code !== 'EBUSY' && err.code !== 'EACCES') throw err;
|
|
158
|
-
if (i === Math.floor(attempts / 2)) killHoldersOfPath(dst);
|
|
159
|
-
try { const { spawnSync } = require('child_process'); spawnSync(process.execPath, ['-e', 'setTimeout(()=>{}, 200)'], { timeout: 400, killSignal: 'SIGKILL', stdio: 'ignore', windowsHide: true }); } catch (_) {}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
return false;
|
|
163
|
-
}
|
|
164
101
|
|
|
165
|
-
function
|
|
102
|
+
function copyWasmToGmTools(wasmPath, wrapperDir, version) {
|
|
166
103
|
const dst = gmToolsDir();
|
|
167
104
|
fs.mkdirSync(dst, { recursive: true });
|
|
168
|
-
const
|
|
169
|
-
const target = path.join(dst, exeName);
|
|
170
|
-
const targetTmp = target + '.new';
|
|
171
|
-
cleanOrphanNewFiles(dst, exeName);
|
|
105
|
+
const target = path.join(dst, 'plugkit.wasm');
|
|
172
106
|
if (fs.existsSync(target)) {
|
|
173
|
-
let needsRefresh = true;
|
|
174
107
|
try {
|
|
175
108
|
const cur = sha256OfFileSync(target);
|
|
176
|
-
const src = sha256OfFileSync(
|
|
177
|
-
if (cur === src)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
try { fs.writeFileSync(path.join(dst, 'plugkit.version'), version); } catch (_) {}
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
try { killHoldersOfPath(target); } catch (_) {}
|
|
184
|
-
}
|
|
185
|
-
fs.copyFileSync(finalPath, targetTmp);
|
|
186
|
-
if (!renameWithRetry(targetTmp, target, 8)) {
|
|
187
|
-
try { killHoldersOfPath(target); } catch (_) {}
|
|
188
|
-
try { fs.unlinkSync(target); } catch (_) {}
|
|
189
|
-
try { fs.renameSync(targetTmp, target); }
|
|
190
|
-
catch (_) {
|
|
191
|
-
try { fs.unlinkSync(targetTmp); } catch (_) {}
|
|
192
|
-
obsEvent('bootstrap', 'gmtools.rename.failed', { target });
|
|
193
|
-
writeBootstrapError({
|
|
194
|
-
expected_version: version,
|
|
195
|
-
cached_version: null,
|
|
196
|
-
error_phase: 'gmtools-rename',
|
|
197
|
-
error_message: `cannot replace ${target} after kill+unlink retry; orphan removed`,
|
|
198
|
-
});
|
|
199
|
-
throw new Error(`gm-tools update blocked: cannot replace ${target} (held open by running plugkit and kill-retry exhausted)`);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
try {
|
|
203
|
-
for (const name of fs.readdirSync(dst)) {
|
|
204
|
-
if (/^plugkit(\.\d+\.\d+\.\d+)?\.new$/i.test(name)) {
|
|
205
|
-
try { fs.unlinkSync(path.join(dst, name)); } catch (_) {}
|
|
109
|
+
const src = sha256OfFileSync(wasmPath);
|
|
110
|
+
if (cur === src) {
|
|
111
|
+
try { fs.writeFileSync(path.join(dst, 'plugkit.version'), version); } catch (_) {}
|
|
112
|
+
return;
|
|
206
113
|
}
|
|
207
|
-
}
|
|
208
|
-
} catch (_) {}
|
|
209
|
-
if (process.platform !== 'win32') {
|
|
210
|
-
try { fs.chmodSync(target, 0o755); } catch (_) {}
|
|
114
|
+
} catch (_) {}
|
|
211
115
|
}
|
|
116
|
+
fs.copyFileSync(wasmPath, target);
|
|
212
117
|
fs.writeFileSync(path.join(dst, 'plugkit.version'), version);
|
|
213
118
|
try {
|
|
214
119
|
const srcSha = path.join(wrapperDir, 'plugkit.sha256');
|
|
@@ -319,88 +224,54 @@ function sha256OfFile(filePath) {
|
|
|
319
224
|
});
|
|
320
225
|
}
|
|
321
226
|
|
|
322
|
-
function
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
res.resume();
|
|
339
|
-
return resolve(fetchToFile(res.headers.location, destPath, expectedTotal));
|
|
340
|
-
}
|
|
341
|
-
if (res.statusCode === 416) {
|
|
342
|
-
res.resume();
|
|
343
|
-
try { fs.unlinkSync(destPath); } catch (_) {}
|
|
344
|
-
return reject(new Error('range-not-satisfiable: cleared partial, retry'));
|
|
345
|
-
}
|
|
346
|
-
if (!(res.statusCode === 200 || res.statusCode === 206)) {
|
|
347
|
-
res.resume();
|
|
348
|
-
return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
|
|
227
|
+
async function extractNpmPackageWasm(destPath) {
|
|
228
|
+
const tempDir = path.join(path.dirname(destPath), '.npm-extract-' + Date.now());
|
|
229
|
+
try {
|
|
230
|
+
ensureDir(tempDir);
|
|
231
|
+
const startMs = Date.now();
|
|
232
|
+
log(`extracting npm package ${NPM_PACKAGE}@latest to ${tempDir}`);
|
|
233
|
+
obsEvent('bootstrap', 'npm.extract.start', { package: NPM_PACKAGE, version: 'latest' });
|
|
234
|
+
|
|
235
|
+
const result = spawnSync(
|
|
236
|
+
process.platform === 'win32' ? 'bunx.cmd' : 'bunx',
|
|
237
|
+
['--bun', NPM_PACKAGE + '@latest', '--save-exact', '--prefix', tempDir],
|
|
238
|
+
{
|
|
239
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
240
|
+
timeout: ATTEMPT_TIMEOUT_MS,
|
|
241
|
+
encoding: 'utf8',
|
|
242
|
+
windowsHide: true,
|
|
349
243
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
res.on('data', c => {
|
|
369
|
-
bytes += c.length;
|
|
370
|
-
lastByte = Date.now();
|
|
371
|
-
if (Date.now() - lastStderr > 5000) {
|
|
372
|
-
const pct = expectedTotal ? ` ${Math.floor(bytes / expectedTotal * 100)}%` : '';
|
|
373
|
-
try { process.stderr.write(`[plugkit-bootstrap] downloading: ${(bytes / 1048576).toFixed(1)} MiB${pct}\n`); } catch (_) {}
|
|
374
|
-
lastStderr = Date.now();
|
|
375
|
-
}
|
|
376
|
-
});
|
|
377
|
-
res.pipe(out);
|
|
378
|
-
out.on('finish', () => {
|
|
379
|
-
clearInterval(stallTimer);
|
|
380
|
-
obsEvent('bootstrap', 'fetch.end', { url: safeUrl, bytes, dur_ms: Date.now() - fetchStart, ok: true });
|
|
381
|
-
out.close(() => resolve(bytes));
|
|
382
|
-
});
|
|
383
|
-
out.on('error', err => { clearInterval(stallTimer); obsEvent('bootstrap', 'fetch.end', { url: safeUrl, bytes, dur_ms: Date.now() - fetchStart, ok: false, err: String(err.message || err) }); reject(err); });
|
|
384
|
-
res.on('error', err => { clearInterval(stallTimer); reject(err); });
|
|
385
|
-
res.on('end', () => clearInterval(stallTimer));
|
|
386
|
-
});
|
|
387
|
-
req.on('timeout', () => { req.destroy(new Error(`timeout after ${ATTEMPT_TIMEOUT_MS}ms`)); });
|
|
388
|
-
req.on('error', reject);
|
|
389
|
-
req.end();
|
|
390
|
-
});
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
if (result.error) throw result.error;
|
|
247
|
+
if (result.status !== 0) {
|
|
248
|
+
throw new Error(`bunx extraction failed: ${result.stderr || result.stdout || 'unknown error'}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const nodeModulesPath = path.join(tempDir, 'node_modules', NPM_PACKAGE, 'plugkit.wasm');
|
|
252
|
+
if (!fs.existsSync(nodeModulesPath)) {
|
|
253
|
+
throw new Error(`plugkit.wasm not found in extracted npm package at ${nodeModulesPath}`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
fs.copyFileSync(nodeModulesPath, destPath);
|
|
257
|
+
log(`extracted ${nodeModulesPath} → ${destPath}`);
|
|
258
|
+
obsEvent('bootstrap', 'npm.extract.end', { dur_ms: Date.now() - startMs, ok: true });
|
|
259
|
+
} finally {
|
|
260
|
+
try { fs.rmSync(tempDir, { recursive: true, force: true, maxRetries: 1, retryDelay: 50 }); } catch (_) {}
|
|
261
|
+
}
|
|
391
262
|
}
|
|
392
263
|
|
|
393
|
-
async function
|
|
264
|
+
async function extractNpmPackageWithRetry(destPath) {
|
|
394
265
|
let lastErr;
|
|
395
266
|
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
396
267
|
try {
|
|
397
|
-
log(`
|
|
398
|
-
await
|
|
268
|
+
log(`npm extract attempt ${attempt}/${MAX_ATTEMPTS}: ${NPM_PACKAGE}@latest`);
|
|
269
|
+
await extractNpmPackageWasm(destPath);
|
|
399
270
|
return;
|
|
400
271
|
} catch (err) {
|
|
401
272
|
lastErr = err;
|
|
402
273
|
log(`attempt ${attempt} failed: ${err.message}`);
|
|
403
|
-
obsEvent('bootstrap', '
|
|
274
|
+
obsEvent('bootstrap', 'npm.extract.attempt_failed', { package: NPM_PACKAGE, attempt, max: MAX_ATTEMPTS, err: String(err.message || err) });
|
|
404
275
|
if (attempt < MAX_ATTEMPTS) {
|
|
405
276
|
const wait = BACKOFF_MS[attempt - 1] || 120000;
|
|
406
277
|
log(`backing off ${wait}ms`);
|
|
@@ -447,8 +318,8 @@ async function bootstrap(opts) {
|
|
|
447
318
|
const wrapperDir = opts.wrapperDir || __dirname;
|
|
448
319
|
const version = opts.version || readVersionFile(wrapperDir);
|
|
449
320
|
const shaManifest = readShaManifest(wrapperDir);
|
|
450
|
-
const
|
|
451
|
-
const
|
|
321
|
+
const wasmName = 'plugkit.wasm';
|
|
322
|
+
const wasmExpectedSha = shaManifest ? shaManifest[wasmName] : null;
|
|
452
323
|
|
|
453
324
|
let root = cacheRoot();
|
|
454
325
|
try { ensureDir(root); }
|
|
@@ -457,121 +328,116 @@ async function bootstrap(opts) {
|
|
|
457
328
|
const verDir = path.join(root, `v${version}`);
|
|
458
329
|
ensureDir(verDir);
|
|
459
330
|
|
|
460
|
-
const
|
|
461
|
-
const
|
|
462
|
-
const
|
|
331
|
+
const wasmFinalPath = path.join(verDir, wasmName);
|
|
332
|
+
const wasmOkSentinel = path.join(verDir, '.wasm-ok');
|
|
333
|
+
const wasmPartialPath = `${wasmFinalPath}.partial`;
|
|
463
334
|
|
|
464
|
-
if (fs.existsSync(
|
|
465
|
-
if (
|
|
466
|
-
const actualSha = sha256OfFileSync(
|
|
467
|
-
if (actualSha ===
|
|
468
|
-
obsEvent('bootstrap', 'decision.hit', { reason: 'sha-match', version, path:
|
|
469
|
-
|
|
335
|
+
if (fs.existsSync(wasmFinalPath) && fs.existsSync(wasmOkSentinel)) {
|
|
336
|
+
if (wasmExpectedSha) {
|
|
337
|
+
const actualSha = sha256OfFileSync(wasmFinalPath);
|
|
338
|
+
if (actualSha === wasmExpectedSha) {
|
|
339
|
+
obsEvent('bootstrap', 'decision.hit', { reason: 'sha-match', version, path: wasmFinalPath });
|
|
340
|
+
copyWasmToGmTools(wasmFinalPath, wrapperDir, version);
|
|
470
341
|
clearBootstrapError();
|
|
471
|
-
return
|
|
342
|
+
return wasmFinalPath;
|
|
472
343
|
}
|
|
473
|
-
log(`decision: fetch reason: cache-hit-sha-mismatch (dir=v${version} expected ${
|
|
344
|
+
log(`decision: fetch reason: cache-hit-sha-mismatch (dir=v${version} expected ${wasmExpectedSha.slice(0,12)}… got ${(actualSha||'').slice(0,12)}…)`);
|
|
474
345
|
writeBootstrapError({
|
|
475
346
|
expected_version: version,
|
|
476
347
|
cached_version: null,
|
|
477
348
|
error_phase: 'cache-hit-sha-mismatch',
|
|
478
|
-
error_message: `cached
|
|
349
|
+
error_message: `cached wasm at ${wasmFinalPath} sha=${actualSha} but manifest expects ${wasmExpectedSha}`,
|
|
479
350
|
});
|
|
480
|
-
try { fs.unlinkSync(
|
|
481
|
-
try { fs.unlinkSync(
|
|
351
|
+
try { fs.unlinkSync(wasmFinalPath); } catch (_) {}
|
|
352
|
+
try { fs.unlinkSync(wasmOkSentinel); } catch (_) {}
|
|
482
353
|
} else {
|
|
483
|
-
obsEvent('bootstrap', 'decision.hit', { reason: 'sentinel+no-sha-manifest', path:
|
|
484
|
-
|
|
354
|
+
obsEvent('bootstrap', 'decision.hit', { reason: 'sentinel+no-sha-manifest', path: wasmFinalPath });
|
|
355
|
+
copyWasmToGmTools(wasmFinalPath, wrapperDir, version);
|
|
485
356
|
clearBootstrapError();
|
|
486
|
-
return
|
|
357
|
+
return wasmFinalPath;
|
|
487
358
|
}
|
|
488
359
|
}
|
|
489
360
|
|
|
490
|
-
if (healIfShaMatches(
|
|
491
|
-
obsEvent('bootstrap', 'decision.heal', { reason: 'sha-match', path:
|
|
361
|
+
if (healIfShaMatches(wasmFinalPath, wasmExpectedSha, wasmOkSentinel, wasmPartialPath, 'wasm')) {
|
|
362
|
+
obsEvent('bootstrap', 'decision.heal', { reason: 'sha-match', path: wasmFinalPath });
|
|
492
363
|
spawnDetachedRtkFetch(wrapperDir);
|
|
493
|
-
|
|
364
|
+
copyWasmToGmTools(wasmFinalPath, wrapperDir, version);
|
|
494
365
|
clearBootstrapError();
|
|
495
|
-
return
|
|
366
|
+
return wasmFinalPath;
|
|
496
367
|
}
|
|
497
368
|
|
|
498
369
|
const lockPath = path.join(verDir, '.lock');
|
|
499
370
|
acquireLock(lockPath);
|
|
500
371
|
try {
|
|
501
|
-
if (fs.existsSync(
|
|
502
|
-
obsEvent('bootstrap', 'decision.hit', { reason: 'lock-race-resolved', path:
|
|
503
|
-
|
|
372
|
+
if (fs.existsSync(wasmFinalPath) && fs.existsSync(wasmOkSentinel)) {
|
|
373
|
+
obsEvent('bootstrap', 'decision.hit', { reason: 'lock-race-resolved', path: wasmFinalPath });
|
|
374
|
+
copyWasmToGmTools(wasmFinalPath, wrapperDir, version);
|
|
504
375
|
clearBootstrapError();
|
|
505
|
-
return
|
|
376
|
+
return wasmFinalPath;
|
|
506
377
|
}
|
|
507
|
-
if (healIfShaMatches(
|
|
508
|
-
obsEvent('bootstrap', 'decision.heal', { reason: 'sha-match-under-lock', path:
|
|
378
|
+
if (healIfShaMatches(wasmFinalPath, wasmExpectedSha, wasmOkSentinel, wasmPartialPath, 'wasm')) {
|
|
379
|
+
obsEvent('bootstrap', 'decision.heal', { reason: 'sha-match-under-lock', path: wasmFinalPath });
|
|
509
380
|
spawnDetachedRtkFetch(wrapperDir);
|
|
510
|
-
|
|
381
|
+
copyWasmToGmTools(wasmFinalPath, wrapperDir, version);
|
|
511
382
|
clearBootstrapError();
|
|
512
|
-
return
|
|
383
|
+
return wasmFinalPath;
|
|
513
384
|
}
|
|
514
385
|
|
|
515
|
-
if (fs.existsSync(
|
|
386
|
+
if (fs.existsSync(wasmPartialPath)) {
|
|
516
387
|
try {
|
|
517
|
-
const st = fs.statSync(
|
|
388
|
+
const st = fs.statSync(wasmPartialPath);
|
|
518
389
|
if (Date.now() - st.mtimeMs > LOCK_STALE_MS) {
|
|
519
|
-
fs.unlinkSync(
|
|
520
|
-
log(`cleared stale partial: ${
|
|
390
|
+
fs.unlinkSync(wasmPartialPath);
|
|
391
|
+
log(`cleared stale partial: ${wasmPartialPath}`);
|
|
521
392
|
}
|
|
522
393
|
} catch (_) {}
|
|
523
394
|
}
|
|
524
|
-
const url = `https://github.com/${RELEASE_REPO}/releases/download/v${version}/${binName}`;
|
|
525
395
|
try {
|
|
526
|
-
await
|
|
527
|
-
} catch (
|
|
396
|
+
await extractNpmPackageWithRetry(wasmPartialPath);
|
|
397
|
+
} catch (extractErr) {
|
|
528
398
|
writeBootstrapError({
|
|
529
399
|
expected_version: version,
|
|
530
400
|
cached_version: null,
|
|
531
|
-
error_phase: '
|
|
532
|
-
error_message:
|
|
401
|
+
error_phase: 'npm-extract',
|
|
402
|
+
error_message: extractErr && extractErr.message ? extractErr.message : String(extractErr),
|
|
533
403
|
});
|
|
534
|
-
throw
|
|
404
|
+
throw extractErr;
|
|
535
405
|
}
|
|
536
406
|
|
|
537
|
-
if (
|
|
538
|
-
const got = await sha256OfFile(
|
|
539
|
-
if (got !==
|
|
540
|
-
try { fs.unlinkSync(
|
|
407
|
+
if (wasmExpectedSha) {
|
|
408
|
+
const got = await sha256OfFile(wasmPartialPath);
|
|
409
|
+
if (got !== wasmExpectedSha) {
|
|
410
|
+
try { fs.unlinkSync(wasmPartialPath); } catch (_) {}
|
|
541
411
|
writeBootstrapError({
|
|
542
412
|
expected_version: version,
|
|
543
413
|
cached_version: null,
|
|
544
414
|
error_phase: 'sha256-mismatch',
|
|
545
|
-
error_message: `sha256 mismatch for ${
|
|
415
|
+
error_message: `sha256 mismatch for ${wasmName}: expected ${wasmExpectedSha}, got ${got}`,
|
|
546
416
|
});
|
|
547
|
-
throw new Error(`sha256 mismatch for ${
|
|
417
|
+
throw new Error(`sha256 mismatch for ${wasmName}: expected ${wasmExpectedSha}, got ${got}`);
|
|
548
418
|
}
|
|
549
419
|
log('sha256 verified');
|
|
550
420
|
} else {
|
|
551
421
|
log('no sha256 manifest — skipping verify');
|
|
552
422
|
}
|
|
553
423
|
|
|
554
|
-
try { fs.renameSync(
|
|
424
|
+
try { fs.renameSync(wasmPartialPath, wasmFinalPath); }
|
|
555
425
|
catch (err) {
|
|
556
426
|
if (err.code === 'EEXIST' || err.code === 'EPERM') {
|
|
557
|
-
try { fs.unlinkSync(
|
|
558
|
-
fs.renameSync(
|
|
427
|
+
try { fs.unlinkSync(wasmFinalPath); } catch (_) {}
|
|
428
|
+
fs.renameSync(wasmPartialPath, wasmFinalPath);
|
|
559
429
|
} else throw err;
|
|
560
430
|
}
|
|
561
431
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
fs.writeFileSync(okSentinel, new Date().toISOString());
|
|
567
|
-
log(`decision: fetch reason: install-complete (${finalPath})`);
|
|
568
|
-
obsEvent('bootstrap', 'install.done', { path: finalPath, version, kind: 'plugkit' });
|
|
569
|
-
proactiveKillForNewInstall(version, finalPath);
|
|
432
|
+
fs.writeFileSync(wasmOkSentinel, new Date().toISOString());
|
|
433
|
+
log(`decision: fetch reason: install-complete (${wasmFinalPath})`);
|
|
434
|
+
obsEvent('bootstrap', 'install.done', { path: wasmFinalPath, version, kind: 'wasm' });
|
|
570
435
|
pruneOldVersions(root, version, readRtkVersion(wrapperDir));
|
|
571
436
|
spawnDetachedRtkFetch(wrapperDir);
|
|
572
|
-
|
|
437
|
+
copyWasmToGmTools(wasmFinalPath, wrapperDir, version);
|
|
438
|
+
|
|
573
439
|
clearBootstrapError();
|
|
574
|
-
return
|
|
440
|
+
return wasmFinalPath;
|
|
575
441
|
} finally {
|
|
576
442
|
releaseLock(lockPath);
|
|
577
443
|
}
|
|
@@ -616,8 +482,33 @@ async function bootstrapRtk(plugkitVerDir, plugkitVersion, wrapperDir, silent, r
|
|
|
616
482
|
if (!silent) log(`rtk cache heal (sha match): ${rtkPath}`);
|
|
617
483
|
return rtkPath;
|
|
618
484
|
}
|
|
619
|
-
const
|
|
620
|
-
|
|
485
|
+
const RTKS_RELEASE_REPO = 'AnEntrypoint/plugkit-bin';
|
|
486
|
+
const url = `https://github.com/${RTKS_RELEASE_REPO}/releases/download/v${plugkitVersion}/${rtkName}`;
|
|
487
|
+
const startMs = Date.now();
|
|
488
|
+
let lastErr;
|
|
489
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
490
|
+
try {
|
|
491
|
+
log(`rtk download attempt ${attempt}/${MAX_ATTEMPTS}: ${url}`);
|
|
492
|
+
const result = spawnSync(
|
|
493
|
+
'curl',
|
|
494
|
+
['-fSL', '--max-time', String(Math.floor(ATTEMPT_TIMEOUT_MS / 1000)), '-o', tmp, url],
|
|
495
|
+
{ stdio: 'pipe', timeout: ATTEMPT_TIMEOUT_MS + 5000, windowsHide: true }
|
|
496
|
+
);
|
|
497
|
+
if (result.error) throw result.error;
|
|
498
|
+
if (result.status !== 0) throw new Error(`curl failed with status ${result.status}`);
|
|
499
|
+
break;
|
|
500
|
+
} catch (err) {
|
|
501
|
+
lastErr = err;
|
|
502
|
+
log(`rtk attempt ${attempt} failed: ${err.message}`);
|
|
503
|
+
obsEvent('bootstrap', 'rtk.download.attempt_failed', { attempt, max: MAX_ATTEMPTS, err: String(err.message || err) });
|
|
504
|
+
if (attempt < MAX_ATTEMPTS) {
|
|
505
|
+
const wait = BACKOFF_MS[attempt - 1] || 120000;
|
|
506
|
+
log(`backing off ${wait}ms`);
|
|
507
|
+
await new Promise(r => setTimeout(r, wait));
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (lastErr) throw lastErr;
|
|
621
512
|
if (expected) {
|
|
622
513
|
const got = await sha256OfFile(tmp);
|
|
623
514
|
if (got !== expected) {
|
|
@@ -635,7 +526,7 @@ async function bootstrapRtk(plugkitVerDir, plugkitVersion, wrapperDir, silent, r
|
|
|
635
526
|
if (os.platform() !== 'win32') { try { fs.chmodSync(rtkPath, 0o755); } catch (_) {} }
|
|
636
527
|
fs.writeFileSync(rtkOk, new Date().toISOString());
|
|
637
528
|
log(`installed ${rtkPath}`);
|
|
638
|
-
obsEvent('bootstrap', 'install.done', { path: rtkPath, plugkit_version: plugkitVersion, rtk_version: readRtkVersion(wrapperDir) || plugkitVersion, kind: 'rtk' });
|
|
529
|
+
obsEvent('bootstrap', 'install.done', { path: rtkPath, plugkit_version: plugkitVersion, rtk_version: readRtkVersion(wrapperDir) || plugkitVersion, kind: 'rtk', dur_ms: Date.now() - startMs });
|
|
639
530
|
return rtkPath;
|
|
640
531
|
}
|
|
641
532
|
|
|
@@ -655,7 +546,7 @@ function resolveCachedRtk(opts) {
|
|
|
655
546
|
return null;
|
|
656
547
|
}
|
|
657
548
|
|
|
658
|
-
function
|
|
549
|
+
function getWasmPath(opts) {
|
|
659
550
|
opts = opts || {};
|
|
660
551
|
const wrapperDir = opts.wrapperDir || __dirname;
|
|
661
552
|
const version = opts.version || readVersionFile(wrapperDir);
|
|
@@ -664,9 +555,9 @@ function resolveCachedBinary(opts) {
|
|
|
664
555
|
catch (_) { const r = fallbackCacheRoot(); ensureDir(r); return r; }
|
|
665
556
|
})();
|
|
666
557
|
const verDir = path.join(root, `v${version}`);
|
|
667
|
-
const
|
|
668
|
-
const okSentinel = path.join(verDir, '.ok');
|
|
669
|
-
if (fs.existsSync(
|
|
558
|
+
const wasmPath = path.join(verDir, 'plugkit.wasm');
|
|
559
|
+
const okSentinel = path.join(verDir, '.wasm-ok');
|
|
560
|
+
if (fs.existsSync(wasmPath) && fs.existsSync(okSentinel)) return wasmPath;
|
|
670
561
|
return null;
|
|
671
562
|
}
|
|
672
563
|
|
|
@@ -729,72 +620,6 @@ function killRunningDaemons(reason) {
|
|
|
729
620
|
return killedPids;
|
|
730
621
|
}
|
|
731
622
|
|
|
732
|
-
function listRunningPlugkitImagePaths() {
|
|
733
|
-
const out = [];
|
|
734
|
-
try {
|
|
735
|
-
const { spawnSync } = require('child_process');
|
|
736
|
-
if (os.platform() === 'win32') {
|
|
737
|
-
let parsed = null;
|
|
738
|
-
try {
|
|
739
|
-
const p = spawnSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', "Get-Process plugkit* -ErrorAction SilentlyContinue | Select-Object Id,Path | ConvertTo-Json -Compress"], { windowsHide: true, encoding: 'utf8', timeout: 5000, killSignal: 'SIGKILL', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
740
|
-
const text = ((p && p.stdout) || '').trim();
|
|
741
|
-
if (text) {
|
|
742
|
-
const j = JSON.parse(text);
|
|
743
|
-
parsed = Array.isArray(j) ? j : [j];
|
|
744
|
-
}
|
|
745
|
-
} catch (_) {}
|
|
746
|
-
if (parsed) {
|
|
747
|
-
for (const item of parsed) {
|
|
748
|
-
if (!item) continue;
|
|
749
|
-
const pid = parseInt(item.Id, 10);
|
|
750
|
-
if (!Number.isFinite(pid)) continue;
|
|
751
|
-
out.push({ pid, path: (item.Path || '').trim() });
|
|
752
|
-
}
|
|
753
|
-
} else {
|
|
754
|
-
const r = spawnSync('tasklist', ['/FI', 'IMAGENAME eq plugkit*', '/FO', 'CSV', '/NH'], { windowsHide: true, encoding: 'utf8', timeout: 5000, killSignal: 'SIGKILL', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
755
|
-
const text = (r && r.stdout) || '';
|
|
756
|
-
const seen = new Set();
|
|
757
|
-
for (const line of text.split(/\r?\n/)) {
|
|
758
|
-
const m = line.match(/^"([^"]+)","(\d+)"/);
|
|
759
|
-
if (!m) continue;
|
|
760
|
-
const pid = parseInt(m[2], 10);
|
|
761
|
-
if (!Number.isFinite(pid) || seen.has(pid)) continue;
|
|
762
|
-
seen.add(pid);
|
|
763
|
-
out.push({ pid, path: '' });
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
} else if (os.platform() === 'linux') {
|
|
767
|
-
let entries = [];
|
|
768
|
-
try { entries = fs.readdirSync('/proc'); } catch (_) {}
|
|
769
|
-
for (const e of entries) {
|
|
770
|
-
if (!/^\d+$/.test(e)) continue;
|
|
771
|
-
const pid = parseInt(e, 10);
|
|
772
|
-
let comm = '';
|
|
773
|
-
try { comm = fs.readFileSync(`/proc/${pid}/comm`, 'utf8').trim(); } catch (_) { continue; }
|
|
774
|
-
if (!/^plugkit/i.test(comm)) continue;
|
|
775
|
-
let imagePath = '';
|
|
776
|
-
try { imagePath = fs.readlinkSync(`/proc/${pid}/exe`); } catch (_) {}
|
|
777
|
-
out.push({ pid, path: imagePath });
|
|
778
|
-
}
|
|
779
|
-
} else {
|
|
780
|
-
const r = spawnSync('ps', ['-axo', 'pid=,comm='], { encoding: 'utf8', timeout: 5000, killSignal: 'SIGKILL', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
781
|
-
const text = (r && r.stdout) || '';
|
|
782
|
-
for (const line of text.split(/\r?\n/)) {
|
|
783
|
-
const m = line.match(/^\s*(\d+)\s+(.+?)\s*$/);
|
|
784
|
-
if (!m) continue;
|
|
785
|
-
if (!/plugkit/i.test(m[2])) continue;
|
|
786
|
-
const pid = parseInt(m[1], 10);
|
|
787
|
-
let imagePath = '';
|
|
788
|
-
try {
|
|
789
|
-
const p = spawnSync('ps', ['-p', String(pid), '-o', 'command='], { encoding: 'utf8', timeout: 3000, killSignal: 'SIGKILL', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
790
|
-
imagePath = ((p && p.stdout) || '').trim().split(/\s+/)[0] || '';
|
|
791
|
-
} catch (_) {}
|
|
792
|
-
out.push({ pid, path: imagePath });
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
} catch (_) {}
|
|
796
|
-
return out;
|
|
797
|
-
}
|
|
798
623
|
|
|
799
624
|
function killSpoolWatcherInCwd(reason) {
|
|
800
625
|
try {
|
|
@@ -811,39 +636,21 @@ function killSpoolWatcherInCwd(reason) {
|
|
|
811
636
|
return null;
|
|
812
637
|
}
|
|
813
638
|
|
|
814
|
-
function proactiveKillForNewInstall(installedVersion
|
|
639
|
+
function proactiveKillForNewInstall(installedVersion) {
|
|
815
640
|
try {
|
|
816
641
|
const reason = `install:v${installedVersion}`;
|
|
817
|
-
const target = finalPath ? path.resolve(finalPath).toLowerCase() : null;
|
|
818
|
-
const cacheRootNorm = (() => {
|
|
819
|
-
try { return path.resolve(cacheRoot()).toLowerCase(); } catch (_) { return null; }
|
|
820
|
-
})();
|
|
821
|
-
const procs = listRunningPlugkitImagePaths();
|
|
822
|
-
for (const { pid, path: imagePath } of procs) {
|
|
823
|
-
if (!Number.isFinite(pid) || pid === process.pid) continue;
|
|
824
|
-
if (!imagePath) continue;
|
|
825
|
-
const norm = path.resolve(imagePath).toLowerCase();
|
|
826
|
-
if (target && norm === target) continue;
|
|
827
|
-
if (!cacheRootNorm || !norm.startsWith(cacheRootNorm + path.sep.toLowerCase())) continue;
|
|
828
|
-
if (killPid(pid)) {
|
|
829
|
-
try { process.stderr.write(`[bootstrap] killed stale daemon pid=${pid} path=${imagePath} (current install: v${installedVersion})\n`); } catch (_) {}
|
|
830
|
-
obsEvent('bootstrap', 'daemon.killed', { pid, oldPath: imagePath, installedVersion, mechanism: 'process-path' });
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
642
|
killRunningDaemons(reason);
|
|
834
643
|
killSpoolWatcherInCwd(reason);
|
|
835
644
|
writeDaemonVersion(installedVersion);
|
|
836
645
|
} catch (_) {}
|
|
837
646
|
}
|
|
838
647
|
|
|
839
|
-
// Compare wrapper-pinned version against last-recorded daemon version. If
|
|
840
|
-
// they differ, kill the daemon so it respawns under the new binary.
|
|
841
648
|
function killStaleDaemonIfVersionChanged(wrapperDir) {
|
|
842
649
|
let currentVersion;
|
|
843
650
|
try { currentVersion = readVersionFile(wrapperDir); } catch (_) { return; }
|
|
844
|
-
const cached =
|
|
651
|
+
const cached = getWasmPath({ wrapperDir, version: currentVersion });
|
|
845
652
|
if (cached) {
|
|
846
|
-
proactiveKillForNewInstall(currentVersion
|
|
653
|
+
proactiveKillForNewInstall(currentVersion);
|
|
847
654
|
return;
|
|
848
655
|
}
|
|
849
656
|
const recorded = readDaemonVersion();
|
|
@@ -852,7 +659,7 @@ function killStaleDaemonIfVersionChanged(wrapperDir) {
|
|
|
852
659
|
writeDaemonVersion(currentVersion);
|
|
853
660
|
}
|
|
854
661
|
|
|
855
|
-
module.exports = { bootstrap,
|
|
662
|
+
module.exports = { bootstrap, getWasmPath, resolveCachedRtk, rtkBinaryName, cacheRoot, obsEvent, killRunningDaemons, killStaleDaemonIfVersionChanged, killSpoolWatcherInCwd, proactiveKillForNewInstall };
|
|
856
663
|
|
|
857
664
|
if (require.main === module) {
|
|
858
665
|
const argv = process.argv.slice(2);
|
package/bin/plugkit.js
CHANGED
|
@@ -81,13 +81,40 @@ function ensureReady(silent) {
|
|
|
81
81
|
return r.status === 0 && isReady();
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
function wasmPath() {
|
|
85
|
+
return path.join(wrapperDir, 'plugkit.wasm');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function shouldUseWasm() {
|
|
89
|
+
if (process.env.GM_USE_WASM === '1') return true;
|
|
90
|
+
if (fs.existsSync(wasmPath()) && !fs.existsSync(toolsBin())) return true;
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function runWasm(args) {
|
|
95
|
+
try {
|
|
96
|
+
const WasmHost = require('../lib/wasm-host');
|
|
97
|
+
const host = new WasmHost(wasmPath());
|
|
98
|
+
const verb = args[0] || 'health';
|
|
99
|
+
const body = args.slice(1).join(' ') || '{}';
|
|
100
|
+
const result = await host.dispatch(verb, body);
|
|
101
|
+
console.log(JSON.stringify(result, null, 2));
|
|
102
|
+
process.exit(result.ok ? 0 : 1);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
console.error('[plugkit wasm]', err.message);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
84
109
|
function main() {
|
|
85
110
|
const args = process.argv.slice(2);
|
|
86
111
|
const isHook = args[0] === 'hook';
|
|
87
112
|
const hookSubcmd = isHook ? (args[1] || '') : '';
|
|
88
113
|
|
|
89
|
-
|
|
90
|
-
|
|
114
|
+
if (shouldUseWasm()) {
|
|
115
|
+
return runWasm(args);
|
|
116
|
+
}
|
|
117
|
+
|
|
91
118
|
const blocksUntilReady = hookSubcmd === 'session-start' || hookSubcmd === 'prompt-submit';
|
|
92
119
|
|
|
93
120
|
if (blocksUntilReady) {
|
|
@@ -96,8 +123,6 @@ function main() {
|
|
|
96
123
|
process.exit(1);
|
|
97
124
|
}
|
|
98
125
|
} else if (!fs.existsSync(toolsBin())) {
|
|
99
|
-
// For non-blocking hooks (pre-tool-use, post-tool-use, stop, etc.): if the
|
|
100
|
-
// binary doesn't exist yet, exit cleanly — session-start will populate it.
|
|
101
126
|
if (isHook) process.exit(0);
|
|
102
127
|
process.exit(1);
|
|
103
128
|
}
|
package/gm.json
CHANGED
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -5,7 +5,7 @@ description: Mandatory codebase search workflow. Use whenever you need to find a
|
|
|
5
5
|
|
|
6
6
|
# Codebase search
|
|
7
7
|
|
|
8
|
-
`exec:codesearch` is the only codebase search tool. Grep, Glob, Find, Explore, raw `grep`/`rg`/`find` inside `exec:bash
|
|
8
|
+
`exec:codesearch` is the only codebase search tool. Never use Grep, Glob, Find, Explore, raw `grep`/`rg`/`find` inside `exec:bash`. No fallback.
|
|
9
9
|
|
|
10
10
|
A `@<discipline>` first-token after the verb scopes the search to that discipline's index; absent the sigil, results fan across default plus enabled disciplines, prefixed by source.
|
|
11
11
|
|
|
@@ -26,7 +26,7 @@ Failure triage: broken output to EMIT, wrong logic to EXECUTE, new unknown to PL
|
|
|
26
26
|
- `git_clean` — `git status --porcelain` returns empty
|
|
27
27
|
- `git_pushed` — `git log origin/main..HEAD --oneline` returns empty
|
|
28
28
|
- `ci_passed` — every GitHub Actions run reaches `conclusion: success`
|
|
29
|
-
- `mutables_resolved` — `.gm/mutables.yml` deleted OR every entry `status: witnessed`.
|
|
29
|
+
- `mutables_resolved` — `.gm/mutables.yml` deleted OR every entry `status: witnessed`. Never stop the turn while any entry is `status: unknown` — this gate is self-enforced.
|
|
30
30
|
- `prd_empty` — `.gm/prd.yml` deleted AFTER residual scan: enumerate every in-spirit reachable residual surfaced this session; any hit re-enters `planning`, appends PRD items, executes. Empty PRD is necessary, not sufficient — done = empty PRD AND zero reachable in-spirit residuals. Out-of-spirit-or-unreachable residuals are named in the response and skipped; everything else is this turn's work.
|
|
31
31
|
- `stress_suite_clear` — change walked through M1–D1 (governance), none flunked
|
|
32
32
|
- `hidden_decision_posture` — open → down_weighted → closed only when CI is green AND stress suite is clear
|
|
@@ -81,7 +81,7 @@ Both must return empty. Local commit without push is not complete.
|
|
|
81
81
|
|
|
82
82
|
## CI is automated
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
After `git push`, poll `gh run list --branch main --limit 5 --json status,name,databaseId` until all runs reach a terminal state. Green → continue; failure → investigate via `gh run view <id> --log-failed`, fix, push again. Deadline 180s (override `GM_CI_WATCH_SECS`). Poll every 10s via a nodejs spool file with a `setInterval` loop writing results to stdout.
|
|
85
85
|
|
|
86
86
|
## Hygiene sweep
|
|
87
87
|
|
package/skills/gm-emit/SKILL.md
CHANGED
|
@@ -49,7 +49,7 @@ Re-import from disk — in-memory state is stale and inadmissible. Run identical
|
|
|
49
49
|
|
|
50
50
|
## Mutables gate
|
|
51
51
|
|
|
52
|
-
Before pre-emit run, read `.gm/mutables.yml`. Any entry with `status: unknown` → regress to `gm-execute`.
|
|
52
|
+
Before pre-emit run, read `.gm/mutables.yml`. Any entry with `status: unknown` → regress to `gm-execute`. Never use Write/Edit/NotebookEdit while unresolved entries exist — this gate is self-enforced. Zero unresolved is the precondition for every legitimacy question below.
|
|
53
53
|
|
|
54
54
|
## Gate (all true at once)
|
|
55
55
|
|
|
@@ -30,7 +30,7 @@ Resolves to KNOWN only when all four pass:
|
|
|
30
30
|
|
|
31
31
|
Unresolved after 2 passes regresses to `planning`. Never narrate past an unresolved mutable.
|
|
32
32
|
|
|
33
|
-
Every witness that resolves a mutable writes back to `.gm/mutables.yml` the same step: set `status: witnessed` and fill `witness_evidence` with concrete proof (file:line, codesearch hit, exec output snippet). No write-back = the mutable stays unknown and the EMIT-gate stays closed. The
|
|
33
|
+
Every witness that resolves a mutable writes back to `.gm/mutables.yml` the same step: set `status: witnessed` and fill `witness_evidence` with concrete proof (file:line, codesearch hit, exec output snippet). No write-back = the mutable stays unknown and the EMIT-gate stays closed. The file is the record; the agent's memory of "I resolved it" does not count.
|
|
34
34
|
|
|
35
35
|
Route candidates from PLAN are `weak_prior` only. Plausibility is the right to test, not the right to believe. A claim with no witness in the current session is a hypothesis — say so when stating it, and say what would settle it. The next reader (you, next turn) needs to know which lines were earned and which were carried forward.
|
|
36
36
|
|
|
@@ -44,13 +44,13 @@ Code AND utility verbs both run through the file-spool. Write a file to `.gm/exe
|
|
|
44
44
|
|
|
45
45
|
Pack runs: `Promise.allSettled`, each idea own try/catch, under 12s per call. Runner: write `in/runner/<N>.txt` with body `start` | `stop` | `status`.
|
|
46
46
|
|
|
47
|
-
Every exec daemonizes. The
|
|
47
|
+
Every exec daemonizes. The spool watcher tails the task logfile up to 30s wall-clock and returns whatever is there — short tasks complete inside the window and look synchronous; long tasks return a task_id with partial output. Continue with `exec:tail` (drain, bounded), `exec:watch` (resume blocking until match or timeout), or `exec:close` (terminate). Never re-spawn a long task to check on it — that orphans the first one. `exec:wait` is a pure timer; `exec:sleep` blocks on a specific task's output; `exec:watch` is the match-or-timeout primitive. Every execution-platform RPC returns the live list of running tasks for this session — close stragglers via `exec:close\n<id>` so the list stays scannable. Session-end (clear/logout/prompt_input_exit) kills the session's tasks; compaction/handoff preserves them.
|
|
48
48
|
|
|
49
|
-
Every utility verb dispatches via `in/<verb>/<N>.txt`; the body of the file is the verb's argument. There is no inline form and no Bash-prefix form —
|
|
49
|
+
Every utility verb dispatches via `in/<verb>/<N>.txt`; the body of the file is the verb's argument. There is no inline form and no Bash-prefix form — use only the spool path.
|
|
50
50
|
|
|
51
51
|
## Codebase search
|
|
52
52
|
|
|
53
|
-
Codesearch only. Grep, Glob, Find, Explore, raw grep/rg/find inside Bash
|
|
53
|
+
Codesearch only. Never use Grep, Glob, Find, Explore, raw grep/rg/find inside Bash. Write query to `.gm/exec-spool/in/codesearch/<N>.txt`. Read result from `.gm/exec-spool/out/<N>.out`.
|
|
54
54
|
|
|
55
55
|
Start two words, change/add one per pass, minimum four attempts before concluding absent. Known absolute path → `Read`. Known directory → nodejs spool file + `fs.readdirSync`.
|
|
56
56
|
|
|
@@ -80,4 +80,4 @@ Up to 3 `gm:gm` subagents for independent items in one message. Browser escalati
|
|
|
80
80
|
|
|
81
81
|
## CI is automated
|
|
82
82
|
|
|
83
|
-
`git push
|
|
83
|
+
After `git push`, poll `gh run list --branch main --limit 5 --json status,name,databaseId` until all runs reach a terminal state. Green → continue; failure → investigate via `gh run view <id> --log-failed`, fix, push again. Deadline 180s (override `GM_CI_WATCH_SECS`). Poll every 10s via a nodejs spool file with a `setInterval` loop writing results to stdout.
|
package/skills/planning/SKILL.md
CHANGED
|
@@ -77,7 +77,7 @@ Plan exits when zero new unknowns surfaced last pass AND every item has acceptan
|
|
|
77
77
|
|
|
78
78
|
## .gm/mutables.yml — co-equal with .gm/prd.yml
|
|
79
79
|
|
|
80
|
-
Every unknown surfaced during PLAN lands as an entry in `.gm/mutables.yml` the same pass. Live during work, deleted when empty.
|
|
80
|
+
Every unknown surfaced during PLAN lands as an entry in `.gm/mutables.yml` the same pass. Live during work, deleted when empty. Self-enforced: never use Write/Edit/NotebookEdit, never run `git commit`/`git push`, never stop the turn while any entry has `status: unknown`. This discipline is owned by the agent — not by external infrastructure.
|
|
81
81
|
|
|
82
82
|
```yaml
|
|
83
83
|
- id: kebab-id
|
|
@@ -143,7 +143,7 @@ The 200 lines are a *budget* for maximum surface coverage, not a target. Subsyst
|
|
|
143
143
|
|
|
144
144
|
Code execution AND utility verbs both write to `.gm/exec-spool/in/<lang-or-verb>/<N>.<ext>`. Languages live under `in/<lang>/` (nodejs, python, bash, typescript, go, rust, c, cpp, java, deno); verbs live under `in/<verb>/` (codesearch, recall, memorize, wait, sleep, status, close, browser, runner, type, kill-port, forget, feedback, learn-status, learn-debug, learn-build, discipline, pause, health). The spool watcher runs the file and streams to `out/<N>.out` (stdout) + `out/<N>.err` (stderr) line-by-line, then writes `out/<N>.json` metadata (exitCode, durationMs, timedOut, startedAt, endedAt) at completion. Both streams return as systemMessage with `--- stdout ---` / `--- stderr ---` separators. `in/` and `out/` are wiped at session start and at real-exit session end. Only `git` (and `gh`) run directly via Bash; never `Bash(node/npm/npx/bun)`, never `Bash(exec:<anything>)`. Spool paths in nodejs files are platform-literal — use `os.tmpdir()` and `path.join`. The spool enforces per-task timeouts; on timeout, partial output is preserved and the watcher emits `[exec timed out after Nms; partial output above]`.
|
|
145
145
|
|
|
146
|
-
Codesearch only — Grep/Glob/Find/Explore
|
|
146
|
+
Codesearch only — never use Grep/Glob/Find/Explore. Write to `.gm/exec-spool/in/codesearch/<N>.txt`. Start two words, change/add one per pass, minimum four attempts before concluding absent.
|
|
147
147
|
|
|
148
148
|
Pack runs use `Promise.allSettled`, each idea its own try/catch, under 12s per call.
|
|
149
149
|
|