bullswarm 0.1.4 → 0.2.0
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/AGENTS.md +51 -0
- package/connectors/claude-code.json +35 -9
- package/package.json +3 -2
- package/skill/SKILL.md +34 -37
- package/src/cli.js +97 -8
- package/src/setup.js +57 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# AGENTS.md — bullswarm
|
|
2
|
+
|
|
3
|
+
Instructions for AI agents working in this repository.
|
|
4
|
+
|
|
5
|
+
## What bullswarm is
|
|
6
|
+
|
|
7
|
+
A CLI that routes bounded tasks to whichever coding-agent CLI subscription
|
|
8
|
+
has the most quota headroom, paced by live provider meters, verified by
|
|
9
|
+
content. Published as `bullswarm` on npm.
|
|
10
|
+
|
|
11
|
+
## Non-negotiable doctrine
|
|
12
|
+
|
|
13
|
+
1. Judge delegate output by CONTENT, not exit code (see `src/lib/verify.js`).
|
|
14
|
+
2. Pace by meter surplus = elapsed% (from provider resets_at) − used%.
|
|
15
|
+
Weekly/monthly windows pace; 5h windows are burst gates only (M1–M5 in
|
|
16
|
+
`src/meters/framework.js`).
|
|
17
|
+
3. Connector quirks live in `connectors/*.json`, never in core logic.
|
|
18
|
+
4. Quarantine always auto-releases; recursion depth is core-owned via env
|
|
19
|
+
(`BULLSWARM_DEPTH`).
|
|
20
|
+
|
|
21
|
+
## Development
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm test # 61+ tests, no network needed (meters read from cache)
|
|
25
|
+
node bin/bullswarm.js doctor --json # readiness report
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- Zero runtime dependencies. Node >= 18. Tests must never require network:
|
|
29
|
+
prime `~/.bullswarm/meters/*.json` caches with fresh timestamps if needed.
|
|
30
|
+
- Every verb must work non-interactively (no TTY). The interactive wizard is
|
|
31
|
+
a human convenience, never a requirement.
|
|
32
|
+
- Version single source: package.json. Release via
|
|
33
|
+
`node bin/bullswarm.js release patch|minor|major` then `git push --tags`
|
|
34
|
+
— CI publishes through npm trusted publishing (OIDC), no tokens.
|
|
35
|
+
|
|
36
|
+
## Adding a connector
|
|
37
|
+
|
|
38
|
+
Copy an existing file in `connectors/`, set: bin name, configDirs for
|
|
39
|
+
discovery, spawn argv template (`{taskFile}` `{cwd}` `{bullswarmDir}`
|
|
40
|
+
substitutions), authSignatures (output strings meaning auth/throttle
|
|
41
|
+
failure), outputExtraction strategy, meter type, costRank, lanes. Add a
|
|
42
|
+
meter reader in `src/meters/` only if the provider exposes a usage API —
|
|
43
|
+
declared meters are the fallback, never the goal.
|
|
44
|
+
|
|
45
|
+
## Releasing
|
|
46
|
+
|
|
47
|
+
1. All tests green.
|
|
48
|
+
2. `node bin/bullswarm.js release patch` (creates commit + tag v*).
|
|
49
|
+
3. `git push && git push --tags`.
|
|
50
|
+
4. GitHub Actions publishes to npm via trusted publishing; verify with
|
|
51
|
+
`npm view bullswarm version`.
|
|
@@ -1,17 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code",
|
|
3
3
|
"bin": "claude",
|
|
4
|
-
"configDirs": [
|
|
4
|
+
"configDirs": [
|
|
5
|
+
"~/.claude"
|
|
6
|
+
],
|
|
5
7
|
"spawn": {
|
|
6
|
-
"cmd": [
|
|
8
|
+
"cmd": [
|
|
9
|
+
"claude",
|
|
10
|
+
"-p",
|
|
11
|
+
"Read the file {taskFile} and follow its instructions exactly.",
|
|
12
|
+
"--add-dir",
|
|
13
|
+
"{cwd}",
|
|
14
|
+
"--dangerously-skip-permissions"
|
|
15
|
+
],
|
|
7
16
|
"cwdMode": "add-dir"
|
|
8
17
|
},
|
|
9
|
-
"authSignatures": [
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
18
|
+
"authSignatures": [
|
|
19
|
+
"unauthorized",
|
|
20
|
+
"authentication failed",
|
|
21
|
+
"credit balance",
|
|
22
|
+
"not logged in",
|
|
23
|
+
"please run /login"
|
|
24
|
+
],
|
|
25
|
+
"outputExtraction": {
|
|
26
|
+
"strategy": "stdout"
|
|
27
|
+
},
|
|
28
|
+
"$comment-meter": "the CALLER pool \u2014 competes in analyze/build, wins only when no delegate can take the lane",
|
|
29
|
+
"meter": {
|
|
30
|
+
"type": "reader",
|
|
31
|
+
"window": "weekly+5h"
|
|
32
|
+
},
|
|
13
33
|
"costRank": 4,
|
|
14
|
-
"lanes": [
|
|
15
|
-
|
|
34
|
+
"lanes": [
|
|
35
|
+
"analyze",
|
|
36
|
+
"build",
|
|
37
|
+
"chore"
|
|
38
|
+
],
|
|
39
|
+
"flags": {
|
|
40
|
+
"stealth": false
|
|
41
|
+
},
|
|
16
42
|
"timeoutSec": 900
|
|
17
|
-
}
|
|
43
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bullswarm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Route work across coding-agent CLI subscriptions — paced by live quota meters, verified by content, never trusting exit codes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"skill/",
|
|
18
18
|
"mcp/",
|
|
19
19
|
"README.md",
|
|
20
|
-
"LICENSE"
|
|
20
|
+
"LICENSE",
|
|
21
|
+
"AGENTS.md"
|
|
21
22
|
],
|
|
22
23
|
"scripts": {
|
|
23
24
|
"test": "node --test \"tests/*.test.js\"",
|
package/skill/SKILL.md
CHANGED
|
@@ -1,53 +1,50 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: bullswarm-setup
|
|
3
|
-
description: Use when the user wants to offload work to other coding-agent subscriptions, asks about bullswarm, or wants quota-aware routing across agent CLIs.
|
|
3
|
+
description: Use when the user wants to offload work to other coding-agent subscriptions, asks about bullswarm, or wants quota-aware routing across agent CLIs. bullswarm is fully self-initializing — any agent can use it with no human setup.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# bullswarm
|
|
6
|
+
# bullswarm agent guide
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
bullswarm routes bounded tasks to whichever coding-agent CLI subscription has
|
|
9
|
+
the most quota headroom, then verifies every delegate's output by content.
|
|
10
|
+
**It requires no human setup.** Every verb self-initializes on first use.
|
|
9
11
|
|
|
10
|
-
##
|
|
12
|
+
## Quick start (zero-touch)
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
```bash
|
|
15
|
+
# 1. Check readiness (also self-heals a missing config):
|
|
16
|
+
bullswarm doctor --json
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
# 2. See pools with live quota meters:
|
|
19
|
+
bullswarm pools
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
# 3. Offload a task:
|
|
22
|
+
bullswarm run --lane <analyze|build|chore> \
|
|
23
|
+
--add-dir /abs/path/to/repo --task-file /abs/task.md --json
|
|
19
24
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
# 4. After every offload round:
|
|
26
|
+
bullswarm health
|
|
27
|
+
```
|
|
23
28
|
|
|
24
|
-
|
|
29
|
+
## Reading the verdict
|
|
25
30
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
> quota state, and lets you pick which pools to enable. No credentials
|
|
31
|
-
> are entered anywhere; it only reads what's already on your machine.
|
|
31
|
+
- `ok:true` → read `outFile`; it passed content verification
|
|
32
|
+
- `keepOnClaude:true` → do it in-session; no pool could take it
|
|
33
|
+
- `ok:false` → `why` names the failed gate. `contentUsableDespiteExit:true`
|
|
34
|
+
means the file is complete despite a non-zero exit — read before re-running.
|
|
32
35
|
|
|
33
|
-
|
|
36
|
+
## Rules for agents
|
|
34
37
|
|
|
35
|
-
|
|
38
|
+
- Delegate output is INPUT you verify, never the answer you present. Final
|
|
39
|
+
synthesis, architecture decisions, and live-context work stay with you.
|
|
40
|
+
- Lanes are work nature: `analyze` (root-cause, review), `build`
|
|
41
|
+
(implement, tests), `chore` (summarize, convert, smoke-check).
|
|
42
|
+
- Run `bullswarm health` after every round; investigate any `gateFailures`.
|
|
43
|
+
- Never edit `~/.bullswarm/state.json` directly; use the CLI.
|
|
44
|
+
- No TTY needed anywhere: every verb works in scripts and CI.
|
|
36
45
|
|
|
37
|
-
|
|
38
|
-
bullswarm pools
|
|
39
|
-
```
|
|
46
|
+
## Human customization (optional)
|
|
40
47
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
> outputs against their verdicts and catches verify-gate failures that
|
|
45
|
-
> would otherwise be invisible.
|
|
46
|
-
|
|
47
|
-
## Hard rules
|
|
48
|
-
|
|
49
|
-
- NEVER edit `~/.bullswarm/` state directly; the CLI owns it.
|
|
50
|
-
- NEVER bypass the wizard's approval gates by writing CLAUDE.md/AGENTS.md
|
|
51
|
-
blocks yourself — direct the user through `bullswarm setup` so the diff is
|
|
52
|
-
shown and approved.
|
|
53
|
-
- Delegate output is input to verify, never the answer you present.
|
|
48
|
+
Humans can rerun `bullswarm setup` interactively to pick specific pools or
|
|
49
|
+
approve CLAUDE.md/AGENTS.md integration blocks. Agents should never do this
|
|
50
|
+
on the user's behalf without asking — but never *require* it either.
|
package/src/cli.js
CHANGED
|
@@ -245,24 +245,111 @@ function cmdHealth(opts) {
|
|
|
245
245
|
// --- setup ------------------------------------------------------------------
|
|
246
246
|
|
|
247
247
|
async function cmdSetup(opts) {
|
|
248
|
-
const { runWizard } = await import('./setup.js');
|
|
248
|
+
const { runWizard, autoSetup } = await import('./setup.js');
|
|
249
|
+
// Agent-friendly: --yes (or no TTY on stdin) initializes with discovered
|
|
250
|
+
// defaults and never prompts.
|
|
251
|
+
if (opts.yes || !process.stdin.isTTY) {
|
|
252
|
+
const r = autoSetup(BULLSWARM_DIR, { reason: opts.yes ? 'flag' : 'non-tty' });
|
|
253
|
+
if (opts.json) console.log(JSON.stringify({ ok: true, mode: 'auto', ...r }, null, 2));
|
|
254
|
+
else {
|
|
255
|
+
console.log(`setup complete (${r.reason}): enabled ${r.enabledPools.join(', ')}`);
|
|
256
|
+
if (r.repaired.length) console.log(`repaired connector files: ${r.repaired.join(', ')}`);
|
|
257
|
+
}
|
|
258
|
+
return 0;
|
|
259
|
+
}
|
|
249
260
|
return runWizard(BULLSWARM_DIR, opts);
|
|
250
261
|
}
|
|
251
262
|
|
|
263
|
+
// --- doctor -------------------------------------------------------------------
|
|
264
|
+
// Machine-readable readiness report for agents: what works, what's missing,
|
|
265
|
+
// exactly which command fixes each gap. Never prompts.
|
|
266
|
+
|
|
267
|
+
async function cmdDoctor(opts) {
|
|
268
|
+
const { discoverConnectors, isConfigured, autoSetup } = await import('./setup.js');
|
|
269
|
+
const checks = [];
|
|
270
|
+
let configured = isConfigured(BULLSWARM_DIR);
|
|
271
|
+
|
|
272
|
+
checks.push({
|
|
273
|
+
id: 'config',
|
|
274
|
+
ok: configured,
|
|
275
|
+
detail: configured ? `${BULLSWARM_DIR}/state.json present` : 'no config yet',
|
|
276
|
+
fix: 'bullswarm setup --yes # or run any verb; it self-initializes',
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// Self-heal before reporting when not configured — an agent calling
|
|
280
|
+
// doctor should end up ready-to-use in the same invocation.
|
|
281
|
+
if (!configured) {
|
|
282
|
+
autoSetup(BULLSWARM_DIR, { reason: 'doctor' });
|
|
283
|
+
configured = true;
|
|
284
|
+
checks[0] = { ...checks[0], ok: true, detail: `initialized at ${BULLSWARM_DIR} (was missing)` };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const discovered = discoverConnectors();
|
|
288
|
+
const found = discovered.filter((d) => d.discovered && !d.broken);
|
|
289
|
+
checks.push({
|
|
290
|
+
id: 'connectors',
|
|
291
|
+
ok: found.length > 0,
|
|
292
|
+
detail: `${found.length} agent CLI(s) found: ${found.map((d) => d.name).join(', ') || '(none)'}`,
|
|
293
|
+
fix: found.length ? null : 'install at least one agent CLI (codex, grok, opencode…) — echo pool still works',
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const { pools } = await buildPoolsLive(BULLSWARM_DIR, Date.now(), {
|
|
298
|
+
getReadings: getAllMeterReadings,
|
|
299
|
+
});
|
|
300
|
+
const live = pools.filter((p) => p.meterSource === 'live' || p.meterSource === 'cache');
|
|
301
|
+
const enabled = pools.filter((p) => p.enabled);
|
|
302
|
+
checks.push({
|
|
303
|
+
id: 'meters',
|
|
304
|
+
ok: enabled.length > 0,
|
|
305
|
+
detail: `${live.length}/${pools.length} pools with provider meters; ${enabled.length} enabled`,
|
|
306
|
+
fix: enabled.length ? null : 'bullswarm setup --yes',
|
|
307
|
+
});
|
|
308
|
+
checks.push({
|
|
309
|
+
id: 'offload-capable',
|
|
310
|
+
ok: enabled.some((p) => p.name !== 'echo') || enabled.length > 0,
|
|
311
|
+
detail: `enabled pools: ${enabled.map((p) => p.name).join(', ') || 'none'}`,
|
|
312
|
+
fix: null,
|
|
313
|
+
});
|
|
314
|
+
} catch (err) {
|
|
315
|
+
checks.push({ id: 'meters', ok: false, detail: err.message, fix: 'check network / re-run' });
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const report = {
|
|
319
|
+
version: getVersion(),
|
|
320
|
+
configured,
|
|
321
|
+
ok: checks.every((c) => c.ok),
|
|
322
|
+
checks,
|
|
323
|
+
nextActions: checks.filter((c) => !c.ok && c.fix).map((c) => c.fix),
|
|
324
|
+
};
|
|
325
|
+
if (opts.json) console.log(JSON.stringify(report, null, 2));
|
|
326
|
+
else {
|
|
327
|
+
console.log(`bullswarm doctor (v${report.version}) — ${report.ok ? 'READY' : 'DEGRADED'}`);
|
|
328
|
+
for (const c of checks) {
|
|
329
|
+
console.log(` ${c.ok ? '✓' : '✗'} ${c.id}: ${c.detail}`);
|
|
330
|
+
if (!c.ok && c.fix) console.log(` fix: ${c.fix}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return report.ok ? 0 : 1;
|
|
334
|
+
}
|
|
335
|
+
|
|
252
336
|
// --- main ---------------------------------------------------------------------
|
|
253
337
|
|
|
254
338
|
export async function main(argv) {
|
|
255
339
|
const [verb, ...rest] = argv;
|
|
256
340
|
const opts = parseArgs(rest);
|
|
341
|
+
const { ensureSetup } = await import('./setup.js');
|
|
342
|
+
|
|
343
|
+
// Agent-friendly guarantee: EVERY verb works on a fresh machine. If config
|
|
344
|
+
// is missing, self-initialize with discovered defaults (never prompts).
|
|
345
|
+
ensureSetup(BULLSWARM_DIR);
|
|
257
346
|
|
|
258
|
-
if (!verb || verb === 'setup') {
|
|
259
|
-
if (!existsSync(join(BULLSWARM_DIR, 'state.json')) || verb === 'setup') {
|
|
260
|
-
return cmdSetup(opts);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
347
|
switch (verb) {
|
|
264
348
|
case undefined:
|
|
265
|
-
|
|
349
|
+
// Bare bullswarm: interactive wizard for humans on a TTY, auto-setup
|
|
350
|
+
// + status for everyone else (agents, scripts).
|
|
351
|
+
if (opts.yes || !process.stdin.isTTY) return cmdSetup({ ...opts, yes: true });
|
|
352
|
+
return cmdSetup(opts);
|
|
266
353
|
case 'setup':
|
|
267
354
|
return cmdSetup(opts);
|
|
268
355
|
case 'run':
|
|
@@ -271,6 +358,8 @@ export async function main(argv) {
|
|
|
271
358
|
return cmdHealth(opts);
|
|
272
359
|
case 'pools':
|
|
273
360
|
return cmdPools(opts);
|
|
361
|
+
case 'doctor':
|
|
362
|
+
return cmdDoctor(opts);
|
|
274
363
|
case 'version':
|
|
275
364
|
console.log(getVersion());
|
|
276
365
|
return 0;
|
|
@@ -278,7 +367,7 @@ export async function main(argv) {
|
|
|
278
367
|
return cmdRelease(opts);
|
|
279
368
|
default:
|
|
280
369
|
console.error(
|
|
281
|
-
`unknown verb "${verb}". try: setup | run | health | pools | version | release`,
|
|
370
|
+
`unknown verb "${verb}". try: setup | run | health | pools | doctor | version | release`,
|
|
282
371
|
);
|
|
283
372
|
return 2;
|
|
284
373
|
}
|
package/src/setup.js
CHANGED
|
@@ -185,6 +185,63 @@ export function repairConnectors(bullswarmDir) {
|
|
|
185
185
|
return repaired;
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
// --- auto-setup ---------------------------------------------------------------
|
|
189
|
+
// Zero-touch initialization: enable every discovered pool, write config,
|
|
190
|
+
// never prompt. Used by `setup --yes`, by any verb on first use, and by
|
|
191
|
+
// non-TTY invocations (agents). Humans who want choices run plain
|
|
192
|
+
// `bullswarm setup` on a terminal.
|
|
193
|
+
|
|
194
|
+
export function autoSetup(bullswarmDir, { reason = 'auto' } = {}) {
|
|
195
|
+
const state = loadState(bullswarmDir);
|
|
196
|
+
const discovered = discoverConnectors();
|
|
197
|
+
const repaired = repairConnectors(bullswarmDir);
|
|
198
|
+
|
|
199
|
+
const usable = discovered.filter((d) => !d.broken && d.discovered);
|
|
200
|
+
// Always include the deterministic echo pool so offloading works even on
|
|
201
|
+
// a machine with zero other agent CLIs installed.
|
|
202
|
+
const enabled = new Set(usable.map((d) => d.name));
|
|
203
|
+
enabled.add('echo');
|
|
204
|
+
|
|
205
|
+
state.pools ??= {};
|
|
206
|
+
for (const d of discovered.filter((x) => !x.broken)) {
|
|
207
|
+
state.pools[d.name] ??= {};
|
|
208
|
+
state.pools[d.name].enabled = enabled.has(d.name);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const chosen = discovered.filter((d) => enabled.has(d.name));
|
|
212
|
+
const table = suggestRoutingTable(chosen);
|
|
213
|
+
|
|
214
|
+
mkdirSync(join(bullswarmDir, 'connectors'), { recursive: true });
|
|
215
|
+
saveState(bullswarmDir, state);
|
|
216
|
+
writeFileSync(
|
|
217
|
+
join(bullswarmDir, 'routing.json'),
|
|
218
|
+
`${JSON.stringify(table, null, 2)}\n`,
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
initialized: true,
|
|
223
|
+
reason,
|
|
224
|
+
enabledPools: [...enabled],
|
|
225
|
+
discoveredCount: usable.length,
|
|
226
|
+
repaired,
|
|
227
|
+
routingTable: table,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function isConfigured(bullswarmDir) {
|
|
232
|
+
return existsSync(join(bullswarmDir, 'state.json'));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Idempotent first-use guarantee for every verb: if config is missing,
|
|
237
|
+
* initialize it silently. Returns null when already configured, else the
|
|
238
|
+
* autoSetup result (callers may surface it).
|
|
239
|
+
*/
|
|
240
|
+
export function ensureSetup(bullswarmDir) {
|
|
241
|
+
if (isConfigured(bullswarmDir)) return null;
|
|
242
|
+
return autoSetup(bullswarmDir, { reason: 'first-use' });
|
|
243
|
+
}
|
|
244
|
+
|
|
188
245
|
// --- wizard -------------------------------------------------------------------
|
|
189
246
|
|
|
190
247
|
export async function runWizard(bullswarmDir, opts = {}) {
|