gm-skill 2.0.1607 → 2.0.1609

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/AGENTS.md CHANGED
@@ -156,6 +156,8 @@ Orchestration state is tracked via `.gm/` marker files, not hook events; the CLI
156
156
 
157
157
  **Dead-watcher recovery uses `bun x gm-plugkit@latest spool`, never direct-node boot** (mechanism in rs-learn: `recall: dead-watcher recovery bun x not direct-node`).
158
158
 
159
+ **Apparent tooling failure is mechanical self-recovery, NEVER a question for the user and never an a/b-test/blind-restart.** "The spooler is not working" / a missing spool response / a stale watcher is the agent's own job to fix: honor a future `busy_until` (wait), else boot the watcher and re-dispatch; on a transient boot hiccup (`FailedToOpenSocket`) retry `@latest`, never the non-`@latest` cache (stale). The spooler is sound by construction -- `.status.json` is written atomically (temp+rename, `atomicWriteJson`) and every long verb advertises `busy_until` -- so a transient unreadable/stale read is a respawn/idle-teardown window to boot through, not a broken tool; asking the user to do what a verb can do is a paper-spirit violation. Debug the live page via `window.*` globals + the `browser` verb's `page.evaluate` as a process of elimination, never variant-after-variant a/b testing. This IS the core gm method on every surface including its own tooling: record all mutables, eliminate each by witness, discover more, keep going.
160
+
159
161
  **The first verb after a genuine multi-minute IDLE is `instruction`, to reset the long-gap clock**: gate fires when >300s since last instruction AND >300s since any SPOOL verb. Platform `Bash`/`Read`/`Edit`/`Grep` do NOT reset the clock -- a long investigation run in them trips a false stall; interleave `prd-add` or `instruction` to keep warm. For a predictable blocking wait (`TaskOutput`/`gh run watch`), dispatch `instruction` BEFORE entering the wait. Detail + platform-tool exception in rs-learn (`recall: first verb after multi-minute wait instruction long-gap`).
160
162
 
161
163
  **A stop-hook firing on a terminal chain does not authorize re-polling**: when a stop-hook fires while already at `phase=COMPLETE` AND `prd_pending_count=0`, re-dispatching `instruction`/`phase-status` to "re-confirm" is a deviation (`deviation.complete-chain-poll`, `instructions/mod.rs`). Two admissible responses: (a) a prose-only turn (COMPLETE is in hand), or (b) genuinely new planned work opened with a FRESH `{"prompt":...}` body (resets phase to PLAN, driven through the skill). Repeatedly answering the same hook is a loop; state the terminal facts once and stop, or open new work.
@@ -10,6 +10,18 @@ const { pathToFileURL } = require('url');
10
10
  const ROOT = process.cwd();
11
11
  const WITNESS_DIR = path.join(ROOT, '.gm', 'witness');
12
12
 
13
+ function freePort() {
14
+ const net = require('net');
15
+ return new Promise((resolve, reject) => {
16
+ const srv = net.createServer();
17
+ srv.once('error', reject);
18
+ srv.listen(0, '127.0.0.1', () => {
19
+ const p = srv.address().port;
20
+ srv.close(() => resolve(p));
21
+ });
22
+ });
23
+ }
24
+
13
25
  function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
14
26
  function rmrf(p) { try { fs.rmSync(p, { recursive: true, force: true }); } catch (_) {} }
15
27
  function write(file, text) { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, text); }
@@ -26,7 +38,7 @@ function which(cmds) {
26
38
  async function renderPreview() {
27
39
  const preview = fs.mkdtempSync(path.join(os.tmpdir(), 'gm-shell-preview-'));
28
40
  fs.mkdirSync(path.join(preview, 'vendor'), { recursive: true });
29
- cp.execSync(`powershell.exe -NoProfile -NonInteractive -Command "Copy-Item -Recurse -Force '${path.join(ROOT, 'site', 'vendor', '*')}' '${path.join(preview, 'vendor')}'"`, { stdio: 'ignore', windowsHide: true });
41
+ fs.cpSync(path.join(ROOT, 'site', 'vendor'), path.join(preview, 'vendor'), { recursive: true });
30
42
 
31
43
  const renderScript = `
32
44
  import { writeFileSync } from 'fs';
@@ -44,17 +56,18 @@ async function renderPreview() {
44
56
  }
45
57
  };
46
58
  const out = await mod.default.render(ctx);
47
- writeFileSync(resolve('${preview.replace(/\\/g, '\\\\')}', 'index.html'), out[0].html);
59
+ writeFileSync(resolve(process.env.GM_SHELL_PREVIEW, 'index.html'), out[0].html);
48
60
  `;
49
61
  const tmp = path.join(os.tmpdir(), `gm-shell-render-${Date.now()}.mjs`);
50
62
  fs.writeFileSync(tmp, renderScript);
51
- cp.execFileSync('node', [tmp], { stdio: 'inherit', windowsHide: true });
63
+ cp.execFileSync('node', [tmp], { stdio: 'inherit', windowsHide: true, env: { ...process.env, GM_SHELL_PREVIEW: preview } });
52
64
  try { fs.unlinkSync(tmp); } catch (_) {}
53
65
 
54
- const server = cp.spawn('python', ['-m', 'http.server', '4210', '--directory', preview], { cwd: ROOT, detached: true, stdio: 'ignore', windowsHide: true });
66
+ const port = await freePort();
67
+ const server = cp.spawn('python', ['-m', 'http.server', String(port), '--directory', preview], { cwd: ROOT, detached: true, stdio: 'ignore', windowsHide: true });
55
68
  server.unref();
56
69
  await sleep(1500);
57
- return { preview, port: 4210, serverPid: server.pid };
70
+ return { preview, port, serverPid: server.pid };
58
71
  }
59
72
 
60
73
  function killServer(pid) {
@@ -96,7 +109,7 @@ return JSON.stringify(result);
96
109
  const response = await fetch(relayUrl, {
97
110
  method: 'POST',
98
111
  headers: { 'Content-Type': 'application/json' },
99
- body: JSON.stringify({ sessionId: '9', code: script, timeout: 60000, cwd: ROOT }),
112
+ body: JSON.stringify({ sessionId: `gm-shell-${process.pid}-${Date.now()}`, code: script, timeout: 60000, cwd: ROOT }),
100
113
  });
101
114
  const result = await response.json();
102
115
  const out = result.text || '';
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-plugkit",
3
- "version": "2.0.1607",
3
+ "version": "2.0.1609",
4
4
  "description": "Bootstrap and daemon-spawn tool for gm plugkit binary. Downloads the correct platform binary, verifies SHA256, and starts the spool watcher daemon. Includes plugkit-wasm-wrapper for WASM-based spool watching.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -1 +1 @@
1
- 0.1.683
1
+ 0.1.684
package/gm.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm",
3
- "version": "2.0.1607",
3
+ "version": "2.0.1609",
4
4
  "description": "Spool-dispatch orchestration engine with unified state machine, skills, and automated git enforcement",
5
5
  "author": "AnEntrypoint",
6
6
  "license": "MIT",
package/lib/spool.js CHANGED
@@ -55,7 +55,13 @@ function writeSpool(body, lang = 'nodejs', options = {}) {
55
55
  fs.mkdirSync(inDir, { recursive: true });
56
56
 
57
57
  const sessionId = options.sessionId || process.env.CLAUDE_SESSION_ID;
58
- const code = sessionId ? `const SESSION_ID = ${JSON.stringify(sessionId)};\n${body}` : body;
58
+ let code = body;
59
+ if (sessionId) {
60
+ const prelude = validLang === 'bash'
61
+ ? `SESSION_ID=${JSON.stringify(String(sessionId))}\n`
62
+ : `const SESSION_ID = ${JSON.stringify(sessionId)};\n`;
63
+ code = prelude + body;
64
+ }
59
65
 
60
66
  fs.writeFileSync(inFile, code, 'utf8');
61
67
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-skill",
3
- "version": "2.0.1607",
3
+ "version": "2.0.1609",
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",
@@ -38,7 +38,18 @@
38
38
  "bin/plugkit.sha256",
39
39
  "bin/plugkit.version",
40
40
  "bin/plugkit.wasm.sha256",
41
- "gm-plugkit/",
41
+ "gm-plugkit/bootstrap.js",
42
+ "gm-plugkit/browser-idle.js",
43
+ "gm-plugkit/cli.js",
44
+ "gm-plugkit/index.js",
45
+ "gm-plugkit/lang-host-runner.js",
46
+ "gm-plugkit/package.json",
47
+ "gm-plugkit/plugkit-wasm-wrapper.js",
48
+ "gm-plugkit/supervisor.js",
49
+ "gm-plugkit/plugkit.version",
50
+ "gm-plugkit/plugkit.sha256",
51
+ "gm-plugkit/instructions/",
52
+ "gm-plugkit/scripts/",
42
53
  "AGENTS.md",
43
54
  "README.md",
44
55
  "gm.json"
@@ -48,6 +48,10 @@ bun x gm-plugkit@latest spool > /dev/null 2>&1 &
48
48
 
49
49
  **Dead-watcher recovery is mandatory.** Two consecutive missing re-Reads AND stale `ts` (>15s) AND no future `busy_until` = dead: `bun x gm-plugkit@latest spool` to boot a fresh watcher, then re-dispatch the original verb. Never substitute an alternative tool (puppeteer-core, WebFetch, raw chrome) for the `browser` verb -- reaching outside plugkit orphans state and bypasses the witness gates. Recovery is always notice-dead -> boot -> re-dispatch.
50
50
 
51
+ **Apparent tooling failure is NEVER grounds to ask the user, and never a reason to a/b-test or blind-restart.** "The spooler is not working" / a missing spool response / a stale watcher is YOUR mechanical, self-service recovery, not a question for the user: honor a future `busy_until` (wait), else boot the watcher and re-dispatch -- you have the authority to boot, so asking the user to do it (or to do anything the verbs can do) is a paper-spirit violation. The spooler mechanics are sound by construction (`.status.json` is written atomically temp+rename, every long verb advertises `busy_until`), so a transient unreadable/stale read is a respawn/idle-teardown window to boot through, not a broken tool. When a transient boot hiccup occurs (e.g. `FailedToOpenSocket`), retry `bun x gm-plugkit@latest spool` -- blips resolve in seconds; never escalate to the user and never fall back to a non-`@latest` cache (it lands a stale watcher). This is the gm method applied to your own tooling: record each candidate cause as a mutable, eliminate it by witness, discover more, keep going.
52
+
53
+ **Debug the live page via globals + process-of-elimination, never a/b testing.** When a browser/client issue is hard, the move is NOT to guess-and-restart or try variant after variant: surface the relevant state as a `window.*` global and read it live via the `browser` verb's `page.evaluate`, running experiments in the page to eliminate hypotheses one by one (record each as a mutable, witness its resolution, add the new mutables it reveals). A global plus one evaluate observes real runtime state in a single dispatch; the restart-and-eyeball / a/b loop observes almost nothing and burns turns. This process -- record all mutables, eliminate by witness, discover more, keep going -- is the core of gm and applies to every debugging surface, the browser most of all.
54
+
51
55
  From PowerShell, write spool input as UTF-8 no-BOM (`-Encoding utf8` or `[System.IO.File]::WriteAllText`); the 5.1 default UTF-16+BOM trips `spool.body-encoding-recoded`. Prefer the `Write` tool for JSON bodies. First-turn body is `{"prompt":"<user request>"}` (derives orient_nouns + recall_hits); later same-conversation turns may use `{}`. A `Write` to `in/<verb>/` that errors `ENOENT` (a fast watcher consumed and unlinked the file before the tool's post-write stat) has STILL dispatched -- confirm via the `out/` response, never blind-retry (a non-idempotent verb like `git_finalize` would double-fire); a Bash heredoc `cat > in/<verb>/<N>.txt` has no post-write stat and never surfaces this.
52
56
 
53
57
  **Batch writes and reads together.** Write request + Read response is one logical step -- issue both in one block, not three turns. Fan-out is the same: N independent verbs = N Writes in one block then N Reads in one block. Only a real data dependency (verb B needs A's response) forces separate turns.
@@ -1,46 +0,0 @@
1
- const assert = require('assert');
2
- const { selectIdleBrowserSessions } = require('./browser-idle.js');
3
-
4
- const NOW = 1_000_000;
5
- const LIMIT = 10 * 60 * 1000;
6
-
7
- (function onlyPastLimitSelected() {
8
- const ports = {
9
- active: { pid: 1, lastUse: NOW - 1000 },
10
- idle: { pid: 2, lastUse: NOW - (LIMIT + 5000) },
11
- };
12
- const idle = selectIdleBrowserSessions(ports, NOW, LIMIT);
13
- assert.strictEqual(idle.length, 1, 'exactly one idle session selected');
14
- assert.strictEqual(idle[0].sid, 'idle', 'the idle session is selected, active untouched');
15
- })();
16
-
17
- (function boundaryIsInclusive() {
18
- const ports = { edge: { pid: 1, lastUse: NOW - LIMIT } };
19
- const idle = selectIdleBrowserSessions(ports, NOW, LIMIT);
20
- assert.strictEqual(idle.length, 1, 'idleMs == limit closes (>=)');
21
- })();
22
-
23
- (function missingLastUseReapedAsStale() {
24
- const ports = { orphan: { pid: 1 } };
25
- const idle = selectIdleBrowserSessions(ports, NOW, LIMIT);
26
- assert.strictEqual(idle.length, 1, 'entry with no lastUse is treated as stale (epoch 0) and reaped');
27
- assert.strictEqual(idle[0].sid, 'orphan');
28
- })();
29
-
30
- (function concurrentIsolation() {
31
- const ports = {
32
- sessA: { pid: 1, lastUse: NOW - 2000 },
33
- sessB: { pid: 2, lastUse: NOW - (LIMIT + 1) },
34
- sessC: { pid: 3, lastUse: NOW - (LIMIT + 999999) },
35
- };
36
- const idle = selectIdleBrowserSessions(ports, NOW, LIMIT).map(x => x.sid).sort();
37
- assert.deepStrictEqual(idle, ['sessB', 'sessC'], 'only the idle sessions, active sessA preserved');
38
- })();
39
-
40
- (function emptyAndMalformed() {
41
- assert.deepStrictEqual(selectIdleBrowserSessions({}, NOW, LIMIT), [], 'empty ports');
42
- assert.deepStrictEqual(selectIdleBrowserSessions(null, NOW, LIMIT), [], 'null ports');
43
- assert.deepStrictEqual(selectIdleBrowserSessions({ bad: null, str: 'x' }, NOW, LIMIT), [], 'malformed entries skipped');
44
- })();
45
-
46
- console.log('browser-idle.test.js: all assertions passed');
@@ -1,25 +0,0 @@
1
- const assert = require('assert');
2
- const fs = require('fs');
3
- const path = require('path');
4
-
5
- const wrapper = fs.readFileSync(path.join(__dirname, 'plugkit-wasm-wrapper.js'), 'utf-8');
6
-
7
- const aliasInChildScript = /\bspawnSync\s*\(\s*process\.execPath\s*,\s*\[\s*['"]-e['"]\s*,\s*`[^`]*\b_(?:net|http|https|crypto|childProcess)Module\b[^`]*`/;
8
-
9
- (function noParentAliasInChildEvalTemplates() {
10
- assert.strictEqual(
11
- aliasInChildScript.test(wrapper),
12
- false,
13
- 'no spawnSync(process.execPath, ["-e", `...`]) child-script template may reference a parent-scope _*Module alias; the spawned child has no such binding (use require() inside the template). This regression broke findFreePortSync/isPort*/fetchJsonSync and surfaced only as "could not allocate free port" at browser-spawn time.'
14
- );
15
- })();
16
-
17
- (function childTemplatesUseRequire() {
18
- const childEvalBlocks = wrapper.match(/spawnSync\s*\(\s*process\.execPath\s*,\s*\[\s*['"]-e['"]\s*,\s*`[^`]*`/g) || [];
19
- for (const block of childEvalBlocks) {
20
- if (/\brequire\s*\(\s*['"](?:net|http|https)['"]\s*\)/.test(block) || !/\b(?:net|http|https)\b/.test(block)) continue;
21
- assert.fail(`child -e template uses a node builtin without require(): ${block.slice(0, 80)}...`);
22
- }
23
- })();
24
-
25
- console.log('child-script-alias.test.js: all assertions passed');