@cat-factory/orchestration 0.36.5 → 0.37.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/dist/container.d.ts +6 -4
- package/dist/container.d.ts.map +1 -1
- package/dist/container.js.map +1 -1
- package/dist/modules/artifacts/artifactRetention.d.ts +8 -5
- package/dist/modules/artifacts/artifactRetention.d.ts.map +1 -1
- package/dist/modules/artifacts/artifactRetention.js +10 -4
- package/dist/modules/artifacts/artifactRetention.js.map +1 -1
- package/dist/modules/execution/ExecutionService.d.ts +35 -63
- package/dist/modules/execution/ExecutionService.d.ts.map +1 -1
- package/dist/modules/execution/ExecutionService.js +275 -260
- package/dist/modules/execution/ExecutionService.js.map +1 -1
- package/dist/modules/execution/StepGraph.d.ts +70 -0
- package/dist/modules/execution/StepGraph.d.ts.map +1 -0
- package/dist/modules/execution/StepGraph.js +124 -0
- package/dist/modules/execution/StepGraph.js.map +1 -0
- package/dist/modules/execution/VisualConfirmationController.d.ts +13 -4
- package/dist/modules/execution/VisualConfirmationController.d.ts.map +1 -1
- package/dist/modules/execution/VisualConfirmationController.js +22 -11
- package/dist/modules/execution/VisualConfirmationController.js.map +1 -1
- package/dist/modules/execution/step-handler-registry.d.ts +22 -1
- package/dist/modules/execution/step-handler-registry.d.ts.map +1 -1
- package/package.json +10 -10
|
@@ -11,6 +11,7 @@ import { CONFLICTS_AGENT_KIND, MERGER_AGENT_KIND, REQUIREMENTS_REVIEW_AGENT_KIND
|
|
|
11
11
|
import { DEFAULT_FOLLOW_UP_MAX_LOOPS, FOLLOW_UP_PRODUCER_KIND, followUpsToSendBack, hasPendingFollowUps, renderFollowUpRework, shouldLoopCoder, } from './followUp.logic.js';
|
|
12
12
|
import { AgentContextBuilder, } from './AgentContextBuilder.js';
|
|
13
13
|
import { CompanionController } from './CompanionController.js';
|
|
14
|
+
import { StepGraph } from './StepGraph.js';
|
|
14
15
|
import { inferTechnicalLabel } from './technical.logic.js';
|
|
15
16
|
import { MergeResolver } from './MergeResolver.js';
|
|
16
17
|
import { ReviewGateController } from './ReviewGateController.js';
|
|
@@ -65,6 +66,16 @@ function parseRepoFromPullUrl(url) {
|
|
|
65
66
|
return undefined;
|
|
66
67
|
return { owner: match[1], repo: match[2] };
|
|
67
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* The inline review/brainstorm gate kinds, all driven through the {@link ReviewGateController}
|
|
71
|
+
* by the engine's `review-gate` StepHandler. Kept in sync with the handler's `switch`.
|
|
72
|
+
*/
|
|
73
|
+
const REVIEW_GATE_AGENT_KINDS = new Set([
|
|
74
|
+
REQUIREMENTS_REVIEW_AGENT_KIND,
|
|
75
|
+
CLARITY_REVIEW_AGENT_KIND,
|
|
76
|
+
REQUIREMENTS_BRAINSTORM_AGENT_KIND,
|
|
77
|
+
ARCHITECTURE_BRAINSTORM_AGENT_KIND,
|
|
78
|
+
]);
|
|
68
79
|
/**
|
|
69
80
|
* The execution engine. It orchestrates a pipeline of agent-performed steps and
|
|
70
81
|
* is fully deterministic: `advanceInstance` moves one run forward by exactly one
|
|
@@ -83,6 +94,8 @@ export class ExecutionService {
|
|
|
83
94
|
accountRepository;
|
|
84
95
|
idGenerator;
|
|
85
96
|
clock;
|
|
97
|
+
/** The pure step/cursor mutators (start/finish/park/reset + the companion rework loop). */
|
|
98
|
+
stepGraph;
|
|
86
99
|
agentExecutor;
|
|
87
100
|
workRunner;
|
|
88
101
|
events;
|
|
@@ -153,7 +166,12 @@ export class ExecutionService {
|
|
|
153
166
|
* and {@link StepHandler}. Engine-internal (no public registration seam).
|
|
154
167
|
*/
|
|
155
168
|
stepHandlerCache;
|
|
156
|
-
|
|
169
|
+
/**
|
|
170
|
+
* Lazily-built, order-sorted completion-path interceptor list (companion/tester verdict
|
|
171
|
+
* short-circuits). See {@link dispatchStepCompletionInterceptor}. Engine-internal.
|
|
172
|
+
*/
|
|
173
|
+
stepCompletionInterceptorCache;
|
|
174
|
+
constructor({ workspaceRepository, blockRepository, pipelineRepository, executionRepository, accountRepository, idGenerator, clock, agentExecutor, workRunner, executionEventPublisher, boardService, spendService, documentRepository, documentUrlResolver, taskRepository, requirementReviewRepository, requirementReviewService, kaizenScheduler, clarityReviewRepository, clarityReviewService, brainstormServices, brainstormSessionRepository, fragmentResolver, environmentProvisioning, environmentTeardown, branchUpdater, blueprintReconciler, notificationService, resolveBinaryArtifactStore, workspaceSettingsService, llmObservability, pullRequestMerger, mergePresetRepository, ticketTrackerProvider, issueWriteback, subscriptionActivationRepository, resolveWorkspaceModelDefault, resolveProviderCapabilities, localTestInfraSupported, resolveRunRepoContext, resolveTesterFallbackDefault, resolveRequireEnvironmentProvider, assertAgentBackendConfigured, runInitiatorScope, }) {
|
|
157
175
|
this.runInitiatorScope = runInitiatorScope ?? ((_initiatedBy, fn) => fn());
|
|
158
176
|
this.workspaceRepository = workspaceRepository;
|
|
159
177
|
this.blockRepository = blockRepository;
|
|
@@ -162,6 +180,7 @@ export class ExecutionService {
|
|
|
162
180
|
this.accountRepository = accountRepository;
|
|
163
181
|
this.idGenerator = idGenerator;
|
|
164
182
|
this.clock = clock;
|
|
183
|
+
this.stepGraph = new StepGraph(clock);
|
|
165
184
|
this.agentExecutor = agentExecutor;
|
|
166
185
|
this.workRunner = workRunner;
|
|
167
186
|
this.events = executionEventPublisher;
|
|
@@ -200,9 +219,9 @@ export class ExecutionService {
|
|
|
200
219
|
idGenerator,
|
|
201
220
|
previewStepModel: (ctx) => this.previewStepModel(ctx),
|
|
202
221
|
runAgent: (ctx, opts) => this.runAgent(ctx, opts),
|
|
203
|
-
finishStep: (s) => this.finishStep(s),
|
|
204
|
-
startStep: (s) => this.startStep(s),
|
|
205
|
-
pauseStepForInput: (s) => this.pauseStepForInput(s),
|
|
222
|
+
finishStep: (s) => this.stepGraph.finishStep(s),
|
|
223
|
+
startStep: (s) => this.stepGraph.startStep(s),
|
|
224
|
+
pauseStepForInput: (s) => this.stepGraph.pauseStepForInput(s),
|
|
206
225
|
updateBlockProgress: (ws, i, st) => this.updateBlockProgress(ws, i, st),
|
|
207
226
|
persistInstance: (ws, i) => this.executionRepository.upsert(ws, i),
|
|
208
227
|
emitInstance: (ws, i) => this.emitInstance(ws, i),
|
|
@@ -210,7 +229,7 @@ export class ExecutionService {
|
|
|
210
229
|
finalizeBlock: (ws, i, c) => this.finalizeBlock(ws, i, c),
|
|
211
230
|
parkStepOnDecision: (ws, i, s, p) => this.parkStepOnDecision(ws, i, s, p),
|
|
212
231
|
raiseDecisionRequired: (ws, i) => this.raiseDecisionRequired(ws, i),
|
|
213
|
-
loopCompanionProducer: (i, ci, rw) => this.loopCompanionProducer(i, ci, rw),
|
|
232
|
+
loopCompanionProducer: (i, ci, rw) => this.stepGraph.loopCompanionProducer(i, ci, rw),
|
|
214
233
|
inferTechnicalLabel: (ws, block, producer, companionStep) => this.inferBlockTechnical(ws, block, producer, companionStep),
|
|
215
234
|
});
|
|
216
235
|
this.testerController = new TesterController({
|
|
@@ -255,8 +274,8 @@ export class ExecutionService {
|
|
|
255
274
|
...(branchUpdater ? { branchUpdater } : {}),
|
|
256
275
|
resolveMergePreset: (ws, block) => this.resolveMergePreset(ws, block),
|
|
257
276
|
parkStepOnDecision: (ws, i, s, p) => this.parkStepOnDecision(ws, i, s, p),
|
|
258
|
-
finishStep: (s) => this.finishStep(s),
|
|
259
|
-
startStep: (s) => this.startStep(s),
|
|
277
|
+
finishStep: (s) => this.stepGraph.finishStep(s),
|
|
278
|
+
startStep: (s) => this.stepGraph.startStep(s),
|
|
260
279
|
updateBlockProgress: (ws, i, st) => this.updateBlockProgress(ws, i, st),
|
|
261
280
|
finalizeBlock: (ws, i, c) => this.finalizeBlock(ws, i, c),
|
|
262
281
|
stopRunContainer: (ws, i) => this.stopRunContainer(ws, i),
|
|
@@ -271,11 +290,11 @@ export class ExecutionService {
|
|
|
271
290
|
agentExecutor,
|
|
272
291
|
contextBuilder: this.contextBuilder,
|
|
273
292
|
notificationService,
|
|
274
|
-
...(
|
|
293
|
+
...(resolveBinaryArtifactStore ? { resolveBinaryArtifactStore } : {}),
|
|
275
294
|
resolveMergePreset: (ws, block) => this.resolveMergePreset(ws, block),
|
|
276
295
|
parkStepOnDecision: (ws, i, s, p) => this.parkStepOnDecision(ws, i, s, p),
|
|
277
|
-
finishStep: (s) => this.finishStep(s),
|
|
278
|
-
startStep: (s) => this.startStep(s),
|
|
296
|
+
finishStep: (s) => this.stepGraph.finishStep(s),
|
|
297
|
+
startStep: (s) => this.stepGraph.startStep(s),
|
|
279
298
|
updateBlockProgress: (ws, i, st) => this.updateBlockProgress(ws, i, st),
|
|
280
299
|
finalizeBlock: (ws, i, c) => this.finalizeBlock(ws, i, c),
|
|
281
300
|
stopRunContainer: (ws, i) => this.stopRunContainer(ws, i),
|
|
@@ -292,8 +311,8 @@ export class ExecutionService {
|
|
|
292
311
|
advancePastResolvedGate: (ws, i, idx) => this.advancePastResolvedGate(ws, i, idx),
|
|
293
312
|
dispatchIterationCap: (ws, blockId, choice, handlers) => this.dispatchIterationCap(ws, blockId, choice, handlers),
|
|
294
313
|
raiseDecisionRequired: (ws, i) => this.raiseDecisionRequired(ws, i),
|
|
295
|
-
finishStep: (s) => this.finishStep(s),
|
|
296
|
-
startStep: (s) => this.startStep(s),
|
|
314
|
+
finishStep: (s) => this.stepGraph.finishStep(s),
|
|
315
|
+
startStep: (s) => this.stepGraph.startStep(s),
|
|
297
316
|
updateBlockProgress: (ws, i, st) => this.updateBlockProgress(ws, i, st),
|
|
298
317
|
finalizeBlock: (ws, i, c) => this.finalizeBlock(ws, i, c),
|
|
299
318
|
stopRunContainer: (ws, i) => this.stopRunContainer(ws, i),
|
|
@@ -804,7 +823,7 @@ export class ExecutionService {
|
|
|
804
823
|
}
|
|
805
824
|
}
|
|
806
825
|
}
|
|
807
|
-
this.startStep(step);
|
|
826
|
+
this.stepGraph.startStep(step);
|
|
808
827
|
const block = await this.blockRepository.get(workspaceId, instance.blockId);
|
|
809
828
|
if (!block)
|
|
810
829
|
return { kind: 'noop' };
|
|
@@ -819,7 +838,7 @@ export class ExecutionService {
|
|
|
819
838
|
}
|
|
820
839
|
// The fixed run-lifecycle preamble is done; hand the per-kind work to the
|
|
821
840
|
// engine-internal StepHandler registry (the first handler whose `canHandle` claims
|
|
822
|
-
// this step). See {@link dispatchStepHandler} / {@link
|
|
841
|
+
// this step). See {@link dispatchStepHandler} / {@link handleAgentStep}.
|
|
823
842
|
return this.dispatchStepHandler({
|
|
824
843
|
workspaceId,
|
|
825
844
|
instance,
|
|
@@ -830,77 +849,16 @@ export class ExecutionService {
|
|
|
830
849
|
});
|
|
831
850
|
}
|
|
832
851
|
/**
|
|
833
|
-
* The
|
|
834
|
-
*
|
|
835
|
-
*
|
|
836
|
-
*
|
|
837
|
-
*
|
|
852
|
+
* The generic container/inline-agent step — the lowest-priority StepHandler, claiming
|
|
853
|
+
* every step no more-specific handler did (coder, architect, spec-writer, merger,
|
|
854
|
+
* task-estimator, the container-backed companions, …). Builds the agent context, runs the
|
|
855
|
+
* kind's pre-ops, then either dispatches an async container job and parks (the durable
|
|
856
|
+
* driver polls between sleeps) or runs the inline LLM call and records the result. This is
|
|
857
|
+
* what the dispatch chain falls through to; all the deterministic / gate / inline-review
|
|
858
|
+
* kinds are claimed earlier by their own handlers (see {@link buildStepHandlerRegistry}).
|
|
838
859
|
*/
|
|
839
|
-
async
|
|
860
|
+
async handleAgentStep(ctx) {
|
|
840
861
|
const { workspaceId, instance, step, block, isFinalStep, options } = ctx;
|
|
841
|
-
// (The `deployer` and `tracker` steps are handled by their own StepHandlers — see
|
|
842
|
-
// {@link buildStepHandlerRegistry} — so they no longer branch here.)
|
|
843
|
-
// A `requirements-review` step runs the inline reviewer and parks for the dedicated
|
|
844
|
-
// review window, driving the iterative answer → incorporate → re-review loop. NOT a
|
|
845
|
-
// container/prose agent. Pass-through when the reviewer isn't wired. The clarity gate
|
|
846
|
-
// shares the SAME flow (only the subject + persisted doc differ); both run through the
|
|
847
|
-
// {@link ReviewGateController}, parameterised by their {@link ReviewKind}.
|
|
848
|
-
if (step.agentKind === REQUIREMENTS_REVIEW_AGENT_KIND) {
|
|
849
|
-
return this.reviewGate.evaluate(this.requirementsKind, workspaceId, instance, step, block, isFinalStep);
|
|
850
|
-
}
|
|
851
|
-
// A `clarity-review` step triages the block's bug report (optionally enriched by an
|
|
852
|
-
// upstream `bug-investigator` step) and parks for the dedicated review window, driving
|
|
853
|
-
// the same iterative loop as the requirements gate. NOT a container/prose agent.
|
|
854
|
-
// Pass-through when the reviewer isn't wired.
|
|
855
|
-
if (step.agentKind === CLARITY_REVIEW_AGENT_KIND) {
|
|
856
|
-
return this.reviewGate.evaluate(this.clarityKind, workspaceId, instance, step, block, isFinalStep);
|
|
857
|
-
}
|
|
858
|
-
// The two brainstorm (structured-dialogue) gates run the inline option-generator and park
|
|
859
|
-
// for the dedicated brainstorm window, driving the same iterative loop as the requirements
|
|
860
|
-
// gate. NOT container/prose agents. Pass-through when the brainstorm module isn't wired.
|
|
861
|
-
if (step.agentKind === REQUIREMENTS_BRAINSTORM_AGENT_KIND) {
|
|
862
|
-
return this.reviewGate.evaluate(this.requirementsBrainstormKind, workspaceId, instance, step, block, isFinalStep);
|
|
863
|
-
}
|
|
864
|
-
if (step.agentKind === ARCHITECTURE_BRAINSTORM_AGENT_KIND) {
|
|
865
|
-
return this.reviewGate.evaluate(this.architectureBrainstormKind, workspaceId, instance, step, block, isFinalStep);
|
|
866
|
-
}
|
|
867
|
-
// A `human-test` gate spins up an ephemeral environment and PARKS for a human to
|
|
868
|
-
// validate the change in a live URL before the run continues — NOT a container/prose
|
|
869
|
-
// agent and NOT a programmatic polling gate (the human is the verdict). It also drives
|
|
870
|
-
// the same helpers the other gates use on demand: the Tester's `fixer` (from findings)
|
|
871
|
-
// and the `conflict-resolver` (after a conflicting pull-main). Degrades to a manual
|
|
872
|
-
// (no-env) mode when no ephemeral-environment provider is wired. See {@link HumanTestController}.
|
|
873
|
-
if (step.agentKind === HUMAN_TEST_AGENT_KIND) {
|
|
874
|
-
return this.humanTestController.evaluate(workspaceId, instance, step, block, isFinalStep);
|
|
875
|
-
}
|
|
876
|
-
// A `visual-confirmation` gate gathers the UI tester's screenshots + the uploaded
|
|
877
|
-
// reference designs and PARKS for a human to review actual-vs-reference, then on demand
|
|
878
|
-
// dispatches the Tester's `fixer`. Passes through (auto-advances) when no binary-artifact
|
|
879
|
-
// store is wired. See {@link VisualConfirmationController}.
|
|
880
|
-
if (step.agentKind === VISUAL_CONFIRM_AGENT_KIND) {
|
|
881
|
-
return this.visualConfirmationController.evaluate(workspaceId, instance, step, block, isFinalStep);
|
|
882
|
-
}
|
|
883
|
-
// A polling gate step (`ci` / `conflicts`) runs a programmatic precheck and only
|
|
884
|
-
// escalates to a helper container agent (`ci-fixer` / `conflict-resolver`) on a
|
|
885
|
-
// negative verdict — no LLM of its own. Pass-through when the gate's provider is
|
|
886
|
-
// not wired. One generic machine drives every gate; see {@link evaluateGate}.
|
|
887
|
-
const gate = this.gateFor(step.agentKind);
|
|
888
|
-
if (gate) {
|
|
889
|
-
return this.evaluateGate(workspaceId, instance, step, block, isFinalStep, gate);
|
|
890
|
-
}
|
|
891
|
-
// A companion step grades the nearest preceding producer of one of its target
|
|
892
|
-
// kinds, looping it back for automatic rework below the threshold (and failing
|
|
893
|
-
// the run once the budget is spent) before any human gate. See evaluateCompanion.
|
|
894
|
-
//
|
|
895
|
-
// INLINE companions (architect-companion / spec-companion) run their LLM grading right
|
|
896
|
-
// here. CONTAINER-backed companions (reviewer / doc-reviewer) instead fall through to the
|
|
897
|
-
// generic async container dispatch below — they clone the producer's PR branch and review
|
|
898
|
-
// the REAL repository — and their verdict is resolved in `recordStepResult` via
|
|
899
|
-
// `companionController.resolveContainerVerdict` (which runs the SAME threshold / rework
|
|
900
|
-
// loop). A summary-only review is useless; the container reviewer reads the actual diff.
|
|
901
|
-
if (isCompanionKind(step.agentKind) && !isContainerBackedCompanion(step.agentKind)) {
|
|
902
|
-
return this.companionController.evaluate(workspaceId, instance, step, block, isFinalStep, options);
|
|
903
|
-
}
|
|
904
862
|
// Async (container) steps don't block: dispatch the job and park. The durable
|
|
905
863
|
// driver polls `pollAgentJob` between sleeps so the run can span far longer
|
|
906
864
|
// than a single durable step's timeout, while each step stays short. A set
|
|
@@ -1434,29 +1392,6 @@ export class ExecutionService {
|
|
|
1434
1392
|
return false;
|
|
1435
1393
|
}
|
|
1436
1394
|
}
|
|
1437
|
-
startStep(step) {
|
|
1438
|
-
step.state = 'working';
|
|
1439
|
-
if (step.startedAt == null)
|
|
1440
|
-
step.startedAt = this.clock.now();
|
|
1441
|
-
// (Re)entering `working` means the step is no longer parked on a human: resume
|
|
1442
|
-
// its duration clock (see {@link pauseStepForInput}).
|
|
1443
|
-
step.pausedAt = null;
|
|
1444
|
-
}
|
|
1445
|
-
/**
|
|
1446
|
-
* Transition a step into `done`, stamping its finish time once. Set-once so the
|
|
1447
|
-
* approval-gate flow (which re-asserts `done` after a human approves, long after
|
|
1448
|
-
* the agent actually finished) keeps the agent's true completion time, and so a
|
|
1449
|
-
* replay doesn't move it. With {@link startStep}'s `startedAt` this yields the
|
|
1450
|
-
* step's execution duration. A step finished directly out of a parked approval
|
|
1451
|
-
* stopped *working* when it parked, so its duration is billed to the pause instant
|
|
1452
|
-
* ({@link pauseStepForInput}), not the (later) moment the human decided.
|
|
1453
|
-
*/
|
|
1454
|
-
finishStep(step) {
|
|
1455
|
-
step.state = 'done';
|
|
1456
|
-
if (step.finishedAt == null)
|
|
1457
|
-
step.finishedAt = step.pausedAt ?? this.clock.now();
|
|
1458
|
-
step.pausedAt = null;
|
|
1459
|
-
}
|
|
1460
1395
|
/**
|
|
1461
1396
|
* Finish a gated step that was skipped (its estimate gate was not satisfied) and either
|
|
1462
1397
|
* complete the run or advance to the next step — the deterministic finish/advance tail
|
|
@@ -1469,7 +1404,7 @@ export class ExecutionService {
|
|
|
1469
1404
|
step.output = '';
|
|
1470
1405
|
step.progress = 1;
|
|
1471
1406
|
step.subtasks = undefined;
|
|
1472
|
-
this.finishStep(step);
|
|
1407
|
+
this.stepGraph.finishStep(step);
|
|
1473
1408
|
if (isFinalStep) {
|
|
1474
1409
|
instance.status = 'done';
|
|
1475
1410
|
await this.finalizeBlock(workspaceId, instance, undefined);
|
|
@@ -1481,24 +1416,12 @@ export class ExecutionService {
|
|
|
1481
1416
|
instance.currentStep += 1;
|
|
1482
1417
|
const next = instance.steps[instance.currentStep];
|
|
1483
1418
|
if (next)
|
|
1484
|
-
this.startStep(next);
|
|
1419
|
+
this.stepGraph.startStep(next);
|
|
1485
1420
|
await this.updateBlockProgress(workspaceId, instance, 'in_progress');
|
|
1486
1421
|
await this.executionRepository.upsert(workspaceId, instance);
|
|
1487
1422
|
await this.emitInstance(workspaceId, instance);
|
|
1488
1423
|
return { kind: 'continue' };
|
|
1489
1424
|
}
|
|
1490
|
-
/**
|
|
1491
|
-
* Park a step on a human decision and freeze its duration clock. Records when the
|
|
1492
|
-
* step stopped working (`pausedAt`) so elapsed time no longer accrues while it waits
|
|
1493
|
-
* for input — the symmetric counterpart of the terminal freeze on `finishedAt`.
|
|
1494
|
-
* Set-once (a Workflows replay re-parking keeps the original instant); cleared when
|
|
1495
|
-
* the step resumes ({@link startStep}) or finishes ({@link finishStep}).
|
|
1496
|
-
*/
|
|
1497
|
-
pauseStepForInput(step) {
|
|
1498
|
-
step.state = 'waiting_decision';
|
|
1499
|
-
if (step.pausedAt == null)
|
|
1500
|
-
step.pausedAt = this.clock.now();
|
|
1501
|
-
}
|
|
1502
1425
|
/**
|
|
1503
1426
|
* Infer + persist the block's `technical` label from the settled spec phase (item 5):
|
|
1504
1427
|
* combine the spec-writer's `noBusinessSpecs` determination (recorded on the producer
|
|
@@ -1542,33 +1465,28 @@ export class ExecutionService {
|
|
|
1542
1465
|
options: [...result.decision.options],
|
|
1543
1466
|
chosen: null,
|
|
1544
1467
|
};
|
|
1545
|
-
this.pauseStepForInput(step);
|
|
1468
|
+
this.stepGraph.pauseStepForInput(step);
|
|
1546
1469
|
instance.status = 'blocked';
|
|
1547
1470
|
await this.updateBlockProgress(workspaceId, instance, 'blocked');
|
|
1548
1471
|
await this.executionRepository.upsert(workspaceId, instance);
|
|
1549
1472
|
await this.emitInstance(workspaceId, instance);
|
|
1550
1473
|
return { kind: 'awaiting_decision', decisionId: step.decision.id };
|
|
1551
1474
|
}
|
|
1552
|
-
//
|
|
1553
|
-
//
|
|
1554
|
-
//
|
|
1555
|
-
//
|
|
1556
|
-
//
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
if (isTesterKind(step.agentKind) && result.testReport !== undefined) {
|
|
1568
|
-
const looped = await this.testerController.resolveTesterResult(workspaceId, instance, step, result);
|
|
1569
|
-
if (looped)
|
|
1570
|
-
return looped;
|
|
1571
|
-
}
|
|
1475
|
+
// Completion-path interceptors short-circuit before the normal finish/advance for the
|
|
1476
|
+
// few kinds whose verdict drives run flow: a container-backed companion applies its
|
|
1477
|
+
// threshold/rework/human-gate loop, and a Tester re-runs its `fixer` on a withheld
|
|
1478
|
+
// greenlight. A non-null outcome replaces the normal completion; null (a Tester
|
|
1479
|
+
// greenlight, or a companion whose block can't be loaded) falls through. See
|
|
1480
|
+
// {@link buildStepCompletionInterceptors}.
|
|
1481
|
+
const intercepted = await this.dispatchStepCompletionInterceptor({
|
|
1482
|
+
workspaceId,
|
|
1483
|
+
instance,
|
|
1484
|
+
step,
|
|
1485
|
+
isFinalStep,
|
|
1486
|
+
result,
|
|
1487
|
+
});
|
|
1488
|
+
if (intercepted)
|
|
1489
|
+
return intercepted;
|
|
1572
1490
|
// The step completed.
|
|
1573
1491
|
step.output = result.output ?? '';
|
|
1574
1492
|
// Surface a registered custom kind's structured JSON on the step so the SPA's
|
|
@@ -1579,7 +1497,7 @@ export class ExecutionService {
|
|
|
1579
1497
|
if (result.model)
|
|
1580
1498
|
step.model = result.model;
|
|
1581
1499
|
step.progress = 1;
|
|
1582
|
-
this.finishStep(step);
|
|
1500
|
+
this.stepGraph.finishStep(step);
|
|
1583
1501
|
// Live subtask counts only describe an in-flight run; drop them now the step
|
|
1584
1502
|
// is done so the board doesn't show a stale "3/8" against a finished step.
|
|
1585
1503
|
step.subtasks = undefined;
|
|
@@ -1612,38 +1530,25 @@ export class ExecutionService {
|
|
|
1612
1530
|
.catch(() => { });
|
|
1613
1531
|
}
|
|
1614
1532
|
}
|
|
1615
|
-
//
|
|
1616
|
-
//
|
|
1617
|
-
//
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
//
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
await
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
}
|
|
1635
|
-
// A `task-estimator` step emits a JSON triage (complexity/risk/impact). Parse it
|
|
1636
|
-
// tolerantly, persist it on the block (used to gate consensus steps + surfaced in
|
|
1637
|
-
// the UI), and replace the raw JSON output with a readable summary. An unparseable
|
|
1638
|
-
// estimate leaves the block untouched and keeps the raw output (no run failure).
|
|
1639
|
-
// The estimate works the same whether the single-actor estimator or the consensus
|
|
1640
|
-
// ranked-scoring variant produced the JSON — both land here.
|
|
1641
|
-
if (step.agentKind === TASK_ESTIMATOR_AGENT_KIND) {
|
|
1642
|
-
const estimate = coerceTaskEstimate(step.output, result.model ?? step.model ?? null, this.clock.now());
|
|
1643
|
-
if (estimate) {
|
|
1644
|
-
await this.blockRepository.update(workspaceId, instance.blockId, { estimate });
|
|
1645
|
-
step.output = summarizeEstimate(estimate);
|
|
1646
|
-
}
|
|
1533
|
+
// Run any POST-COMPLETION resolver registered for this step kind (blueprint/spec
|
|
1534
|
+
// ingestion, task-estimate persistence). It reshapes the agent's structured result into
|
|
1535
|
+
// domain state and may replace `step.output` (the estimator's readable summary). Its
|
|
1536
|
+
// POSITION is load-bearing — it runs after the output is recorded but BEFORE the
|
|
1537
|
+
// reviewable-output rendering and the follow-up/approval gates read `step.output`, so it
|
|
1538
|
+
// sits exactly where the old inline ingestion branches did. See
|
|
1539
|
+
// {@link buildStepResolverRegistry} and {@link StepCompletionResolver.phase}.
|
|
1540
|
+
const postCompletionResolver = this.stepResolverFor(step.agentKind);
|
|
1541
|
+
if (postCompletionResolver?.phase === 'post-completion' &&
|
|
1542
|
+
(postCompletionResolver.applies?.(result) ?? true)) {
|
|
1543
|
+
const resolution = await postCompletionResolver.resolve({
|
|
1544
|
+
workspaceId,
|
|
1545
|
+
instance,
|
|
1546
|
+
step,
|
|
1547
|
+
result,
|
|
1548
|
+
isFinalStep,
|
|
1549
|
+
});
|
|
1550
|
+
if (resolution?.output !== undefined)
|
|
1551
|
+
step.output = resolution.output;
|
|
1647
1552
|
}
|
|
1648
1553
|
// A producer that emits a STRUCTURED ARTIFACT (the spec doc, the blueprint tree, …)
|
|
1649
1554
|
// returns its raw Pi transcript summary as `result.output` — useless for review.
|
|
@@ -1679,7 +1584,7 @@ export class ExecutionService {
|
|
|
1679
1584
|
status: 'pending',
|
|
1680
1585
|
proposal: step.output,
|
|
1681
1586
|
};
|
|
1682
|
-
this.pauseStepForInput(step);
|
|
1587
|
+
this.stepGraph.pauseStepForInput(step);
|
|
1683
1588
|
instance.status = 'blocked';
|
|
1684
1589
|
await this.updateBlockProgress(workspaceId, instance, 'blocked');
|
|
1685
1590
|
await this.executionRepository.upsert(workspaceId, instance);
|
|
@@ -1702,7 +1607,9 @@ export class ExecutionService {
|
|
|
1702
1607
|
// tells `finalizeBlock` to leave it alone.
|
|
1703
1608
|
const resolver = this.stepResolverFor(step.agentKind);
|
|
1704
1609
|
let resolverOwnsTerminalStatus = false;
|
|
1705
|
-
if (resolver &&
|
|
1610
|
+
if (resolver &&
|
|
1611
|
+
(resolver.phase ?? 'terminal') === 'terminal' &&
|
|
1612
|
+
(resolver.applies?.(result) ?? true)) {
|
|
1706
1613
|
const resolution = await resolver.resolve({
|
|
1707
1614
|
workspaceId,
|
|
1708
1615
|
instance,
|
|
@@ -1740,7 +1647,7 @@ export class ExecutionService {
|
|
|
1740
1647
|
instance.currentStep += 1;
|
|
1741
1648
|
const next = instance.steps[instance.currentStep];
|
|
1742
1649
|
if (next)
|
|
1743
|
-
this.startStep(next);
|
|
1650
|
+
this.stepGraph.startStep(next);
|
|
1744
1651
|
// A resolver that already set the block's TERMINAL status (the merger flips it to
|
|
1745
1652
|
// `done`/`pr_ready` mid-pipeline) must not be clobbered back to `in_progress` as we
|
|
1746
1653
|
// advance to a trailing step — refresh progress only, preserving that status. (The
|
|
@@ -1755,74 +1662,6 @@ export class ExecutionService {
|
|
|
1755
1662
|
await this.emitInstance(workspaceId, instance);
|
|
1756
1663
|
return { kind: 'continue' };
|
|
1757
1664
|
}
|
|
1758
|
-
/**
|
|
1759
|
-
* Reset a step so the durable driver re-runs it from scratch: clear its live
|
|
1760
|
-
* container job handle (so it dispatches FRESH work rather than re-attaching to a
|
|
1761
|
-
* finished or evicted job), its timings, approval gate, live subtasks and last
|
|
1762
|
-
* output, and drop it back to `pending`. Preserves the step's identity
|
|
1763
|
-
* (`agentKind` / `requiresApproval`) and any companion budget/verdict history.
|
|
1764
|
-
*/
|
|
1765
|
-
resetStepForRerun(step) {
|
|
1766
|
-
step.state = 'pending';
|
|
1767
|
-
step.startedAt = null;
|
|
1768
|
-
step.finishedAt = null;
|
|
1769
|
-
step.pausedAt = null;
|
|
1770
|
-
step.jobId = undefined;
|
|
1771
|
-
step.approval = null;
|
|
1772
|
-
step.subtasks = undefined;
|
|
1773
|
-
step.progress = 0;
|
|
1774
|
-
step.output = undefined;
|
|
1775
|
-
// Drop the prior run's structured output too, so a re-run that produces no `custom`
|
|
1776
|
-
// doesn't leave stale JSON for the `generic-structured` result view to render.
|
|
1777
|
-
step.custom = undefined;
|
|
1778
|
-
step.rework = undefined;
|
|
1779
|
-
}
|
|
1780
|
-
/**
|
|
1781
|
-
* Loop a producer step back for rework and re-run every step from it up to and
|
|
1782
|
-
* including the companion at `companionIndex`: each one is reset (crucially clearing
|
|
1783
|
-
* stale container job handles so an intermediate container step re-dispatches fresh
|
|
1784
|
-
* work instead of re-attaching to its evicted job), the producer is handed the
|
|
1785
|
-
* `rework` feedback + started, and the instance cursor is moved back to the producer.
|
|
1786
|
-
* Shared by the automatic companion loop and the human "request changes" path.
|
|
1787
|
-
*/
|
|
1788
|
-
rerunProducerThrough(instance, producerIndex, companionIndex, rework) {
|
|
1789
|
-
for (let i = producerIndex; i <= companionIndex; i++) {
|
|
1790
|
-
this.resetStepForRerun(instance.steps[i]);
|
|
1791
|
-
}
|
|
1792
|
-
const producer = instance.steps[producerIndex];
|
|
1793
|
-
producer.rework = rework;
|
|
1794
|
-
this.startStep(producer);
|
|
1795
|
-
instance.currentStep = producerIndex;
|
|
1796
|
-
}
|
|
1797
|
-
/**
|
|
1798
|
-
* The index of the nearest preceding step a companion grades (one of its target
|
|
1799
|
-
* producer kinds), or -1 when none precedes it. The single producer-search used by the
|
|
1800
|
-
* automatic companion loop, the human "request changes" redirect, and the iteration-cap
|
|
1801
|
-
* extra-round resolution.
|
|
1802
|
-
*/
|
|
1803
|
-
companionProducerIndex(instance, companionIndex) {
|
|
1804
|
-
const targets = companionTargets(instance.steps[companionIndex].agentKind);
|
|
1805
|
-
for (let i = companionIndex - 1; i >= 0; i--) {
|
|
1806
|
-
if (targets.includes(instance.steps[i].agentKind))
|
|
1807
|
-
return i;
|
|
1808
|
-
}
|
|
1809
|
-
return -1;
|
|
1810
|
-
}
|
|
1811
|
-
/**
|
|
1812
|
-
* Loop a companion's producer back for one more automatic rework cycle: charge one
|
|
1813
|
-
* attempt against the budget, then re-run the producer (and any intermediate steps) up
|
|
1814
|
-
* to and including the companion so it re-grades. Shared by the automatic
|
|
1815
|
-
* below-threshold loop ({@link evaluateCompanion}) and the human-granted extra round
|
|
1816
|
-
* ({@link resolveCompanionExceeded}), so both consume the budget identically.
|
|
1817
|
-
*/
|
|
1818
|
-
loopCompanionProducer(instance, companionIndex, rework) {
|
|
1819
|
-
const companionStep = instance.steps[companionIndex];
|
|
1820
|
-
const producerIndex = this.companionProducerIndex(instance, companionIndex);
|
|
1821
|
-
companionStep.companion.attempts += 1;
|
|
1822
|
-
this.rerunProducerThrough(instance, producerIndex, companionIndex, rework);
|
|
1823
|
-
if (instance.status === 'blocked')
|
|
1824
|
-
instance.status = 'running';
|
|
1825
|
-
}
|
|
1826
1665
|
/**
|
|
1827
1666
|
* Deterministically provision an ephemeral environment for a deployer step.
|
|
1828
1667
|
* Produces a human-readable summary as the step output and reports no token
|
|
@@ -2118,17 +1957,144 @@ export class ExecutionService {
|
|
|
2118
1957
|
return this.recordStepResult(workspaceId, instance, step, isFinalStep, result);
|
|
2119
1958
|
},
|
|
2120
1959
|
},
|
|
2121
|
-
// The
|
|
2122
|
-
//
|
|
1960
|
+
// The `requirements-review` / `clarity-review` / `requirements-brainstorm` /
|
|
1961
|
+
// `architecture-brainstorm` steps are inline reviewers that park for their dedicated
|
|
1962
|
+
// window and drive the iterative answer → incorporate → re-review loop — NOT
|
|
1963
|
+
// container/prose agents. All four run through the {@link ReviewGateController},
|
|
1964
|
+
// parameterised by their {@link ReviewKind} (the per-case `switch` binds each kind's
|
|
1965
|
+
// review type so `evaluate`'s generic infers). Pass-through when the service isn't wired.
|
|
1966
|
+
{
|
|
1967
|
+
kind: 'review-gate',
|
|
1968
|
+
order: 120,
|
|
1969
|
+
canHandle: ({ step }) => REVIEW_GATE_AGENT_KINDS.has(step.agentKind),
|
|
1970
|
+
handle: ({ workspaceId, instance, step, block, isFinalStep }) => {
|
|
1971
|
+
switch (step.agentKind) {
|
|
1972
|
+
case REQUIREMENTS_REVIEW_AGENT_KIND:
|
|
1973
|
+
return this.reviewGate.evaluate(this.requirementsKind, workspaceId, instance, step, block, isFinalStep);
|
|
1974
|
+
case CLARITY_REVIEW_AGENT_KIND:
|
|
1975
|
+
return this.reviewGate.evaluate(this.clarityKind, workspaceId, instance, step, block, isFinalStep);
|
|
1976
|
+
case REQUIREMENTS_BRAINSTORM_AGENT_KIND:
|
|
1977
|
+
return this.reviewGate.evaluate(this.requirementsBrainstormKind, workspaceId, instance, step, block, isFinalStep);
|
|
1978
|
+
case ARCHITECTURE_BRAINSTORM_AGENT_KIND:
|
|
1979
|
+
return this.reviewGate.evaluate(this.architectureBrainstormKind, workspaceId, instance, step, block, isFinalStep);
|
|
1980
|
+
// `canHandle` admits only the kinds in REVIEW_GATE_AGENT_KINDS, so every member
|
|
1981
|
+
// must have an explicit case above. Throw loudly if the two ever drift (a new
|
|
1982
|
+
// review kind added to the Set without a case here) rather than silently routing
|
|
1983
|
+
// it to the wrong reviewer.
|
|
1984
|
+
default:
|
|
1985
|
+
throw new Error(`Unhandled review-gate agentKind "${step.agentKind}"`);
|
|
1986
|
+
}
|
|
1987
|
+
},
|
|
1988
|
+
},
|
|
1989
|
+
// A `human-test` gate spins up an ephemeral environment and PARKS for a human to
|
|
1990
|
+
// validate the change in a live URL — NOT a container/prose agent and NOT a
|
|
1991
|
+
// programmatic polling gate (the human is the verdict). Degrades to a manual (no-env)
|
|
1992
|
+
// mode when no ephemeral-environment provider is wired. See {@link HumanTestController}.
|
|
2123
1993
|
{
|
|
2124
|
-
kind:
|
|
1994
|
+
kind: HUMAN_TEST_AGENT_KIND,
|
|
1995
|
+
order: 130,
|
|
1996
|
+
canHandle: ({ step }) => step.agentKind === HUMAN_TEST_AGENT_KIND,
|
|
1997
|
+
handle: ({ workspaceId, instance, step, block, isFinalStep }) => this.humanTestController.evaluate(workspaceId, instance, step, block, isFinalStep),
|
|
1998
|
+
},
|
|
1999
|
+
// A `visual-confirmation` gate gathers the UI tester's screenshots + uploaded reference
|
|
2000
|
+
// designs and PARKS for a human to review actual-vs-reference, then on demand dispatches
|
|
2001
|
+
// the Tester's `fixer`. Passes through when no binary-artifact store is wired.
|
|
2002
|
+
// See {@link VisualConfirmationController}.
|
|
2003
|
+
{
|
|
2004
|
+
kind: VISUAL_CONFIRM_AGENT_KIND,
|
|
2005
|
+
order: 140,
|
|
2006
|
+
canHandle: ({ step }) => step.agentKind === VISUAL_CONFIRM_AGENT_KIND,
|
|
2007
|
+
handle: ({ workspaceId, instance, step, block, isFinalStep }) => this.visualConfirmationController.evaluate(workspaceId, instance, step, block, isFinalStep),
|
|
2008
|
+
},
|
|
2009
|
+
// A polling gate step (`ci` / `conflicts` / `post-release-health` / `human-review`) runs
|
|
2010
|
+
// a programmatic precheck and only escalates to a helper container agent on a negative
|
|
2011
|
+
// verdict — no LLM of its own. Pass-through when the gate's provider is not wired. One
|
|
2012
|
+
// generic machine drives every gate; see {@link evaluateGate}. `canHandle` is the gate
|
|
2013
|
+
// registry lookup, so this claims exactly the registered gate kinds.
|
|
2014
|
+
{
|
|
2015
|
+
kind: 'polling-gate',
|
|
2016
|
+
order: 150,
|
|
2017
|
+
canHandle: ({ step }) => this.gateFor(step.agentKind) !== undefined,
|
|
2018
|
+
handle: ({ workspaceId, instance, step, block, isFinalStep }) => this.evaluateGate(workspaceId, instance, step, block, isFinalStep, this.gateFor(step.agentKind)),
|
|
2019
|
+
},
|
|
2020
|
+
// An INLINE companion (architect-companion / spec-companion) grades the nearest
|
|
2021
|
+
// preceding producer right here and loops it back for automatic rework below the
|
|
2022
|
+
// threshold before any human gate. CONTAINER-backed companions (reviewer / doc-reviewer)
|
|
2023
|
+
// do NOT match — they fall through to the generic async container dispatch and have their
|
|
2024
|
+
// verdict resolved by the completion interceptor instead. See {@link CompanionController}.
|
|
2025
|
+
{
|
|
2026
|
+
kind: 'inline-companion',
|
|
2027
|
+
order: 160,
|
|
2028
|
+
canHandle: ({ step }) => isCompanionKind(step.agentKind) && !isContainerBackedCompanion(step.agentKind),
|
|
2029
|
+
handle: ({ workspaceId, instance, step, block, isFinalStep, options }) => this.companionController.evaluate(workspaceId, instance, step, block, isFinalStep, options),
|
|
2030
|
+
},
|
|
2031
|
+
// The generic container/inline-agent step — claims every step no more-specific handler
|
|
2032
|
+
// did. Highest order so it always runs last. See {@link handleAgentStep}.
|
|
2033
|
+
{
|
|
2034
|
+
kind: 'agent',
|
|
2125
2035
|
order: FALLTHROUGH_STEP_HANDLER_ORDER,
|
|
2126
2036
|
canHandle: () => true,
|
|
2127
|
-
handle: (ctx) => this.
|
|
2037
|
+
handle: (ctx) => this.handleAgentStep(ctx),
|
|
2128
2038
|
},
|
|
2129
2039
|
];
|
|
2130
2040
|
return handlers.sort((a, b) => a.order - b.order);
|
|
2131
2041
|
}
|
|
2042
|
+
/**
|
|
2043
|
+
* Run the first completion-path interceptor that claims this finished step, returning its
|
|
2044
|
+
* short-circuit {@link AdvanceResult} (the companion verdict loop / tester re-test) or
|
|
2045
|
+
* `null` to let `recordStepResult`'s normal finish/advance spine run. Engine-internal,
|
|
2046
|
+
* mirroring {@link dispatchStepHandler}.
|
|
2047
|
+
*/
|
|
2048
|
+
async dispatchStepCompletionInterceptor(ctx) {
|
|
2049
|
+
if (!this.stepCompletionInterceptorCache) {
|
|
2050
|
+
this.stepCompletionInterceptorCache = this.buildStepCompletionInterceptors();
|
|
2051
|
+
}
|
|
2052
|
+
for (const interceptor of this.stepCompletionInterceptorCache) {
|
|
2053
|
+
if (interceptor.canIntercept(ctx)) {
|
|
2054
|
+
const outcome = await interceptor.intercept(ctx);
|
|
2055
|
+
if (outcome)
|
|
2056
|
+
return outcome;
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
return null;
|
|
2060
|
+
}
|
|
2061
|
+
/**
|
|
2062
|
+
* Build the order-sorted completion-path interceptors (companion / tester verdict
|
|
2063
|
+
* short-circuits), mirroring {@link buildStepHandlerRegistry} — built-ins constructed
|
|
2064
|
+
* inline closing over `this`, no public registration seam.
|
|
2065
|
+
*/
|
|
2066
|
+
buildStepCompletionInterceptors() {
|
|
2067
|
+
const interceptors = [
|
|
2068
|
+
// A container-backed companion (reviewer / doc-reviewer) just finished reviewing the
|
|
2069
|
+
// real repository on the producer's PR branch and returned its verdict as
|
|
2070
|
+
// `result.custom`. Hand it to the companion loop, which parses the verdict and applies
|
|
2071
|
+
// the SAME threshold / rework / human-gate handling an inline companion gets. Routed
|
|
2072
|
+
// here (not the normal step completion) so the verdict drives the loop instead of being
|
|
2073
|
+
// recorded as plain output. Falls through (returns null) when the block can't be loaded.
|
|
2074
|
+
{
|
|
2075
|
+
kind: 'companion-verdict',
|
|
2076
|
+
order: 100,
|
|
2077
|
+
canIntercept: ({ step }) => isCompanionKind(step.agentKind) && isContainerBackedCompanion(step.agentKind),
|
|
2078
|
+
intercept: async ({ workspaceId, instance, step, isFinalStep, result }) => {
|
|
2079
|
+
const companionBlock = await this.blockRepository.get(workspaceId, instance.blockId);
|
|
2080
|
+
if (!companionBlock)
|
|
2081
|
+
return null;
|
|
2082
|
+
return this.companionController.resolveContainerVerdict(workspaceId, instance, step, companionBlock, isFinalStep, result);
|
|
2083
|
+
},
|
|
2084
|
+
},
|
|
2085
|
+
// A `tester` step returned a structured report. On a withheld greenlight we do NOT
|
|
2086
|
+
// finish the step: loop the `fixer` (within the attempt budget) and re-test, mirroring
|
|
2087
|
+
// the CI gate. A greenlight (or no provider) returns null and falls through to the
|
|
2088
|
+
// normal finish/advance below. Records the report on the step either way.
|
|
2089
|
+
{
|
|
2090
|
+
kind: 'tester-verdict',
|
|
2091
|
+
order: 110,
|
|
2092
|
+
canIntercept: ({ step, result }) => isTesterKind(step.agentKind) && result.testReport !== undefined,
|
|
2093
|
+
intercept: ({ workspaceId, instance, step, result }) => this.testerController.resolveTesterResult(workspaceId, instance, step, result),
|
|
2094
|
+
},
|
|
2095
|
+
];
|
|
2096
|
+
return interceptors.sort((a, b) => a.order - b.order);
|
|
2097
|
+
}
|
|
2132
2098
|
stepResolverFor(agentKind) {
|
|
2133
2099
|
if (!this.stepResolverCache)
|
|
2134
2100
|
this.stepResolverCache = this.buildStepResolverRegistry();
|
|
@@ -2151,6 +2117,55 @@ export class ExecutionService {
|
|
|
2151
2117
|
return { ownsTerminalStatus: true };
|
|
2152
2118
|
},
|
|
2153
2119
|
},
|
|
2120
|
+
// POST-COMPLETION resolvers — run at the early slot (after output is recorded, before
|
|
2121
|
+
// the follow-up/approval gates), reshaping the agent's structured result into domain
|
|
2122
|
+
// state. Lifted verbatim from the old inline `recordStepResult` branches.
|
|
2123
|
+
//
|
|
2124
|
+
// A Blueprinter step produced a fresh service decomposition. Validate it with the
|
|
2125
|
+
// authoritative schema (a bad payload must never touch the board), then reconcile it
|
|
2126
|
+
// in place onto the run's service frame.
|
|
2127
|
+
{
|
|
2128
|
+
kind: BLUEPRINTS_AGENT_KIND,
|
|
2129
|
+
phase: 'post-completion',
|
|
2130
|
+
resolve: async ({ workspaceId, instance, result }) => {
|
|
2131
|
+
if (result.blueprintService !== undefined) {
|
|
2132
|
+
await this.ingestBlueprint(workspaceId, instance.blockId, result.blueprintService);
|
|
2133
|
+
}
|
|
2134
|
+
},
|
|
2135
|
+
},
|
|
2136
|
+
// A spec-writer step produced the service's unified specification (`spec.json`) and
|
|
2137
|
+
// committed it to the implementation branch — strict-validate it then nudge clients —
|
|
2138
|
+
// and reports its BUSINESS-vs-TECHNICAL determination. "No business specs" (a purely
|
|
2139
|
+
// technical task) is a valid outcome the spec-companion's convergence later combines
|
|
2140
|
+
// with its `technicalCorroborated` verdict; recorded even when false so a re-run
|
|
2141
|
+
// reflects the latest.
|
|
2142
|
+
{
|
|
2143
|
+
kind: SPEC_WRITER_AGENT_KIND,
|
|
2144
|
+
phase: 'post-completion',
|
|
2145
|
+
resolve: async ({ workspaceId, step, result }) => {
|
|
2146
|
+
if (result.spec !== undefined)
|
|
2147
|
+
await this.ingestSpec(workspaceId, result.spec);
|
|
2148
|
+
step.noBusinessSpecs = result.noBusinessSpecs === true;
|
|
2149
|
+
},
|
|
2150
|
+
},
|
|
2151
|
+
// A `task-estimator` step emits a JSON triage (complexity/risk/impact). Parse it
|
|
2152
|
+
// tolerantly, persist it on the block (used to gate consensus steps + surfaced in the
|
|
2153
|
+
// UI), and replace the raw JSON output with a readable summary. An unparseable estimate
|
|
2154
|
+
// leaves the block untouched and keeps the raw output (no run failure). Works the same
|
|
2155
|
+
// whether the single-actor estimator or the consensus ranked-scoring variant produced
|
|
2156
|
+
// the JSON. Running at the post-completion slot keeps the summary in `step.output`
|
|
2157
|
+
// before the approval gate reads it as the proposal.
|
|
2158
|
+
{
|
|
2159
|
+
kind: TASK_ESTIMATOR_AGENT_KIND,
|
|
2160
|
+
phase: 'post-completion',
|
|
2161
|
+
resolve: async ({ workspaceId, instance, step, result }) => {
|
|
2162
|
+
const estimate = coerceTaskEstimate(step.output ?? '', result.model ?? step.model ?? null, this.clock.now());
|
|
2163
|
+
if (estimate) {
|
|
2164
|
+
await this.blockRepository.update(workspaceId, instance.blockId, { estimate });
|
|
2165
|
+
return { output: summarizeEstimate(estimate) };
|
|
2166
|
+
}
|
|
2167
|
+
},
|
|
2168
|
+
},
|
|
2154
2169
|
];
|
|
2155
2170
|
const map = new Map(resolvers.map((r) => [r.kind, r]));
|
|
2156
2171
|
// Merge deployment-registered resolvers, mirroring the gate registry below. A
|
|
@@ -2462,9 +2477,9 @@ export class ExecutionService {
|
|
|
2462
2477
|
state.loops = (state.loops ?? 0) + 1;
|
|
2463
2478
|
// Reset the step for a fresh dispatch; `step.followUps` is intentionally preserved
|
|
2464
2479
|
// (resetStepForRerun doesn't touch it) so the surfaced items survive the loop.
|
|
2465
|
-
this.resetStepForRerun(step);
|
|
2480
|
+
this.stepGraph.resetStepForRerun(step);
|
|
2466
2481
|
step.rework = { previousProposal: '', feedback };
|
|
2467
|
-
this.startStep(step);
|
|
2482
|
+
this.stepGraph.startStep(step);
|
|
2468
2483
|
if (instance.status === 'blocked')
|
|
2469
2484
|
instance.status = 'running';
|
|
2470
2485
|
}
|
|
@@ -2760,7 +2775,7 @@ export class ExecutionService {
|
|
|
2760
2775
|
*/
|
|
2761
2776
|
async parkStepOnDecision(workspaceId, instance, step, proposal = '') {
|
|
2762
2777
|
step.approval = { id: this.idGenerator.next('appr'), status: 'pending', proposal };
|
|
2763
|
-
this.pauseStepForInput(step);
|
|
2778
|
+
this.stepGraph.pauseStepForInput(step);
|
|
2764
2779
|
instance.status = 'blocked';
|
|
2765
2780
|
await this.updateBlockProgress(workspaceId, instance, 'blocked');
|
|
2766
2781
|
await this.executionRepository.upsert(workspaceId, instance);
|
|
@@ -3041,8 +3056,8 @@ export class ExecutionService {
|
|
|
3041
3056
|
extraRound: async () => {
|
|
3042
3057
|
step.companion.maxAttempts += 1;
|
|
3043
3058
|
step.companion.exceeded = undefined;
|
|
3044
|
-
const producer = instance.steps[this.companionProducerIndex(instance, stepIndex)];
|
|
3045
|
-
this.loopCompanionProducer(instance, stepIndex, {
|
|
3059
|
+
const producer = instance.steps[this.stepGraph.companionProducerIndex(instance, stepIndex)];
|
|
3060
|
+
this.stepGraph.loopCompanionProducer(instance, stepIndex, {
|
|
3046
3061
|
previousProposal: producer?.output ?? '',
|
|
3047
3062
|
feedback: step.companion.verdicts.at(-1)?.feedback ?? '',
|
|
3048
3063
|
});
|
|
@@ -3060,7 +3075,7 @@ export class ExecutionService {
|
|
|
3060
3075
|
// so infer the block's `technical` label here too — best-effort, human-authority
|
|
3061
3076
|
// preserved — before advancing.
|
|
3062
3077
|
if (step.agentKind === 'spec-companion') {
|
|
3063
|
-
const producer = instance.steps[this.companionProducerIndex(instance, stepIndex)];
|
|
3078
|
+
const producer = instance.steps[this.stepGraph.companionProducerIndex(instance, stepIndex)];
|
|
3064
3079
|
const block = await this.blockRepository.get(workspaceId, instance.blockId);
|
|
3065
3080
|
if (producer && block)
|
|
3066
3081
|
await this.inferBlockTechnical(workspaceId, block, producer, step);
|
|
@@ -3081,7 +3096,7 @@ export class ExecutionService {
|
|
|
3081
3096
|
async advancePastResolvedGate(workspaceId, instance, stepIndex) {
|
|
3082
3097
|
const step = instance.steps[stepIndex];
|
|
3083
3098
|
const decisionId = step.approval.id;
|
|
3084
|
-
this.finishStep(step);
|
|
3099
|
+
this.stepGraph.finishStep(step);
|
|
3085
3100
|
step.progress = 1;
|
|
3086
3101
|
const isFinalStep = stepIndex === instance.steps.length - 1;
|
|
3087
3102
|
if (isFinalStep) {
|
|
@@ -3093,7 +3108,7 @@ export class ExecutionService {
|
|
|
3093
3108
|
instance.currentStep = stepIndex + 1;
|
|
3094
3109
|
const next = instance.steps[instance.currentStep];
|
|
3095
3110
|
if (next)
|
|
3096
|
-
this.startStep(next);
|
|
3111
|
+
this.stepGraph.startStep(next);
|
|
3097
3112
|
if (instance.status === 'blocked')
|
|
3098
3113
|
instance.status = 'running';
|
|
3099
3114
|
await this.updateBlockProgress(workspaceId, instance, 'in_progress');
|
|
@@ -3541,7 +3556,7 @@ export class ExecutionService {
|
|
|
3541
3556
|
if (!step || !step.decision)
|
|
3542
3557
|
throw new NotFoundError('Decision', decisionId);
|
|
3543
3558
|
step.decision.chosen = choice;
|
|
3544
|
-
this.startStep(step);
|
|
3559
|
+
this.stepGraph.startStep(step);
|
|
3545
3560
|
if (instance.status === 'blocked')
|
|
3546
3561
|
instance.status = 'running';
|
|
3547
3562
|
await this.updateBlockProgress(workspaceId, instance, 'in_progress');
|
|
@@ -3630,7 +3645,7 @@ export class ExecutionService {
|
|
|
3630
3645
|
// including the companion, then the companion re-grades. Does NOT touch the
|
|
3631
3646
|
// companion's automatic-rework budget — a human-driven iteration is unbounded.
|
|
3632
3647
|
const previousProposal = producer.output ?? step.approval.proposal;
|
|
3633
|
-
this.rerunProducerThrough(instance, producerIndex, stepIndex, {
|
|
3648
|
+
this.stepGraph.rerunProducerThrough(instance, producerIndex, stepIndex, {
|
|
3634
3649
|
previousProposal,
|
|
3635
3650
|
feedback: review.feedback ?? '',
|
|
3636
3651
|
...(review.comments?.length ? { comments: review.comments } : {}),
|
|
@@ -3650,7 +3665,7 @@ export class ExecutionService {
|
|
|
3650
3665
|
// start/finish times this attempt rather than spanning the human gate wait.
|
|
3651
3666
|
step.startedAt = null;
|
|
3652
3667
|
step.finishedAt = null;
|
|
3653
|
-
this.startStep(step);
|
|
3668
|
+
this.stepGraph.startStep(step);
|
|
3654
3669
|
if (instance.status === 'blocked')
|
|
3655
3670
|
instance.status = 'running';
|
|
3656
3671
|
await this.executionRepository.upsert(workspaceId, instance);
|