bullswarm 0.10.1 → 0.10.3

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 # 195 tests, no network needed (meters read from cache)
40
+ npm test # 235 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,32 @@
1
1
  # bullswarm changelog
2
2
 
3
+ ## 0.10.3 — contextual help everywhere
4
+
5
+ - Added side-effect-free `-h` / `--help` handling for the top-level CLI and
6
+ every command and nested subcommand, including workflow drafts, run history,
7
+ approvals, actions, integrations, and strategy policy controls.
8
+ - Added a centralized command help tree so contextual help is consistent and
9
+ intercepted before setup, provider discovery, state writes, or destructive
10
+ command execution.
11
+
12
+ ## 0.10.2 — cross-agent skill integration
13
+
14
+ - Added explicit `bullswarm integrate status|install|remove` support for Codex,
15
+ Claude, and Grok. Installation registers one packaged `bullswarm` skill with
16
+ all selected agents and writes concise marker-delimited global awareness
17
+ rules; removal touches only Bullswarm-managed links and blocks.
18
+ - Added recoverable `integrate retire-legacy --yes` migration for the retired
19
+ Claude `offload` skill. Detection is read-only and retirement always moves the
20
+ old skill into `~/.claude/skills-archive/`.
21
+ - Added recursion-aware global guidance: a worker with `BULLSWARM_DEPTH` set
22
+ performs its assigned task directly instead of casually spawning another
23
+ swarm.
24
+ - Renamed the published agent skill from `bullswarm-setup` to `bullswarm` and
25
+ documented single-task, zero-graph goal, fixed-workflow, observation, and
26
+ integration paths together.
27
+ - Corrected README language so agent/time targets are advisory while graph
28
+ expansion limits remain hard safeguards.
29
+
3
30
  ## 0.10.1 — initiated-time workflow history search
4
31
 
5
32
  - Added `workflow runs --since <time> --until <time>` filtering against the
package/README.md CHANGED
@@ -5,6 +5,15 @@ orchestrator, build and expand the plan, route bounded worker actions by quota,
5
5
  verify the result, and finish without an initiating agent authoring a graph.
6
6
  Every delegate output is judged by content before it counts.
7
7
 
8
+ Every command and nested subcommand supports contextual `-h` / `--help`
9
+ without initializing state or executing the command:
10
+
11
+ ```bash
12
+ bullswarm --help
13
+ bullswarm workflow run --help
14
+ bullswarm workflow draft step add --help
15
+ ```
16
+
8
17
  ## The doctrine (non-negotiable)
9
18
 
10
19
  1. **Judge by CONTENT, not exit code.** Every delegate CLI can exit 0 while
@@ -23,8 +32,24 @@ Every delegate output is judged by content before it counts.
23
32
 
24
33
  ```bash
25
34
  npm install -g bullswarm # or: node bin/bullswarm.js directly from a checkout
35
+ bullswarm integrate install --agents codex,claude,grok --yes
26
36
  ```
27
37
 
38
+ The integration command registers Bullswarm's packaged `bullswarm` skill with
39
+ Codex, Claude, and Grok and appends a concise, marker-delimited awareness rule
40
+ to each agent's global instructions. It is explicit, idempotent, and reversible:
41
+
42
+ ```bash
43
+ bullswarm integrate status --json
44
+ bullswarm integrate remove --agents codex,claude,grok --yes
45
+ ```
46
+
47
+ If the retired pre-Bullswarm Claude `offload` skill is detected, status reports
48
+ it without changing it. Archive it recoverably with
49
+ `bullswarm integrate retire-legacy --yes`. The awareness rule prevents workers
50
+ already launched by Bullswarm (`BULLSWARM_DEPTH` is set) from casually
51
+ re-delegating and creating recursive swarms.
52
+
28
53
  ## Quick start
29
54
 
30
55
  ```bash
@@ -42,6 +67,7 @@ bullswarm health # re-judge saved outputs; catch gate failures
42
67
  | Verb | Purpose |
43
68
  |---|---|
44
69
  | `setup` | Discover installed agent CLIs, show quota state, toggle pools, suggest a routing table, write config. Approval-gated, idempotent. |
70
+ | `integrate` | Register or remove the canonical Bullswarm skill and global awareness rules for Codex, Claude, and Grok. |
45
71
  | `run` | route → dispatch → watch → verify → one JSON verdict |
46
72
  | `health` | Re-judge saved outputs against their verdicts; surface verify-gate failures and quarantine clusters |
47
73
  | `pools` | Show each pool's meter state, pace position, quarantine status |
@@ -107,7 +133,7 @@ orchestrator by live quota surplus. The orchestrator observes durable evidence,
107
133
  and decides when another expansion or verification is necessary. Bullswarm
108
134
  validates the proposal, owns agent/process selection, routes workers, and calls
109
135
  the orchestrator again until completion, cancellation, failure, approval, or a
110
- budget limit. No initial phases, prompts, JSON schema, or agent choice are
136
+ hard graph-growth safeguard. No initial phases, prompts, JSON schema, or agent choice are
111
137
  required from the user.
112
138
 
113
139
  The detached response includes a short ID and exact observation commands:
@@ -128,9 +154,10 @@ bullswarm workflow goal --resume <shortId> --json
128
154
  ```
129
155
 
130
156
  `--orchestrator <pool>` exists for controlled testing; ordinary use should
131
- leave selection on `auto`. Hard limits can be adjusted with `--max-agents`,
132
- `--max-expansion-rounds`, `--max-actions`, `--max-items-per-expansion`, and
133
- `--max-workflow-seconds`. Interactive setup also records a worktree-isolation
157
+ leave selection on `auto`. `--max-agents` and `--max-workflow-seconds` are
158
+ advisory planning targets; hard graph-growth safeguards are adjusted with
159
+ `--max-expansion-rounds`, `--max-actions`, and `--max-items-per-expansion`.
160
+ Interactive setup also records a worktree-isolation
134
161
  preference (`agent-decides`, `off`, or `required`); Bullswarm communicates that
135
162
  policy to the orchestrator without imposing repository topology itself.
136
163
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bullswarm",
3
- "version": "0.10.1",
3
+ "version": "0.10.3",
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
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: bullswarm-setup
2
+ name: bullswarm
3
3
  description: Use when you want to offload work to coding-agent subscriptions or run a self-contained goal across heterogeneous providers. bullswarm can accept one goal, select an orchestrator, expand and execute a bounded plan, verify the outcome, and expose the whole run through CLI state and events. Use `bullswarm workflow goal` for ordinary multi-step work, an explicit workflow draft when the graph itself is the contract, and `bullswarm run` for one bounded task. Every verb self-initializes.
4
4
  ---
5
5
 
@@ -10,6 +10,20 @@ CLI subscription has the most quota headroom. Every delegate output is
10
10
  judged by **content**, not exit code. A non-zero exit is never a success; a
11
11
  `verified` output is.
12
12
 
13
+ Every command and nested subcommand supports side-effect-free `-h` / `--help`.
14
+ When a flag or argument is uncertain, inspect the exact surface before acting,
15
+ for example `bullswarm workflow draft step add --help`.
16
+
17
+ This skill is registered globally by:
18
+
19
+ ```bash
20
+ bullswarm integrate install --agents codex,claude,grok --yes
21
+ ```
22
+
23
+ If `BULLSWARM_DEPTH` is already set, you are a Bullswarm delegate. Complete the
24
+ assigned task directly; do not recursively invoke Bullswarm unless the task
25
+ explicitly requires another bounded delegation.
26
+
13
27
  ## When to reach for it
14
28
 
15
29
  You should consider bullswarm when **any** of the following apply:
@@ -306,6 +320,10 @@ You do NOT need to set up bullswarm. Every verb self-initializes:
306
320
  bullswarm doctor --json
307
321
  ```
308
322
 
323
+ Self-initialization prepares routing and connectors; it does not silently edit
324
+ global agent instructions. Inspect or explicitly install agent awareness with
325
+ `bullswarm integrate status --json` and `bullswarm integrate install --yes`.
326
+
309
327
  The output has a `checks[]` array with one entry per readiness concern
310
328
  (config, connectors, meters, offload-capable) and a `nextActions[]` list
311
329
  of exact commands to fix anything missing.
package/src/cli.js CHANGED
@@ -16,6 +16,8 @@ import { getVersion } from './lib/version.js';
16
16
  import { release } from './lib/release.js';
17
17
  import { cmdWorkflow } from './workflow/cli.js';
18
18
  import { cmdStrategy, maybeRefreshStrategy } from './strategy-cli.js';
19
+ import { cmdIntegrate, installIntegration } from './integrate.js';
20
+ import { helpForArgs } from './help.js';
19
21
 
20
22
  export function getBullswarmDir() {
21
23
  const h = process.env.BULLSWARM_HOME?.trim();
@@ -31,8 +33,10 @@ function parseArgs(argv) {
31
33
  const rest = [];
32
34
  for (let i = 0; i < argv.length; i++) {
33
35
  if (argv[i].startsWith('--')) {
34
- const key = argv[i].slice(2);
36
+ const eq = argv[i].indexOf('=');
37
+ const key = argv[i].slice(2, eq > 0 ? eq : undefined);
35
38
  if (key === 'json') args.json = true;
39
+ else if (eq > 0) args[key] = argv[i].slice(eq + 1);
36
40
  else if (i + 1 < argv.length && !argv[i + 1].startsWith('--')) args[key] = argv[++i];
37
41
  else args[key] = true;
38
42
  } else rest.push(argv[i]);
@@ -295,12 +299,20 @@ async function cmdSetup(opts) {
295
299
  const report = await refreshStrategy(getBullswarmDir());
296
300
  strategy = applyStrategyRecommendations(getBullswarmDir(), report);
297
301
  }
298
- if (opts.json) console.log(JSON.stringify({ ok: true, mode: 'auto', ...r, strategy }, null, 2));
302
+ let integration = null;
303
+ if (opts.yes && opts.integrate) {
304
+ integration = installIntegration({
305
+ agents: opts.agents,
306
+ approved: true,
307
+ });
308
+ }
309
+ if (opts.json) console.log(JSON.stringify({ ok: true, mode: 'auto', ...r, strategy, integration }, null, 2));
299
310
  else {
300
311
  console.log(`setup complete (${r.reason}): enabled ${r.enabledPools.join(', ')}`);
301
312
  if (r.repaired.length) console.log(`repaired connector files: ${r.repaired.join(', ')}`);
302
313
  console.log(`model strategy: ${r.strategyCommand} (discovers models and refreshes tier suggestions)`);
303
314
  if (strategy) console.log(`strategy autopilot: applied ${Object.keys(strategy.applied).join(', ')} tiers; refresh every ${strategy.policy.refreshHours}h`);
315
+ if (integration) console.log('agent integration: installed (inspect with bullswarm integrate status)');
304
316
  }
305
317
  return 0;
306
318
  }
@@ -383,6 +395,11 @@ async function cmdDoctor(opts) {
383
395
  // --- main ---------------------------------------------------------------------
384
396
 
385
397
  export async function main(argv) {
398
+ const help = helpForArgs(argv);
399
+ if (help) {
400
+ console.log(help);
401
+ return 0;
402
+ }
386
403
  const [verb, ...rest] = argv;
387
404
  const opts = parseArgs(rest);
388
405
  const { ensureSetup } = await import('./setup.js');
@@ -413,6 +430,8 @@ export async function main(argv) {
413
430
  return cmdWorkflow(['runs', ...rest]);
414
431
  case 'strategy':
415
432
  return cmdStrategy(rest, { bullswarmDir: getBullswarmDir() });
433
+ case 'integrate':
434
+ return cmdIntegrate(opts);
416
435
  case 'version':
417
436
  case '--version':
418
437
  console.log(getVersion());
@@ -421,7 +440,7 @@ export async function main(argv) {
421
440
  return cmdRelease(opts);
422
441
  default:
423
442
  console.error(
424
- `unknown verb "${verb}". try: setup | run | health | pools | strategy | doctor | workflow | runs | version | release`,
443
+ `unknown verb "${verb}". try: setup | integrate | run | health | pools | strategy | doctor | workflow | runs | version | release`,
425
444
  );
426
445
  return 2;
427
446
  }
package/src/help.js ADDED
@@ -0,0 +1,150 @@
1
+ // Central CLI help tree. Help is resolved before setup or command dispatch so
2
+ // `--help` is side-effect free even for commands that normally touch state.
3
+
4
+ const top = `Usage: bullswarm <command> [options]
5
+
6
+ Route bounded work across coding-agent subscriptions and verify the result.
7
+
8
+ Commands:
9
+ setup discover and configure installed coding agents
10
+ integrate register Bullswarm guidance with Codex, Claude, and Grok
11
+ run dispatch one bounded task
12
+ health re-judge saved delegate outputs
13
+ pools show routing pools, meters, and quarantine state
14
+ strategy discover models and manage tier assignments
15
+ doctor report installation readiness
16
+ workflow create, execute, observe, and audit workflows
17
+ runs alias for workflow runs
18
+ version print the installed version
19
+ release create a version commit and tag
20
+
21
+ Run bullswarm <command> --help for command-specific help.`;
22
+
23
+ const leaf = (usage, body = '') => `Usage: ${usage}${body ? `\n\n${body}` : ''}`;
24
+
25
+ const HELP = {
26
+ _text: top,
27
+ setup: { _text: leaf('bullswarm setup [--yes] [--integrate] [--agents <list>] [--json]',
28
+ 'Discover installed agent CLIs and initialize routing. Without --yes on a TTY, opens the interactive wizard.') },
29
+ integrate: {
30
+ _text: leaf('bullswarm integrate <status|install|remove|retire-legacy> [options]',
31
+ 'Manage the canonical Bullswarm skill and recursion-safe global awareness rules.'),
32
+ status: { _text: leaf('bullswarm integrate status [--agents codex,claude,grok] [--json]') },
33
+ install: { _text: leaf('bullswarm integrate install [--agents codex,claude,grok] --yes [--json]') },
34
+ remove: { _text: leaf('bullswarm integrate remove [--agents codex,claude,grok] --yes [--json]') },
35
+ 'retire-legacy': { _text: leaf('bullswarm integrate retire-legacy --yes [--json]',
36
+ 'Recoverably archive the retired Claude offload skill.') },
37
+ },
38
+ run: { _text: leaf('bullswarm run --lane <analyze|build|chore> --add-dir <dir> (--task-file <file> | --prompt <text>) [options]',
39
+ 'Options: --effort <high|medium|low>, --timeout <seconds>, --json. Routes one task and verifies its content.') },
40
+ health: { _text: leaf('bullswarm health [--json]', 'Re-judge saved outputs and report failed gates or quarantine clusters.') },
41
+ pools: { _text: leaf('bullswarm pools [--force] [--json]', 'Show live meter, quota-surplus, burst-gate, and quarantine state.') },
42
+ doctor: { _text: leaf('bullswarm doctor [--json]', 'Self-initialize if needed and report readiness without dispatching work.') },
43
+ version: { _text: leaf('bullswarm version') },
44
+ release: { _text: leaf('bullswarm release <patch|minor|major> [--dry-run]') },
45
+ strategy: {
46
+ _text: leaf('bullswarm strategy <command> [options]',
47
+ 'Commands: refresh, apply, show, assign, clear-assignment, set-subscription, auto.'),
48
+ refresh: { _text: leaf('bullswarm strategy refresh [--json] [--apply --yes] [--refresh-hours <n>]') },
49
+ recommend: { _text: leaf('bullswarm strategy recommend [--json] [--apply --yes]', 'Alias for strategy refresh.') },
50
+ apply: { _text: leaf('bullswarm strategy apply --yes [--refresh-hours <n>]') },
51
+ show: { _text: leaf('bullswarm strategy show [--json]') },
52
+ assign: { _text: leaf('bullswarm strategy assign <high|medium|low> --pool <pool> --model <model>') },
53
+ 'clear-assignment': { _text: leaf('bullswarm strategy clear-assignment <high|medium|low>') },
54
+ 'set-subscription': { _text: leaf('bullswarm strategy set-subscription <pool> [--plan <name>] [--monthly-usd <n|unknown>] [--included-usd <n|unknown>] [--quota-window <name>]') },
55
+ auto: {
56
+ _text: leaf('bullswarm strategy auto <status|off> [--yes]'),
57
+ status: { _text: leaf('bullswarm strategy auto status') },
58
+ off: { _text: leaf('bullswarm strategy auto off --yes') },
59
+ },
60
+ },
61
+ workflow: {
62
+ _text: leaf('bullswarm workflow <command> [options]',
63
+ 'Commands: goal, run, validate, list, draft, runs, capabilities, inspect, tui, watch, events, steer, action, approval.'),
64
+ goal: { _text: leaf('bullswarm workflow goal "<goal>" [--cwd <dir>] [--detach] [--json] [planning options]',
65
+ 'Use --resume <shortId|runId> to resume. Planning options include --orchestrator, --max-agents, --max-expansion-rounds, --max-actions, --max-items-per-expansion, --max-workflow-seconds, --concurrency, and --retry-attempts.') },
66
+ run: { _text: leaf('bullswarm workflow run <file-or-name> [--input k=v]... [--resume <shortId|runId>] [--json] [--quiet]') },
67
+ validate: { _text: leaf('bullswarm workflow validate <file-or-name>') },
68
+ list: { _text: leaf('bullswarm workflow list [--json]') },
69
+ capabilities: { _text: leaf('bullswarm workflow capabilities [--json]') },
70
+ inspect: { _text: leaf('bullswarm workflow inspect <file-or-name>') },
71
+ tui: { _text: leaf('bullswarm workflow tui [<runId>] [--json] [--all] [--show <runId>] [--cancel <runId>]') },
72
+ watch: { _text: leaf('bullswarm workflow watch <runId> [--interval <seconds>] [--heartbeat <seconds>] [--jsonl] [--once]') },
73
+ events: { _text: leaf('bullswarm workflow events <runId> [--after <sequence>] [--json]') },
74
+ steer: { _text: leaf('bullswarm workflow steer <runId> --message <guidance> [--json]') },
75
+ action: {
76
+ _text: leaf('bullswarm workflow action <command> ...', 'Commands: show.'),
77
+ show: { _text: leaf('bullswarm workflow action show <runId> <actionId> [--json]') },
78
+ },
79
+ approval: {
80
+ _text: leaf('bullswarm workflow approval <approve|reject> <runId> [--json]'),
81
+ approve: { _text: leaf('bullswarm workflow approval approve <runId> [--json]') },
82
+ reject: { _text: leaf('bullswarm workflow approval reject <runId> [--json]') },
83
+ },
84
+ runs: runsHelp(),
85
+ draft: draftHelp(),
86
+ },
87
+ };
88
+
89
+ // Top-level `runs` is a documented alias and gets the same nested help.
90
+ HELP.runs = HELP.workflow.runs;
91
+
92
+ export const HELP_PATHS = Object.freeze(collectPaths(HELP));
93
+
94
+ export function helpForArgs(argv) {
95
+ const wantsHelp = argv.includes('--help') || argv.includes('-h') || argv[0] === 'help';
96
+ if (!wantsHelp) return null;
97
+ const tokens = argv[0] === 'help' ? argv.slice(1) : argv;
98
+ let node = HELP;
99
+ for (const token of tokens) {
100
+ if (token === '--help' || token === '-h' || token.startsWith('-')) continue;
101
+ if (!node[token]) break;
102
+ node = node[token];
103
+ }
104
+ return node._text ?? HELP._text;
105
+ }
106
+
107
+ function runsHelp() {
108
+ return {
109
+ _text: leaf('bullswarm workflow runs [list] [--all|--historical] [--name <workflow>] [--since <time>] [--until <time>] [--limit <n>] [--json]',
110
+ 'Commands: show <id>, delete <id> --yes. Time filters use the workflow initiation timestamp.'),
111
+ list: { _text: leaf('bullswarm workflow runs list [--all|--historical] [--name <workflow>] [--since <time>] [--until <time>] [--limit <n>] [--json]') },
112
+ show: { _text: leaf('bullswarm workflow runs show <shortId|runId> [--json]') },
113
+ delete: { _text: leaf('bullswarm workflow runs delete <shortId|runId> --yes [--force] [--json]') },
114
+ };
115
+ }
116
+
117
+ function draftHelp() {
118
+ return {
119
+ _text: leaf('bullswarm workflow draft <command> [options]',
120
+ 'Commands: create, show, list, phase, step, set, validate, export, delete, run.'),
121
+ create: { _text: leaf('bullswarm workflow draft create <name> [--description <text>] [--input k=v]... [--required <keys>] [--json]') },
122
+ show: { _text: leaf('bullswarm workflow draft show <name> [--json]') },
123
+ list: { _text: leaf('bullswarm workflow draft list [--json]') },
124
+ phase: {
125
+ _text: leaf('bullswarm workflow draft phase <add|remove> <draft> <phase> [--json]'),
126
+ add: { _text: leaf('bullswarm workflow draft phase add <draft> <phase> [--json]') },
127
+ remove: { _text: leaf('bullswarm workflow draft phase remove <draft> <phase> [--json]') },
128
+ },
129
+ step: {
130
+ _text: leaf('bullswarm workflow draft step <add|remove|set> <draft> <phase> <step-id> [options]'),
131
+ add: { _text: leaf('bullswarm workflow draft step add <draft> <phase> <step-id> [--type <run|fanout|verify|decide>] [--lane <lane>] [--prompt <text>] [step options]') },
132
+ remove: { _text: leaf('bullswarm workflow draft step remove <draft> <phase> <step-id> [--json]') },
133
+ set: { _text: leaf('bullswarm workflow draft step set <draft> <phase> <step-id> <field> --value <text> [--json]') },
134
+ },
135
+ set: { _text: leaf('bullswarm workflow draft set <draft> <field> --value <text> [--json]') },
136
+ validate: { _text: leaf('bullswarm workflow draft validate <name> [--json]') },
137
+ export: { _text: leaf('bullswarm workflow draft export <name> <out-file> [--json]') },
138
+ delete: { _text: leaf('bullswarm workflow draft delete <name> --yes [--json]') },
139
+ run: { _text: leaf('bullswarm workflow draft run <name> [--input k=v]... [--resume <shortId|runId>] [--json] [--quiet]') },
140
+ };
141
+ }
142
+
143
+ function collectPaths(node, prefix = []) {
144
+ const paths = prefix.length ? [prefix] : [[]];
145
+ for (const [name, child] of Object.entries(node)) {
146
+ if (name === '_text') continue;
147
+ paths.push(...collectPaths(child, [...prefix, name]));
148
+ }
149
+ return paths;
150
+ }
@@ -0,0 +1,265 @@
1
+ // bullswarm integrate — make the published agent guide discoverable by coding CLIs.
2
+ //
3
+ // Integration is explicit, idempotent, and reversible. The full operating
4
+ // procedure remains in skill/SKILL.md; global instruction files receive only a
5
+ // compact trigger policy so CLI documentation cannot drift in three places.
6
+
7
+ import {
8
+ existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, renameSync,
9
+ symlinkSync, unlinkSync, writeFileSync,
10
+ } from 'node:fs';
11
+ import { dirname, join, resolve } from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+
14
+ const SKILL_SOURCE = fileURLToPath(new URL('../skill', import.meta.url));
15
+ const MARKER_BEGIN = '<!-- bullswarm:begin v2 -->';
16
+ const MARKER_END = '<!-- bullswarm:end -->';
17
+ const MARKER_RE = /<!-- bullswarm:begin v\d+ -->[\s\S]*?<!-- bullswarm:end -->\n?/;
18
+
19
+ const AGENT_LAYOUT = {
20
+ codex: { skill: ['.codex', 'skills', 'bullswarm'], instructions: ['.codex', 'AGENTS.md'] },
21
+ claude: { skill: ['.claude', 'skills', 'bullswarm'], instructions: ['.claude', 'CLAUDE.md'] },
22
+ grok: { skill: ['.grok', 'skills', 'bullswarm'], instructions: ['.grok', 'AGENTS.md'] },
23
+ };
24
+
25
+ export const INTEGRATION_AGENTS = Object.freeze(Object.keys(AGENT_LAYOUT));
26
+
27
+ export function awarenessBlock() {
28
+ return `${MARKER_BEGIN}
29
+ ## Bullswarm delegation
30
+
31
+ Bullswarm is available for bounded external delegation. When delegation,
32
+ offloading, independent verification, or autonomous multi-step execution is
33
+ requested, read the \`bullswarm\` skill before acting. Use \`bullswarm run\` for
34
+ one bounded task, \`bullswarm workflow goal\` for ordinary autonomous multi-step
35
+ work, and workflow drafts only when the graph itself is the contract. Treat
36
+ returned artifacts and verification as evidence, not authority. This policy
37
+ supersedes retired pre-Bullswarm \`offload\` routing instructions. If
38
+ \`BULLSWARM_DEPTH\` is already set, perform the assigned task directly and do
39
+ not recursively invoke Bullswarm unless the task explicitly requires it.
40
+ ${MARKER_END}`;
41
+ }
42
+
43
+ export function applyAwarenessBlock(filePath, { approved }) {
44
+ if (!approved) return { changed: false, reason: 'not approved' };
45
+ const existing = readOptional(filePath);
46
+ const stripped = existing.replace(MARKER_RE, '').trimEnd();
47
+ const next = stripped ? `${stripped}\n\n${awarenessBlock()}\n` : `${awarenessBlock()}\n`;
48
+ mkdirSync(dirname(filePath), { recursive: true });
49
+ if (next === existing) return { changed: false, reason: 'already current' };
50
+ writeFileSync(filePath, next);
51
+ return { changed: true, reason: existing.match(MARKER_RE) ? 'updated' : 'installed' };
52
+ }
53
+
54
+ export function removeAwarenessBlock(filePath, { approved }) {
55
+ if (!approved) return { changed: false, reason: 'not approved' };
56
+ if (!existsSync(filePath)) return { changed: false, reason: 'not installed' };
57
+ const existing = readFileSync(filePath, 'utf8');
58
+ const next = existing.replace(MARKER_RE, '').trimEnd();
59
+ if (next === existing.trimEnd()) return { changed: false, reason: 'not installed' };
60
+ writeFileSync(filePath, next ? `${next}\n` : '');
61
+ return { changed: true, reason: 'removed' };
62
+ }
63
+
64
+ export function awarenessBlockPresent(filePath) {
65
+ return MARKER_RE.test(readOptional(filePath));
66
+ }
67
+
68
+ export function parseIntegrationAgents(value) {
69
+ const requested = value == null || value === true
70
+ ? INTEGRATION_AGENTS
71
+ : String(value).split(',').map((item) => item.trim().toLowerCase()).filter(Boolean);
72
+ const unique = [...new Set(requested)];
73
+ const invalid = unique.filter((agent) => !INTEGRATION_AGENTS.includes(agent));
74
+ if (invalid.length) {
75
+ throw new Error(`unknown integration agent(s): ${invalid.join(', ')}; use ${INTEGRATION_AGENTS.join(', ')}`);
76
+ }
77
+ if (!unique.length) throw new Error('--agents must name at least one agent');
78
+ return unique;
79
+ }
80
+
81
+ export function integrationStatus({
82
+ homeDir = process.env.HOME ?? '', agents = INTEGRATION_AGENTS, skillSource = SKILL_SOURCE,
83
+ } = {}) {
84
+ const selected = parseIntegrationAgents(agents);
85
+ const entries = selected.map((agent) => {
86
+ const paths = pathsFor(homeDir, agent);
87
+ return {
88
+ agent,
89
+ skillPath: paths.skillPath,
90
+ skill: skillLinkStatus(paths.skillPath, skillSource),
91
+ instructionsPath: paths.instructionsPath,
92
+ awareness: awarenessBlockPresent(paths.instructionsPath),
93
+ };
94
+ });
95
+ const legacyPath = join(homeDir, '.claude', 'skills', 'offload');
96
+ return {
97
+ ok: entries.every((entry) => entry.skill.status === 'installed' && entry.awareness),
98
+ skillSource,
99
+ agents: entries,
100
+ legacyOffload: {
101
+ path: legacyPath,
102
+ detected: existsSync(legacyPath),
103
+ action: existsSync(legacyPath)
104
+ ? 'bullswarm integrate retire-legacy --yes'
105
+ : null,
106
+ },
107
+ };
108
+ }
109
+
110
+ export function installIntegration({
111
+ homeDir = process.env.HOME ?? '', agents = INTEGRATION_AGENTS,
112
+ skillSource = SKILL_SOURCE, approved = false,
113
+ } = {}) {
114
+ if (!approved) throw new Error('integration changes global agent configuration; pass --yes to approve');
115
+ const selected = parseIntegrationAgents(agents);
116
+ if (!existsSync(join(skillSource, 'SKILL.md'))) {
117
+ throw new Error(`packaged Bullswarm skill is missing: ${join(skillSource, 'SKILL.md')}`);
118
+ }
119
+ for (const agent of selected) {
120
+ const { skillPath } = pathsFor(homeDir, agent);
121
+ if (skillLinkStatus(skillPath, skillSource).status === 'conflict') {
122
+ throw new Error(`refusing to replace non-Bullswarm skill path: ${skillPath}`);
123
+ }
124
+ }
125
+ const changes = [];
126
+ for (const agent of selected) {
127
+ const paths = pathsFor(homeDir, agent);
128
+ const skill = installSkillLink(paths.skillPath, skillSource);
129
+ const awareness = applyAwarenessBlock(paths.instructionsPath, { approved: true });
130
+ changes.push({ agent, skill, awareness });
131
+ }
132
+ return { action: 'install', changes, status: integrationStatus({ homeDir, agents: selected, skillSource }) };
133
+ }
134
+
135
+ export function removeIntegration({
136
+ homeDir = process.env.HOME ?? '', agents = INTEGRATION_AGENTS,
137
+ skillSource = SKILL_SOURCE, approved = false,
138
+ } = {}) {
139
+ if (!approved) throw new Error('integration removal changes global agent configuration; pass --yes to approve');
140
+ const selected = parseIntegrationAgents(agents);
141
+ const changes = [];
142
+ for (const agent of selected) {
143
+ const paths = pathsFor(homeDir, agent);
144
+ const skill = removeSkillLink(paths.skillPath, skillSource);
145
+ const awareness = removeAwarenessBlock(paths.instructionsPath, { approved: true });
146
+ changes.push({ agent, skill, awareness });
147
+ }
148
+ return { action: 'remove', changes, status: integrationStatus({ homeDir, agents: selected, skillSource }) };
149
+ }
150
+
151
+ export function retireLegacyOffload({ homeDir = process.env.HOME ?? '', approved = false, now = new Date() } = {}) {
152
+ if (!approved) throw new Error('legacy offload retirement moves a user skill; pass --yes to approve');
153
+ const source = join(homeDir, '.claude', 'skills', 'offload');
154
+ if (!existsSync(source)) return { action: 'retire-legacy', changed: false, reason: 'not installed' };
155
+ const archiveRoot = join(homeDir, '.claude', 'skills-archive');
156
+ mkdirSync(archiveRoot, { recursive: true });
157
+ const stamp = now.toISOString().replace(/[:.]/g, '-');
158
+ const destination = join(archiveRoot, `offload-before-bullswarm-${stamp}`);
159
+ renameSync(source, destination);
160
+ return { action: 'retire-legacy', changed: true, source, destination, recoverable: true };
161
+ }
162
+
163
+ export function integrateUsage() {
164
+ return `usage: bullswarm integrate [status] [--agents codex,claude,grok] [--json]
165
+ bullswarm integrate install --agents codex,claude,grok --yes [--json]
166
+ bullswarm integrate remove --agents codex,claude,grok --yes [--json]
167
+ bullswarm integrate retire-legacy --yes [--json]
168
+
169
+ Install registers the packaged Bullswarm skill and a concise recursion-safe
170
+ awareness rule. Removal only deletes Bullswarm-managed symlinks and marker
171
+ blocks. retire-legacy moves ~/.claude/skills/offload into skills-archive.`;
172
+ }
173
+
174
+ export function cmdIntegrate(opts) {
175
+ const subcommand = opts.rest[0] ?? 'status';
176
+ if (opts.help || subcommand === 'help') {
177
+ console.log(integrateUsage());
178
+ return 0;
179
+ }
180
+ let agents;
181
+ try {
182
+ agents = parseIntegrationAgents(opts.agents);
183
+ let result;
184
+ if (subcommand === 'status') result = { action: 'status', ...integrationStatus({ agents }) };
185
+ else if (subcommand === 'install') result = installIntegration({ agents, approved: opts.yes === true });
186
+ else if (subcommand === 'remove') result = removeIntegration({ agents, approved: opts.yes === true });
187
+ else if (subcommand === 'retire-legacy') result = retireLegacyOffload({ approved: opts.yes === true });
188
+ else {
189
+ console.error(integrateUsage());
190
+ return 2;
191
+ }
192
+ if (opts.json) console.log(JSON.stringify(result, null, 2));
193
+ else printIntegrationResult(result);
194
+ const ok = result.status?.ok ?? result.ok;
195
+ return ok === false && subcommand === 'status' ? 1 : 0;
196
+ } catch (error) {
197
+ console.error(`✗ ${error.message}`);
198
+ return 1;
199
+ }
200
+ }
201
+
202
+ function pathsFor(homeDir, agent) {
203
+ const layout = AGENT_LAYOUT[agent];
204
+ return {
205
+ skillPath: join(homeDir, ...layout.skill),
206
+ instructionsPath: join(homeDir, ...layout.instructions),
207
+ };
208
+ }
209
+
210
+ function installSkillLink(skillPath, skillSource) {
211
+ const current = skillLinkStatus(skillPath, skillSource);
212
+ if (current.status === 'installed') return { changed: false, ...current };
213
+ if (current.status === 'conflict') {
214
+ throw new Error(`refusing to replace non-Bullswarm skill path: ${skillPath}`);
215
+ }
216
+ mkdirSync(dirname(skillPath), { recursive: true });
217
+ symlinkSync(skillSource, skillPath, 'dir');
218
+ return { changed: true, status: 'installed', path: skillPath, target: skillSource };
219
+ }
220
+
221
+ function removeSkillLink(skillPath, skillSource) {
222
+ const current = skillLinkStatus(skillPath, skillSource);
223
+ if (current.status === 'missing') return { changed: false, ...current };
224
+ if (current.status === 'conflict') {
225
+ return { changed: false, ...current, reason: 'left conflict untouched' };
226
+ }
227
+ unlinkSync(skillPath);
228
+ return { changed: true, status: 'missing', path: skillPath, target: skillSource };
229
+ }
230
+
231
+ function skillLinkStatus(skillPath, skillSource) {
232
+ let stat;
233
+ try { stat = lstatSync(skillPath); } catch { return { status: 'missing', path: skillPath, target: null }; }
234
+ if (!stat.isSymbolicLink()) return { status: 'conflict', path: skillPath, target: null };
235
+ const rawTarget = readlinkSync(skillPath);
236
+ const target = resolve(dirname(skillPath), rawTarget);
237
+ return {
238
+ status: target === resolve(skillSource) ? 'installed' : 'conflict',
239
+ path: skillPath,
240
+ target,
241
+ };
242
+ }
243
+
244
+ function readOptional(filePath) {
245
+ try { return readFileSync(filePath, 'utf8'); } catch { return ''; }
246
+ }
247
+
248
+ function printIntegrationResult(result) {
249
+ if (result.action === 'retire-legacy') {
250
+ console.log(result.changed
251
+ ? `✓ archived retired offload skill at ${result.destination}`
252
+ : 'retired offload skill is not installed');
253
+ return;
254
+ }
255
+ const status = result.status ?? result;
256
+ for (const entry of status.agents ?? []) {
257
+ const skill = entry.skill.status === 'installed' ? 'skill ✓' : `skill ${entry.skill.status}`;
258
+ const awareness = entry.awareness ? 'awareness ✓' : 'awareness missing';
259
+ console.log(`${entry.agent.padEnd(8)} ${skill}; ${awareness}`);
260
+ }
261
+ if (status.legacyOffload?.detected) {
262
+ console.log(`⚠ retired Claude offload skill detected: ${status.legacyOffload.path}`);
263
+ console.log(` recoverably archive it with: ${status.legacyOffload.action}`);
264
+ }
265
+ }
package/src/setup.js CHANGED
@@ -5,9 +5,8 @@
5
5
  // entry. Burn rate starts EMPTY and is labeled "learning".
6
6
  // U2. The wizard suggests a routing table as an EDITABLE ARTIFACT, never a
7
7
  // questionnaire.
8
- // U3. CLAUDE.md / AGENTS.md integration is a DIFF with explicit approval
9
- // before any write, delimited by versioned bullswarm:begin/end
10
- // markers, idempotent on re-run.
8
+ // U3. Cross-agent skill/instruction integration requires explicit approval,
9
+ // uses versioned bullswarm:begin/end markers, and is idempotent.
11
10
  // U4. `bullswarm setup` on a configured machine reports state and repairs
12
11
  // broken connector files.
13
12
 
@@ -15,14 +14,16 @@ import { execFileSync } from 'node:child_process';
15
14
  import {
16
15
  existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, copyFileSync,
17
16
  } from 'node:fs';
18
- import { join, dirname } from 'node:path';
17
+ import { join } from 'node:path';
19
18
  import { fileURLToPath } from 'node:url';
20
19
  import { stdin as input } from 'node:process';
21
20
  import { loadState, saveState } from './lib/state.js';
21
+ import {
22
+ awarenessBlock, applyAwarenessBlock, awarenessBlockPresent,
23
+ installIntegration, retireLegacyOffload,
24
+ } from './integrate.js';
22
25
 
23
26
  const REPO_ROOT = fileURLToPath(new URL('..', import.meta.url));
24
- const MARKER_BEGIN = '<!-- bullswarm:begin v1 -->';
25
- const MARKER_END = '<!-- bullswarm:end -->';
26
27
 
27
28
  // --- prompting ------------------------------------------------------------
28
29
  // Sequential prompts that work identically on a TTY and with piped answers.
@@ -121,45 +122,15 @@ export function suggestRoutingTable(enabledPools) {
121
122
  // --- integration block ------------------------------------------------------------
122
123
 
123
124
  export function integrationBlock() {
124
- return `${MARKER_BEGIN}
125
- ## bullswarm offload policy
126
-
127
- When a task fits a bounded lane, prefer offloading it:
128
-
129
- bullswarm run --lane <analyze|build|chore> --add-dir <repo-dir> --task-file <file> --json
130
-
131
- Read the verdict JSON: ok:true -> use outFile; keepOnClaude:true -> do it in-session;
132
- ok:false -> the why field names the failed gate. Delegate output is INPUT you verify,
133
- never the answer. Final synthesis, architecture decisions, and live-context work stay
134
- with you. Run \`bullswarm health\` after every offload round.
135
- ${MARKER_END}`;
125
+ return awarenessBlock();
136
126
  }
137
127
 
138
128
  export function applyIntegrationBlock(filePath, { approved }) {
139
- if (!approved) return { changed: false, reason: 'not approved' };
140
- let existing = '';
141
- try {
142
- existing = readFileSync(filePath, 'utf8');
143
- } catch {
144
- /* new file */
145
- }
146
- const stripped = existing
147
- .replace(new RegExp(`${MARKER_BEGIN}[\\s\\S]*?${MARKER_END}\n?`), '')
148
- .trimEnd();
149
- const next = stripped
150
- ? `${stripped}\n\n${integrationBlock()}\n`
151
- : `${integrationBlock()}\n`;
152
- mkdirSync(dirname(filePath), { recursive: true });
153
- writeFileSync(filePath, next);
154
- return { changed: true };
129
+ return applyAwarenessBlock(filePath, { approved });
155
130
  }
156
131
 
157
132
  export function integrationBlockPresent(filePath) {
158
- try {
159
- return readFileSync(filePath, 'utf8').includes(MARKER_BEGIN);
160
- } catch {
161
- return false;
162
- }
133
+ return awarenessBlockPresent(filePath);
163
134
  }
164
135
 
165
136
  // --- repair ---------------------------------------------------------------
@@ -398,21 +369,25 @@ export async function runWizard(bullswarmDir, opts = {}) {
398
369
  console.log(' strategy autopilot: off (enable later with bullswarm strategy apply --yes)');
399
370
  }
400
371
 
401
- // 6. Integration blocks — diff + approval
402
- for (const [label, path] of [
403
- ['CLAUDE.md', join(process.env.HOME ?? '', '.claude', 'CLAUDE.md')],
404
- ['AGENTS.md', join(process.cwd(), 'AGENTS.md')],
405
- ]) {
406
- const present = integrationBlockPresent(path);
407
- const preview = present
408
- ? 'block already present (idempotent re-run)'
409
- : `will append to ${path}:\n\n${integrationBlock()}\n`;
410
- console.log(`\n${label}: ${preview}`);
411
- const ans = (
412
- await rl.question(`write ${label} integration block? [y/N] `)
413
- ).trim().toLowerCase();
414
- const result = applyIntegrationBlock(path, { approved: ans === 'y' && !present });
415
- console.log(result.changed ? ` wrote ${path}` : ` skipped ${label}`);
372
+ // 6. Cross-agent integration — one canonical skill plus concise global
373
+ // awareness rules. Nothing is written without this explicit answer.
374
+ const integrateAnswer = (
375
+ await rl.question('install Bullswarm skill for Codex, Claude, and Grok? [y/N] ')
376
+ ).trim().toLowerCase();
377
+ if (integrateAnswer === 'y' || integrateAnswer === 'yes') {
378
+ const integrated = installIntegration({ approved: true });
379
+ console.log(` agent integration: ${integrated.status.ok ? 'ready' : 'incomplete'}`);
380
+ if (integrated.status.legacyOffload.detected) {
381
+ const retireAnswer = (
382
+ await rl.question('archive the retired Claude offload skill? [y/N] ')
383
+ ).trim().toLowerCase();
384
+ if (retireAnswer === 'y' || retireAnswer === 'yes') {
385
+ const retired = retireLegacyOffload({ approved: true });
386
+ console.log(` retired offload: ${retired.changed ? `archived at ${retired.destination}` : retired.reason}`);
387
+ }
388
+ }
389
+ } else {
390
+ console.log(' agent integration: skipped (install later with bullswarm integrate install --yes)');
416
391
  }
417
392
 
418
393
  rl.close();