gm-skill 2.0.1116 → 2.0.1118
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/plugkit-wasm-wrapper.js +16 -7
- package/gm.json +1 -1
- package/lib/skill-bootstrap.js +210 -6
- package/package.json +2 -2
- package/skills/gm-skill/SKILL.md +62 -10
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ npx gm-skill-bootstrap
|
|
|
28
28
|
|
|
29
29
|
## Version
|
|
30
30
|
|
|
31
|
-
`2.0.
|
|
31
|
+
`2.0.1118` — auto-bumped from the canonical `gm` repo. Every push to `AnEntrypoint/gm` republishes this package alongside all 15 platform packages.
|
|
32
32
|
|
|
33
33
|
## Source of truth
|
|
34
34
|
|
|
@@ -660,10 +660,14 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
660
660
|
const dir = path.dirname(relPath);
|
|
661
661
|
const verb = dir === '.' ? path.basename(filePath, path.extname(filePath)) : dir;
|
|
662
662
|
const body = content.trim() || '{}';
|
|
663
|
+
const taskBase = path.basename(filePath, path.extname(filePath));
|
|
663
664
|
|
|
664
665
|
const verbBytes = new TextEncoder().encode(verb);
|
|
665
666
|
const bodyBytes = new TextEncoder().encode(body);
|
|
666
667
|
|
|
668
|
+
const t0 = Date.now();
|
|
669
|
+
console.log(`[dispatch] → verb=${verb} task=${taskBase} body=${bodyBytes.length}b`);
|
|
670
|
+
|
|
667
671
|
const verbPtr = instance.exports.plugkit_alloc(verbBytes.length);
|
|
668
672
|
const bodyPtr = instance.exports.plugkit_alloc(bodyBytes.length);
|
|
669
673
|
new Uint8Array(instance.exports.memory.buffer, verbPtr, verbBytes.length).set(verbBytes);
|
|
@@ -676,9 +680,9 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
676
680
|
const resultBytes = new Uint8Array(instance.exports.memory.buffer, ptr, len);
|
|
677
681
|
const resultStr = new TextDecoder().decode(resultBytes);
|
|
678
682
|
|
|
679
|
-
const taskBase = path.basename(filePath, path.extname(filePath));
|
|
680
683
|
const outName = dir === '.' ? `${taskBase}.json` : `${verb}-${taskBase}.json`;
|
|
681
684
|
fs.writeFileSync(path.join(outDir, outName), resultStr);
|
|
685
|
+
console.log(`[dispatch] ← verb=${verb} task=${taskBase} ms=${Date.now() - t0} out=${resultStr.length}b`);
|
|
682
686
|
|
|
683
687
|
try { instance.exports.plugkit_free(verbPtr, verbBytes.length); } catch (_) {}
|
|
684
688
|
try { instance.exports.plugkit_free(bodyPtr, bodyBytes.length); } catch (_) {}
|
|
@@ -742,19 +746,22 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
742
746
|
setInterval(() => {
|
|
743
747
|
try {
|
|
744
748
|
const cutoff = Date.now() - 3600_000;
|
|
749
|
+
let swept = 0;
|
|
745
750
|
for (const entry of fs.readdirSync(outDir)) {
|
|
746
751
|
try {
|
|
747
752
|
const fp = path.join(outDir, entry);
|
|
748
753
|
const s = fs.statSync(fp);
|
|
749
|
-
if (s.mtimeMs < cutoff) fs.unlinkSync(fp);
|
|
750
|
-
} catch (
|
|
754
|
+
if (s.mtimeMs < cutoff) { fs.unlinkSync(fp); swept++; }
|
|
755
|
+
} catch (e) { console.error(`[retention] failed to sweep ${entry}: ${e.message}`); }
|
|
751
756
|
}
|
|
752
|
-
|
|
757
|
+
if (swept > 0) console.log(`[retention] swept ${swept} out/ files older than 1h`);
|
|
758
|
+
} catch (e) { console.error(`[retention] sweep error: ${e.message}`); }
|
|
753
759
|
}, 60_000);
|
|
754
760
|
|
|
755
761
|
setInterval(() => {
|
|
756
762
|
try {
|
|
757
763
|
const cutoff = Date.now() - 600_000;
|
|
764
|
+
let stale = 0;
|
|
758
765
|
const walk = (dir) => {
|
|
759
766
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
760
767
|
const fp = path.join(dir, entry.name);
|
|
@@ -768,14 +775,16 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
768
775
|
const outName = verbDir === '.' ? `${base}.json` : `${verbDir}-${base}.json`;
|
|
769
776
|
try {
|
|
770
777
|
fs.writeFileSync(path.join(outDir, outName), JSON.stringify({ ok: false, error: 'stale input — never dispatched or watcher crash mid-flight' }));
|
|
771
|
-
} catch (
|
|
772
|
-
try { fs.unlinkSync(fp); } catch (
|
|
778
|
+
} catch (e) { console.error(`[stale-sweep] failed to write error for ${rel}: ${e.message}`); }
|
|
779
|
+
try { fs.unlinkSync(fp); stale++; } catch (e) { console.error(`[stale-sweep] failed to unlink ${rel}: ${e.message}`); }
|
|
780
|
+
console.error(`[stale-sweep] auto-failed ${rel} (age >${600}s)`);
|
|
773
781
|
}
|
|
774
782
|
}
|
|
775
783
|
}
|
|
776
784
|
};
|
|
777
785
|
walk(inDir);
|
|
778
|
-
|
|
786
|
+
if (stale > 0) console.log(`[stale-sweep] failed ${stale} orphaned inputs`);
|
|
787
|
+
} catch (e) { console.error(`[stale-sweep] sweep error: ${e.message}`); }
|
|
779
788
|
}, 300_000);
|
|
780
789
|
|
|
781
790
|
const existing = walkDir(inDir);
|
package/gm.json
CHANGED
package/lib/skill-bootstrap.js
CHANGED
|
@@ -10,6 +10,9 @@ const PLUGKIT_TOOLS_DIR = path.join(os.homedir(), '.claude', 'gm-tools');
|
|
|
10
10
|
const PLUGKIT_VERSION_FILE = path.join(PLUGKIT_TOOLS_DIR, 'plugkit.version');
|
|
11
11
|
const PLUGKIT_WASM_PATH = path.join(PLUGKIT_TOOLS_DIR, 'plugkit.wasm');
|
|
12
12
|
const PLUGKIT_WASM_WRAPPER = path.join(PLUGKIT_TOOLS_DIR, 'plugkit-wasm-wrapper.js');
|
|
13
|
+
const PLUGKIT_LATEST_CACHE = path.join(PLUGKIT_TOOLS_DIR, 'plugkit-latest.json');
|
|
14
|
+
const PLUGKIT_LATEST_TTL_MS = 60 * 60 * 1000;
|
|
15
|
+
const NPM_PACKAGE = '@anentrypoint/plugkit-wasm';
|
|
13
16
|
const BOOTSTRAP_STATUS_FILE = path.join(os.homedir(), '.gm', 'bootstrap-status.json');
|
|
14
17
|
const BOOTSTRAP_ERROR_FILE = path.join(os.homedir(), '.gm', 'bootstrap-error.json');
|
|
15
18
|
const LOG_DIR = path.join(os.homedir(), '.claude', 'gm-log');
|
|
@@ -100,6 +103,172 @@ function computeFileHash(filePath) {
|
|
|
100
103
|
return crypto.createHash('sha256').update(content).digest('hex');
|
|
101
104
|
}
|
|
102
105
|
|
|
106
|
+
function httpGet(url, timeoutMs) {
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
const req = https.get(url, { timeout: timeoutMs, headers: { 'accept': 'application/json', 'user-agent': 'gm-skill-bootstrap' } }, (res) => {
|
|
109
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
110
|
+
res.resume();
|
|
111
|
+
httpGet(res.headers.location, timeoutMs).then(resolve, reject);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (res.statusCode !== 200) {
|
|
115
|
+
res.resume();
|
|
116
|
+
reject(new Error(`HTTP ${res.statusCode} ${url}`));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const chunks = [];
|
|
120
|
+
res.on('data', (c) => chunks.push(c));
|
|
121
|
+
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
122
|
+
res.on('error', reject);
|
|
123
|
+
});
|
|
124
|
+
req.on('timeout', () => { req.destroy(new Error(`timeout ${timeoutMs}ms ${url}`)); });
|
|
125
|
+
req.on('error', reject);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function getLatestRemoteVersion() {
|
|
130
|
+
try {
|
|
131
|
+
const stat = fs.statSync(PLUGKIT_LATEST_CACHE);
|
|
132
|
+
if (Date.now() - stat.mtimeMs < PLUGKIT_LATEST_TTL_MS) {
|
|
133
|
+
const cached = JSON.parse(fs.readFileSync(PLUGKIT_LATEST_CACHE, 'utf-8'));
|
|
134
|
+
if (cached && cached.version) {
|
|
135
|
+
emitBootstrapEvent('info', 'Using cached latest version', { version: cached.version, ageMs: Date.now() - stat.mtimeMs });
|
|
136
|
+
return cached;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch (_) {}
|
|
140
|
+
let version = null;
|
|
141
|
+
let source = null;
|
|
142
|
+
try {
|
|
143
|
+
const buf = await httpGet('https://api.github.com/repos/AnEntrypoint/plugkit-bin/releases/latest', 3000);
|
|
144
|
+
const rel = JSON.parse(buf.toString('utf-8'));
|
|
145
|
+
const tag = rel && rel.tag_name;
|
|
146
|
+
if (tag) {
|
|
147
|
+
version = tag.replace(/^v/, '');
|
|
148
|
+
source = 'github-releases';
|
|
149
|
+
}
|
|
150
|
+
} catch (e) {
|
|
151
|
+
emitBootstrapEvent('warn', 'GitHub Releases lookup failed', { error: e.message });
|
|
152
|
+
}
|
|
153
|
+
if (!version) {
|
|
154
|
+
try {
|
|
155
|
+
const buf = await httpGet('https://registry.npmjs.org/gm-plugkit/latest', 3000);
|
|
156
|
+
const pkg = JSON.parse(buf.toString('utf-8'));
|
|
157
|
+
if (pkg && pkg.plugkitVersion) {
|
|
158
|
+
version = pkg.plugkitVersion;
|
|
159
|
+
source = 'npm-gm-plugkit';
|
|
160
|
+
} else if (pkg && pkg.version) {
|
|
161
|
+
version = pkg.version;
|
|
162
|
+
source = 'npm-gm-plugkit-fallback';
|
|
163
|
+
}
|
|
164
|
+
} catch (e) {
|
|
165
|
+
emitBootstrapEvent('warn', 'npm fallback lookup failed', { error: e.message });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (!version) {
|
|
169
|
+
emitBootstrapEvent('warn', 'All latest-version lookups failed; falling back to manifest');
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
let sha = '';
|
|
173
|
+
try {
|
|
174
|
+
const shaBuf = await httpGet(`https://github.com/AnEntrypoint/plugkit-bin/releases/download/v${version}/plugkit.wasm.sha256`, 3000);
|
|
175
|
+
sha = shaBuf.toString('utf-8').trim().split(/\s+/)[0];
|
|
176
|
+
} catch (e) {
|
|
177
|
+
emitBootstrapEvent('warn', 'sha fetch failed; will verify after download', { error: e.message, version });
|
|
178
|
+
}
|
|
179
|
+
const payload = { version, sha, source, fetchedAt: Date.now() };
|
|
180
|
+
try {
|
|
181
|
+
fs.mkdirSync(PLUGKIT_TOOLS_DIR, { recursive: true });
|
|
182
|
+
fs.writeFileSync(PLUGKIT_LATEST_CACHE, JSON.stringify(payload, null, 2));
|
|
183
|
+
} catch (_) {}
|
|
184
|
+
emitBootstrapEvent('info', 'Resolved latest plugkit version', { version, source, hasSha: Boolean(sha) });
|
|
185
|
+
return payload;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function gitignorePath(cwd) { return path.join(cwd, '.gitignore'); }
|
|
189
|
+
|
|
190
|
+
function getManagedGitignoreEntries() {
|
|
191
|
+
return [
|
|
192
|
+
'.gm/exec-spool/',
|
|
193
|
+
'.gm/gm-fired-*',
|
|
194
|
+
'.gm/needs-gm',
|
|
195
|
+
'.gm/lastskill',
|
|
196
|
+
'.gm/turn-state.json',
|
|
197
|
+
'.gm/turn-state.json.corrupted-*',
|
|
198
|
+
'.gm/residual-check-fired',
|
|
199
|
+
'.gm/bootstrap-status.json',
|
|
200
|
+
'.gm/bootstrap-error.json',
|
|
201
|
+
'.gm/rslearn-counter.json',
|
|
202
|
+
'.gm/trajectory-drafts/',
|
|
203
|
+
'.gm/ingest-drafts/',
|
|
204
|
+
'.gm/prd-state.json',
|
|
205
|
+
'.gm/subagent-*.json',
|
|
206
|
+
'.plugkit-browser-profile/',
|
|
207
|
+
'.plugkit-browser-profile-*/',
|
|
208
|
+
];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function getMustStayTracked() {
|
|
212
|
+
return [
|
|
213
|
+
'.gm/rs-learn.db',
|
|
214
|
+
'.gm/code-search/',
|
|
215
|
+
'.gm/disciplines/',
|
|
216
|
+
'.gm/prd.yml',
|
|
217
|
+
'.gm/mutables.yml',
|
|
218
|
+
'gm-data/rs-learn.db',
|
|
219
|
+
'gm-data/code-search/',
|
|
220
|
+
'gm-data/disciplines/',
|
|
221
|
+
];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function ensureManagedGitignore(cwd) {
|
|
225
|
+
try {
|
|
226
|
+
const gi = gitignorePath(cwd);
|
|
227
|
+
let content = '';
|
|
228
|
+
try { content = fs.readFileSync(gi, 'utf-8'); } catch (_) {}
|
|
229
|
+
const START = '# >>> plugkit managed';
|
|
230
|
+
const END = '# <<< plugkit managed';
|
|
231
|
+
const entries = getManagedGitignoreEntries();
|
|
232
|
+
const block = [START, ...entries, END].join('\n');
|
|
233
|
+
const startIdx = content.indexOf(START);
|
|
234
|
+
const endIdx = content.indexOf(END);
|
|
235
|
+
let cleaned;
|
|
236
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
237
|
+
const before = content.slice(0, startIdx).replace(/\n+$/, '');
|
|
238
|
+
const after = content.slice(endIdx + END.length).replace(/^\n+/, '');
|
|
239
|
+
cleaned = [before, block, after].filter(Boolean).join('\n');
|
|
240
|
+
} else {
|
|
241
|
+
cleaned = content.replace(/\n+$/, '');
|
|
242
|
+
cleaned = cleaned ? `${cleaned}\n\n${block}` : block;
|
|
243
|
+
}
|
|
244
|
+
if (!cleaned.endsWith('\n')) cleaned += '\n';
|
|
245
|
+
if (cleaned !== content) {
|
|
246
|
+
fs.writeFileSync(gi, cleaned);
|
|
247
|
+
emitBootstrapEvent('info', 'Managed .gitignore block updated', { path: gi, entries: entries.length });
|
|
248
|
+
}
|
|
249
|
+
const mustTrack = getMustStayTracked();
|
|
250
|
+
const lines = cleaned.split(/\r?\n/);
|
|
251
|
+
const inManaged = (idx) => {
|
|
252
|
+
let inside = false;
|
|
253
|
+
for (let i = 0; i <= idx; i++) {
|
|
254
|
+
if (lines[i] === START) inside = true;
|
|
255
|
+
else if (lines[i] === END) inside = false;
|
|
256
|
+
}
|
|
257
|
+
return inside;
|
|
258
|
+
};
|
|
259
|
+
for (let i = 0; i < lines.length; i++) {
|
|
260
|
+
const t = lines[i].trim();
|
|
261
|
+
if (!t || t.startsWith('#')) continue;
|
|
262
|
+
if (inManaged(i)) continue;
|
|
263
|
+
if (mustTrack.includes(t)) {
|
|
264
|
+
emitBootstrapEvent('warn', 'Hostile .gitignore entry — must stay tracked', { entry: t, line: i + 1 });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
} catch (e) {
|
|
268
|
+
emitBootstrapEvent('warn', 'ensureManagedGitignore failed', { error: e.message });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
103
272
|
async function downloadPlugkitBinary(version) {
|
|
104
273
|
const binaryName = 'plugkit.wasm';
|
|
105
274
|
const url = `https://github.com/AnEntrypoint/plugkit-bin/releases/download/v${version}/${binaryName}`;
|
|
@@ -232,6 +401,24 @@ async function verifyBinaryHealth(filePath) {
|
|
|
232
401
|
}
|
|
233
402
|
}
|
|
234
403
|
|
|
404
|
+
function openWatcherLog(projectDir) {
|
|
405
|
+
const spoolDir = path.join(projectDir, '.gm', 'exec-spool');
|
|
406
|
+
fs.mkdirSync(spoolDir, { recursive: true });
|
|
407
|
+
const logPath = path.join(spoolDir, '.watcher.log');
|
|
408
|
+
try {
|
|
409
|
+
const stat = fs.statSync(logPath);
|
|
410
|
+
if (stat.size > 10 * 1024 * 1024) {
|
|
411
|
+
const rotated = path.join(spoolDir, '.watcher.log.1');
|
|
412
|
+
try { fs.unlinkSync(rotated); } catch (_) {}
|
|
413
|
+
fs.renameSync(logPath, rotated);
|
|
414
|
+
}
|
|
415
|
+
} catch (_) {}
|
|
416
|
+
const fd = fs.openSync(logPath, 'a');
|
|
417
|
+
const header = `\n--- watcher boot ${new Date().toISOString()} pid=${process.pid} ---\n`;
|
|
418
|
+
try { fs.writeSync(fd, header); } catch (_) {}
|
|
419
|
+
return fd;
|
|
420
|
+
}
|
|
421
|
+
|
|
235
422
|
async function spawnPlugkitWatcher(wasmPath) {
|
|
236
423
|
try {
|
|
237
424
|
emitBootstrapEvent('info', 'Spawning plugkit WASM watcher daemon');
|
|
@@ -249,18 +436,23 @@ async function spawnPlugkitWatcher(wasmPath) {
|
|
|
249
436
|
throw new Error(`WASM wrapper not found at ${wrapperPath}`);
|
|
250
437
|
}
|
|
251
438
|
|
|
439
|
+
const projectDir = process.cwd();
|
|
440
|
+
const logFd = openWatcherLog(projectDir);
|
|
441
|
+
|
|
252
442
|
const runtime = process.platform === 'win32' ? 'bun.exe' : 'bun';
|
|
253
443
|
const proc = spawn(runtime, [wrapperPath, 'spool'], {
|
|
254
444
|
detached: true,
|
|
255
|
-
stdio: 'ignore',
|
|
445
|
+
stdio: ['ignore', logFd, logFd],
|
|
256
446
|
windowsHide: true,
|
|
257
|
-
env: { ...process.env, CLAUDE_PROJECT_DIR:
|
|
447
|
+
env: { ...process.env, CLAUDE_PROJECT_DIR: projectDir },
|
|
258
448
|
});
|
|
259
449
|
|
|
450
|
+
try { fs.closeSync(logFd); } catch (_) {}
|
|
451
|
+
|
|
260
452
|
const pid = proc.pid;
|
|
261
453
|
proc.unref();
|
|
262
454
|
|
|
263
|
-
emitBootstrapEvent('info', 'Plugkit WASM watcher spawned', { pid });
|
|
455
|
+
emitBootstrapEvent('info', 'Plugkit WASM watcher spawned', { pid, logPath: path.join(projectDir, '.gm', 'exec-spool', '.watcher.log') });
|
|
264
456
|
return pid;
|
|
265
457
|
} catch (e) {
|
|
266
458
|
emitBootstrapEvent('error', 'Failed to spawn plugkit WASM watcher', { error: e.message });
|
|
@@ -274,11 +466,20 @@ async function bootstrapPlugkit(sessionId) {
|
|
|
274
466
|
try {
|
|
275
467
|
emitBootstrapEvent('info', 'Bootstrap started');
|
|
276
468
|
|
|
277
|
-
|
|
469
|
+
ensureManagedGitignore(process.cwd());
|
|
470
|
+
|
|
471
|
+
const manifest = readManifest();
|
|
472
|
+
const latest = await getLatestRemoteVersion();
|
|
473
|
+
const targetVersion = (latest && latest.version) || manifest.version;
|
|
474
|
+
const expectedHash = (latest && latest.sha) || manifest.expectedHash;
|
|
475
|
+
if (latest && latest.version !== manifest.version) {
|
|
476
|
+
emitBootstrapEvent('info', 'Latest plugkit newer than manifest pin', { latest: latest.version, manifest: manifest.version });
|
|
477
|
+
}
|
|
278
478
|
const installedVersion = getInstalledVersion();
|
|
279
479
|
const plugkitPath = getPlugkitPath();
|
|
280
480
|
|
|
281
|
-
const
|
|
481
|
+
const manifestVersion = targetVersion;
|
|
482
|
+
const versionMismatch = installedVersion !== targetVersion;
|
|
282
483
|
const binaryMissing = !fs.existsSync(plugkitPath);
|
|
283
484
|
|
|
284
485
|
if (!binaryMissing && !versionMismatch) {
|
|
@@ -324,9 +525,12 @@ async function bootstrapPlugkit(sessionId) {
|
|
|
324
525
|
|
|
325
526
|
if (binaryData) {
|
|
326
527
|
const downloadedHash = crypto.createHash('sha256').update(binaryData).digest('hex');
|
|
327
|
-
if (downloadedHash !== expectedHash) {
|
|
528
|
+
if (expectedHash && downloadedHash !== expectedHash) {
|
|
328
529
|
throw new Error(`Hash mismatch: got ${downloadedHash}, expected ${expectedHash}`);
|
|
329
530
|
}
|
|
531
|
+
if (!expectedHash) {
|
|
532
|
+
emitBootstrapEvent('warn', 'No expected hash; trusting npm-resolved download', { sha: downloadedHash, version: manifestVersion });
|
|
533
|
+
}
|
|
330
534
|
|
|
331
535
|
killExistingPlugkit();
|
|
332
536
|
await writeBinaryWithRetry(plugkitPath, binaryData);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gm-skill",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1118",
|
|
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.1118"
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|
|
45
45
|
"node": ">=16.0.0"
|
package/skills/gm-skill/SKILL.md
CHANGED
|
@@ -1,32 +1,86 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: gm-skill
|
|
3
|
-
description: AI-native software engineering harness. plugkit
|
|
3
|
+
description: AI-native software engineering harness. plugkit owns all state and serves every instruction via the spool. The agent dispatches verbs; plugkit tracks phase, mutables, PRD, and recall.
|
|
4
4
|
allowed-tools: Skill, Read, Write, Bash(node *), Bash(bun *)
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# gm — single entry point
|
|
8
8
|
|
|
9
|
-
The wasm artifact lives at `~/.claude/gm-tools/plugkit.wasm`; the spool watcher runs it.
|
|
9
|
+
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.
|
|
10
10
|
|
|
11
11
|
## Boot the spool watcher (first turn only)
|
|
12
12
|
|
|
13
|
-
Check `.gm/exec-spool/.status.json`. If absent or `ts` > 15s old
|
|
13
|
+
Check `.gm/exec-spool/.status.json`. If absent or `ts` > 15s old:
|
|
14
14
|
|
|
15
15
|
`node ~/.claude/gm-tools/plugkit-wasm-wrapper.js spool > /dev/null 2>&1 &`
|
|
16
16
|
|
|
17
|
-
Wait 2 seconds, verify `.status.json` is fresh. Then proceed
|
|
17
|
+
Wait 2 seconds, verify `.status.json` is fresh. Then proceed.
|
|
18
18
|
|
|
19
19
|
## Dispatch ABI
|
|
20
20
|
|
|
21
|
-
Write request body to `.gm/exec-spool/in/<verb>/<N>.txt`. Read response from `.gm/exec-spool/out/<verb>-<N>.json`
|
|
21
|
+
Write request body to `.gm/exec-spool/in/<verb>/<N>.txt`. Read response from `.gm/exec-spool/out/<verb>-<N>.json` (nested verbs) or `out/<N>.json` (root verbs). Bodies are JSON, raw code, or a single phase name depending on the verb.
|
|
22
|
+
|
|
23
|
+
## Batch dispatch — never serial round-trips for independent verbs
|
|
24
|
+
|
|
25
|
+
The watcher processes verbs sequentially internally, but the agent's bottleneck is round-trip latency, not the watcher. **Write N inputs in one message via parallel Write tool calls, then read N outputs in one message via parallel Read calls.** A 5-verb batch is one agent turn, not five.
|
|
26
|
+
|
|
27
|
+
Example PLAN orient pack — 3 recalls + 3 codesearches in ONE message:
|
|
28
|
+
```
|
|
29
|
+
Write .gm/exec-spool/in/recall/1.txt body: {"query":"<noun A>"}
|
|
30
|
+
Write .gm/exec-spool/in/recall/2.txt body: {"query":"<noun B>"}
|
|
31
|
+
Write .gm/exec-spool/in/recall/3.txt body: {"query":"<noun C>"}
|
|
32
|
+
Write .gm/exec-spool/in/codesearch/1.txt body: {"query":"<phrase X>"}
|
|
33
|
+
Write .gm/exec-spool/in/codesearch/2.txt body: {"query":"<phrase Y>"}
|
|
34
|
+
Write .gm/exec-spool/in/codesearch/3.txt body: {"query":"<phrase Z>"}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Then in the NEXT message, all 6 Reads in parallel.
|
|
38
|
+
|
|
39
|
+
For dependent verbs (transition after instruction, prd-resolve after work), the agent must serialize — but only at the dependency boundary, not across independent dispatches.
|
|
40
|
+
|
|
41
|
+
## State lives in plugkit, not in conversation context
|
|
42
|
+
|
|
43
|
+
Never Read `.gm/prd.yml` or `.gm/mutables.yml` directly. Every `instruction` response carries the data you need:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
{
|
|
47
|
+
phase, // current phase
|
|
48
|
+
instruction, // phase prose (the active discipline)
|
|
49
|
+
prd_items: [...], // full PRD items with id, subject, status, fields
|
|
50
|
+
prd_pending_count,
|
|
51
|
+
mutables_pending: [{id, claim, witness_method, witness_evidence, status}, ...],
|
|
52
|
+
recall_hits: [...], // auto-fired against phase + first pending PRD subject
|
|
53
|
+
next_phase_hint
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Plugkit observability — read .watcher.log
|
|
58
|
+
|
|
59
|
+
The watcher writes its own stdout + stderr (plus the wasm cdylib's `println!`/`eprintln!`) to `.gm/exec-spool/.watcher.log`. Useful when:
|
|
60
|
+
|
|
61
|
+
- A dispatch returned an error you don't understand → tail the log for the stack
|
|
62
|
+
- A verb seems slow → log shows `[dispatch] ← verb=X ms=N`
|
|
63
|
+
- Sweep cleaned up something → log shows `[retention]` or `[stale-sweep]` lines
|
|
64
|
+
- Watcher boot issues → `--- watcher boot ... ---` markers
|
|
65
|
+
|
|
66
|
+
Read with `offset` to tail:
|
|
67
|
+
```
|
|
68
|
+
Read .gm/exec-spool/.watcher.log offset=<last-known-line>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The log is rotated at 10MB (older content moves to `.watcher.log.1`).
|
|
22
72
|
|
|
23
73
|
## The loop
|
|
24
74
|
|
|
25
|
-
Dispatch `instruction`
|
|
75
|
+
Dispatch `instruction` with empty body to get current-phase guidance + full state snapshot. Follow the `instruction` prose imperatively. Add PRD items via `prd-add` (JSON body), resolve via `prd-resolve` (id as body). Add mutables via `mutable-add`, resolve via `mutable-resolve` once `witness_evidence` is filled. Every resolve auto-fires `memorize-fire` so the evidence becomes recall-able.
|
|
76
|
+
|
|
77
|
+
Resolve every entry in `mutables_pending` before transitioning. When the phase's exit condition is met, dispatch `transition` with the next phase name (or empty for auto-advance). Each transition response embeds `recall_hits` automatically — relevant prior memos surface without you asking.
|
|
78
|
+
|
|
79
|
+
Stop when `next_phase_hint` is null or phase is `COMPLETE`.
|
|
26
80
|
|
|
27
81
|
## Orchestrator verbs
|
|
28
82
|
|
|
29
|
-
`instruction`, `transition`, `phase-status`, `mutable-resolve`, `memorize-fire`, `residual-scan`, `auto-recall`.
|
|
83
|
+
`instruction`, `transition`, `phase-status`, `prd-add`, `prd-resolve`, `prd-list`, `mutable-add`, `mutable-resolve`, `mutable-list`, `memorize-fire`, `residual-scan`, `auto-recall`.
|
|
30
84
|
|
|
31
85
|
## Host verbs
|
|
32
86
|
|
|
@@ -42,6 +96,4 @@ Dispatch `.gm/exec-spool/in/browser/<N>.txt` with raw JavaScript as the body. Th
|
|
|
42
96
|
|
|
43
97
|
Special commands (body starts with `session `): `session new`, `session list`, `session close <id>` pass through to playwriter directly.
|
|
44
98
|
|
|
45
|
-
Chrome is detected from system install paths; profile dir is project-scoped so cookies/login persist per project.
|
|
46
|
-
|
|
47
|
-
Plugkit serves what prior skills (`gm:planning`, `gm:gm-execute`) used to serve, on demand, per phase. There is no other skill.
|
|
99
|
+
Chrome is detected from system install paths; profile dir is project-scoped so cookies/login persist per project. The wrapper auto-adds `.plugkit-browser-profile/` to `.gitignore`.
|