gm-skill 2.0.1216 → 2.0.1218

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 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.1216` — auto-bumped from the canonical `gm` repo. Every push to `AnEntrypoint/gm` (or any cascading sibling crate) republishes this package.
38
+ `2.0.1218` — 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
 
@@ -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);
@@ -124,11 +161,35 @@ function ensureSpoolPollGate(cwd) {
124
161
  const gateScript = path.join(gmHooks, 'spool-poll-gate.js');
125
162
  const want = spoolPollGateScript();
126
163
  let need = true;
164
+ let oldSha = '';
165
+ let existed = false;
127
166
  try {
128
167
  const existing = fs.readFileSync(gateScript, 'utf8');
168
+ existed = true;
129
169
  if (existing === want) need = false;
170
+ else {
171
+ try {
172
+ const _crypto = require('crypto');
173
+ oldSha = _crypto.createHash('sha256').update(existing).digest('hex').slice(0, 12);
174
+ } catch (_) {}
175
+ }
130
176
  } catch (_) {}
131
- if (need) fs.writeFileSync(gateScript, want);
177
+ if (need) {
178
+ fs.writeFileSync(gateScript, want);
179
+ try {
180
+ const _crypto = require('crypto');
181
+ const newSha = _crypto.createHash('sha256').update(want).digest('hex').slice(0, 12);
182
+ try {
183
+ logEvent('bootstrap', existed ? 'gate.refreshed' : 'gate.installed', {
184
+ cwd,
185
+ path: gateScript,
186
+ old_sha: oldSha || null,
187
+ new_sha: newSha,
188
+ bytes: Buffer.byteLength(want, 'utf8'),
189
+ });
190
+ } catch (_) {}
191
+ } catch (_) {}
192
+ }
132
193
 
133
194
  const claudeDir = path.join(cwd, '.claude');
134
195
  fs.mkdirSync(claudeDir, { recursive: true });
@@ -141,15 +202,17 @@ function ensureSpoolPollGate(cwd) {
141
202
  if (!settings.hooks || typeof settings.hooks !== 'object') settings.hooks = {};
142
203
  if (!Array.isArray(settings.hooks.PreToolUse)) settings.hooks.PreToolUse = [];
143
204
  const wantCommand = `node "\${CLAUDE_PROJECT_DIR}/.gm/hooks/spool-poll-gate.js"`;
144
- let bashEntry = settings.hooks.PreToolUse.find(e => e && e.matcher === 'Bash');
145
- if (!bashEntry) {
146
- bashEntry = { matcher: 'Bash', hooks: [] };
147
- settings.hooks.PreToolUse.push(bashEntry);
148
- }
149
- if (!Array.isArray(bashEntry.hooks)) bashEntry.hooks = [];
150
- const already = bashEntry.hooks.some(h => h && typeof h.command === 'string' && h.command.includes('spool-poll-gate.js'));
151
- if (!already) {
152
- bashEntry.hooks.push({ type: 'command', command: wantCommand });
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
+ }
153
216
  }
154
217
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
155
218
  } catch (_) {}
@@ -172,6 +235,99 @@ function applyDisciplineSigil(rawBody) {
172
235
  return JSON.stringify(parsed);
173
236
  }
174
237
 
238
+ function isInstructionTurnStart(sess) {
239
+ const key = sess || '(no-session)';
240
+ const now = Date.now();
241
+ const t = _turns.get(key);
242
+ if (!t) return true;
243
+ if ((now - t.lastTs) > TURN_IDLE_MS) return true;
244
+ return false;
245
+ }
246
+
247
+ function readUserPromptForRecall(cwd) {
248
+ const root = cwd || process.cwd();
249
+ try {
250
+ const p = path.join(root, '.gm', 'last-prompt.txt');
251
+ const txt = fs.readFileSync(p, 'utf8').trim();
252
+ if (txt) return txt;
253
+ } catch (_) {}
254
+ try {
255
+ const p = path.join(root, '.gm', 'turn-state.json');
256
+ const obj = JSON.parse(fs.readFileSync(p, 'utf8'));
257
+ if (obj && typeof obj.last_prompt === 'string' && obj.last_prompt.trim()) return obj.last_prompt.trim();
258
+ if (obj && typeof obj.prompt === 'string' && obj.prompt.trim()) return obj.prompt.trim();
259
+ } catch (_) {}
260
+ return '';
261
+ }
262
+
263
+ function dispatchVerbToWasmInternal(instance, verb, body) {
264
+ const dispatch = instance.exports.dispatch_verb;
265
+ if (!dispatch) return null;
266
+ const verbBytes = new TextEncoder().encode(verb);
267
+ const bodyBytes = new TextEncoder().encode(body || '');
268
+ const verbPtr = instance.exports.plugkit_alloc(verbBytes.length);
269
+ const bodyPtr = instance.exports.plugkit_alloc(bodyBytes.length);
270
+ try {
271
+ new Uint8Array(instance.exports.memory.buffer, verbPtr, verbBytes.length).set(verbBytes);
272
+ new Uint8Array(instance.exports.memory.buffer, bodyPtr, bodyBytes.length).set(bodyBytes);
273
+ const result = dispatch(verbPtr, verbBytes.length, bodyPtr, bodyBytes.length);
274
+ const ptr = Number(result & 0xffffffffn);
275
+ const len = Number(result >> 32n);
276
+ const out = new TextDecoder().decode(new Uint8Array(instance.exports.memory.buffer, ptr, len));
277
+ try { instance.exports.plugkit_free(ptr, len); } catch (_) {}
278
+ return out;
279
+ } finally {
280
+ try { instance.exports.plugkit_free(verbPtr, verbBytes.length); } catch (_) {}
281
+ try { instance.exports.plugkit_free(bodyPtr, bodyBytes.length); } catch (_) {}
282
+ }
283
+ }
284
+
285
+ function tryAutoRecallForTurnEntry(instance, sess, cwd) {
286
+ try {
287
+ const prompt = readUserPromptForRecall(cwd);
288
+ if (!prompt) return null;
289
+ const out = dispatchVerbToWasmInternal(instance, 'auto-recall', prompt);
290
+ if (!out) return null;
291
+ let parsed;
292
+ try { parsed = JSON.parse(out); } catch (_) { return null; }
293
+ if (!parsed || parsed.ok !== true) return null;
294
+ let inner = parsed.data;
295
+ if (typeof parsed.stdout === 'string' && parsed.stdout.length > 0) {
296
+ try { inner = JSON.parse(parsed.stdout); } catch (_) {}
297
+ }
298
+ if (!inner || typeof inner !== 'object') return null;
299
+ const hits = Array.isArray(inner.results) ? inner.results : (Array.isArray(inner.hits) ? inner.hits : []);
300
+ const payload = { query: inner.query || '', hits, fired_at: new Date().toISOString(), turn_entry: true };
301
+ logEvent('plugkit', 'auto_recall.turn-entry', { sess, query: payload.query, count: hits.length });
302
+ return payload;
303
+ } catch (e) {
304
+ logEvent('plugkit', 'auto_recall.error', { sess, error: String(e && e.message || e) });
305
+ return null;
306
+ }
307
+ }
308
+
309
+ function mergeAutoRecallIntoInstructionResponse(resultStr, autoRecall) {
310
+ if (!autoRecall) return resultStr;
311
+ let parsed;
312
+ try { parsed = JSON.parse(resultStr); } catch (_) { return resultStr; }
313
+ if (!parsed || typeof parsed !== 'object') return resultStr;
314
+ if (parsed.data && typeof parsed.data === 'object') {
315
+ parsed.data.auto_recall = autoRecall;
316
+ } else {
317
+ parsed.auto_recall = autoRecall;
318
+ }
319
+ if (typeof parsed.stdout === 'string' && parsed.stdout.length > 0) {
320
+ try {
321
+ const inner = JSON.parse(parsed.stdout);
322
+ if (inner && typeof inner === 'object') {
323
+ inner.auto_recall = autoRecall;
324
+ parsed.stdout = JSON.stringify(inner);
325
+ }
326
+ } catch (_) {}
327
+ }
328
+ return JSON.stringify(parsed);
329
+ }
330
+
175
331
  function turnTick(sess, verb, taskBase, phase) {
176
332
  const key = sess || '(no-session)';
177
333
  const now = Date.now();
@@ -1203,6 +1359,8 @@ async function runSpoolWatcher(instance, spoolDir) {
1203
1359
  fs.mkdirSync(inDir, { recursive: true });
1204
1360
  fs.mkdirSync(outDir, { recursive: true });
1205
1361
 
1362
+ try { ensureSpoolPollGate(process.env.CLAUDE_PROJECT_DIR || process.cwd()); } catch (_) {}
1363
+
1206
1364
  const LOCK_PATH = path.join(spoolDir, '.watcher.lock');
1207
1365
  let _ownWrapperSha12 = '';
1208
1366
  try {
@@ -1506,18 +1664,35 @@ async function runSpoolWatcher(instance, spoolDir) {
1506
1664
  prior_status: _priorStatus,
1507
1665
  prior_status_age_ms: _priorStatus && Number.isFinite(_priorStatus.ts) ? Date.now() - _priorStatus.ts : null,
1508
1666
  };
1509
- const _isPlannedBoot = _priorShutdown && (_priorShutdown.reason === 'idle' || _priorShutdown.reason === 'sigterm' || _priorShutdown.reason === 'version-change');
1667
+ const _PLANNED_REASONS = new Set(['idle', 'sigterm', 'version-change', 'wrapper-change', 'peer-stale-takeover', 'version-drift', 'external-planned']);
1668
+ const _isPlannedBoot = _priorShutdown && _PLANNED_REASONS.has(_priorShutdown.reason);
1510
1669
  const _isFirstBoot = !_priorShutdown && !_priorStatus;
1511
1670
  const UNPLANNED_RESTART_MARKER = path.join(spoolDir, '.unplanned-restart.json');
1512
- if (!_isPlannedBoot && !_isFirstBoot) {
1671
+ const HEARTBEAT_RECENT_MS = 60_000;
1672
+ const HEARTBEAT_DEAD_MS = 5 * 60_000;
1673
+ let _severity = 'critical';
1674
+ if (_isPlannedBoot) {
1675
+ _severity = 'info';
1676
+ } else if (!_priorShutdown && _priorStatus && Number.isFinite(_priorStatus.ts)) {
1677
+ const _statusAge = Date.now() - _priorStatus.ts;
1678
+ if (_statusAge <= HEARTBEAT_RECENT_MS) _severity = 'warn';
1679
+ else if (_statusAge < HEARTBEAT_DEAD_MS) _severity = 'warn';
1680
+ else _severity = 'critical';
1681
+ }
1682
+ if (!_isFirstBoot) {
1513
1683
  const incidentPayload = {
1514
1684
  ts: Date.now(),
1515
1685
  version: _bootVersion,
1516
- severity: 'critical',
1686
+ severity: _severity,
1687
+ planned: _isPlannedBoot,
1517
1688
  ...restartContext,
1518
1689
  log_tail_path: path.join(spoolDir, '.watcher.log'),
1519
1690
  gm_log_dir: GM_LOG_ROOT,
1520
- instruction: 'Prior watcher died without a planned shutdown. This is treated as a critical failure. Inspect .watcher.log and gm-log/<day>/plugkit.jsonl events supervisor.watcher-exited-unexpectedly + supervisor.heartbeat-stale around the prior_status.ts timestamp to diagnose root cause.',
1691
+ instruction: _isPlannedBoot
1692
+ ? `Planned restart: prior watcher exited with reason="${_priorShutdown.reason}". No action required.`
1693
+ : (_severity === 'warn'
1694
+ ? 'Prior watcher disappeared with a recent heartbeat — likely a clean shutdown that did not write .shutdown-reason.json. Inspect .watcher.log if recurrent.'
1695
+ : 'Prior watcher died without a planned shutdown and without a recent heartbeat. This is treated as a critical failure. Inspect .watcher.log and gm-log/<day>/plugkit.jsonl events supervisor.watcher-exited-unexpectedly + supervisor.heartbeat-stale around the prior_status.ts timestamp to diagnose root cause.'),
1521
1696
  };
1522
1697
  logEvent('plugkit', 'watcher.unplanned-restart', incidentPayload);
1523
1698
  try {
@@ -1575,6 +1750,14 @@ async function runSpoolWatcher(instance, spoolDir) {
1575
1750
  console.log(`[dispatch] → verb=${verb} task=${taskBase} body=${bodyBytes.length}b`);
1576
1751
  logEvent('plugkit', 'dispatch.start', { verb, task: taskBase, body_bytes: bodyBytes.length, cwd: process.cwd() });
1577
1752
 
1753
+ let autoRecallPayload = null;
1754
+ if (verb === 'instruction') {
1755
+ const sessForRecall = readCurrentSess();
1756
+ if (isInstructionTurnStart(sessForRecall)) {
1757
+ autoRecallPayload = tryAutoRecallForTurnEntry(instance, sessForRecall, process.cwd());
1758
+ }
1759
+ }
1760
+
1578
1761
  const verbPtr = instance.exports.plugkit_alloc(verbBytes.length);
1579
1762
  const bodyPtr = instance.exports.plugkit_alloc(bodyBytes.length);
1580
1763
  new Uint8Array(instance.exports.memory.buffer, verbPtr, verbBytes.length).set(verbBytes);
@@ -1585,7 +1768,11 @@ async function runSpoolWatcher(instance, spoolDir) {
1585
1768
  const ptr = Number(result & 0xffffffffn);
1586
1769
  const len = Number(result >> 32n);
1587
1770
  const resultBytes = new Uint8Array(instance.exports.memory.buffer, ptr, len);
1588
- const resultStr = new TextDecoder().decode(resultBytes);
1771
+ let resultStr = new TextDecoder().decode(resultBytes);
1772
+
1773
+ if (autoRecallPayload) {
1774
+ resultStr = mergeAutoRecallIntoInstructionResponse(resultStr, autoRecallPayload);
1775
+ }
1589
1776
 
1590
1777
  const outName = dir === '.' ? `${taskBase}.json` : `${verb}-${taskBase}.json`;
1591
1778
  fs.writeFileSync(path.join(outDir, outName), resultStr);
package/gm.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm",
3
- "version": "2.0.1216",
3
+ "version": "2.0.1218",
4
4
  "description": "Spool-dispatch orchestration engine with unified state machine, skills, and automated git enforcement",
5
5
  "author": "AnEntrypoint",
6
6
  "license": "MIT",
@@ -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) fs.writeFileSync(gateScript, want);
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 bashEntry = settings.hooks.PreToolUse.find(e => e && e.matcher === 'Bash');
438
- if (!bashEntry) {
439
- bashEntry = { matcher: 'Bash', hooks: [] };
440
- settings.hooks.PreToolUse.push(bashEntry);
441
- }
442
- if (!Array.isArray(bashEntry.hooks)) bashEntry.hooks = [];
443
- const already = bashEntry.hooks.some(h => h && typeof h.command === 'string' && h.command.includes('spool-poll-gate.js'));
444
- if (!already) {
445
- bashEntry.hooks.push({ type: 'command', command: wantCommand });
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: already });
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-skill",
3
- "version": "2.0.1216",
3
+ "version": "2.0.1218",
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.1216"
42
+ "gm-plugkit": "^2.0.1218"
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.
@@ -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.