swarmdo 1.58.18 → 1.58.23

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.
@@ -2,15 +2,6 @@
2
2
  /**
3
3
  * Swarmdo Hook Handler (Cross-Platform)
4
4
  * Dispatches hook events to the appropriate helper modules.
5
- *
6
- * Usage: node hook-handler.cjs <command> [args...]
7
- *
8
- * Commands:
9
- * route - Route a task to optimal agent (reads PROMPT from env/stdin)
10
- * pre-bash - Validate command safety before execution
11
- * post-edit - Record edit outcome for learning
12
- * session-restore - Restore previous session state
13
- * session-end - End session and persist state
14
5
  */
15
6
 
16
7
  const path = require('path');
@@ -18,9 +9,6 @@ const fs = require('fs');
18
9
 
19
10
  const helpersDir = __dirname;
20
11
 
21
- // Safe require with stdout suppression - the helper modules have CLI
22
- // sections that run unconditionally on require(), so we mute console
23
- // during the require to prevent noisy output.
24
12
  function safeRequire(modulePath) {
25
13
  try {
26
14
  if (fs.existsSync(modulePath)) {
@@ -42,43 +30,52 @@ function safeRequire(modulePath) {
42
30
  return null;
43
31
  }
44
32
 
45
- const router = safeRequire(path.join(helpersDir, 'router.js'));
46
- const session = safeRequire(path.join(helpersDir, 'session.js'));
47
- const memory = safeRequire(path.join(helpersDir, 'memory.js'));
33
+ const router = safeRequire(path.join(helpersDir, 'router.cjs'));
34
+ const session = safeRequire(path.join(helpersDir, 'session.cjs'));
35
+ const memory = safeRequire(path.join(helpersDir, 'memory.cjs'));
48
36
  const intelligence = safeRequire(path.join(helpersDir, 'intelligence.cjs'));
37
+ const agentBridge = safeRequire(path.join(helpersDir, 'agent-bridge-hook.cjs'));
49
38
 
50
39
  // ── Intelligence timeout protection (fixes #1530, #1531) ───────────────────
51
- const INTELLIGENCE_TIMEOUT_MS = 3000;
52
- // Race the (possibly-async) work against a real timeout. The previous version
53
- // called fn() and clearTimeout(timer) immediately, so an async fn returned a
54
- // pending promise that resolved THROUGH the race — the timeout protected
55
- // nothing. This settles on whichever finishes first, then clears the timer.
40
+ var INTELLIGENCE_TIMEOUT_MS = 3000;
41
+ // Race the (possibly-async) work against a real timeout.
56
42
  //
57
- // LIMITATION: a synchronous blocking fn (the current intelligence.init() does
58
- // blocking fs reads) cannot be interrupted by any in-process timer the event
59
- // loop is blocked. The real guard for that case is the readJSON file-size
60
- // limit in intelligence.cjs. This util only bounds work that yields (async I/O).
43
+ // The previous implementation called `fn()` and then `clearTimeout(timer)`
44
+ // immediately but if `fn()` returns a *pending* promise (async work), the
45
+ // timer was cancelled before the work settled and the promise resolved with the
46
+ // still-pending promise, so the timeout protected nothing. This version only
47
+ // settles when the work resolves OR the timeout fires, whichever comes first,
48
+ // and clears the timer afterwards.
49
+ //
50
+ // LIMITATION (be honest): a *synchronous* CPU-bound `fn` (e.g. the current
51
+ // intelligence.init(), which does blocking fs reads) cannot be interrupted by
52
+ // any in-process timer — the event loop is blocked, so the timeout callback
53
+ // can't run until `fn` already returned. The real guard for that case lives in
54
+ // intelligence.cjs (MAX_DATA_FILE_SIZE / MAX_GRAPH_NODES caps). This util only
55
+ // bounds work that yields to the event loop (async I/O, dynamic import, etc.).
61
56
  function runWithTimeout(fn, label) {
62
- let timer;
63
- const timeout = new Promise((resolve) => {
64
- timer = setTimeout(() => {
57
+ var timer;
58
+ var timeout = new Promise(function(resolve) {
59
+ timer = setTimeout(function() {
65
60
  process.stderr.write("[WARN] " + label + " timed out after " + INTELLIGENCE_TIMEOUT_MS + "ms, skipping\n");
66
61
  resolve(null);
67
62
  }, INTELLIGENCE_TIMEOUT_MS);
68
63
  });
69
- const work = Promise.resolve().then(fn).catch(() => null);
70
- return Promise.race([work, timeout]).then((result) => {
64
+ // Promise.resolve().then(fn) turns a synchronous throw into a rejection so it
65
+ // is handled here rather than escaping the Promise constructor.
66
+ var work = Promise.resolve().then(fn).catch(function() { return null; });
67
+ return Promise.race([work, timeout]).then(function(result) {
71
68
  clearTimeout(timer);
72
69
  return result;
73
70
  });
74
71
  }
75
72
 
76
73
 
77
- // Get the command from argv
78
74
  const [,, command, ...args] = process.argv;
79
75
 
80
- // Read stdin with timeout — Claude Code sends hook data as JSON via stdin.
81
- // Timeout prevents hanging when stdin is not properly closed (common on Windows).
76
+ // Read stdin — Claude Code sends hook data as JSON via stdin
77
+ // Uses a timeout to prevent hanging when stdin is in an ambiguous state
78
+ // (not TTY, not a proper pipe) which happens with Claude Code hook invocations.
82
79
  async function readStdin() {
83
80
  if (process.stdin.isTTY) return '';
84
81
  return new Promise((resolve) => {
@@ -98,11 +95,11 @@ async function readStdin() {
98
95
 
99
96
  async function main() {
100
97
  // Global safety timeout: hooks must NEVER hang (#1530, #1531)
101
- const safetyTimer = setTimeout(() => {
98
+ var safetyTimer = setTimeout(function() {
102
99
  process.stderr.write("[WARN] Hook handler global timeout (5s), forcing exit\n");
103
100
  process.exit(0);
104
101
  }, 5000);
105
- safetyTimer.unref(); // don't keep process alive just for this timer
102
+ safetyTimer.unref();
106
103
 
107
104
  let stdinData = '';
108
105
  try { stdinData = await readStdin(); } catch (e) { /* ignore stdin errors */ }
@@ -112,21 +109,21 @@ async function main() {
112
109
  try { hookInput = JSON.parse(stdinData); } catch (e) { /* ignore parse errors */ }
113
110
  }
114
111
 
112
+ // Merge stdin data into prompt resolution: prefer stdin fields, then env vars.
113
+ // NEVER fall back to argv args — shell glob expansion of braces in bash output
114
+ // creates junk files (#1342). Use env vars or stdin only.
115
115
  // Normalize snake_case/camelCase: Claude Code sends tool_input/tool_name (snake_case)
116
- const toolInput = hookInput.toolInput || hookInput.tool_input || {};
117
- const toolName = hookInput.toolName || hookInput.tool_name || '';
116
+ var toolInput = hookInput.toolInput || hookInput.tool_input || {};
117
+ var toolName = hookInput.toolName || hookInput.tool_name || '';
118
118
 
119
- // Merge stdin data into prompt resolution: prefer stdin fields, then env, then argv.
120
- // `toolInput` is an object (e.g. {command:"ls"}) — it's truthy but not a string,
121
- // so falling back to it directly bound `prompt` to the object and tripped
122
- // `.toLowerCase()` / `.substring()` on every Bash hook (#1944). Use the
123
- // `.command` field instead, which is the actual string the hook needs.
124
- const prompt = hookInput.prompt || hookInput.command || toolInput.command
125
- || process.env.PROMPT || process.env.TOOL_INPUT_command || args.join(' ') || '';
119
+ // `toolInput` is an object (e.g. {command:"ls"}) falling back to it
120
+ // directly bound `prompt` to the object and tripped `.toLowerCase()` /
121
+ // `.substring()` on every Bash hook (#1944). Use the `.command` field.
122
+ var prompt = hookInput.prompt || hookInput.command || toolInput.command
123
+ || process.env.PROMPT || process.env.TOOL_INPUT_command || '';
126
124
 
127
125
  const handlers = {
128
126
  'route': () => {
129
- // Inject ranked intelligence context before routing
130
127
  if (intelligence && intelligence.getContext) {
131
128
  try {
132
129
  const ctx = intelligence.getContext(prompt);
@@ -135,33 +132,49 @@ const handlers = {
135
132
  }
136
133
  if (router && router.routeTask) {
137
134
  const result = router.routeTask(prompt);
138
- // Format output for Claude Code hook consumption — real data only
139
- const output = [
140
- `[INFO] Routing task: ${prompt.substring(0, 80) || '(no prompt)'}`,
141
- '',
142
- '+------------------- Primary Recommendation -------------------+',
143
- `| Agent: ${result.agent.padEnd(53)}|`,
144
- `| Confidence: ${(result.confidence * 100).toFixed(1)}%${' '.repeat(44)}|`,
145
- `| Reason: ${(result.reason || '').substring(0, 53).padEnd(53)}|`,
146
- '+--------------------------------------------------------------+',
147
- ];
135
+ var output = [];
136
+ output.push('[INFO] Routing task: ' + (prompt.substring(0, 80) || '(no prompt)'));
137
+ output.push('');
138
+ output.push('+------------------- Primary Recommendation -------------------+');
139
+ output.push('| Agent: ' + result.agent.padEnd(53) + '|');
140
+ output.push('| Confidence: ' + (result.confidence * 100).toFixed(1) + '%' + ' '.repeat(44) + '|');
141
+ output.push('| Reason: ' + result.reason.substring(0, 53).padEnd(53) + '|');
142
+ output.push('+--------------------------------------------------------------+');
148
143
  console.log(output.join('\n'));
149
144
  } else {
150
145
  console.log('[INFO] Router not available, using default routing');
151
146
  }
147
+ // Agentic prompts → suggest spawning a swarm, and which roles. Swarmdo
148
+ // cannot call Claude Code's Agent tool itself, so this advisory is the only
149
+ // way it can ask. Fully guarded — never breaks the prompt.
150
+ //
151
+ // Deliberately does NOT mention `agent bridge register` (#108): the
152
+ // SubagentStart hook registers every subagent on spawn, so telling the main
153
+ // agent to register by hand is both wrong and redundant work. This fires on
154
+ // every agentic prompt, so it is kept to one line — the three lines of
155
+ // manual-registration instructions it replaced were pure token cost.
156
+ try {
157
+ if (router && router.classifyAgentic) {
158
+ const intent = router.classifyAgentic(prompt);
159
+ if (intent.requiresAgents) {
160
+ const roles = intent.roles.join(', ');
161
+ console.log('');
162
+ console.log('[SWARMDO] Agentic task — consider spawning agents: ' + roles
163
+ + ' (auto-registered on spawn; see `swarmdo swarm status`)');
164
+ }
165
+ }
166
+ } catch (e) { /* advisory only — never break the prompt */ }
152
167
  },
153
168
 
154
169
  'pre-bash': () => {
155
- // Basic command safety check — prefer stdin command data from Claude Code.
156
170
  // String() wrap is belt-and-suspenders for #2017: even if a future regression
157
171
  // re-binds `prompt` or `hookInput.command` to a non-string, `.toLowerCase()`
158
- // can no longer throw a TypeError that the global try/catch would swallow
159
- // (silently exiting 0 and letting the dangerous command through).
160
- const cmd = String(hookInput.command || toolInput.command || prompt || '').toLowerCase();
161
- const dangerous = ['rm -rf /', 'format c:', 'del /s /q c:\\', ':(){:|:&};:'];
162
- for (const d of dangerous) {
163
- if (cmd.includes(d)) {
164
- console.error(`[BLOCKED] Dangerous command detected: ${d}`);
172
+ // can no longer throw a TypeError that the global try/catch would swallow.
173
+ var cmd = String(hookInput.command || toolInput.command || prompt || '').toLowerCase();
174
+ var dangerous = ['rm -rf /', 'format c:', 'del /s /q c:\\', ':(){:|:&};:'];
175
+ for (var i = 0; i < dangerous.length; i++) {
176
+ if (cmd.includes(dangerous[i])) {
177
+ console.error('[BLOCKED] Dangerous command detected: ' + dangerous[i]);
165
178
  process.exit(1);
166
179
  }
167
180
  }
@@ -169,14 +182,12 @@ const handlers = {
169
182
  },
170
183
 
171
184
  'post-edit': () => {
172
- // Record edit for session metrics
173
185
  if (session && session.metric) {
174
186
  try { session.metric('edits'); } catch (e) { /* no active session */ }
175
187
  }
176
- // Record edit for intelligence consolidation — prefer stdin data from Claude Code
177
188
  if (intelligence && intelligence.recordEdit) {
178
189
  try {
179
- const file = hookInput.file_path || toolInput.file_path
190
+ var file = hookInput.file_path || toolInput.file_path
180
191
  || process.env.TOOL_INPUT_file_path || args[0] || '';
181
192
  intelligence.recordEdit(file);
182
193
  } catch (e) { /* non-fatal */ }
@@ -186,43 +197,31 @@ const handlers = {
186
197
 
187
198
  'session-restore': async () => {
188
199
  if (session) {
189
- // Try restore first, fall back to start
190
- const existing = session.restore && session.restore();
200
+ var existing = session.restore && session.restore();
191
201
  if (!existing) {
192
202
  session.start && session.start();
193
203
  }
194
204
  } else {
195
- // Minimal session restore output
196
- const sessionId = `session-${Date.now()}`;
197
- console.log(`[INFO] Restoring session: %SESSION_ID%`);
198
- console.log('');
199
- console.log(`[OK] Session restored from %SESSION_ID%`);
200
- console.log(`New session ID: ${sessionId}`);
201
- console.log('');
202
- console.log('Restored State');
203
- console.log('+----------------+-------+');
204
- console.log('| Item | Count |');
205
- console.log('+----------------+-------+');
206
- console.log('| Tasks | 0 |');
207
- console.log('| Agents | 0 |');
208
- console.log('| Memory Entries | 0 |');
209
- console.log('+----------------+-------+');
205
+ console.log('[OK] Session restored: session-' + Date.now());
210
206
  }
211
- // Initialize intelligence graph after session restore (with timeout — #1530)
207
+ // Initialize intelligence (with timeout — #1530)
212
208
  if (intelligence && intelligence.init) {
213
- const initResult = await runWithTimeout(() => intelligence.init(), 'intelligence.init()');
209
+ var initResult = await runWithTimeout(function() { return intelligence.init(); }, 'intelligence.init()');
214
210
  if (initResult && initResult.nodes > 0) {
215
- console.log(`[INTELLIGENCE] Loaded ${initResult.nodes} patterns, ${initResult.edges} edges`);
211
+ console.log('[INTELLIGENCE] Loaded ' + initResult.nodes + ' patterns, ' + initResult.edges + ' edges');
216
212
  }
217
213
  }
218
214
  },
219
215
 
220
216
  'session-end': async () => {
221
- // Consolidate intelligence before ending session (with timeout — #1530)
217
+ // Consolidate intelligence (with timeout — #1530)
222
218
  if (intelligence && intelligence.consolidate) {
223
- const consResult = await runWithTimeout(() => intelligence.consolidate(), 'intelligence.consolidate()');
219
+ var consResult = await runWithTimeout(function() { return intelligence.consolidate(); }, 'intelligence.consolidate()');
224
220
  if (consResult && consResult.entries > 0) {
225
- console.log(`[INTELLIGENCE] Consolidated: ${consResult.entries} entries, ${consResult.edges} edges${consResult.newEntries > 0 ? `, ${consResult.newEntries} new` : ''}, PageRank recomputed`);
221
+ var msg = '[INTELLIGENCE] Consolidated: ' + consResult.entries + ' entries, ' + consResult.edges + ' edges';
222
+ if (consResult.newEntries > 0) msg += ', ' + consResult.newEntries + ' new';
223
+ msg += ', PageRank recomputed';
224
+ console.log(msg);
226
225
  }
227
226
  }
228
227
  if (session && session.end) {
@@ -236,17 +235,15 @@ const handlers = {
236
235
  if (session && session.metric) {
237
236
  try { session.metric('tasks'); } catch (e) { /* no active session */ }
238
237
  }
239
- // Route the task if router is available
240
238
  if (router && router.routeTask && prompt) {
241
- const result = router.routeTask(prompt);
242
- console.log(`[INFO] Task routed to: ${result.agent} (confidence: ${result.confidence})`);
239
+ var result = router.routeTask(prompt);
240
+ console.log('[INFO] Task routed to: ' + result.agent + ' (confidence: ' + result.confidence + ')');
243
241
  } else {
244
242
  console.log('[OK] Task started');
245
243
  }
246
244
  },
247
245
 
248
246
  'post-task': () => {
249
- // Implicit success feedback for intelligence
250
247
  if (intelligence && intelligence.feedback) {
251
248
  try {
252
249
  intelligence.feedback(true);
@@ -255,6 +252,50 @@ const handlers = {
255
252
  console.log('[OK] Task completed');
256
253
  },
257
254
 
255
+ 'compact-manual': () => {
256
+ console.log('PreCompact Guidance:');
257
+ console.log('IMPORTANT: Review CLAUDE.md in project root for:');
258
+ console.log(' - Available agents and concurrent usage patterns');
259
+ console.log(' - Swarm coordination strategies (hierarchical, mesh, adaptive)');
260
+ console.log(' - Critical concurrent execution rules (1 MESSAGE = ALL OPERATIONS)');
261
+ console.log('Ready for compact operation');
262
+ },
263
+
264
+ 'compact-auto': () => {
265
+ console.log('Auto-Compact Guidance (Context Window Full):');
266
+ console.log('CRITICAL: Before compacting, ensure you understand:');
267
+ console.log(' - All agents available in .claude/agents/ directory');
268
+ console.log(' - Concurrent execution patterns from CLAUDE.md');
269
+ console.log(' - Swarm coordination strategies for complex tasks');
270
+ console.log('Apply GOLDEN RULE: Always batch operations in single messages');
271
+ console.log('Auto-compact proceeding with full agent context');
272
+ },
273
+
274
+ 'status': () => {
275
+ console.log('[OK] Status check');
276
+ },
277
+
278
+ // SubagentStart — bind the spawned Claude Code subagent into Swarmdo's
279
+ // canonical registry so `swarmdo agent list` and the statusline's
280
+ // `🐝 Swarms N 🤖 Agents M` reflect agents that actually exist (#108).
281
+ // Registering also auto-forms the swarm, so both counters move.
282
+ 'agent-register': () => {
283
+ if (!agentBridge) return;
284
+ if (!agentBridge.isSubagentEvent(hookInput)) return; // main-thread call
285
+ var ok = agentBridge.register(hookInput);
286
+ console.log(ok
287
+ ? '[OK] Bridged subagent ' + (hookInput.agent_type || 'agent') + ' into Swarmdo'
288
+ : '[WARN] Could not bridge subagent (no installed CLI found)');
289
+ },
290
+
291
+ // SubagentStop — mark the bound record terminated so the Agents count
292
+ // decrements. Same deterministic id the register side derived (#108).
293
+ 'agent-terminate': () => {
294
+ if (!agentBridge) return;
295
+ if (!agentBridge.isSubagentEvent(hookInput)) return;
296
+ agentBridge.terminate(hookInput);
297
+ },
298
+
258
299
  'stats': () => {
259
300
  if (intelligence && intelligence.stats) {
260
301
  intelligence.stats(args.includes('--json'));
@@ -264,27 +305,29 @@ const handlers = {
264
305
  },
265
306
  };
266
307
 
267
- // Execute the handler
268
- if (command && handlers[command]) {
308
+ if (command && handlers[command]) {
269
309
  try {
270
310
  await Promise.resolve(handlers[command]());
271
311
  } catch (e) {
272
- // Hooks should never crash Claude Code - fail silently
273
- console.log(`[WARN] Hook ${command} encountered an error: ${e.message}`);
312
+ console.log('[WARN] Hook ' + command + ' encountered an error: ' + e.message);
274
313
  }
275
314
  } else if (command) {
276
- // Unknown command - pass through without error
277
- console.log(`[OK] Hook: ${command}`);
315
+ console.log('[OK] Hook: ' + command);
278
316
  } else {
279
- console.log('Usage: hook-handler.cjs <route|pre-bash|post-edit|session-restore|session-end|pre-task|post-task|stats>');
317
+ console.log('Usage: hook-handler.cjs <route|pre-bash|post-edit|session-restore|session-end|pre-task|post-task|agent-register|agent-terminate|compact-manual|compact-auto|status|stats>');
280
318
  }
281
319
  }
282
320
 
283
- // Hooks must ALWAYS exit 0 Claude Code treats non-zero as "hook error"
284
- // and skips all subsequent hooks for the event.
285
- process.exitCode = 0;
286
- main().catch((e) => {
287
- try { console.log(`[WARN] Hook handler error: ${e.message}`); } catch (_) {}
288
- }).finally(() => {
289
- process.exit(0);
290
- });
321
+ // Only dispatch hooks when run directly (node hook-handler.cjs ...). When
322
+ // require()'d by a test, just expose the internals so they can be unit-tested
323
+ // without triggering stdin reads / process.exit.
324
+ if (require.main === module) {
325
+ main().catch(function(e) {
326
+ console.log('[WARN] Hook handler error: ' + e.message);
327
+ }).finally(function() {
328
+ // Ensure clean exit for Claude Code hooks
329
+ process.exit(0);
330
+ });
331
+ }
332
+
333
+ module.exports = { runWithTimeout: runWithTimeout, INTELLIGENCE_TIMEOUT_MS: INTELLIGENCE_TIMEOUT_MS };