gm-skill 0.1.2 → 2.0.1080

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.
Files changed (85) hide show
  1. package/AGENTS.md +1 -0
  2. package/LICENSE +21 -0
  3. package/README.md +20 -84
  4. package/agents/gm.md +22 -0
  5. package/agents/memorize.md +100 -0
  6. package/agents/research-worker.md +36 -0
  7. package/agents/textprocessing.md +47 -0
  8. package/bin/bootstrap.js +702 -0
  9. package/bin/plugkit.js +136 -0
  10. package/bin/plugkit.sha256 +7 -0
  11. package/bin/plugkit.version +1 -0
  12. package/bin/plugkit.wasm +0 -0
  13. package/bin/plugkit.wasm.sha256 +1 -0
  14. package/bin/rtk.sha256 +6 -0
  15. package/bin/rtk.version +1 -0
  16. package/gm-plugkit/bootstrap.js +694 -0
  17. package/gm-plugkit/cli.js +48 -0
  18. package/gm-plugkit/index.js +12 -0
  19. package/gm-plugkit/package.json +26 -0
  20. package/gm-plugkit/plugkit-wasm-wrapper.js +190 -0
  21. package/gm-plugkit/plugkit.sha256 +6 -0
  22. package/gm-plugkit/plugkit.version +1 -0
  23. package/gm.json +27 -0
  24. package/lang/browser.js +45 -0
  25. package/lang/ssh.js +166 -0
  26. package/lib/browser-spool-handler.js +130 -0
  27. package/lib/browser.js +131 -0
  28. package/lib/codeinsight.js +109 -0
  29. package/lib/daemon-bootstrap.js +253 -132
  30. package/lib/git.js +0 -1
  31. package/lib/learning.js +169 -0
  32. package/lib/skill-bootstrap.js +406 -0
  33. package/lib/spool-dispatch.js +100 -0
  34. package/lib/spool.js +87 -49
  35. package/lib/wasm-host.js +241 -0
  36. package/package.json +38 -20
  37. package/prompts/bash-deny.txt +22 -0
  38. package/prompts/pre-compact.txt +21 -0
  39. package/prompts/prompt-submit.txt +83 -0
  40. package/prompts/session-start.txt +15 -0
  41. package/scripts/run-hook.sh +7 -0
  42. package/scripts/watch-cascade.js +166 -0
  43. package/skills/browser/SKILL.md +80 -0
  44. package/skills/code-search/SKILL.md +48 -0
  45. package/skills/create-lang-plugin/SKILL.md +121 -0
  46. package/skills/gm/SKILL.md +10 -49
  47. package/skills/gm-complete/SKILL.md +16 -87
  48. package/skills/gm-emit/SKILL.md +17 -50
  49. package/skills/gm-execute/SKILL.md +18 -69
  50. package/skills/gm-skill/SKILL.md +43 -0
  51. package/skills/gm-skill/index.js +21 -0
  52. package/skills/governance/SKILL.md +97 -0
  53. package/skills/pages/SKILL.md +208 -0
  54. package/skills/planning/SKILL.md +21 -97
  55. package/skills/research/SKILL.md +43 -0
  56. package/skills/ssh/SKILL.md +71 -0
  57. package/skills/textprocessing/SKILL.md +40 -0
  58. package/skills/update-docs/SKILL.md +24 -43
  59. package/gm-complete.SKILL.md +0 -106
  60. package/gm-emit.SKILL.md +0 -70
  61. package/gm-execute.SKILL.md +0 -88
  62. package/gm.SKILL.md +0 -63
  63. package/index.js +0 -1
  64. package/lib/index.js +0 -37
  65. package/lib/loader.js +0 -66
  66. package/lib/manifest.js +0 -99
  67. package/lib/prepare.js +0 -14
  68. package/planning.SKILL.md +0 -118
  69. package/skills/gm/index.js +0 -113
  70. package/skills/gm-complete/index.js +0 -118
  71. package/skills/gm-complete.SKILL.md +0 -106
  72. package/skills/gm-emit/index.js +0 -90
  73. package/skills/gm-emit.SKILL.md +0 -70
  74. package/skills/gm-execute/index.js +0 -91
  75. package/skills/gm-execute.SKILL.md +0 -88
  76. package/skills/gm.SKILL.md +0 -63
  77. package/skills/planning/index.js +0 -107
  78. package/skills/planning.SKILL.md +0 -118
  79. package/skills/update-docs/index.js +0 -108
  80. package/skills/update-docs.SKILL.md +0 -66
  81. package/test-build.js +0 -29
  82. package/test-e2e.js +0 -117
  83. package/test-unified.js +0 -24
  84. package/test.js +0 -89
  85. package/update-docs.SKILL.md +0 -66
@@ -0,0 +1,169 @@
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
+ };
@@ -0,0 +1,406 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const https = require('https');
4
+ const { execSync, spawn } = require('child_process');
5
+ const crypto = require('crypto');
6
+ const os = require('os');
7
+ const spool = require('./spool.js');
8
+
9
+ const PLUGKIT_TOOLS_DIR = path.join(os.homedir(), '.claude', 'gm-tools');
10
+ const PLUGKIT_VERSION_FILE = path.join(PLUGKIT_TOOLS_DIR, 'plugkit.version');
11
+ const PLUGKIT_WASM_PATH = path.join(PLUGKIT_TOOLS_DIR, 'plugkit.wasm');
12
+ const PLUGKIT_WASM_WRAPPER = path.join(PLUGKIT_TOOLS_DIR, 'plugkit-wasm-wrapper.js');
13
+ const BOOTSTRAP_STATUS_FILE = path.join(os.homedir(), '.gm', 'bootstrap-status.json');
14
+ const BOOTSTRAP_ERROR_FILE = path.join(os.homedir(), '.gm', 'bootstrap-error.json');
15
+ const LOG_DIR = path.join(os.homedir(), '.claude', 'gm-log');
16
+
17
+ function getPlugkitPath() {
18
+ if (fs.existsSync(PLUGKIT_WASM_WRAPPER) && fs.existsSync(PLUGKIT_WASM_PATH)) {
19
+ return PLUGKIT_WASM_WRAPPER;
20
+ }
21
+ throw new Error(`plugkit WASM not found at ${PLUGKIT_WASM_PATH}`);
22
+ }
23
+
24
+ function emitBootstrapEvent(severity, message, details) {
25
+ try {
26
+ const date = new Date().toISOString().split('T')[0];
27
+ const logDir = path.join(LOG_DIR, date);
28
+ if (!fs.existsSync(logDir)) {
29
+ fs.mkdirSync(logDir, { recursive: true });
30
+ }
31
+ const logFile = path.join(logDir, 'bootstrap.jsonl');
32
+ const entry = {
33
+ ts: new Date().toISOString(),
34
+ severity,
35
+ message,
36
+ ...details,
37
+ };
38
+ fs.appendFileSync(logFile, JSON.stringify(entry) + '\n');
39
+ } catch (e) {
40
+ console.error(`[bootstrap] Failed to emit event: ${e.message}`);
41
+ }
42
+ }
43
+
44
+ function readManifest() {
45
+ try {
46
+ const gmJsonPath = path.join(process.cwd(), 'gm-starter', 'gm.json');
47
+ if (!fs.existsSync(gmJsonPath)) {
48
+ throw new Error('gm-starter/gm.json not found');
49
+ }
50
+ const gm = JSON.parse(fs.readFileSync(gmJsonPath, 'utf8'));
51
+ const version = gm.plugkitVersion;
52
+
53
+ const sha256Path = path.join(process.cwd(), 'gm-starter', 'bin', 'plugkit.wasm.sha256');
54
+ if (!fs.existsSync(sha256Path)) {
55
+ throw new Error('gm-starter/bin/plugkit.wasm.sha256 not found');
56
+ }
57
+ const sha256Content = fs.readFileSync(sha256Path, 'utf8').trim();
58
+ const expectedHash = sha256Content.split(/\s+/)[0];
59
+
60
+ return { version, expectedHash };
61
+ } catch (e) {
62
+ emitBootstrapEvent('error', 'Failed to read manifest', { error: e.message });
63
+ throw e;
64
+ }
65
+ }
66
+
67
+ function getInstalledVersion() {
68
+ try {
69
+ if (fs.existsSync(PLUGKIT_VERSION_FILE)) {
70
+ return fs.readFileSync(PLUGKIT_VERSION_FILE, 'utf8').trim();
71
+ }
72
+ return null;
73
+ } catch (e) {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ function computeFileHash(filePath) {
79
+ const content = fs.readFileSync(filePath);
80
+ return crypto.createHash('sha256').update(content).digest('hex');
81
+ }
82
+
83
+ async function downloadPlugkitBinary(version) {
84
+ const binaryName = 'plugkit.wasm';
85
+ const url = `https://github.com/AnEntrypoint/plugkit-bin/releases/download/v${version}/${binaryName}`;
86
+
87
+ emitBootstrapEvent('info', 'Starting WASM download', { version, url });
88
+
89
+ return new Promise((resolve, reject) => {
90
+ https
91
+ .get(url, { timeout: 30000 }, (res) => {
92
+ if (res.statusCode === 404) {
93
+ reject(new Error(`WASM not found: v${version}`));
94
+ return;
95
+ }
96
+ if (res.statusCode !== 200) {
97
+ reject(new Error(`HTTP ${res.statusCode} downloading plugkit.wasm`));
98
+ return;
99
+ }
100
+
101
+ const chunks = [];
102
+ res.on('data', (chunk) => chunks.push(chunk));
103
+ res.on('end', () => {
104
+ const data = Buffer.concat(chunks);
105
+ emitBootstrapEvent('info', 'WASM download complete', { bytes: data.length });
106
+ resolve(data);
107
+ });
108
+ })
109
+ .on('error', (e) => {
110
+ emitBootstrapEvent('error', 'Download failed', { error: e.message });
111
+ reject(e);
112
+ });
113
+ });
114
+ }
115
+
116
+ function isProcessRunning(pidOrName) {
117
+ try {
118
+ const plat = getPlatformKey();
119
+ if (plat === 'win32') {
120
+ const output = execSync('tasklist /FO CSV /NH', { encoding: 'utf8' });
121
+ const lines = output.split('\n').filter(Boolean);
122
+ return lines.some(line => {
123
+ const parts = line.split(',').map(p => p.trim().replace(/^"/, '').replace(/"$/, ''));
124
+ return parts[0] === 'plugkit.exe' || parts[0] === pidOrName;
125
+ });
126
+ } else {
127
+ try {
128
+ execSync(`ps -p ${pidOrName} > /dev/null 2>&1`);
129
+ return true;
130
+ } catch {
131
+ return false;
132
+ }
133
+ }
134
+ } catch (e) {
135
+ return false;
136
+ }
137
+ }
138
+
139
+ function killExistingPlugkit() {
140
+ try {
141
+ const plat = getPlatformKey();
142
+ if (plat === 'win32') {
143
+ execSync('taskkill /IM plugkit.exe /F 2>nul || true', { shell: true });
144
+ emitBootstrapEvent('info', 'Killed existing plugkit process on Windows');
145
+ } else {
146
+ execSync('pkill -f "plugkit" || true', { shell: true });
147
+ emitBootstrapEvent('info', 'Killed existing plugkit process on Unix');
148
+ }
149
+ } catch (e) {
150
+ emitBootstrapEvent('warn', 'Failed to kill existing plugkit', { error: e.message });
151
+ }
152
+ }
153
+
154
+ async function ensureBinaryWritable(filePath) {
155
+ try {
156
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
157
+ if (fs.existsSync(filePath)) {
158
+ fs.unlinkSync(filePath);
159
+ }
160
+ } catch (e) {
161
+ throw new Error(`Cannot write to ${filePath}: ${e.message}`);
162
+ }
163
+ }
164
+
165
+ async function writeBinaryWithRetry(filePath, data, maxRetries = 3) {
166
+ let lastErr;
167
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
168
+ try {
169
+ await ensureBinaryWritable(filePath);
170
+ fs.writeFileSync(filePath, data);
171
+ fs.chmodSync(filePath, 0o755);
172
+ emitBootstrapEvent('info', 'Binary written successfully', { path: filePath });
173
+ return;
174
+ } catch (e) {
175
+ lastErr = e;
176
+ emitBootstrapEvent('warn', `Write attempt ${attempt + 1} failed`, { error: e.message });
177
+ if (attempt < maxRetries - 1) {
178
+ await new Promise(r => setTimeout(r, 50 * Math.pow(2, attempt)));
179
+ }
180
+ }
181
+ }
182
+ throw lastErr;
183
+ }
184
+
185
+ async function verifyBinaryHealth(filePath) {
186
+ try {
187
+ if (!fs.existsSync(filePath)) {
188
+ throw new Error(`File not found: ${filePath}`);
189
+ }
190
+ const stat = fs.statSync(filePath);
191
+ if (stat.size < 1024) {
192
+ throw new Error(`File too small: ${stat.size} bytes`);
193
+ }
194
+ emitBootstrapEvent('info', 'Binary health check passed', { size: stat.size });
195
+ return true;
196
+ } catch (e) {
197
+ emitBootstrapEvent('warn', 'Binary health check failed', { error: e.message });
198
+ return false;
199
+ }
200
+ }
201
+
202
+ async function spawnPlugkitWatcher(wasmPath) {
203
+ try {
204
+ emitBootstrapEvent('info', 'Spawning plugkit WASM watcher daemon');
205
+
206
+ let wrapperPath;
207
+ try {
208
+ const gmPlugkit = require('gm-plugkit');
209
+ wrapperPath = path.join(path.dirname(gmPlugkit.getPath ? gmPlugkit.getPath() : require.resolve('gm-plugkit')), 'plugkit-wasm-wrapper.js');
210
+ } catch (e) {
211
+ emitBootstrapEvent('warn', 'gm-plugkit npm not available, using bundled wrapper', { error: e.message });
212
+ wrapperPath = path.join(path.dirname(wasmPath), 'plugkit-wasm-wrapper.js');
213
+ }
214
+
215
+ if (!fs.existsSync(wrapperPath)) {
216
+ throw new Error(`WASM wrapper not found at ${wrapperPath}`);
217
+ }
218
+
219
+ const runtime = process.platform === 'win32' ? 'bun.exe' : 'bun';
220
+ const proc = spawn(runtime, [wrapperPath, 'spool'], {
221
+ detached: true,
222
+ stdio: 'ignore',
223
+ windowsHide: true,
224
+ env: { ...process.env, CLAUDE_PROJECT_DIR: process.cwd() },
225
+ });
226
+
227
+ const pid = proc.pid;
228
+ proc.unref();
229
+
230
+ emitBootstrapEvent('info', 'Plugkit WASM watcher spawned', { pid });
231
+ return pid;
232
+ } catch (e) {
233
+ emitBootstrapEvent('error', 'Failed to spawn plugkit WASM watcher', { error: e.message });
234
+ throw e;
235
+ }
236
+ }
237
+
238
+ async function bootstrapPlugkit() {
239
+ const startTime = Date.now();
240
+
241
+ try {
242
+ emitBootstrapEvent('info', 'Bootstrap started');
243
+
244
+ const { version: manifestVersion, expectedHash } = readManifest();
245
+ const installedVersion = getInstalledVersion();
246
+ const plugkitPath = getPlugkitPath();
247
+
248
+ const versionMismatch = installedVersion !== manifestVersion;
249
+ const binaryMissing = !fs.existsSync(plugkitPath);
250
+
251
+ if (!binaryMissing && !versionMismatch) {
252
+ emitBootstrapEvent('info', 'Binary up-to-date', { version: installedVersion });
253
+
254
+ if (isProcessRunning('plugkit')) {
255
+ emitBootstrapEvent('info', 'Plugkit watcher already running');
256
+ const statusPayload = {
257
+ ok: true,
258
+ version: installedVersion,
259
+ status: 'running',
260
+ timestamp: new Date().toISOString(),
261
+ durationMs: Date.now() - startTime,
262
+ };
263
+ fs.mkdirSync(path.dirname(BOOTSTRAP_STATUS_FILE), { recursive: true });
264
+ fs.writeFileSync(BOOTSTRAP_STATUS_FILE, JSON.stringify(statusPayload, null, 2));
265
+ return { ok: true };
266
+ }
267
+ }
268
+
269
+ if (binaryMissing || versionMismatch) {
270
+ emitBootstrapEvent('info', 'Downloading binary', {
271
+ reason: binaryMissing ? 'missing' : 'version-mismatch',
272
+ version: manifestVersion,
273
+ });
274
+
275
+ let binaryData;
276
+ try {
277
+ binaryData = await downloadPlugkitBinary(manifestVersion);
278
+ } catch (downloadErr) {
279
+ emitBootstrapEvent('error', 'Download failed, checking for cached binary', {
280
+ error: downloadErr.message,
281
+ fallback: fs.existsSync(plugkitPath),
282
+ });
283
+
284
+ if (!fs.existsSync(plugkitPath)) {
285
+ throw downloadErr;
286
+ }
287
+ emitBootstrapEvent('info', 'Using cached binary as fallback');
288
+ binaryData = null;
289
+ }
290
+
291
+ if (binaryData) {
292
+ const downloadedHash = crypto.createHash('sha256').update(binaryData).digest('hex');
293
+ if (downloadedHash !== expectedHash) {
294
+ throw new Error(`Hash mismatch: got ${downloadedHash}, expected ${expectedHash}`);
295
+ }
296
+
297
+ killExistingPlugkit();
298
+ await writeBinaryWithRetry(plugkitPath, binaryData);
299
+
300
+ fs.mkdirSync(path.dirname(PLUGKIT_VERSION_FILE), { recursive: true });
301
+ fs.writeFileSync(PLUGKIT_VERSION_FILE, manifestVersion + '\n');
302
+ emitBootstrapEvent('info', 'Binary installed', { version: manifestVersion });
303
+ }
304
+ }
305
+
306
+ const isHealthy = await verifyBinaryHealth(plugkitPath);
307
+ if (!isHealthy) {
308
+ emitBootstrapEvent('warn', 'Binary health check failed, but proceeding');
309
+ }
310
+
311
+ const watcherRunning = isProcessRunning('plugkit');
312
+ let watcherPid;
313
+ if (!watcherRunning) {
314
+ watcherPid = await spawnPlugkitWatcher(plugkitPath);
315
+ } else {
316
+ watcherPid = 'already-running';
317
+ emitBootstrapEvent('info', 'Watcher already running');
318
+ }
319
+
320
+ const currentVersion = getInstalledVersion() || manifestVersion;
321
+ const statusPayload = {
322
+ ok: true,
323
+ version: currentVersion,
324
+ watcherPid,
325
+ timestamp: new Date().toISOString(),
326
+ durationMs: Date.now() - startTime,
327
+ };
328
+
329
+ fs.mkdirSync(path.dirname(BOOTSTRAP_STATUS_FILE), { recursive: true });
330
+ fs.writeFileSync(BOOTSTRAP_STATUS_FILE, JSON.stringify(statusPayload, null, 2));
331
+
332
+ emitBootstrapEvent('info', 'Bootstrap completed successfully', statusPayload);
333
+ return { ok: true };
334
+ } catch (err) {
335
+ const errorPayload = {
336
+ ok: false,
337
+ error: err.message,
338
+ timestamp: new Date().toISOString(),
339
+ durationMs: Date.now() - startTime,
340
+ stack: err.stack,
341
+ };
342
+
343
+ fs.mkdirSync(path.dirname(BOOTSTRAP_ERROR_FILE), { recursive: true });
344
+ fs.writeFileSync(BOOTSTRAP_ERROR_FILE, JSON.stringify(errorPayload, null, 2));
345
+
346
+ emitBootstrapEvent('error', 'Bootstrap failed', errorPayload);
347
+ console.error(`[skill-bootstrap] ${err.message}`);
348
+
349
+ return { ok: false, error: err.message };
350
+ }
351
+ }
352
+
353
+ async function checkPortReachable(host, port, timeoutMs = 500) {
354
+ try {
355
+ const result = await spool.execSpool('health', 'health', { timeoutMs, sessionId: process.env.CLAUDE_SESSION_ID || 'unknown' });
356
+ return !!(result && result.ok);
357
+ } catch (e) {
358
+ return false;
359
+ }
360
+ }
361
+
362
+ async function bootstrapAcptoapi() {
363
+ const port = 4800;
364
+ const running = await checkPortReachable('127.0.0.1', port);
365
+ if (running) return { ok: true, status: 'already-running' };
366
+
367
+ emitBootstrapEvent('info', 'Spawning acptoapi daemon');
368
+ try {
369
+ const child = spawn('bun', ['x', 'acptoapi@latest'], {
370
+ detached: true,
371
+ stdio: 'ignore',
372
+ windowsHide: true,
373
+ });
374
+ child.unref();
375
+ emitBootstrapEvent('info', 'acptoapi spawned', { pid: child.pid });
376
+ return { ok: true, status: 'spawned', pid: child.pid };
377
+ } catch (e) {
378
+ emitBootstrapEvent('error', 'Failed to spawn acptoapi', { error: e.message });
379
+ return { ok: false, error: e.message };
380
+ }
381
+ }
382
+
383
+ async function getSnapshot(sessionId, cwd) {
384
+ const plugkitPath = getPlugkitPath();
385
+ if (!fs.existsSync(plugkitPath)) {
386
+ return { git: { ok: false }, tasks: [], error: 'plugkit not found' };
387
+ }
388
+
389
+ try {
390
+ const sid = sessionId || process.env.CLAUDE_SESSION_ID || 'default';
391
+ const c = cwd || process.cwd();
392
+ const cmd = `"${plugkitPath}" snapshot --session "${sid}" --cwd "${c}"`;
393
+ const output = execSync(cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
394
+ return JSON.parse(output);
395
+ } catch (e) {
396
+ emitBootstrapEvent('warn', 'Failed to get snapshot', { error: e.message });
397
+ return { git: { ok: false }, tasks: [], error: e.message };
398
+ }
399
+ }
400
+
401
+ module.exports = {
402
+ bootstrapPlugkit,
403
+ bootstrapAcptoapi,
404
+ getSnapshot,
405
+ checkPortReachable
406
+ };
@@ -0,0 +1,100 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ async function dispatchSpool(cmd, lang, body, timeoutMs, sessionId) {
6
+ const taskId = `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
7
+ const langDir = lang.match(/^(nodejs|python|bash|typescript|go|rust|c|cpp|java|deno)$/) ? lang : 'nodejs';
8
+ const ext = {
9
+ nodejs: 'js',
10
+ python: 'py',
11
+ bash: 'sh',
12
+ typescript: 'ts',
13
+ go: 'go',
14
+ rust: 'rs',
15
+ c: 'c',
16
+ cpp: 'cpp',
17
+ java: 'java',
18
+ deno: 'ts'
19
+ }[langDir] || 'js';
20
+
21
+ const inDir = path.join(process.cwd(), '.gm', 'exec-spool', 'in', langDir);
22
+ const outDir = path.join(process.cwd(), '.gm', 'exec-spool', 'out');
23
+ const inFile = path.join(inDir, `${taskId}.${ext}`);
24
+ const jsonFile = path.join(outDir, `${taskId}.json`);
25
+
26
+ fs.mkdirSync(inDir, { recursive: true });
27
+ fs.mkdirSync(outDir, { recursive: true });
28
+
29
+ const code = sessionId ? `const SESSION_ID = '${sessionId}';\n${body}` : body;
30
+ fs.writeFileSync(inFile, code, 'utf8');
31
+
32
+ return pollForCompletion(jsonFile, timeoutMs, taskId);
33
+ }
34
+
35
+ async function pollForCompletion(jsonFile, timeoutMs, taskId) {
36
+ const start = Date.now();
37
+ const interval = 50;
38
+
39
+ while (Date.now() - start < timeoutMs) {
40
+ if (fs.existsSync(jsonFile)) {
41
+ try {
42
+ const metadata = JSON.parse(fs.readFileSync(jsonFile, 'utf8'));
43
+ const outFile = jsonFile.replace(/\.json$/, '.out');
44
+ const errFile = jsonFile.replace(/\.json$/, '.err');
45
+ const stdout = fs.existsSync(outFile) ? fs.readFileSync(outFile, 'utf8') : '';
46
+ const stderr = fs.existsSync(errFile) ? fs.readFileSync(errFile, 'utf8') : '';
47
+ return {
48
+ ok: metadata.exitCode === 0 && !metadata.timedOut,
49
+ exitCode: metadata.exitCode,
50
+ stdout,
51
+ stderr,
52
+ durationMs: metadata.durationMs,
53
+ taskId,
54
+ timedOut: metadata.timedOut || false
55
+ };
56
+ } catch (e) {
57
+ await new Promise(r => setTimeout(r, interval));
58
+ }
59
+ } else {
60
+ await new Promise(r => setTimeout(r, interval));
61
+ }
62
+ }
63
+
64
+ return {
65
+ ok: false,
66
+ exitCode: -1,
67
+ stdout: '',
68
+ stderr: `[spool dispatch timeout after ${timeoutMs}ms]`,
69
+ durationMs: Date.now() - start,
70
+ taskId,
71
+ timedOut: true
72
+ };
73
+ }
74
+
75
+ function checkDispatchGates(sessionId, operation) {
76
+ const gm = path.join(process.cwd(), '.gm');
77
+ const prdPath = path.join(gm, 'prd.yml');
78
+ const mutsPath = path.join(gm, 'mutables.yml');
79
+ const needsGmPath = path.join(gm, 'needs-gm');
80
+ const gmFiredPath = path.join(gm, `gm-fired-${sessionId}`);
81
+
82
+ if (!['write', 'edit', 'git'].includes(operation)) return { allowed: true };
83
+
84
+ if (fs.existsSync(prdPath) && fs.existsSync(needsGmPath) && !fs.existsSync(gmFiredPath)) {
85
+ return { allowed: false, reason: 'gm orchestration in progress; skills must complete work before tools execute' };
86
+ }
87
+
88
+ if (fs.existsSync(mutsPath)) {
89
+ try {
90
+ const content = fs.readFileSync(mutsPath, 'utf8');
91
+ if (content.includes('status: unknown')) {
92
+ return { allowed: false, reason: 'unresolved mutables block tool execution; resolve all mutables before proceeding' };
93
+ }
94
+ } catch (_) {}
95
+ }
96
+
97
+ return { allowed: true };
98
+ }
99
+
100
+ module.exports = { dispatchSpool, checkDispatchGates };