gm-skill 2.0.1215 → 2.0.1217

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.1215` — auto-bumped from the canonical `gm` repo. Every push to `AnEntrypoint/gm` (or any cascading sibling crate) republishes this package.
38
+ `2.0.1217` — 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
 
@@ -100,7 +100,7 @@ process.stdin.on('end', () => {
100
100
  sub: 'hook',
101
101
  event: 'deviation.spool-poll',
102
102
  pid: process.pid,
103
- sess: process.env.CLAUDE_SESSION_ID || process.env.GM_SESSION_ID || '',
103
+ sess: event.session_id || process.env.CLAUDE_SESSION_ID || process.env.GM_SESSION_ID || '',
104
104
  cwd: process.cwd(),
105
105
  operation: 'bash',
106
106
  pattern,
@@ -124,11 +124,35 @@ function ensureSpoolPollGate(cwd) {
124
124
  const gateScript = path.join(gmHooks, 'spool-poll-gate.js');
125
125
  const want = spoolPollGateScript();
126
126
  let need = true;
127
+ let oldSha = '';
128
+ let existed = false;
127
129
  try {
128
130
  const existing = fs.readFileSync(gateScript, 'utf8');
131
+ existed = true;
129
132
  if (existing === want) need = false;
133
+ else {
134
+ try {
135
+ const _crypto = require('crypto');
136
+ oldSha = _crypto.createHash('sha256').update(existing).digest('hex').slice(0, 12);
137
+ } catch (_) {}
138
+ }
130
139
  } catch (_) {}
131
- if (need) fs.writeFileSync(gateScript, want);
140
+ if (need) {
141
+ fs.writeFileSync(gateScript, want);
142
+ try {
143
+ const _crypto = require('crypto');
144
+ const newSha = _crypto.createHash('sha256').update(want).digest('hex').slice(0, 12);
145
+ try {
146
+ logEvent('bootstrap', existed ? 'gate.refreshed' : 'gate.installed', {
147
+ cwd,
148
+ path: gateScript,
149
+ old_sha: oldSha || null,
150
+ new_sha: newSha,
151
+ bytes: Buffer.byteLength(want, 'utf8'),
152
+ });
153
+ } catch (_) {}
154
+ } catch (_) {}
155
+ }
132
156
 
133
157
  const claudeDir = path.join(cwd, '.claude');
134
158
  fs.mkdirSync(claudeDir, { recursive: true });
@@ -172,6 +196,99 @@ function applyDisciplineSigil(rawBody) {
172
196
  return JSON.stringify(parsed);
173
197
  }
174
198
 
199
+ function isInstructionTurnStart(sess) {
200
+ const key = sess || '(no-session)';
201
+ const now = Date.now();
202
+ const t = _turns.get(key);
203
+ if (!t) return true;
204
+ if ((now - t.lastTs) > TURN_IDLE_MS) return true;
205
+ return false;
206
+ }
207
+
208
+ function readUserPromptForRecall(cwd) {
209
+ const root = cwd || process.cwd();
210
+ try {
211
+ const p = path.join(root, '.gm', 'last-prompt.txt');
212
+ const txt = fs.readFileSync(p, 'utf8').trim();
213
+ if (txt) return txt;
214
+ } catch (_) {}
215
+ try {
216
+ const p = path.join(root, '.gm', 'turn-state.json');
217
+ const obj = JSON.parse(fs.readFileSync(p, 'utf8'));
218
+ if (obj && typeof obj.last_prompt === 'string' && obj.last_prompt.trim()) return obj.last_prompt.trim();
219
+ if (obj && typeof obj.prompt === 'string' && obj.prompt.trim()) return obj.prompt.trim();
220
+ } catch (_) {}
221
+ return '';
222
+ }
223
+
224
+ function dispatchVerbToWasmInternal(instance, verb, body) {
225
+ const dispatch = instance.exports.dispatch_verb;
226
+ if (!dispatch) return null;
227
+ const verbBytes = new TextEncoder().encode(verb);
228
+ const bodyBytes = new TextEncoder().encode(body || '');
229
+ const verbPtr = instance.exports.plugkit_alloc(verbBytes.length);
230
+ const bodyPtr = instance.exports.plugkit_alloc(bodyBytes.length);
231
+ try {
232
+ new Uint8Array(instance.exports.memory.buffer, verbPtr, verbBytes.length).set(verbBytes);
233
+ new Uint8Array(instance.exports.memory.buffer, bodyPtr, bodyBytes.length).set(bodyBytes);
234
+ const result = dispatch(verbPtr, verbBytes.length, bodyPtr, bodyBytes.length);
235
+ const ptr = Number(result & 0xffffffffn);
236
+ const len = Number(result >> 32n);
237
+ const out = new TextDecoder().decode(new Uint8Array(instance.exports.memory.buffer, ptr, len));
238
+ try { instance.exports.plugkit_free(ptr, len); } catch (_) {}
239
+ return out;
240
+ } finally {
241
+ try { instance.exports.plugkit_free(verbPtr, verbBytes.length); } catch (_) {}
242
+ try { instance.exports.plugkit_free(bodyPtr, bodyBytes.length); } catch (_) {}
243
+ }
244
+ }
245
+
246
+ function tryAutoRecallForTurnEntry(instance, sess, cwd) {
247
+ try {
248
+ const prompt = readUserPromptForRecall(cwd);
249
+ if (!prompt) return null;
250
+ const out = dispatchVerbToWasmInternal(instance, 'auto-recall', prompt);
251
+ if (!out) return null;
252
+ let parsed;
253
+ try { parsed = JSON.parse(out); } catch (_) { return null; }
254
+ if (!parsed || parsed.ok !== true) return null;
255
+ let inner = parsed.data;
256
+ if (typeof parsed.stdout === 'string' && parsed.stdout.length > 0) {
257
+ try { inner = JSON.parse(parsed.stdout); } catch (_) {}
258
+ }
259
+ if (!inner || typeof inner !== 'object') return null;
260
+ const hits = Array.isArray(inner.results) ? inner.results : (Array.isArray(inner.hits) ? inner.hits : []);
261
+ const payload = { query: inner.query || '', hits, fired_at: new Date().toISOString(), turn_entry: true };
262
+ logEvent('plugkit', 'auto_recall.turn-entry', { sess, query: payload.query, count: hits.length });
263
+ return payload;
264
+ } catch (e) {
265
+ logEvent('plugkit', 'auto_recall.error', { sess, error: String(e && e.message || e) });
266
+ return null;
267
+ }
268
+ }
269
+
270
+ function mergeAutoRecallIntoInstructionResponse(resultStr, autoRecall) {
271
+ if (!autoRecall) return resultStr;
272
+ let parsed;
273
+ try { parsed = JSON.parse(resultStr); } catch (_) { return resultStr; }
274
+ if (!parsed || typeof parsed !== 'object') return resultStr;
275
+ if (parsed.data && typeof parsed.data === 'object') {
276
+ parsed.data.auto_recall = autoRecall;
277
+ } else {
278
+ parsed.auto_recall = autoRecall;
279
+ }
280
+ if (typeof parsed.stdout === 'string' && parsed.stdout.length > 0) {
281
+ try {
282
+ const inner = JSON.parse(parsed.stdout);
283
+ if (inner && typeof inner === 'object') {
284
+ inner.auto_recall = autoRecall;
285
+ parsed.stdout = JSON.stringify(inner);
286
+ }
287
+ } catch (_) {}
288
+ }
289
+ return JSON.stringify(parsed);
290
+ }
291
+
175
292
  function turnTick(sess, verb, taskBase, phase) {
176
293
  const key = sess || '(no-session)';
177
294
  const now = Date.now();
@@ -256,6 +373,13 @@ function emitOrchestratorEvents(verb, taskBase, resultStr) {
256
373
  let parsed;
257
374
  try { parsed = JSON.parse(resultStr); } catch (_) { return; }
258
375
  if (!parsed || parsed.ok !== true) {
376
+ let errData = null;
377
+ if (parsed && typeof parsed.stdout === 'string' && parsed.stdout.length > 0) {
378
+ try { errData = JSON.parse(parsed.stdout); } catch (_) {}
379
+ }
380
+ if (verb === 'prd-resolve' && errData && errData.deviation_kind === 'prd-resolve-unknown-id') {
381
+ logEvent('hook', 'deviation.prd-resolve-unknown-id', { task: taskBase, prd_id: errData.prd_id, reason: errData.error });
382
+ }
259
383
  logEvent('plugkit', 'orchestrator.error', { verb, task: taskBase, error: parsed && parsed.error ? String(parsed.error) : 'unknown' });
260
384
  return;
261
385
  }
@@ -276,7 +400,11 @@ function emitOrchestratorEvents(verb, taskBase, resultStr) {
276
400
  logEvent('plugkit', 'prd.added', { task: taskBase, id: data.added });
277
401
  break;
278
402
  case 'prd-resolve':
279
- logEvent('plugkit', 'prd.resolved', { task: taskBase, id: data.resolved });
403
+ if (data && data.deviation_kind === 'prd-resolve-unknown-id') {
404
+ logEvent('hook', 'deviation.prd-resolve-unknown-id', { task: taskBase, prd_id: data.prd_id, reason: data.error });
405
+ } else {
406
+ logEvent('plugkit', 'prd.resolved', { task: taskBase, id: data.resolved });
407
+ }
280
408
  break;
281
409
  case 'mutable-add':
282
410
  logEvent('plugkit', 'mutable.added', { task: taskBase, id: data.added });
@@ -1192,6 +1320,8 @@ async function runSpoolWatcher(instance, spoolDir) {
1192
1320
  fs.mkdirSync(inDir, { recursive: true });
1193
1321
  fs.mkdirSync(outDir, { recursive: true });
1194
1322
 
1323
+ try { ensureSpoolPollGate(process.env.CLAUDE_PROJECT_DIR || process.cwd()); } catch (_) {}
1324
+
1195
1325
  const LOCK_PATH = path.join(spoolDir, '.watcher.lock');
1196
1326
  let _ownWrapperSha12 = '';
1197
1327
  try {
@@ -1495,18 +1625,35 @@ async function runSpoolWatcher(instance, spoolDir) {
1495
1625
  prior_status: _priorStatus,
1496
1626
  prior_status_age_ms: _priorStatus && Number.isFinite(_priorStatus.ts) ? Date.now() - _priorStatus.ts : null,
1497
1627
  };
1498
- const _isPlannedBoot = _priorShutdown && (_priorShutdown.reason === 'idle' || _priorShutdown.reason === 'sigterm' || _priorShutdown.reason === 'version-change');
1628
+ const _PLANNED_REASONS = new Set(['idle', 'sigterm', 'version-change', 'wrapper-change', 'peer-stale-takeover', 'version-drift', 'external-planned']);
1629
+ const _isPlannedBoot = _priorShutdown && _PLANNED_REASONS.has(_priorShutdown.reason);
1499
1630
  const _isFirstBoot = !_priorShutdown && !_priorStatus;
1500
1631
  const UNPLANNED_RESTART_MARKER = path.join(spoolDir, '.unplanned-restart.json');
1501
- if (!_isPlannedBoot && !_isFirstBoot) {
1632
+ const HEARTBEAT_RECENT_MS = 60_000;
1633
+ const HEARTBEAT_DEAD_MS = 5 * 60_000;
1634
+ let _severity = 'critical';
1635
+ if (_isPlannedBoot) {
1636
+ _severity = 'info';
1637
+ } else if (!_priorShutdown && _priorStatus && Number.isFinite(_priorStatus.ts)) {
1638
+ const _statusAge = Date.now() - _priorStatus.ts;
1639
+ if (_statusAge <= HEARTBEAT_RECENT_MS) _severity = 'warn';
1640
+ else if (_statusAge < HEARTBEAT_DEAD_MS) _severity = 'warn';
1641
+ else _severity = 'critical';
1642
+ }
1643
+ if (!_isFirstBoot) {
1502
1644
  const incidentPayload = {
1503
1645
  ts: Date.now(),
1504
1646
  version: _bootVersion,
1505
- severity: 'critical',
1647
+ severity: _severity,
1648
+ planned: _isPlannedBoot,
1506
1649
  ...restartContext,
1507
1650
  log_tail_path: path.join(spoolDir, '.watcher.log'),
1508
1651
  gm_log_dir: GM_LOG_ROOT,
1509
- 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.',
1652
+ instruction: _isPlannedBoot
1653
+ ? `Planned restart: prior watcher exited with reason="${_priorShutdown.reason}". No action required.`
1654
+ : (_severity === 'warn'
1655
+ ? 'Prior watcher disappeared with a recent heartbeat — likely a clean shutdown that did not write .shutdown-reason.json. Inspect .watcher.log if recurrent.'
1656
+ : '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.'),
1510
1657
  };
1511
1658
  logEvent('plugkit', 'watcher.unplanned-restart', incidentPayload);
1512
1659
  try {
@@ -1564,6 +1711,14 @@ async function runSpoolWatcher(instance, spoolDir) {
1564
1711
  console.log(`[dispatch] → verb=${verb} task=${taskBase} body=${bodyBytes.length}b`);
1565
1712
  logEvent('plugkit', 'dispatch.start', { verb, task: taskBase, body_bytes: bodyBytes.length, cwd: process.cwd() });
1566
1713
 
1714
+ let autoRecallPayload = null;
1715
+ if (verb === 'instruction') {
1716
+ const sessForRecall = readCurrentSess();
1717
+ if (isInstructionTurnStart(sessForRecall)) {
1718
+ autoRecallPayload = tryAutoRecallForTurnEntry(instance, sessForRecall, process.cwd());
1719
+ }
1720
+ }
1721
+
1567
1722
  const verbPtr = instance.exports.plugkit_alloc(verbBytes.length);
1568
1723
  const bodyPtr = instance.exports.plugkit_alloc(bodyBytes.length);
1569
1724
  new Uint8Array(instance.exports.memory.buffer, verbPtr, verbBytes.length).set(verbBytes);
@@ -1574,7 +1729,11 @@ async function runSpoolWatcher(instance, spoolDir) {
1574
1729
  const ptr = Number(result & 0xffffffffn);
1575
1730
  const len = Number(result >> 32n);
1576
1731
  const resultBytes = new Uint8Array(instance.exports.memory.buffer, ptr, len);
1577
- const resultStr = new TextDecoder().decode(resultBytes);
1732
+ let resultStr = new TextDecoder().decode(resultBytes);
1733
+
1734
+ if (autoRecallPayload) {
1735
+ resultStr = mergeAutoRecallIntoInstructionResponse(resultStr, autoRecallPayload);
1736
+ }
1578
1737
 
1579
1738
  const outName = dir === '.' ? `${taskBase}.json` : `${verb}-${taskBase}.json`;
1580
1739
  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.1215",
3
+ "version": "2.0.1217",
4
4
  "description": "Spool-dispatch orchestration engine with unified state machine, skills, and automated git enforcement",
5
5
  "author": "AnEntrypoint",
6
6
  "license": "MIT",
@@ -393,7 +393,7 @@ process.stdin.on('end', () => {
393
393
  sub: 'hook',
394
394
  event: 'deviation.spool-poll',
395
395
  pid: process.pid,
396
- sess: process.env.CLAUDE_SESSION_ID || process.env.GM_SESSION_ID || '',
396
+ sess: event.session_id || process.env.CLAUDE_SESSION_ID || process.env.GM_SESSION_ID || '',
397
397
  cwd: process.cwd(),
398
398
  operation: 'bash',
399
399
  pattern,
@@ -11,14 +11,21 @@ function logDeviation(event, fields) {
11
11
  const day = new Date().toISOString().slice(0, 10);
12
12
  const dir = path.join(GM_LOG_ROOT, day);
13
13
  fs.mkdirSync(dir, { recursive: true });
14
+ const f = fields || {};
15
+ const sessOverride = (f.sess !== undefined) ? f.sess : null;
16
+ const rest = { ...f };
17
+ delete rest.sess;
18
+ const sess = (sessOverride && String(sessOverride).length > 0)
19
+ ? String(sessOverride)
20
+ : (process.env.CLAUDE_SESSION_ID || process.env.GM_SESSION_ID || '');
14
21
  const line = JSON.stringify({
15
22
  ts: new Date().toISOString(),
16
23
  sub: 'hook',
17
24
  event,
18
25
  pid: process.pid,
19
- sess: process.env.CLAUDE_SESSION_ID || process.env.GM_SESSION_ID || '',
26
+ sess,
20
27
  cwd: process.cwd(),
21
- ...fields,
28
+ ...rest,
22
29
  });
23
30
  fs.appendFileSync(path.join(dir, 'hook.jsonl'), line + '\n');
24
31
  } catch (_) {}
@@ -25,6 +25,7 @@ process.stdin.on('end', () => {
25
25
  pattern,
26
26
  command_excerpt: String(command).slice(0, 200),
27
27
  via: 'pre-tool-use-hook',
28
+ sess: event.session_id || process.env.CLAUDE_SESSION_ID || process.env.GM_SESSION_ID || '',
28
29
  });
29
30
  } catch (_) {}
30
31
  process.stdout.write(JSON.stringify({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-skill",
3
- "version": "2.0.1215",
3
+ "version": "2.0.1217",
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.1215"
42
+ "gm-plugkit": "^2.0.1217"
43
43
  },
44
44
  "engines": {
45
45
  "node": ">=16.0.0"