swarmdo 1.50.0 → 1.58.1

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 (31) hide show
  1. package/.claude/helpers/hook-handler.cjs +16 -0
  2. package/.claude/helpers/router.cjs +27 -1
  3. package/.claude/helpers/statusline.cjs +10 -3
  4. package/README.md +6 -3
  5. package/package.json +1 -1
  6. package/v3/@swarmdo/cli/dist/src/agent-bridge/bridge.d.ts +104 -0
  7. package/v3/@swarmdo/cli/dist/src/agent-bridge/bridge.js +133 -0
  8. package/v3/@swarmdo/cli/dist/src/command-usage/usage.d.ts +74 -0
  9. package/v3/@swarmdo/cli/dist/src/command-usage/usage.js +161 -0
  10. package/v3/@swarmdo/cli/dist/src/commands/agent.js +133 -1
  11. package/v3/@swarmdo/cli/dist/src/commands/commands.d.ts +19 -0
  12. package/v3/@swarmdo/cli/dist/src/commands/commands.js +107 -0
  13. package/v3/@swarmdo/cli/dist/src/commands/index.js +11 -3
  14. package/v3/@swarmdo/cli/dist/src/commands/standup.d.ts +18 -0
  15. package/v3/@swarmdo/cli/dist/src/commands/standup.js +116 -0
  16. package/v3/@swarmdo/cli/dist/src/commands/swarm.js +29 -0
  17. package/v3/@swarmdo/cli/dist/src/commands/usage.js +57 -1
  18. package/v3/@swarmdo/cli/dist/src/comms/store.d.ts +11 -1
  19. package/v3/@swarmdo/cli/dist/src/comms/store.js +16 -2
  20. package/v3/@swarmdo/cli/dist/src/init/helpers-generator.js +41 -1
  21. package/v3/@swarmdo/cli/dist/src/init/statusline-generator.js +9 -2
  22. package/v3/@swarmdo/cli/dist/src/integrations/integrations.js +19 -8
  23. package/v3/@swarmdo/cli/dist/src/mcp-tools/agent-tools.js +166 -0
  24. package/v3/@swarmdo/cli/dist/src/mcp-tools/comms-tools.js +4 -4
  25. package/v3/@swarmdo/cli/dist/src/mcp-tools/swarm-tools.d.ts +33 -0
  26. package/v3/@swarmdo/cli/dist/src/mcp-tools/swarm-tools.js +73 -38
  27. package/v3/@swarmdo/cli/dist/src/standup/standup.d.ts +83 -0
  28. package/v3/@swarmdo/cli/dist/src/standup/standup.js +157 -0
  29. package/v3/@swarmdo/cli/dist/src/usage/transcript-friction.d.ts +77 -0
  30. package/v3/@swarmdo/cli/dist/src/usage/transcript-friction.js +151 -0
  31. package/v3/@swarmdo/cli/package.json +1 -1
@@ -277,6 +277,31 @@ function routeTask(task) {
277
277
  };
278
278
  }
279
279
 
280
+ // Mirror of src/agent-bridge/bridge.ts classifyPrompt — decides whether a
281
+ // prompt warrants spinning up a bound-agent swarm (and which roles), so the
282
+ // UserPromptSubmit hook can tell the main agent to spawn + bridge agents
283
+ // (making Swarmdo used by default instead of sitting idle).
284
+ const AGENTIC_RE = /\\b(implement|build|create|add(?:ing)?|develop|refactor\\w*|migrat\\w*|redesign|re-?architect\\w*|architect\\w*|feature|integrat\\w*|overhaul|rewrite|port|scaffold|end-to-end|multi-file|test suite|coverage|audit|harden\\w*|vulnerab\\w*|cve|fix\\w*|debug\\w*|optimi[sz]e|performance)\\b/i;
285
+ const TRIVIAL_RE = /\\b(what|why|how|explain|describe|show|list|find|search|where|which|typo|readme|comment|one-?liner|quick question|rename|bump (?:the )?version|status|help)\\b/i;
286
+ const ROLE_SIGNALS = [
287
+ { re: /\\b(security|vulnerab\\w*|cve|auth\\w*|inject\\w*|xss|ssrf)\\b/i, roles: ['security-auditor', 'coder', 'reviewer'] },
288
+ { re: /\\b(refactor\\w*|migrat\\w*|redesign|re-?architect\\w*|overhaul|rewrite)\\b/i, roles: ['system-architect', 'coder', 'reviewer'] },
289
+ { re: /\\b(perf\\w*|optimi[sz]e|benchmark|latency|throughput)\\b/i, roles: ['perf-analyzer', 'coder', 'tester'] },
290
+ { re: /\\b(test|coverage|tdd|spec)\\b/i, roles: ['tester', 'coder', 'reviewer'] },
291
+ { re: /\\b(feature|implement|build|integrat\\w*|end-to-end|api)\\b/i, roles: ['researcher', 'system-architect', 'coder', 'tester', 'reviewer'] },
292
+ ];
293
+ function classifyAgentic(task) {
294
+ const p = String(task == null ? '' : task).trim();
295
+ if (!p) return { requiresAgents: false, roles: [] };
296
+ if (!AGENTIC_RE.test(p)) return { requiresAgents: false, roles: [] };
297
+ if (TRIVIAL_RE.test(p) && p.length < 40) return { requiresAgents: false, roles: [] };
298
+ let roles = ['coder'];
299
+ for (const s of ROLE_SIGNALS) {
300
+ if (s.re.test(p)) { roles = s.roles; break; }
301
+ }
302
+ return { requiresAgents: true, roles };
303
+ }
304
+
280
305
  // CLI
281
306
  const task = process.argv.slice(2).join(' ');
282
307
 
@@ -288,7 +313,7 @@ if (task) {
288
313
  console.log('\\nAvailable agents:', Object.keys(AGENT_CAPABILITIES).join(', '));
289
314
  }
290
315
 
291
- module.exports = { routeTask, AGENT_CAPABILITIES, TASK_PATTERNS, buildPattern };
316
+ module.exports = { routeTask, classifyAgentic, AGENT_CAPABILITIES, TASK_PATTERNS, buildPattern };
292
317
  `;
293
318
  }
294
319
  /**
@@ -482,6 +507,21 @@ export function generateHookHandler() {
482
507
  ' } else {',
483
508
  " console.log('[INFO] Router not available, using default routing');",
484
509
  ' }',
510
+ ' // Agentic prompts → remind the main agent to actually USE Swarmdo:',
511
+ ' // spawn Claude Code agents AND bridge them (auto-forms a swarm).',
512
+ ' // Advisory + fully guarded — never breaks the prompt.',
513
+ ' try {',
514
+ ' if (router && router.classifyAgentic) {',
515
+ ' var intent = router.classifyAgentic(prompt);',
516
+ ' if (intent.requiresAgents) {',
517
+ " console.log('');",
518
+ " console.log('[SWARMDO] Agentic task — use the swarm: after you spawn Claude Code agents,');",
519
+ " console.log(' register each so Swarmdo tracks them and auto-forms a swarm from your config:');",
520
+ " console.log(' swarmdo agent bridge register -n <agent-name> -t <role> -s <session>');",
521
+ " console.log(' suggested roles: ' + intent.roles.join(', ') + ' then: swarmdo agent bridge list / swarm status');",
522
+ ' }',
523
+ ' }',
524
+ ' } catch (e) { /* advisory only — never break the prompt */ }',
485
525
  ' },',
486
526
  '',
487
527
  " 'pre-bash': () => {",
@@ -166,7 +166,7 @@ function resolveCliBin() {
166
166
  ];
167
167
  try {
168
168
  const binDir = path.dirname(process.execPath);
169
- const globalModuleDirs = [path.join(binDir, '..', 'lib', 'node_modules'), path.join(binDir, 'node_modules')];
169
+ const globalModuleDirs = [path.join(binDir, '..', 'lib', 'node_modules'), path.join(binDir, 'node_modules'), '/opt/homebrew/lib/node_modules', '/usr/local/lib/node_modules'];
170
170
  for (const prefix of [process.env.npm_config_prefix, process.env.PREFIX, path.join(home, '.npm-global')]) {
171
171
  if (prefix) globalModuleDirs.push(path.join(prefix, 'lib', 'node_modules'));
172
172
  }
@@ -209,6 +209,13 @@ function writeCache(data) {
209
209
  * and only counted ADRs in v3/implementation/adrs/ (missed v3/docs/adr/).
210
210
  */
211
211
  function getStatuslineData() {
212
+ // SWARMDO_STATUSLINE_NO_CLI: skip the CLI-delegation fork and render from local
213
+ // reads + stdin only. A cold render otherwise forks the hooks-statusline CLI
214
+ // (now more reachable via the global-node_modules probes below), and that fork
215
+ // can disturb the stdin cost payload; skipping it keeps renders fast, hermetic,
216
+ // and subprocess-free. Set by the statusline tests; usable by anyone who wants
217
+ // a no-fork statusline.
218
+ if (process.env.SWARMDO_STATUSLINE_NO_CLI) return buildLocalFallback();
212
219
  const cached = readCache();
213
220
  if (cached) return cached;
214
221
 
@@ -592,7 +599,7 @@ function getPkgVersion() {
592
599
  // (bin/node_modules) layouts.
593
600
  try {
594
601
  const binDir = path.dirname(process.execPath);
595
- const globalModuleDirs = [path.join(binDir, '..', 'lib', 'node_modules'), path.join(binDir, 'node_modules')];
602
+ const globalModuleDirs = [path.join(binDir, '..', 'lib', 'node_modules'), path.join(binDir, 'node_modules'), '/opt/homebrew/lib/node_modules', '/usr/local/lib/node_modules'];
596
603
  // #2221 follow-up: a custom npm prefix (e.g. ~/.npm-global) is decoupled from
597
604
  // the node binary location, so the binDir-derived probes above all miss. Also
598
605
  // probe the npm prefix from the environment and the common ~/.npm-global default.
@@ -63,23 +63,34 @@ export function crossAgentAgentsMd() {
63
63
  ## What swarmdo gives you
64
64
 
65
65
  This project is wired to **swarmdo** — agent orchestration with persistent
66
- vector memory, swarm coordination, and 300+ MCP tools.
66
+ vector memory, swarm coordination, cross-agent messaging, and 300+ MCP tools.
67
+ Everything below works the same from Codex, Copilot, pi, or a plain shell —
68
+ not just Claude Code.
67
69
 
68
- - **MCP server**: \`swarmdo mcp start\` (stdio). If your CLI supports MCP,
69
- \`swarmdo integrations install <your-cli>\` wires it; key tools:
70
- \`memory_search\`, \`memory_store\`, \`swarm_init\`, \`agent_spawn\`.
70
+ - **MCP server**: \`swarmdo mcp start\` (stdio; serves every tool by default).
71
+ If your CLI supports MCP, \`swarmdo integrations install <your-cli>\` wires
72
+ it. Key tools: \`memory_search\`, \`memory_store\`, \`swarm_init\`,
73
+ \`agent_spawn\`, \`comms_send\` / \`comms_inbox\` (message other agents),
74
+ \`hotspots\` / \`coupling\` / \`ownership\` (git-history analysis).
71
75
  - **CLI**: \`npx swarmdo@latest <command>\` — \`memory search -q "topic"\`,
72
- \`usage\`, \`task\`, \`hud\`, \`security scan\`, \`config lint\`.
76
+ \`comms send\` / \`comms inbox\`, \`standup\`, \`hotspots\`, \`coupling\`,
77
+ \`ownership\`, \`hidden-coupling\`, \`task\`, \`hud\`.
73
78
 
74
79
  ## Working agreement (any agent)
75
80
 
76
81
  1. **Search memory BEFORE starting**: \`memory_search(query="task keywords")\`
77
82
  — patterns with score > 0.7 are load-bearing precedent.
78
- 2. **You are the executor** swarmdo coordinates and remembers; it never
83
+ 2. **Coordinate through the cross-tool mailbox**: announce the files you're
84
+ about to change with \`comms_send(to="all", message="…")\` and check
85
+ \`comms_inbox\` before overlapping work. Non-Claude agents: set a stable
86
+ identity with \`export SWARMDO_AGENT=<your-name>\` (or pass \`from\` /
87
+ \`--self\`) so replies address you — without it every agent on this host
88
+ shares the hostname identity.
89
+ 3. **You are the executor** — swarmdo coordinates and remembers; it never
79
90
  writes your code for you.
80
- 3. **Store what worked AFTER success**:
91
+ 4. **Store what worked AFTER success**:
81
92
  \`memory_store(key="pattern-…", value="what worked", namespace="patterns")\`.
82
- 4. Claude Code users: the same surfaces are namespaced under \`/sDo:\`
93
+ 5. Claude Code users: the same surfaces are namespaced under \`/sDo:\`
83
94
  commands and \`/sdo-\` skills.
84
95
 
85
96
  ## Do not
@@ -9,6 +9,7 @@ import { join } from 'node:path';
9
9
  import { getProjectCwd } from './types.js';
10
10
  import { validateIdentifier, validateText, validateAgentSpawn } from './validate-input.js';
11
11
  import { executeAgentTask } from './agent-execute-core.js';
12
+ import { buildSpawnInput, reconcile, isBound, orphanedAgentIds } from '../agent-bridge/bridge.js';
12
13
  // Storage paths
13
14
  const STORAGE_DIR = '.swarmdo';
14
15
  const AGENT_DIR = 'agents';
@@ -74,6 +75,27 @@ function loadHiveAgents() {
74
75
  function loadAllAgents() {
75
76
  return { ...loadHiveAgents(), ...loadAgentStore().agents };
76
77
  }
78
+ /**
79
+ * Read the project's swarm topology defaults from swarmdo.config.json for the
80
+ * agent bridge's auto-swarm. Defensive — returns {} on any error so the bridge
81
+ * falls back to the anti-drift defaults (hierarchical / 8 / specialized).
82
+ * Accepts either a top-level `swarm` block or the root object.
83
+ */
84
+ function readSwarmConfigDefaults() {
85
+ try {
86
+ const p = join(getProjectCwd(), 'swarmdo.config.json');
87
+ if (!existsSync(p))
88
+ return {};
89
+ const j = JSON.parse(readFileSync(p, 'utf-8'));
90
+ const sw = (j.swarm || j) ?? {};
91
+ const str = (v) => (typeof v === 'string' ? v : undefined);
92
+ const num = (v) => (typeof v === 'number' ? v : undefined);
93
+ return { topology: str(sw.topology), maxAgents: num(sw.maxAgents), strategy: str(sw.strategy) };
94
+ }
95
+ catch {
96
+ return {};
97
+ }
98
+ }
77
99
  // Default model mappings for agent types (can be overridden)
78
100
  const AGENT_TYPE_MODEL_DEFAULTS = {
79
101
  // Complex agents → opus
@@ -323,6 +345,150 @@ export const agentTools = [
323
345
  return response;
324
346
  },
325
347
  },
348
+ {
349
+ name: 'agent_bridge_register',
350
+ description: "Register a REAL Claude Code Agent-tool agent into Swarmdo's registry so `swarmdo agent list` and `swarm_status` reflect it. Call this right AFTER you spawn a Claude Code subagent (Task/Agent tool): pass its name, session id, subagent type, and a one-line task. Idempotent — re-registering the same name+session UPDATES the bound record instead of duplicating. This is the bridge that stops Swarmdo sitting empty while real agents run: unlike agent_spawn (which only registers coordination metadata), this binds the record to a worker that actually exists.",
351
+ category: 'agent',
352
+ inputSchema: {
353
+ type: 'object',
354
+ properties: {
355
+ name: { type: 'string', description: 'The Claude Code agent name (e.g. "research-ccgap")' },
356
+ sessionId: { type: 'string', description: 'The Claude Code session id (from name@session-…)' },
357
+ agentType: { type: 'string', description: 'The subagent_type (e.g. general-purpose, coder). Default general-purpose.' },
358
+ task: { type: 'string', description: 'One-line task/prompt summary' },
359
+ },
360
+ required: ['name'],
361
+ },
362
+ handler: async (input) => {
363
+ const src = input;
364
+ const name = String(src.name || '').trim();
365
+ if (!name)
366
+ return { success: false, error: 'name is required (the Claude Code agent name)' };
367
+ const descriptor = {
368
+ name,
369
+ sessionId: src.sessionId ? String(src.sessionId) : undefined,
370
+ agentType: String(src.agentType || 'general-purpose'),
371
+ task: src.task ? String(src.task) : undefined,
372
+ };
373
+ const spawnInput = buildSpawnInput(descriptor, new Date().toISOString());
374
+ // Auto-swarm: registering a Claude Code agent spins up a swarm from the
375
+ // project config (anti-drift defaults if absent) when none is running, and
376
+ // enrolls this agent into it — so bridged agents coordinate instead of
377
+ // floating unattached. registerAgent honors spawnInput.swarmId to join.
378
+ let swarm;
379
+ try {
380
+ const { ensureActiveSwarm } = await import('./swarm-tools.js');
381
+ const cfg = readSwarmConfigDefaults();
382
+ swarm = ensureActiveSwarm({
383
+ topology: cfg.topology || 'hierarchical',
384
+ maxAgents: cfg.maxAgents || 8,
385
+ strategy: cfg.strategy || 'specialized',
386
+ });
387
+ spawnInput.swarmId = swarm.swarmId;
388
+ }
389
+ catch {
390
+ /* swarm optional — the agent still registers in the global store */
391
+ }
392
+ const res = await registerAgent(spawnInput);
393
+ if (!res.success)
394
+ return res;
395
+ return {
396
+ ...res,
397
+ bound: true,
398
+ origin: 'claude-code',
399
+ claudeName: descriptor.name,
400
+ status: 'registered',
401
+ ...(swarm ? { swarm } : {}),
402
+ };
403
+ },
404
+ },
405
+ {
406
+ name: 'agent_bridge_list',
407
+ description: 'List Swarmdo agent records split into Claude-Code-BOUND (each mirrors a real Task/Agent-tool agent) vs NATIVE, with binding detail. Pass `live` (array of current Claude Code agent names) to also get a reconciliation: which live agents are unmirrored (need agent_bridge_register), and which bound records are orphaned (their Claude agent is gone).',
408
+ category: 'agent',
409
+ inputSchema: {
410
+ type: 'object',
411
+ properties: {
412
+ live: {
413
+ type: 'array',
414
+ items: { type: 'string' },
415
+ description: 'Current live Claude Code agent names, for reconciliation',
416
+ },
417
+ },
418
+ },
419
+ handler: async (input) => {
420
+ const all = Object.values(loadAllAgents());
421
+ const summarize = (a) => ({
422
+ agentId: a.agentId,
423
+ agentType: a.agentType,
424
+ status: a.status,
425
+ ...(isBound(a) ? { binding: a.config.binding } : {}),
426
+ });
427
+ const bound = all.filter(isBound).map(summarize);
428
+ const native = all.filter((a) => !isBound(a)).map(summarize);
429
+ const liveRaw = input.live;
430
+ const live = Array.isArray(liveRaw) ? liveRaw.map(String) : undefined;
431
+ const reconciliation = live ? reconcile(all, live) : undefined;
432
+ return {
433
+ total: all.length,
434
+ boundCount: bound.length,
435
+ nativeCount: native.length,
436
+ bound,
437
+ native,
438
+ ...(reconciliation ? { reconciliation } : {}),
439
+ };
440
+ },
441
+ },
442
+ {
443
+ name: 'agent_bridge_prune',
444
+ description: "Reap orphaned Claude-Code-bound records — bound agents whose Claude Code agent is no longer live. Pass the CURRENT live Claude Code agent names as `live`; any bound record NOT in that list is removed from the agent store and from every swarm roster. Native (unbound) Swarmdo agents are never touched. Idempotent — use it to stop stale bindings accumulating across sessions.",
445
+ category: 'agent',
446
+ inputSchema: {
447
+ type: 'object',
448
+ properties: {
449
+ live: {
450
+ type: 'array',
451
+ items: { type: 'string' },
452
+ description: 'Current live Claude Code agent names; bound records not in this list are pruned',
453
+ },
454
+ },
455
+ required: ['live'],
456
+ },
457
+ handler: async (input) => {
458
+ const liveRaw = input.live;
459
+ const live = Array.isArray(liveRaw) ? liveRaw.map(String) : [];
460
+ const all = Object.values(loadAllAgents());
461
+ const toPrune = orphanedAgentIds(all, live);
462
+ if (toPrune.length === 0)
463
+ return { pruned: [], count: 0 };
464
+ // Remove from the canonical agent store (bound records always live here).
465
+ const store = loadAgentStore();
466
+ for (const id of toPrune)
467
+ delete store.agents[id];
468
+ saveAgentStore(store);
469
+ // Remove from any swarm roster so swarm_status doesn't report ghosts.
470
+ try {
471
+ const { loadSwarmStore: _loadSwarmStore, saveSwarmStore: _saveSwarmStore } = await import('./swarm-tools.js');
472
+ const swarmStore = _loadSwarmStore();
473
+ const pruneSet = new Set(toPrune);
474
+ let changed = false;
475
+ for (const swarm of Object.values(swarmStore.swarms)) {
476
+ if (Array.isArray(swarm.agents)) {
477
+ const before = swarm.agents.length;
478
+ swarm.agents = swarm.agents.filter((a) => !pruneSet.has(a));
479
+ if (swarm.agents.length !== before)
480
+ changed = true;
481
+ }
482
+ }
483
+ if (changed)
484
+ _saveSwarmStore(swarmStore);
485
+ }
486
+ catch {
487
+ /* swarm store optional — records already removed from the agent store */
488
+ }
489
+ return { pruned: toPrune, count: toPrune.length };
490
+ },
491
+ },
326
492
  {
327
493
  // ADR-095 G3 — spawn + execute fused. Closes the #1 UX trap from the
328
494
  // 2026-04 audit (@roman-rr): `agent_spawn` looks like it spawns a worker
@@ -12,7 +12,7 @@ import { loadMailbox, saveMailbox, resolveSelf, newMessageId, RETENTION } from '
12
12
  export const commsTools = [
13
13
  {
14
14
  name: 'comms_send',
15
- description: 'Send a message to another Claude Code session working on the same repo (cross-session mailbox). Address "all" to broadcast to every session. Use for multi-session/"multiplayer" coordination; delivery is via a shared file, not the network.',
15
+ description: 'Send a message to another agent session — Claude Code, Codex, Copilot, pi, or any tool on the same repo (a cross-session/cross-tool mailbox). Address "all" to broadcast to every session. Use for multi-agent "multiplayer" coordination; delivery is via a shared file, not the network.',
16
16
  category: 'comms',
17
17
  inputSchema: {
18
18
  type: 'object',
@@ -20,7 +20,7 @@ export const commsTools = [
20
20
  to: { type: 'string', description: 'Recipient session name, or "all" to broadcast' },
21
21
  message: { type: 'string', description: 'Message body' },
22
22
  subject: { type: 'string', description: 'Optional subject line' },
23
- from: { type: 'string', description: 'Sender name (default: this session $SWARMDO_SESSION or hostname)' },
23
+ from: { type: 'string', description: 'Sender name. Default: this session ($SWARMDO_SESSION, else $SWARMDO_AGENT, else hostname). Non-Claude agents should pass this (or export $SWARMDO_AGENT) so replies address them, not the shared hostname.' },
24
24
  },
25
25
  required: ['to', 'message'],
26
26
  },
@@ -49,12 +49,12 @@ export const commsTools = [
49
49
  },
50
50
  {
51
51
  name: 'comms_inbox',
52
- description: 'List messages other sessions have sent to this session (cross-session mailbox), newest first. Includes "all" broadcasts. Set markRead to acknowledge them.',
52
+ description: 'List messages other agent sessions (Claude Code, Codex, Copilot, pi, …) have sent to this one — a cross-session/cross-tool mailbox newest first. Includes "all" broadcasts. Set markRead to acknowledge them.',
53
53
  category: 'comms',
54
54
  inputSchema: {
55
55
  type: 'object',
56
56
  properties: {
57
- to: { type: 'string', description: 'Whose inbox (default: this session)' },
57
+ to: { type: 'string', description: 'Whose inbox (default: this session — $SWARMDO_SESSION, else $SWARMDO_AGENT, else hostname)' },
58
58
  unreadOnly: { type: 'boolean', description: 'Only unread messages' },
59
59
  from: { type: 'string', description: 'Only messages from this sender' },
60
60
  since: { type: 'string', description: 'Only messages after this ISO-8601 timestamp' },
@@ -32,6 +32,39 @@ interface SwarmStore {
32
32
  }
33
33
  export declare function loadSwarmStore(): SwarmStore;
34
34
  export declare function saveSwarmStore(store: SwarmStore): void;
35
+ export interface CreateSwarmOptions {
36
+ topology?: string;
37
+ maxAgents?: number;
38
+ strategy?: string;
39
+ config?: Record<string, unknown>;
40
+ /**
41
+ * When true, do NOT stamp the host `pid` — the swarm is a persistent logical
42
+ * coordination record, not tied to one process's lifetime, so it survives
43
+ * ephemeral CLI invocations (the agent bridge) instead of being reaped by the
44
+ * #1799 PID-orphan check the instant the creating process exits. Such swarms
45
+ * still expire via the 24h idle-TTL fallback. Daemon-hosted swarms
46
+ * (`swarm_init`) leave this false so a dead host IS reaped promptly.
47
+ */
48
+ persistent?: boolean;
49
+ }
50
+ /**
51
+ * Create + persist a swarm. Extracted from the `swarm_init` handler so other
52
+ * subsystems (the Claude-Code agent bridge) can spin up a swarm from config
53
+ * WITHOUT duplicating the store-write. Throws on an invalid topology. Shared
54
+ * single source of truth — no parallel swarm creation.
55
+ */
56
+ export declare function createSwarm(opts: CreateSwarmOptions): SwarmState;
57
+ /**
58
+ * Return the most-recent RUNNING swarm, or create one from `defaults` if none
59
+ * exists. Used by the agent bridge so registering a Claude Code agent
60
+ * auto-spins-up a swarm (from the project's anti-drift config) and enrolls it,
61
+ * instead of leaving the agent unattached.
62
+ */
63
+ export declare function ensureActiveSwarm(defaults: CreateSwarmOptions): {
64
+ swarmId: string;
65
+ topology: string;
66
+ created: boolean;
67
+ };
35
68
  export declare const swarmTools: MCPTool[];
36
69
  export {};
37
70
  //# sourceMappingURL=swarm-tools.d.ts.map
@@ -109,6 +109,64 @@ export function saveSwarmStore(store) {
109
109
  const VALID_TOPOLOGIES = new Set([
110
110
  'hierarchical', 'mesh', 'hierarchical-mesh', 'ring', 'star', 'hybrid', 'adaptive',
111
111
  ]);
112
+ /**
113
+ * Create + persist a swarm. Extracted from the `swarm_init` handler so other
114
+ * subsystems (the Claude-Code agent bridge) can spin up a swarm from config
115
+ * WITHOUT duplicating the store-write. Throws on an invalid topology. Shared
116
+ * single source of truth — no parallel swarm creation.
117
+ */
118
+ export function createSwarm(opts) {
119
+ const topology = opts.topology || 'hierarchical-mesh';
120
+ const maxAgents = Math.min(Math.max(opts.maxAgents || 15, 1), 50);
121
+ const strategy = opts.strategy || 'specialized';
122
+ const config = opts.config || {};
123
+ if (!VALID_TOPOLOGIES.has(topology)) {
124
+ throw new Error(`Invalid topology: ${topology}. Valid: ${[...VALID_TOPOLOGIES].join(', ')}`);
125
+ }
126
+ const swarmId = `swarm-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
127
+ const now = new Date().toISOString();
128
+ const swarmState = {
129
+ swarmId,
130
+ topology,
131
+ maxAgents,
132
+ status: 'running',
133
+ agents: [],
134
+ tasks: [],
135
+ config: {
136
+ topology,
137
+ maxAgents,
138
+ strategy,
139
+ communicationProtocol: config.communicationProtocol || 'message-bus',
140
+ autoScaling: config.autoScaling ?? true,
141
+ consensusMechanism: config.consensusMechanism || 'majority',
142
+ },
143
+ createdAt: now,
144
+ updatedAt: now,
145
+ // Persistent (bridge) swarms omit the pid so they aren't reaped on process
146
+ // exit; daemon-hosted swarms record it for prompt orphan reconciliation.
147
+ ...(opts.persistent ? {} : { pid: process.pid }),
148
+ };
149
+ const store = loadSwarmStore();
150
+ store.swarms[swarmId] = swarmState;
151
+ saveSwarmStore(store);
152
+ return swarmState;
153
+ }
154
+ /**
155
+ * Return the most-recent RUNNING swarm, or create one from `defaults` if none
156
+ * exists. Used by the agent bridge so registering a Claude Code agent
157
+ * auto-spins-up a swarm (from the project's anti-drift config) and enrolls it,
158
+ * instead of leaving the agent unattached.
159
+ */
160
+ export function ensureActiveSwarm(defaults) {
161
+ const store = loadSwarmStore();
162
+ const running = Object.values(store.swarms)
163
+ .filter((s) => s.status === 'running')
164
+ .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())[0];
165
+ if (running)
166
+ return { swarmId: running.swarmId, topology: running.topology, created: false };
167
+ const s = createSwarm({ ...defaults, persistent: true });
168
+ return { swarmId: s.swarmId, topology: s.topology, created: true };
169
+ }
112
170
  export const swarmTools = [
113
171
  {
114
172
  name: 'swarm_init',
@@ -135,49 +193,26 @@ export const swarmTools = [
135
193
  if (!v.valid)
136
194
  return { success: false, error: v.error };
137
195
  }
138
- const topology = input.topology || 'hierarchical-mesh';
139
- const maxAgents = Math.min(Math.max(input.maxAgents || 15, 1), 50);
140
196
  const strategy = input.strategy || 'specialized';
141
- const config = (input.config || {});
142
- if (!VALID_TOPOLOGIES.has(topology)) {
143
- return {
144
- success: false,
145
- error: `Invalid topology: ${topology}. Valid: ${[...VALID_TOPOLOGIES].join(', ')}`,
146
- };
147
- }
148
- const swarmId = `swarm-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
149
- const now = new Date().toISOString();
150
- const swarmState = {
151
- swarmId,
152
- topology,
153
- maxAgents,
154
- status: 'running',
155
- agents: [],
156
- tasks: [],
157
- config: {
158
- topology,
159
- maxAgents,
197
+ let swarmState;
198
+ try {
199
+ swarmState = createSwarm({
200
+ topology: input.topology,
201
+ maxAgents: input.maxAgents,
160
202
  strategy,
161
- communicationProtocol: config.communicationProtocol || 'message-bus',
162
- autoScaling: config.autoScaling ?? true,
163
- consensusMechanism: config.consensusMechanism || 'majority',
164
- },
165
- createdAt: now,
166
- updatedAt: now,
167
- // #1799 — record host PID so subsequent loads can detect orphans
168
- // when this process exits without a graceful swarm_shutdown.
169
- pid: process.pid,
170
- };
171
- const store = loadSwarmStore();
172
- store.swarms[swarmId] = swarmState;
173
- saveSwarmStore(store);
203
+ config: (input.config || {}),
204
+ });
205
+ }
206
+ catch (e) {
207
+ return { success: false, error: e.message };
208
+ }
174
209
  return {
175
210
  success: true,
176
- swarmId,
177
- topology,
211
+ swarmId: swarmState.swarmId,
212
+ topology: swarmState.topology,
178
213
  strategy,
179
- maxAgents,
180
- initializedAt: now,
214
+ maxAgents: swarmState.maxAgents,
215
+ initializedAt: swarmState.createdAt,
181
216
  config: swarmState.config,
182
217
  persisted: true,
183
218
  };
@@ -0,0 +1,83 @@
1
+ /**
2
+ * standup.ts — "what did I do?" recall from git history, weekend-aware.
3
+ *
4
+ * Ports the signature behavior of git-standup: list the commits since your last
5
+ * WORKING day, so on Monday you reach back to Friday (not merely yesterday).
6
+ * Power users juggling many repos + AI-agent sessions use it to re-orient at the
7
+ * start of a session and to answer the standup question in one command.
8
+ *
9
+ * Pure + deterministic: the grouping core takes the raw `git log --numstat`
10
+ * dump (control-char delimited, subject included) and a reference Date, and
11
+ * folds it into per-day buckets. No LLM, no network — the git subprocess lives
12
+ * in ../commands/standup.ts, so this module is fully fixture-testable.
13
+ * `resolveRenamePath` is reused from hotspots so renamed files fold correctly.
14
+ *
15
+ * Expected log format (control-char delimited):
16
+ * <SOH>%H<US>%aN<US>%aI<US>%s ← one header line per commit (subject last)
17
+ * <added>\t<deleted>\t<path> ← one numstat line per file ('-' = binary)
18
+ */
19
+ export interface StandupCommit {
20
+ hash: string;
21
+ author: string;
22
+ /** epoch milliseconds, parsed from the ISO author date */
23
+ date: number;
24
+ subject: string;
25
+ /** insertions summed across the commit's files (binary counts as 0) */
26
+ added: number;
27
+ /** deletions summed across the commit's files */
28
+ deleted: number;
29
+ /** count of files touched by the commit */
30
+ files: number;
31
+ }
32
+ export interface DayBucket {
33
+ /** local calendar day, `YYYY-MM-DD` */
34
+ day: string;
35
+ /** commits on this day, newest first */
36
+ commits: StandupCommit[];
37
+ /** day totals */
38
+ added: number;
39
+ deleted: number;
40
+ files: number;
41
+ }
42
+ /**
43
+ * How many days back to the last WORKING day (Mon–Fri), given a day-of-week
44
+ * (0=Sun..6=Sat). Weekend-aware, matching git-standup's signature behavior:
45
+ * Monday (1) → 3 (reach back to Friday)
46
+ * Sunday (0) → 2 (reach back to Friday)
47
+ * any other → 1 (yesterday)
48
+ * Pure integer core — no Date, no timezone. Out-of-range inputs are normalized
49
+ * into 0..6 so callers can pass raw arithmetic safely.
50
+ */
51
+ export declare function daysToLastWorkingDay(dayOfWeek: number): number;
52
+ /**
53
+ * The default weekend-aware window for a reference date. Reads the LOCAL
54
+ * day-of-week — a user's "last working day" is local to them. Pure w.r.t. the
55
+ * injected Date.
56
+ */
57
+ export declare function sinceLastWorkingDay(refDate: Date): {
58
+ sinceDays: number;
59
+ };
60
+ /** Parse the control-char-delimited standup log dump into commits. Pure. */
61
+ export declare function parseStandupLog(raw: string): StandupCommit[];
62
+ /**
63
+ * Local `YYYY-MM-DD` for an epoch-ms instant. Uses LOCAL calendar components so
64
+ * the day matches the user's wall clock; round-trips with local-constructed
65
+ * dates regardless of the runner timezone. Pure.
66
+ */
67
+ export declare function dayKey(ms: number): string;
68
+ /**
69
+ * Fold commits into per-day buckets — newest day first, and newest commit first
70
+ * within a day. Day boundaries are LOCAL. Pure (Array.sort is stable, so
71
+ * same-timestamp commits keep git-log order).
72
+ */
73
+ export declare function groupByDay(commits: StandupCommit[]): DayBucket[];
74
+ /** Weekday name for a `YYYY-MM-DD` day key (local). '' if unparseable. Pure. */
75
+ export declare function weekdayOf(day: string): string;
76
+ /**
77
+ * Human-readable standup: a heading per day (weekday + date + counts) followed
78
+ * by each commit's abbreviated hash and subject, then a totals footer. Pure.
79
+ */
80
+ export declare function formatStandup(buckets: DayBucket[], opts?: {
81
+ abbrev?: number;
82
+ }): string;
83
+ //# sourceMappingURL=standup.d.ts.map