gm-skill 2.0.1116 → 2.0.1117

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
@@ -28,7 +28,7 @@ npx gm-skill-bootstrap
28
28
 
29
29
  ## Version
30
30
 
31
- `2.0.1116` — auto-bumped from the canonical `gm` repo. Every push to `AnEntrypoint/gm` republishes this package alongside all 15 platform packages.
31
+ `2.0.1117` — 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
- } catch (_) {}
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
- } catch (_) {}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm",
3
- "version": "2.0.1116",
3
+ "version": "2.0.1117",
4
4
  "description": "Spool-dispatch orchestration engine with unified state machine, skills, and automated git enforcement",
5
5
  "author": "AnEntrypoint",
6
6
  "license": "MIT",
@@ -232,6 +232,24 @@ async function verifyBinaryHealth(filePath) {
232
232
  }
233
233
  }
234
234
 
235
+ function openWatcherLog(projectDir) {
236
+ const spoolDir = path.join(projectDir, '.gm', 'exec-spool');
237
+ fs.mkdirSync(spoolDir, { recursive: true });
238
+ const logPath = path.join(spoolDir, '.watcher.log');
239
+ try {
240
+ const stat = fs.statSync(logPath);
241
+ if (stat.size > 10 * 1024 * 1024) {
242
+ const rotated = path.join(spoolDir, '.watcher.log.1');
243
+ try { fs.unlinkSync(rotated); } catch (_) {}
244
+ fs.renameSync(logPath, rotated);
245
+ }
246
+ } catch (_) {}
247
+ const fd = fs.openSync(logPath, 'a');
248
+ const header = `\n--- watcher boot ${new Date().toISOString()} pid=${process.pid} ---\n`;
249
+ try { fs.writeSync(fd, header); } catch (_) {}
250
+ return fd;
251
+ }
252
+
235
253
  async function spawnPlugkitWatcher(wasmPath) {
236
254
  try {
237
255
  emitBootstrapEvent('info', 'Spawning plugkit WASM watcher daemon');
@@ -249,18 +267,23 @@ async function spawnPlugkitWatcher(wasmPath) {
249
267
  throw new Error(`WASM wrapper not found at ${wrapperPath}`);
250
268
  }
251
269
 
270
+ const projectDir = process.cwd();
271
+ const logFd = openWatcherLog(projectDir);
272
+
252
273
  const runtime = process.platform === 'win32' ? 'bun.exe' : 'bun';
253
274
  const proc = spawn(runtime, [wrapperPath, 'spool'], {
254
275
  detached: true,
255
- stdio: 'ignore',
276
+ stdio: ['ignore', logFd, logFd],
256
277
  windowsHide: true,
257
- env: { ...process.env, CLAUDE_PROJECT_DIR: process.cwd() },
278
+ env: { ...process.env, CLAUDE_PROJECT_DIR: projectDir },
258
279
  });
259
280
 
281
+ try { fs.closeSync(logFd); } catch (_) {}
282
+
260
283
  const pid = proc.pid;
261
284
  proc.unref();
262
285
 
263
- emitBootstrapEvent('info', 'Plugkit WASM watcher spawned', { pid });
286
+ emitBootstrapEvent('info', 'Plugkit WASM watcher spawned', { pid, logPath: path.join(projectDir, '.gm', 'exec-spool', '.watcher.log') });
264
287
  return pid;
265
288
  } catch (e) {
266
289
  emitBootstrapEvent('error', 'Failed to spawn plugkit WASM watcher', { error: e.message });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-skill",
3
- "version": "2.0.1116",
3
+ "version": "2.0.1117",
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.1116"
42
+ "gm-plugkit": "^2.0.1117"
43
43
  },
44
44
  "engines": {
45
45
  "node": ">=16.0.0"
@@ -1,32 +1,86 @@
1
1
  ---
2
2
  name: gm-skill
3
- description: AI-native software engineering harness. plugkit serves all instructions, state, guardrails via the spool.
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, run:
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 with dispatch.
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` for nested verbs, `.gm/exec-spool/out/<N>.json` for root verbs.
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` (empty body for current phase; `phase=<NAME>` line, `{"phase":"<NAME>"}`, or a raw phase name to override). The response carries `{phase, instruction, mutables_pending, prd_pending_count, next_phase_hint}`. Follow the `instruction` prose imperatively it is the operative guidance for this phase. Resolve every `mutables_pending` entry through `mutable-resolve` before transitioning; the gate will refuse otherwise. When the phase's exit condition is met, dispatch `transition` (body: a phase name from `EXECUTE`/`EMIT`/`VERIFY`/`COMPLETE`, or empty to auto-advance), then re-enter with the new phase. Stop when `next_phase_hint` is null or phase is `COMPLETE`.
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. Add `.plugkit-browser-profile/` to your repo's `.gitignore` — the wrapper does this automatically.
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`.