bullswarm 0.10.0 → 0.10.2

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 # 232 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,35 @@
1
1
  # bullswarm changelog
2
2
 
3
+ ## 0.10.2 — cross-agent skill integration
4
+
5
+ - Added explicit `bullswarm integrate status|install|remove` support for Codex,
6
+ Claude, and Grok. Installation registers one packaged `bullswarm` skill with
7
+ all selected agents and writes concise marker-delimited global awareness
8
+ rules; removal touches only Bullswarm-managed links and blocks.
9
+ - Added recoverable `integrate retire-legacy --yes` migration for the retired
10
+ Claude `offload` skill. Detection is read-only and retirement always moves the
11
+ old skill into `~/.claude/skills-archive/`.
12
+ - Added recursion-aware global guidance: a worker with `BULLSWARM_DEPTH` set
13
+ performs its assigned task directly instead of casually spawning another
14
+ swarm.
15
+ - Renamed the published agent skill from `bullswarm-setup` to `bullswarm` and
16
+ documented single-task, zero-graph goal, fixed-workflow, observation, and
17
+ integration paths together.
18
+ - Corrected README language so agent/time targets are advisory while graph
19
+ expansion limits remain hard safeguards.
20
+
21
+ ## 0.10.1 — initiated-time workflow history search
22
+
23
+ - Added `workflow runs --since <time> --until <time>` filtering against the
24
+ workflow's initiation timestamp (`startedAt`), with an inclusive lower bound
25
+ and exclusive upper bound.
26
+ - Added `--from`/`--to` and `--started-after`/`--started-before` aliases plus
27
+ ISO timestamps, local dates, calendar keywords, and relative durations such
28
+ as `7d`.
29
+ - Historical listing now falls back to `report.startedAt` when an older state
30
+ record lacks its initiation timestamp, and JSON output reports the normalized
31
+ range used for the audit.
32
+
3
33
  ## 0.10.0 — battle-tested advisory orchestration and agent activity
4
34
 
5
35
  - Made `maxAgents` and `maxWorkflowSeconds` advisory planning targets instead
package/README.md CHANGED
@@ -23,8 +23,24 @@ Every delegate output is judged by content before it counts.
23
23
 
24
24
  ```bash
25
25
  npm install -g bullswarm # or: node bin/bullswarm.js directly from a checkout
26
+ bullswarm integrate install --agents codex,claude,grok --yes
26
27
  ```
27
28
 
29
+ The integration command registers Bullswarm's packaged `bullswarm` skill with
30
+ Codex, Claude, and Grok and appends a concise, marker-delimited awareness rule
31
+ to each agent's global instructions. It is explicit, idempotent, and reversible:
32
+
33
+ ```bash
34
+ bullswarm integrate status --json
35
+ bullswarm integrate remove --agents codex,claude,grok --yes
36
+ ```
37
+
38
+ If the retired pre-Bullswarm Claude `offload` skill is detected, status reports
39
+ it without changing it. Archive it recoverably with
40
+ `bullswarm integrate retire-legacy --yes`. The awareness rule prevents workers
41
+ already launched by Bullswarm (`BULLSWARM_DEPTH` is set) from casually
42
+ re-delegating and creating recursive swarms.
43
+
28
44
  ## Quick start
29
45
 
30
46
  ```bash
@@ -42,6 +58,7 @@ bullswarm health # re-judge saved outputs; catch gate failures
42
58
  | Verb | Purpose |
43
59
  |---|---|
44
60
  | `setup` | Discover installed agent CLIs, show quota state, toggle pools, suggest a routing table, write config. Approval-gated, idempotent. |
61
+ | `integrate` | Register or remove the canonical Bullswarm skill and global awareness rules for Codex, Claude, and Grok. |
45
62
  | `run` | route → dispatch → watch → verify → one JSON verdict |
46
63
  | `health` | Re-judge saved outputs against their verdicts; surface verify-gate failures and quarantine clusters |
47
64
  | `pools` | Show each pool's meter state, pace position, quarantine status |
@@ -107,7 +124,7 @@ orchestrator by live quota surplus. The orchestrator observes durable evidence,
107
124
  and decides when another expansion or verification is necessary. Bullswarm
108
125
  validates the proposal, owns agent/process selection, routes workers, and calls
109
126
  the orchestrator again until completion, cancellation, failure, approval, or a
110
- budget limit. No initial phases, prompts, JSON schema, or agent choice are
127
+ hard graph-growth safeguard. No initial phases, prompts, JSON schema, or agent choice are
111
128
  required from the user.
112
129
 
113
130
  The detached response includes a short ID and exact observation commands:
@@ -128,9 +145,10 @@ bullswarm workflow goal --resume <shortId> --json
128
145
  ```
129
146
 
130
147
  `--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
148
+ leave selection on `auto`. `--max-agents` and `--max-workflow-seconds` are
149
+ advisory planning targets; hard graph-growth safeguards are adjusted with
150
+ `--max-expansion-rounds`, `--max-actions`, and `--max-items-per-expansion`.
151
+ Interactive setup also records a worktree-isolation
134
152
  preference (`agent-decides`, `off`, or `required`); Bullswarm communicates that
135
153
  policy to the orchestrator without imposing repository topology itself.
136
154
 
@@ -173,6 +191,9 @@ bullswarm workflow runs # ongoing only (default)
173
191
  bullswarm workflow runs --all # ongoing + historical
174
192
  bullswarm workflow runs --historical # only historical
175
193
  bullswarm workflow runs --name audit-code # filter by workflow
194
+ bullswarm workflow runs --all --since 7d # initiated in the last 7 days
195
+ bullswarm workflow runs --historical --since yesterday --until today
196
+ bullswarm workflow runs --all --from 2026-08-20 --to 2026-08-27
176
197
  bullswarm workflow runs --limit 20 # cap the result count
177
198
  bullswarm workflow runs show <shortId> # state + report + summary
178
199
  bullswarm runs show <shortId> # top-level shorthand
@@ -182,6 +203,14 @@ bullswarm workflow runs delete <shortId> --yes # remove the run dir
182
203
  bullswarm workflow run audit-code --resume <shortId>
183
204
  ```
184
205
 
206
+ Run-history time filters always compare when the workflow was initiated
207
+ (`startedAt`), never when it finished. `--since` is inclusive and `--until` is
208
+ exclusive; `--started-after`/`--from` and `--started-before`/`--to` are aliases.
209
+ Values accept ISO timestamps, local `YYYY-MM-DD` dates, `today`, `yesterday`,
210
+ `tomorrow`, `now`, or relative durations such as `30m`, `24h`, `7d`, and `2w`.
211
+ Time filters preserve the existing scope, so use `--all` or `--historical` when
212
+ auditing completed runs.
213
+
185
214
  ### Live workflow dashboard
186
215
 
187
216
  For ordinary observation, use the non-interactive watcher. It prints only when
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bullswarm",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
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,16 @@ 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
+ This skill is registered globally by:
14
+
15
+ ```bash
16
+ bullswarm integrate install --agents codex,claude,grok --yes
17
+ ```
18
+
19
+ If `BULLSWARM_DEPTH` is already set, you are a Bullswarm delegate. Complete the
20
+ assigned task directly; do not recursively invoke Bullswarm unless the task
21
+ explicitly requires another bounded delegation.
22
+
13
23
  ## When to reach for it
14
24
 
15
25
  You should consider bullswarm when **any** of the following apply:
@@ -166,10 +176,19 @@ Every run gets a 6-character shortId (Crockford-style alphabet, no
166
176
  bullswarm workflow runs # ongoing only (default)
167
177
  bullswarm workflow runs --all # ongoing + historical
168
178
  bullswarm workflow runs --name <workflow> # filter by workflow name
179
+ bullswarm workflow runs --all --since 7d # initiated in the last 7 days
180
+ bullswarm workflow runs --historical --since yesterday --until today
169
181
  bullswarm workflow runs show <shortId> # state + report + summary
170
182
  bullswarm workflow runs delete <shortId> --yes
171
183
  ```
172
184
 
185
+ Historical time ranges filter the workflow's initiation time (`startedAt`),
186
+ not completion time. `--since` is inclusive and `--until` is exclusive, with
187
+ `--from`/`--to` and `--started-after`/`--started-before` aliases. Bounds accept
188
+ ISO timestamps, local dates, today/yesterday/tomorrow/now, or durations such as
189
+ `7d`. Add `--all` or `--historical`; time filters do not silently change the
190
+ normal ongoing-only scope.
191
+
173
192
  Before authoring or choosing a workflow, agents can inspect the live execution
174
193
  fabric and the workflow document itself:
175
194
 
@@ -297,6 +316,10 @@ You do NOT need to set up bullswarm. Every verb self-initializes:
297
316
  bullswarm doctor --json
298
317
  ```
299
318
 
319
+ Self-initialization prepares routing and connectors; it does not silently edit
320
+ global agent instructions. Inspect or explicitly install agent awareness with
321
+ `bullswarm integrate status --json` and `bullswarm integrate install --yes`.
322
+
300
323
  The output has a `checks[]` array with one entry per readiness concern
301
324
  (config, connectors, meters, offload-capable) and a `nextActions[]` list
302
325
  of exact commands to fix anything missing.
package/src/cli.js CHANGED
@@ -16,6 +16,7 @@ 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';
19
20
 
20
21
  export function getBullswarmDir() {
21
22
  const h = process.env.BULLSWARM_HOME?.trim();
@@ -31,8 +32,10 @@ function parseArgs(argv) {
31
32
  const rest = [];
32
33
  for (let i = 0; i < argv.length; i++) {
33
34
  if (argv[i].startsWith('--')) {
34
- const key = argv[i].slice(2);
35
+ const eq = argv[i].indexOf('=');
36
+ const key = argv[i].slice(2, eq > 0 ? eq : undefined);
35
37
  if (key === 'json') args.json = true;
38
+ else if (eq > 0) args[key] = argv[i].slice(eq + 1);
36
39
  else if (i + 1 < argv.length && !argv[i + 1].startsWith('--')) args[key] = argv[++i];
37
40
  else args[key] = true;
38
41
  } else rest.push(argv[i]);
@@ -295,12 +298,20 @@ async function cmdSetup(opts) {
295
298
  const report = await refreshStrategy(getBullswarmDir());
296
299
  strategy = applyStrategyRecommendations(getBullswarmDir(), report);
297
300
  }
298
- if (opts.json) console.log(JSON.stringify({ ok: true, mode: 'auto', ...r, strategy }, null, 2));
301
+ let integration = null;
302
+ if (opts.yes && opts.integrate) {
303
+ integration = installIntegration({
304
+ agents: opts.agents,
305
+ approved: true,
306
+ });
307
+ }
308
+ if (opts.json) console.log(JSON.stringify({ ok: true, mode: 'auto', ...r, strategy, integration }, null, 2));
299
309
  else {
300
310
  console.log(`setup complete (${r.reason}): enabled ${r.enabledPools.join(', ')}`);
301
311
  if (r.repaired.length) console.log(`repaired connector files: ${r.repaired.join(', ')}`);
302
312
  console.log(`model strategy: ${r.strategyCommand} (discovers models and refreshes tier suggestions)`);
303
313
  if (strategy) console.log(`strategy autopilot: applied ${Object.keys(strategy.applied).join(', ')} tiers; refresh every ${strategy.policy.refreshHours}h`);
314
+ if (integration) console.log('agent integration: installed (inspect with bullswarm integrate status)');
304
315
  }
305
316
  return 0;
306
317
  }
@@ -413,6 +424,8 @@ export async function main(argv) {
413
424
  return cmdWorkflow(['runs', ...rest]);
414
425
  case 'strategy':
415
426
  return cmdStrategy(rest, { bullswarmDir: getBullswarmDir() });
427
+ case 'integrate':
428
+ return cmdIntegrate(opts);
416
429
  case 'version':
417
430
  case '--version':
418
431
  console.log(getVersion());
@@ -421,7 +434,7 @@ export async function main(argv) {
421
434
  return cmdRelease(opts);
422
435
  default:
423
436
  console.error(
424
- `unknown verb "${verb}". try: setup | run | health | pools | strategy | doctor | workflow | runs | version | release`,
437
+ `unknown verb "${verb}". try: setup | integrate | run | health | pools | strategy | doctor | workflow | runs | version | release`,
425
438
  );
426
439
  return 2;
427
440
  }
@@ -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 blocksdiff + 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 integrationone 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();
@@ -4,6 +4,8 @@
4
4
  // bullswarm workflow runs --all # list ongoing + historical
5
5
  // bullswarm workflow runs --historical # only historical
6
6
  // bullswarm workflow runs --name <workflow> # filter by workflow name
7
+ // bullswarm workflow runs --all --since 7d # initiated in the last 7 days
8
+ // bullswarm workflow runs --all --since yesterday --until today
7
9
  // bullswarm workflow runs --limit N # cap result count
8
10
  // bullswarm workflow runs show <id> # dump state + report
9
11
  // bullswarm workflow runs delete <id> --yes # remove the run directory
@@ -32,29 +34,60 @@ export function cmdRuns(args) {
32
34
  export function runsUsage() {
33
35
  return `usage:
34
36
  bullswarm workflow runs # list ongoing runs
35
- bullswarm workflow runs [--all | --historical] [--name <workflow>] [--limit N] [--json]
37
+ bullswarm workflow runs [--all | --historical] [--name <workflow>]
38
+ [--since <time>] [--until <time>] [--limit N] [--json]
36
39
  bullswarm workflow runs show <id> # <id> = shortId (6 chars) or full runId
37
- bullswarm workflow runs delete <id> --yes # remove the run directory`;
40
+ bullswarm workflow runs delete <id> --yes # remove the run directory
41
+
42
+ Time filters compare the workflow initiation timestamp (startedAt):
43
+ --since / --started-after / --from inclusive lower bound
44
+ --until / --started-before / --to exclusive upper bound
45
+ Values accept ISO timestamps, local dates, today/yesterday/tomorrow/now,
46
+ or relative durations such as 30m, 24h, 7d, and 2w.`;
38
47
  }
39
48
 
40
49
  function parseRunsFlags(argv) {
41
50
  const out = { _positional: [] };
51
+ const valueFlags = new Map([
52
+ ['name', 'name'],
53
+ ['limit', 'limit'],
54
+ ['since', 'since'],
55
+ ['started-after', 'since'],
56
+ ['from', 'since'],
57
+ ['until', 'until'],
58
+ ['started-before', 'until'],
59
+ ['to', 'until'],
60
+ ]);
42
61
  for (let i = 0; i < argv.length; i++) {
43
62
  const a = argv[i];
44
63
  if (a === '--json') out.json = true;
45
64
  else if (a === '--all') out.all = true;
46
65
  else if (a === '--historical') out.historical = true;
47
66
  else if (a === '--yes' || a === '-y') out.yes = true;
48
- else if (a === '--name') out.name = argv[++i];
49
- else if (a === '--limit') out.limit = Number(argv[++i]);
50
67
  else if (a === '--force') out.force = true;
51
- else if (a.startsWith('--')) out[a.slice(2)] = true;
68
+ else if (a.startsWith('--')) {
69
+ const eq = a.indexOf('=');
70
+ const key = a.slice(2, eq > 0 ? eq : undefined);
71
+ const target = valueFlags.get(key);
72
+ if (target) {
73
+ const value = eq > 0 ? a.slice(eq + 1) : argv[++i];
74
+ out[target] = target === 'limit' ? Number(value) : value;
75
+ } else {
76
+ out[key] = eq > 0 ? a.slice(eq + 1) : true;
77
+ }
78
+ }
52
79
  else out._positional.push(a);
53
80
  }
54
81
  return out;
55
82
  }
56
83
 
57
84
  function runsList(opts) {
85
+ let initiatedRange;
86
+ try {
87
+ initiatedRange = resolveInitiatedRange(opts);
88
+ } catch (error) {
89
+ return err(error.message);
90
+ }
58
91
  const all = listRuns(BULLSWARM_DIR());
59
92
  let filtered = all;
60
93
  if (opts.name) filtered = filtered.filter((r) => r.state?.workflow === opts.name);
@@ -64,10 +97,22 @@ function runsList(opts) {
64
97
  } else if (opts.historical) {
65
98
  filtered = filtered.filter((r) => !r.ongoing);
66
99
  }
100
+ if (initiatedRange.sinceMs != null) {
101
+ filtered = filtered.filter((r) => {
102
+ const startedMs = Date.parse(runStartedAt(r) ?? '');
103
+ return Number.isFinite(startedMs) && startedMs >= initiatedRange.sinceMs;
104
+ });
105
+ }
106
+ if (initiatedRange.untilMs != null) {
107
+ filtered = filtered.filter((r) => {
108
+ const startedMs = Date.parse(runStartedAt(r) ?? '');
109
+ return Number.isFinite(startedMs) && startedMs < initiatedRange.untilMs;
110
+ });
111
+ }
67
112
  // Newest first.
68
113
  filtered.sort((a, b) => {
69
- const ta = a.state?.startedAt ?? '';
70
- const tb = b.state?.startedAt ?? '';
114
+ const ta = runStartedAt(a) ?? '';
115
+ const tb = runStartedAt(b) ?? '';
71
116
  return tb.localeCompare(ta);
72
117
  });
73
118
  if (opts.limit && opts.limit > 0) filtered = filtered.slice(0, opts.limit);
@@ -77,6 +122,15 @@ function runsList(opts) {
77
122
  ongoing: opts.all || !opts.historical,
78
123
  historical: opts.all || opts.historical,
79
124
  name: opts.name ?? null,
125
+ initiatedRange: {
126
+ field: 'startedAt',
127
+ sinceInclusive: initiatedRange.sinceMs == null
128
+ ? null
129
+ : new Date(initiatedRange.sinceMs).toISOString(),
130
+ untilExclusive: initiatedRange.untilMs == null
131
+ ? null
132
+ : new Date(initiatedRange.untilMs).toISOString(),
133
+ },
80
134
  count: filtered.length,
81
135
  runs: filtered.map(summarize),
82
136
  }, opts);
@@ -96,7 +150,7 @@ function runsList(opts) {
96
150
  for (const r of filtered) {
97
151
  const wf = r.state?.workflow ?? '?';
98
152
  const status = r.state?.status ?? (r.ongoing ? 'running' : 'unknown');
99
- const age = humanAge(r.state?.startedAt);
153
+ const age = humanAge(runStartedAt(r));
100
154
  const phases = `${r.state?.steps?.filter((s) => s.ok).length ?? 0}/${r.state?.steps?.length ?? 0}`;
101
155
  console.log(
102
156
  `${r.ongoing ? '●' : '○'} ${(r.shortId ?? '------').padEnd(8)} ` +
@@ -167,8 +221,8 @@ function summarize(r) {
167
221
  shortId: r.shortId,
168
222
  workflow: r.state?.workflow ?? null,
169
223
  status: r.state?.status ?? null,
170
- startedAt: r.state?.startedAt ?? null,
171
- finishedAt: r.state?.finishedAt ?? null,
224
+ startedAt: runStartedAt(r),
225
+ finishedAt: r.state?.finishedAt ?? r.report?.finishedAt ?? null,
172
226
  ongoing: r.ongoing,
173
227
  stepsOk: r.state?.steps?.filter((s) => s.ok).length ?? 0,
174
228
  stepsFailed: r.state?.steps?.filter((s) => s.ok === false).length ?? 0,
@@ -177,6 +231,58 @@ function summarize(r) {
177
231
  };
178
232
  }
179
233
 
234
+ function runStartedAt(run) {
235
+ return run.state?.startedAt ?? run.report?.startedAt ?? null;
236
+ }
237
+
238
+ function resolveInitiatedRange(opts, nowMs = Date.now()) {
239
+ const sinceMs = opts.since == null ? null : parseTimeBound(opts.since, nowMs, '--since');
240
+ const untilMs = opts.until == null ? null : parseTimeBound(opts.until, nowMs, '--until');
241
+ if (sinceMs != null && untilMs != null && sinceMs >= untilMs) {
242
+ throw new Error('initiated-time range is empty: --since must be earlier than --until');
243
+ }
244
+ return { sinceMs, untilMs };
245
+ }
246
+
247
+ function parseTimeBound(value, nowMs, flag) {
248
+ const raw = String(value ?? '').trim();
249
+ if (!raw) throw new Error(`${flag} requires a time value`);
250
+ const normalized = raw.toLowerCase();
251
+
252
+ if (normalized === 'now') return nowMs;
253
+ if (['yesterday', 'today', 'tomorrow'].includes(normalized)) {
254
+ const date = new Date(nowMs);
255
+ date.setHours(0, 0, 0, 0);
256
+ if (normalized === 'yesterday') date.setDate(date.getDate() - 1);
257
+ if (normalized === 'tomorrow') date.setDate(date.getDate() + 1);
258
+ return date.getTime();
259
+ }
260
+
261
+ const duration = normalized.match(/^(\d+(?:\.\d+)?)(m|h|d|w)$/);
262
+ if (duration) {
263
+ const unitMs = { m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000 }[duration[2]];
264
+ return nowMs - Number(duration[1]) * unitMs;
265
+ }
266
+
267
+ const localDate = normalized.match(/^(\d{4})-(\d{2})-(\d{2})$/);
268
+ if (localDate) {
269
+ const [, year, month, day] = localDate;
270
+ const date = new Date(Number(year), Number(month) - 1, Number(day));
271
+ if (date.getFullYear() !== Number(year)
272
+ || date.getMonth() !== Number(month) - 1
273
+ || date.getDate() !== Number(day)) {
274
+ throw new Error(`${flag} has an invalid calendar date: "${raw}"`);
275
+ }
276
+ return date.getTime();
277
+ }
278
+
279
+ const parsed = Date.parse(raw);
280
+ if (!Number.isFinite(parsed)) {
281
+ throw new Error(`${flag} has an invalid time: "${raw}"`);
282
+ }
283
+ return parsed;
284
+ }
285
+
180
286
  function humanAge(iso) {
181
287
  if (!iso) return '—';
182
288
  const ms = Date.now() - Date.parse(iso);