bullswarm 0.18.0 → 0.18.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/CHANGELOG.md +29 -0
- package/package.json +1 -1
- package/src/workflow/dashboard.js +67 -19
- package/src/workflow/decision.js +31 -0
- package/src/workflow/goal.js +39 -3
- package/src/workflow/runner.js +47 -9
- package/src/workflow/runtime.js +111 -18
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# bullswarm changelog
|
|
2
2
|
|
|
3
|
+
## 0.18.2 — truthful recovery timeline and verifier cleanup
|
|
4
|
+
|
|
5
|
+
- Dependency-blocked plan branches now appear as a single skipped phase in the
|
|
6
|
+
workflow timeline instead of several failed agents followed by a
|
|
7
|
+
contradictory completed phase.
|
|
8
|
+
- The Live panel ignores stale terminal agent records, and verifier retry
|
|
9
|
+
cleanup now removes the retrying agent as soon as the bounded retry ends.
|
|
10
|
+
- Verifier verdict parsing conservatively repairs an otherwise valid JSON
|
|
11
|
+
object truncated only by missing final closing brackets. Malformed content
|
|
12
|
+
still fails closed, while provider truncation no longer forces an expensive
|
|
13
|
+
planner recovery round.
|
|
14
|
+
|
|
15
|
+
## 0.18.1 — original-goal verification and denser timeline
|
|
16
|
+
|
|
17
|
+
- Goal workflows now derive a durable requirement ledger from the original
|
|
18
|
+
user goal. Planner verify actions declare which requirements they cover, and
|
|
19
|
+
neither explicit nor program-level completion is accepted until every
|
|
20
|
+
requirement has a successful verifier with specific evidence. The verifier
|
|
21
|
+
receives the original goal from the runtime, so a reduced planner scope can
|
|
22
|
+
no longer silently omit requested APIs, events, tests, or documentation.
|
|
23
|
+
- Successful verifier concerns are preserved in a verified
|
|
24
|
+
`completed_with_concerns` result instead of being discarded or triggering
|
|
25
|
+
unnecessary follow-up spending.
|
|
26
|
+
- The human timeline calls its first accepted planner decision `plan created`,
|
|
27
|
+
later decisions `plan updated`, and the final one `completion confirmed`.
|
|
28
|
+
Execution milestones are rendered as one dense block without blank rows.
|
|
29
|
+
Finished workflows now say `No agents running · workflow finished` and
|
|
30
|
+
`Workflow finished · result ready` instead of control-plane terminology.
|
|
31
|
+
|
|
3
32
|
## 0.18.0 — exact routes, cheaper plans, clearer results
|
|
4
33
|
|
|
5
34
|
- `workflow goal` can now guarantee an exact planner model and a separate exact
|
package/package.json
CHANGED
|
@@ -201,15 +201,19 @@ const TERMINAL_ACTIONS = new Set([
|
|
|
201
201
|
]);
|
|
202
202
|
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
203
203
|
|
|
204
|
+
function isLiveAgent(agent) {
|
|
205
|
+
return !agent?.status || agent.status === 'running';
|
|
206
|
+
}
|
|
207
|
+
|
|
204
208
|
function autonomousControlPlane(state) {
|
|
205
209
|
const autonomous = state.intent?.autonomous === true || state.orchestration?.mode === 'autonomous';
|
|
206
210
|
if (!autonomous) return { autonomous: false, actionId: null, attempts: [], active: null };
|
|
207
211
|
const actionId = state.decisions?.find((decision) => decision.gateId)?.gateId ?? 'orchestrator';
|
|
208
212
|
const attempts = (state.attempts ?? []).filter((attempt) => attempt.actionId === actionId);
|
|
209
|
-
const active = Object.values(state.activeAgents ?? {}).find((agent) => agent.stepId === actionId) ?? null;
|
|
213
|
+
const active = Object.values(state.activeAgents ?? {}).find((agent) => agent.stepId === actionId && isLiveAgent(agent)) ?? null;
|
|
210
214
|
const latestAttempt = attempts.at(-1) ?? null;
|
|
211
215
|
const terminal = Boolean(state.finishedAt);
|
|
212
|
-
const workerActive = Object.values(state.activeAgents ?? {}).some((agent) => agent.stepId !== actionId);
|
|
216
|
+
const workerActive = Object.values(state.activeAgents ?? {}).some((agent) => agent.stepId !== actionId && isLiveAgent(agent));
|
|
213
217
|
const status = terminal
|
|
214
218
|
? state.status === 'completed' ? 'completed' : state.status ?? 'finished'
|
|
215
219
|
: active ? 'planning'
|
|
@@ -233,7 +237,7 @@ function effectiveActionStatus(action, state) {
|
|
|
233
237
|
// running even when a previous round recorded ok:false — a failed mark on
|
|
234
238
|
// work that is still being retried misreports the run (user report 2026-08-29).
|
|
235
239
|
const active = Object.values(state.activeAgents ?? {}).some((agent) =>
|
|
236
|
-
agent.stepId === action.id || String(agent.stepId ?? '').startsWith(`${action.id}[`));
|
|
240
|
+
isLiveAgent(agent) && (agent.stepId === action.id || String(agent.stepId ?? '').startsWith(`${action.id}[`)));
|
|
237
241
|
if (active || action.status === 'running') return 'running';
|
|
238
242
|
const output = state.outputs?.[action.id];
|
|
239
243
|
if (output?.ok === false) return 'failed_terminal';
|
|
@@ -588,9 +592,9 @@ function workflowTimelineLines(model, width) {
|
|
|
588
592
|
const { state, orchestrator } = model;
|
|
589
593
|
const ledger = state.actionLedger ?? [];
|
|
590
594
|
const events = [];
|
|
591
|
-
const add = (at, lines, sequence = Number.MAX_SAFE_INTEGER) => {
|
|
595
|
+
const add = (at, lines, sequence = Number.MAX_SAFE_INTEGER, group = null) => {
|
|
592
596
|
if (!at) return;
|
|
593
|
-
events.push({ at, sequence, lines: Array.isArray(lines) ? lines : [lines] });
|
|
597
|
+
events.push({ at, sequence, group, lines: Array.isArray(lines) ? lines : [lines] });
|
|
594
598
|
};
|
|
595
599
|
const scout = ledger.find((action) => action.id === 'scout');
|
|
596
600
|
add(state.startedAt, [
|
|
@@ -620,10 +624,17 @@ function workflowTimelineLines(model, width) {
|
|
|
620
624
|
const decision = decisionForPlannerAttempt(state, attempt, index, orchestrator.attempts);
|
|
621
625
|
const summary = decision?.reason ? sentencePreview(decision.reason, Math.max(30, width - 10))
|
|
622
626
|
: decision ? decisionLabel(decision.decision) : 'No accepted decision; correction or retry turn';
|
|
627
|
+
const acceptedBefore = orchestrator.attempts.slice(0, index).filter((entry, priorIndex) =>
|
|
628
|
+
decisionForPlannerAttempt(state, entry, priorIndex, orchestrator.attempts)).length;
|
|
629
|
+
const plannerLabel = !decision
|
|
630
|
+
? `planning retry #${index + 1}`
|
|
631
|
+
: decision.decision === 'complete'
|
|
632
|
+
? 'completion confirmed'
|
|
633
|
+
: acceptedBefore === 0 ? 'plan created' : `plan updated #${acceptedBefore + 1}`;
|
|
623
634
|
add(attempt.finishedAt, [
|
|
624
|
-
timelineRow(attempt.finishedAt, `◆ [Workflow Planner]
|
|
635
|
+
timelineRow(attempt.finishedAt, `◆ [Workflow Planner] ${plannerLabel}`, durationText(attempt.startedAt, attempt.finishedAt), width),
|
|
625
636
|
timelineDetail(summary, width),
|
|
626
|
-
]);
|
|
637
|
+
], Number.MAX_SAFE_INTEGER, 'execution');
|
|
627
638
|
});
|
|
628
639
|
|
|
629
640
|
const phases = new Map();
|
|
@@ -638,7 +649,18 @@ function workflowTimelineLines(model, width) {
|
|
|
638
649
|
const startedAt = realStart ?? earliestTimestamp(actions.map((action) => actionFinishedAt(state, action)));
|
|
639
650
|
if (!startedAt) continue;
|
|
640
651
|
const label = phaseLabel(name, orchestrator);
|
|
641
|
-
|
|
652
|
+
const dependencyBlocked = actions.filter((action) => state.outputs?.[action.id]?.dependencyBlocked === true);
|
|
653
|
+
if (!realStart && dependencyBlocked.length === actions.length) {
|
|
654
|
+
const finishedAt = latestTimestamp(actions.map((action) => actionFinishedAt(state, action)));
|
|
655
|
+
if (finishedAt) {
|
|
656
|
+
add(finishedAt, [
|
|
657
|
+
timelineRow(finishedAt, `⊘ [Phase: ${label}] skipped`, `${actions.length} action${actions.length === 1 ? '' : 's'} not run`, width),
|
|
658
|
+
timelineDetail('Required earlier work did not pass; the planner chose a recovery path', width),
|
|
659
|
+
], Number.MAX_SAFE_INTEGER, 'execution');
|
|
660
|
+
}
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
add(startedAt, timelineRow(startedAt, `├─ [Phase: ${label}] ${realStart ? 'started' : 'blocked'}`, '', width), Number.MAX_SAFE_INTEGER, 'execution');
|
|
642
664
|
const finished = actions
|
|
643
665
|
.filter((action) => actionFinishedAt(state, action) && TERMINAL_ACTIONS.has(effectiveActionStatus(action, state)))
|
|
644
666
|
.sort((a, b) => Date.parse(actionFinishedAt(state, a)) - Date.parse(actionFinishedAt(state, b)));
|
|
@@ -647,29 +669,33 @@ function workflowTimelineLines(model, width) {
|
|
|
647
669
|
const branch = terminalPhase ? '│ └─' : '│ ├─';
|
|
648
670
|
const actionFinished = actionFinishedAt(state, action);
|
|
649
671
|
const actionStarted = actionStartedAt(state, action);
|
|
672
|
+
const blocked = state.outputs?.[action.id]?.dependencyBlocked === true;
|
|
650
673
|
add(actionFinished, timelineRow(
|
|
651
674
|
actionFinished,
|
|
652
|
-
`${branch}${statusIcon(effectiveActionStatus(action, state))} [${label}] ${action.id}`,
|
|
675
|
+
`${branch}${blocked ? '⊘' : statusIcon(effectiveActionStatus(action, state))} [${label}] ${action.id}`,
|
|
653
676
|
actionStarted ? durationText(actionStarted, actionFinished) : '',
|
|
654
677
|
width,
|
|
655
|
-
));
|
|
678
|
+
), Number.MAX_SAFE_INTEGER, 'execution');
|
|
656
679
|
});
|
|
657
680
|
if (finished.length === actions.length && actions.length) {
|
|
658
681
|
const finishedAt = latestTimestamp(actions.map((action) => actionFinishedAt(state, action)));
|
|
659
|
-
const
|
|
660
|
-
|
|
682
|
+
const blocked = actions.some((action) => state.outputs?.[action.id]?.dependencyBlocked === true);
|
|
683
|
+
const failed = actions.some((action) => state.outputs?.[action.id]?.dependencyBlocked !== true
|
|
684
|
+
&& String(effectiveActionStatus(action, state)).startsWith('failed'));
|
|
685
|
+
const outcome = failed ? 'finished with failures' : blocked ? 'incomplete' : 'completed';
|
|
686
|
+
add(finishedAt, timelineRow(finishedAt, `└─${failed ? '✗' : blocked ? '!' : '✓'} [Phase: ${label}] ${outcome}`, `${finished.length}/${actions.length}`, width), Number.MAX_SAFE_INTEGER, 'execution');
|
|
661
687
|
}
|
|
662
688
|
}
|
|
663
689
|
|
|
664
690
|
for (const event of model.events) {
|
|
665
691
|
const detail = timelineControlEvent(event, width);
|
|
666
|
-
if (detail) add(event.committedAt, detail, Number(event.sequence));
|
|
692
|
+
if (detail) add(event.committedAt, detail, Number(event.sequence), event.type.startsWith('decision.') ? 'execution' : null);
|
|
667
693
|
}
|
|
668
694
|
|
|
669
695
|
events.sort((a, b) => Date.parse(a.at) - Date.parse(b.at) || a.sequence - b.sequence);
|
|
670
696
|
const lines = [];
|
|
671
697
|
events.forEach((event, index) => {
|
|
672
|
-
if (index) lines.push('');
|
|
698
|
+
if (index && (!event.group || event.group !== events[index - 1].group)) lines.push('');
|
|
673
699
|
lines.push(...event.lines);
|
|
674
700
|
});
|
|
675
701
|
return { lines: lines.length ? lines : ['Waiting for the first durable workflow milestone'], milestoneCount: events.length };
|
|
@@ -718,7 +744,7 @@ function timelineControlEvent(event, width) {
|
|
|
718
744
|
function workflowLiveLines(model, width, spinnerFrame) {
|
|
719
745
|
const { state, orchestrator } = model;
|
|
720
746
|
const activeWorkers = Object.values(state.activeAgents ?? {})
|
|
721
|
-
.filter((agent) => agent.stepId !== orchestrator.actionId)
|
|
747
|
+
.filter((agent) => agent.stepId !== orchestrator.actionId && isLiveAgent(agent))
|
|
722
748
|
.sort((a, b) => String(b.lastEventAt ?? b.lastActivityAt ?? '').localeCompare(String(a.lastEventAt ?? a.lastActivityAt ?? '')));
|
|
723
749
|
const lines = [];
|
|
724
750
|
let running = activeWorkers.length + (orchestrator.active ? 1 : 0);
|
|
@@ -757,13 +783,35 @@ function workflowLiveLines(model, width, spinnerFrame) {
|
|
|
757
783
|
if (stream) lines.push(` ${stream}`);
|
|
758
784
|
lines.push('');
|
|
759
785
|
}
|
|
760
|
-
if (!lines.length) lines.push(state.finishedAt
|
|
786
|
+
if (!lines.length) lines.push(state.finishedAt
|
|
787
|
+
? `${statusIcon(state.status)} No agents running · ${terminalWorkflowLabel(state.status)}`
|
|
788
|
+
: '⧖ Waiting for the next dispatch');
|
|
761
789
|
return { lines, running, waiting };
|
|
762
790
|
}
|
|
763
791
|
|
|
792
|
+
function terminalWorkflowLabel(status) {
|
|
793
|
+
if (status === 'completed') return 'workflow finished';
|
|
794
|
+
if (status === 'completed_with_concerns') return 'workflow finished with concerns';
|
|
795
|
+
if (status === 'blocked') return 'workflow stopped with blockers';
|
|
796
|
+
if (status === 'failed') return 'workflow failed';
|
|
797
|
+
if (status === 'cancelled') return 'workflow cancelled';
|
|
798
|
+
if (status === 'interrupted') return 'workflow interrupted';
|
|
799
|
+
return 'workflow stopped';
|
|
800
|
+
}
|
|
801
|
+
|
|
764
802
|
function workflowNextLines(model, width) {
|
|
765
803
|
const { state, orchestrator } = model;
|
|
766
|
-
if (state.finishedAt)
|
|
804
|
+
if (state.finishedAt) {
|
|
805
|
+
const next = state.status === 'completed' || state.status === 'completed_with_concerns'
|
|
806
|
+
? 'result ready'
|
|
807
|
+
: state.status === 'blocked' ? 'review blockers and partial work'
|
|
808
|
+
: state.status === 'failed' ? 'inspect the failure before using partial work'
|
|
809
|
+
: state.status === 'cancelled' ? 'review any partial work'
|
|
810
|
+
: state.status === 'interrupted' ? 'resume the workflow or inspect partial work'
|
|
811
|
+
: 'inspect the workflow result';
|
|
812
|
+
const label = terminalWorkflowLabel(state.status);
|
|
813
|
+
return [truncate(`${statusIcon(state.status)} ${label[0].toUpperCase()}${label.slice(1)} · ${next}`, width)];
|
|
814
|
+
}
|
|
767
815
|
const ledger = state.actionLedger ?? [];
|
|
768
816
|
const pending = ledger.find((action) => action.id !== 'scout'
|
|
769
817
|
&& action.id !== orchestrator.actionId
|
|
@@ -871,7 +919,7 @@ function plannerDisplayStatus(model) {
|
|
|
871
919
|
const { orchestrator, state } = model;
|
|
872
920
|
if (state.finishedAt) return state.status === 'completed' ? 'Completed' : humanStatus(state.status);
|
|
873
921
|
if (orchestrator.active) return 'Planning next actions';
|
|
874
|
-
const workers = Object.values(state.activeAgents ?? {}).filter((agent) => agent.stepId !== orchestrator.actionId);
|
|
922
|
+
const workers = Object.values(state.activeAgents ?? {}).filter((agent) => agent.stepId !== orchestrator.actionId && isLiveAgent(agent));
|
|
875
923
|
if (workers.length) return 'Waiting for workers';
|
|
876
924
|
if (orchestrator.status === 'reviewing evidence') return 'Reviewing evidence';
|
|
877
925
|
return humanStatus(orchestrator.status);
|
|
@@ -903,7 +951,7 @@ function orchestratorDetailLines(model, width, spinnerFrame, { verbose = false }
|
|
|
903
951
|
const workerAttempts = (state.attempts ?? []).filter((attempt) => attempt.actionId !== orchestrator.actionId);
|
|
904
952
|
const completedWorkers = workerAttempts.filter((attempt) => TERMINAL_ACTIONS.has(attempt.status)).length;
|
|
905
953
|
const activeWorkers = Object.values(state.activeAgents ?? {})
|
|
906
|
-
.filter((agent) => agent.stepId !== orchestrator.actionId);
|
|
954
|
+
.filter((agent) => agent.stepId !== orchestrator.actionId && isLiveAgent(agent));
|
|
907
955
|
const nextActions = latestDecision?.actions?.map((action) => action.id).filter(Boolean) ?? [];
|
|
908
956
|
const stateLabel = active
|
|
909
957
|
? 'Choosing the next smallest useful action'
|
package/src/workflow/decision.js
CHANGED
|
@@ -111,6 +111,8 @@ export function normalizeDecisionProposal(proposal) {
|
|
|
111
111
|
|
|
112
112
|
export function validateDecisionProposal(proposal, {
|
|
113
113
|
knownActionIds = [],
|
|
114
|
+
requiredRequirementIds = [],
|
|
115
|
+
completedRequirementIds = [],
|
|
114
116
|
closedPhases = [],
|
|
115
117
|
currentActionCount = 0,
|
|
116
118
|
maxActions = 100,
|
|
@@ -161,6 +163,8 @@ export function validateDecisionProposal(proposal, {
|
|
|
161
163
|
}
|
|
162
164
|
|
|
163
165
|
const known = new Set(knownActionIds);
|
|
166
|
+
const requiredRequirements = new Set(requiredRequirementIds);
|
|
167
|
+
const completedRequirements = new Set(completedRequirementIds);
|
|
164
168
|
const proposedIds = new Set(safeActions.map((action) => action?.id).filter((id) => typeof id === 'string'));
|
|
165
169
|
const closed = new Set(closedPhases);
|
|
166
170
|
const proposed = new Set();
|
|
@@ -261,6 +265,21 @@ export function validateDecisionProposal(proposal, {
|
|
|
261
265
|
}
|
|
262
266
|
}
|
|
263
267
|
if (action.type === 'verify') {
|
|
268
|
+
if (requiredRequirements.size) {
|
|
269
|
+
if (!Array.isArray(action.covers) || action.covers.length === 0) {
|
|
270
|
+
issues.push(`${at}.covers must name one or more intent requirement IDs`);
|
|
271
|
+
} else {
|
|
272
|
+
const seen = new Set();
|
|
273
|
+
for (const requirementId of action.covers) {
|
|
274
|
+
if (typeof requirementId !== 'string' || !requiredRequirements.has(requirementId)) {
|
|
275
|
+
issues.push(`${at}.covers references unknown requirement "${requirementId}"`);
|
|
276
|
+
} else if (seen.has(requirementId)) {
|
|
277
|
+
issues.push(`${at}.covers repeats requirement "${requirementId}"`);
|
|
278
|
+
}
|
|
279
|
+
seen.add(requirementId);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
264
283
|
if (action.review == null && action.reviewScope === 'repository') {
|
|
265
284
|
// Normalized zero-dependency audit: no artifact to review.
|
|
266
285
|
} else if (typeof action.review !== 'string') {
|
|
@@ -274,6 +293,9 @@ export function validateDecisionProposal(proposal, {
|
|
|
274
293
|
}
|
|
275
294
|
}
|
|
276
295
|
}
|
|
296
|
+
if (action.type !== 'verify' && action.covers != null) {
|
|
297
|
+
issues.push(`${at}.covers is only valid on verify actions`);
|
|
298
|
+
}
|
|
277
299
|
}
|
|
278
300
|
if (proposedItems > maxItemsPerExpansion) {
|
|
279
301
|
issues.push(`proposal has ${proposedItems} fanout items, exceeding maxItemsPerExpansion=${maxItemsPerExpansion}`);
|
|
@@ -300,6 +322,15 @@ export function validateDecisionProposal(proposal, {
|
|
|
300
322
|
};
|
|
301
323
|
for (const id of proposed) visit(id);
|
|
302
324
|
|
|
325
|
+
if (requiredRequirements.size && (proposal.completion || proposal.decision === 'complete')) {
|
|
326
|
+
const covered = new Set(completedRequirements);
|
|
327
|
+
for (const action of safeActions.filter((entry) => entry?.type === 'verify')) {
|
|
328
|
+
for (const requirementId of action.covers ?? []) covered.add(requirementId);
|
|
329
|
+
}
|
|
330
|
+
const missing = [...requiredRequirements].filter((id) => !covered.has(id));
|
|
331
|
+
if (missing.length) issues.push(`completion lacks verifier coverage for ${missing.join(', ')}`);
|
|
332
|
+
}
|
|
333
|
+
|
|
303
334
|
if (issues.length) throw new DecisionValidationError(issues);
|
|
304
335
|
return {
|
|
305
336
|
schemaVersion: DECISION_SCHEMA_VERSION,
|
package/src/workflow/goal.js
CHANGED
|
@@ -16,7 +16,7 @@ export const PLANNER_RULES_SECTION = [
|
|
|
16
16
|
'5. For unknown items, create discovery ending with RETURN ONLY a JSON object containing an items array, then data-driven fan-out via itemsFrom outputs.<id>.outFile or outputs.<id>.data.<field>; the runtime extracts the list, retrying once read-only if needed.',
|
|
17
17
|
'6. Put outputSchema only on a worker whose object a LATER action reads via itemsFrom or outputs.<id>.data.<field>, and tell it to RETURN ONLY the object; a prose report or any answer with fenced JSON gets no schema: the runtime parses the last {...} of the text, so a schema on prose costs a retry and a planner turn.',
|
|
18
18
|
'7. Put verify.repair on every verify. ok:false means unusable: the goal\'s acceptance command fails, a deliverable is missing, or the answer is nonsense; everything else is a concern under ok:true (style, cosmetic mismatches, later-scheduled work, files other actions changed, and any process rule the goal does not state such as append-only or tests untouched). ok:false is repaired and re-checked inside the program; the repair edits only its unit\'s files and cannot rewrite the answer under review, so report a wrong claim as a concern with the true value.',
|
|
19
|
-
'8. Add completion with all-actions-ok
|
|
19
|
+
'8. Every verify declares covers:["R1",...] from intent.requirements. Before completion, successful verifies must cover every requirement with runtime evidence against the ORIGINAL goal, not a reduced plan. Add completion with all-actions-ok when that verified program finishes the goal; return complete rather than adding polish. The program\'s LAST worker must be covered by a successful verify. Never proceed, never ask the user, and stop only for a concrete unresolved blocker.',
|
|
20
20
|
'9. Budgets (agents, duration, expansion rounds) are advisory targets, never hard stops; the dispatch budget counts this planner call plus workers, verifiers and retries. Converge as targets approach: skip optional work; exceed a target only for one essential action or a required verification.',
|
|
21
21
|
'10. Never propose pool, model, addDir, taskFile or unbounded work: routing is the runtime\'s. Set lane (analyze to read or judge, build to edit, chore for mechanical steps) and effort (low for checks and mechanical edits, high where judgement decides) per action or repair; they pick the model tier (unset: build, medium).',
|
|
22
22
|
'Shared tree: run substantial DISJOINT units concurrently; order shared files after feeders with dependsOn. Workers and unit verifies use focused commands, never the full suite while siblings write; run the suite once in the final verify after edits and repairs. Reuse it unless code changed. operatorSteering applies within the original intent and cannot weaken verification or expand authority.',
|
|
@@ -24,12 +24,47 @@ export const PLANNER_RULES_SECTION = [
|
|
|
24
24
|
|
|
25
25
|
export const PLANNER_EXAMPLES_SECTION = [
|
|
26
26
|
'Action shapes:',
|
|
27
|
-
'[{"type":"run","phase":"implement","prompt":"..."},{"type":"run","phase":"inventory","lane":"chore","effort":"low","prompt":"... RETURN ONLY a JSON object.","outputSchema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"string"}}},"required":["items"]}},{"type":"fanout","phase":"fix","items":["alpha"],"stepTemplate":{"prompt":"Handle {{item}}."}},{"type":"verify","phase":"verify","lane":"analyze","prompt":"Check the artifact.","repair":{"prompt":"Fix rejected concerns.","maxRounds":1}}]',
|
|
27
|
+
'[{"type":"run","phase":"implement","prompt":"..."},{"type":"run","phase":"inventory","lane":"chore","effort":"low","prompt":"... RETURN ONLY a JSON object.","outputSchema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"string"}}},"required":["items"]}},{"type":"fanout","phase":"fix","items":["alpha"],"stepTemplate":{"prompt":"Handle {{item}}."}},{"type":"verify","phase":"verify","lane":"analyze","covers":["R1"],"prompt":"Check the artifact.","repair":{"prompt":"Fix rejected concerns.","maxRounds":1}}]',
|
|
28
28
|
'Complete program (tests depend on fix, not verify-fix, so both run at once):',
|
|
29
|
-
'{"actions":[{"id":"discover","type":"run","phase":"discover","
|
|
29
|
+
'{"actions":[{"id":"discover","type":"run","phase":"discover","prompt":"In /abs/repo list modules needing work; RETURN ONLY a JSON object with an items array.","outputSchema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"string"}}},"required":["items"]}},{"id":"fix","type":"fanout","phase":"fix","itemsFrom":"outputs.discover.data.items","dependsOn":["discover"],"stepTemplate":{"prompt":"In /abs/repo edit only src/{{item}}.js and run node --test tests/{{item}}.test.js."}},{"id":"verify-fix","type":"verify","phase":"verify","covers":["R1"],"dependsOn":["fix"],"prompt":"Check each fixed module against the spec.","repair":{"prompt":"Fix rejected concerns in /abs/repo and rerun that module\'s test.","maxRounds":2}},{"id":"tests","type":"fanout","phase":"tests","itemsFrom":"outputs.discover.data.items","dependsOn":["fix"],"stepTemplate":{"prompt":"In /abs/repo write only tests/{{item}}.guards.test.js and run node --test on it."}},{"id":"verify-tests","type":"verify","phase":"verify","covers":["R1"],"dependsOn":["tests"],"prompt":"Check the new tests are non-vacuous.","repair":{"prompt":"Fix rejected tests in /abs/repo.","maxRounds":1}},{"id":"verify-suite","type":"verify","phase":"verify","covers":["R1"],"dependsOn":["verify-fix","verify-tests"],"prompt":"Run npm test in /abs/repo.","repair":{"prompt":"Fix the suite failure in /abs/repo and rerun it.","maxRounds":1}},{"id":"report","type":"run","phase":"report","dependsOn":["verify-suite"],"prompt":"In /abs/repo list each changed file with a reason and quote the suite tail; plain markdown."},{"id":"verify-report","type":"verify","phase":"report","covers":["R1"],"dependsOn":["report"],"prompt":"Check each claim against git status and a fresh suite run; a wrong number is a concern with the true value.","repair":{"prompt":"Fix any real repository defect in /abs/repo.","maxRounds":1}}],"completion":{"when":"all-actions-ok","reason":"Fix, tests, suite and report are each verified."}}',
|
|
30
30
|
'Rules the validator enforces: action type is run, fanout, or verify; fanout has stepTemplate and either items or itemsFrom; verify.review, when given, is outputs.<id>.outFile; ids are unique across the whole run, finished and failed actions included; dependsOn names existing or proposed actions; lane is analyze|build|chore and effort is low|medium|high; runtime-owned fields are rejected.',
|
|
31
31
|
].join('\n');
|
|
32
32
|
|
|
33
|
+
function compactRequirement(text, max = 600) {
|
|
34
|
+
const compact = String(text ?? '').replace(/\s+/g, ' ').trim();
|
|
35
|
+
return compact.length > max ? `${compact.slice(0, max - 1)}…` : compact;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function extractGoalRequirements(goal) {
|
|
39
|
+
const text = String(goal ?? '').trim();
|
|
40
|
+
if (!text) return [];
|
|
41
|
+
const lines = text.split(/\r?\n/);
|
|
42
|
+
const numbered = [];
|
|
43
|
+
let current = null;
|
|
44
|
+
for (const raw of lines) {
|
|
45
|
+
const line = raw.trim();
|
|
46
|
+
const match = /^(\d+)[.)]\s+(.+)$/.exec(line);
|
|
47
|
+
if (match) {
|
|
48
|
+
if (current) numbered.push(current);
|
|
49
|
+
current = { number: match[1], text: match[2] };
|
|
50
|
+
} else if (current && line && !/^(?:finish with|before completion|acceptance(?: criteria)?|finally)\b/i.test(line)) {
|
|
51
|
+
current.text += ` ${line}`;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (current) numbered.push(current);
|
|
55
|
+
const requirements = numbered.map((entry, index) => ({
|
|
56
|
+
id: `R${index + 1}`,
|
|
57
|
+
text: compactRequirement(entry.text),
|
|
58
|
+
}));
|
|
59
|
+
const finalLines = lines
|
|
60
|
+
.map((line) => line.trim())
|
|
61
|
+
.filter((line) => /^(?:finish with|before completion|acceptance(?: criteria)?|finally)\b/i.test(line));
|
|
62
|
+
if (finalLines.length) {
|
|
63
|
+
requirements.push({ id: `R${requirements.length + 1}`, text: compactRequirement(finalLines.join(' ')) });
|
|
64
|
+
}
|
|
65
|
+
return requirements.length ? requirements : [{ id: 'R1', text: compactRequirement(text) }];
|
|
66
|
+
}
|
|
67
|
+
|
|
33
68
|
export const AUTONOMOUS_ORCHESTRATOR_PROMPT = [
|
|
34
69
|
'You are the autonomous orchestrator for the user goal in the durable workflow context.',
|
|
35
70
|
'This is a control-plane decision thread. Compile the goal into a complete workflow program, own decomposition through independent verification, and use only the supplied context. Do not invoke Bullswarm, run shell commands, call tools, modify files, or ask the user to steer routine execution.',
|
|
@@ -133,6 +168,7 @@ export function buildGoalWorkflow({
|
|
|
133
168
|
description: 'Autonomous goal-driven workflow generated by Bullswarm.',
|
|
134
169
|
intent: {
|
|
135
170
|
goal: goal.trim(),
|
|
171
|
+
requirements: extractGoalRequirements(goal),
|
|
136
172
|
cwd: targetDir,
|
|
137
173
|
autonomous: true,
|
|
138
174
|
requestedOrchestrator: orchestrator ?? 'auto',
|
package/src/workflow/runner.js
CHANGED
|
@@ -505,7 +505,19 @@ export function verifiesWorker(verify, worker) {
|
|
|
505
505
|
&& new RegExp(`^${verify.id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}-repair-\\d+$`).test(worker.id);
|
|
506
506
|
}
|
|
507
507
|
|
|
508
|
-
export function
|
|
508
|
+
export function completedRequirementIds(dynamicActions, outputs = {}) {
|
|
509
|
+
const completed = new Set();
|
|
510
|
+
for (const action of dynamicActions) {
|
|
511
|
+
if (action.kind !== 'verify' || action.status !== 'succeeded' || !actionOutputOk(action, outputs)) continue;
|
|
512
|
+
for (const id of action.covers ?? []) {
|
|
513
|
+
const evidence = outputs?.[action.id]?.verify?.requirements?.[id];
|
|
514
|
+
if (evidence?.ok === true && typeof evidence.evidence === 'string' && evidence.evidence.trim()) completed.add(id);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return [...completed];
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export function completionEvidenceGaps(dynamicActions, policy, outputs = {}, requirements = []) {
|
|
509
521
|
const missing = [];
|
|
510
522
|
const successfulWorkers = dynamicActions.filter(
|
|
511
523
|
(action) => action.kind !== 'verify' && action.status === 'succeeded'
|
|
@@ -526,9 +538,20 @@ export function completionEvidenceGaps(dynamicActions, policy, outputs = {}) {
|
|
|
526
538
|
? `a successful verification of latest worker ${latestSuccessfulWorker.id}`
|
|
527
539
|
: 'a successful verification action');
|
|
528
540
|
}
|
|
541
|
+
const covered = new Set(completedRequirementIds(dynamicActions, outputs));
|
|
542
|
+
const missingRequirements = requirements.filter((entry) => !covered.has(entry.id)).map((entry) => entry.id);
|
|
543
|
+
if (missingRequirements.length) missing.push(`verified original-goal coverage for ${missingRequirements.join(', ')}`);
|
|
529
544
|
return missing;
|
|
530
545
|
}
|
|
531
546
|
|
|
547
|
+
function successfulVerifyConcerns(dynamicActions, outputs = {}) {
|
|
548
|
+
return [...new Set(dynamicActions
|
|
549
|
+
.filter((action) => action.kind === 'verify' && action.status === 'succeeded' && actionOutputOk(action, outputs))
|
|
550
|
+
.flatMap((action) => Array.isArray(outputs?.[action.id]?.verify?.concerns)
|
|
551
|
+
? outputs[action.id].verify.concerns.map(String) : [])
|
|
552
|
+
.filter(Boolean))];
|
|
553
|
+
}
|
|
554
|
+
|
|
532
555
|
function plannerCorrectionAllowance(settings) {
|
|
533
556
|
const value = Number(settings?.maxPlannerCorrections);
|
|
534
557
|
if (!Number.isFinite(value)) return 2;
|
|
@@ -553,6 +576,7 @@ function terminalPlannerOutcome(state, gate, reason) {
|
|
|
553
576
|
dynamicActions,
|
|
554
577
|
state.orchestration?.completionPolicy,
|
|
555
578
|
state.outputs,
|
|
579
|
+
state.intent?.requirements ?? [],
|
|
556
580
|
);
|
|
557
581
|
for (const action of observedActions.filter((entry) => entry.kind === 'verify')) {
|
|
558
582
|
const output = state.outputs?.[action.id];
|
|
@@ -964,6 +988,11 @@ async function runDecisionLoop({ runtime, gate, phase, state, retryAttempts }) {
|
|
|
964
988
|
}
|
|
965
989
|
proposal = validateDecisionProposal(normalizedProposal, {
|
|
966
990
|
knownActionIds: (state.plan?.actions ?? []).map((action) => action.id),
|
|
991
|
+
requiredRequirementIds: (state.intent?.requirements ?? []).map((entry) => entry.id),
|
|
992
|
+
completedRequirementIds: completedRequirementIds(
|
|
993
|
+
(state.actionLedger ?? []).filter((action) => action.parentId === gate.id),
|
|
994
|
+
state.outputs,
|
|
995
|
+
),
|
|
967
996
|
closedPhases: (state.plan?.actions ?? [])
|
|
968
997
|
.filter((action) => action.source === 'planner')
|
|
969
998
|
.map((action) => action.definition?.phase)
|
|
@@ -1071,7 +1100,7 @@ async function runDecisionLoop({ runtime, gate, phase, state, retryAttempts }) {
|
|
|
1071
1100
|
if (proposal.decision === 'complete') {
|
|
1072
1101
|
const policy = state.orchestration?.completionPolicy;
|
|
1073
1102
|
const dynamicActions = (state.actionLedger ?? []).filter((action) => action.parentId === gate.id);
|
|
1074
|
-
const missing = completionEvidenceGaps(dynamicActions, policy, state.outputs);
|
|
1103
|
+
const missing = completionEvidenceGaps(dynamicActions, policy, state.outputs, state.intent?.requirements ?? []);
|
|
1075
1104
|
if (missing.length) {
|
|
1076
1105
|
const why = `autonomous completion rejected: missing ${missing.join(' and ')}`;
|
|
1077
1106
|
decision.accepted = false;
|
|
@@ -1081,16 +1110,18 @@ async function runDecisionLoop({ runtime, gate, phase, state, retryAttempts }) {
|
|
|
1081
1110
|
runtime.persist();
|
|
1082
1111
|
continue;
|
|
1083
1112
|
}
|
|
1113
|
+
const concerns = successfulVerifyConcerns(dynamicActions, state.outputs);
|
|
1114
|
+
const terminalStatus = concerns.length ? 'completed_with_concerns' : 'completed';
|
|
1084
1115
|
state.outcome = {
|
|
1085
|
-
status:
|
|
1116
|
+
status: terminalStatus,
|
|
1086
1117
|
verified: true,
|
|
1087
1118
|
bestEffort: false,
|
|
1088
1119
|
reason: proposal.reason,
|
|
1089
|
-
concerns
|
|
1120
|
+
concerns,
|
|
1090
1121
|
deliveryActionId: dynamicActions.filter((action) =>
|
|
1091
1122
|
action.kind !== 'verify' && actionOutputOk(action, state.outputs)).at(-1)?.id ?? null,
|
|
1092
1123
|
};
|
|
1093
|
-
return { ok: true, why: proposal.reason, complete: true, decision: proposal };
|
|
1124
|
+
return { ok: true, why: proposal.reason, complete: true, terminalStatus, decision: proposal };
|
|
1094
1125
|
}
|
|
1095
1126
|
if (proposal.decision === 'proceed') return { ok: true, why: proposal.reason, decision: proposal };
|
|
1096
1127
|
if (proposal.decision === 'stop') {
|
|
@@ -1152,7 +1183,12 @@ async function runDecisionLoop({ runtime, gate, phase, state, retryAttempts }) {
|
|
|
1152
1183
|
.filter((entry) => !ledgerById.has(entry.id) || !actionOutputOk(ledgerById.get(entry.id), state.outputs))
|
|
1153
1184
|
.map((entry) => entry.id);
|
|
1154
1185
|
const dynamicActions = (state.actionLedger ?? []).filter((action) => action.parentId === gate.id);
|
|
1155
|
-
const gaps = failing.length ? [] : completionEvidenceGaps(
|
|
1186
|
+
const gaps = failing.length ? [] : completionEvidenceGaps(
|
|
1187
|
+
dynamicActions,
|
|
1188
|
+
state.orchestration?.completionPolicy,
|
|
1189
|
+
state.outputs,
|
|
1190
|
+
state.intent?.requirements ?? [],
|
|
1191
|
+
);
|
|
1156
1192
|
// Pending operator steering blocks self-completion: the documented
|
|
1157
1193
|
// contract is delivery at the next planner gate, so a clean program
|
|
1158
1194
|
// returns to the planner (which delivers the steer) instead of
|
|
@@ -1182,19 +1218,21 @@ async function runDecisionLoop({ runtime, gate, phase, state, retryAttempts }) {
|
|
|
1182
1218
|
gateId: gate.id, sequence: auto.sequence, programSequence: decision.sequence,
|
|
1183
1219
|
actions: programActions.map((entry) => entry.id), reason,
|
|
1184
1220
|
});
|
|
1221
|
+
const concerns = successfulVerifyConcerns(dynamicActions, state.outputs);
|
|
1222
|
+
const terminalStatus = concerns.length ? 'completed_with_concerns' : 'completed';
|
|
1185
1223
|
state.outcome = {
|
|
1186
|
-
status:
|
|
1224
|
+
status: terminalStatus,
|
|
1187
1225
|
verified: true,
|
|
1188
1226
|
bestEffort: false,
|
|
1189
1227
|
reason,
|
|
1190
|
-
concerns
|
|
1228
|
+
concerns,
|
|
1191
1229
|
deliveryActionId: dynamicActions.filter((action) =>
|
|
1192
1230
|
action.kind !== 'verify' && actionOutputOk(action, state.outputs)).at(-1)?.id ?? null,
|
|
1193
1231
|
source: 'program-completion',
|
|
1194
1232
|
};
|
|
1195
1233
|
state.outputs[gate.id] = { ...state.outputs[gate.id], ok: true, why: reason, autoCompleted: true };
|
|
1196
1234
|
runtime.persist();
|
|
1197
|
-
return { ok: true, why: reason, complete: true, decision: { decision: 'complete', reason, actions: [], completion: proposal.completion } };
|
|
1235
|
+
return { ok: true, why: reason, complete: true, terminalStatus, decision: { decision: 'complete', reason, actions: [], completion: proposal.completion } };
|
|
1198
1236
|
}
|
|
1199
1237
|
if (pendingSteering.length) {
|
|
1200
1238
|
runtime.emit('decision.completion_deferred', {
|
package/src/workflow/runtime.js
CHANGED
|
@@ -56,6 +56,78 @@ export const BURST_WAIT_GRACE_MS = 10 * 60_000;
|
|
|
56
56
|
export const BURST_WAIT_UNKNOWN_RESET_MS = 5 * 3600_000;
|
|
57
57
|
export const QUOTA_POLL_MS = 60_000;
|
|
58
58
|
|
|
59
|
+
export function enforceVerifyRequirementCoverage(parsed, requiredCoverage = []) {
|
|
60
|
+
if (!parsed || !requiredCoverage.length) return { parsed, concerns: [] };
|
|
61
|
+
const normalized = { ...parsed };
|
|
62
|
+
normalized.requirements = parsed.requirements && typeof parsed.requirements === 'object'
|
|
63
|
+
? parsed.requirements : {};
|
|
64
|
+
const concerns = [];
|
|
65
|
+
for (const requirement of requiredCoverage) {
|
|
66
|
+
const evidence = normalized.requirements[requirement.id];
|
|
67
|
+
if (!evidence || evidence.ok !== true || typeof evidence.evidence !== 'string' || !evidence.evidence.trim()) {
|
|
68
|
+
concerns.push(`${requirement.id} lacks passing, specific verification evidence: ${requirement.text}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (concerns.length) {
|
|
72
|
+
normalized.ok = false;
|
|
73
|
+
normalized.concerns = [...new Set([...(Array.isArray(parsed.concerns) ? parsed.concerns : []), ...concerns])];
|
|
74
|
+
}
|
|
75
|
+
return { parsed: normalized, concerns };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function parseVerifyJsonText(text) {
|
|
79
|
+
const source = String(text ?? '').trim();
|
|
80
|
+
const starts = [];
|
|
81
|
+
for (let index = 0; index < source.length; index++) {
|
|
82
|
+
if (source[index] === '{') starts.push(index);
|
|
83
|
+
}
|
|
84
|
+
let parseError = null;
|
|
85
|
+
for (const start of starts) {
|
|
86
|
+
const suffix = source.slice(start);
|
|
87
|
+
const candidates = [];
|
|
88
|
+
const lastClose = suffix.lastIndexOf('}');
|
|
89
|
+
if (lastClose > 0) candidates.push(suffix.slice(0, lastClose + 1));
|
|
90
|
+
const repaired = closeTruncatedJsonObject(suffix);
|
|
91
|
+
if (repaired && !candidates.includes(repaired)) candidates.push(repaired);
|
|
92
|
+
for (const candidate of candidates) {
|
|
93
|
+
try {
|
|
94
|
+
const parsed = JSON.parse(candidate);
|
|
95
|
+
if (parsed && typeof parsed === 'object'
|
|
96
|
+
&& typeof parsed.ok === 'boolean'
|
|
97
|
+
&& Array.isArray(parsed.concerns)
|
|
98
|
+
&& typeof parsed.summary === 'string') {
|
|
99
|
+
return { parsed, parseError: null };
|
|
100
|
+
}
|
|
101
|
+
} catch (err) {
|
|
102
|
+
parseError = err.message;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { parsed: null, parseError };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function closeTruncatedJsonObject(source) {
|
|
110
|
+
const stack = [];
|
|
111
|
+
let quoted = false;
|
|
112
|
+
let escaped = false;
|
|
113
|
+
for (const char of source) {
|
|
114
|
+
if (quoted) {
|
|
115
|
+
if (escaped) escaped = false;
|
|
116
|
+
else if (char === '\\') escaped = true;
|
|
117
|
+
else if (char === '"') quoted = false;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (char === '"') quoted = true;
|
|
121
|
+
else if (char === '{') stack.push('}');
|
|
122
|
+
else if (char === '[') stack.push(']');
|
|
123
|
+
else if (char === '}' || char === ']') {
|
|
124
|
+
if (stack.pop() !== char) return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (quoted || !stack.length) return null;
|
|
128
|
+
return `${source}${stack.reverse().join('')}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
59
131
|
export function plannerBudgetContext(budget = {}) {
|
|
60
132
|
const dispatchesUsedBeforePlanner = Number(budget.dispatchesUsed ?? 0);
|
|
61
133
|
const rawTarget = budget.dispatchTarget ?? budget.dispatchLimit;
|
|
@@ -239,6 +311,7 @@ export class WorkflowRuntime {
|
|
|
239
311
|
parentId: opts.itemIndex == null ? (step.parentId ?? null) : step.id,
|
|
240
312
|
kind: opts.itemIndex == null ? step.type : 'run',
|
|
241
313
|
dependsOn: [...(step.dependsOn ?? [])],
|
|
314
|
+
...(step.type === 'verify' && Array.isArray(step.covers) ? { covers: [...step.covers] } : {}),
|
|
242
315
|
status: 'queued',
|
|
243
316
|
phase: opts.phase ?? null,
|
|
244
317
|
item: opts.item,
|
|
@@ -1127,6 +1200,22 @@ export class WorkflowRuntime {
|
|
|
1127
1200
|
})();
|
|
1128
1201
|
|
|
1129
1202
|
const reverify = step._reverify && typeof step._reverify === 'object' ? step._reverify : null;
|
|
1203
|
+
const requiredCoverage = Array.isArray(step.covers)
|
|
1204
|
+
? step.covers.map((id) => this.state.intent?.requirements?.find((entry) => entry.id === id)).filter(Boolean)
|
|
1205
|
+
: [];
|
|
1206
|
+
const coverageVerdictShape = requiredCoverage.length
|
|
1207
|
+
? JSON.stringify(Object.fromEntries(requiredCoverage.map((entry) => [entry.id, { ok: true, evidence: 'specific evidence' }])))
|
|
1208
|
+
: null;
|
|
1209
|
+
const coverageContract = requiredCoverage.length ? [
|
|
1210
|
+
'',
|
|
1211
|
+
'ORIGINAL USER GOAL (runtime-owned; it overrides any narrower planner or worker scope):',
|
|
1212
|
+
this.state.intent?.goal ?? '(goal unavailable)',
|
|
1213
|
+
'',
|
|
1214
|
+
'REQUIRED COVERAGE FOR THIS VERIFIER:',
|
|
1215
|
+
...requiredCoverage.map((entry) => `- ${entry.id}: ${entry.text}`),
|
|
1216
|
+
'A missing explicitly required deliverable is unusable and MUST produce ok:false.',
|
|
1217
|
+
'For every ID above, include requirements.<ID> = {"ok":true|false,"evidence":"specific file, symbol, command, or observed result"}. Empty or generic evidence is invalid.',
|
|
1218
|
+
] : [];
|
|
1130
1219
|
const reviewInstructions = [
|
|
1131
1220
|
step.prompt ?? 'You are a skeptical reviewer. Independently inspect the work and its current repository state.',
|
|
1132
1221
|
...(reverify ? [
|
|
@@ -1139,9 +1228,12 @@ export class WorkflowRuntime {
|
|
|
1139
1228
|
] : []),
|
|
1140
1229
|
'',
|
|
1141
1230
|
'Acceptance standard (runtime-owned; it overrides any stricter rule in the instructions above): ok:false means the work is unusable: its acceptance command fails, a required deliverable is missing, or the answer is nonsense or contradicts its own evidence. Everything else goes in concerns under ok:true: style, wording, scope, cosmetic mismatches, process rules the goal never stated (append-only, diff size), and files changed by other actions that share this working tree, which are never this unit\'s defect. Never reject for something the goal does not require.',
|
|
1231
|
+
...coverageContract,
|
|
1142
1232
|
'',
|
|
1143
1233
|
'RETURN ONLY a single JSON object of the form',
|
|
1144
|
-
|
|
1234
|
+
requiredCoverage.length
|
|
1235
|
+
? `{"ok": <true|false>, "concerns": [<string>...], "summary": <string>, "requirements": ${coverageVerdictShape}}.`
|
|
1236
|
+
: '{"ok": <true|false>, "concerns": [<string>...], "summary": <string>}.',
|
|
1145
1237
|
'No prose and no markdown fences. Set ok by the acceptance standard above: true unless the work is unusable.',
|
|
1146
1238
|
].join('\n');
|
|
1147
1239
|
// Only the reviewer INSTRUCTIONS are a template. The review target is a
|
|
@@ -1184,13 +1276,7 @@ export class WorkflowRuntime {
|
|
|
1184
1276
|
const parseVerdictFile = (path) => {
|
|
1185
1277
|
try {
|
|
1186
1278
|
const out = readFileSync(path, 'utf8');
|
|
1187
|
-
|
|
1188
|
-
const end = out.lastIndexOf('}');
|
|
1189
|
-
if (start >= 0 && end > start) {
|
|
1190
|
-
const j = JSON.parse(out.slice(start, end + 1));
|
|
1191
|
-
if (j && typeof j === 'object') return { parsed: j, parseError: null };
|
|
1192
|
-
}
|
|
1193
|
-
return { parsed: null, parseError: null };
|
|
1279
|
+
return parseVerifyJsonText(out);
|
|
1194
1280
|
} catch (err) {
|
|
1195
1281
|
return { parsed: null, parseError: err.message };
|
|
1196
1282
|
}
|
|
@@ -1204,16 +1290,22 @@ export class WorkflowRuntime {
|
|
|
1204
1290
|
// same economics that give outputSchema its single retry.
|
|
1205
1291
|
if (verdict.ok && !parsed) {
|
|
1206
1292
|
this.emit('verify.verdict_retry', { actionId: step.id, why: parseError ?? 'no JSON object in verdict' });
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1293
|
+
let retryVerdict;
|
|
1294
|
+
try {
|
|
1295
|
+
retryVerdict = await this.dispatch(step, [
|
|
1296
|
+
taskText,
|
|
1297
|
+
'',
|
|
1298
|
+
`Your previous reply could not be used: ${parseError ?? 'it did not contain a parseable JSON object'}.`,
|
|
1299
|
+
`Do the review again if needed, then RETURN ONLY the single JSON object ${requiredCoverage.length ? `{"ok":true,"concerns":[],"summary":"...","requirements":${coverageVerdictShape}}` : '{"ok": <true|false>, "concerns": [...], "summary": "..."}'} — no prose, no markdown fences, nothing before "{" or after "}".`,
|
|
1300
|
+
].join('\n'), targetDir, paths, {
|
|
1301
|
+
escalate: this.state.settings.escalateOnFail !== false,
|
|
1302
|
+
retryAttempts: 0,
|
|
1303
|
+
phase: opts.phase,
|
|
1304
|
+
});
|
|
1305
|
+
} finally {
|
|
1306
|
+
delete this.state.activeAgents?.[step.id];
|
|
1307
|
+
this.persist();
|
|
1308
|
+
}
|
|
1217
1309
|
if (retryVerdict.ok) {
|
|
1218
1310
|
verdict = retryVerdict;
|
|
1219
1311
|
finalPaths.taskFile = retryVerdict.taskFile ?? finalPaths.taskFile;
|
|
@@ -1222,6 +1314,7 @@ export class WorkflowRuntime {
|
|
|
1222
1314
|
}
|
|
1223
1315
|
}
|
|
1224
1316
|
|
|
1317
|
+
({ parsed } = enforceVerifyRequirementCoverage(parsed, requiredCoverage));
|
|
1225
1318
|
const ok = verdict.ok && !!parsed && parsed.ok === true;
|
|
1226
1319
|
const verifyVerdict = {
|
|
1227
1320
|
ok,
|