bullswarm 0.10.1 → 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 +1 -1
- package/CHANGELOG.md +18 -0
- package/README.md +22 -4
- package/package.json +1 -1
- package/skill/SKILL.md +15 -1
- package/src/cli.js +16 -3
- package/src/integrate.js +265 -0
- package/src/setup.js +29 -54
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 #
|
|
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,23 @@
|
|
|
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
|
+
|
|
3
21
|
## 0.10.1 — initiated-time workflow history search
|
|
4
22
|
|
|
5
23
|
- Added `workflow runs --since <time> --until <time>` filtering against the
|
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
|
-
|
|
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`.
|
|
132
|
-
|
|
133
|
-
`--max-
|
|
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
|
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: bullswarm
|
|
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:
|
|
@@ -306,6 +316,10 @@ You do NOT need to set up bullswarm. Every verb self-initializes:
|
|
|
306
316
|
bullswarm doctor --json
|
|
307
317
|
```
|
|
308
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
|
+
|
|
309
323
|
The output has a `checks[]` array with one entry per readiness concern
|
|
310
324
|
(config, connectors, meters, offload-capable) and a `nextActions[]` list
|
|
311
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
|
|
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
|
-
|
|
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
|
}
|
package/src/integrate.js
ADDED
|
@@ -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.
|
|
9
|
-
//
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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();
|