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 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": ["~/.claude"],
4
+ "configDirs": [
5
+ "~/.claude"
6
+ ],
5
7
  "spawn": {
6
- "cmd": ["claude", "-p", "Read the file {taskFile} and follow its instructions exactly.", "--add-dir", "{cwd}", "--no-session"],
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": ["unauthorized", "authentication failed", "credit balance"],
10
- "outputExtraction": { "strategy": "stdout" },
11
- "$comment-meter": "the CALLER pool — competes in analyze/build, wins only when no delegate can take the lane",
12
- "meter": { "type": "reader", "window": "weekly+5h" },
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": ["analyze", "build", "chore"],
15
- "flags": { "stealth": false },
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.1.4",
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. Guides the user to run the bullswarm setup wizard, which discovers installed agent CLIs, shows quota state, and configures routing.
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 setup companion
6
+ # bullswarm agent guide
7
7
 
8
- You are a guide, not the installer. The CLI owns the flow; you nudge.
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
- ## When to use
12
+ ## Quick start (zero-touch)
11
13
 
12
- - The user asks to route/offload work to other agent CLIs (codex, grok, opencode, etc.)
13
- - The user mentions quota exhaustion on one subscription while others sit idle
14
- - The user asks what bullswarm is or how to set it up
14
+ ```bash
15
+ # 1. Check readiness (also self-heals a missing config):
16
+ bullswarm doctor --json
15
17
 
16
- ## What to do
18
+ # 2. See pools with live quota meters:
19
+ bullswarm pools
17
20
 
18
- 1. Check whether bullswarm is configured:
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
- ```bash
21
- ls ~/.bullswarm/state.json 2>/dev/null && echo configured || echo not-configured
22
- ```
25
+ # 4. After every offload round:
26
+ bullswarm health
27
+ ```
23
28
 
24
- 2. If **not-configured**, tell the user:
29
+ ## Reading the verdict
25
30
 
26
- > bullswarm routes work across your installed coding-agent CLIs, paced by
27
- > each subscription's quota window, and verifies every delegate's output
28
- > by content before trusting it. Run `bullswarm` (bare) to start the
29
- > setup wizard it discovers your installed agent CLIs, shows their
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
- Then offer: "Want me to run `bullswarm setup` for you now?"
36
+ ## Rules for agents
34
37
 
35
- 3. If **configured**, surface the current state:
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
- ```bash
38
- bullswarm pools
39
- ```
46
+ ## Human customization (optional)
40
47
 
41
- and remind them of the daily habit:
42
-
43
- > After every offload round, run `bullswarm health` it re-judges saved
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
- return cmdSetup(opts); // bare bullswarm with config present still guides
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 = {}) {