gm-skill 2.0.1117 → 2.0.1119
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 +41 -0
- package/gm.json +1 -1
- package/lib/skill-bootstrap.js +178 -5
- package/package.json +2 -2
- package/skills/gm-skill/SKILL.md +4 -0
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.1119` — 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
|
|
|
@@ -2,6 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import crypto from 'crypto';
|
|
5
|
+
import https from 'https';
|
|
5
6
|
import { watch } from 'fs';
|
|
6
7
|
import { spawn, spawnSync } from 'child_process';
|
|
7
8
|
import net from 'net';
|
|
@@ -736,6 +737,46 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
736
737
|
setInterval(writeStatus, 5000);
|
|
737
738
|
writeStatus();
|
|
738
739
|
|
|
740
|
+
const UPDATE_AVAILABLE_PATH = path.join(spoolDir, '.update-available.json');
|
|
741
|
+
const UPDATE_CHECK_INTERVAL_MS = 5 * 60 * 1000;
|
|
742
|
+
function checkForUpdate() {
|
|
743
|
+
const installed = resolveVersion(instance);
|
|
744
|
+
const req = https.get({
|
|
745
|
+
host: 'api.github.com',
|
|
746
|
+
path: '/repos/AnEntrypoint/plugkit-bin/releases/latest',
|
|
747
|
+
headers: { 'user-agent': 'plugkit-watcher', 'accept': 'application/json' },
|
|
748
|
+
timeout: 5000,
|
|
749
|
+
}, (res) => {
|
|
750
|
+
if (res.statusCode !== 200) { res.resume(); return; }
|
|
751
|
+
const chunks = [];
|
|
752
|
+
res.on('data', c => chunks.push(c));
|
|
753
|
+
res.on('end', () => {
|
|
754
|
+
try {
|
|
755
|
+
const rel = JSON.parse(Buffer.concat(chunks).toString('utf-8'));
|
|
756
|
+
const tag = rel && rel.tag_name;
|
|
757
|
+
if (!tag) return;
|
|
758
|
+
const latest = tag.replace(/^v/, '');
|
|
759
|
+
if (latest === installed) {
|
|
760
|
+
try { fs.unlinkSync(UPDATE_AVAILABLE_PATH); } catch (_) {}
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
fs.writeFileSync(UPDATE_AVAILABLE_PATH, JSON.stringify({
|
|
764
|
+
installed,
|
|
765
|
+
latest,
|
|
766
|
+
checked_at_ms: Date.now(),
|
|
767
|
+
instruction: 'plugkit is out of date. To update, close the running watcher and re-bootstrap with the @latest flag, e.g. node ~/.claude/gm-tools/plugkit-wasm-wrapper.js spool & after running bootstrap with {latest: true}.',
|
|
768
|
+
update_url: `https://github.com/AnEntrypoint/plugkit-bin/releases/tag/v${latest}`,
|
|
769
|
+
}, null, 2));
|
|
770
|
+
console.log(`[update] available: installed=${installed} latest=${latest} → wrote ${UPDATE_AVAILABLE_PATH}`);
|
|
771
|
+
} catch (_) {}
|
|
772
|
+
});
|
|
773
|
+
});
|
|
774
|
+
req.on('timeout', () => req.destroy());
|
|
775
|
+
req.on('error', () => {});
|
|
776
|
+
}
|
|
777
|
+
setTimeout(checkForUpdate, 10_000);
|
|
778
|
+
setInterval(checkForUpdate, UPDATE_CHECK_INTERVAL_MS);
|
|
779
|
+
|
|
739
780
|
const pollInterval = setInterval(async () => {
|
|
740
781
|
const existing = walkDir(inDir);
|
|
741
782
|
for (const fullPath of existing) {
|
package/gm.json
CHANGED
package/lib/skill-bootstrap.js
CHANGED
|
@@ -100,6 +100,157 @@ function computeFileHash(filePath) {
|
|
|
100
100
|
return crypto.createHash('sha256').update(content).digest('hex');
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
function httpGet(url, timeoutMs) {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
const req = https.get(url, { timeout: timeoutMs, headers: { 'accept': 'application/json', 'user-agent': 'gm-skill-bootstrap' } }, (res) => {
|
|
106
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
107
|
+
res.resume();
|
|
108
|
+
httpGet(res.headers.location, timeoutMs).then(resolve, reject);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (res.statusCode !== 200) {
|
|
112
|
+
res.resume();
|
|
113
|
+
reject(new Error(`HTTP ${res.statusCode} ${url}`));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const chunks = [];
|
|
117
|
+
res.on('data', (c) => chunks.push(c));
|
|
118
|
+
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
119
|
+
res.on('error', reject);
|
|
120
|
+
});
|
|
121
|
+
req.on('timeout', () => { req.destroy(new Error(`timeout ${timeoutMs}ms ${url}`)); });
|
|
122
|
+
req.on('error', reject);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function getLatestRemoteVersion() {
|
|
127
|
+
let version = null;
|
|
128
|
+
let source = null;
|
|
129
|
+
try {
|
|
130
|
+
const buf = await httpGet('https://api.github.com/repos/AnEntrypoint/plugkit-bin/releases/latest', 3000);
|
|
131
|
+
const rel = JSON.parse(buf.toString('utf-8'));
|
|
132
|
+
const tag = rel && rel.tag_name;
|
|
133
|
+
if (tag) {
|
|
134
|
+
version = tag.replace(/^v/, '');
|
|
135
|
+
source = 'github-releases';
|
|
136
|
+
}
|
|
137
|
+
} catch (e) {
|
|
138
|
+
emitBootstrapEvent('warn', 'GitHub Releases lookup failed', { error: e.message });
|
|
139
|
+
}
|
|
140
|
+
if (!version) {
|
|
141
|
+
try {
|
|
142
|
+
const buf = await httpGet('https://registry.npmjs.org/gm-plugkit/latest', 3000);
|
|
143
|
+
const pkg = JSON.parse(buf.toString('utf-8'));
|
|
144
|
+
if (pkg && pkg.plugkitVersion) {
|
|
145
|
+
version = pkg.plugkitVersion;
|
|
146
|
+
source = 'npm-gm-plugkit';
|
|
147
|
+
} else if (pkg && pkg.version) {
|
|
148
|
+
version = pkg.version;
|
|
149
|
+
source = 'npm-gm-plugkit-fallback';
|
|
150
|
+
}
|
|
151
|
+
} catch (e) {
|
|
152
|
+
emitBootstrapEvent('warn', 'npm fallback lookup failed', { error: e.message });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (!version) {
|
|
156
|
+
emitBootstrapEvent('warn', 'All latest-version lookups failed; falling back to manifest');
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
let sha = '';
|
|
160
|
+
try {
|
|
161
|
+
const shaBuf = await httpGet(`https://github.com/AnEntrypoint/plugkit-bin/releases/download/v${version}/plugkit.wasm.sha256`, 3000);
|
|
162
|
+
sha = shaBuf.toString('utf-8').trim().split(/\s+/)[0];
|
|
163
|
+
} catch (e) {
|
|
164
|
+
emitBootstrapEvent('warn', 'sha fetch failed; will verify after download', { error: e.message, version });
|
|
165
|
+
}
|
|
166
|
+
emitBootstrapEvent('info', 'Resolved latest plugkit version', { version, source, hasSha: Boolean(sha) });
|
|
167
|
+
return { version, sha, source };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function gitignorePath(cwd) { return path.join(cwd, '.gitignore'); }
|
|
171
|
+
|
|
172
|
+
function getManagedGitignoreEntries() {
|
|
173
|
+
return [
|
|
174
|
+
'.gm/exec-spool/',
|
|
175
|
+
'.gm/gm-fired-*',
|
|
176
|
+
'.gm/needs-gm',
|
|
177
|
+
'.gm/lastskill',
|
|
178
|
+
'.gm/turn-state.json',
|
|
179
|
+
'.gm/turn-state.json.corrupted-*',
|
|
180
|
+
'.gm/residual-check-fired',
|
|
181
|
+
'.gm/bootstrap-status.json',
|
|
182
|
+
'.gm/bootstrap-error.json',
|
|
183
|
+
'.gm/rslearn-counter.json',
|
|
184
|
+
'.gm/trajectory-drafts/',
|
|
185
|
+
'.gm/ingest-drafts/',
|
|
186
|
+
'.gm/prd-state.json',
|
|
187
|
+
'.gm/subagent-*.json',
|
|
188
|
+
'.plugkit-browser-profile/',
|
|
189
|
+
'.plugkit-browser-profile-*/',
|
|
190
|
+
];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function getMustStayTracked() {
|
|
194
|
+
return [
|
|
195
|
+
'.gm/rs-learn.db',
|
|
196
|
+
'.gm/code-search/',
|
|
197
|
+
'.gm/disciplines/',
|
|
198
|
+
'.gm/prd.yml',
|
|
199
|
+
'.gm/mutables.yml',
|
|
200
|
+
'gm-data/rs-learn.db',
|
|
201
|
+
'gm-data/code-search/',
|
|
202
|
+
'gm-data/disciplines/',
|
|
203
|
+
];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function ensureManagedGitignore(cwd) {
|
|
207
|
+
try {
|
|
208
|
+
const gi = gitignorePath(cwd);
|
|
209
|
+
let content = '';
|
|
210
|
+
try { content = fs.readFileSync(gi, 'utf-8'); } catch (_) {}
|
|
211
|
+
const START = '# >>> plugkit managed';
|
|
212
|
+
const END = '# <<< plugkit managed';
|
|
213
|
+
const entries = getManagedGitignoreEntries();
|
|
214
|
+
const block = [START, ...entries, END].join('\n');
|
|
215
|
+
const startIdx = content.indexOf(START);
|
|
216
|
+
const endIdx = content.indexOf(END);
|
|
217
|
+
let cleaned;
|
|
218
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
219
|
+
const before = content.slice(0, startIdx).replace(/\n+$/, '');
|
|
220
|
+
const after = content.slice(endIdx + END.length).replace(/^\n+/, '');
|
|
221
|
+
cleaned = [before, block, after].filter(Boolean).join('\n');
|
|
222
|
+
} else {
|
|
223
|
+
cleaned = content.replace(/\n+$/, '');
|
|
224
|
+
cleaned = cleaned ? `${cleaned}\n\n${block}` : block;
|
|
225
|
+
}
|
|
226
|
+
if (!cleaned.endsWith('\n')) cleaned += '\n';
|
|
227
|
+
if (cleaned !== content) {
|
|
228
|
+
fs.writeFileSync(gi, cleaned);
|
|
229
|
+
emitBootstrapEvent('info', 'Managed .gitignore block updated', { path: gi, entries: entries.length });
|
|
230
|
+
}
|
|
231
|
+
const mustTrack = getMustStayTracked();
|
|
232
|
+
const lines = cleaned.split(/\r?\n/);
|
|
233
|
+
const inManaged = (idx) => {
|
|
234
|
+
let inside = false;
|
|
235
|
+
for (let i = 0; i <= idx; i++) {
|
|
236
|
+
if (lines[i] === START) inside = true;
|
|
237
|
+
else if (lines[i] === END) inside = false;
|
|
238
|
+
}
|
|
239
|
+
return inside;
|
|
240
|
+
};
|
|
241
|
+
for (let i = 0; i < lines.length; i++) {
|
|
242
|
+
const t = lines[i].trim();
|
|
243
|
+
if (!t || t.startsWith('#')) continue;
|
|
244
|
+
if (inManaged(i)) continue;
|
|
245
|
+
if (mustTrack.includes(t)) {
|
|
246
|
+
emitBootstrapEvent('warn', 'Hostile .gitignore entry — must stay tracked', { entry: t, line: i + 1 });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
} catch (e) {
|
|
250
|
+
emitBootstrapEvent('warn', 'ensureManagedGitignore failed', { error: e.message });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
103
254
|
async function downloadPlugkitBinary(version) {
|
|
104
255
|
const binaryName = 'plugkit.wasm';
|
|
105
256
|
const url = `https://github.com/AnEntrypoint/plugkit-bin/releases/download/v${version}/${binaryName}`;
|
|
@@ -291,17 +442,36 @@ async function spawnPlugkitWatcher(wasmPath) {
|
|
|
291
442
|
}
|
|
292
443
|
}
|
|
293
444
|
|
|
294
|
-
async function bootstrapPlugkit(sessionId) {
|
|
445
|
+
async function bootstrapPlugkit(sessionId, options) {
|
|
295
446
|
const startTime = Date.now();
|
|
447
|
+
const opts = options || {};
|
|
448
|
+
const forceLatest = Boolean(opts.latest);
|
|
296
449
|
|
|
297
450
|
try {
|
|
298
|
-
emitBootstrapEvent('info', 'Bootstrap started');
|
|
451
|
+
emitBootstrapEvent('info', 'Bootstrap started', { forceLatest });
|
|
452
|
+
|
|
453
|
+
ensureManagedGitignore(process.cwd());
|
|
454
|
+
|
|
455
|
+
const manifest = readManifest();
|
|
456
|
+
let targetVersion = manifest.version;
|
|
457
|
+
let expectedHash = manifest.expectedHash;
|
|
458
|
+
|
|
459
|
+
if (forceLatest) {
|
|
460
|
+
const latest = await getLatestRemoteVersion();
|
|
461
|
+
if (latest && latest.version) {
|
|
462
|
+
targetVersion = latest.version;
|
|
463
|
+
expectedHash = latest.sha || expectedHash;
|
|
464
|
+
if (latest.version !== manifest.version) {
|
|
465
|
+
emitBootstrapEvent('info', 'forceLatest: using newer remote version', { latest: latest.version, manifest: manifest.version });
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
299
469
|
|
|
300
|
-
const { version: manifestVersion, expectedHash } = readManifest();
|
|
301
470
|
const installedVersion = getInstalledVersion();
|
|
302
471
|
const plugkitPath = getPlugkitPath();
|
|
303
472
|
|
|
304
|
-
const
|
|
473
|
+
const manifestVersion = targetVersion;
|
|
474
|
+
const versionMismatch = installedVersion !== targetVersion;
|
|
305
475
|
const binaryMissing = !fs.existsSync(plugkitPath);
|
|
306
476
|
|
|
307
477
|
if (!binaryMissing && !versionMismatch) {
|
|
@@ -347,9 +517,12 @@ async function bootstrapPlugkit(sessionId) {
|
|
|
347
517
|
|
|
348
518
|
if (binaryData) {
|
|
349
519
|
const downloadedHash = crypto.createHash('sha256').update(binaryData).digest('hex');
|
|
350
|
-
if (downloadedHash !== expectedHash) {
|
|
520
|
+
if (expectedHash && downloadedHash !== expectedHash) {
|
|
351
521
|
throw new Error(`Hash mismatch: got ${downloadedHash}, expected ${expectedHash}`);
|
|
352
522
|
}
|
|
523
|
+
if (!expectedHash) {
|
|
524
|
+
emitBootstrapEvent('warn', 'No expected hash; trusting npm-resolved download', { sha: downloadedHash, version: manifestVersion });
|
|
525
|
+
}
|
|
353
526
|
|
|
354
527
|
killExistingPlugkit();
|
|
355
528
|
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.1119",
|
|
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.1119"
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|
|
45
45
|
"node": ">=16.0.0"
|
package/skills/gm-skill/SKILL.md
CHANGED
|
@@ -16,6 +16,10 @@ Check `.gm/exec-spool/.status.json`. If absent or `ts` > 15s old:
|
|
|
16
16
|
|
|
17
17
|
Wait 2 seconds, verify `.status.json` is fresh. Then proceed.
|
|
18
18
|
|
|
19
|
+
## Plugkit version updates
|
|
20
|
+
|
|
21
|
+
The watcher checks GitHub Releases every 5 minutes for a newer plugkit. If drift is detected, it writes `.gm/exec-spool/.update-available.json` with `{installed, latest, instruction, update_url}`; if no drift, the file is removed. Read this file at session start (and occasionally afterward); if present, kill the current watcher, run `bootstrapPlugkit({latest: true})` once to fetch the new wasm, then restart the watcher. Default bootstrap never hits the network — only `{latest: true}` fetches the newest binary.
|
|
22
|
+
|
|
19
23
|
## Dispatch ABI
|
|
20
24
|
|
|
21
25
|
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.
|