gm-skill 2.0.1116 → 2.0.1118

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.1118` — 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.1118",
4
4
  "description": "Spool-dispatch orchestration engine with unified state machine, skills, and automated git enforcement",
5
5
  "author": "AnEntrypoint",
6
6
  "license": "MIT",
@@ -10,6 +10,9 @@ const PLUGKIT_TOOLS_DIR = path.join(os.homedir(), '.claude', 'gm-tools');
10
10
  const PLUGKIT_VERSION_FILE = path.join(PLUGKIT_TOOLS_DIR, 'plugkit.version');
11
11
  const PLUGKIT_WASM_PATH = path.join(PLUGKIT_TOOLS_DIR, 'plugkit.wasm');
12
12
  const PLUGKIT_WASM_WRAPPER = path.join(PLUGKIT_TOOLS_DIR, 'plugkit-wasm-wrapper.js');
13
+ const PLUGKIT_LATEST_CACHE = path.join(PLUGKIT_TOOLS_DIR, 'plugkit-latest.json');
14
+ const PLUGKIT_LATEST_TTL_MS = 60 * 60 * 1000;
15
+ const NPM_PACKAGE = '@anentrypoint/plugkit-wasm';
13
16
  const BOOTSTRAP_STATUS_FILE = path.join(os.homedir(), '.gm', 'bootstrap-status.json');
14
17
  const BOOTSTRAP_ERROR_FILE = path.join(os.homedir(), '.gm', 'bootstrap-error.json');
15
18
  const LOG_DIR = path.join(os.homedir(), '.claude', 'gm-log');
@@ -100,6 +103,172 @@ function computeFileHash(filePath) {
100
103
  return crypto.createHash('sha256').update(content).digest('hex');
101
104
  }
102
105
 
106
+ function httpGet(url, timeoutMs) {
107
+ return new Promise((resolve, reject) => {
108
+ const req = https.get(url, { timeout: timeoutMs, headers: { 'accept': 'application/json', 'user-agent': 'gm-skill-bootstrap' } }, (res) => {
109
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
110
+ res.resume();
111
+ httpGet(res.headers.location, timeoutMs).then(resolve, reject);
112
+ return;
113
+ }
114
+ if (res.statusCode !== 200) {
115
+ res.resume();
116
+ reject(new Error(`HTTP ${res.statusCode} ${url}`));
117
+ return;
118
+ }
119
+ const chunks = [];
120
+ res.on('data', (c) => chunks.push(c));
121
+ res.on('end', () => resolve(Buffer.concat(chunks)));
122
+ res.on('error', reject);
123
+ });
124
+ req.on('timeout', () => { req.destroy(new Error(`timeout ${timeoutMs}ms ${url}`)); });
125
+ req.on('error', reject);
126
+ });
127
+ }
128
+
129
+ async function getLatestRemoteVersion() {
130
+ try {
131
+ const stat = fs.statSync(PLUGKIT_LATEST_CACHE);
132
+ if (Date.now() - stat.mtimeMs < PLUGKIT_LATEST_TTL_MS) {
133
+ const cached = JSON.parse(fs.readFileSync(PLUGKIT_LATEST_CACHE, 'utf-8'));
134
+ if (cached && cached.version) {
135
+ emitBootstrapEvent('info', 'Using cached latest version', { version: cached.version, ageMs: Date.now() - stat.mtimeMs });
136
+ return cached;
137
+ }
138
+ }
139
+ } catch (_) {}
140
+ let version = null;
141
+ let source = null;
142
+ try {
143
+ const buf = await httpGet('https://api.github.com/repos/AnEntrypoint/plugkit-bin/releases/latest', 3000);
144
+ const rel = JSON.parse(buf.toString('utf-8'));
145
+ const tag = rel && rel.tag_name;
146
+ if (tag) {
147
+ version = tag.replace(/^v/, '');
148
+ source = 'github-releases';
149
+ }
150
+ } catch (e) {
151
+ emitBootstrapEvent('warn', 'GitHub Releases lookup failed', { error: e.message });
152
+ }
153
+ if (!version) {
154
+ try {
155
+ const buf = await httpGet('https://registry.npmjs.org/gm-plugkit/latest', 3000);
156
+ const pkg = JSON.parse(buf.toString('utf-8'));
157
+ if (pkg && pkg.plugkitVersion) {
158
+ version = pkg.plugkitVersion;
159
+ source = 'npm-gm-plugkit';
160
+ } else if (pkg && pkg.version) {
161
+ version = pkg.version;
162
+ source = 'npm-gm-plugkit-fallback';
163
+ }
164
+ } catch (e) {
165
+ emitBootstrapEvent('warn', 'npm fallback lookup failed', { error: e.message });
166
+ }
167
+ }
168
+ if (!version) {
169
+ emitBootstrapEvent('warn', 'All latest-version lookups failed; falling back to manifest');
170
+ return null;
171
+ }
172
+ let sha = '';
173
+ try {
174
+ const shaBuf = await httpGet(`https://github.com/AnEntrypoint/plugkit-bin/releases/download/v${version}/plugkit.wasm.sha256`, 3000);
175
+ sha = shaBuf.toString('utf-8').trim().split(/\s+/)[0];
176
+ } catch (e) {
177
+ emitBootstrapEvent('warn', 'sha fetch failed; will verify after download', { error: e.message, version });
178
+ }
179
+ const payload = { version, sha, source, fetchedAt: Date.now() };
180
+ try {
181
+ fs.mkdirSync(PLUGKIT_TOOLS_DIR, { recursive: true });
182
+ fs.writeFileSync(PLUGKIT_LATEST_CACHE, JSON.stringify(payload, null, 2));
183
+ } catch (_) {}
184
+ emitBootstrapEvent('info', 'Resolved latest plugkit version', { version, source, hasSha: Boolean(sha) });
185
+ return payload;
186
+ }
187
+
188
+ function gitignorePath(cwd) { return path.join(cwd, '.gitignore'); }
189
+
190
+ function getManagedGitignoreEntries() {
191
+ return [
192
+ '.gm/exec-spool/',
193
+ '.gm/gm-fired-*',
194
+ '.gm/needs-gm',
195
+ '.gm/lastskill',
196
+ '.gm/turn-state.json',
197
+ '.gm/turn-state.json.corrupted-*',
198
+ '.gm/residual-check-fired',
199
+ '.gm/bootstrap-status.json',
200
+ '.gm/bootstrap-error.json',
201
+ '.gm/rslearn-counter.json',
202
+ '.gm/trajectory-drafts/',
203
+ '.gm/ingest-drafts/',
204
+ '.gm/prd-state.json',
205
+ '.gm/subagent-*.json',
206
+ '.plugkit-browser-profile/',
207
+ '.plugkit-browser-profile-*/',
208
+ ];
209
+ }
210
+
211
+ function getMustStayTracked() {
212
+ return [
213
+ '.gm/rs-learn.db',
214
+ '.gm/code-search/',
215
+ '.gm/disciplines/',
216
+ '.gm/prd.yml',
217
+ '.gm/mutables.yml',
218
+ 'gm-data/rs-learn.db',
219
+ 'gm-data/code-search/',
220
+ 'gm-data/disciplines/',
221
+ ];
222
+ }
223
+
224
+ function ensureManagedGitignore(cwd) {
225
+ try {
226
+ const gi = gitignorePath(cwd);
227
+ let content = '';
228
+ try { content = fs.readFileSync(gi, 'utf-8'); } catch (_) {}
229
+ const START = '# >>> plugkit managed';
230
+ const END = '# <<< plugkit managed';
231
+ const entries = getManagedGitignoreEntries();
232
+ const block = [START, ...entries, END].join('\n');
233
+ const startIdx = content.indexOf(START);
234
+ const endIdx = content.indexOf(END);
235
+ let cleaned;
236
+ if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
237
+ const before = content.slice(0, startIdx).replace(/\n+$/, '');
238
+ const after = content.slice(endIdx + END.length).replace(/^\n+/, '');
239
+ cleaned = [before, block, after].filter(Boolean).join('\n');
240
+ } else {
241
+ cleaned = content.replace(/\n+$/, '');
242
+ cleaned = cleaned ? `${cleaned}\n\n${block}` : block;
243
+ }
244
+ if (!cleaned.endsWith('\n')) cleaned += '\n';
245
+ if (cleaned !== content) {
246
+ fs.writeFileSync(gi, cleaned);
247
+ emitBootstrapEvent('info', 'Managed .gitignore block updated', { path: gi, entries: entries.length });
248
+ }
249
+ const mustTrack = getMustStayTracked();
250
+ const lines = cleaned.split(/\r?\n/);
251
+ const inManaged = (idx) => {
252
+ let inside = false;
253
+ for (let i = 0; i <= idx; i++) {
254
+ if (lines[i] === START) inside = true;
255
+ else if (lines[i] === END) inside = false;
256
+ }
257
+ return inside;
258
+ };
259
+ for (let i = 0; i < lines.length; i++) {
260
+ const t = lines[i].trim();
261
+ if (!t || t.startsWith('#')) continue;
262
+ if (inManaged(i)) continue;
263
+ if (mustTrack.includes(t)) {
264
+ emitBootstrapEvent('warn', 'Hostile .gitignore entry — must stay tracked', { entry: t, line: i + 1 });
265
+ }
266
+ }
267
+ } catch (e) {
268
+ emitBootstrapEvent('warn', 'ensureManagedGitignore failed', { error: e.message });
269
+ }
270
+ }
271
+
103
272
  async function downloadPlugkitBinary(version) {
104
273
  const binaryName = 'plugkit.wasm';
105
274
  const url = `https://github.com/AnEntrypoint/plugkit-bin/releases/download/v${version}/${binaryName}`;
@@ -232,6 +401,24 @@ async function verifyBinaryHealth(filePath) {
232
401
  }
233
402
  }
234
403
 
404
+ function openWatcherLog(projectDir) {
405
+ const spoolDir = path.join(projectDir, '.gm', 'exec-spool');
406
+ fs.mkdirSync(spoolDir, { recursive: true });
407
+ const logPath = path.join(spoolDir, '.watcher.log');
408
+ try {
409
+ const stat = fs.statSync(logPath);
410
+ if (stat.size > 10 * 1024 * 1024) {
411
+ const rotated = path.join(spoolDir, '.watcher.log.1');
412
+ try { fs.unlinkSync(rotated); } catch (_) {}
413
+ fs.renameSync(logPath, rotated);
414
+ }
415
+ } catch (_) {}
416
+ const fd = fs.openSync(logPath, 'a');
417
+ const header = `\n--- watcher boot ${new Date().toISOString()} pid=${process.pid} ---\n`;
418
+ try { fs.writeSync(fd, header); } catch (_) {}
419
+ return fd;
420
+ }
421
+
235
422
  async function spawnPlugkitWatcher(wasmPath) {
236
423
  try {
237
424
  emitBootstrapEvent('info', 'Spawning plugkit WASM watcher daemon');
@@ -249,18 +436,23 @@ async function spawnPlugkitWatcher(wasmPath) {
249
436
  throw new Error(`WASM wrapper not found at ${wrapperPath}`);
250
437
  }
251
438
 
439
+ const projectDir = process.cwd();
440
+ const logFd = openWatcherLog(projectDir);
441
+
252
442
  const runtime = process.platform === 'win32' ? 'bun.exe' : 'bun';
253
443
  const proc = spawn(runtime, [wrapperPath, 'spool'], {
254
444
  detached: true,
255
- stdio: 'ignore',
445
+ stdio: ['ignore', logFd, logFd],
256
446
  windowsHide: true,
257
- env: { ...process.env, CLAUDE_PROJECT_DIR: process.cwd() },
447
+ env: { ...process.env, CLAUDE_PROJECT_DIR: projectDir },
258
448
  });
259
449
 
450
+ try { fs.closeSync(logFd); } catch (_) {}
451
+
260
452
  const pid = proc.pid;
261
453
  proc.unref();
262
454
 
263
- emitBootstrapEvent('info', 'Plugkit WASM watcher spawned', { pid });
455
+ emitBootstrapEvent('info', 'Plugkit WASM watcher spawned', { pid, logPath: path.join(projectDir, '.gm', 'exec-spool', '.watcher.log') });
264
456
  return pid;
265
457
  } catch (e) {
266
458
  emitBootstrapEvent('error', 'Failed to spawn plugkit WASM watcher', { error: e.message });
@@ -274,11 +466,20 @@ async function bootstrapPlugkit(sessionId) {
274
466
  try {
275
467
  emitBootstrapEvent('info', 'Bootstrap started');
276
468
 
277
- const { version: manifestVersion, expectedHash } = readManifest();
469
+ ensureManagedGitignore(process.cwd());
470
+
471
+ const manifest = readManifest();
472
+ const latest = await getLatestRemoteVersion();
473
+ const targetVersion = (latest && latest.version) || manifest.version;
474
+ const expectedHash = (latest && latest.sha) || manifest.expectedHash;
475
+ if (latest && latest.version !== manifest.version) {
476
+ emitBootstrapEvent('info', 'Latest plugkit newer than manifest pin', { latest: latest.version, manifest: manifest.version });
477
+ }
278
478
  const installedVersion = getInstalledVersion();
279
479
  const plugkitPath = getPlugkitPath();
280
480
 
281
- const versionMismatch = installedVersion !== manifestVersion;
481
+ const manifestVersion = targetVersion;
482
+ const versionMismatch = installedVersion !== targetVersion;
282
483
  const binaryMissing = !fs.existsSync(plugkitPath);
283
484
 
284
485
  if (!binaryMissing && !versionMismatch) {
@@ -324,9 +525,12 @@ async function bootstrapPlugkit(sessionId) {
324
525
 
325
526
  if (binaryData) {
326
527
  const downloadedHash = crypto.createHash('sha256').update(binaryData).digest('hex');
327
- if (downloadedHash !== expectedHash) {
528
+ if (expectedHash && downloadedHash !== expectedHash) {
328
529
  throw new Error(`Hash mismatch: got ${downloadedHash}, expected ${expectedHash}`);
329
530
  }
531
+ if (!expectedHash) {
532
+ emitBootstrapEvent('warn', 'No expected hash; trusting npm-resolved download', { sha: downloadedHash, version: manifestVersion });
533
+ }
330
534
 
331
535
  killExistingPlugkit();
332
536
  await writeBinaryWithRetry(plugkitPath, binaryData);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-skill",
3
- "version": "2.0.1116",
3
+ "version": "2.0.1118",
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.1118"
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`.