gm-skill 2.0.1217 → 2.0.1219
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/bin/plugkit.version +1 -1
- package/bin/plugkit.wasm +0 -0
- package/bin/plugkit.wasm.sha256 +1 -1
- package/gm-plugkit/plugkit-wasm-wrapper.js +75 -10
- package/gm.json +2 -2
- package/lib/skill-bootstrap.js +74 -11
- package/lib/spool-dispatch.js +71 -1
- package/lib/spool-poll-gate.js +19 -1
- package/package.json +2 -2
- package/prompts/prompt-submit.txt +4 -0
- package/skills/gm-skill/SKILL.md +2 -0
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ An earlier generation fanned out fifteen per-platform downstream repos (gm-cc, g
|
|
|
35
35
|
|
|
36
36
|
## Version
|
|
37
37
|
|
|
38
|
-
`2.0.
|
|
38
|
+
`2.0.1219` — auto-bumped from the canonical `gm` repo. Every push to `AnEntrypoint/gm` (or any cascading sibling crate) republishes this package.
|
|
39
39
|
|
|
40
40
|
## Source of truth
|
|
41
41
|
|
package/bin/plugkit.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.1.
|
|
1
|
+
0.1.447
|
package/bin/plugkit.wasm
CHANGED
|
Binary file
|
package/bin/plugkit.wasm.sha256
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
0ce920f00138b9524a0a1ad062dab691e4959497853644a7d06403c2b77ed480 plugkit.wasm
|
|
@@ -70,6 +70,32 @@ function isSpoolPollCommand(command) {
|
|
|
70
70
|
return null;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
function isBrowserRunningFileLocal(rel) {
|
|
74
|
+
if (!rel) return false;
|
|
75
|
+
const norm = String(rel).replace(/\\\\/g, '/');
|
|
76
|
+
if (/\\.(html?|tsx|jsx|vue|svelte)$/i.test(norm)) return true;
|
|
77
|
+
if (/\\.(mjs|cjs|js|ts|css|scss|sass)$/i.test(norm) && /^(src|public|site|app|pages|components|client|web)\\//i.test(norm)) return true;
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function recordBrowserEditLocal(cwd, filePath) {
|
|
82
|
+
try {
|
|
83
|
+
const fs = require('fs');
|
|
84
|
+
const path = require('path');
|
|
85
|
+
let rel = filePath;
|
|
86
|
+
try { rel = path.relative(cwd, filePath); } catch (_) {}
|
|
87
|
+
if (!isBrowserRunningFileLocal(rel)) return false;
|
|
88
|
+
const editsFile = path.join(cwd, '.gm', 'exec-spool', '.turn-browser-edits.json');
|
|
89
|
+
fs.mkdirSync(path.dirname(editsFile), { recursive: true });
|
|
90
|
+
let list = [];
|
|
91
|
+
try { list = JSON.parse(fs.readFileSync(editsFile, 'utf8')); if (!Array.isArray(list)) list = []; } catch (_) {}
|
|
92
|
+
const entry = { file: rel.replace(/\\\\/g, '/'), ts: Date.now() };
|
|
93
|
+
if (!list.some(e => e && e.file === entry.file)) list.push(entry);
|
|
94
|
+
fs.writeFileSync(editsFile, JSON.stringify(list));
|
|
95
|
+
return true;
|
|
96
|
+
} catch (_) { return false; }
|
|
97
|
+
}
|
|
98
|
+
|
|
73
99
|
let raw = '';
|
|
74
100
|
process.stdin.setEncoding('utf8');
|
|
75
101
|
process.stdin.on('data', (chunk) => { raw += chunk; });
|
|
@@ -78,6 +104,17 @@ process.stdin.on('end', () => {
|
|
|
78
104
|
try { event = JSON.parse(raw || '{}'); } catch (_) { event = {}; }
|
|
79
105
|
const tool = event.tool_name || event.tool || '';
|
|
80
106
|
const input = event.tool_input || event.input || {};
|
|
107
|
+
const cwd = event.cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
108
|
+
|
|
109
|
+
if (tool === 'Write' || tool === 'Edit' || tool === 'MultiEdit') {
|
|
110
|
+
const fp = input.file_path || input.filePath || input.path || '';
|
|
111
|
+
if (fp) {
|
|
112
|
+
try { recordBrowserEditLocal(cwd, fp); } catch (_) {}
|
|
113
|
+
}
|
|
114
|
+
process.stdout.write(JSON.stringify({ continue: true }));
|
|
115
|
+
process.exit(0);
|
|
116
|
+
}
|
|
117
|
+
|
|
81
118
|
if (tool !== 'Bash') {
|
|
82
119
|
process.stdout.write(JSON.stringify({ continue: true }));
|
|
83
120
|
process.exit(0);
|
|
@@ -165,15 +202,17 @@ function ensureSpoolPollGate(cwd) {
|
|
|
165
202
|
if (!settings.hooks || typeof settings.hooks !== 'object') settings.hooks = {};
|
|
166
203
|
if (!Array.isArray(settings.hooks.PreToolUse)) settings.hooks.PreToolUse = [];
|
|
167
204
|
const wantCommand = `node "\${CLAUDE_PROJECT_DIR}/.gm/hooks/spool-poll-gate.js"`;
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
205
|
+
for (const matcher of ['Bash', 'Write', 'Edit', 'MultiEdit']) {
|
|
206
|
+
let entry = settings.hooks.PreToolUse.find(e => e && e.matcher === matcher);
|
|
207
|
+
if (!entry) {
|
|
208
|
+
entry = { matcher, hooks: [] };
|
|
209
|
+
settings.hooks.PreToolUse.push(entry);
|
|
210
|
+
}
|
|
211
|
+
if (!Array.isArray(entry.hooks)) entry.hooks = [];
|
|
212
|
+
const already = entry.hooks.some(h => h && typeof h.command === 'string' && h.command.includes('spool-poll-gate.js'));
|
|
213
|
+
if (!already) {
|
|
214
|
+
entry.hooks.push({ type: 'command', command: wantCommand });
|
|
215
|
+
}
|
|
177
216
|
}
|
|
178
217
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
179
218
|
} catch (_) {}
|
|
@@ -1184,7 +1223,17 @@ function makeHostFunctions(instanceRef) {
|
|
|
1184
1223
|
const opts = optsStr ? JSON.parse(optsStr) : {};
|
|
1185
1224
|
const lang = opts.lang || 'nodejs';
|
|
1186
1225
|
const cwd = opts.cwd || process.cwd();
|
|
1187
|
-
const
|
|
1226
|
+
const rawTimeout = opts.timeoutMs;
|
|
1227
|
+
if (rawTimeout === undefined || rawTimeout === null || typeof rawTimeout !== 'number' || !Number.isFinite(rawTimeout) || rawTimeout <= 0 || !Number.isInteger(rawTimeout)) {
|
|
1228
|
+
return writeWasmJson(instanceRef.value, {
|
|
1229
|
+
ok: false,
|
|
1230
|
+
error: 'missing timeoutMs',
|
|
1231
|
+
required: 'positive integer milliseconds',
|
|
1232
|
+
paper_ref: '§20',
|
|
1233
|
+
received: rawTimeout === undefined ? null : rawTimeout,
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
const timeoutMs = rawTimeout;
|
|
1188
1237
|
let cmd, args;
|
|
1189
1238
|
if (lang === 'nodejs' || lang === 'js') { cmd = process.execPath; args = ['-e', code]; }
|
|
1190
1239
|
else if (lang === 'python') { cmd = 'python'; args = ['-c', code]; }
|
|
@@ -1716,6 +1765,13 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
1716
1765
|
const sessForRecall = readCurrentSess();
|
|
1717
1766
|
if (isInstructionTurnStart(sessForRecall)) {
|
|
1718
1767
|
autoRecallPayload = tryAutoRecallForTurnEntry(instance, sessForRecall, process.cwd());
|
|
1768
|
+
try {
|
|
1769
|
+
const _spoolDir = path.join(process.cwd(), '.gm', 'exec-spool');
|
|
1770
|
+
for (const _f of ['.turn-browser-edits.json', '.turn-browser-witnessed']) {
|
|
1771
|
+
const _p = path.join(_spoolDir, _f);
|
|
1772
|
+
if (fs.existsSync(_p)) fs.unlinkSync(_p);
|
|
1773
|
+
}
|
|
1774
|
+
} catch (_) {}
|
|
1719
1775
|
}
|
|
1720
1776
|
}
|
|
1721
1777
|
|
|
@@ -1742,6 +1798,15 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
1742
1798
|
logEvent('plugkit', 'dispatch.end', { verb, task: taskBase, dur_ms, out_bytes: resultStr.length });
|
|
1743
1799
|
emitOrchestratorEvents(verb, taskBase, resultStr);
|
|
1744
1800
|
|
|
1801
|
+
if (verb === 'browser') {
|
|
1802
|
+
try {
|
|
1803
|
+
const witnessFile = path.join(process.cwd(), '.gm', 'exec-spool', '.turn-browser-witnessed');
|
|
1804
|
+
fs.mkdirSync(path.dirname(witnessFile), { recursive: true });
|
|
1805
|
+
fs.writeFileSync(witnessFile, JSON.stringify({ ts: Date.now(), task: taskBase, dur_ms }));
|
|
1806
|
+
logEvent('plugkit', 'browser.witness-marked', { task: taskBase });
|
|
1807
|
+
} catch (_) {}
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1745
1810
|
try { instance.exports.plugkit_free(verbPtr, verbBytes.length); } catch (_) {}
|
|
1746
1811
|
try { instance.exports.plugkit_free(bodyPtr, bodyBytes.length); } catch (_) {}
|
|
1747
1812
|
try { instance.exports.plugkit_free(ptr, len); } catch (_) {}
|
package/gm.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gm",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1219",
|
|
4
4
|
"description": "Spool-dispatch orchestration engine with unified state machine, skills, and automated git enforcement",
|
|
5
5
|
"author": "AnEntrypoint",
|
|
6
6
|
"license": "MIT",
|
|
@@ -17,5 +17,5 @@
|
|
|
17
17
|
"publishConfig": {
|
|
18
18
|
"access": "public"
|
|
19
19
|
},
|
|
20
|
-
"plugkitVersion": "0.1.
|
|
20
|
+
"plugkitVersion": "0.1.447"
|
|
21
21
|
}
|
package/lib/skill-bootstrap.js
CHANGED
|
@@ -363,6 +363,32 @@ function isSpoolPollCommand(command) {
|
|
|
363
363
|
return null;
|
|
364
364
|
}
|
|
365
365
|
|
|
366
|
+
function isBrowserRunningFileLocal(rel) {
|
|
367
|
+
if (!rel) return false;
|
|
368
|
+
const norm = String(rel).replace(/\\\\/g, '/');
|
|
369
|
+
if (/\\.(html?|tsx|jsx|vue|svelte)$/i.test(norm)) return true;
|
|
370
|
+
if (/\\.(mjs|cjs|js|ts|css|scss|sass)$/i.test(norm) && /^(src|public|site|app|pages|components|client|web)\\//i.test(norm)) return true;
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function recordBrowserEditLocal(cwd, filePath) {
|
|
375
|
+
try {
|
|
376
|
+
const fs = require('fs');
|
|
377
|
+
const path = require('path');
|
|
378
|
+
let rel = filePath;
|
|
379
|
+
try { rel = path.relative(cwd, filePath); } catch (_) {}
|
|
380
|
+
if (!isBrowserRunningFileLocal(rel)) return false;
|
|
381
|
+
const editsFile = path.join(cwd, '.gm', 'exec-spool', '.turn-browser-edits.json');
|
|
382
|
+
fs.mkdirSync(path.dirname(editsFile), { recursive: true });
|
|
383
|
+
let list = [];
|
|
384
|
+
try { list = JSON.parse(fs.readFileSync(editsFile, 'utf8')); if (!Array.isArray(list)) list = []; } catch (_) {}
|
|
385
|
+
const entry = { file: rel.replace(/\\\\/g, '/'), ts: Date.now() };
|
|
386
|
+
if (!list.some(e => e && e.file === entry.file)) list.push(entry);
|
|
387
|
+
fs.writeFileSync(editsFile, JSON.stringify(list));
|
|
388
|
+
return true;
|
|
389
|
+
} catch (_) { return false; }
|
|
390
|
+
}
|
|
391
|
+
|
|
366
392
|
let raw = '';
|
|
367
393
|
process.stdin.setEncoding('utf8');
|
|
368
394
|
process.stdin.on('data', (chunk) => { raw += chunk; });
|
|
@@ -371,6 +397,17 @@ process.stdin.on('end', () => {
|
|
|
371
397
|
try { event = JSON.parse(raw || '{}'); } catch (_) { event = {}; }
|
|
372
398
|
const tool = event.tool_name || event.tool || '';
|
|
373
399
|
const input = event.tool_input || event.input || {};
|
|
400
|
+
const cwd = event.cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
401
|
+
|
|
402
|
+
if (tool === 'Write' || tool === 'Edit' || tool === 'MultiEdit') {
|
|
403
|
+
const fp = input.file_path || input.filePath || input.path || '';
|
|
404
|
+
if (fp) {
|
|
405
|
+
try { recordBrowserEditLocal(cwd, fp); } catch (_) {}
|
|
406
|
+
}
|
|
407
|
+
process.stdout.write(JSON.stringify({ continue: true }));
|
|
408
|
+
process.exit(0);
|
|
409
|
+
}
|
|
410
|
+
|
|
374
411
|
if (tool !== 'Bash') {
|
|
375
412
|
process.stdout.write(JSON.stringify({ continue: true }));
|
|
376
413
|
process.exit(0);
|
|
@@ -417,11 +454,33 @@ function ensureSpoolPollGate(cwd) {
|
|
|
417
454
|
const gateScript = path.join(gmHooks, 'spool-poll-gate.js');
|
|
418
455
|
const want = spoolPollGateScript();
|
|
419
456
|
let need = true;
|
|
457
|
+
let oldSha = '';
|
|
458
|
+
let existed = false;
|
|
420
459
|
try {
|
|
421
460
|
const existing = fs.readFileSync(gateScript, 'utf8');
|
|
461
|
+
existed = true;
|
|
422
462
|
if (existing === want) need = false;
|
|
463
|
+
else {
|
|
464
|
+
try {
|
|
465
|
+
const _crypto = require('crypto');
|
|
466
|
+
oldSha = _crypto.createHash('sha256').update(existing).digest('hex').slice(0, 12);
|
|
467
|
+
} catch (_) {}
|
|
468
|
+
}
|
|
423
469
|
} catch (_) {}
|
|
424
|
-
if (need)
|
|
470
|
+
if (need) {
|
|
471
|
+
fs.writeFileSync(gateScript, want);
|
|
472
|
+
try {
|
|
473
|
+
const _crypto = require('crypto');
|
|
474
|
+
const newSha = _crypto.createHash('sha256').update(want).digest('hex').slice(0, 12);
|
|
475
|
+
emitBootstrapEvent('info', existed ? 'gate.refreshed' : 'gate.installed', {
|
|
476
|
+
cwd,
|
|
477
|
+
path: gateScript,
|
|
478
|
+
old_sha: oldSha || null,
|
|
479
|
+
new_sha: newSha,
|
|
480
|
+
bytes: Buffer.byteLength(want, 'utf8'),
|
|
481
|
+
});
|
|
482
|
+
} catch (_) {}
|
|
483
|
+
}
|
|
425
484
|
|
|
426
485
|
const claudeDir = path.join(cwd, '.claude');
|
|
427
486
|
fs.mkdirSync(claudeDir, { recursive: true });
|
|
@@ -434,18 +493,22 @@ function ensureSpoolPollGate(cwd) {
|
|
|
434
493
|
if (!settings.hooks || typeof settings.hooks !== 'object') settings.hooks = {};
|
|
435
494
|
if (!Array.isArray(settings.hooks.PreToolUse)) settings.hooks.PreToolUse = [];
|
|
436
495
|
const wantCommand = `node "\${CLAUDE_PROJECT_DIR}/.gm/hooks/spool-poll-gate.js"`;
|
|
437
|
-
let
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
496
|
+
let anyAlready = true;
|
|
497
|
+
for (const matcher of ['Bash', 'Write', 'Edit', 'MultiEdit']) {
|
|
498
|
+
let entry = settings.hooks.PreToolUse.find(e => e && e.matcher === matcher);
|
|
499
|
+
if (!entry) {
|
|
500
|
+
entry = { matcher, hooks: [] };
|
|
501
|
+
settings.hooks.PreToolUse.push(entry);
|
|
502
|
+
}
|
|
503
|
+
if (!Array.isArray(entry.hooks)) entry.hooks = [];
|
|
504
|
+
const already = entry.hooks.some(h => h && typeof h.command === 'string' && h.command.includes('spool-poll-gate.js'));
|
|
505
|
+
if (!already) {
|
|
506
|
+
entry.hooks.push({ type: 'command', command: wantCommand });
|
|
507
|
+
anyAlready = false;
|
|
508
|
+
}
|
|
446
509
|
}
|
|
447
510
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
448
|
-
emitBootstrapEvent('info', 'Spool-poll gate ensured', { gateScript, settingsPath, hookAlreadyPresent:
|
|
511
|
+
emitBootstrapEvent('info', 'Spool-poll gate ensured', { gateScript, settingsPath, hookAlreadyPresent: anyAlready });
|
|
449
512
|
} catch (e) {
|
|
450
513
|
emitBootstrapEvent('warn', 'ensureSpoolPollGate failed', { error: e.message });
|
|
451
514
|
}
|
package/lib/spool-dispatch.js
CHANGED
|
@@ -59,6 +59,69 @@ function hasUnpushedCommits(cwd) {
|
|
|
59
59
|
|
|
60
60
|
const TOPLEVEL_DOC_ALLOWLIST = new Set(['AGENTS.md', 'CLAUDE.md', 'README.md', 'SKILLS.md', 'CHANGELOG.md', 'LICENSE', 'LICENSE.md']);
|
|
61
61
|
|
|
62
|
+
const BROWSER_FILE_EXT_RE = /\.(html?|tsx|jsx|vue|svelte|mjs|cjs|js|ts|css|scss|sass)$/i;
|
|
63
|
+
const BROWSER_FILE_DIR_RE = /^(src|public|site|app|pages|components|client|web)[\\/]/i;
|
|
64
|
+
|
|
65
|
+
function isBrowserRunningFile(rel) {
|
|
66
|
+
if (!rel) return false;
|
|
67
|
+
const norm = String(rel).replace(/\\/g, '/');
|
|
68
|
+
if (/\.(html?|tsx|jsx|vue|svelte)$/i.test(norm)) return true;
|
|
69
|
+
if (/\.(mjs|cjs|js|ts|css|scss|sass)$/i.test(norm) && BROWSER_FILE_DIR_RE.test(norm)) return true;
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function browserEditsFile(cwd) {
|
|
74
|
+
return path.join(cwd || process.cwd(), '.gm', 'exec-spool', '.turn-browser-edits.json');
|
|
75
|
+
}
|
|
76
|
+
function browserWitnessFile(cwd) {
|
|
77
|
+
return path.join(cwd || process.cwd(), '.gm', 'exec-spool', '.turn-browser-witnessed');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function recordBrowserEdit(cwd, filePath) {
|
|
81
|
+
try {
|
|
82
|
+
const root = cwd || process.cwd();
|
|
83
|
+
let rel = filePath;
|
|
84
|
+
try { rel = path.relative(root, filePath); } catch (_) {}
|
|
85
|
+
if (!isBrowserRunningFile(rel)) return false;
|
|
86
|
+
const f = browserEditsFile(root);
|
|
87
|
+
fs.mkdirSync(path.dirname(f), { recursive: true });
|
|
88
|
+
let list = [];
|
|
89
|
+
try { list = JSON.parse(fs.readFileSync(f, 'utf8')); if (!Array.isArray(list)) list = []; } catch (_) {}
|
|
90
|
+
const entry = { file: rel.replace(/\\/g, '/'), ts: Date.now() };
|
|
91
|
+
if (!list.some(e => e && e.file === entry.file)) list.push(entry);
|
|
92
|
+
fs.writeFileSync(f, JSON.stringify(list));
|
|
93
|
+
return true;
|
|
94
|
+
} catch (_) { return false; }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function clearBrowserTurnMarkers(cwd) {
|
|
98
|
+
const root = cwd || process.cwd();
|
|
99
|
+
for (const p of [browserEditsFile(root), browserWitnessFile(root)]) {
|
|
100
|
+
try { if (fs.existsSync(p)) fs.unlinkSync(p); } catch (_) {}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function markBrowserWitnessed(cwd, meta) {
|
|
105
|
+
try {
|
|
106
|
+
const f = browserWitnessFile(cwd);
|
|
107
|
+
fs.mkdirSync(path.dirname(f), { recursive: true });
|
|
108
|
+
fs.writeFileSync(f, JSON.stringify({ ts: Date.now(), ...(meta || {}) }));
|
|
109
|
+
} catch (_) {}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function readBrowserEdits(cwd) {
|
|
113
|
+
try {
|
|
114
|
+
const f = browserEditsFile(cwd);
|
|
115
|
+
if (!fs.existsSync(f)) return [];
|
|
116
|
+
const list = JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
117
|
+
return Array.isArray(list) ? list : [];
|
|
118
|
+
} catch (_) { return []; }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isBrowserWitnessed(cwd) {
|
|
122
|
+
try { return fs.existsSync(browserWitnessFile(cwd)); } catch (_) { return false; }
|
|
123
|
+
}
|
|
124
|
+
|
|
62
125
|
function unsolicitedDocs(cwd) {
|
|
63
126
|
try {
|
|
64
127
|
const r = spawnSync('git', ['status', '--porcelain'], {
|
|
@@ -285,6 +348,13 @@ function checkDispatchGates(sessionId, operation, extra) {
|
|
|
285
348
|
logDeviation('deviation.unsolicited-doc-created', { file: f, operation });
|
|
286
349
|
}
|
|
287
350
|
}
|
|
351
|
+
const browserEdits = readBrowserEdits(cwd);
|
|
352
|
+
if (browserEdits.length > 0 && !isBrowserWitnessed(cwd)) {
|
|
353
|
+
const files = browserEdits.map(e => e.file);
|
|
354
|
+
const shown = files.slice(0, 5).join(', ') + (files.length > 5 ? `, +${files.length - 5} more` : '');
|
|
355
|
+
residuals.push(`Browser Witness required: you edited ${shown} without dispatching the browser verb to witness the change in a live page. Per paper §23 this is non-negotiable. Either dispatch browser to verify the edit works in-browser, or revert the changes.`);
|
|
356
|
+
logDeviation('deviation.browser-witness-missing', { files, operation });
|
|
357
|
+
}
|
|
288
358
|
if (residuals.length > 0) {
|
|
289
359
|
logDeviation('deviation.gate-deny', { operation, reason: 'stop-gate residuals', residuals });
|
|
290
360
|
return { allowed: false, reason: `stop-gate residuals: ${residuals.join('; ')}`, residuals };
|
|
@@ -339,4 +409,4 @@ function checkDispatchGates(sessionId, operation, extra) {
|
|
|
339
409
|
return { allowed: true };
|
|
340
410
|
}
|
|
341
411
|
|
|
342
|
-
module.exports = { dispatchSpool, checkDispatchGates, isWorktreeDirty, hasUnpushedCommits, unsolicitedDocs, logDeviation, markInstructionSeen, hasDispatchedInstruction, isSpoolPollCommand, SPOOL_POLL_REASON };
|
|
412
|
+
module.exports = { dispatchSpool, checkDispatchGates, isWorktreeDirty, hasUnpushedCommits, unsolicitedDocs, logDeviation, markInstructionSeen, hasDispatchedInstruction, isSpoolPollCommand, SPOOL_POLL_REASON, recordBrowserEdit, markBrowserWitnessed, clearBrowserTurnMarkers, isBrowserRunningFile, readBrowserEdits, isBrowserWitnessed };
|
package/lib/spool-poll-gate.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const { isSpoolPollCommand, SPOOL_POLL_REASON, logDeviation } = require('./spool-dispatch.js');
|
|
2
|
+
const { isSpoolPollCommand, SPOOL_POLL_REASON, logDeviation, recordBrowserEdit, isBrowserRunningFile } = require('./spool-dispatch.js');
|
|
3
3
|
|
|
4
4
|
let raw = '';
|
|
5
5
|
process.stdin.setEncoding('utf8');
|
|
@@ -9,6 +9,24 @@ process.stdin.on('end', () => {
|
|
|
9
9
|
try { event = JSON.parse(raw || '{}'); } catch (_) { event = {}; }
|
|
10
10
|
const tool = event.tool_name || event.tool || '';
|
|
11
11
|
const input = event.tool_input || event.input || {};
|
|
12
|
+
const cwd = event.cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
13
|
+
|
|
14
|
+
if (tool === 'Write' || tool === 'Edit' || tool === 'MultiEdit') {
|
|
15
|
+
const fp = input.file_path || input.filePath || input.path || '';
|
|
16
|
+
if (fp && isBrowserRunningFile(require('path').relative(cwd, fp))) {
|
|
17
|
+
try { recordBrowserEdit(cwd, fp); } catch (_) {}
|
|
18
|
+
try {
|
|
19
|
+
logDeviation('browser-edit.recorded', {
|
|
20
|
+
operation: tool.toLowerCase(),
|
|
21
|
+
file: fp,
|
|
22
|
+
sess: event.session_id || process.env.CLAUDE_SESSION_ID || process.env.GM_SESSION_ID || '',
|
|
23
|
+
});
|
|
24
|
+
} catch (_) {}
|
|
25
|
+
}
|
|
26
|
+
process.stdout.write(JSON.stringify({ continue: true }));
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
|
|
12
30
|
if (tool !== 'Bash') {
|
|
13
31
|
process.stdout.write(JSON.stringify({ continue: true }));
|
|
14
32
|
process.exit(0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gm-skill",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1219",
|
|
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.1219"
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|
|
45
45
|
"node": ">=16.0.0"
|
|
@@ -66,6 +66,10 @@ Asking is permitted only as last resort — destructive-irreversible action with
|
|
|
66
66
|
|
|
67
67
|
The size of the task, the cost of context, the duration of CI, and the number of repos involved are never grounds to ask. Neither is "this change touches files the user reads" — your job is to land the change correctly, not to defer to a review you imagine the user wants. Audit findings, prose rewrites, configuration edits, and refactors all ship inside the same turn as the analysis that produced them. Stopping mid-loop to ask "should I apply these?" is itself the deviation pattern: it routes the doubt back to the user instead of executing.
|
|
68
68
|
|
|
69
|
+
=== AUTO-RECALL ON TURN ENTRY ===
|
|
70
|
+
|
|
71
|
+
On every first `instruction` dispatch after a >30s idle gap (or at session-start), plugkit derives a 2–6 word recall query from `.gm/last-prompt.txt` / `.gm/turn-state.json` and attaches the resulting hits to the `instruction` response under the top-level field `auto_recall: {query, hits, fired_at, turn_entry: true}`. This is in addition to the existing `recall_hits` field (which is the phase+PRD-subject pack). Plugkit attaches the auto-recall pack to your instruction response on turn entry — YOU read it the same way you read `recall_hits`. Subsequent `instruction` dispatches in the same turn do not re-fire auto-recall; if you need a different query mid-turn, dispatch the `auto-recall` verb explicitly with your prompt as the body.
|
|
72
|
+
|
|
69
73
|
=== NO WAITING FOR PLUGKIT — HARD RULE ===
|
|
70
74
|
|
|
71
75
|
Plugkit is synchronous from your perspective. You write the request file; the spool watcher processes it; you read the response file. There is no background work happening "while you wait." If you find yourself running `sleep 2; ls .gm/exec-spool/out/`, `Start-Sleep -Milliseconds 2500; Test-Path ...`, or any other poll-wait on the spool, you have misread the architecture. Read the response file directly with the Read tool — if it does not exist yet, the watcher is either dead (check `.gm/exec-spool/.status.json` mtime) or the verb is genuinely slow (check `.gm/exec-spool/.watcher.log` for the dispatch trace). Polling is forbidden. Wait-narration ("the orchestrator is processing, while waiting let me…") is a tell that you have lost the loop.
|
package/skills/gm-skill/SKILL.md
CHANGED
|
@@ -21,3 +21,5 @@ bun x gm-plugkit@latest spool > /dev/null 2>&1 &
|
|
|
21
21
|
(`npx -y gm-plugkit@latest spool > /dev/null 2>&1 &` if `bun` missing.) Heartbeat fresh → YOU dispatch `instruction` — first turn body `{"prompt":"<user request>"}` so orient_nouns and recall_hits derive from the request; subsequent turns may use empty body. Read the response file directly with the Read tool. Never poll the spool dir with `sleep && ls` or `Start-Sleep && Test-Path` — plugkit is synchronous from your view; if the response is not there, the watcher is dead (check `.status.json` mtime) or the verb is slow (check `.watcher.log`), not "still processing."
|
|
22
22
|
|
|
23
23
|
Response body is not a mutation surface. Memory writes route through `memorize-fire` only — another verb YOU dispatch.
|
|
24
|
+
|
|
25
|
+
On turn entry (first `instruction` dispatch after a >30s idle gap or session-start), plugkit attaches an `auto_recall` pack to your `instruction` response: `{query, hits, fired_at, turn_entry: true}`. The query is derived from `.gm/last-prompt.txt` / `.gm/turn-state.json`; hits are the top recall results plugkit pulled before serving your instruction. Read `auto_recall.hits` alongside the existing `recall_hits` (which is the phase+PRD-subject pack) — both surface prior memory, but `auto_recall` is the per-turn user-prompt pack and only fires on turn entry. Subsequent `instruction` dispatches in the same turn carry no `auto_recall` field (or carry the same pack from the turn-start fire); do not re-trigger it manually.
|