bullswarm 0.10.4 → 0.10.6

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 CHANGED
@@ -37,7 +37,7 @@ content. Published as `bullswarm` on npm.
37
37
  ## Development
38
38
 
39
39
  ```bash
40
- npm test # 256 tests, no network needed (meters read from cache)
40
+ npm test # 261 tests, no network needed (meters read from cache)
41
41
  node bin/bullswarm.js doctor --json # readiness report
42
42
  node bin/bullswarm.js workflow list # discover workflows
43
43
  node bin/bullswarm.js workflow runs # ongoing workflow instances
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # bullswarm changelog
2
2
 
3
+ ## 0.10.6 — safe setup and boolean flags
4
+
5
+ - Marked the deterministic `echo` connector as a test fixture and excluded it
6
+ from automatic setup, routing suggestions, connector readiness, and delegate
7
+ readiness. Existing installations receive a one-time migration that disables
8
+ an accidentally enabled fixture while preserving later explicit choices.
9
+ - Interactive setup labels test fixtures and defaults them to disabled;
10
+ `doctor` now requires at least one real enabled delegate instead of treating
11
+ canned fixture output as offload capability.
12
+ - Made top-level boolean flags explicit so options such as `--no-caller`,
13
+ `--force`, and `--dry-run` cannot swallow a following positional task.
14
+ - Thanks to @kwunlokng for reporting both defect classes and supplying focused
15
+ reproductions in #1, #2, and #3.
16
+
3
17
  ## 0.10.4 — durable interactive workflow viewer
4
18
 
5
19
  - Added `workflow runs result <id> [--json]`, a stable
@@ -23,6 +23,6 @@
23
23
  "pricingSource": "local deterministic fixture",
24
24
  "pricingUpdatedAt": "2026-08-27"
25
25
  }],
26
- "flags": { "stealth": false },
26
+ "flags": { "stealth": false, "testFixture": true },
27
27
  "$comment-timeout": "No implicit wall-clock timeout; callers may opt in with --timeout or workflow step.timeoutSec"
28
28
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bullswarm",
3
- "version": "0.10.4",
3
+ "version": "0.10.6",
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": {
package/skill/SKILL.md CHANGED
@@ -353,8 +353,8 @@ work*: "In 200 words: (1) the top 3 risks, (2) cited file paths,
353
353
  You do NOT need to set up bullswarm. Every verb self-initializes:
354
354
 
355
355
  - Missing `~/.bullswarm/`? Created on first call.
356
- - No enabled pools? `autoSetup` enables every installed agent CLI plus
357
- the deterministic `echo` pool (so offload works on a fresh machine).
356
+ - No enabled pools? `autoSetup` enables every installed real agent CLI. The
357
+ deterministic `echo` connector is a test fixture and is never auto-enabled.
358
358
  - `bullswarm doctor --json` returns a readiness report; non-zero exit
359
359
  only on a real problem. The first call to `doctor` self-heals.
360
360
 
@@ -376,7 +376,7 @@ of exact commands to fix anything missing.
376
376
  ### "Audit every file in this directory"
377
377
 
378
378
  A discover-based fanout requires the discover delegate to return a bare
379
- JSON array in its output file. The built-in `echo` pool is a verifier
379
+ JSON array in its output file. The built-in `echo` pool is a disabled test
380
380
  fixture and deliberately returns prose, so use a real provider for this
381
381
  variant:
382
382
 
package/src/cli.js CHANGED
@@ -29,14 +29,18 @@ export function getBullswarmDir() {
29
29
  // call getBullswarmDir() so BULLSWARM_HOME is honored at invocation time.
30
30
  export const BULLSWARM_DIR = getBullswarmDir();
31
31
 
32
- function parseArgs(argv) {
32
+ const BOOLEAN_FLAGS = new Set([
33
+ 'json', 'force', 'no-caller', 'yes', 'strategy', 'integrate', 'dry-run',
34
+ ]);
35
+
36
+ export function parseArgs(argv) {
33
37
  const args = {};
34
38
  const rest = [];
35
39
  for (let i = 0; i < argv.length; i++) {
36
40
  if (argv[i].startsWith('--')) {
37
41
  const eq = argv[i].indexOf('=');
38
42
  const key = argv[i].slice(2, eq > 0 ? eq : undefined);
39
- if (key === 'json') args.json = true;
43
+ if (BOOLEAN_FLAGS.has(key)) args[key] = true;
40
44
  else if (eq > 0) args[key] = argv[i].slice(eq + 1);
41
45
  else if (i + 1 < argv.length && !argv[i + 1].startsWith('--')) args[key] = argv[++i];
42
46
  else args[key] = true;
@@ -355,12 +359,12 @@ async function cmdDoctor(opts) {
355
359
  }
356
360
 
357
361
  const discovered = discoverConnectors();
358
- const found = discovered.filter((d) => d.discovered && !d.broken);
362
+ const found = discovered.filter((d) => d.discovered && !d.broken && !d.testFixture);
359
363
  checks.push({
360
364
  id: 'connectors',
361
365
  ok: found.length > 0,
362
366
  detail: `${found.length} agent CLI(s) found: ${found.map((d) => d.name).join(', ') || '(none)'}`,
363
- fix: found.length ? null : 'install at least one agent CLI (codex, grok, opencode…) — echo pool still works',
367
+ fix: found.length ? null : 'install at least one agent CLI (codex, grok, opencode…)',
364
368
  });
365
369
 
366
370
  try {
@@ -369,6 +373,7 @@ async function cmdDoctor(opts) {
369
373
  });
370
374
  const live = pools.filter((p) => p.meterSource === 'live' || p.meterSource === 'cache');
371
375
  const enabled = pools.filter((p) => p.enabled);
376
+ const delegates = enabled.filter((p) => !p.testFixture);
372
377
  checks.push({
373
378
  id: 'meters',
374
379
  ok: enabled.length > 0,
@@ -377,9 +382,9 @@ async function cmdDoctor(opts) {
377
382
  });
378
383
  checks.push({
379
384
  id: 'offload-capable',
380
- ok: enabled.some((p) => p.name !== 'echo') || enabled.length > 0,
381
- detail: `enabled pools: ${enabled.map((p) => p.name).join(', ') || 'none'}`,
382
- fix: null,
385
+ ok: delegates.length > 0,
386
+ detail: `enabled delegate pools: ${delegates.map((p) => p.name).join(', ') || 'none'}`,
387
+ fix: delegates.length ? null : 'install an agent CLI and run bullswarm setup --yes',
383
388
  });
384
389
  } catch (err) {
385
390
  checks.push({ id: 'meters', ok: false, detail: err.message, fix: 'check network / re-run' });
package/src/lib/config.js CHANGED
@@ -45,7 +45,8 @@ export function buildPools(bullswarmDir, now = Date.now(), readings = {}) {
45
45
  const pool = {
46
46
  name,
47
47
  connector: conn,
48
- enabled: ps.enabled !== false,
48
+ testFixture: conn.flags?.testFixture === true,
49
+ enabled: conn.flags?.testFixture === true ? ps.enabled === true : ps.enabled !== false,
49
50
  costRank: conn.costRank ?? 5,
50
51
  lanes: conn.lanes,
51
52
  capabilities: conn.capabilities ?? [],
package/src/setup.js CHANGED
@@ -97,6 +97,7 @@ export function discoverConnectors() {
97
97
  meter: conn.meter?.type ?? 'none',
98
98
  costRank: conn.costRank,
99
99
  lanes: conn.lanes,
100
+ testFixture: conn.flags?.testFixture === true,
100
101
  });
101
102
  }
102
103
  return found;
@@ -187,6 +188,10 @@ export function upgradeConnectorMetadata(bullswarmDir) {
187
188
  installed.model = packaged.model;
188
189
  changed = true;
189
190
  }
191
+ if (packaged.flags?.testFixture === true && installed.flags?.testFixture !== true) {
192
+ installed.flags = { ...(installed.flags ?? {}), testFixture: true };
193
+ changed = true;
194
+ }
190
195
  if (installed.eventStream == null && packaged.eventStream != null) {
191
196
  installed.eventStream = packaged.eventStream;
192
197
  // Older packaged connectors used stdout extraction. Once JSONL flags
@@ -218,6 +223,34 @@ export function upgradeConnectorMetadata(bullswarmDir) {
218
223
  return upgraded;
219
224
  }
220
225
 
226
+ // One-time safety migration for installations created before connectors could
227
+ // identify deterministic test fixtures. Explicit choices made after this
228
+ // migration remain respected.
229
+ export function migrateTestFixturePools(bullswarmDir) {
230
+ const state = loadState(bullswarmDir);
231
+ state.config ??= {};
232
+ if (state.config.testFixturesMigrated === true) return [];
233
+
234
+ const connectorsDir = join(bullswarmDir, 'connectors');
235
+ const disabled = [];
236
+ if (existsSync(connectorsDir)) {
237
+ for (const file of readdirSync(connectorsDir)) {
238
+ if (!file.endsWith('.json') || file.startsWith('_')) continue;
239
+ try {
240
+ const connector = JSON.parse(readFileSync(join(connectorsDir, file), 'utf8'));
241
+ if (connector.flags?.testFixture !== true) continue;
242
+ state.pools ??= {};
243
+ state.pools[connector.name] ??= {};
244
+ if (state.pools[connector.name].enabled !== false) disabled.push(connector.name);
245
+ state.pools[connector.name].enabled = false;
246
+ } catch { /* connector repair owns malformed files */ }
247
+ }
248
+ }
249
+ state.config.testFixturesMigrated = true;
250
+ saveState(bullswarmDir, state);
251
+ return disabled;
252
+ }
253
+
221
254
  // --- auto-setup ---------------------------------------------------------------
222
255
  // Zero-touch initialization: enable every discovered pool, write config,
223
256
  // never prompt. Used by `setup --yes`, by any verb on first use, and by
@@ -229,13 +262,12 @@ export function autoSetup(bullswarmDir, { reason = 'auto' } = {}) {
229
262
  const discovered = discoverConnectors();
230
263
  const repaired = repairConnectors(bullswarmDir);
231
264
 
232
- const usable = discovered.filter((d) => !d.broken && d.discovered);
233
- // Always include the deterministic echo pool so offloading works even on
234
- // a machine with zero other agent CLIs installed.
265
+ const usable = discovered.filter((d) => !d.broken && d.discovered && !d.testFixture);
235
266
  const enabled = new Set(usable.map((d) => d.name));
236
- enabled.add('echo');
237
267
 
238
268
  state.pools ??= {};
269
+ state.config ??= {};
270
+ state.config.testFixturesMigrated = true;
239
271
  for (const d of discovered.filter((x) => !x.broken)) {
240
272
  state.pools[d.name] ??= {};
241
273
  state.pools[d.name].enabled = enabled.has(d.name);
@@ -275,6 +307,7 @@ export function isConfigured(bullswarmDir) {
275
307
  export function ensureSetup(bullswarmDir) {
276
308
  if (isConfigured(bullswarmDir)) {
277
309
  upgradeConnectorMetadata(bullswarmDir);
310
+ migrateTestFixturePools(bullswarmDir);
278
311
  return null;
279
312
  }
280
313
  return autoSetup(bullswarmDir, { reason: 'first-use' });
@@ -306,7 +339,7 @@ export async function runWizard(bullswarmDir, opts = {}) {
306
339
  ? 'quota: unmetered'
307
340
  : `quota: ${d.meter} window (burn rate: learning)`;
308
341
  console.log(
309
- ` ${d.name.padEnd(14)} ${d.discovered ? 'found' : 'not found'} ${meter}`,
342
+ ` ${d.name.padEnd(14)} ${d.discovered ? 'found' : 'not found'} ${meter}${d.testFixture ? ' TEST FIXTURE' : ''}`,
310
343
  );
311
344
  }
312
345
  console.log('');
@@ -314,8 +347,11 @@ export async function runWizard(bullswarmDir, opts = {}) {
314
347
  // 2. Toggle pools
315
348
  const enabled = [];
316
349
  for (const d of discovered.filter((x) => !x.broken && x.discovered)) {
317
- const ans = (await rl.question(`enable ${d.name}? [Y/n] `)).trim().toLowerCase();
318
- if (ans !== 'n') enabled.push(d.name);
350
+ const prompt = d.testFixture
351
+ ? `enable ${d.name} test fixture? [y/N] `
352
+ : `enable ${d.name}? [Y/n] `;
353
+ const ans = (await rl.question(prompt)).trim().toLowerCase();
354
+ if (d.testFixture ? (ans === 'y' || ans === 'yes') : ans !== 'n') enabled.push(d.name);
319
355
  }
320
356
 
321
357
  if (enabled.length === 0) {
@@ -332,6 +368,8 @@ export async function runWizard(bullswarmDir, opts = {}) {
332
368
  mkdirSync(join(bullswarmDir, 'connectors'), { recursive: true });
333
369
  const repaired = repairConnectors(bullswarmDir);
334
370
  state.pools ??= {};
371
+ state.config ??= {};
372
+ state.config.testFixturesMigrated = true;
335
373
  for (const d of discovered.filter((x) => !x.broken)) {
336
374
  state.pools[d.name] ??= {};
337
375
  state.pools[d.name].enabled = enabled.includes(d.name);