@cat-factory/orchestration 0.37.1 → 0.37.3
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/modules/execution/CompanionController.d.ts +6 -11
- package/dist/modules/execution/CompanionController.d.ts.map +1 -1
- package/dist/modules/execution/CompanionController.js +20 -20
- package/dist/modules/execution/CompanionController.js.map +1 -1
- package/dist/modules/execution/ExecutionService.d.ts +25 -192
- package/dist/modules/execution/ExecutionService.d.ts.map +1 -1
- package/dist/modules/execution/ExecutionService.js +128 -610
- package/dist/modules/execution/ExecutionService.js.map +1 -1
- package/dist/modules/execution/HumanTestController.d.ts +6 -8
- package/dist/modules/execution/HumanTestController.d.ts.map +1 -1
- package/dist/modules/execution/HumanTestController.js +26 -26
- package/dist/modules/execution/HumanTestController.js.map +1 -1
- package/dist/modules/execution/ReviewGateController.d.ts +11 -19
- package/dist/modules/execution/ReviewGateController.d.ts.map +1 -1
- package/dist/modules/execution/ReviewGateController.js +19 -19
- package/dist/modules/execution/ReviewGateController.js.map +1 -1
- package/dist/modules/execution/RunStateMachine.d.ts +131 -0
- package/dist/modules/execution/RunStateMachine.d.ts.map +1 -0
- package/dist/modules/execution/RunStateMachine.js +389 -0
- package/dist/modules/execution/RunStateMachine.js.map +1 -0
- package/dist/modules/execution/TesterController.d.ts +3 -4
- package/dist/modules/execution/TesterController.d.ts.map +1 -1
- package/dist/modules/execution/TesterController.js +6 -6
- package/dist/modules/execution/TesterController.js.map +1 -1
- package/dist/modules/execution/VisualConfirmationController.d.ts +6 -8
- package/dist/modules/execution/VisualConfirmationController.d.ts.map +1 -1
- package/dist/modules/execution/VisualConfirmationController.js +18 -18
- package/dist/modules/execution/VisualConfirmationController.js.map +1 -1
- package/dist/modules/execution/gate-window-facades.d.ts +107 -0
- package/dist/modules/execution/gate-window-facades.d.ts.map +1 -0
- package/dist/modules/execution/gate-window-facades.js +114 -0
- package/dist/modules/execution/gate-window-facades.js.map +1 -0
- package/package.json +3 -3
|
@@ -12,9 +12,11 @@ import { DEFAULT_FOLLOW_UP_MAX_LOOPS, FOLLOW_UP_PRODUCER_KIND, followUpsToSendBa
|
|
|
12
12
|
import { AgentContextBuilder, } from './AgentContextBuilder.js';
|
|
13
13
|
import { CompanionController } from './CompanionController.js';
|
|
14
14
|
import { StepGraph } from './StepGraph.js';
|
|
15
|
+
import { RunStateMachine } from './RunStateMachine.js';
|
|
15
16
|
import { inferTechnicalLabel } from './technical.logic.js';
|
|
16
17
|
import { MergeResolver } from './MergeResolver.js';
|
|
17
18
|
import { ReviewGateController } from './ReviewGateController.js';
|
|
19
|
+
import { BrainstormActions, ClarityReviewActions, RequirementReviewActions, } from './gate-window-facades.js';
|
|
18
20
|
import { TesterController } from './TesterController.js';
|
|
19
21
|
import { HumanTestController } from './HumanTestController.js';
|
|
20
22
|
import { VisualConfirmationController } from './VisualConfirmationController.js';
|
|
@@ -27,22 +29,6 @@ import { requireWorkspace } from '@cat-factory/kernel';
|
|
|
27
29
|
import { planResumedSteps, planRestartFromStep } from './retry.logic.js';
|
|
28
30
|
import { isContainerEvictionError, isTransientEviction, MAX_EVICTION_RECOVERIES, MAX_TRANSIENT_EVICTION_RECOVERIES, } from './job.logic.js';
|
|
29
31
|
import { decideTesterInfra, resolveTesterEnvironment, TESTER_INFRA_MESSAGES, } from './tester-infra.logic.js';
|
|
30
|
-
/**
|
|
31
|
-
* "What to do next" guidance per failure kind a pipeline run can produce, shown
|
|
32
|
-
* under the failure banner on the board (mirrors bootstrap's FAILURE_HINTS). Only
|
|
33
|
-
* the execution-relevant subset of {@link AgentFailureKind} is keyed.
|
|
34
|
-
*/
|
|
35
|
-
const EXECUTION_FAILURE_HINTS = {
|
|
36
|
-
agent: 'An agent step failed after its automatic retries. Review the run, then retry to re-run the pipeline.',
|
|
37
|
-
job_failed: 'The implementation container reported a failure. Inspect its logs (Cloudflare Workers Observability, filtered by the run id), then retry to spin a fresh container.',
|
|
38
|
-
evicted: 'The implementation container kept vanishing mid-run even after automatic fresh-container restarts. Most often this is transient: a deploy / new-version rollout draining the container, in which case simply retrying once the rollout has finished succeeds. If it persists, it points at a memory or crash issue on the run — inspect its logs (Cloudflare Workers Observability, filtered by the run id) and consider a heavier container instance type. Retry to try again.',
|
|
39
|
-
timeout: 'The run exceeded its time budget — a step or the implementation job did not finish in time. Retry to start it again.',
|
|
40
|
-
rejected: 'You rejected this step’s proposal, stopping the run. Retry to re-run the pipeline from the rejected step.',
|
|
41
|
-
companion_rejected: 'A companion agent could not return a usable quality assessment (its reply was truncated or malformed) even after a repair retry. Review the companion’s raw output on the run, then retry.',
|
|
42
|
-
cancelled: 'You stopped this run; its container was killed. Retry to start it again.',
|
|
43
|
-
dispatch: 'The agent’s container could not be started — the run never began executing. The provider/runtime’s verbatim response is shown below. Most often this is transient (a capacity blip or a new-version rollout); retrying spins a fresh container. If it persists it points at a misconfigured container binding/image or runner pool. Retry to try again.',
|
|
44
|
-
unknown: 'The run failed for an unclassified reason. Review the run, then retry.',
|
|
45
|
-
};
|
|
46
32
|
/**
|
|
47
33
|
* Step kinds whose run details surface the ephemeral-environment lifecycle: the
|
|
48
34
|
* `deployer` provisions it and the `tester`/`playwright` exercise it. Used to gate
|
|
@@ -91,23 +77,21 @@ export class ExecutionService {
|
|
|
91
77
|
blockRepository;
|
|
92
78
|
pipelineRepository;
|
|
93
79
|
executionRepository;
|
|
94
|
-
accountRepository;
|
|
95
80
|
idGenerator;
|
|
96
81
|
clock;
|
|
97
82
|
/** The pure step/cursor mutators (start/finish/park/reset + the companion rework loop). */
|
|
98
83
|
stepGraph;
|
|
84
|
+
/** The async instance/block state-machine spine (persist/emit/park/advance/finalize/fail). */
|
|
85
|
+
runStateMachine;
|
|
99
86
|
agentExecutor;
|
|
100
87
|
workRunner;
|
|
101
88
|
events;
|
|
102
89
|
board;
|
|
103
90
|
spend;
|
|
104
91
|
requirementReviewService;
|
|
105
|
-
kaizenScheduler;
|
|
106
92
|
clarityReviewService;
|
|
107
93
|
brainstormServices;
|
|
108
94
|
environmentProvisioning;
|
|
109
|
-
environmentTeardown;
|
|
110
|
-
branchUpdater;
|
|
111
95
|
/** Assembles the per-step agent context (requirements, docs, env, service frame, fragments). */
|
|
112
96
|
contextBuilder;
|
|
113
97
|
/** Resolves a `merger` step's assessment into an auto-merge or a `merge_review` notification. */
|
|
@@ -129,10 +113,15 @@ export class ExecutionService {
|
|
|
129
113
|
/** The two brainstorm (structured-dialogue) subjects for {@link reviewGate}, by stage. */
|
|
130
114
|
requirementsBrainstormKind;
|
|
131
115
|
architectureBrainstormKind;
|
|
116
|
+
/** Requirements-review window actions (exposed via {@link requirementsReview}). */
|
|
117
|
+
requirementsReviewActions;
|
|
118
|
+
/** Clarity-review (bug triage) window actions (exposed via {@link clarityReview}). */
|
|
119
|
+
clarityReviewActions;
|
|
120
|
+
/** Brainstorm window actions (exposed via {@link brainstorm}). */
|
|
121
|
+
brainstormActions;
|
|
132
122
|
blueprintReconciler;
|
|
133
123
|
notificationService;
|
|
134
124
|
workspaceSettingsService;
|
|
135
|
-
llmObservability;
|
|
136
125
|
prMerger;
|
|
137
126
|
mergePresetRepository;
|
|
138
127
|
ticketTrackerProvider;
|
|
@@ -177,22 +166,32 @@ export class ExecutionService {
|
|
|
177
166
|
this.blockRepository = blockRepository;
|
|
178
167
|
this.pipelineRepository = pipelineRepository;
|
|
179
168
|
this.executionRepository = executionRepository;
|
|
180
|
-
this.accountRepository = accountRepository;
|
|
181
169
|
this.idGenerator = idGenerator;
|
|
182
170
|
this.clock = clock;
|
|
183
171
|
this.stepGraph = new StepGraph(clock);
|
|
172
|
+
this.runStateMachine = new RunStateMachine({
|
|
173
|
+
executionRepository,
|
|
174
|
+
blockRepository,
|
|
175
|
+
events: executionEventPublisher,
|
|
176
|
+
workRunner,
|
|
177
|
+
agentExecutor,
|
|
178
|
+
idGenerator,
|
|
179
|
+
clock,
|
|
180
|
+
stepGraph: this.stepGraph,
|
|
181
|
+
notificationService,
|
|
182
|
+
kaizenScheduler,
|
|
183
|
+
subscriptionActivations: subscriptionActivationRepository,
|
|
184
|
+
llmObservability,
|
|
185
|
+
});
|
|
184
186
|
this.agentExecutor = agentExecutor;
|
|
185
187
|
this.workRunner = workRunner;
|
|
186
188
|
this.events = executionEventPublisher;
|
|
187
189
|
this.board = boardService;
|
|
188
190
|
this.spend = spendService;
|
|
189
191
|
this.requirementReviewService = requirementReviewService;
|
|
190
|
-
this.kaizenScheduler = kaizenScheduler;
|
|
191
192
|
this.clarityReviewService = clarityReviewService;
|
|
192
193
|
this.brainstormServices = brainstormServices;
|
|
193
194
|
this.environmentProvisioning = environmentProvisioning;
|
|
194
|
-
this.environmentTeardown = environmentTeardown;
|
|
195
|
-
this.branchUpdater = branchUpdater;
|
|
196
195
|
this.contextBuilder = new AgentContextBuilder({
|
|
197
196
|
workspaceRepository,
|
|
198
197
|
blockRepository,
|
|
@@ -219,17 +218,8 @@ export class ExecutionService {
|
|
|
219
218
|
idGenerator,
|
|
220
219
|
previewStepModel: (ctx) => this.previewStepModel(ctx),
|
|
221
220
|
runAgent: (ctx, opts) => this.runAgent(ctx, opts),
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
pauseStepForInput: (s) => this.stepGraph.pauseStepForInput(s),
|
|
225
|
-
updateBlockProgress: (ws, i, st) => this.updateBlockProgress(ws, i, st),
|
|
226
|
-
persistInstance: (ws, i) => this.executionRepository.upsert(ws, i),
|
|
227
|
-
emitInstance: (ws, i) => this.emitInstance(ws, i),
|
|
228
|
-
stopRunContainer: (ws, i) => this.stopRunContainer(ws, i),
|
|
229
|
-
finalizeBlock: (ws, i, c) => this.finalizeBlock(ws, i, c),
|
|
230
|
-
parkStepOnDecision: (ws, i, s, p) => this.parkStepOnDecision(ws, i, s, p),
|
|
231
|
-
raiseDecisionRequired: (ws, i) => this.raiseDecisionRequired(ws, i),
|
|
232
|
-
loopCompanionProducer: (i, ci, rw) => this.stepGraph.loopCompanionProducer(i, ci, rw),
|
|
221
|
+
stateMachine: this.runStateMachine,
|
|
222
|
+
stepGraph: this.stepGraph,
|
|
233
223
|
inferTechnicalLabel: (ws, block, producer, companionStep) => this.inferBlockTechnical(ws, block, producer, companionStep),
|
|
234
224
|
});
|
|
235
225
|
this.testerController = new TesterController({
|
|
@@ -238,9 +228,7 @@ export class ExecutionService {
|
|
|
238
228
|
agentExecutor,
|
|
239
229
|
contextBuilder: this.contextBuilder,
|
|
240
230
|
resolveMergePreset: (ws, block) => this.resolveMergePreset(ws, block),
|
|
241
|
-
|
|
242
|
-
persistInstance: (ws, i) => this.executionRepository.upsert(ws, i),
|
|
243
|
-
emitInstance: (ws, i) => this.emitInstance(ws, i),
|
|
231
|
+
stateMachine: this.runStateMachine,
|
|
244
232
|
});
|
|
245
233
|
this.humanTestController = new HumanTestController({
|
|
246
234
|
blockRepository,
|
|
@@ -273,14 +261,8 @@ export class ExecutionService {
|
|
|
273
261
|
: {}),
|
|
274
262
|
...(branchUpdater ? { branchUpdater } : {}),
|
|
275
263
|
resolveMergePreset: (ws, block) => this.resolveMergePreset(ws, block),
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
startStep: (s) => this.stepGraph.startStep(s),
|
|
279
|
-
updateBlockProgress: (ws, i, st) => this.updateBlockProgress(ws, i, st),
|
|
280
|
-
finalizeBlock: (ws, i, c) => this.finalizeBlock(ws, i, c),
|
|
281
|
-
stopRunContainer: (ws, i) => this.stopRunContainer(ws, i),
|
|
282
|
-
persistInstance: (ws, i) => this.executionRepository.upsert(ws, i),
|
|
283
|
-
emitInstance: (ws, i) => this.emitInstance(ws, i),
|
|
264
|
+
stateMachine: this.runStateMachine,
|
|
265
|
+
stepGraph: this.stepGraph,
|
|
284
266
|
clockNow: () => this.clock.now(),
|
|
285
267
|
});
|
|
286
268
|
this.visualConfirmationController = new VisualConfirmationController({
|
|
@@ -292,41 +274,32 @@ export class ExecutionService {
|
|
|
292
274
|
notificationService,
|
|
293
275
|
...(resolveBinaryArtifactStore ? { resolveBinaryArtifactStore } : {}),
|
|
294
276
|
resolveMergePreset: (ws, block) => this.resolveMergePreset(ws, block),
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
startStep: (s) => this.stepGraph.startStep(s),
|
|
298
|
-
updateBlockProgress: (ws, i, st) => this.updateBlockProgress(ws, i, st),
|
|
299
|
-
finalizeBlock: (ws, i, c) => this.finalizeBlock(ws, i, c),
|
|
300
|
-
stopRunContainer: (ws, i) => this.stopRunContainer(ws, i),
|
|
301
|
-
persistInstance: (ws, i) => this.executionRepository.upsert(ws, i),
|
|
302
|
-
emitInstance: (ws, i) => this.emitInstance(ws, i),
|
|
277
|
+
stateMachine: this.runStateMachine,
|
|
278
|
+
stepGraph: this.stepGraph,
|
|
303
279
|
clockNow: () => this.clock.now(),
|
|
304
280
|
});
|
|
305
281
|
this.reviewGate = new ReviewGateController({
|
|
306
282
|
blockRepository,
|
|
307
283
|
executionRepository,
|
|
308
284
|
workRunner,
|
|
285
|
+
stateMachine: this.runStateMachine,
|
|
286
|
+
stepGraph: this.stepGraph,
|
|
309
287
|
resolveMergePreset: (ws, block) => this.resolveMergePreset(ws, block),
|
|
310
|
-
parkStepOnDecision: (ws, i, s, p) => this.parkStepOnDecision(ws, i, s, p),
|
|
311
|
-
advancePastResolvedGate: (ws, i, idx) => this.advancePastResolvedGate(ws, i, idx),
|
|
312
288
|
dispatchIterationCap: (ws, blockId, choice, handlers) => this.dispatchIterationCap(ws, blockId, choice, handlers),
|
|
313
|
-
raiseDecisionRequired: (ws, i) => this.raiseDecisionRequired(ws, i),
|
|
314
|
-
finishStep: (s) => this.stepGraph.finishStep(s),
|
|
315
|
-
startStep: (s) => this.stepGraph.startStep(s),
|
|
316
|
-
updateBlockProgress: (ws, i, st) => this.updateBlockProgress(ws, i, st),
|
|
317
|
-
finalizeBlock: (ws, i, c) => this.finalizeBlock(ws, i, c),
|
|
318
|
-
stopRunContainer: (ws, i) => this.stopRunContainer(ws, i),
|
|
319
|
-
persistInstance: (ws, i) => this.executionRepository.upsert(ws, i),
|
|
320
|
-
emitInstance: (ws, i) => this.emitInstance(ws, i),
|
|
321
289
|
});
|
|
322
290
|
this.requirementsKind = this.buildRequirementsKind();
|
|
323
291
|
this.clarityKind = this.buildClarityKind();
|
|
324
292
|
this.requirementsBrainstormKind = this.buildBrainstormKind('requirements', REQUIREMENTS_BRAINSTORM_AGENT_KIND);
|
|
325
293
|
this.architectureBrainstormKind = this.buildBrainstormKind('architecture', ARCHITECTURE_BRAINSTORM_AGENT_KIND);
|
|
294
|
+
// Group the per-feature gate-window actions into cohesive sub-facades (exposed as
|
|
295
|
+
// getters below) so they stop bloating the engine's public surface as ~30 near-identical
|
|
296
|
+
// 3-line delegations. They close over the same shared collaborators the handlers use.
|
|
297
|
+
this.requirementsReviewActions = new RequirementReviewActions(this.reviewGate, this.requirementsKind);
|
|
298
|
+
this.clarityReviewActions = new ClarityReviewActions(this.reviewGate, this.clarityKind);
|
|
299
|
+
this.brainstormActions = new BrainstormActions(this.reviewGate, (stage) => this.brainstormKindFor(stage));
|
|
326
300
|
this.blueprintReconciler = blueprintReconciler;
|
|
327
301
|
this.notificationService = notificationService;
|
|
328
302
|
this.workspaceSettingsService = workspaceSettingsService;
|
|
329
|
-
this.llmObservability = llmObservability;
|
|
330
303
|
this.prMerger = pullRequestMerger;
|
|
331
304
|
this.mergePresetRepository = mergePresetRepository;
|
|
332
305
|
this.ticketTrackerProvider = ticketTrackerProvider;
|
|
@@ -340,6 +313,30 @@ export class ExecutionService {
|
|
|
340
313
|
this.resolveRequireEnvironmentProvider = resolveRequireEnvironmentProvider;
|
|
341
314
|
this.assertAgentBackendConfigured = assertAgentBackendConfigured;
|
|
342
315
|
}
|
|
316
|
+
// ---- gate-window action sub-facades -------------------------------------
|
|
317
|
+
// Per-feature groupings of the dedicated review/test window actions, consumed by the
|
|
318
|
+
// matching server controllers. See {@link gate-window-facades}. The `executionService` is
|
|
319
|
+
// still the single injected object, so the runtimes stay symmetric (no composition-root edit).
|
|
320
|
+
/** Requirements-review window actions (run / incorporate / re-review / proceed / …). */
|
|
321
|
+
get requirementsReview() {
|
|
322
|
+
return this.requirementsReviewActions;
|
|
323
|
+
}
|
|
324
|
+
/** Clarity-review (bug-report triage) window actions. */
|
|
325
|
+
get clarityReview() {
|
|
326
|
+
return this.clarityReviewActions;
|
|
327
|
+
}
|
|
328
|
+
/** Brainstorm (structured-dialogue) window actions, keyed by stage. */
|
|
329
|
+
get brainstorm() {
|
|
330
|
+
return this.brainstormActions;
|
|
331
|
+
}
|
|
332
|
+
/** Human-testing gate window actions (confirm / request-fix / pull-main / recreate / destroy). */
|
|
333
|
+
get humanTest() {
|
|
334
|
+
return this.humanTestController;
|
|
335
|
+
}
|
|
336
|
+
/** Visual-confirmation gate window actions (approve / request-fix / recapture). */
|
|
337
|
+
get visualConfirm() {
|
|
338
|
+
return this.visualConfirmationController;
|
|
339
|
+
}
|
|
343
340
|
requireWorkspace(workspaceId) {
|
|
344
341
|
return requireWorkspace(this.workspaceRepository, workspaceId);
|
|
345
342
|
}
|
|
@@ -636,7 +633,7 @@ export class ExecutionService {
|
|
|
636
633
|
// a browser open. With the no-op runner (tests) this does nothing and the run
|
|
637
634
|
// is advanced directly via advanceInstance.
|
|
638
635
|
await this.workRunner.startRun(workspaceId, instance.id);
|
|
639
|
-
await this.emitInstance(workspaceId, instance);
|
|
636
|
+
await this.runStateMachine.emitInstance(workspaceId, instance);
|
|
640
637
|
return instance;
|
|
641
638
|
}
|
|
642
639
|
/**
|
|
@@ -759,10 +756,10 @@ export class ExecutionService {
|
|
|
759
756
|
// auto-passed, or the run reached a terminal state) clear that waiting card so the
|
|
760
757
|
// escalation sweep can't later flip a settled decision red ("Overdue").
|
|
761
758
|
if (result.kind === 'awaiting_decision') {
|
|
762
|
-
await this.ensureWaitingNotification(workspaceId, instance);
|
|
759
|
+
await this.runStateMachine.ensureWaitingNotification(workspaceId, instance);
|
|
763
760
|
}
|
|
764
761
|
else {
|
|
765
|
-
await this.clearWaitingNotification(workspaceId, instance);
|
|
762
|
+
await this.runStateMachine.clearWaitingNotification(workspaceId, instance);
|
|
766
763
|
}
|
|
767
764
|
return result;
|
|
768
765
|
}
|
|
@@ -784,7 +781,7 @@ export class ExecutionService {
|
|
|
784
781
|
if (instance.status !== 'paused') {
|
|
785
782
|
instance.status = 'paused';
|
|
786
783
|
await this.executionRepository.upsert(workspaceId, instance);
|
|
787
|
-
await this.emitInstance(workspaceId, instance);
|
|
784
|
+
await this.runStateMachine.emitInstance(workspaceId, instance);
|
|
788
785
|
}
|
|
789
786
|
return { kind: 'paused' };
|
|
790
787
|
}
|
|
@@ -818,7 +815,7 @@ export class ExecutionService {
|
|
|
818
815
|
if (pendingId) {
|
|
819
816
|
instance.status = 'blocked';
|
|
820
817
|
await this.executionRepository.upsert(workspaceId, instance);
|
|
821
|
-
await this.emitInstance(workspaceId, instance);
|
|
818
|
+
await this.runStateMachine.emitInstance(workspaceId, instance);
|
|
822
819
|
return { kind: 'awaiting_decision', decisionId: pendingId };
|
|
823
820
|
}
|
|
824
821
|
}
|
|
@@ -891,7 +888,7 @@ export class ExecutionService {
|
|
|
891
888
|
// phase, so a run's details show the env spinning up next to the container.
|
|
892
889
|
await this.attachEnvironmentProjection(workspaceId, instance.blockId, step);
|
|
893
890
|
await this.executionRepository.upsert(workspaceId, instance);
|
|
894
|
-
await this.emitInstance(workspaceId, instance);
|
|
891
|
+
await this.runStateMachine.emitInstance(workspaceId, instance);
|
|
895
892
|
let handle;
|
|
896
893
|
try {
|
|
897
894
|
handle = await executor.startJob(context);
|
|
@@ -904,7 +901,7 @@ export class ExecutionService {
|
|
|
904
901
|
// "run failed". A dispatch-time eviction still routes to the evicted framing.
|
|
905
902
|
step.startingContainer = false;
|
|
906
903
|
await this.executionRepository.upsert(workspaceId, instance);
|
|
907
|
-
await this.emitInstance(workspaceId, instance);
|
|
904
|
+
await this.runStateMachine.emitInstance(workspaceId, instance);
|
|
908
905
|
const message = getErrorMessage(error);
|
|
909
906
|
const evicted = isContainerEvictionError(message);
|
|
910
907
|
return {
|
|
@@ -921,7 +918,7 @@ export class ExecutionService {
|
|
|
921
918
|
// The dispatch returned, so the container is up and execution has begun.
|
|
922
919
|
step.startingContainer = false;
|
|
923
920
|
await this.executionRepository.upsert(workspaceId, instance);
|
|
924
|
-
await this.emitInstance(workspaceId, instance);
|
|
921
|
+
await this.runStateMachine.emitInstance(workspaceId, instance);
|
|
925
922
|
}
|
|
926
923
|
return { kind: 'awaiting_job', jobId: step.jobId, stepIndex: instance.currentStep };
|
|
927
924
|
}
|
|
@@ -932,7 +929,7 @@ export class ExecutionService {
|
|
|
932
929
|
if (previewModel && previewModel !== step.model) {
|
|
933
930
|
step.model = previewModel;
|
|
934
931
|
await this.executionRepository.upsert(workspaceId, instance);
|
|
935
|
-
await this.emitInstance(workspaceId, instance);
|
|
932
|
+
await this.runStateMachine.emitInstance(workspaceId, instance);
|
|
936
933
|
}
|
|
937
934
|
const result = await this.runAgent(context, options);
|
|
938
935
|
return this.recordStepResult(workspaceId, instance, step, isFinalStep, result);
|
|
@@ -1069,7 +1066,7 @@ export class ExecutionService {
|
|
|
1069
1066
|
}
|
|
1070
1067
|
if (changed) {
|
|
1071
1068
|
await this.executionRepository.upsert(workspaceId, instance);
|
|
1072
|
-
await this.emitInstance(workspaceId, instance);
|
|
1069
|
+
await this.runStateMachine.emitInstance(workspaceId, instance);
|
|
1073
1070
|
}
|
|
1074
1071
|
return { kind: 'awaiting_job', jobId: step.jobId, stepIndex: instance.currentStep };
|
|
1075
1072
|
}
|
|
@@ -1158,7 +1155,7 @@ export class ExecutionService {
|
|
|
1158
1155
|
if (step.gate)
|
|
1159
1156
|
step.gate.phase = 'checking';
|
|
1160
1157
|
await this.executionRepository.upsert(workspaceId, instance);
|
|
1161
|
-
await this.emitInstance(workspaceId, instance);
|
|
1158
|
+
await this.runStateMachine.emitInstance(workspaceId, instance);
|
|
1162
1159
|
return { kind: 'awaiting_gate', stepIndex: instance.currentStep };
|
|
1163
1160
|
}
|
|
1164
1161
|
// A `tester` step in its `fixing` phase has a Fixer job in flight, NOT the
|
|
@@ -1175,7 +1172,7 @@ export class ExecutionService {
|
|
|
1175
1172
|
// Reclaim the finished Fixer container before re-dispatching the Tester so it
|
|
1176
1173
|
// boots fresh against the just-pushed fixes (rather than re-attaching to the
|
|
1177
1174
|
// completed job by run id).
|
|
1178
|
-
await this.stopRunContainer(workspaceId, instance);
|
|
1175
|
+
await this.runStateMachine.stopRunContainer(workspaceId, instance);
|
|
1179
1176
|
return this.testerController.dispatchTester(workspaceId, instance, step, block);
|
|
1180
1177
|
}
|
|
1181
1178
|
// A `human-test` gate in its `fixing` / `resolving_conflicts` phase has a helper job
|
|
@@ -1225,7 +1222,7 @@ export class ExecutionService {
|
|
|
1225
1222
|
step.subtasks = undefined;
|
|
1226
1223
|
step.progress = 0;
|
|
1227
1224
|
await this.executionRepository.upsert(workspaceId, instance);
|
|
1228
|
-
await this.emitInstance(workspaceId, instance);
|
|
1225
|
+
await this.runStateMachine.emitInstance(workspaceId, instance);
|
|
1229
1226
|
return { kind: 'continue' };
|
|
1230
1227
|
}
|
|
1231
1228
|
return {
|
|
@@ -1407,19 +1404,19 @@ export class ExecutionService {
|
|
|
1407
1404
|
this.stepGraph.finishStep(step);
|
|
1408
1405
|
if (isFinalStep) {
|
|
1409
1406
|
instance.status = 'done';
|
|
1410
|
-
await this.finalizeBlock(workspaceId, instance, undefined);
|
|
1407
|
+
await this.runStateMachine.finalizeBlock(workspaceId, instance, undefined);
|
|
1411
1408
|
await this.executionRepository.upsert(workspaceId, instance);
|
|
1412
|
-
await this.emitInstance(workspaceId, instance);
|
|
1413
|
-
await this.stopRunContainer(workspaceId, instance);
|
|
1409
|
+
await this.runStateMachine.emitInstance(workspaceId, instance);
|
|
1410
|
+
await this.runStateMachine.stopRunContainer(workspaceId, instance);
|
|
1414
1411
|
return { kind: 'done' };
|
|
1415
1412
|
}
|
|
1416
1413
|
instance.currentStep += 1;
|
|
1417
1414
|
const next = instance.steps[instance.currentStep];
|
|
1418
1415
|
if (next)
|
|
1419
1416
|
this.stepGraph.startStep(next);
|
|
1420
|
-
await this.updateBlockProgress(workspaceId, instance, 'in_progress');
|
|
1417
|
+
await this.runStateMachine.updateBlockProgress(workspaceId, instance, 'in_progress');
|
|
1421
1418
|
await this.executionRepository.upsert(workspaceId, instance);
|
|
1422
|
-
await this.emitInstance(workspaceId, instance);
|
|
1419
|
+
await this.runStateMachine.emitInstance(workspaceId, instance);
|
|
1423
1420
|
return { kind: 'continue' };
|
|
1424
1421
|
}
|
|
1425
1422
|
/**
|
|
@@ -1467,9 +1464,9 @@ export class ExecutionService {
|
|
|
1467
1464
|
};
|
|
1468
1465
|
this.stepGraph.pauseStepForInput(step);
|
|
1469
1466
|
instance.status = 'blocked';
|
|
1470
|
-
await this.updateBlockProgress(workspaceId, instance, 'blocked');
|
|
1467
|
+
await this.runStateMachine.updateBlockProgress(workspaceId, instance, 'blocked');
|
|
1471
1468
|
await this.executionRepository.upsert(workspaceId, instance);
|
|
1472
|
-
await this.emitInstance(workspaceId, instance);
|
|
1469
|
+
await this.runStateMachine.emitInstance(workspaceId, instance);
|
|
1473
1470
|
return { kind: 'awaiting_decision', decisionId: step.decision.id };
|
|
1474
1471
|
}
|
|
1475
1472
|
// Completion-path interceptors short-circuit before the normal finish/advance for the
|
|
@@ -1586,9 +1583,9 @@ export class ExecutionService {
|
|
|
1586
1583
|
};
|
|
1587
1584
|
this.stepGraph.pauseStepForInput(step);
|
|
1588
1585
|
instance.status = 'blocked';
|
|
1589
|
-
await this.updateBlockProgress(workspaceId, instance, 'blocked');
|
|
1586
|
+
await this.runStateMachine.updateBlockProgress(workspaceId, instance, 'blocked');
|
|
1590
1587
|
await this.executionRepository.upsert(workspaceId, instance);
|
|
1591
|
-
await this.emitInstance(workspaceId, instance);
|
|
1588
|
+
await this.runStateMachine.emitInstance(workspaceId, instance);
|
|
1592
1589
|
return { kind: 'awaiting_decision', decisionId: step.approval.id };
|
|
1593
1590
|
}
|
|
1594
1591
|
// Persist the agent's reported confidence whenever a step reports it, for board
|
|
@@ -1634,14 +1631,14 @@ export class ExecutionService {
|
|
|
1634
1631
|
// POSITION-INDEPENDENTLY: confidence at the top of recordStepResult and the merger's
|
|
1635
1632
|
// real merge via the step-completion resolver registry (so a trailing
|
|
1636
1633
|
// post-release-health gate doesn't disable auto-merge). Nothing merge-specific here.
|
|
1637
|
-
await this.finalizeBlock(workspaceId, instance, result.confidence);
|
|
1634
|
+
await this.runStateMachine.finalizeBlock(workspaceId, instance, result.confidence);
|
|
1638
1635
|
await this.executionRepository.upsert(workspaceId, instance);
|
|
1639
|
-
await this.emitInstance(workspaceId, instance);
|
|
1636
|
+
await this.runStateMachine.emitInstance(workspaceId, instance);
|
|
1640
1637
|
// The run is finished: reclaim its per-run container now instead of letting it
|
|
1641
1638
|
// idle out its sleepAfter window (~10 min of billed-but-useless compute). All
|
|
1642
1639
|
// pipeline steps share the one container keyed by the execution id, so this is
|
|
1643
1640
|
// only safe on the FINAL step — never between steps. Best-effort/idempotent.
|
|
1644
|
-
await this.stopRunContainer(workspaceId, instance);
|
|
1641
|
+
await this.runStateMachine.stopRunContainer(workspaceId, instance);
|
|
1645
1642
|
return { kind: 'done' };
|
|
1646
1643
|
}
|
|
1647
1644
|
instance.currentStep += 1;
|
|
@@ -1653,13 +1650,13 @@ export class ExecutionService {
|
|
|
1653
1650
|
// advance to a trailing step — refresh progress only, preserving that status. (The
|
|
1654
1651
|
// final step's `finalizeBlock` then leaves a `done` block alone.)
|
|
1655
1652
|
if (resolverOwnsTerminalStatus) {
|
|
1656
|
-
await this.refreshBlockProgress(workspaceId, instance);
|
|
1653
|
+
await this.runStateMachine.refreshBlockProgress(workspaceId, instance);
|
|
1657
1654
|
}
|
|
1658
1655
|
else {
|
|
1659
|
-
await this.updateBlockProgress(workspaceId, instance, 'in_progress');
|
|
1656
|
+
await this.runStateMachine.updateBlockProgress(workspaceId, instance, 'in_progress');
|
|
1660
1657
|
}
|
|
1661
1658
|
await this.executionRepository.upsert(workspaceId, instance);
|
|
1662
|
-
await this.emitInstance(workspaceId, instance);
|
|
1659
|
+
await this.runStateMachine.emitInstance(workspaceId, instance);
|
|
1663
1660
|
return { kind: 'continue' };
|
|
1664
1661
|
}
|
|
1665
1662
|
/**
|
|
@@ -2282,7 +2279,7 @@ export class ExecutionService {
|
|
|
2282
2279
|
// Keep polling. Persist the head sha + phase so the board can reflect it.
|
|
2283
2280
|
step.gate.phase = 'checking';
|
|
2284
2281
|
await this.executionRepository.upsert(workspaceId, instance);
|
|
2285
|
-
await this.emitInstance(workspaceId, instance);
|
|
2282
|
+
await this.runStateMachine.emitInstance(workspaceId, instance);
|
|
2286
2283
|
return { kind: 'awaiting_gate', stepIndex: instance.currentStep };
|
|
2287
2284
|
}
|
|
2288
2285
|
// probe.status === 'fail'.
|
|
@@ -2337,75 +2334,9 @@ export class ExecutionService {
|
|
|
2337
2334
|
headSha: step.gate?.headSha ?? null,
|
|
2338
2335
|
};
|
|
2339
2336
|
await this.executionRepository.upsert(workspaceId, instance);
|
|
2340
|
-
await this.emitInstance(workspaceId, instance);
|
|
2337
|
+
await this.runStateMachine.emitInstance(workspaceId, instance);
|
|
2341
2338
|
return { kind: 'awaiting_job', jobId: step.jobId, stepIndex: instance.currentStep };
|
|
2342
2339
|
}
|
|
2343
|
-
/**
|
|
2344
|
-
* Raise a `decision_required` notification when a run parks on an iteration-cap gate
|
|
2345
|
-
* after spending its automatic budget — a quality companion at its rework cap or an
|
|
2346
|
-
* iterative reviewer (requirements / clarity) at its iteration cap. Without it the
|
|
2347
|
-
* three-choice decision is reachable only by drilling into the parked step, so the run
|
|
2348
|
-
* looks silently stuck. Best-effort: a missing notification service (tests) or block is
|
|
2349
|
-
* a no-op.
|
|
2350
|
-
*/
|
|
2351
|
-
async raiseDecisionRequired(workspaceId, instance) {
|
|
2352
|
-
if (!this.notificationService)
|
|
2353
|
-
return;
|
|
2354
|
-
const block = await this.blockRepository.get(workspaceId, instance.blockId);
|
|
2355
|
-
if (!block)
|
|
2356
|
-
return;
|
|
2357
|
-
await this.notificationService.raise(workspaceId, {
|
|
2358
|
-
type: 'decision_required',
|
|
2359
|
-
blockId: block.id,
|
|
2360
|
-
executionId: instance.id,
|
|
2361
|
-
title: `"${block.title}" ran out of automatic iterations and needs your decision`,
|
|
2362
|
-
body: 'An automatic review loop reached its iteration cap without converging. Open the ' +
|
|
2363
|
-
'task to choose: one more round, proceed with the current result, or stop and reset.',
|
|
2364
|
-
payload: { pipelineName: instance.pipelineName },
|
|
2365
|
-
});
|
|
2366
|
-
}
|
|
2367
|
-
/**
|
|
2368
|
-
* Ensure an open notification exists for a run that has just parked waiting for a human
|
|
2369
|
-
* (an agent-raised decision, an approval gate, or an iterative review gate). Without
|
|
2370
|
-
* the old decision timeout the run waits indefinitely, so the inbox card — which the
|
|
2371
|
-
* periodic sweep escalates yellow → red — is the only signal a human is needed.
|
|
2372
|
-
*
|
|
2373
|
-
* Non-clobbering: if ANY open notification is already on the block (a more specific
|
|
2374
|
-
* `merge_review`, iteration-cap `decision_required`, etc.), it is left untouched and we
|
|
2375
|
-
* raise nothing — so the richer message wins. Best-effort: no notification service
|
|
2376
|
-
* (tests) or a missing block is a no-op.
|
|
2377
|
-
*/
|
|
2378
|
-
async ensureWaitingNotification(workspaceId, instance) {
|
|
2379
|
-
const svc = this.notificationService;
|
|
2380
|
-
if (!svc)
|
|
2381
|
-
return;
|
|
2382
|
-
const open = await svc.listOpen(workspaceId);
|
|
2383
|
-
if (open.some((n) => n.blockId === instance.blockId))
|
|
2384
|
-
return;
|
|
2385
|
-
const block = await this.blockRepository.get(workspaceId, instance.blockId);
|
|
2386
|
-
if (!block)
|
|
2387
|
-
return;
|
|
2388
|
-
await svc.raise(workspaceId, {
|
|
2389
|
-
type: 'decision_required',
|
|
2390
|
-
blockId: block.id,
|
|
2391
|
-
executionId: instance.id,
|
|
2392
|
-
title: `"${block.title}" is waiting for your input`,
|
|
2393
|
-
body: 'A pipeline step is parked awaiting a human decision. Open the task to respond.',
|
|
2394
|
-
payload: { pipelineName: instance.pipelineName },
|
|
2395
|
-
});
|
|
2396
|
-
}
|
|
2397
|
-
/**
|
|
2398
|
-
* Clear the auto-raised "waiting for a human decision" card once a run advances past
|
|
2399
|
-
* the decision it was parked on (so the escalation sweep can't flip a settled decision
|
|
2400
|
-
* red). Scoped to the `decision_required` type, so the human-actionable cards a stopped
|
|
2401
|
-
* run leaves behind are untouched. Best-effort: no notification service (tests) is a no-op.
|
|
2402
|
-
*/
|
|
2403
|
-
async clearWaitingNotification(workspaceId, instance) {
|
|
2404
|
-
const svc = this.notificationService;
|
|
2405
|
-
if (!svc)
|
|
2406
|
-
return;
|
|
2407
|
-
await svc.clearWaitingDecision(workspaceId, instance.blockId);
|
|
2408
|
-
}
|
|
2409
2340
|
// ---- Follow-up companion (future-looking Coder) -------------------------
|
|
2410
2341
|
// The Coder streams forward-looking items (loose ends / side-tasks / questions) which
|
|
2411
2342
|
// accrue on its `step.followUps` live (see pollAgentJob). At the Coder's completion the
|
|
@@ -2449,13 +2380,13 @@ export class ExecutionService {
|
|
|
2449
2380
|
return undefined;
|
|
2450
2381
|
if (hasPendingFollowUps(state)) {
|
|
2451
2382
|
await this.raiseFollowUpPending(workspaceId, instance, state);
|
|
2452
|
-
return this.parkStepOnDecision(workspaceId, instance, step);
|
|
2383
|
+
return this.runStateMachine.parkStepOnDecision(workspaceId, instance, step);
|
|
2453
2384
|
}
|
|
2454
2385
|
if (shouldLoopCoder(state)) {
|
|
2455
2386
|
this.loopCoderForFollowUps(instance, step);
|
|
2456
|
-
await this.updateBlockProgress(workspaceId, instance, 'in_progress');
|
|
2387
|
+
await this.runStateMachine.updateBlockProgress(workspaceId, instance, 'in_progress');
|
|
2457
2388
|
await this.executionRepository.upsert(workspaceId, instance);
|
|
2458
|
-
await this.emitInstance(workspaceId, instance);
|
|
2389
|
+
await this.runStateMachine.emitInstance(workspaceId, instance);
|
|
2459
2390
|
return { kind: 'continue' };
|
|
2460
2391
|
}
|
|
2461
2392
|
return undefined;
|
|
@@ -2624,19 +2555,19 @@ export class ExecutionService {
|
|
|
2624
2555
|
if (!parkedHere || hasPendingFollowUps(step.followUps)) {
|
|
2625
2556
|
// Still collecting decisions (or the run isn't parked on this gate): just record it.
|
|
2626
2557
|
await this.executionRepository.upsert(workspaceId, instance);
|
|
2627
|
-
await this.emitInstance(workspaceId, instance);
|
|
2558
|
+
await this.runStateMachine.emitInstance(workspaceId, instance);
|
|
2628
2559
|
return;
|
|
2629
2560
|
}
|
|
2630
2561
|
// Every item is decided and the run is parked here: clear the waiting card and either
|
|
2631
2562
|
// loop the Coder for the send-back items or advance past the gate.
|
|
2632
|
-
await this.clearWaitingNotification(workspaceId, instance);
|
|
2563
|
+
await this.runStateMachine.clearWaitingNotification(workspaceId, instance);
|
|
2633
2564
|
if (shouldLoopCoder(step.followUps)) {
|
|
2634
2565
|
const decisionId = step.approval.id;
|
|
2635
2566
|
this.loopCoderForFollowUps(instance, step);
|
|
2636
|
-
await this.updateBlockProgress(workspaceId, instance, 'in_progress');
|
|
2567
|
+
await this.runStateMachine.updateBlockProgress(workspaceId, instance, 'in_progress');
|
|
2637
2568
|
await this.executionRepository.upsert(workspaceId, instance);
|
|
2638
2569
|
await this.workRunner.signalDecision(workspaceId, instance.id, decisionId, 'approved');
|
|
2639
|
-
await this.emitInstance(workspaceId, instance);
|
|
2570
|
+
await this.runStateMachine.emitInstance(workspaceId, instance);
|
|
2640
2571
|
return;
|
|
2641
2572
|
}
|
|
2642
2573
|
// The follow-up gate is settled and we won't loop. If this step ALSO carries a human
|
|
@@ -2651,11 +2582,11 @@ export class ExecutionService {
|
|
|
2651
2582
|
if (step.requiresApproval && !isFinalStep && step.approval?.status === 'pending') {
|
|
2652
2583
|
step.approval = { ...step.approval, proposal: step.output ?? '' };
|
|
2653
2584
|
await this.executionRepository.upsert(workspaceId, instance);
|
|
2654
|
-
await this.ensureWaitingNotification(workspaceId, instance);
|
|
2655
|
-
await this.emitInstance(workspaceId, instance);
|
|
2585
|
+
await this.runStateMachine.ensureWaitingNotification(workspaceId, instance);
|
|
2586
|
+
await this.runStateMachine.emitInstance(workspaceId, instance);
|
|
2656
2587
|
return;
|
|
2657
2588
|
}
|
|
2658
|
-
await this.advancePastResolvedGate(workspaceId, instance, index);
|
|
2589
|
+
await this.runStateMachine.advancePastResolvedGate(workspaceId, instance, index);
|
|
2659
2590
|
}
|
|
2660
2591
|
/** Provision inputs (`{{input.*}}`) derived from the block under deployment. */
|
|
2661
2592
|
deployInputs(block) {
|
|
@@ -2765,23 +2696,6 @@ export class ExecutionService {
|
|
|
2765
2696
|
// state-machine primitives stay here — they are reused by the generic approval path and
|
|
2766
2697
|
// the companion iteration-cap gate, so they have a single home: {@link parkStepOnDecision},
|
|
2767
2698
|
// {@link advancePastResolvedGate} and {@link dispatchIterationCap}.
|
|
2768
|
-
/**
|
|
2769
|
-
* Park a step on the durable decision-wait the approval gate uses, so a human (or the
|
|
2770
|
-
* dedicated review window) can drive an iterative loop and resume the run. Shared by the
|
|
2771
|
-
* requirements gate and the companion iteration-cap gate: both reuse the SAME parking
|
|
2772
|
-
* mechanism rather than each rolling its own. `proposal` seeds the gate's stored text
|
|
2773
|
-
* (the companion's latest feedback; empty for the requirements window, which renders its
|
|
2774
|
-
* own structured surface via the universal result-view registry).
|
|
2775
|
-
*/
|
|
2776
|
-
async parkStepOnDecision(workspaceId, instance, step, proposal = '') {
|
|
2777
|
-
step.approval = { id: this.idGenerator.next('appr'), status: 'pending', proposal };
|
|
2778
|
-
this.stepGraph.pauseStepForInput(step);
|
|
2779
|
-
instance.status = 'blocked';
|
|
2780
|
-
await this.updateBlockProgress(workspaceId, instance, 'blocked');
|
|
2781
|
-
await this.executionRepository.upsert(workspaceId, instance);
|
|
2782
|
-
await this.emitInstance(workspaceId, instance);
|
|
2783
|
-
return { kind: 'awaiting_decision', decisionId: step.approval.id };
|
|
2784
|
-
}
|
|
2785
2699
|
/**
|
|
2786
2700
|
* Two gates park on a `step.approval` but are NOT generic prose approvals — they are
|
|
2787
2701
|
* iterative gates driven by their own dedicated surface, never the generic
|
|
@@ -2930,83 +2844,13 @@ export class ExecutionService {
|
|
|
2930
2844
|
? this.architectureBrainstormKind
|
|
2931
2845
|
: this.requirementsBrainstormKind;
|
|
2932
2846
|
}
|
|
2933
|
-
/** Run a fresh brainstorm pass over a block + stage (off-path inspector / window surface). */
|
|
2934
|
-
reviewBrainstorm(workspaceId, blockId, stage) {
|
|
2935
|
-
return this.reviewGate.review(this.brainstormKindFor(stage), workspaceId, blockId);
|
|
2936
|
-
}
|
|
2937
|
-
/** Incorporate the human's picks ASYNCHRONOUSLY (the brainstorm mirror of {@link incorporateRequirements}). */
|
|
2938
|
-
incorporateBrainstorm(workspaceId, blockId, stage, feedback) {
|
|
2939
|
-
return this.reviewGate.incorporate(this.brainstormKindFor(stage), workspaceId, blockId, feedback);
|
|
2940
|
-
}
|
|
2941
|
-
/** Re-run the brainstorm against the converged direction (one more pass). */
|
|
2942
|
-
reReviewBrainstorm(workspaceId, blockId, stage) {
|
|
2943
|
-
return this.reviewGate.reReview(this.brainstormKindFor(stage), workspaceId, blockId);
|
|
2944
|
-
}
|
|
2945
|
-
/** Proceed: settle the brainstorm (last converged direction wins downstream) and advance. */
|
|
2946
|
-
proceedBrainstorm(workspaceId, blockId, stage) {
|
|
2947
|
-
return this.reviewGate.proceed(this.brainstormKindFor(stage), workspaceId, blockId);
|
|
2948
|
-
}
|
|
2949
|
-
/** Resolve a brainstorm that hit its iteration cap (extra-round / proceed / stop-reset). */
|
|
2950
|
-
resolveBrainstormExceeded(workspaceId, blockId, stage, choice) {
|
|
2951
|
-
return this.reviewGate.resolveExceeded(this.brainstormKindFor(stage), workspaceId, blockId, choice);
|
|
2952
|
-
}
|
|
2953
|
-
/**
|
|
2954
|
-
* Run a fresh reviewer pass over a block's collected requirements, snapshotting the
|
|
2955
|
-
* task's merge-preset knobs (iteration budget + tolerated severity) onto the review.
|
|
2956
|
-
* Shared by the pipeline gate and the off-path inspector "Run review" surface, so both
|
|
2957
|
-
* honour the task's preset identically.
|
|
2958
|
-
*/
|
|
2959
|
-
reviewRequirements(workspaceId, blockId) {
|
|
2960
|
-
return this.reviewGate.review(this.requirementsKind, workspaceId, blockId);
|
|
2961
|
-
}
|
|
2962
|
-
/**
|
|
2963
|
-
* Incorporate the human's settled answers ASYNCHRONOUSLY. Validates that every finding is
|
|
2964
|
-
* answered/dismissed, flags the review `incorporating`, records the intent on the parked
|
|
2965
|
-
* gate step, and signals the durable driver to wake — which folds the answers and
|
|
2966
|
-
* re-reviews in the background. Off-path (no parked run) the fold + re-review run inline.
|
|
2967
|
-
*/
|
|
2968
|
-
incorporateRequirements(workspaceId, blockId, feedback) {
|
|
2969
|
-
return this.reviewGate.incorporate(this.requirementsKind, workspaceId, blockId, feedback);
|
|
2970
|
-
}
|
|
2971
|
-
/**
|
|
2972
|
-
* Re-review the incorporated document (one more reviewer pass). On convergence
|
|
2973
|
-
* (`incorporated`) the parked run advances; otherwise the window shows the next cycle
|
|
2974
|
-
* (`ready`) or the iteration-cap choices (`exceeded`).
|
|
2975
|
-
*/
|
|
2976
|
-
reReviewRequirements(workspaceId, blockId) {
|
|
2977
|
-
return this.reviewGate.reReview(this.requirementsKind, workspaceId, blockId);
|
|
2978
|
-
}
|
|
2979
|
-
/**
|
|
2980
|
-
* Proceed: settle the requirements (the last incorporated doc, if any, becomes what
|
|
2981
|
-
* downstream agents consume) and advance the parked run.
|
|
2982
|
-
*/
|
|
2983
|
-
proceedRequirements(workspaceId, blockId) {
|
|
2984
|
-
return this.reviewGate.proceed(this.requirementsKind, workspaceId, blockId);
|
|
2985
|
-
}
|
|
2986
|
-
/**
|
|
2987
|
-
* Ask the Requirement Writer to recommend answers for a batch of findings ASYNCHRONOUSLY:
|
|
2988
|
-
* append `pending` placeholder recommendations at once and signal the durable driver to run
|
|
2989
|
-
* the Writer per finding in the background (filling them in + notifying when done). Returns the
|
|
2990
|
-
* review with the placeholders so the SPA shows "generating…" and hands the user back.
|
|
2991
|
-
*/
|
|
2992
|
-
requestRecommendations(workspaceId, blockId, itemIds, note) {
|
|
2993
|
-
return this.reviewGate.requestRecommendations(this.requirementsKind, workspaceId, blockId, itemIds, note);
|
|
2994
|
-
}
|
|
2995
|
-
/**
|
|
2996
|
-
* Re-request a single recommendation with a "do it differently" note — resets it to `pending`
|
|
2997
|
-
* and drives the Writer through the same async path. Review-scoped (the re-request endpoint
|
|
2998
|
-
* addresses the recommendation by review id).
|
|
2999
|
-
*/
|
|
3000
|
-
reRequestRecommendation(workspaceId, reviewId, recId, note) {
|
|
3001
|
-
return this.reviewGate.reRequestRecommendation(this.requirementsKind, workspaceId, reviewId, recId, note);
|
|
3002
|
-
}
|
|
3003
2847
|
/**
|
|
3004
2848
|
* Route an iteration-cap resolution to its gate-specific handlers. `stop-reset` is
|
|
3005
2849
|
* uniform across gates: cancel the run and return the block to phase zero (editable),
|
|
3006
2850
|
* keeping whatever reference artifact each gate persists (the requirements doc on its
|
|
3007
2851
|
* own table; a companion's producer output on its branch). Shared by the requirements
|
|
3008
|
-
* gate ({@link
|
|
3009
|
-
* ({@link resolveCompanionExceeded}) so the three-way choice lives in one place.
|
|
2852
|
+
* gate (`requirementsReview.resolveExceeded`, via {@link ReviewGateController}) and the
|
|
2853
|
+
* companion gate ({@link resolveCompanionExceeded}) so the three-way choice lives in one place.
|
|
3010
2854
|
*/
|
|
3011
2855
|
async dispatchIterationCap(workspaceId, blockId, choice, handlers) {
|
|
3012
2856
|
if (choice === 'extra-round') {
|
|
@@ -3020,18 +2864,11 @@ export class ExecutionService {
|
|
|
3020
2864
|
await this.cancel(workspaceId, blockId);
|
|
3021
2865
|
}
|
|
3022
2866
|
}
|
|
3023
|
-
/**
|
|
3024
|
-
* Resolve a requirements review that hit its iteration cap: grant one more round,
|
|
3025
|
-
* proceed with the last incorporated doc, or stop the task and reset it to phase zero.
|
|
3026
|
-
*/
|
|
3027
|
-
resolveRequirementsExceeded(workspaceId, blockId, choice) {
|
|
3028
|
-
return this.reviewGate.resolveExceeded(this.requirementsKind, workspaceId, blockId, choice);
|
|
3029
|
-
}
|
|
3030
2867
|
/**
|
|
3031
2868
|
* Resolve a companion step parked at its automatic-rework cap (`companion.exceeded`):
|
|
3032
2869
|
* grant one more round, proceed accepting the producer's current output, or stop the
|
|
3033
|
-
* task and reset it to phase zero. The companion mirror of
|
|
3034
|
-
*
|
|
2870
|
+
* task and reset it to phase zero. The companion mirror of the requirements
|
|
2871
|
+
* iteration-cap resolution (`requirementsReview.resolveExceeded`), sharing the iteration-cap dispatch + the
|
|
3035
2872
|
* gate-resume plumbing. Idempotent — an already-resolved gate returns the instance
|
|
3036
2873
|
* unchanged. Scoped by execution + approval id (the execution controller surface),
|
|
3037
2874
|
* since a companion gate is not block-addressed like the requirements window.
|
|
@@ -3061,10 +2898,10 @@ export class ExecutionService {
|
|
|
3061
2898
|
previousProposal: producer?.output ?? '',
|
|
3062
2899
|
feedback: step.companion.verdicts.at(-1)?.feedback ?? '',
|
|
3063
2900
|
});
|
|
3064
|
-
await this.updateBlockProgress(workspaceId, instance, 'in_progress');
|
|
2901
|
+
await this.runStateMachine.updateBlockProgress(workspaceId, instance, 'in_progress');
|
|
3065
2902
|
await this.executionRepository.upsert(workspaceId, instance);
|
|
3066
2903
|
await this.workRunner.signalDecision(workspaceId, instance.id, approvalId, 'extra-round');
|
|
3067
|
-
await this.emitInstance(workspaceId, instance);
|
|
2904
|
+
await this.runStateMachine.emitInstance(workspaceId, instance);
|
|
3068
2905
|
},
|
|
3069
2906
|
// Proceed: accept the producer's current output and advance past the gate.
|
|
3070
2907
|
proceed: async () => {
|
|
@@ -3080,43 +2917,11 @@ export class ExecutionService {
|
|
|
3080
2917
|
if (producer && block)
|
|
3081
2918
|
await this.inferBlockTechnical(workspaceId, block, producer, step);
|
|
3082
2919
|
}
|
|
3083
|
-
await this.advancePastResolvedGate(workspaceId, instance, stepIndex);
|
|
2920
|
+
await this.runStateMachine.advancePastResolvedGate(workspaceId, instance, stepIndex);
|
|
3084
2921
|
},
|
|
3085
2922
|
});
|
|
3086
2923
|
return instance;
|
|
3087
2924
|
}
|
|
3088
|
-
/**
|
|
3089
|
-
* Finish a gate step the human just resolved (its `approval` already marked `approved`),
|
|
3090
|
-
* then either finish the run (final step) or advance to the next step, persist, and wake
|
|
3091
|
-
* the parked durable driver. The single advance/finalize/signal path shared by every
|
|
3092
|
-
* gate-resume site — the generic approval ({@link approveStep}), the review gates (via
|
|
3093
|
-
* {@link ReviewGateController}) and the companion iteration-cap proceed
|
|
3094
|
-
* ({@link resolveCompanionExceeded}) — so the logic lives in exactly one place.
|
|
3095
|
-
*/
|
|
3096
|
-
async advancePastResolvedGate(workspaceId, instance, stepIndex) {
|
|
3097
|
-
const step = instance.steps[stepIndex];
|
|
3098
|
-
const decisionId = step.approval.id;
|
|
3099
|
-
this.stepGraph.finishStep(step);
|
|
3100
|
-
step.progress = 1;
|
|
3101
|
-
const isFinalStep = stepIndex === instance.steps.length - 1;
|
|
3102
|
-
if (isFinalStep) {
|
|
3103
|
-
instance.status = 'done';
|
|
3104
|
-
await this.finalizeBlock(workspaceId, instance, undefined);
|
|
3105
|
-
await this.stopRunContainer(workspaceId, instance);
|
|
3106
|
-
}
|
|
3107
|
-
else {
|
|
3108
|
-
instance.currentStep = stepIndex + 1;
|
|
3109
|
-
const next = instance.steps[instance.currentStep];
|
|
3110
|
-
if (next)
|
|
3111
|
-
this.stepGraph.startStep(next);
|
|
3112
|
-
if (instance.status === 'blocked')
|
|
3113
|
-
instance.status = 'running';
|
|
3114
|
-
await this.updateBlockProgress(workspaceId, instance, 'in_progress');
|
|
3115
|
-
}
|
|
3116
|
-
await this.executionRepository.upsert(workspaceId, instance);
|
|
3117
|
-
await this.workRunner.signalDecision(workspaceId, instance.id, decisionId, 'approved');
|
|
3118
|
-
await this.emitInstance(workspaceId, instance);
|
|
3119
|
-
}
|
|
3120
2925
|
// ---- clarity-review context helpers (bug-report triage) ------------------
|
|
3121
2926
|
// The clarity gate triages a block's bug report — optionally enriched by an upstream
|
|
3122
2927
|
// `bug-investigator` step's prose output — through the SAME {@link ReviewGateController}
|
|
@@ -3139,69 +2944,9 @@ export class ExecutionService {
|
|
|
3139
2944
|
const instance = await this.executionRepository.get(workspaceId, block.executionId);
|
|
3140
2945
|
return instance ? this.investigationFor(instance) : undefined;
|
|
3141
2946
|
}
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
* `bug-investigator` output as the triage subject. Shared by the gate + the off-path
|
|
3146
|
-
* inspector "Run review" surface.
|
|
3147
|
-
*/
|
|
3148
|
-
reviewClarity(workspaceId, blockId) {
|
|
3149
|
-
return this.reviewGate.review(this.clarityKind, workspaceId, blockId);
|
|
3150
|
-
}
|
|
3151
|
-
/** Incorporate the human's settled answers ASYNCHRONOUSLY (the clarity mirror of {@link incorporateRequirements}). */
|
|
3152
|
-
incorporateClarity(workspaceId, blockId, feedback) {
|
|
3153
|
-
return this.reviewGate.incorporate(this.clarityKind, workspaceId, blockId, feedback);
|
|
3154
|
-
}
|
|
3155
|
-
/** Re-review the clarified report (one more pass). On convergence the parked run advances. */
|
|
3156
|
-
reReviewClarity(workspaceId, blockId) {
|
|
3157
|
-
return this.reviewGate.reReview(this.clarityKind, workspaceId, blockId);
|
|
3158
|
-
}
|
|
3159
|
-
/** Proceed: settle the clarity review and advance the parked run. */
|
|
3160
|
-
proceedClarity(workspaceId, blockId) {
|
|
3161
|
-
return this.reviewGate.proceed(this.clarityKind, workspaceId, blockId);
|
|
3162
|
-
}
|
|
3163
|
-
/** Resolve a clarity review that hit its iteration cap (extra-round / proceed / stop-reset). */
|
|
3164
|
-
resolveClarityExceeded(workspaceId, blockId, choice) {
|
|
3165
|
-
return this.reviewGate.resolveExceeded(this.clarityKind, workspaceId, blockId, choice);
|
|
3166
|
-
}
|
|
3167
|
-
// ---- human-testing gate actions (driven from the dedicated window) -------
|
|
3168
|
-
// Each mutates the parked gate step and wakes the durable driver, which re-enters the gate
|
|
3169
|
-
// and performs the (env / helper) work; see {@link HumanTestController}.
|
|
3170
|
-
/** Confirm the change works: tear the ephemeral env down and advance the run. */
|
|
3171
|
-
confirmHumanTest(workspaceId, blockId) {
|
|
3172
|
-
return this.humanTestController.confirm(workspaceId, blockId);
|
|
3173
|
-
}
|
|
3174
|
-
/** Submit findings and request a fix: dispatch the Tester's `fixer`, then rebuild the env. */
|
|
3175
|
-
requestHumanTestFix(workspaceId, blockId, findings) {
|
|
3176
|
-
return this.humanTestController.requestFix(workspaceId, blockId, findings);
|
|
3177
|
-
}
|
|
3178
|
-
/** Pull the repo default branch into the PR branch + redeploy (conflict → conflict-resolver). */
|
|
3179
|
-
pullMainHumanTest(workspaceId, blockId) {
|
|
3180
|
-
return this.humanTestController.pullMain(workspaceId, blockId);
|
|
3181
|
-
}
|
|
3182
|
-
/** Rebuild the ephemeral environment on demand. */
|
|
3183
|
-
recreateHumanTestEnv(workspaceId, blockId) {
|
|
3184
|
-
return this.humanTestController.recreateEnvironment(workspaceId, blockId);
|
|
3185
|
-
}
|
|
3186
|
-
/** Destroy the ephemeral environment on demand (the run stays parked). */
|
|
3187
|
-
destroyHumanTestEnv(workspaceId, blockId) {
|
|
3188
|
-
return this.humanTestController.destroyEnvironment(workspaceId, blockId);
|
|
3189
|
-
}
|
|
3190
|
-
// ---- visual-confirmation gate actions (driven from the dedicated window) --
|
|
3191
|
-
// Each mutates the parked gate step and wakes the durable driver; see
|
|
3192
|
-
// {@link VisualConfirmationController}.
|
|
3193
|
-
/** Approve the reviewed screenshots: advance the run. */
|
|
3194
|
-
approveVisualConfirm(workspaceId, blockId) {
|
|
3195
|
-
return this.visualConfirmationController.approve(workspaceId, blockId);
|
|
3196
|
-
}
|
|
3197
|
-
/** Submit findings and request a fix: dispatch the Tester's `fixer`, then re-park. */
|
|
3198
|
-
requestVisualConfirmFix(workspaceId, blockId, findings) {
|
|
3199
|
-
return this.visualConfirmationController.requestFix(workspaceId, blockId, findings);
|
|
3200
|
-
}
|
|
3201
|
-
/** Refresh the screenshot pairs from the latest UI-tester report. */
|
|
3202
|
-
recaptureVisualConfirm(workspaceId, blockId) {
|
|
3203
|
-
return this.visualConfirmationController.recapture(workspaceId, blockId);
|
|
3204
|
-
}
|
|
2947
|
+
// The clarity / human-testing / visual-confirmation gate-window actions now live on the
|
|
2948
|
+
// per-feature sub-facades (`clarityReview` / `humanTest` / `visualConfirm`); see the getters
|
|
2949
|
+
// above and {@link gate-window-facades}.
|
|
3205
2950
|
/**
|
|
3206
2951
|
* Dispatch the `fixer` against the human-review gate's PR branch from a human's freeform
|
|
3207
2952
|
* instructions — bypassing the precheck + grace window. Parks a `pendingFix` on the gate step,
|
|
@@ -3239,166 +2984,13 @@ export class ExecutionService {
|
|
|
3239
2984
|
if (instance.status === 'blocked')
|
|
3240
2985
|
instance.status = 'running';
|
|
3241
2986
|
await this.executionRepository.upsert(workspaceId, instance);
|
|
3242
|
-
await this.emitInstance(workspaceId, instance);
|
|
2987
|
+
await this.runStateMachine.emitInstance(workspaceId, instance);
|
|
3243
2988
|
// Ensure a driver is active to consume the pending fix (idempotent for a live run).
|
|
3244
2989
|
if (instance.status === 'running') {
|
|
3245
2990
|
await this.workRunner.startRun(workspaceId, instance.id);
|
|
3246
2991
|
}
|
|
3247
2992
|
return instance;
|
|
3248
2993
|
}
|
|
3249
|
-
/**
|
|
3250
|
-
* Push the run's latest state to subscribed clients, alongside its rolled-up
|
|
3251
|
-
* block so the board updates without a refetch. Best-effort: the publisher
|
|
3252
|
-
* swallows its own errors, and the persisted run remains the source of truth.
|
|
3253
|
-
*/
|
|
3254
|
-
async emitInstance(workspaceId, instance) {
|
|
3255
|
-
// Stamp each step with the run id so a lone step (in a pushed event, a log line, a
|
|
3256
|
-
// detail view) is self-describing for debugging; the value always equals the run id.
|
|
3257
|
-
for (const step of instance.steps)
|
|
3258
|
-
step.runId = instance.id;
|
|
3259
|
-
// The metrics rollup and the block fetch are independent, so run them concurrently
|
|
3260
|
-
// — the rollup adds no serial latency to the (frequent) emit path.
|
|
3261
|
-
const [, block] = await Promise.all([
|
|
3262
|
-
this.attachStepMetrics(workspaceId, instance),
|
|
3263
|
-
this.blockRepository.get(workspaceId, instance.blockId),
|
|
3264
|
-
]);
|
|
3265
|
-
await this.events.executionChanged(workspaceId, instance, block);
|
|
3266
|
-
// When a run reaches a terminal state, schedule a post-run Kaizen grading for each
|
|
3267
|
-
// completed agent step (the scheduler skips verified combos + already-graded steps).
|
|
3268
|
-
// Best-effort + idempotent: a failure here must never derail the emit, and a re-emit
|
|
3269
|
-
// of an already-scheduled run is a no-op. The actual LLM grading runs later in the
|
|
3270
|
-
// background sweep, so this only does cheap inserts.
|
|
3271
|
-
if (this.kaizenScheduler && (instance.status === 'done' || instance.status === 'failed')) {
|
|
3272
|
-
try {
|
|
3273
|
-
await this.kaizenScheduler.scheduleForRun(workspaceId, instance);
|
|
3274
|
-
}
|
|
3275
|
-
catch {
|
|
3276
|
-
// Swallow — grading is an observability concern and must never break a run.
|
|
3277
|
-
}
|
|
3278
|
-
}
|
|
3279
|
-
// When a run reaches a terminal state, delete its per-run personal-credential
|
|
3280
|
-
// activation immediately (individual-usage subscriptions) so the system-encrypted
|
|
3281
|
-
// token copy doesn't linger to its TTL. Best-effort + idempotent — a missing repo or
|
|
3282
|
-
// a re-emit of an already-cleared run is a no-op, and a failure here must never
|
|
3283
|
-
// derail the emit.
|
|
3284
|
-
if (this.subscriptionActivations &&
|
|
3285
|
-
(instance.status === 'done' || instance.status === 'failed')) {
|
|
3286
|
-
try {
|
|
3287
|
-
await this.subscriptionActivations.deleteByExecution(instance.id);
|
|
3288
|
-
}
|
|
3289
|
-
catch {
|
|
3290
|
-
// Swallow — a failure here must never derail the emit. This is not a silent
|
|
3291
|
-
// data-loss path: the TTL sweep reclaims the row as a backstop, and the sweep
|
|
3292
|
-
// (Worker cron / Node retention timer) logs its own errors, so a *systemic*
|
|
3293
|
-
// cleanup failure surfaces there rather than being lost here.
|
|
3294
|
-
}
|
|
3295
|
-
}
|
|
3296
|
-
}
|
|
3297
|
-
/**
|
|
3298
|
-
* Roll the run's recorded LLM calls into per-step `metrics` for the board, in
|
|
3299
|
-
* place on the emitted instance. The proxy keys calls by execution + agentKind
|
|
3300
|
-
* (not step index), so the aggregate is per-agent-kind within the run; steps
|
|
3301
|
-
* sharing a kind get the same rollup. Best-effort and a no-op when the sink is
|
|
3302
|
-
* not wired, so it never blocks an emit.
|
|
3303
|
-
*/
|
|
3304
|
-
async attachStepMetrics(workspaceId, instance) {
|
|
3305
|
-
if (!this.llmObservability)
|
|
3306
|
-
return;
|
|
3307
|
-
try {
|
|
3308
|
-
const summaries = await this.llmObservability.summarizeByExecution(workspaceId, instance.id);
|
|
3309
|
-
if (summaries.length === 0)
|
|
3310
|
-
return;
|
|
3311
|
-
const byKind = new Map(summaries.map((s) => [s.agentKind, s]));
|
|
3312
|
-
for (const step of instance.steps) {
|
|
3313
|
-
const s = byKind.get(step.agentKind);
|
|
3314
|
-
if (!s)
|
|
3315
|
-
continue;
|
|
3316
|
-
step.metrics = {
|
|
3317
|
-
calls: s.calls,
|
|
3318
|
-
promptTokens: s.promptTokens,
|
|
3319
|
-
cachedPromptTokens: s.cachedPromptTokens,
|
|
3320
|
-
completionTokens: s.completionTokens,
|
|
3321
|
-
peakCompletionTokens: s.peakCompletionTokens,
|
|
3322
|
-
maxOutputTokens: s.maxOutputTokens,
|
|
3323
|
-
truncatedCalls: s.truncatedCalls,
|
|
3324
|
-
upstreamMs: s.upstreamMs,
|
|
3325
|
-
overheadMs: s.overheadMs,
|
|
3326
|
-
errors: s.errors,
|
|
3327
|
-
warnings: s.warnings,
|
|
3328
|
-
};
|
|
3329
|
-
}
|
|
3330
|
-
}
|
|
3331
|
-
catch (error) {
|
|
3332
|
-
// Observability is best-effort; never block an emit on a metrics read.
|
|
3333
|
-
void error;
|
|
3334
|
-
}
|
|
3335
|
-
}
|
|
3336
|
-
/** Set the block's in-progress/blocked status and step-completion progress. */
|
|
3337
|
-
async updateBlockProgress(workspaceId, instance, status) {
|
|
3338
|
-
const total = instance.steps.length || 1;
|
|
3339
|
-
const done = instance.steps.filter((s) => s.state === 'done').length;
|
|
3340
|
-
await this.blockRepository.update(workspaceId, instance.blockId, {
|
|
3341
|
-
status,
|
|
3342
|
-
progress: Math.min(1, done / total),
|
|
3343
|
-
});
|
|
3344
|
-
}
|
|
3345
|
-
/**
|
|
3346
|
-
* Advance the block's step PROGRESS without touching its status — used when a step
|
|
3347
|
-
* resolver already owns the block's terminal status (the merger set `done`/`pr_ready`)
|
|
3348
|
-
* and a trailing step still follows, so the bar moves on without downgrading that status.
|
|
3349
|
-
*/
|
|
3350
|
-
async refreshBlockProgress(workspaceId, instance) {
|
|
3351
|
-
const total = instance.steps.length || 1;
|
|
3352
|
-
const done = instance.steps.filter((s) => s.state === 'done').length;
|
|
3353
|
-
await this.blockRepository.update(workspaceId, instance.blockId, {
|
|
3354
|
-
progress: Math.min(1, done / total),
|
|
3355
|
-
});
|
|
3356
|
-
}
|
|
3357
|
-
/**
|
|
3358
|
-
* A pipeline finished. A frame becomes `done` (a mapping-only run leaves it
|
|
3359
|
-
* `ready`). A *task* never auto-`done`s from a confidence score any more — that
|
|
3360
|
-
* looked merged when the PR was still open with red CI. Instead:
|
|
3361
|
-
* - if the pipeline has a `merger` step, it already owned the merge/notify
|
|
3362
|
-
* decision (see {@link resolveMergerStep}); we only backstop a missing one;
|
|
3363
|
-
* - otherwise the work is complete but unmerged: leave the PR open (`pr_ready`)
|
|
3364
|
-
* and raise a `pipeline_complete` notification for a human to confirm + merge.
|
|
3365
|
-
* `done` now strictly means the PR was merged (see {@link finalizeMerge}).
|
|
3366
|
-
*/
|
|
3367
|
-
async finalizeBlock(workspaceId, instance, confidence) {
|
|
3368
|
-
const block = await this.blockRepository.get(workspaceId, instance.blockId);
|
|
3369
|
-
if (!block || block.status === 'done')
|
|
3370
|
-
return;
|
|
3371
|
-
if ((block.level ?? 'frame') !== 'task') {
|
|
3372
|
-
// A mapping-only run (just the `blueprints` step, e.g. kicked off after a
|
|
3373
|
-
// bootstrap) leaves the service frame `ready` and droppable rather than
|
|
3374
|
-
// marking the whole service "done".
|
|
3375
|
-
const mappingOnly = instance.steps.every((s) => s.agentKind === 'blueprints');
|
|
3376
|
-
await this.blockRepository.update(workspaceId, block.id, {
|
|
3377
|
-
status: mappingOnly ? 'ready' : 'done',
|
|
3378
|
-
progress: 1,
|
|
3379
|
-
});
|
|
3380
|
-
return;
|
|
3381
|
-
}
|
|
3382
|
-
// Confidence is recorded by the caller (recordStepResult) before any merge, so
|
|
3383
|
-
// it persists on both the merge and review paths; `confidence` is unused here.
|
|
3384
|
-
void confidence;
|
|
3385
|
-
const hasMerger = instance.steps.some((s) => s.agentKind === MERGER_AGENT_KIND);
|
|
3386
|
-
if (hasMerger) {
|
|
3387
|
-
// The `merger` step already merged (→ `done`) or raised a review (→ `pr_ready`).
|
|
3388
|
-
// Only backstop the case where it produced no decision at all.
|
|
3389
|
-
const fresh = await this.blockRepository.get(workspaceId, block.id);
|
|
3390
|
-
if (fresh && fresh.status !== 'done' && fresh.status !== 'pr_ready') {
|
|
3391
|
-
await this.blockRepository.update(workspaceId, block.id, {
|
|
3392
|
-
status: 'pr_ready',
|
|
3393
|
-
progress: 1,
|
|
3394
|
-
});
|
|
3395
|
-
}
|
|
3396
|
-
return;
|
|
3397
|
-
}
|
|
3398
|
-
// No merger in this pipeline: complete but unmerged — ask a human to confirm.
|
|
3399
|
-
await this.blockRepository.update(workspaceId, block.id, { status: 'pr_ready', progress: 1 });
|
|
3400
|
-
await this.raisePipelineComplete(workspaceId, instance, block);
|
|
3401
|
-
}
|
|
3402
2994
|
/**
|
|
3403
2995
|
* Merge a block's PR for real, then mark it `done`. The remote merge happens
|
|
3404
2996
|
* FIRST (via the {@link PullRequestMerger} port) and only on its success does the
|
|
@@ -3501,23 +3093,6 @@ export class ExecutionService {
|
|
|
3501
3093
|
}
|
|
3502
3094
|
return DEFAULT_MERGE_PRESET;
|
|
3503
3095
|
}
|
|
3504
|
-
/** Raise a `pipeline_complete` notification for a no-merger run awaiting confirmation. */
|
|
3505
|
-
async raisePipelineComplete(workspaceId, instance, block) {
|
|
3506
|
-
if (!this.notificationService)
|
|
3507
|
-
return;
|
|
3508
|
-
await this.notificationService.raise(workspaceId, {
|
|
3509
|
-
type: 'pipeline_complete',
|
|
3510
|
-
blockId: block.id,
|
|
3511
|
-
executionId: instance.id,
|
|
3512
|
-
title: `Confirm "${block.title}" is complete`,
|
|
3513
|
-
body: `The "${instance.pipelineName}" pipeline finished and opened a PR, but it has no ` +
|
|
3514
|
-
`merger step. Review the work and confirm it as complete (this merges the PR).`,
|
|
3515
|
-
payload: {
|
|
3516
|
-
...(block.pullRequest?.url ? { prUrl: block.pullRequest.url } : {}),
|
|
3517
|
-
pipelineName: instance.pipelineName,
|
|
3518
|
-
},
|
|
3519
|
-
});
|
|
3520
|
-
}
|
|
3521
3096
|
/**
|
|
3522
3097
|
* Implementing a task assigned to a module materialises that module: create it
|
|
3523
3098
|
* in the service if missing, then move the task inside it.
|
|
@@ -3559,13 +3134,13 @@ export class ExecutionService {
|
|
|
3559
3134
|
this.stepGraph.startStep(step);
|
|
3560
3135
|
if (instance.status === 'blocked')
|
|
3561
3136
|
instance.status = 'running';
|
|
3562
|
-
await this.updateBlockProgress(workspaceId, instance, 'in_progress');
|
|
3137
|
+
await this.runStateMachine.updateBlockProgress(workspaceId, instance, 'in_progress');
|
|
3563
3138
|
await this.executionRepository.upsert(workspaceId, instance);
|
|
3564
3139
|
// Wake the parked durable run, if any. The DB write above remains the source
|
|
3565
3140
|
// of truth (so the backstop sweeper can still re-drive it); the signal is an
|
|
3566
3141
|
// optimisation that lets the workflow continue immediately.
|
|
3567
3142
|
await this.workRunner.signalDecision(workspaceId, instance.id, decisionId, choice);
|
|
3568
|
-
await this.emitInstance(workspaceId, instance);
|
|
3143
|
+
await this.runStateMachine.emitInstance(workspaceId, instance);
|
|
3569
3144
|
return instance;
|
|
3570
3145
|
}
|
|
3571
3146
|
/**
|
|
@@ -3593,7 +3168,7 @@ export class ExecutionService {
|
|
|
3593
3168
|
}
|
|
3594
3169
|
step.approval.status = 'approved';
|
|
3595
3170
|
// A gate is never raised on the final step, but the shared advance stays defensive.
|
|
3596
|
-
await this.advancePastResolvedGate(workspaceId, instance, stepIndex);
|
|
3171
|
+
await this.runStateMachine.advancePastResolvedGate(workspaceId, instance, stepIndex);
|
|
3597
3172
|
return instance;
|
|
3598
3173
|
}
|
|
3599
3174
|
/**
|
|
@@ -3654,7 +3229,7 @@ export class ExecutionService {
|
|
|
3654
3229
|
instance.status = 'running';
|
|
3655
3230
|
await this.executionRepository.upsert(workspaceId, instance);
|
|
3656
3231
|
await this.workRunner.signalDecision(workspaceId, instance.id, approvalId, 'changes_requested');
|
|
3657
|
-
await this.emitInstance(workspaceId, instance);
|
|
3232
|
+
await this.runStateMachine.emitInstance(workspaceId, instance);
|
|
3658
3233
|
return instance;
|
|
3659
3234
|
}
|
|
3660
3235
|
}
|
|
@@ -3670,7 +3245,7 @@ export class ExecutionService {
|
|
|
3670
3245
|
instance.status = 'running';
|
|
3671
3246
|
await this.executionRepository.upsert(workspaceId, instance);
|
|
3672
3247
|
await this.workRunner.signalDecision(workspaceId, instance.id, approvalId, 'changes_requested');
|
|
3673
|
-
await this.emitInstance(workspaceId, instance);
|
|
3248
|
+
await this.runStateMachine.emitInstance(workspaceId, instance);
|
|
3674
3249
|
return instance;
|
|
3675
3250
|
}
|
|
3676
3251
|
/**
|
|
@@ -3731,42 +3306,8 @@ export class ExecutionService {
|
|
|
3731
3306
|
* durable driver once a step has exhausted its retries (or a job/decision
|
|
3732
3307
|
* faulted); `kind` classifies the cause so the right hint is shown.
|
|
3733
3308
|
*/
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
if (!instance)
|
|
3737
|
-
return;
|
|
3738
|
-
// Reclaim the per-run container on the failure path too: a failed run otherwise
|
|
3739
|
-
// leaves its container to idle out sleepAfter. This is the single funnel for
|
|
3740
|
-
// every failure kind (job_failed from the driver, the spend/decision timeouts,
|
|
3741
|
-
// and the user-facing stopRun, which already reclaimed — the call is idempotent).
|
|
3742
|
-
await this.stopRunContainer(workspaceId, instance);
|
|
3743
|
-
// The FIRST recorded failure wins: a run already in a terminal `failed` state keeps
|
|
3744
|
-
// its existing (richest) failure rather than being overwritten. An inline gate that
|
|
3745
|
-
// knows the precise kind/detail returns a `job_failed` result the driver funnels here,
|
|
3746
|
-
// so there should only ever be one write — but this guards against a future path that
|
|
3747
|
-
// both records a failure and returns `job_failed`, which would otherwise clobber the
|
|
3748
|
-
// good record with a generic one (the companion-rejected regression).
|
|
3749
|
-
if (instance.status === 'failed')
|
|
3750
|
-
return;
|
|
3751
|
-
const failure = {
|
|
3752
|
-
kind,
|
|
3753
|
-
message,
|
|
3754
|
-
detail,
|
|
3755
|
-
hint: EXECUTION_FAILURE_HINTS[kind] ?? null,
|
|
3756
|
-
occurredAt: this.clock.now(),
|
|
3757
|
-
lastSubtasks: instance.steps[instance.currentStep]?.subtasks ?? null,
|
|
3758
|
-
};
|
|
3759
|
-
await this.executionRepository.markFailed(workspaceId, executionId, failure);
|
|
3760
|
-
// Progress reflects how far the pipeline got before failing.
|
|
3761
|
-
const done = instance.steps.filter((s) => s.state === 'done').length;
|
|
3762
|
-
const progress = instance.steps.length > 0 ? done / instance.steps.length : 0;
|
|
3763
|
-
await this.blockRepository.update(workspaceId, instance.blockId, {
|
|
3764
|
-
status: 'blocked',
|
|
3765
|
-
progress,
|
|
3766
|
-
});
|
|
3767
|
-
const failed = await this.executionRepository.get(workspaceId, executionId);
|
|
3768
|
-
if (failed)
|
|
3769
|
-
await this.emitInstance(workspaceId, failed);
|
|
3309
|
+
failRun(workspaceId, executionId, message, kind = 'agent', detail = null) {
|
|
3310
|
+
return this.runStateMachine.failRun(workspaceId, executionId, message, kind, detail);
|
|
3770
3311
|
}
|
|
3771
3312
|
/**
|
|
3772
3313
|
* Retry a failed run: re-drive the same pipeline on the same block, **resuming
|
|
@@ -3827,7 +3368,7 @@ export class ExecutionService {
|
|
|
3827
3368
|
executionId: instance.id,
|
|
3828
3369
|
});
|
|
3829
3370
|
await this.workRunner.startRun(workspaceId, instance.id);
|
|
3830
|
-
await this.emitInstance(workspaceId, instance);
|
|
3371
|
+
await this.runStateMachine.emitInstance(workspaceId, instance);
|
|
3831
3372
|
return instance;
|
|
3832
3373
|
}
|
|
3833
3374
|
/**
|
|
@@ -3874,7 +3415,7 @@ export class ExecutionService {
|
|
|
3874
3415
|
// container AND its durable driver — before minting the restart. A `done`/`failed`
|
|
3875
3416
|
// run is already terminal (a no-op teardown), but a still-`running` run would
|
|
3876
3417
|
// otherwise leak a container and a live Workflows/pg-boss driver.
|
|
3877
|
-
await this.stopRunContainer(workspaceId, previous);
|
|
3418
|
+
await this.runStateMachine.stopRunContainer(workspaceId, previous);
|
|
3878
3419
|
await this.workRunner.cancelRun(workspaceId, executionId);
|
|
3879
3420
|
const { steps, currentStep } = planRestartFromStep(previous, fromStepIndex);
|
|
3880
3421
|
// Mint the activation before replacing the prior run, so a bad password aborts the
|
|
@@ -3900,7 +3441,7 @@ export class ExecutionService {
|
|
|
3900
3441
|
executionId: instance.id,
|
|
3901
3442
|
});
|
|
3902
3443
|
await this.workRunner.startRun(workspaceId, instance.id);
|
|
3903
|
-
await this.emitInstance(workspaceId, instance);
|
|
3444
|
+
await this.runStateMachine.emitInstance(workspaceId, instance);
|
|
3904
3445
|
return instance;
|
|
3905
3446
|
}
|
|
3906
3447
|
/**
|
|
@@ -3916,7 +3457,7 @@ export class ExecutionService {
|
|
|
3916
3457
|
instance.status = 'running';
|
|
3917
3458
|
await this.executionRepository.upsert(workspaceId, instance);
|
|
3918
3459
|
await this.workRunner.startRun(workspaceId, instance.id);
|
|
3919
|
-
await this.emitInstance(workspaceId, instance);
|
|
3460
|
+
await this.runStateMachine.emitInstance(workspaceId, instance);
|
|
3920
3461
|
}
|
|
3921
3462
|
return this.executionRepository.listByWorkspace(workspaceId);
|
|
3922
3463
|
}
|
|
@@ -3928,7 +3469,7 @@ export class ExecutionService {
|
|
|
3928
3469
|
// the record, so a cancel never leaves a container running until its watchdog.
|
|
3929
3470
|
const existing = await this.executionRepository.getByBlock(workspaceId, blockId);
|
|
3930
3471
|
if (existing) {
|
|
3931
|
-
await this.stopRunContainer(workspaceId, existing);
|
|
3472
|
+
await this.runStateMachine.stopRunContainer(workspaceId, existing);
|
|
3932
3473
|
await this.workRunner.cancelRun(workspaceId, existing.id);
|
|
3933
3474
|
}
|
|
3934
3475
|
await this.executionRepository.deleteByBlock(workspaceId, blockId);
|
|
@@ -3956,7 +3497,7 @@ export class ExecutionService {
|
|
|
3956
3497
|
const instance = assertFound(await this.executionRepository.get(workspaceId, executionId), 'Execution', executionId);
|
|
3957
3498
|
if (instance.status === 'failed' || instance.status === 'done')
|
|
3958
3499
|
return instance;
|
|
3959
|
-
await this.stopRunContainer(workspaceId, instance);
|
|
3500
|
+
await this.runStateMachine.stopRunContainer(workspaceId, instance);
|
|
3960
3501
|
await this.workRunner.cancelRun(workspaceId, executionId);
|
|
3961
3502
|
await this.failRun(workspaceId, executionId, opts.reason ?? 'Stopped by the user.', opts.kind ?? 'cancelled');
|
|
3962
3503
|
return assertFound(await this.executionRepository.get(workspaceId, executionId), 'Execution', executionId);
|
|
@@ -3973,33 +3514,10 @@ export class ExecutionService {
|
|
|
3973
3514
|
const run = await this.executionRepository.getByBlock(workspaceId, blockId);
|
|
3974
3515
|
if (!run)
|
|
3975
3516
|
continue;
|
|
3976
|
-
await this.stopRunContainer(workspaceId, run);
|
|
3517
|
+
await this.runStateMachine.stopRunContainer(workspaceId, run);
|
|
3977
3518
|
await this.workRunner.cancelRun(workspaceId, run.id);
|
|
3978
3519
|
await this.executionRepository.deleteByBlock(workspaceId, blockId);
|
|
3979
3520
|
}
|
|
3980
3521
|
}
|
|
3981
|
-
/**
|
|
3982
|
-
* Best-effort: reclaim the per-run container backing an execution. The container is
|
|
3983
|
-
* addressed by the run (execution) id, so a backend that shares one across the run
|
|
3984
|
-
* (Cloudflare, local Docker) tears the whole thing down. A per-job backend (a
|
|
3985
|
-
* self-hosted pool) has no run container, so it cancels the run's IN-FLIGHT step job
|
|
3986
|
-
* instead — hence we pass the current step's job id alongside the run id. A no-op for
|
|
3987
|
-
* inline executors (no `stopJob`) and for an already-gone container/job; never
|
|
3988
|
-
* throws, so it can't derail the teardown that calls it.
|
|
3989
|
-
*/
|
|
3990
|
-
async stopRunContainer(workspaceId, instance) {
|
|
3991
|
-
const executor = this.agentExecutor;
|
|
3992
|
-
if (!isAsyncAgentExecutor(executor) || !executor.stopJob)
|
|
3993
|
-
return;
|
|
3994
|
-
// The in-flight step's job id (when a job is parked), so a per-job backend can
|
|
3995
|
-
// cancel exactly it; the run-container backends ignore it and use the run id.
|
|
3996
|
-
const jobId = instance.steps[instance.currentStep]?.jobId ?? instance.id;
|
|
3997
|
-
try {
|
|
3998
|
-
await executor.stopJob({ jobId, runId: instance.id, workspaceId });
|
|
3999
|
-
}
|
|
4000
|
-
catch {
|
|
4001
|
-
// The container may already be gone (eviction/completion) — nothing to reclaim.
|
|
4002
|
-
}
|
|
4003
|
-
}
|
|
4004
3522
|
}
|
|
4005
3523
|
//# sourceMappingURL=ExecutionService.js.map
|