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.
- package/.claude/helpers/hook-handler.cjs +16 -0
- package/.claude/helpers/router.cjs +27 -1
- package/.claude/helpers/statusline.cjs +10 -3
- package/README.md +6 -3
- package/package.json +1 -1
- package/v3/@swarmdo/cli/dist/src/agent-bridge/bridge.d.ts +104 -0
- package/v3/@swarmdo/cli/dist/src/agent-bridge/bridge.js +133 -0
- package/v3/@swarmdo/cli/dist/src/command-usage/usage.d.ts +74 -0
- package/v3/@swarmdo/cli/dist/src/command-usage/usage.js +161 -0
- package/v3/@swarmdo/cli/dist/src/commands/agent.js +133 -1
- package/v3/@swarmdo/cli/dist/src/commands/commands.d.ts +19 -0
- package/v3/@swarmdo/cli/dist/src/commands/commands.js +107 -0
- package/v3/@swarmdo/cli/dist/src/commands/index.js +11 -3
- package/v3/@swarmdo/cli/dist/src/commands/standup.d.ts +18 -0
- package/v3/@swarmdo/cli/dist/src/commands/standup.js +116 -0
- package/v3/@swarmdo/cli/dist/src/commands/swarm.js +29 -0
- package/v3/@swarmdo/cli/dist/src/commands/usage.js +57 -1
- package/v3/@swarmdo/cli/dist/src/comms/store.d.ts +11 -1
- package/v3/@swarmdo/cli/dist/src/comms/store.js +16 -2
- package/v3/@swarmdo/cli/dist/src/init/helpers-generator.js +41 -1
- package/v3/@swarmdo/cli/dist/src/init/statusline-generator.js +9 -2
- package/v3/@swarmdo/cli/dist/src/integrations/integrations.js +19 -8
- package/v3/@swarmdo/cli/dist/src/mcp-tools/agent-tools.js +166 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/comms-tools.js +4 -4
- package/v3/@swarmdo/cli/dist/src/mcp-tools/swarm-tools.d.ts +33 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/swarm-tools.js +73 -38
- package/v3/@swarmdo/cli/dist/src/standup/standup.d.ts +83 -0
- package/v3/@swarmdo/cli/dist/src/standup/standup.js +157 -0
- package/v3/@swarmdo/cli/dist/src/usage/transcript-friction.d.ts +77 -0
- package/v3/@swarmdo/cli/dist/src/usage/transcript-friction.js +151 -0
- 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
|
|
69
|
-
\`swarmdo integrations install <your-cli>\` wires
|
|
70
|
-
\`memory_search\`, \`memory_store\`, \`swarm_init\`,
|
|
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
|
-
\`
|
|
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. **
|
|
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
|
-
|
|
91
|
+
4. **Store what worked AFTER success**:
|
|
81
92
|
\`memory_store(key="pattern-…", value="what worked", namespace="patterns")\`.
|
|
82
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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:
|
|
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
|