bullswarm 0.10.7 → 0.10.8
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/CHANGELOG.md +15 -0
- package/README.md +21 -5
- package/connectors/claude-code.json +1 -0
- package/package.json +1 -1
- package/skill/SKILL.md +18 -3
- package/src/cli.js +28 -9
- package/src/help.js +2 -2
- package/src/lib/release.js +10 -9
- package/src/lib/watch.js +1 -1
- package/src/setup.js +8 -0
- package/src/strategy-cli.js +2 -1
- package/src/workflow/cli.js +23 -1
- package/src/workflow/draft-cli.js +18 -16
- package/src/workflow/goal.js +1 -0
- package/src/workflow/runner.js +7 -0
- package/src/workflow/runs-cli.js +6 -6
- package/src/workflow/watch-cli.js +85 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# bullswarm changelog
|
|
2
2
|
|
|
3
|
+
## 0.10.8 — quieter monitoring and resilient orchestration
|
|
4
|
+
|
|
5
|
+
- Made `workflow watch` aggregate low-level activity into compact interval
|
|
6
|
+
heartbeats by default, with event/action deltas, quiet duration, prompt
|
|
7
|
+
semantic transitions, terminal result handoff, and `--verbose` drill-down.
|
|
8
|
+
- Implemented the documented top-level `run --prompt` form and standardized
|
|
9
|
+
usage errors as exit 2 across run, workflow drafts/runs, and strategy paths.
|
|
10
|
+
- Added explicit goal-resume orchestrator pin/unpin behavior and strengthened
|
|
11
|
+
the orchestrator as a control-plane-only decision thread.
|
|
12
|
+
- Recognize Claude's exact `Failed to authenticate` response as an auth failure
|
|
13
|
+
and migrate the connector signature additively without replacing local
|
|
14
|
+
connector customization.
|
|
15
|
+
- Expanded non-network CLI, health, release, watch, resume, auth, help, and
|
|
16
|
+
documentation coverage. The full suite now contains 271 tests.
|
|
17
|
+
|
|
3
18
|
## 0.10.7 — clearer orchestration overview
|
|
4
19
|
|
|
5
20
|
- Replaced the orchestrator trace dump with a summary-first view showing what
|
package/README.md
CHANGED
|
@@ -67,6 +67,7 @@ bullswarm setup # re-run or repair
|
|
|
67
67
|
bullswarm pools # meter state, pace position, quarantine status
|
|
68
68
|
bullswarm strategy refresh --apply --yes # approve capability-aware tier autopilot
|
|
69
69
|
bullswarm run --lane analyze --add-dir ~/some-repo --task-file /tmp/t.md --json
|
|
70
|
+
bullswarm run --lane analyze --add-dir ~/some-repo --prompt "Inspect the parser" --json
|
|
70
71
|
bullswarm workflow goal "Fix the failing tests and verify the change" --cwd ~/some-repo
|
|
71
72
|
bullswarm health # re-judge saved outputs; catch gate failures
|
|
72
73
|
```
|
|
@@ -84,6 +85,17 @@ bullswarm health # re-judge saved outputs; catch gate failures
|
|
|
84
85
|
| `doctor` | Machine-readable readiness report; self-heals on first call |
|
|
85
86
|
| `workflow` | Start an autonomous goal, or run / validate / draft / inspect explicit workflows and their live instances. |
|
|
86
87
|
|
|
88
|
+
Discover and validate workflow definitions without executing them:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
bullswarm workflow list
|
|
92
|
+
bullswarm workflow list --json
|
|
93
|
+
bullswarm workflow validate workflows/my-workflow.json
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`workflow goal --request <path>` and `--run-id <id>` are internal detached-runner
|
|
97
|
+
resume plumbing. Normal callers should provide a goal or use `--resume <shortId|runId>`.
|
|
98
|
+
|
|
87
99
|
## Model strategy and invocation telemetry
|
|
88
100
|
|
|
89
101
|
Bullswarm can inventory the models exposed by installed agent CLIs and combine
|
|
@@ -260,16 +272,20 @@ auditing completed runs.
|
|
|
260
272
|
|
|
261
273
|
### Live workflow dashboard
|
|
262
274
|
|
|
263
|
-
For ordinary observation, use the non-interactive watcher.
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
275
|
+
For ordinary observation, use the non-interactive watcher. Human output is one
|
|
276
|
+
compact aggregate line per semantic change and a 60-second heartbeat while
|
|
277
|
+
otherwise quiet. Each line reports status/location, events and agent actions
|
|
278
|
+
captured since the preceding sample, and quiet duration. It does not repeat
|
|
279
|
+
command or response excerpts. Use `--verbose` for the detailed per-agent and
|
|
280
|
+
last-action view. Compact terminal output reports the overall attempt count and
|
|
281
|
+
elapsed time; `--verbose` includes every attempt's agent/model, outcome, and
|
|
282
|
+
tokens. This keeps agent monitoring cheap while retaining a drill-down path.
|
|
268
283
|
|
|
269
284
|
```bash
|
|
270
285
|
bullswarm workflow watch <shortId>
|
|
271
286
|
bullswarm workflow watch <shortId> --jsonl # automation-friendly stream
|
|
272
287
|
bullswarm workflow watch <shortId> --once # one current/terminal snapshot
|
|
288
|
+
bullswarm workflow watch <shortId> --verbose # detailed agent/action view
|
|
273
289
|
```
|
|
274
290
|
|
|
275
291
|
`workflow tui` is the interactive, Claude-style `/workflows` view. For an
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -59,6 +59,10 @@ bullswarm run --lane <analyze|build|chore> \
|
|
|
59
59
|
--add-dir <abs/path/to/repo> \
|
|
60
60
|
--task-file <abs/path/to/task.md> \
|
|
61
61
|
--json
|
|
62
|
+
|
|
63
|
+
# Equivalent explicit prompt form (also accepts trailing task text)
|
|
64
|
+
bullswarm run --lane analyze --add-dir <abs/path/to/repo> \
|
|
65
|
+
--prompt "Inspect and verify the parser" --json
|
|
62
66
|
```
|
|
63
67
|
|
|
64
68
|
The verdict shape:
|
|
@@ -202,8 +206,15 @@ bullswarm workflow runs --historical --since yesterday --until today
|
|
|
202
206
|
bullswarm workflow runs show <shortId> # state + report + summary
|
|
203
207
|
bullswarm workflow runs result <shortId> --json # stable delivery for the caller
|
|
204
208
|
bullswarm workflow runs delete <shortId> --yes
|
|
209
|
+
|
|
210
|
+
bullswarm workflow list
|
|
211
|
+
bullswarm workflow validate workflows/demo.json
|
|
205
212
|
```
|
|
206
213
|
|
|
214
|
+
`workflow goal --request <path>` and `--run-id <id>` are internal detached-runner
|
|
215
|
+
plumbing. Ordinary callers should provide a goal or resume with
|
|
216
|
+
`--resume <shortId|runId>`.
|
|
217
|
+
|
|
207
218
|
Historical time ranges filter the workflow's initiation time (`startedAt`),
|
|
208
219
|
not completion time. `--since` is inclusive and `--until` is exclusive, with
|
|
209
220
|
`--from`/`--to` and `--started-after`/`--started-before` aliases. Bounds accept
|
|
@@ -279,9 +290,13 @@ reason, and child-process termination evidence. `workflow tui` displays the
|
|
|
279
290
|
same information interactively. `workflow events` supports replay after a
|
|
280
291
|
monotonic sequence cursor.
|
|
281
292
|
|
|
282
|
-
Prefer `workflow watch` for ordinary monitoring;
|
|
283
|
-
|
|
284
|
-
|
|
293
|
+
Prefer `workflow watch` for ordinary monitoring; default human text is a compact
|
|
294
|
+
aggregate heartbeat with status/location, interval event and agent-action counts,
|
|
295
|
+
and quiet duration. It does not repeat command or response excerpts. Add
|
|
296
|
+
`--verbose` for the detailed agent and last-action view. `--jsonl` keeps the
|
|
297
|
+
machine-readable snapshot stream. Compact terminal text includes overall timing
|
|
298
|
+
and the exact `workflow runs result <shortId> --json` next command; `--verbose`
|
|
299
|
+
adds per-attempt timing and token details. `workflow steer` is an optional durable queue for autonomous workflows:
|
|
285
300
|
it never changes the active worker and is delivered only to the next
|
|
286
301
|
not-yet-started decision gate. Steering cannot expand the original authority,
|
|
287
302
|
weaken verification, or bypass proposal validation.
|
package/src/cli.js
CHANGED
|
@@ -96,6 +96,34 @@ async function cmdRun(opts) {
|
|
|
96
96
|
}
|
|
97
97
|
const targetDir = resolve(opts['add-dir'] ?? process.cwd());
|
|
98
98
|
|
|
99
|
+
// Validate task input before routing so malformed invocations never fall
|
|
100
|
+
// through to the caller-session path.
|
|
101
|
+
if (opts['task-file'] === true || opts.prompt === true ||
|
|
102
|
+
(opts['task-file'] != null && typeof opts['task-file'] !== 'string') ||
|
|
103
|
+
(opts.prompt != null && typeof opts.prompt !== 'string')) {
|
|
104
|
+
console.error('usage: --prompt and --task-file require a value');
|
|
105
|
+
return 2;
|
|
106
|
+
}
|
|
107
|
+
if (opts['task-file'] && opts.prompt != null) {
|
|
108
|
+
console.error('usage: choose one of --prompt, --task-file, or trailing task text');
|
|
109
|
+
return 2;
|
|
110
|
+
}
|
|
111
|
+
if (opts['task-file'] && opts.rest.length) {
|
|
112
|
+
console.error('usage: choose one of --task-file or trailing task text');
|
|
113
|
+
return 2;
|
|
114
|
+
}
|
|
115
|
+
if (opts.prompt != null && opts.rest.length) {
|
|
116
|
+
console.error('usage: choose one of --prompt or trailing task text');
|
|
117
|
+
return 2;
|
|
118
|
+
}
|
|
119
|
+
const taskText = opts['task-file']
|
|
120
|
+
? readFileSync(opts['task-file'], 'utf8')
|
|
121
|
+
: opts.prompt ?? opts.rest.join(' ');
|
|
122
|
+
if (!taskText.trim()) {
|
|
123
|
+
console.error('empty task: pass --task-file, --prompt, or the task as arguments');
|
|
124
|
+
return 2;
|
|
125
|
+
}
|
|
126
|
+
|
|
99
127
|
// Recursion guard FIRST — core-owned, env handshake.
|
|
100
128
|
let state = loadState(getBullswarmDir());
|
|
101
129
|
try {
|
|
@@ -166,15 +194,6 @@ async function cmdRun(opts) {
|
|
|
166
194
|
subscription: poolView.subscription ?? connector.subscription ?? null,
|
|
167
195
|
};
|
|
168
196
|
|
|
169
|
-
// Task text: --task-file content or stdin string.
|
|
170
|
-
const taskText = opts['task-file']
|
|
171
|
-
? readFileSync(opts['task-file'], 'utf8')
|
|
172
|
-
: opts.rest.join(' ');
|
|
173
|
-
if (!taskText.trim()) {
|
|
174
|
-
console.error('empty task: pass --task-file or the task as arguments');
|
|
175
|
-
return 2;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
197
|
const stamp = `${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
179
198
|
const runDir = join(getBullswarmDir(), 'runs');
|
|
180
199
|
mkdirSync(runDir, { recursive: true });
|
package/src/help.js
CHANGED
|
@@ -35,7 +35,7 @@ const HELP = {
|
|
|
35
35
|
'retire-legacy': { _text: leaf('bullswarm integrate retire-legacy --yes [--json]',
|
|
36
36
|
'Recoverably archive the retired Claude offload skill.') },
|
|
37
37
|
},
|
|
38
|
-
run: { _text: leaf('bullswarm run --lane <analyze|build|chore> --add-dir <dir> (--task-file <file> | --prompt <text>) [options]',
|
|
38
|
+
run: { _text: leaf('bullswarm run --lane <analyze|build|chore> --add-dir <dir> (--task-file <file> | --prompt <text> | <task text...>) [options]',
|
|
39
39
|
'Options: --effort <high|medium|low>, --timeout <seconds>, --json. Routes one task and verifies its content.') },
|
|
40
40
|
health: { _text: leaf('bullswarm health [--json]', 'Re-judge saved outputs and report failed gates or quarantine clusters.') },
|
|
41
41
|
pools: { _text: leaf('bullswarm pools [--force] [--json]', 'Show live meter, quota-surplus, burst-gate, and quarantine state.') },
|
|
@@ -100,7 +100,7 @@ Run bullswarm workflow <command> --help for complete command options.`),
|
|
|
100
100
|
inspect: { _text: leaf('bullswarm workflow inspect <file-or-name>') },
|
|
101
101
|
tui: { _text: leaf('bullswarm workflow tui [<runId>] [--json] [--all] [--show <runId>] [--cancel <runId>]',
|
|
102
102
|
'Interactive mode opens a full-screen phase → agent → step/detail browser. Up/down selects, Enter drills in, Esc goes back, q detaches without stopping work, and c requests explicit cancellation confirmation.') },
|
|
103
|
-
watch: { _text: leaf('bullswarm workflow watch <runId> [--interval <seconds>] [--heartbeat <seconds>] [--jsonl] [--once]') },
|
|
103
|
+
watch: { _text: leaf('bullswarm workflow watch <runId> [--interval <seconds>] [--heartbeat <seconds>] [--jsonl] [--once] [--verbose]', 'Human output is compact by default; --verbose preserves per-agent action details.') },
|
|
104
104
|
events: { _text: leaf('bullswarm workflow events <runId> [--after <sequence>] [--json]') },
|
|
105
105
|
steer: { _text: leaf('bullswarm workflow steer <runId> --message <guidance> [--json]') },
|
|
106
106
|
action: {
|
package/src/lib/release.js
CHANGED
|
@@ -25,17 +25,18 @@ export function bumpVersion(version, kind) {
|
|
|
25
25
|
return `${maj}.${min}.${pat}`;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
function git(args) {
|
|
29
|
-
return execSync(`git ${args}`, { cwd:
|
|
28
|
+
function git(args, repoRoot = REPO_ROOT) {
|
|
29
|
+
return execSync(`git ${args}`, { cwd: repoRoot, encoding: 'utf8' }).trim();
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
export function release(kind, { dryRun = false } = {}) {
|
|
33
|
-
const
|
|
32
|
+
export function release(kind, { dryRun = false, repoRoot = REPO_ROOT } = {}) {
|
|
33
|
+
const pkgPath = repoRoot === REPO_ROOT ? PKG_PATH : join(repoRoot, 'package.json');
|
|
34
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
34
35
|
const prev = pkg.version;
|
|
35
36
|
const next = bumpVersion(prev, kind);
|
|
36
37
|
|
|
37
38
|
// Dirty-tree guard: a release commit must contain exactly the version change.
|
|
38
|
-
const status = git('status --porcelain');
|
|
39
|
+
const status = git('status --porcelain', repoRoot);
|
|
39
40
|
if (status.trim()) {
|
|
40
41
|
throw new Error(
|
|
41
42
|
`working tree is dirty — commit first before releasing:\n${status}`,
|
|
@@ -47,9 +48,9 @@ export function release(kind, { dryRun = false } = {}) {
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
pkg.version = next;
|
|
50
|
-
writeFileSync(
|
|
51
|
-
git('add package.json');
|
|
52
|
-
git(`commit -m "release v${next}"
|
|
53
|
-
git(`tag -a v${next} -m "v${next}"
|
|
51
|
+
writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
52
|
+
git('add package.json', repoRoot);
|
|
53
|
+
git(`commit -m "release v${next}"`, repoRoot);
|
|
54
|
+
git(`tag -a v${next} -m "v${next}"`, repoRoot);
|
|
54
55
|
return { from: prev, to: next, tag: `v${next}`, dryRun: false };
|
|
55
56
|
}
|
package/src/lib/watch.js
CHANGED
|
@@ -223,7 +223,7 @@ function matchLikelyAuthFailure(connector, text) {
|
|
|
223
223
|
const line = lower.slice(lineStart, lineEnd < 0 ? lower.length : lineEnd).trim();
|
|
224
224
|
// A semantic result may legitimately discuss auth handling. Require the
|
|
225
225
|
// matched line to look like a provider failure instead of source/report text.
|
|
226
|
-
const errorShaped = /^(?:error|fatal)(?:\b|:)|^(?:authentication failed|not authenticated|invalid api key|rate limit(?:ed| exceeded)?|cmd login)(?:[.!:]|$)|\b(?:http\s*(?:401|403|429)|status\s*(?:401|403|429)|login required|please login|access denied|quota exceeded)\b/i.test(line);
|
|
226
|
+
const errorShaped = /^(?:error|fatal)(?:\b|:)|^(?:authentication failed|failed to authenticate|not authenticated|invalid api key|rate limit(?:ed| exceeded)?|cmd login)(?:[.!:]|$)|\b(?:http\s*(?:401|403|429)|status\s*(?:401|403|429)|login required|please login|access denied|quota exceeded)\b/i.test(line);
|
|
227
227
|
return errorShaped ? hit : null;
|
|
228
228
|
}
|
|
229
229
|
|
package/src/setup.js
CHANGED
|
@@ -184,6 +184,14 @@ export function upgradeConnectorMetadata(bullswarmDir) {
|
|
|
184
184
|
changed = true;
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
|
+
if (Array.isArray(packaged.authSignatures)) {
|
|
188
|
+
const existing = Array.isArray(installed.authSignatures) ? installed.authSignatures : [];
|
|
189
|
+
const merged = [...new Set([...existing, ...packaged.authSignatures])];
|
|
190
|
+
if (JSON.stringify(merged) !== JSON.stringify(existing)) {
|
|
191
|
+
installed.authSignatures = merged;
|
|
192
|
+
changed = true;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
187
195
|
if (installed.model == null && packaged.model != null) {
|
|
188
196
|
installed.model = packaged.model;
|
|
189
197
|
changed = true;
|
package/src/strategy-cli.js
CHANGED
|
@@ -262,6 +262,7 @@ export async function cmdStrategy(args, { bullswarmDir }) {
|
|
|
262
262
|
throw new Error(strategyUsage());
|
|
263
263
|
} catch (err) {
|
|
264
264
|
console.error(`✗ ${err.message}`);
|
|
265
|
-
|
|
265
|
+
const usage = /^(usage:|missing |assignment needs |--apply changes|strategy (?:apply|auto off) changes|refresh-hours must be|.* must be a non-negative number|unknown phase|unknown command|unknown pool)/i.test(err.message);
|
|
266
|
+
return usage ? 2 : 1;
|
|
266
267
|
}
|
|
267
268
|
}
|
package/src/workflow/cli.js
CHANGED
|
@@ -310,6 +310,26 @@ export function shouldAutoWatchGoal(opts) {
|
|
|
310
310
|
opts.json !== true && opts.resume == null && opts.request == null;
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
+
export function applyResumeOrchestratorOverride(doc, requested) {
|
|
314
|
+
if (!requested) return doc;
|
|
315
|
+
const pool = requested === 'auto' ? null : requested;
|
|
316
|
+
doc.intent ??= {};
|
|
317
|
+
doc.orchestration ??= {};
|
|
318
|
+
doc.intent.requestedOrchestrator = pool ?? 'auto';
|
|
319
|
+
doc.orchestration.requestedPool = pool;
|
|
320
|
+
doc.orchestration.selection = pool
|
|
321
|
+
? 'user-pinned-for-testing'
|
|
322
|
+
: 'capability-strategy-and-quota';
|
|
323
|
+
for (const phase of doc.phases ?? []) {
|
|
324
|
+
for (const step of phase.steps ?? []) {
|
|
325
|
+
if (step.type !== 'decide') continue;
|
|
326
|
+
if (pool) step.pool = pool;
|
|
327
|
+
else delete step.pool;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return doc;
|
|
331
|
+
}
|
|
332
|
+
|
|
313
333
|
async function wfGoal(opts) {
|
|
314
334
|
if (opts.help) {
|
|
315
335
|
console.log(goalUsage());
|
|
@@ -340,6 +360,7 @@ async function wfGoal(opts) {
|
|
|
340
360
|
console.error(`✗ cannot load durable workflow for ${resumeRunId}: ${err.message}`);
|
|
341
361
|
return 1;
|
|
342
362
|
}
|
|
363
|
+
applyResumeOrchestratorOverride(doc, opts.orchestrator);
|
|
343
364
|
} else if (opts.request) {
|
|
344
365
|
try {
|
|
345
366
|
const request = JSON.parse(readFileSync(resolve(opts.request), 'utf8'));
|
|
@@ -488,7 +509,7 @@ function wfEvents(opts) {
|
|
|
488
509
|
async function wfWatch(opts) {
|
|
489
510
|
const token = opts.rest[0];
|
|
490
511
|
if (!token) {
|
|
491
|
-
console.error('usage: bullswarm workflow watch <runId> [--interval <seconds>] [--heartbeat <seconds>] [--jsonl] [--once]');
|
|
512
|
+
console.error('usage: bullswarm workflow watch <runId> [--interval <seconds>] [--heartbeat <seconds>] [--jsonl] [--once] [--verbose]');
|
|
492
513
|
return 2;
|
|
493
514
|
}
|
|
494
515
|
const intervalSec = Number(opts.interval ?? 2);
|
|
@@ -504,6 +525,7 @@ async function wfWatch(opts) {
|
|
|
504
525
|
heartbeatMs: heartbeatSec * 1000,
|
|
505
526
|
once: opts.once === true,
|
|
506
527
|
jsonl: opts.jsonl === true,
|
|
528
|
+
verbose: opts.verbose === true,
|
|
507
529
|
});
|
|
508
530
|
} catch (err) {
|
|
509
531
|
console.error(`✗ ${err.message}`);
|
|
@@ -107,7 +107,9 @@ function jsonOut(obj, opts) {
|
|
|
107
107
|
if (opts.json) console.log(JSON.stringify(obj, null, 2));
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
function err(msg) { console.error(msg); return
|
|
110
|
+
function err(msg, code = 1) { console.error(msg); return code; }
|
|
111
|
+
|
|
112
|
+
function usageErr(msg) { return err(msg, 2); }
|
|
111
113
|
|
|
112
114
|
// --- subcommand dispatch ---------------------------------------------------
|
|
113
115
|
|
|
@@ -173,7 +175,7 @@ async function livePoolNames() {
|
|
|
173
175
|
|
|
174
176
|
function draftCreate(opts) {
|
|
175
177
|
const [name] = opts._positional;
|
|
176
|
-
if (!name) return
|
|
178
|
+
if (!name) return usageErr('usage: bullswarm workflow draft create <name>');
|
|
177
179
|
if (draftExists(BULLSWARM_DIR(), name)) {
|
|
178
180
|
return err(`draft "${name}" already exists (delete it first or use 'show')`);
|
|
179
181
|
}
|
|
@@ -206,7 +208,7 @@ function draftCreate(opts) {
|
|
|
206
208
|
|
|
207
209
|
function draftShow(opts) {
|
|
208
210
|
const [name] = opts._positional;
|
|
209
|
-
if (!name) return
|
|
211
|
+
if (!name) return usageErr('usage: bullswarm workflow draft show <name>');
|
|
210
212
|
const { doc, meta } = loadDraft(BULLSWARM_DIR(), name);
|
|
211
213
|
if (opts.json) {
|
|
212
214
|
jsonOut({ doc, meta }, opts);
|
|
@@ -247,7 +249,7 @@ function draftList(opts) {
|
|
|
247
249
|
function draftPhase(opts) {
|
|
248
250
|
const [action, name, phaseName] = opts._positional;
|
|
249
251
|
if (!action || !name || !phaseName) {
|
|
250
|
-
return
|
|
252
|
+
return usageErr('usage: bullswarm workflow draft phase <add|remove> <draft> <phase>');
|
|
251
253
|
}
|
|
252
254
|
if (action === 'add') {
|
|
253
255
|
if (!draftExists(BULLSWARM_DIR(), name)) return err(`draft "${name}" does not exist`);
|
|
@@ -266,13 +268,13 @@ function draftPhase(opts) {
|
|
|
266
268
|
else { console.log(`✓ removed phase "${phaseName}" from draft "${name}"`); reportValidation(r.validation); }
|
|
267
269
|
return r.validation?.ok === false ? 1 : 0;
|
|
268
270
|
}
|
|
269
|
-
return
|
|
271
|
+
return usageErr(`unknown phase action "${action}"`);
|
|
270
272
|
}
|
|
271
273
|
|
|
272
274
|
function draftStep(opts) {
|
|
273
275
|
const [action, name, phaseName, stepId] = opts._positional;
|
|
274
276
|
if (!action || !name || !phaseName || !stepId) {
|
|
275
|
-
return
|
|
277
|
+
return usageErr('usage: bullswarm workflow draft step <add|remove|set> <draft> <phase> <step-id> [options]');
|
|
276
278
|
}
|
|
277
279
|
if (action === 'add') {
|
|
278
280
|
const f = opts._flags;
|
|
@@ -314,8 +316,8 @@ function draftStep(opts) {
|
|
|
314
316
|
}
|
|
315
317
|
if (action === 'set') {
|
|
316
318
|
const field = opts._positional[4];
|
|
317
|
-
if (!field) return
|
|
318
|
-
if (!f_has(opts._flags, 'value')) return
|
|
319
|
+
if (!field) return usageErr('usage: bullswarm workflow draft step set <draft> <phase> <step> <field> --value <text>');
|
|
320
|
+
if (!f_has(opts._flags, 'value')) return usageErr('missing --value');
|
|
319
321
|
const r = setStepField(BULLSWARM_DIR(), name, {
|
|
320
322
|
phaseName, stepId, field, value: opts._flags.value,
|
|
321
323
|
});
|
|
@@ -323,7 +325,7 @@ function draftStep(opts) {
|
|
|
323
325
|
else { console.log(`✓ set ${field} on step "${stepId}" in draft "${name}"`); reportValidation(r.validation); }
|
|
324
326
|
return r.validation?.ok === false ? 1 : 0;
|
|
325
327
|
}
|
|
326
|
-
return
|
|
328
|
+
return usageErr(`unknown step action "${action}"`);
|
|
327
329
|
}
|
|
328
330
|
|
|
329
331
|
function f_has(flags, name) {
|
|
@@ -332,8 +334,8 @@ function f_has(flags, name) {
|
|
|
332
334
|
|
|
333
335
|
function draftSet(opts) {
|
|
334
336
|
const [name, field] = opts._positional;
|
|
335
|
-
if (!name || !field) return
|
|
336
|
-
if (!f_has(opts._flags, 'value')) return
|
|
337
|
+
if (!name || !field) return usageErr('usage: bullswarm workflow draft set <draft> <field> --value <text>');
|
|
338
|
+
if (!f_has(opts._flags, 'value')) return usageErr('missing --value');
|
|
337
339
|
const r = setField(BULLSWARM_DIR(), name, { field, value: opts._flags.value });
|
|
338
340
|
if (opts.json) jsonOut({ ok: true, action: 'set', name, field, validation: r.validation }, opts);
|
|
339
341
|
else { console.log(`✓ set ${field} on draft "${name}"`); reportValidation(r.validation); }
|
|
@@ -342,7 +344,7 @@ function draftSet(opts) {
|
|
|
342
344
|
|
|
343
345
|
async function draftValidate(opts) {
|
|
344
346
|
const [name] = opts._positional;
|
|
345
|
-
if (!name) return
|
|
347
|
+
if (!name) return usageErr('usage: bullswarm workflow draft validate <name>');
|
|
346
348
|
if (!draftExists(BULLSWARM_DIR(), name)) return err(`draft "${name}" does not exist`);
|
|
347
349
|
const { doc } = loadDraft(BULLSWARM_DIR(), name);
|
|
348
350
|
const { names } = await livePoolNames();
|
|
@@ -371,7 +373,7 @@ async function draftValidate(opts) {
|
|
|
371
373
|
|
|
372
374
|
function draftExport(opts) {
|
|
373
375
|
const [name, outFile] = opts._positional;
|
|
374
|
-
if (!name || !outFile) return
|
|
376
|
+
if (!name || !outFile) return usageErr('usage: bullswarm workflow draft export <name> <out-file>');
|
|
375
377
|
if (!draftExists(BULLSWARM_DIR(), name)) return err(`draft "${name}" does not exist`);
|
|
376
378
|
const { outPath } = exportDraft(BULLSWARM_DIR(), name, outFile);
|
|
377
379
|
if (opts.json) jsonOut({ ok: true, name, outPath }, opts);
|
|
@@ -381,10 +383,10 @@ function draftExport(opts) {
|
|
|
381
383
|
|
|
382
384
|
function draftDelete(opts) {
|
|
383
385
|
const [name] = opts._positional;
|
|
384
|
-
if (!name) return
|
|
386
|
+
if (!name) return usageErr('usage: bullswarm workflow draft delete <name>');
|
|
385
387
|
if (!f_has(opts._flags, 'yes') && !opts._flags.y) {
|
|
386
388
|
// Refuse without --yes for safety.
|
|
387
|
-
return
|
|
389
|
+
return usageErr(`refusing to delete draft "${name}" without --yes`);
|
|
388
390
|
}
|
|
389
391
|
const ok = deleteDraft(BULLSWARM_DIR(), name);
|
|
390
392
|
if (opts.json) jsonOut({ ok, name }, opts);
|
|
@@ -394,7 +396,7 @@ function draftDelete(opts) {
|
|
|
394
396
|
|
|
395
397
|
async function draftRun(opts) {
|
|
396
398
|
const [name] = opts._positional;
|
|
397
|
-
if (!name) return
|
|
399
|
+
if (!name) return usageErr('usage: bullswarm workflow draft run <name> [--input k=v]...');
|
|
398
400
|
if (!draftExists(BULLSWARM_DIR(), name)) return err(`draft "${name}" does not exist`);
|
|
399
401
|
// Build a synthetic path inside the drafts dir so the existing
|
|
400
402
|
// loadWorkflow + runWorkflow paths work without modification.
|
package/src/workflow/goal.js
CHANGED
|
@@ -11,6 +11,7 @@ export const AUTONOMOUS_ORCHESTRATOR_PROMPT = [
|
|
|
11
11
|
'You are the autonomous orchestrator for the user goal in the durable workflow context.',
|
|
12
12
|
'Own the workflow from initial decomposition through implementation and independent verification.',
|
|
13
13
|
'The user did not author a graph and must not be asked to steer routine execution.',
|
|
14
|
+
'This is a control-plane decision thread, not a worker assignment. Do not invoke Bullswarm, run shell commands, call tools, or modify repository files. Use only the durable context supplied below and return the requested decision JSON; delegate all inspection, implementation, and verification as bounded actions.',
|
|
14
15
|
'',
|
|
15
16
|
'At every checkpoint:',
|
|
16
17
|
'1. Observe the intent, completed actions, artifacts, failures, verification results, available capabilities, and remaining budget.',
|
package/src/workflow/runner.js
CHANGED
|
@@ -213,6 +213,13 @@ export async function runWorkflow(opts) {
|
|
|
213
213
|
// Always keep a reference to the doc on the state so the runtime can
|
|
214
214
|
// enforce inputs.<k>.required (R-run-time-required).
|
|
215
215
|
state._doc = doc;
|
|
216
|
+
if (resuming) {
|
|
217
|
+
state.intent = doc.intent ?? state.intent;
|
|
218
|
+
state.orchestration = doc.orchestration
|
|
219
|
+
? { ...(state.orchestration ?? {}), ...doc.orchestration }
|
|
220
|
+
: state.orchestration;
|
|
221
|
+
writeFileSync(join(runDir, 'workflow.json'), `${JSON.stringify(doc, null, 2)}\n`);
|
|
222
|
+
}
|
|
216
223
|
if (opts.inputs && Object.keys(opts.inputs).length) {
|
|
217
224
|
state.inputs = { ...state.inputs, ...opts.inputs };
|
|
218
225
|
}
|
package/src/workflow/runs-cli.js
CHANGED
|
@@ -20,7 +20,7 @@ import { BULLSWARM_DIR } from './cli.js';
|
|
|
20
20
|
import { buildWorkflowResult } from './result.js';
|
|
21
21
|
|
|
22
22
|
function jsonOut(obj, opts) { if (opts.json) console.log(JSON.stringify(obj, null, 2)); }
|
|
23
|
-
function err(msg) { console.error(msg); return
|
|
23
|
+
function err(msg, code = 1) { console.error(msg); return code; }
|
|
24
24
|
|
|
25
25
|
export function cmdRuns(args) {
|
|
26
26
|
const opts = parseRunsFlags(args);
|
|
@@ -89,7 +89,7 @@ function runsList(opts) {
|
|
|
89
89
|
try {
|
|
90
90
|
initiatedRange = resolveInitiatedRange(opts);
|
|
91
91
|
} catch (error) {
|
|
92
|
-
return err(error.message);
|
|
92
|
+
return err(error.message, 2);
|
|
93
93
|
}
|
|
94
94
|
const all = listRuns(BULLSWARM_DIR());
|
|
95
95
|
let filtered = all;
|
|
@@ -165,7 +165,7 @@ function runsList(opts) {
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
function runsShow(idToken, opts) {
|
|
168
|
-
if (!idToken) return err('usage: bullswarm workflow runs show <id>');
|
|
168
|
+
if (!idToken) return err('usage: bullswarm workflow runs show <id>', 2);
|
|
169
169
|
const resolved = resolveRunId(BULLSWARM_DIR(), idToken);
|
|
170
170
|
if (!resolved) return err(`no run found for "${idToken}"`);
|
|
171
171
|
|
|
@@ -195,7 +195,7 @@ function runsShow(idToken, opts) {
|
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
function runsResult(idToken, opts) {
|
|
198
|
-
if (!idToken) return err('usage: bullswarm workflow runs result <id> [--json]');
|
|
198
|
+
if (!idToken) return err('usage: bullswarm workflow runs result <id> [--json]', 2);
|
|
199
199
|
const resolved = resolveRunId(BULLSWARM_DIR(), idToken);
|
|
200
200
|
if (!resolved) return err(`no run found for "${idToken}"`);
|
|
201
201
|
|
|
@@ -248,8 +248,8 @@ function runsResult(idToken, opts) {
|
|
|
248
248
|
const MAX_HUMAN_RESULT_CHARS = 64 * 1024;
|
|
249
249
|
|
|
250
250
|
function runsDelete(idToken, opts, rest) {
|
|
251
|
-
if (!idToken) return err('usage: bullswarm workflow runs delete <id> --yes');
|
|
252
|
-
if (!opts.yes) return err(`refusing to delete run "${idToken}" without --yes
|
|
251
|
+
if (!idToken) return err('usage: bullswarm workflow runs delete <id> --yes', 2);
|
|
252
|
+
if (!opts.yes) return err(`refusing to delete run "${idToken}" without --yes`, 2);
|
|
253
253
|
const resolved = resolveRunId(BULLSWARM_DIR(), idToken);
|
|
254
254
|
if (!resolved) return err(`no run found for "${idToken}"`);
|
|
255
255
|
|
|
@@ -7,6 +7,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
7
7
|
import { join } from 'node:path';
|
|
8
8
|
import { resolveRunId } from './short-id.js';
|
|
9
9
|
import { readSteering } from './steering.js';
|
|
10
|
+
import { readEvents } from './events.js';
|
|
10
11
|
import { isDeliveredWorkflowStatus, isTerminalWorkflowStatus } from './status.js';
|
|
11
12
|
|
|
12
13
|
function readJson(path) {
|
|
@@ -81,6 +82,7 @@ export function watchSnapshot(runDir, state, now = new Date()) {
|
|
|
81
82
|
tokens: state.usage?.tokens?.totalKnown ?? null,
|
|
82
83
|
pendingSteering: queuedSteering.length,
|
|
83
84
|
deliveredSteering: state.steering?.length ?? 0,
|
|
85
|
+
quietForSec: 0,
|
|
84
86
|
agents: Object.values(state.activeAgents ?? {}).map((agent) => ({
|
|
85
87
|
stepId: agent.stepId,
|
|
86
88
|
pool: agent.pool ?? null,
|
|
@@ -119,9 +121,37 @@ export function snapshotFingerprint(snapshot) {
|
|
|
119
121
|
});
|
|
120
122
|
}
|
|
121
123
|
|
|
122
|
-
|
|
124
|
+
function humanTransitionFingerprint(snapshot) {
|
|
125
|
+
return JSON.stringify({
|
|
126
|
+
status: snapshot.status,
|
|
127
|
+
stage: snapshot.stage,
|
|
128
|
+
phase: snapshot.phase,
|
|
129
|
+
step: snapshot.step,
|
|
130
|
+
dispatchesUsed: snapshot.dispatchesUsed,
|
|
131
|
+
pendingSteering: snapshot.pendingSteering,
|
|
132
|
+
agents: snapshot.agents.map((agent) => ({
|
|
133
|
+
stepId: agent.stepId,
|
|
134
|
+
pool: agent.pool,
|
|
135
|
+
model: agent.model,
|
|
136
|
+
status: agent.status,
|
|
137
|
+
stall: agent.stall,
|
|
138
|
+
})),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function renderWatchSnapshot(snapshot, { heartbeat = false, verbose = false, events = [] } = {}) {
|
|
123
143
|
const target = snapshot.dispatchTarget == null ? '∞' : snapshot.dispatchTarget;
|
|
124
144
|
const location = [snapshot.phase, snapshot.step].filter(Boolean).join('/') || 'starting';
|
|
145
|
+
if (!verbose) {
|
|
146
|
+
const actions = events.filter((event) => event.type === 'attempt.agent_action').length;
|
|
147
|
+
const line = `${snapshot.terminal ? '■' : heartbeat ? '♡' : '●'} +${formatDuration(snapshot.elapsedSec)} ` +
|
|
148
|
+
`${snapshot.status}/${snapshot.stage ?? '?'} ${location} · ${events.length} events, ${actions} actions · ` +
|
|
149
|
+
`quiet ${formatDuration(snapshot.quietForSec)}`;
|
|
150
|
+
if (snapshot.terminal && snapshot.timing) {
|
|
151
|
+
return `${line}\n timing: ${snapshot.timing.attempts.length} attempts in ${formatDuration(snapshot.timing.workflowElapsedSec)}`;
|
|
152
|
+
}
|
|
153
|
+
return line;
|
|
154
|
+
}
|
|
125
155
|
const marker = snapshot.terminal ? '■' : heartbeat ? '♡' : '●';
|
|
126
156
|
const lines = [
|
|
127
157
|
`${marker} +${formatDuration(snapshot.elapsedSec)} ${snapshot.status}/${snapshot.stage ?? '?'} ` +
|
|
@@ -152,6 +182,7 @@ export async function runWorkflowWatch(bullswarmDir, token, {
|
|
|
152
182
|
heartbeatMs = 60000,
|
|
153
183
|
once = false,
|
|
154
184
|
jsonl = false,
|
|
185
|
+
verbose = false,
|
|
155
186
|
output = process.stdout,
|
|
156
187
|
} = {}) {
|
|
157
188
|
const resolved = resolveRunId(bullswarmDir, token);
|
|
@@ -159,21 +190,71 @@ export async function runWorkflowWatch(bullswarmDir, token, {
|
|
|
159
190
|
const statePath = join(resolved.runDir, 'state.json');
|
|
160
191
|
if (!existsSync(statePath)) throw new Error(`run "${token}" has no state.json`);
|
|
161
192
|
let priorFingerprint = null;
|
|
193
|
+
let priorHumanFingerprint = null;
|
|
162
194
|
let lastPrintedAt = 0;
|
|
195
|
+
let priorSequence = null;
|
|
196
|
+
let pendingEvents = [];
|
|
197
|
+
let lastActivityAt = null;
|
|
163
198
|
while (true) {
|
|
164
199
|
const state = readJson(statePath);
|
|
165
200
|
if (state) {
|
|
166
201
|
const snapshot = watchSnapshot(resolved.runDir, state);
|
|
202
|
+
if (priorSequence == null) {
|
|
203
|
+
// A newly attached watcher has no preceding interval. Start at the
|
|
204
|
+
// durable high-water mark instead of replaying the run lifetime.
|
|
205
|
+
priorSequence = state.eventSequence ?? 0;
|
|
206
|
+
lastActivityAt = Math.max(
|
|
207
|
+
Date.parse(state.lastEvent?.committedAt ?? '') || 0,
|
|
208
|
+
Date.parse(state.finishedAt ?? '') || 0,
|
|
209
|
+
...Object.values(state.attempts ?? []).map((attempt) => Math.max(
|
|
210
|
+
Date.parse(attempt.lastActivityAt ?? '') || 0,
|
|
211
|
+
Date.parse(attempt.lastHeartbeatAt ?? '') || 0,
|
|
212
|
+
Date.parse(attempt.finishedAt ?? '') || 0,
|
|
213
|
+
)),
|
|
214
|
+
...Object.values(state.activeAgents ?? {}).map((agent) => Math.max(
|
|
215
|
+
Date.parse(agent.lastActivityAt ?? '') || 0,
|
|
216
|
+
Date.parse(agent.lastActionAt ?? '') || 0,
|
|
217
|
+
Date.parse(agent.startedAt ?? '') || 0,
|
|
218
|
+
)),
|
|
219
|
+
Date.parse(state.startedAt ?? '') || Date.now(),
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
const newEvents = readEvents(resolved.runDir, { after: priorSequence });
|
|
223
|
+
if (newEvents.length) {
|
|
224
|
+
pendingEvents.push(...newEvents);
|
|
225
|
+
lastActivityAt = Math.max(
|
|
226
|
+
lastActivityAt,
|
|
227
|
+
...newEvents.map((event) => Date.parse(event.committedAt ?? '') || Date.now()),
|
|
228
|
+
);
|
|
229
|
+
priorSequence = newEvents.at(-1)?.sequence ?? state.eventSequence ?? priorSequence;
|
|
230
|
+
}
|
|
231
|
+
snapshot.quietForSec = Math.max(0, Math.floor((Date.now() - lastActivityAt) / 1000));
|
|
167
232
|
const fingerprint = snapshotFingerprint(snapshot);
|
|
233
|
+
const humanFingerprint = humanTransitionFingerprint(snapshot);
|
|
168
234
|
const heartbeat = Date.now() - lastPrintedAt >= heartbeatMs;
|
|
169
|
-
|
|
235
|
+
const changed = jsonl || verbose
|
|
236
|
+
? fingerprint !== priorFingerprint
|
|
237
|
+
: humanFingerprint !== priorHumanFingerprint;
|
|
238
|
+
if (changed || heartbeat || once) {
|
|
170
239
|
output.write(jsonl
|
|
171
240
|
? `${JSON.stringify({ type: heartbeat && fingerprint === priorFingerprint ? 'heartbeat' : 'progress', ...snapshot })}\n`
|
|
172
|
-
: `${renderWatchSnapshot(snapshot, {
|
|
241
|
+
: `${renderWatchSnapshot(snapshot, {
|
|
242
|
+
heartbeat: heartbeat && !changed,
|
|
243
|
+
verbose,
|
|
244
|
+
events: pendingEvents,
|
|
245
|
+
})}\n`);
|
|
173
246
|
priorFingerprint = fingerprint;
|
|
247
|
+
priorHumanFingerprint = humanFingerprint;
|
|
174
248
|
lastPrintedAt = Date.now();
|
|
249
|
+
pendingEvents = [];
|
|
250
|
+
}
|
|
251
|
+
if (snapshot.terminal || once) {
|
|
252
|
+
if (!jsonl && snapshot.terminal) {
|
|
253
|
+
output.write(`outcome: ${snapshot.status}\n`);
|
|
254
|
+
output.write(`next: bullswarm workflow runs result ${snapshot.shortId ?? snapshot.runId} --json\n`);
|
|
255
|
+
}
|
|
256
|
+
return isDeliveredWorkflowStatus(snapshot.status) || once ? 0 : 1;
|
|
175
257
|
}
|
|
176
|
-
if (snapshot.terminal || once) return isDeliveredWorkflowStatus(snapshot.status) || once ? 0 : 1;
|
|
177
258
|
}
|
|
178
259
|
await new Promise((resolve) => setTimeout(resolve, Math.max(100, intervalMs)));
|
|
179
260
|
}
|