gm-skill 2.0.1468 → 2.0.1470

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
@@ -40,7 +40,7 @@ Agents dispatch verbs by writing to `.gm/exec-spool/in/<verb>/<N>.txt` (request
40
40
 
41
41
  **git verbs**: `git_status` returns `{dirty, modified, untracked, deleted, staged}` from `git status --porcelain`. `branch_status` returns `{branch, ahead, behind, remote}`, the `remote-pushed` witness. `git_push` is the ONLY admissible push surface, it gates on `git_porcelain()` non-empty (refuses dirty), emits `deviation.push-dirty` on attempt, and shells the push only when clean. A raw `git push` via Bash bypasses the gate and is itself a deviation; ccsniff `--git-discipline` flags it.
42
42
 
43
- **filter verb**: pure stdout → compact-stdout transformation. Body `{kind, input, ...opts}` where kind is one of `grep`, `ls`, `tree`, `json`, `diff`, `git-status`, `log`. Returns `{output, stats:{bytes_in, bytes_out, saved_pct, ...}}`. Pipe raw command output through filter before letting it enter context, rtk's role, in-wasm, no subprocess. Replaces the legacy detached rtk binary download in bootstrap.
43
+ **filter verb**: pure stdout → compact-stdout transformation. Body `{kind, input, ...opts}` where kind is one of `grep`, `ls`, `tree`, `json`, `diff`, `git-status`, `log`. Returns `{output, stats:{bytes_in, bytes_out, saved_pct, ...}}`. Pipe raw command output through filter before letting it enter context, in-wasm, no subprocess. The bootstrap fetches only `plugkit.wasm`, there is no separate filter/rtk binary download.
44
44
 
45
45
  ## Documentation Policy
46
46
 
package/bin/bootstrap.js CHANGED
@@ -187,11 +187,6 @@ function obsEvent(subsystem, event, fields) {
187
187
  }
188
188
 
189
189
 
190
- function rtkBinaryName() {
191
- const key = platformKey();
192
- return key.startsWith('win32') ? `rtk-${key}.exe` : `rtk-${key}`;
193
- }
194
-
195
190
  function cacheRoot() {
196
191
  const home = os.homedir();
197
192
  if (process.env.PLUGKIT_CACHE_DIR) return process.env.PLUGKIT_CACHE_DIR;
@@ -256,12 +251,6 @@ function readVersionFile(wrapperDir) {
256
251
  return fs.readFileSync(p, 'utf8').trim();
257
252
  }
258
253
 
259
- function readRtkVersion(wrapperDir) {
260
- const p = path.join(wrapperDir, 'rtk.version');
261
- if (!fs.existsSync(p)) return null;
262
- const v = fs.readFileSync(p, 'utf8').trim();
263
- return v || null;
264
- }
265
254
 
266
255
  function sha256OfFileSync(filePath) {
267
256
  const h = crypto.createHash('sha256');
@@ -432,15 +421,12 @@ function isLockStale(lockPath) {
432
421
  return false;
433
422
  }
434
423
 
435
- function pruneOldVersions(root, keepVersion, keepRtkVersion) {
424
+ function pruneOldVersions(root, keepVersion) {
436
425
  try {
437
426
  const entries = fs.readdirSync(root);
438
427
  for (const e of entries) {
439
- const isPlugkit = e.startsWith('v') && !e.startsWith('rtk-');
440
- const isRtk = e.startsWith('rtk-v');
441
- if (!isPlugkit && !isRtk) continue;
442
- if (isPlugkit && e === `v${keepVersion}`) continue;
443
- if (isRtk && keepRtkVersion && e === `rtk-v${keepRtkVersion}`) continue;
428
+ if (!e.startsWith('v')) continue;
429
+ if (e === `v${keepVersion}`) continue;
444
430
  const dir = path.join(root, e);
445
431
  const lock = path.join(dir, '.lock');
446
432
  if (fs.existsSync(lock) && !isLockStale(lock)) continue;
@@ -502,7 +488,6 @@ async function bootstrap(opts) {
502
488
 
503
489
  if (healIfShaMatches(wasmFinalPath, wasmExpectedSha, wasmOkSentinel, wasmPartialPath, 'wasm')) {
504
490
  obsEvent('bootstrap', 'decision.heal', { reason: 'sha-match', path: wasmFinalPath });
505
- spawnDetachedRtkFetch(wrapperDir);
506
491
  copyWasmToGmTools(wasmFinalPath, wrapperDir, version);
507
492
  clearBootstrapError();
508
493
  return wasmFinalPath;
@@ -519,7 +504,6 @@ async function bootstrap(opts) {
519
504
  }
520
505
  if (healIfShaMatches(wasmFinalPath, wasmExpectedSha, wasmOkSentinel, wasmPartialPath, 'wasm')) {
521
506
  obsEvent('bootstrap', 'decision.heal', { reason: 'sha-match-under-lock', path: wasmFinalPath });
522
- spawnDetachedRtkFetch(wrapperDir);
523
507
  copyWasmToGmTools(wasmFinalPath, wrapperDir, version);
524
508
  clearBootstrapError();
525
509
  return wasmFinalPath;
@@ -574,8 +558,7 @@ async function bootstrap(opts) {
574
558
  fs.writeFileSync(wasmOkSentinel, new Date().toISOString());
575
559
  log(`decision: fetch reason: install-complete (${wasmFinalPath})`);
576
560
  obsEvent('bootstrap', 'install.done', { path: wasmFinalPath, version, kind: 'wasm' });
577
- pruneOldVersions(root, version, readRtkVersion(wrapperDir));
578
- spawnDetachedRtkFetch(wrapperDir);
561
+ pruneOldVersions(root, version);
579
562
  copyWasmToGmTools(wasmFinalPath, wrapperDir, version);
580
563
 
581
564
  clearBootstrapError();
@@ -585,109 +568,6 @@ async function bootstrap(opts) {
585
568
  }
586
569
  }
587
570
 
588
- function spawnDetachedRtkFetch(wrapperDir) {
589
- try {
590
- const { spawn } = require('child_process');
591
- const child = spawn(process.execPath, [__filename, '--rtk-only', '--wrapper-dir', wrapperDir], {
592
- detached: true,
593
- stdio: 'ignore',
594
- windowsHide: true,
595
- });
596
- child.unref();
597
- obsEvent('bootstrap', 'rtk.detached.spawned', { pid: child.pid, wrapperDir });
598
- } catch (err) {
599
- log(`rtk detach spawn failed: ${err.message}`);
600
- }
601
- }
602
-
603
- function rtkCacheDir(root, wrapperDir, plugkitVerDir) {
604
- const rtkVer = readRtkVersion(wrapperDir);
605
- if (!rtkVer) return plugkitVerDir;
606
- const dir = path.join(root, `rtk-v${rtkVer}`);
607
- ensureDir(dir);
608
- return dir;
609
- }
610
-
611
- async function bootstrapRtk(plugkitVerDir, plugkitVersion, wrapperDir, silent, root) {
612
- const rtkName = rtkBinaryName();
613
- const cacheDir = rtkCacheDir(root || cacheRoot(), wrapperDir, plugkitVerDir);
614
- const rtkPath = path.join(cacheDir, rtkName);
615
- const rtkOk = path.join(cacheDir, '.rtk-ok');
616
- if (fs.existsSync(rtkPath) && fs.existsSync(rtkOk)) {
617
- if (!silent) log(`rtk cache hit: ${rtkPath}`);
618
- return rtkPath;
619
- }
620
- const rtkSha = readShaManifest(wrapperDir, 'rtk.sha256');
621
- const expected = rtkSha ? rtkSha[rtkName] : null;
622
- const tmp = `${rtkPath}.partial`;
623
- if (healIfShaMatches(rtkPath, expected, rtkOk, tmp, 'rtk')) {
624
- if (!silent) log(`rtk cache heal (sha match): ${rtkPath}`);
625
- return rtkPath;
626
- }
627
- const RTKS_RELEASE_REPO = 'AnEntrypoint/plugkit-bin';
628
- const url = `https://github.com/${RTKS_RELEASE_REPO}/releases/download/v${plugkitVersion}/${rtkName}`;
629
- const startMs = Date.now();
630
- let lastErr;
631
- for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
632
- try {
633
- log(`rtk download attempt ${attempt}/${MAX_ATTEMPTS}: ${url}`);
634
- const result = spawnSync(
635
- 'curl',
636
- ['-fSL', '--max-time', String(Math.floor(ATTEMPT_TIMEOUT_MS / 1000)), '-o', tmp, url],
637
- { stdio: 'pipe', timeout: ATTEMPT_TIMEOUT_MS + 5000, windowsHide: true }
638
- );
639
- if (result.error) throw result.error;
640
- if (result.status !== 0) throw new Error(`curl failed with status ${result.status}`);
641
- break;
642
- } catch (err) {
643
- lastErr = err;
644
- log(`rtk attempt ${attempt} failed: ${err.message}`);
645
- obsEvent('bootstrap', 'rtk.download.attempt_failed', { attempt, max: MAX_ATTEMPTS, err: String(err.message || err) });
646
- if (attempt < MAX_ATTEMPTS) {
647
- const wait = BACKOFF_MS[attempt - 1] || 120000;
648
- log(`backing off ${wait}ms`);
649
- await new Promise(r => setTimeout(r, wait));
650
- }
651
- }
652
- }
653
- if (lastErr) throw lastErr;
654
- if (expected) {
655
- const got = await sha256OfFile(tmp);
656
- if (got !== expected) {
657
- try { fs.unlinkSync(tmp); } catch (_) {}
658
- throw new Error(`rtk sha256 mismatch: expected ${expected}, got ${got}`);
659
- }
660
- }
661
- try { fs.renameSync(tmp, rtkPath); }
662
- catch (err) {
663
- if (err.code === 'EEXIST' || err.code === 'EPERM') {
664
- try { fs.unlinkSync(rtkPath); } catch (_) {}
665
- fs.renameSync(tmp, rtkPath);
666
- } else throw err;
667
- }
668
- if (os.platform() !== 'win32') { try { fs.chmodSync(rtkPath, 0o755); } catch (_) {} }
669
- fs.writeFileSync(rtkOk, new Date().toISOString());
670
- log(`installed ${rtkPath}`);
671
- obsEvent('bootstrap', 'install.done', { path: rtkPath, plugkit_version: plugkitVersion, rtk_version: readRtkVersion(wrapperDir) || plugkitVersion, kind: 'rtk', dur_ms: Date.now() - startMs });
672
- return rtkPath;
673
- }
674
-
675
- function resolveCachedRtk(opts) {
676
- opts = opts || {};
677
- const wrapperDir = opts.wrapperDir || __dirname;
678
- const version = opts.version || readVersionFile(wrapperDir);
679
- const root = (() => {
680
- try { const r = cacheRoot(); ensureDir(r); return r; }
681
- catch (_) { const r = fallbackCacheRoot(); ensureDir(r); return r; }
682
- })();
683
- const plugkitVerDir = path.join(root, `v${version}`);
684
- const cacheDir = rtkCacheDir(root, wrapperDir, plugkitVerDir);
685
- const rtkPath = path.join(cacheDir, rtkBinaryName());
686
- const rtkOk = path.join(cacheDir, '.rtk-ok');
687
- if (fs.existsSync(rtkPath) && fs.existsSync(rtkOk)) return rtkPath;
688
- return null;
689
- }
690
-
691
571
  function getWasmPath(opts) {
692
572
  opts = opts || {};
693
573
  const wrapperDir = opts.wrapperDir || __dirname;
@@ -801,44 +681,24 @@ function killStaleDaemonIfVersionChanged(wrapperDir) {
801
681
  writeDaemonVersion(currentVersion);
802
682
  }
803
683
 
804
- module.exports = { bootstrap, getWasmPath, resolveCachedRtk, rtkBinaryName, cacheRoot, obsEvent, killRunningDaemons, killStaleDaemonIfVersionChanged, killSpoolWatcherInCwd, proactiveKillForNewInstall };
684
+ module.exports = { bootstrap, getWasmPath, cacheRoot, obsEvent, killRunningDaemons, killStaleDaemonIfVersionChanged, killSpoolWatcherInCwd, proactiveKillForNewInstall };
805
685
 
806
686
  if (require.main === module) {
807
687
  const argv = process.argv.slice(2);
808
- if (argv.includes('--rtk-only')) {
809
- const wIdx = argv.indexOf('--wrapper-dir');
810
- const wrapperDir = wIdx >= 0 ? argv[wIdx + 1] : __dirname;
811
- (async () => {
688
+ bootstrap({ silent: false })
689
+ .then(p => { process.stdout.write(p + '\n'); process.exit(0); })
690
+ .catch(err => {
691
+ log(`FATAL: ${err.message}`);
692
+ obsEvent('bootstrap', 'fatal', { err: String(err.message || err) });
812
693
  try {
813
- const version = readVersionFile(wrapperDir);
814
- let root = cacheRoot();
815
- try { ensureDir(root); }
816
- catch (_) { root = fallbackCacheRoot(); ensureDir(root); }
817
- const verDir = path.join(root, `v${version}`);
818
- ensureDir(verDir);
819
- await bootstrapRtk(verDir, version, wrapperDir, true, root);
820
- process.exit(0);
821
- } catch (err) {
822
- obsEvent('bootstrap', 'rtk.detached.failed', { err: String(err.message || err) });
823
- process.exit(1);
824
- }
825
- })();
826
- } else {
827
- bootstrap({ silent: false })
828
- .then(p => { process.stdout.write(p + '\n'); process.exit(0); })
829
- .catch(err => {
830
- log(`FATAL: ${err.message}`);
831
- obsEvent('bootstrap', 'fatal', { err: String(err.message || err) });
832
- try {
833
- const pinned = (() => { try { return readVersionFile(__dirname); } catch (_) { return null; } })();
834
- writeBootstrapError({
835
- expected_version: pinned,
836
- cached_version: null,
837
- error_phase: 'fatal',
838
- error_message: String(err && err.message || err),
839
- });
840
- } catch (_) {}
841
- process.exit(1);
842
- });
843
- }
694
+ const pinned = (() => { try { return readVersionFile(__dirname); } catch (_) { return null; } })();
695
+ writeBootstrapError({
696
+ expected_version: pinned,
697
+ cached_version: null,
698
+ error_phase: 'fatal',
699
+ error_message: String(err && err.message || err),
700
+ });
701
+ } catch (_) {}
702
+ process.exit(1);
703
+ });
844
704
  }
@@ -329,14 +329,6 @@ async function extractNpmPackageWithRetry(destPath, version) {
329
329
  }
330
330
 
331
331
 
332
- function platformKey() {
333
- const p = os.platform();
334
- const a = os.arch();
335
- if (p === 'win32') return a === 'arm64' ? 'win32-arm64' : 'win32-x64';
336
- if (p === 'darwin') return a === 'arm64' ? 'darwin-arm64' : 'darwin-x64';
337
- return (a === 'arm64' || a === 'aarch64') ? 'linux-arm64' : 'linux-x64';
338
- }
339
-
340
332
  function healIfShaMatches(binPath, expectedSha, sentinelPath, partialPath, kind) {
341
333
  if (!fs.existsSync(binPath)) return false;
342
334
  if (partialPath) { try { if (fs.existsSync(partialPath)) fs.unlinkSync(partialPath); } catch (_) {} }
@@ -428,10 +420,8 @@ function pruneOldVersions(root, keepVersion) {
428
420
  try {
429
421
  const entries = fs.readdirSync(root);
430
422
  for (const e of entries) {
431
- const isPlugkit = e.startsWith('v') && !e.startsWith('rtk-');
432
- const isRtk = e.startsWith('rtk-v');
433
- if (!isPlugkit && !isRtk) continue;
434
- if (isPlugkit && e === `v${keepVersion}`) continue;
423
+ if (!e.startsWith('v')) continue;
424
+ if (e === `v${keepVersion}`) continue;
435
425
  const dir = path.join(root, e);
436
426
  const lock = path.join(dir, '.lock');
437
427
  if (fs.existsSync(lock) && !isLockStale(lock)) continue;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-plugkit",
3
- "version": "2.0.1468",
3
+ "version": "2.0.1470",
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": {
package/gm.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm",
3
- "version": "2.0.1468",
3
+ "version": "2.0.1470",
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-skill",
3
- "version": "2.0.1468",
3
+ "version": "2.0.1470",
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",
package/bin/rtk.sha256 DELETED
@@ -1,6 +0,0 @@
1
- 53224d66572e937507a8d2877c768e4e6cc3da66aa6f8d0f132afaab2edc2a10 rtk-win32-x64.exe
2
- dedabc1d89641c60c91f09570353b6270dba4f5d53f8597018a708e515265d53 rtk-win32-arm64.exe
3
- cf3190554b82c7395948b7a478c78bbe2241549b00777e660deff4cbb9e0c4b6 rtk-darwin-x64
4
- c815bad459b4eaccc8be4a5d74dba397fdfe7d3716e0b6023b188d2351128b82 rtk-darwin-arm64
5
- 7d60dd5abc15f6d46ffd89b5de7253a067e2a3ef6f1cd8ae5a236eda05a504f4 rtk-linux-x64
6
- cd5dd78035845eef4b362927c61f61e23925af3c12779131024d8334bad87a6b rtk-linux-arm64
@@ -1,109 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const os = require('os');
4
- const spool = require('./spool.js');
5
-
6
- const CODEINSIGHT_HOST = '127.0.0.1';
7
- const CODEINSIGHT_PORT = 4802;
8
- const REQUEST_TIMEOUT_MS = 30000;
9
-
10
- function emitEvent(severity, message, details = {}) {
11
- try {
12
- const date = new Date().toISOString().split('T')[0];
13
- const logDir = path.join(os.homedir(), '.claude', 'gm-log', date);
14
- if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
15
- const entry = { ts: new Date().toISOString(), severity, message, ...details };
16
- fs.appendFileSync(path.join(logDir, 'codeinsight.jsonl'), JSON.stringify(entry) + '\n');
17
- } catch (e) {
18
- console.error(`[codeinsight] emit failed: ${e.message}`);
19
- }
20
- }
21
-
22
- async function checkSocketReachable(host = CODEINSIGHT_HOST, port = CODEINSIGHT_PORT, timeoutMs = 1000) {
23
- try {
24
- const result = await spool.execSpool('health', 'health', { timeoutMs });
25
- return !!(result && result.ok);
26
- } catch (e) {
27
- return false;
28
- }
29
- }
30
-
31
- async function sendRequest(request, sessionId = 'unknown') {
32
- const startTime = Date.now();
33
- const reachable = await checkSocketReachable();
34
-
35
- if (!reachable) {
36
- emitEvent('warn', 'Codeinsight socket unreachable', {
37
- host: CODEINSIGHT_HOST,
38
- port: CODEINSIGHT_PORT,
39
- sessionId,
40
- durationMs: Date.now() - startTime,
41
- });
42
- return { ok: false, error: `Codeinsight daemon unavailable at ${CODEINSIGHT_HOST}:${CODEINSIGHT_PORT}`, durationMs: Date.now() - startTime };
43
- }
44
-
45
- try {
46
- if (request.action === 'search') {
47
- const q = request.discipline && request.discipline !== 'default' ? `@${request.discipline} ${request.query}` : request.query;
48
- const result = await spool.execCodesearch(q, { timeoutMs: REQUEST_TIMEOUT_MS, sessionId });
49
- if (!result.ok) return { ok: false, error: result.stderr || result.stdout || 'codesearch failed', durationMs: Date.now() - startTime };
50
- return { ok: true, raw: result.stdout || '', durationMs: Date.now() - startTime };
51
- }
52
- return { ok: false, error: `Unsupported action via spool: ${request.action}`, durationMs: Date.now() - startTime };
53
- } catch (err) {
54
- return { ok: false, error: err.message, durationMs: Date.now() - startTime };
55
- }
56
- }
57
-
58
- async function searchCode(query, discipline = 'default', sessionId = 'unknown') {
59
- if (!query || typeof query !== 'string' || query.trim().length === 0) {
60
- return { ok: false, error: 'Query must be a non-empty string' };
61
- }
62
- const result = await sendRequest({ action: 'search', query: query.trim(), discipline, sessionId }, sessionId);
63
- if (result.ok) {
64
- emitEvent('info', 'Search completed', { query: query.trim(), discipline, resultCount: (result.results || []).length, sessionId, durationMs: result.durationMs });
65
- }
66
- return result;
67
- }
68
-
69
- async function indexProject(projectPath, discipline = 'default', sessionId = 'unknown') {
70
- if (!projectPath || typeof projectPath !== 'string') {
71
- return { ok: false, error: 'Project path must be a non-empty string' };
72
- }
73
- if (!fs.existsSync(projectPath)) {
74
- emitEvent('warn', 'Project path does not exist', { projectPath, discipline, sessionId });
75
- return { ok: false, error: `Project path does not exist: ${projectPath}` };
76
- }
77
- const result = await sendRequest({ action: 'index', projectPath: path.resolve(projectPath), discipline, sessionId }, sessionId);
78
- if (result.ok) {
79
- emitEvent('info', 'Index completed', { projectPath: path.resolve(projectPath), discipline, filesIndexed: result.filesIndexed, sessionId, durationMs: result.durationMs });
80
- }
81
- return result;
82
- }
83
-
84
- async function getDiagnostics(projectPath = null, discipline = 'default', sessionId = 'unknown') {
85
- if (projectPath && !fs.existsSync(projectPath)) {
86
- return { ok: false, error: `Project path does not exist: ${projectPath}` };
87
- }
88
- const result = await sendRequest({ action: 'diagnostics', projectPath: projectPath ? path.resolve(projectPath) : null, discipline, sessionId }, sessionId);
89
- if (result.ok) {
90
- emitEvent('info', 'Diagnostics retrieved', { projectPath: projectPath ? path.resolve(projectPath) : 'system-wide', discipline, diagnosticCount: (result.diagnostics || []).length, sessionId, durationMs: result.durationMs });
91
- }
92
- return result;
93
- }
94
-
95
- async function getIndexStatus(discipline = 'default', sessionId = 'unknown') {
96
- const result = await sendRequest({ action: 'status', discipline, sessionId }, sessionId);
97
- if (result.ok) {
98
- emitEvent('info', 'Index status retrieved', { discipline, indexed: result.indexed, sessionId, durationMs: result.durationMs });
99
- }
100
- return result;
101
- }
102
-
103
- module.exports = {
104
- searchCode,
105
- indexProject,
106
- getDiagnostics,
107
- getIndexStatus,
108
- checkSocketReachable,
109
- };
@@ -1,443 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const crypto = require('crypto');
4
- const { spawn, execSync } = require('child_process');
5
- const os = require('os');
6
- const spool = require('./spool.js');
7
-
8
- // Resolve a bare command name to its .exe on Windows. cmd.exe + .cmd shim
9
- // chains re-enter conhost (visible window flash) even with windowsHide:true
10
- // on the parent. Spawning the real .exe directly lets CREATE_NO_WINDOW
11
- // propagate cleanly. Falls back to the original name if no .exe is found.
12
- // See [[windows-spawn-cmd-shim-flash]] for the discipline rationale.
13
- function resolveWindowsExe(cmd) {
14
- if (process.platform !== 'win32') return cmd;
15
- try {
16
- const out = execSync(`where ${cmd}`, {
17
- encoding: 'utf-8',
18
- stdio: ['ignore', 'pipe', 'ignore'],
19
- windowsHide: true,
20
- timeout: 800,
21
- });
22
- const lines = out.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
23
- const exe = lines.find(l => /\.exe$/i.test(l));
24
- const shim = lines.find(l => /\.(cmd|bat)$/i.test(l));
25
- return exe || shim || cmd;
26
- } catch {
27
- return cmd;
28
- }
29
- }
30
-
31
- // When spawning a .cmd/.bat shim with shell:true on Windows, cmd.exe re-parses
32
- // the command string and breaks on unquoted spaces (e.g. "C:\Program Files\...").
33
- // Quote the cmd and any space-containing args so cmd.exe sees them as single tokens.
34
- function shellQuoteWin(cmdOrArg) {
35
- const s = String(cmdOrArg);
36
- if (!/[\s"]/.test(s)) return s;
37
- return `"${s.replace(/"/g, '\\"')}"`;
38
- }
39
-
40
- const LOG_DIR = path.join(os.homedir(), '.claude', 'gm-log');
41
- const GM_STATE_DIR = path.join(os.homedir(), '.gm');
42
-
43
- function emitDaemonEvent(daemon, severity, message, details) {
44
- try {
45
- const date = new Date().toISOString().split('T')[0];
46
- const logDir = path.join(LOG_DIR, date);
47
- if (!fs.existsSync(logDir)) {
48
- fs.mkdirSync(logDir, { recursive: true });
49
- }
50
- const logFile = path.join(logDir, 'daemon.jsonl');
51
- const entry = {
52
- ts: new Date().toISOString(),
53
- daemon,
54
- severity,
55
- message,
56
- ...details,
57
- };
58
- fs.appendFileSync(logFile, JSON.stringify(entry) + '\n');
59
- } catch (e) {
60
- console.error(`[daemon-bootstrap] Failed to emit event: ${e.message}`);
61
- }
62
- }
63
-
64
- function getPlatformKey() {
65
- const plat = process.platform;
66
- if (plat === 'win32') return plat;
67
- if (plat === 'darwin') return plat;
68
- if (plat === 'linux') return plat;
69
- throw new Error(`Unsupported platform: ${plat}`);
70
- }
71
-
72
- function getSessionId() {
73
- return process.env.CLAUDE_SESSION_ID || 'unknown';
74
- }
75
-
76
- function isDaemonRunning(daemonName) {
77
- try {
78
- const plat = getPlatformKey();
79
- if (plat === 'win32') {
80
- const output = execSync('tasklist /FO CSV /NH', { encoding: 'utf8' });
81
- const lines = output.split('\n').filter(Boolean);
82
- return lines.some(line => {
83
- const parts = line.split(',').map(p => p.trim().replace(/^"/, '').replace(/"$/, ''));
84
- return parts[0].includes(daemonName);
85
- });
86
- } else {
87
- try {
88
- execSync(`pgrep -f "${daemonName}" > /dev/null 2>&1`);
89
- return true;
90
- } catch {
91
- return false;
92
- }
93
- }
94
- } catch (e) {
95
- return false;
96
- }
97
- }
98
-
99
- function checkPortReachable(host, port, timeoutMs = 500) {
100
- return spool.execSpool('health', 'health', { timeoutMs, sessionId: getSessionId() })
101
- .then((r) => !!(r && r.ok))
102
- .catch(() => false);
103
- }
104
-
105
- function computeIndexDigest(cwd = process.cwd()) {
106
- try {
107
- let mtimeSum = 0;
108
- const walkDir = (dir) => {
109
- try {
110
- const entries = fs.readdirSync(dir, { withFileTypes: true });
111
- for (const entry of entries) {
112
- if (entry.isDirectory()) {
113
- if (!entry.name.startsWith('.') && entry.name !== 'node_modules') {
114
- walkDir(path.join(dir, entry.name));
115
- }
116
- } else {
117
- const fullPath = path.join(dir, entry.name);
118
- const stat = fs.statSync(fullPath);
119
- mtimeSum += stat.mtimeMs;
120
- }
121
- }
122
- } catch (e) {
123
- return;
124
- }
125
- };
126
-
127
- walkDir(cwd);
128
-
129
- let gitHead = '';
130
- try {
131
- gitHead = execSync('git rev-parse HEAD', { cwd, encoding: 'utf8' }).trim();
132
- } catch {
133
- gitHead = 'unknown';
134
- }
135
-
136
- let dirtyStatus = 'clean';
137
- try {
138
- const porcelain = execSync('git status --porcelain', { cwd, encoding: 'utf8' }).trim();
139
- if (porcelain.length > 0) {
140
- dirtyStatus = 'dirty';
141
- }
142
- } catch {
143
- dirtyStatus = 'unknown';
144
- }
145
-
146
- const digestInput = `${mtimeSum}:${gitHead}:${dirtyStatus}`;
147
- const digest = crypto.createHash('sha256').update(digestInput).digest('hex');
148
- return `v1:${digest}:files=${mtimeSum}`;
149
- } catch (e) {
150
- emitDaemonEvent('digest', 'error', 'Failed to compute digest', { error: e.message });
151
- return '';
152
- }
153
- }
154
-
155
- function writeStatusFile(daemonName, status, sessionId, childPid) {
156
- try {
157
- fs.mkdirSync(GM_STATE_DIR, { recursive: true });
158
- const statusFile = path.join(GM_STATE_DIR, `${daemonName}-status.json`);
159
- const payload = {
160
- daemon: daemonName,
161
- status,
162
- sessionId,
163
- timestamp: new Date().toISOString(),
164
- pid: Number.isFinite(childPid) ? childPid : process.pid,
165
- parent_pid: process.pid,
166
- };
167
- fs.writeFileSync(statusFile, JSON.stringify(payload, null, 2));
168
- emitDaemonEvent(daemonName, 'info', 'Status written', { file: statusFile });
169
- } catch (e) {
170
- emitDaemonEvent(daemonName, 'warn', 'Failed to write status file', { error: e.message });
171
- }
172
- }
173
-
174
- async function ensureRsLearningDaemonRunning() {
175
- const daemonName = 'rs-learn';
176
- const sessionId = getSessionId();
177
- const startTime = Date.now();
178
-
179
- try {
180
- emitDaemonEvent(daemonName, 'info', 'Daemon startup check initiated', { sessionId });
181
-
182
- if (isDaemonRunning(daemonName)) {
183
- emitDaemonEvent(daemonName, 'info', 'Daemon already running', { sessionId });
184
- writeStatusFile(daemonName, 'running', sessionId);
185
- return { ok: true, already_running: true };
186
- }
187
-
188
- emitDaemonEvent(daemonName, 'info', 'Spawning daemon', { sessionId });
189
-
190
- const env = Object.assign({}, process.env, {
191
- CLAUDE_SESSION_ID: sessionId,
192
- });
193
-
194
- // CREATE_NO_WINDOW (0x08000000) is inherited by all descendants —
195
- // .cmd shims that bun-x downloads/launches never get a console window.
196
- // DETACHED_PROCESS (0x00000008) detaches the process group. Windows-only;
197
- // Node ignores creationFlags on POSIX.
198
- const bunExe = resolveWindowsExe('bun');
199
- const useShell = process.platform === 'win32' && /\.(cmd|bat)$/i.test(bunExe);
200
- const proc = spawn(
201
- useShell ? shellQuoteWin(bunExe) : bunExe,
202
- useShell ? ['x', 'rs-learn@latest'].map(shellQuoteWin) : ['x', 'rs-learn@latest'],
203
- {
204
- detached: true,
205
- stdio: 'ignore',
206
- windowsHide: true,
207
- env,
208
- ...(useShell ? { shell: true } : {}),
209
- creationFlags: 0x08000000 | 0x00000008,
210
- }
211
- );
212
-
213
- const pid = proc.pid;
214
- proc.unref();
215
-
216
- emitDaemonEvent(daemonName, 'info', 'Daemon spawned successfully', { pid, sessionId });
217
- writeStatusFile(daemonName, 'started', sessionId);
218
-
219
- return {
220
- ok: true,
221
- pid,
222
- sessionId,
223
- durationMs: Date.now() - startTime,
224
- };
225
- } catch (e) {
226
- emitDaemonEvent(daemonName, 'error', 'Daemon spawn failed', {
227
- error: e.message,
228
- sessionId,
229
- durationMs: Date.now() - startTime,
230
- });
231
- writeStatusFile(daemonName, 'error', sessionId);
232
- throw e;
233
- }
234
- }
235
-
236
- async function ensureRsCodeinsightDaemonRunning() {
237
- const daemonName = 'rs-codeinsight';
238
- const sessionId = getSessionId();
239
- const startTime = Date.now();
240
-
241
- try {
242
- emitDaemonEvent(daemonName, 'info', 'Daemon startup check initiated', { sessionId });
243
-
244
- if (isDaemonRunning(daemonName)) {
245
- emitDaemonEvent(daemonName, 'info', 'Daemon already running', { sessionId });
246
- writeStatusFile(daemonName, 'running', sessionId);
247
- return { ok: true, already_running: true };
248
- }
249
-
250
- emitDaemonEvent(daemonName, 'info', 'Spawning daemon', { sessionId });
251
-
252
- const env = Object.assign({}, process.env, {
253
- CLAUDE_SESSION_ID: sessionId,
254
- });
255
-
256
- const bunExe = resolveWindowsExe('bun');
257
- const useShell = process.platform === 'win32' && /\.(cmd|bat)$/i.test(bunExe);
258
- const proc = spawn(
259
- useShell ? shellQuoteWin(bunExe) : bunExe,
260
- useShell ? ['x', 'rs-codeinsight@latest'].map(shellQuoteWin) : ['x', 'rs-codeinsight@latest'],
261
- {
262
- detached: true,
263
- stdio: 'ignore',
264
- windowsHide: true,
265
- env,
266
- ...(useShell ? { shell: true } : {}),
267
- creationFlags: 0x08000000 | 0x00000008,
268
- }
269
- );
270
-
271
- const pid = proc.pid;
272
- proc.unref();
273
-
274
- emitDaemonEvent(daemonName, 'info', 'Daemon spawned successfully', { pid, sessionId });
275
- writeStatusFile(daemonName, 'started', sessionId);
276
-
277
- return { ok: true, pid, sessionId, durationMs: Date.now() - startTime };
278
- } catch (e) {
279
- emitDaemonEvent(daemonName, 'error', 'Daemon spawn failed', {
280
- error: e.message,
281
- sessionId,
282
- durationMs: Date.now() - startTime,
283
- });
284
- writeStatusFile(daemonName, 'error', sessionId);
285
- throw e;
286
- }
287
- }
288
-
289
- async function ensureRsSearchDaemonRunning() {
290
- const daemonName = 'rs-search';
291
- const sessionId = getSessionId();
292
- const startTime = Date.now();
293
-
294
- try {
295
- emitDaemonEvent(daemonName, 'info', 'Daemon startup check initiated', { sessionId });
296
-
297
- if (isDaemonRunning(daemonName)) {
298
- emitDaemonEvent(daemonName, 'info', 'Daemon already running', { sessionId });
299
- writeStatusFile(daemonName, 'running', sessionId);
300
- return { ok: true, already_running: true };
301
- }
302
-
303
- emitDaemonEvent(daemonName, 'info', 'Spawning daemon', { sessionId });
304
-
305
- const env = Object.assign({}, process.env, {
306
- CLAUDE_SESSION_ID: sessionId,
307
- });
308
-
309
- const bunExe = resolveWindowsExe('bun');
310
- const useShell = process.platform === 'win32' && /\.(cmd|bat)$/i.test(bunExe);
311
- const proc = spawn(
312
- useShell ? shellQuoteWin(bunExe) : bunExe,
313
- useShell ? ['x', 'rs-search@latest'].map(shellQuoteWin) : ['x', 'rs-search@latest'],
314
- {
315
- detached: true,
316
- stdio: 'ignore',
317
- windowsHide: true,
318
- env,
319
- ...(useShell ? { shell: true } : {}),
320
- creationFlags: 0x08000000 | 0x00000008,
321
- }
322
- );
323
-
324
- const pid = proc.pid;
325
- proc.unref();
326
-
327
- emitDaemonEvent(daemonName, 'info', 'Daemon spawned successfully', { pid, sessionId });
328
- writeStatusFile(daemonName, 'started', sessionId);
329
-
330
- return { ok: true, pid, sessionId, durationMs: Date.now() - startTime };
331
- } catch (e) {
332
- emitDaemonEvent(daemonName, 'error', 'Daemon spawn failed', {
333
- error: e.message,
334
- sessionId,
335
- durationMs: Date.now() - startTime,
336
- });
337
- writeStatusFile(daemonName, 'error', sessionId);
338
- throw e;
339
- }
340
- }
341
-
342
- async function ensureRsCodeinsightReady(sessionId = getSessionId()) {
343
- const startTime = Date.now();
344
- const daemonName = 'rs-codeinsight';
345
- const host = '127.0.0.1';
346
- const port = 4802;
347
-
348
- try {
349
- emitDaemonEvent(daemonName, 'info', 'Ensuring daemon readiness', { sessionId, host, port });
350
-
351
- await ensureRsCodeinsightDaemonRunning();
352
-
353
- const maxWaitMs = 30000;
354
- const pollIntervalMs = 500;
355
- const deadline = Date.now() + maxWaitMs;
356
-
357
- while (Date.now() < deadline) {
358
- const reachable = await checkPortReachable(host, port, 1000);
359
- if (reachable) {
360
- emitDaemonEvent(daemonName, 'info', 'Daemon ready', {
361
- host,
362
- port,
363
- elapsedMs: Date.now() - startTime,
364
- sessionId,
365
- });
366
- writeStatusFile(daemonName, 'ready', sessionId);
367
- return { ok: true, host, port, sessionId, durationMs: Date.now() - startTime };
368
- }
369
-
370
- await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
371
- }
372
-
373
- emitDaemonEvent(daemonName, 'warn', 'Timeout waiting for readiness', {
374
- host,
375
- port,
376
- maxWaitMs,
377
- sessionId,
378
- elapsedMs: Date.now() - startTime,
379
- });
380
- writeStatusFile(daemonName, 'timeout', sessionId);
381
- return { ok: false, error: 'Timeout waiting for codeinsight daemon', durationMs: Date.now() - startTime };
382
- } catch (e) {
383
- emitDaemonEvent(daemonName, 'error', 'Failed to ensure readiness', {
384
- error: e.message,
385
- sessionId,
386
- durationMs: Date.now() - startTime,
387
- });
388
- writeStatusFile(daemonName, 'error', sessionId);
389
- return { ok: false, error: e.message, durationMs: Date.now() - startTime };
390
- }
391
- }
392
-
393
- async function ensureBrowserReady(sessionId = getSessionId()) {
394
- const startTime = Date.now();
395
- const host = '127.0.0.1';
396
- const port = 5000;
397
-
398
- try {
399
- emitDaemonEvent('browser', 'info', 'Checking browser readiness', { sessionId, host, port });
400
-
401
- const maxWaitMs = 10000;
402
- const pollIntervalMs = 250;
403
- const deadline = Date.now() + maxWaitMs;
404
-
405
- while (Date.now() < deadline) {
406
- const reachable = await checkPortReachable(host, port, 1000);
407
- if (reachable) {
408
- emitDaemonEvent('browser', 'info', 'Browser ready', {
409
- host,
410
- port,
411
- elapsedMs: Date.now() - startTime,
412
- sessionId,
413
- });
414
- return { ok: true, host, port, sessionId, durationMs: Date.now() - startTime };
415
- }
416
-
417
- await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
418
- }
419
-
420
- emitDaemonEvent('browser', 'warn', 'Browser not available', {
421
- host,
422
- port,
423
- maxWaitMs,
424
- sessionId,
425
- });
426
- return { ok: false, error: 'Browser API not available at 127.0.0.1:5000', durationMs: Date.now() - startTime };
427
- } catch (e) {
428
- emitDaemonEvent('browser', 'error', 'Failed to check browser', {
429
- error: e.message,
430
- sessionId,
431
- });
432
- return { ok: false, error: e.message, durationMs: Date.now() - startTime };
433
- }
434
- }
435
-
436
- module.exports = {
437
- ensureRsCodeinsightDaemonRunning,
438
- ensureRsCodeinsightReady,
439
- ensureRsSearchDaemonRunning,
440
- ensureRsLearningDaemonRunning,
441
- ensureBrowserReady,
442
- checkPortReachable,
443
- };
package/lib/learning.js DELETED
@@ -1,169 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const os = require('os');
4
- const spool = require('./spool.js');
5
-
6
- const RS_LEARN_HOST = '127.0.0.1';
7
- const RS_LEARN_PORT = 4801;
8
- const LOG_DIR = path.join(os.homedir(), '.claude', 'gm-log');
9
-
10
- let daemonBootstrap = null;
11
- function getDaemonBootstrap() {
12
- if (!daemonBootstrap) {
13
- try {
14
- daemonBootstrap = require('./daemon-bootstrap.js');
15
- } catch (e) {
16
- console.error('[learning] Failed to load daemon-bootstrap:', e.message);
17
- return null;
18
- }
19
- }
20
- return daemonBootstrap;
21
- }
22
-
23
- let requestIdCounter = 1000;
24
-
25
- function getRequestId() {
26
- return ++requestIdCounter;
27
- }
28
-
29
- function emitLearningEvent(action, severity, message, details = {}) {
30
- try {
31
- const date = new Date().toISOString().split('T')[0];
32
- const logDir = path.join(LOG_DIR, date);
33
- if (!fs.existsSync(logDir)) {
34
- fs.mkdirSync(logDir, { recursive: true });
35
- }
36
- const logFile = path.join(logDir, 'learning.jsonl');
37
- const entry = {
38
- ts: new Date().toISOString(),
39
- action,
40
- severity,
41
- message,
42
- ...details,
43
- };
44
- fs.appendFileSync(logFile, JSON.stringify(entry) + '\n');
45
- } catch (e) {
46
- console.error(`[learning] Failed to emit event: ${e.message}`);
47
- }
48
- }
49
-
50
- async function ensureDaemonRunning(sessionId = null) {
51
- const sid = sessionId || process.env.CLAUDE_SESSION_ID || 'unknown';
52
- const bootstrap = getDaemonBootstrap();
53
-
54
- if (!bootstrap) {
55
- emitLearningEvent('daemon', 'warn', 'daemon-bootstrap not available, skipping spawn', { sessionId: sid });
56
- return false;
57
- }
58
-
59
- try {
60
- const startTime = Date.now();
61
- const result = await bootstrap.ensureRsLearningDaemonRunning();
62
- emitLearningEvent('daemon', 'info', 'daemon spawn result', {
63
- sessionId: sid,
64
- ok: result.ok,
65
- already_running: result.already_running,
66
- durationMs: Date.now() - startTime,
67
- });
68
- return result.ok === true;
69
- } catch (error) {
70
- emitLearningEvent('daemon', 'warn', 'daemon spawn failed', {
71
- sessionId: sid,
72
- error: error.message,
73
- });
74
- return false;
75
- }
76
- }
77
-
78
- async function checkLearningAvailable(timeoutMs = 500) {
79
- const sessionId = process.env.CLAUDE_SESSION_ID || 'unknown';
80
- try {
81
- const result = await spool.execSpool('health', 'health', { timeoutMs, sessionId });
82
- const ok = !!(result && result.ok);
83
- emitLearningEvent('check', ok ? 'info' : 'warn', ok ? 'rs-learn reachable (spool)' : 'rs-learn unavailable (spool)', { sessionId, timeoutMs });
84
- return ok;
85
- } catch (e) {
86
- emitLearningEvent('check', 'warn', 'rs-learn availability check failed (spool)', { sessionId, timeoutMs, error: e.message });
87
- return false;
88
- }
89
- }
90
-
91
- async function queryLearning(query, discipline = 'default', sessionId = null) {
92
- const sid = sessionId || process.env.CLAUDE_SESSION_ID || 'unknown';
93
- const startTime = Date.now();
94
-
95
- try {
96
- emitLearningEvent('query', 'info', `Learning query: ${query}`, { sessionId: sid, query, discipline });
97
-
98
- const available = await checkLearningAvailable();
99
- if (!available) {
100
- emitLearningEvent('query', 'warn', 'Learning unavailable', { sessionId: sid, query });
101
- return null;
102
- }
103
-
104
- const payload = discipline && discipline !== 'default' ? `@${discipline} ${query}` : query;
105
- const result = await spool.execRecall(payload, { timeoutMs: 5000, sessionId: sid });
106
- if (!result.ok) throw new Error(result.stderr || result.stdout || 'recall failed');
107
-
108
- emitLearningEvent('query', 'info', 'Learning query completed', {
109
- sessionId: sid,
110
- query,
111
- discipline,
112
- hitCount: (result.stdout || '').length > 0 ? 1 : 0,
113
- durationMs: Date.now() - startTime,
114
- });
115
-
116
- return result.stdout || '';
117
- } catch (error) {
118
- emitLearningEvent('query', 'error', `Learning query failed: ${query}`, {
119
- sessionId: sid,
120
- query,
121
- error: error.message,
122
- durationMs: Date.now() - startTime,
123
- });
124
- return null;
125
- }
126
- }
127
-
128
- async function persistLearning(fact, discipline = 'default', sessionId = null) {
129
- const sid = sessionId || process.env.CLAUDE_SESSION_ID || 'unknown';
130
- const startTime = Date.now();
131
-
132
- try {
133
- emitLearningEvent('persist', 'info', 'Learning persist initiated', { sessionId: sid, discipline, factLength: fact.length });
134
-
135
- const available = await checkLearningAvailable();
136
- if (!available) {
137
- emitLearningEvent('persist', 'error', 'Learning unavailable for persist', { sessionId: sid, discipline });
138
- throw new Error('rs-learn daemon not available');
139
- }
140
-
141
- const payload = discipline && discipline !== 'default' ? `@${discipline}\n${fact}` : fact;
142
- const result = await spool.execMemorize(payload, { timeoutMs: 5000, sessionId: sid });
143
- if (!result.ok) throw new Error(result.stderr || result.stdout || 'memorize failed');
144
-
145
- emitLearningEvent('persist', 'info', 'Learning persist completed', {
146
- sessionId: sid,
147
- discipline,
148
- durationMs: Date.now() - startTime,
149
- });
150
-
151
- return result;
152
- } catch (error) {
153
- emitLearningEvent('persist', 'error', `Learning persist failed: ${error.message}`, {
154
- sessionId: sid,
155
- discipline,
156
- durationMs: Date.now() - startTime,
157
- });
158
- throw error;
159
- }
160
- }
161
-
162
- module.exports = {
163
- checkLearningAvailable,
164
- queryLearning,
165
- persistLearning,
166
- ensureDaemonRunning,
167
- RS_LEARN_HOST,
168
- RS_LEARN_PORT,
169
- };