gm-skill 2.0.1589 → 2.0.1591

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
@@ -56,9 +56,9 @@ Record only non-obvious technical caveats that cost multiple runs to discover; r
56
56
 
57
57
  ## Coding Style
58
58
 
59
- **No comments in code** -- no inline, block, or JSDoc comments anywhere (source, generated output, hooks, scripts).
59
+ **No comments in code** -- no inline, block, or JSDoc comments anywhere (source, generated output, hooks, scripts). `test.js checkNoComments()` is the structural guard (fails on any leading `//` over tracked `.js/.mjs/.cjs`); one sighting spawns the full-tree sweep.
60
60
 
61
- **No UTF-8 BOM in any tracked source file.** A leading `efbbbf` breaks `node` (`SyntaxError: Invalid or unexpected token`) and strict `JSON.parse`; a BOM in `gm-plugkit/bootstrap.js` shipped a `bun x gm-plugkit@latest spool` that was unbootable for every user. Cause is editing JS/JSON with PowerShell's default UTF-16/UTF-8-BOM encoding -- always `-Encoding utf8` (no BOM) or the `Write` tool. `test.js checkNoBom()` is the structural guard (fails on any leading BOM over tracked text exts); one sighting spawns the full-tree sweep. Mechanics in rs-learn (`recall: BOM regression incident`).
61
+ **No UTF-8 BOM in any tracked source file.** A leading `efbbbf` breaks `node` and strict `JSON.parse`; cause is PowerShell's default UTF-16/UTF-8-BOM encoding -- always `-Encoding utf8` (no BOM) or the `Write` tool. `test.js checkNoBom()` is the structural guard; one sighting spawns the full-tree sweep. Incident + mechanics in rs-learn (`recall: BOM regression incident`).
62
62
 
63
63
  **No graphical symbols; convert to industry-standard text on sight.** Decorative glyphs are forbidden in all output and source: arrows, box/geometric glyphs, stars, filled/hollow dots and bullets, checks/crosses, emojis, any non-ASCII decorative symbol. Convert on sight in the same turn (arrow -> `->`, bullet -> `-`/`*`, check/cross -> `[x]`/`[ ]` or done/todo/pass/fail, status dot -> the word). Tell-tale-AI class: one sighting spawns the full-codebase sweep, never a one-off edit. Exempt: functional code operators (`=>`, `??`, `?.`, comparison/math), frozen changelog/git-log entries, binary stores, intentional icon-font/CSS-content product glyphs. `ccsniff --glyph-discipline` flags decorative glyphs post-hoc (run each audit, like `--git-discipline`/`--search-discipline`).
64
64
 
package/bin/bootstrap.js CHANGED
@@ -114,11 +114,6 @@ function ensureNextStepWiring(cwd) {
114
114
  }
115
115
  }
116
116
 
117
- // Resolve a bare command name to its actual .exe on Windows. cmd.exe + .cmd
118
- // shim chains re-enter conhost (visible window flash) even with
119
- // windowsHide:true on the parent. Spawning the real .exe directly lets
120
- // CREATE_NO_WINDOW propagate. Falls back to .cmd or the original name when
121
- // no .exe is found. See [[windows-spawn-cmd-shim-flash]].
122
117
  function resolveWindowsExe(cmd) {
123
118
  if (process.platform !== 'win32') return cmd;
124
119
  try {
@@ -213,13 +208,6 @@ function gmToolsDir() {
213
208
  return primary;
214
209
  }
215
210
 
216
- // Copy the freshly-resolved plugkit binary + its version+sha manifests to
217
- // ~/.gm-tools (or ~/.claude/gm-tools for legacy installs) so hooks.json can
218
- // invoke plugkit directly without going through node. Self-update inside the
219
- // Rust binary keeps gm-tools fresh from
220
- // here on. Skipped silently on any error -- the next session-start hook will
221
- // retry via ensure_tools_current.
222
-
223
211
  function copyWasmToGmTools(wasmPath, wrapperDir, version) {
224
212
  const dst = gmToolsDir();
225
213
  fs.mkdirSync(dst, { recursive: true });
@@ -584,17 +572,6 @@ function getWasmPath(opts) {
584
572
  return null;
585
573
  }
586
574
 
587
- // ---------------------------------------------------------------------------
588
- // Daemon kill on version change.
589
- //
590
- // The plugin tarball pins `plugkit.version`. When that pin advances and we
591
- // install a newer cached binary, any long-running daemon (the runner) holds
592
- // stale code and serves stale RPCs until killed. We track which version the
593
- // daemon was last started under via `.daemon-version`; on every wrapper
594
- // invocation, if the wrapper-pinned version differs, we kill the daemon so
595
- // the next exec spawns it fresh under the new binary.
596
- // ---------------------------------------------------------------------------
597
-
598
575
  function daemonVersionSentinel() {
599
576
  const root = (() => {
600
577
  try { const r = cacheRoot(); ensureDir(r); return r; }
@@ -226,8 +226,8 @@ async function validateBrowserEmbed() {
226
226
  const pw = which('playwriter') || which('playwriter.cmd');
227
227
  if (!pw) { v.skipped = true; v.errors.push('playwriter not found'); return v; }
228
228
 
229
- const tbDir = 'C:/dev/thebird';
230
- if (!fs.existsSync(path.join(tbDir, 'docs'))) { v.skipped = true; v.errors.push('thebird docs missing'); return v; }
229
+ const tbDir = path.resolve(__dirname, '..');
230
+ if (!fs.existsSync(path.join(tbDir, 'docs'))) { v.skipped = true; v.errors.push('repo docs/ missing -- cannot serve a fixture page'); return v; }
231
231
 
232
232
  let serveProc = null;
233
233
  const port = 3088;
@@ -250,7 +250,7 @@ async function validateBrowserEmbed() {
250
250
  ready = true; break;
251
251
  } catch (_) { await sleep(500); }
252
252
  }
253
- if (!ready) { v.errors.push('thebird serve never came up on ' + port); return v; }
253
+ if (!ready) { v.errors.push('docs serve never came up on ' + port); return v; }
254
254
 
255
255
  let sessionId = '';
256
256
  try {
package/bin/plugkit.js CHANGED
@@ -38,7 +38,6 @@ function readExpectedSha() {
38
38
  return null;
39
39
  }
40
40
 
41
- // Returns true if gm-tools WASM matches pinned version by sha. Fast: no network.
42
41
  function isReady() {
43
42
  const home = process.env.USERPROFILE || process.env.HOME || os.homedir();
44
43
  const primaryWasm = path.join(home, '.gm-tools', 'plugkit.wasm');
@@ -51,9 +50,6 @@ function isReady() {
51
50
  return actual && actual.toLowerCase() === expected;
52
51
  }
53
52
 
54
- // Synchronously run bootstrap.js in a child node. Blocks until install finishes
55
- // (or fails). Bootstrap itself is cache-aware: re-download only when sha differs
56
- // from manifest. Wraps stdio:inherit so the user sees progress.
57
53
  function ensureReady(silent) {
58
54
  if (isReady()) return true;
59
55
  const bootstrap = path.join(wrapperDir, 'bootstrap.js');
@@ -1 +1 @@
1
- 0.1.673
1
+ 0.1.674
@@ -1 +1 @@
1
- d98bd5e7040372a6d0965337ebd831597675b66e30bca581456923e483f83a3d plugkit.wasm
1
+ 4b5920d31272fa0371440b50d174ac5688053e9db5d689e412871c30f706a1f4 plugkit.wasm
@@ -1,6 +1,6 @@
1
1
  # BROWSER
2
2
 
3
- ## Hard Rule: Browser Witness Mandate (paper section 23)
3
+ ## Hard Rule: Browser Witness Mandate
4
4
 
5
5
  **Every edit to code that runs in a browser requires a live `browser` dispatch in the same turn as the edit.** Client-side surfaces -- `.html`, `.js`, `.jsx`, `.ts`, `.tsx`, `.vue`, `.svelte`, `.mjs`, `.css`, web components, service workers, every asset loaded by `<script>`, every path reached by `import` from a browser-side entry -- must be witnessed by a live `page.evaluate` of the specific invariant the edit establishes. A passing node test, build, `curl` of the HTML, or static-analysis pass witnesses server delivery, not browser behavior, and is non-substitutive. The witness IS the proof; prose is not.
6
6
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-plugkit",
3
- "version": "2.0.1589",
3
+ "version": "2.0.1591",
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": {
@@ -2342,8 +2342,6 @@ async function runSpoolWatcher(instance, spoolDir) {
2342
2342
  lastActivityMs = Date.now();
2343
2343
  }
2344
2344
 
2345
- /* killPidQuiet, purgeProfileLockFiles, gracefulCloseBrowser are module-scope (defined above spool()). */
2346
-
2347
2345
  function teardownAll(reason) {
2348
2346
  try {
2349
2347
  logEvent('plugkit', 'watcher.teardown', { reason, idle_ms: Date.now() - lastActivityMs });
package/gm.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm",
3
- "version": "2.0.1589",
3
+ "version": "2.0.1591",
4
4
  "description": "Spool-dispatch orchestration engine with unified state machine, skills, and automated git enforcement",
5
5
  "author": "AnEntrypoint",
6
6
  "license": "MIT",
@@ -17,5 +17,5 @@
17
17
  "publishConfig": {
18
18
  "access": "public"
19
19
  },
20
- "plugkitVersion": "0.1.673"
20
+ "plugkitVersion": "0.1.674"
21
21
  }
package/lang/ssh.js CHANGED
@@ -83,7 +83,12 @@ function runSsh(target, cmd, onData) {
83
83
  };
84
84
 
85
85
  const timeout = setTimeout(() => {
86
- if (!done) { done = true; try { ssh.end(); } catch (_) {} resolve(out.trimEnd() || '[timeout after 55s]'); }
86
+ if (!done) {
87
+ done = true;
88
+ try { ssh.end(); } catch (_) {}
89
+ const partial = out.trimEnd();
90
+ resolve(partial ? `${partial}\n[ssh timed out after 55s; output above is partial]` : '[ssh timed out after 55s; no output]');
91
+ }
87
92
  }, 55000);
88
93
 
89
94
  ssh.on('ready', () => {
@@ -46,7 +46,7 @@ async function handleBrowserVerb(body, sessionId) {
46
46
  const available = await isBrowserAvailable();
47
47
  if (!available) {
48
48
  throw new Error(
49
- 'Browser API unavailable at 127.0.0.1:5000. Ensure rs-exec is running with browser support enabled.'
49
+ 'Browser unavailable: the plugkit health probe did not respond. Check the watcher is alive (.gm/exec-spool/.status.json ts within 15s); if dead, boot it with `bun x gm-plugkit@latest spool`.'
50
50
  );
51
51
  }
52
52
 
@@ -373,13 +373,13 @@ function findPlugkitWasmPids() {
373
373
  try {
374
374
  if (process.platform === 'win32') {
375
375
  const ps = "Get-CimInstance Win32_Process -Filter \"Name='bun.exe' OR Name='node.exe'\" | Where-Object { $_.CommandLine -match 'plugkit-wasm-wrapper' } | Select-Object -ExpandProperty ProcessId";
376
- const output = execFileSync('powershell', ['-NoProfile', '-Command', ps], { encoding: 'utf8', windowsHide: true });
376
+ const output = execFileSync('powershell', ['-NoProfile', '-Command', ps], { encoding: 'utf8', windowsHide: true, timeout: 3000 });
377
377
  for (const line of output.split(/\r?\n/)) {
378
378
  const trimmed = line.trim();
379
379
  if (/^\d+$/.test(trimmed)) pids.push(trimmed);
380
380
  }
381
381
  } else {
382
- const output = execSync("ps -eo pid,args", { encoding: 'utf8' });
382
+ const output = execSync("ps -eo pid,args", { encoding: 'utf8', timeout: 3000 });
383
383
  const lines = output.split('\n').filter(Boolean);
384
384
  for (const line of lines) {
385
385
  if (!line.includes('plugkit-wasm-wrapper')) continue;
@@ -722,10 +722,14 @@ async function bootstrapPlugkit(sessionId, options) {
722
722
  if (forceLatest) {
723
723
  const latest = await getLatestRemoteVersion();
724
724
  if (latest && latest.version) {
725
- targetVersion = latest.version;
726
- expectedHash = latest.sha || expectedHash;
727
- if (latest.version !== manifest.version) {
725
+ if (latest.version === manifest.version) {
726
+ expectedHash = latest.sha || expectedHash;
727
+ } else if (latest.sha) {
728
+ targetVersion = latest.version;
729
+ expectedHash = latest.sha;
728
730
  emitBootstrapEvent('info', 'forceLatest: using newer remote version', { latest: latest.version, manifest: manifest.version });
731
+ } else {
732
+ emitBootstrapEvent('warn', 'forceLatest: no sha for newer version; staying on pinned manifest version', { latest: latest.version, manifest: manifest.version });
729
733
  }
730
734
  }
731
735
  }
@@ -173,19 +173,22 @@ function unsolicitedDocs(cwd) {
173
173
  }
174
174
  }
175
175
 
176
+ function yamlStatusValues(content) {
177
+ const values = [];
178
+ for (const raw of String(content).split(/\r?\n/)) {
179
+ const line = raw.replace(/#.*$/, '');
180
+ const m = line.match(/^\s*(?:-\s*)?status\s*:\s*([A-Za-z0-9_-]+)\s*$/);
181
+ if (m) values.push(m[1]);
182
+ }
183
+ return values;
184
+ }
185
+
176
186
  function sessionMarkerPath(sessionId, kind) {
177
187
  const cwd = process.cwd();
178
188
  return path.join(cwd, '.gm', 'exec-spool', `.session-${kind}-${sessionId || 'anon'}`);
179
189
  }
180
190
 
181
191
  function hasDispatchedInstruction(sessionId) {
182
- try {
183
- const outDir = path.join(process.cwd(), '.gm', 'exec-spool', 'out');
184
- if (!fs.existsSync(outDir)) return false;
185
- for (const f of fs.readdirSync(outDir)) {
186
- if (f.startsWith('instruction-')) return true;
187
- }
188
- } catch (_) {}
189
192
  return fs.existsSync(sessionMarkerPath(sessionId, 'instruction-seen'));
190
193
  }
191
194
 
@@ -318,16 +321,18 @@ function checkDispatchGates(sessionId, operation, extra) {
318
321
  const residuals = [];
319
322
  if (fs.existsSync(prdPath)) {
320
323
  try {
321
- const content = fs.readFileSync(prdPath, 'utf8');
322
- if (content.includes('status: pending') || content.includes('status: in_progress')) {
324
+ const statuses = yamlStatusValues(fs.readFileSync(prdPath, 'utf8'));
325
+ if (statuses.includes('pending') || statuses.includes('in_progress')) {
323
326
  residuals.push('PRD has open items -- resolve or name-and-stop before declaring done');
324
327
  }
325
- } catch (_) {}
328
+ } catch (e) {
329
+ residuals.push(`prd.yml unreadable (${e.message}) -- cannot verify PRD state`);
330
+ }
326
331
  }
327
332
  if (fs.existsSync(mutsPath)) {
328
333
  try {
329
- const content = fs.readFileSync(mutsPath, 'utf8');
330
- if (content.includes('status: unknown')) {
334
+ const statuses = yamlStatusValues(fs.readFileSync(mutsPath, 'utf8'));
335
+ if (statuses.includes('unknown')) {
331
336
  residuals.push('unresolved mutables present -- resolve with witness_evidence before declaring done');
332
337
  }
333
338
  } catch (e) {
@@ -353,7 +358,7 @@ function checkDispatchGates(sessionId, operation, extra) {
353
358
  if (browserEdits.length > 0 && !isBrowserWitnessed(cwd)) {
354
359
  const files = browserEdits.map(e => e.file);
355
360
  const shown = files.slice(0, 5).join(', ') + (files.length > 5 ? `, +${files.length - 5} more` : '');
356
- residuals.push(`Browser Witness required: you edited ${shown} without dispatching the browser verb to witness the change in a live page. Per paper section 23 this is non-negotiable. Either dispatch browser to verify the edit works in-browser, or revert the changes.`);
361
+ residuals.push(`Browser Witness required: you edited ${shown} without dispatching the browser verb to witness the change in a live page. This is non-negotiable. Either dispatch browser to verify the edit works in-browser, or revert the changes.`);
357
362
  logDeviation('deviation.browser-witness-missing', { files, operation });
358
363
  } else if (browserEdits.length > 0 && isBrowserWitnessed(cwd)) {
359
364
  const witness = readBrowserWitness(cwd) || {};
@@ -413,7 +418,7 @@ function checkDispatchGates(sessionId, operation, extra) {
413
418
  logDeviation('deviation.commit-message-defer', { marker, operation });
414
419
  return {
415
420
  allowed: false,
416
- reason: `commit message rejected: deferral phrase '${marker}' detected. Per paper section 22 Fix on Sight, defer markers are forced closure. Either inline-fix and re-witness, or split the deferred work as a separate PRD item with blockedBy: [external] before committing. Rewrite the commit message and retry.`,
421
+ reason: `commit message rejected: deferral phrase '${marker}' detected. Defer markers force closure: either inline-fix and re-witness, or split the deferred work as a separate PRD item with blockedBy: [external] before committing. Rewrite the commit message and retry.`,
417
422
  };
418
423
  }
419
424
  }
package/lib/spool.js CHANGED
@@ -13,7 +13,10 @@ function generateTaskId() {
13
13
 
14
14
  function validateLang(lang) {
15
15
  const valid = ['nodejs', 'python', 'bash', 'typescript', 'go', 'rust', 'c', 'cpp', 'java', 'deno'];
16
- return valid.includes(lang) ? lang : 'nodejs';
16
+ if (!valid.includes(lang)) {
17
+ throw new Error(`unknown lang '${lang}'; valid: ${valid.join(', ')}`);
18
+ }
19
+ return lang;
17
20
  }
18
21
 
19
22
  function getExtForLang(lang) {
@@ -33,8 +36,10 @@ function getExtForLang(lang) {
33
36
  }
34
37
 
35
38
  function validateVerb(verb) {
36
- const valid = ['codesearch', 'recall', 'memorize', 'wait', 'sleep', 'status', 'close', 'browser', 'runner', 'type', 'kill-port', 'forget', 'feedback', 'learn-status', 'learn-debug', 'learn-build', 'discipline', 'pause', 'health'];
37
- return valid.includes(verb) ? verb : 'status';
39
+ if (typeof verb !== 'string' || !/^[a-z][a-z0-9_-]*$/.test(verb)) {
40
+ throw new Error(`invalid verb '${verb}'; expected a kebab/snake-case identifier`);
41
+ }
42
+ return verb;
38
43
  }
39
44
 
40
45
  function writeSpool(body, lang = 'nodejs', options = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-skill",
3
- "version": "2.0.1589",
3
+ "version": "2.0.1591",
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",
@@ -7,15 +7,4 @@ function loadCanonicalSkill() {
7
7
  return fs.readFileSync(SKILL_MD_PATH, 'utf-8');
8
8
  }
9
9
 
10
- function renderPlatformSkill(platformName) {
11
- return `---
12
- name: gm-${platformName}
13
- description: AI-native software engineering via skill-driven orchestration on ${platformName}; bootstraps plugkit for task execution and session isolation
14
- allowed-tools: Skill
15
- ---
16
-
17
- See [gm-skill](../gm-skill/SKILL.md). All platforms share the same plugkit dispatch surface.
18
- `;
19
- }
20
-
21
- module.exports = { loadCanonicalSkill, renderPlatformSkill, SKILL_MD_PATH };
10
+ module.exports = { loadCanonicalSkill, SKILL_MD_PATH };