gm-skill 2.0.1469 → 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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-plugkit",
3
- "version": "2.0.1469",
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.1469",
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.1469",
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",
@@ -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
- };