bullswarm 0.29.0 → 0.29.1
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 +22 -0
- package/package.json +1 -1
- package/skill/SKILL.md +3 -1
- package/skill/references/operations.md +4 -2
- package/src/lib/strategy.js +5 -1
- package/src/workflow/dashboard.js +4 -2
- package/src/workflow/v2-dispatch.js +1 -1
- package/src/workflow/v2-runtime.js +23 -5
- package/src/workflow/watch-cli.js +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# bullswarm changelog
|
|
2
2
|
|
|
3
|
+
## 0.29.1 — a revised cancelled run runs its cancelled steps again
|
|
4
|
+
|
|
5
|
+
- workflow: revising a cancelled run reopened it but left every step the
|
|
6
|
+
cancellation had stopped as `cancelled`, so the kernel finalized `partial`
|
|
7
|
+
in the same second and nothing ran. Found on a real claude-code run
|
|
8
|
+
(`pxkr7s`, revision 7). Reopening now returns those steps to pending, lists
|
|
9
|
+
them as `reopened.requeued` in the revise output and the `workflow.reopened`
|
|
10
|
+
event, and never counts their earlier attempts as completion. Failed steps
|
|
11
|
+
still wait for an explicit `--rerun`.
|
|
12
|
+
- watch: a run waiting for its caller planner now also prints the
|
|
13
|
+
`plan export` / `plan revise` commands next to `plan show`.
|
|
14
|
+
- workflow: an attempt stopped by a plan revision or a pause now records
|
|
15
|
+
`failureKind: superseded` or `paused` on the attempt and in
|
|
16
|
+
`attempt.finished`, the same kind `action.finished` already carried. It was
|
|
17
|
+
recorded as a plain `cancelled`, which made deliberate steering look like
|
|
18
|
+
worker failures.
|
|
19
|
+
- strategy: `strategy rungs` no longer counts a stopped dispatch against a
|
|
20
|
+
pool's "% ok". On 2026-09-14 all 11 claude-code dispatches recorded as not
|
|
21
|
+
ok were workflow cancellations, plan revisions or pauses, none a worker
|
|
22
|
+
failure. Dispatch records now carry `failureKind`. The share was display
|
|
23
|
+
only; routing never read it.
|
|
24
|
+
|
|
3
25
|
## 0.29.0 — steer a running workflow, providers become directories, a declared reset date
|
|
4
26
|
|
|
5
27
|
- workflow: the plan of a caller-planned program run can be changed at any
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -174,7 +174,9 @@ Rules that matter when you edit:
|
|
|
174
174
|
must not remain, give a new or amended step the job of reverting or repairing
|
|
175
175
|
them.
|
|
176
176
|
- Revising a finished run (`completed`, `partial`, `cancelled`) reopens it: the
|
|
177
|
-
new plan runs and the run finishes again with a new result.
|
|
177
|
+
new plan runs and the run finishes again with a new result. Steps a
|
|
178
|
+
cancellation stopped run again; failed steps run again only when named in
|
|
179
|
+
`rerun`.
|
|
178
180
|
- To think without new work starting, pause first: `bullswarm workflow pause
|
|
179
181
|
<shortId>` starts nothing new and lets running agents finish (`--now` stops
|
|
180
182
|
them; they run again after resume). Export and revise while paused, then
|
|
@@ -209,8 +209,10 @@ being replaced is stopped before the new plan is committed
|
|
|
209
209
|
CLI applies the revision itself under the kernel lease and relaunches the
|
|
210
210
|
kernel detached, unless the run is paused. A finished run is reopened: its
|
|
211
211
|
`result.json` moves to `result-before-revision-<n>.json`, a
|
|
212
|
-
`workflow.reopened` event is written, and the new plan runs to a new result.
|
|
213
|
-
|
|
212
|
+
`workflow.reopened` event is written, and the new plan runs to a new result.
|
|
213
|
+
Steps a cancellation stopped return to pending (`reopened.requeued`); failed
|
|
214
|
+
steps stay failed unless the revision names them in `rerun`. A revision also
|
|
215
|
+
clears a caller-planner wait, so it can replace `plan submit`.
|
|
214
216
|
|
|
215
217
|
Stopping a process does not undo its edits in the shared tree. When a stopped
|
|
216
218
|
or removed step's partial changes must go, amend it or add a step whose prompt
|
package/src/lib/strategy.js
CHANGED
|
@@ -303,7 +303,11 @@ export function rungRecord(decisionLog, pool, tier) {
|
|
|
303
303
|
const window = attemptWindow(row);
|
|
304
304
|
if (window) minutes.push(window.minutes);
|
|
305
305
|
}
|
|
306
|
-
|
|
306
|
+
// A dispatch someone stopped (a workflow cancel, a plan revision, a pause) is
|
|
307
|
+
// not a verdict on the pool. Older rows carry no failureKind, only the
|
|
308
|
+
// dispatcher's fixed cancellation message.
|
|
309
|
+
const stopped = (row) => row.failureKind === 'cancelled' || row.why === 'workflow cancellation requested';
|
|
310
|
+
const verdicts = rows.filter((row) => typeof row.ok === 'boolean' && !stopped(row));
|
|
307
311
|
return {
|
|
308
312
|
dispatches: rows.length,
|
|
309
313
|
medianMinutes: minutes.length ? Math.round(medianOf(minutes) * 100) / 100 : null,
|
|
@@ -452,7 +452,9 @@ export function workflowPanelModel(row, { phaseIndex = null, agentIndex = null }
|
|
|
452
452
|
agents.push({
|
|
453
453
|
key: `attempt:${attempt.id}`, action, attempt: { ...attempt, attemptNumber: attempt.ordinal, outFile: attempt.outputFile },
|
|
454
454
|
active: attempt.status === 'running' ? { ...attempt, stepId: action.id, attempt: attempt.ordinal, outFile: attempt.outputFile } : null,
|
|
455
|
-
pool: attempt.pool ?? 'unassigned', model: attempt.model ?? 'connector model',
|
|
455
|
+
pool: attempt.pool ?? 'unassigned', model: attempt.model ?? 'connector model',
|
|
456
|
+
// A worker stopped by a plan revision or a pause did not fail; say why it stopped.
|
|
457
|
+
status: attempt.status === 'cancelled' && ['superseded', 'paused'].includes(attempt.failureKind) ? attempt.failureKind : attempt.status,
|
|
456
458
|
});
|
|
457
459
|
}
|
|
458
460
|
}
|
|
@@ -1326,7 +1328,7 @@ function statusIcon(status, spinnerFrame = 0) {
|
|
|
1326
1328
|
if (value.includes('waiting') || value === 'queued' || value === 'paused'
|
|
1327
1329
|
|| value === 'blocked' || value === 'starting' || value === 'reviewing evidence'
|
|
1328
1330
|
|| value === 'directing execution') return glyphs().waiting;
|
|
1329
|
-
if (value === 'skipped' || value === 'removed') return '–';
|
|
1331
|
+
if (value === 'skipped' || value === 'removed' || value === 'superseded') return '–';
|
|
1330
1332
|
return glyphs().pending;
|
|
1331
1333
|
}
|
|
1332
1334
|
|
|
@@ -417,7 +417,7 @@ export async function dispatchV2Action({
|
|
|
417
417
|
onAttempt?.('finished', clone(record), verdict);
|
|
418
418
|
appendDecision(bullswarmDir, {
|
|
419
419
|
ts: finishedAt, lane: action.lane ?? 'chore', picked: pool.name,
|
|
420
|
-
keepOnClaude: false, ok: verdict.ok, why: verdict.why ?? null,
|
|
420
|
+
keepOnClaude: false, ok: verdict.ok, failureKind: verdict.ok ? null : kind, why: verdict.why ?? null,
|
|
421
421
|
wallSec: verdict.meta?.wallSec ?? null, model: record.model,
|
|
422
422
|
reasoning: clone(reasoning),
|
|
423
423
|
usage: verdict.meta?.usage ?? null, routing: record.routing,
|
|
@@ -33,7 +33,7 @@ import { scoutPrompt } from './goal.js';
|
|
|
33
33
|
import {
|
|
34
34
|
createIsolatedWorkspace, disposeIsolatedWorkspace, integrateIsolatedWorkspace,
|
|
35
35
|
} from './v2-workspace.js';
|
|
36
|
-
import { presentationStageStatus, stageForAction } from './v2-presentation.js';
|
|
36
|
+
import { deriveV2LiveStages, presentationStageStatus, stageForAction } from './v2-presentation.js';
|
|
37
37
|
import { deliverSteering, peekSteering, readSteering } from './steering.js';
|
|
38
38
|
import { enforcesOwnership, isProgramWorkflow, v2SchedulingOptions } from './execution-policy.js';
|
|
39
39
|
import { buildWorkspaceReport, captureWorkspaceStatus } from './workspace-report.js';
|
|
@@ -264,8 +264,22 @@ function commitRevisionUnderLease(runDir, request, { now }) {
|
|
|
264
264
|
if (state.cancellation.requested) state.cancellation = { requested: false, requestedAt: null, reason: null };
|
|
265
265
|
rmSync(cancelFile, { force: true });
|
|
266
266
|
if (['completed', 'cancelled', 'failed'].includes(state.planner.status)) state.planner.status = 'waiting';
|
|
267
|
-
|
|
268
|
-
|
|
267
|
+
// Steps the cancellation stopped were never judged; reopening the run is
|
|
268
|
+
// what lifts that cancellation, so they run again. Their earlier attempts
|
|
269
|
+
// stay on record but never count as this step's completion. Failed steps
|
|
270
|
+
// are left as they are: rerunning them is the caller's decision.
|
|
271
|
+
const requeued = [];
|
|
272
|
+
for (const action of state.actions) {
|
|
273
|
+
if (action.status !== 'cancelled') continue;
|
|
274
|
+
Object.assign(action, {
|
|
275
|
+
status: 'pending', startedAt: null, finishedAt: null, outputFile: null, artifactIds: [],
|
|
276
|
+
lastFailure: null, supersededAttempts: action.attempts,
|
|
277
|
+
});
|
|
278
|
+
requeued.push(action.id);
|
|
279
|
+
}
|
|
280
|
+
if (requeued.length) state.presentation.stages = deriveV2LiveStages(state, { revision: state.program.revision, at });
|
|
281
|
+
reopened = { previousStatus, archivedResult: hadResult ? archived : null, requeued };
|
|
282
|
+
appendEvent(runDir, state, 'workflow.reopened', { previousStatus, requestId: request.id, archivedResult: reopened.archivedResult, requeued });
|
|
269
283
|
}
|
|
270
284
|
// A revision answers a caller-planner pause as fully as a submission does.
|
|
271
285
|
if (state.planner.awaiting) {
|
|
@@ -1307,6 +1321,10 @@ async function runV2Kernel({
|
|
|
1307
1321
|
emit('attempt.started', { actionId: action.id, attemptId: currentAttemptId, pool: record.pool, model: record.model, reasoning: clone(record.reasoning ?? null) });
|
|
1308
1322
|
} else {
|
|
1309
1323
|
lease.assertOwner();
|
|
1324
|
+
// An attempt stopped by a plan revision or a pause is not a failure of
|
|
1325
|
+
// its pool: record why it stopped, the same kind action.finished carries.
|
|
1326
|
+
const stop = record.status === 'cancelled' && !interrupted ? stopRequested.get(action.id) ?? null : null;
|
|
1327
|
+
if (stop) record = { ...record, failureKind: stop.kind, why: stop.message };
|
|
1310
1328
|
if (record.status === 'succeeded') writeCompletionReceipt(receiptPath, {
|
|
1311
1329
|
attemptId: currentAttemptId, finishedAt: record.finishedAt, before, isolated,
|
|
1312
1330
|
verdict: verdict ?? { ok: true, outFile: record.outFile ?? record.outputFile },
|
|
@@ -1460,10 +1478,10 @@ async function runV2Kernel({
|
|
|
1460
1478
|
runtime.finishedAt = finishedAt;
|
|
1461
1479
|
runtime.lastFailure = stop ? { kind: stop.kind, message: stop.message } : { kind: 'runtime', message: error?.message || String(error) };
|
|
1462
1480
|
for (const attempt of state.attempts) if (attempt.actionId === action.id && attempt.status === 'running') {
|
|
1463
|
-
Object.assign(attempt, { status: runtime.status, finishedAt, failureKind:
|
|
1481
|
+
Object.assign(attempt, { status: runtime.status, finishedAt, failureKind: runtime.lastFailure.kind, why: runtime.lastFailure.message });
|
|
1464
1482
|
runtime.outputFile ??= attempt.outputFile;
|
|
1465
1483
|
}
|
|
1466
|
-
emit('action.finished', { actionId: action.id, status: runtime.status, failureKind:
|
|
1484
|
+
emit('action.finished', { actionId: action.id, status: runtime.status, failureKind: runtime.lastFailure.kind, why: runtime.lastFailure.message });
|
|
1467
1485
|
completePresentationStages();
|
|
1468
1486
|
}
|
|
1469
1487
|
};
|
|
@@ -867,7 +867,8 @@ export async function runWorkflowWatch(bullswarmDir, token, {
|
|
|
867
867
|
output.write(`outcome: waiting for the caller planner (${snapshot.awaitingPlanner.boundary} boundary)\n`);
|
|
868
868
|
output.write(snapshot.cancellationRequested
|
|
869
869
|
? `next: cancellation requested; bullswarm workflow cancel ${runToken} --json finalizes it\n`
|
|
870
|
-
: `next: bullswarm workflow plan show ${runToken} --json\n`
|
|
870
|
+
: `next: bullswarm workflow plan show ${runToken} --json\n`
|
|
871
|
+
+ ` or revise: bullswarm workflow plan export ${runToken} --out plan.json, then bullswarm workflow plan revise ${runToken} --program plan.json\n`);
|
|
871
872
|
}
|
|
872
873
|
return 0;
|
|
873
874
|
}
|