@cat-factory/orchestration 0.177.0 → 0.177.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/modules/execution/ExecutionService.d.ts +9 -62
- package/dist/modules/execution/ExecutionService.d.ts.map +1 -1
- package/dist/modules/execution/ExecutionService.js +46 -320
- package/dist/modules/execution/ExecutionService.js.map +1 -1
- package/dist/modules/execution/OneShotStepController.d.ts +92 -0
- package/dist/modules/execution/OneShotStepController.d.ts.map +1 -0
- package/dist/modules/execution/OneShotStepController.js +202 -0
- package/dist/modules/execution/OneShotStepController.js.map +1 -0
- package/dist/modules/execution/PollRunningController.d.ts +113 -0
- package/dist/modules/execution/PollRunningController.d.ts.map +1 -0
- package/dist/modules/execution/PollRunningController.js +298 -0
- package/dist/modules/execution/PollRunningController.js.map +1 -0
- package/dist/modules/execution/RunDispatcher.d.ts +4 -108
- package/dist/modules/execution/RunDispatcher.d.ts.map +1 -1
- package/dist/modules/execution/RunDispatcher.js +51 -468
- package/dist/modules/execution/RunDispatcher.js.map +1 -1
- package/dist/modules/execution/StepDecisionController.d.ts +120 -0
- package/dist/modules/execution/StepDecisionController.d.ts.map +1 -0
- package/dist/modules/execution/StepDecisionController.js +347 -0
- package/dist/modules/execution/StepDecisionController.js.map +1 -0
- package/package.json +2 -2
|
@@ -94,6 +94,8 @@ export declare class ExecutionService {
|
|
|
94
94
|
* / `resolveGatePollExhaustion` + the follow-up human-action API are thin pass-throughs.
|
|
95
95
|
*/
|
|
96
96
|
private readonly runDispatcher;
|
|
97
|
+
/** The human decision surface on a parked run (approve / reject / merge / …). */
|
|
98
|
+
private readonly stepDecisions;
|
|
97
99
|
constructor(dependencies: ExecutionServiceDependencies);
|
|
98
100
|
/**
|
|
99
101
|
* Assemble the post-merge board controller. A method rather than a literal in the constructor
|
|
@@ -215,18 +217,6 @@ export declare class ExecutionService {
|
|
|
215
217
|
* wedge the run — a failed write is swallowed.
|
|
216
218
|
*/
|
|
217
219
|
private inferBlockTechnical;
|
|
218
|
-
/**
|
|
219
|
-
* Several gates park on a `step.approval` but are NOT generic prose approvals — they are
|
|
220
|
-
* driven by their own dedicated surface, never the generic
|
|
221
|
-
* approve/request-changes/reject resolvers (which would advance the run bypassing the
|
|
222
|
-
* loop). Guard those resolvers so a stray approve can't short-circuit any of them:
|
|
223
|
-
* the requirements/clarity review gates, the brainstorms, the human-testing and
|
|
224
|
-
* visual-confirmation gates, an interview-trait gate, a companion at its rework cap
|
|
225
|
-
* (`companion.exceeded`), the follow-up companion with undecided items, and a coder
|
|
226
|
-
* parked on the implementation-fork decision (whose approve would skip the build
|
|
227
|
-
* dispatch entirely).
|
|
228
|
-
*/
|
|
229
|
-
private assertNotIterativeGate;
|
|
230
220
|
/** Pick the brainstorm kind for a stage (the dedicated window drives both via the same loop). */
|
|
231
221
|
private brainstormKindFor;
|
|
232
222
|
/**
|
|
@@ -248,20 +238,6 @@ export declare class ExecutionService {
|
|
|
248
238
|
* since a companion gate is not block-addressed like the requirements window.
|
|
249
239
|
*/
|
|
250
240
|
resolveCompanionExceeded(workspaceId: string, executionId: string, approvalId: string, choice: IterationCapChoice): Promise<ExecutionInstance>;
|
|
251
|
-
/**
|
|
252
|
-
* Dispatch the `fixer` against the human-review gate's PR branch from a human's freeform
|
|
253
|
-
* instructions — bypassing the precheck + grace window. Parks a `pendingFix` on the gate step,
|
|
254
|
-
* consumed on the gate's next poll (see {@link evaluateGate}) which dispatches the fixer with
|
|
255
|
-
* the instructions folded in. A second request before the first is consumed simply replaces the
|
|
256
|
-
* pending instructions. Throws when no human-review gate is currently parked.
|
|
257
|
-
*
|
|
258
|
-
* The run is re-driven via `workRunner.startRun` so the pending fix is picked up promptly even
|
|
259
|
-
* when the driver had died (e.g. its durable advance job expired/was evicted before the stale-
|
|
260
|
-
* run sweeper re-drove it) — `startRun` is idempotent for a live run (the exclusive advance
|
|
261
|
-
* queue no-ops a duplicate send), so this only has an effect when no driver is currently
|
|
262
|
-
* polling. A spend-paused run is left paused (it resumes through its own path).
|
|
263
|
-
*/
|
|
264
|
-
requestHumanReviewFix(workspaceId: string, blockId: string, instructions: string): Promise<ExecutionInstance>;
|
|
265
241
|
/**
|
|
266
242
|
* Merge a block's PR(s) for real, then mark it `done`. The remote merge happens FIRST (via
|
|
267
243
|
* the {@link PullRequestMerger} port) and only on its success does the block flip to `done`
|
|
@@ -300,51 +276,22 @@ export declare class ExecutionService {
|
|
|
300
276
|
private applyModuleAssignment;
|
|
301
277
|
/** Resolve a pending decision; the run's next step lets the agent finish it. */
|
|
302
278
|
resolveDecision(workspaceId: string, executionId: string, decisionId: string, choice: string): Promise<ExecutionInstance>;
|
|
303
|
-
/**
|
|
304
|
-
* Approve a step's gated proposal: the run advances to the next step, carrying
|
|
305
|
-
* the (optionally human-edited) proposal forward as context. Mirrors
|
|
306
|
-
* {@link resolveDecision}'s durable-wake but *advances* the pipeline instead of
|
|
307
|
-
* re-running the step (the step is already done). Idempotent — re-approving an
|
|
308
|
-
* already-approved gate is a no-op.
|
|
309
|
-
*/
|
|
279
|
+
/** Approve a step's gated proposal (optionally replacing it with the human's edit). */
|
|
310
280
|
approveStep(workspaceId: string, executionId: string, approvalId: string, opts?: {
|
|
311
281
|
proposal?: string;
|
|
312
282
|
}): Promise<ExecutionInstance>;
|
|
313
|
-
/**
|
|
314
|
-
* Request changes on a step's gated proposal: the same step re-runs with the
|
|
315
|
-
* human's freeform feedback and/or per-block comments (and its prior proposal)
|
|
316
|
-
* folded into the agent's context (see {@link AgentContextBuilder}). The run is left
|
|
317
|
-
* `running` on the same step; on the re-run's completion the gate is raised
|
|
318
|
-
* afresh. At least one of `feedback`/`comments` is expected (the controller
|
|
319
|
-
* validates this), but an empty review is harmless — the agent simply re-runs.
|
|
320
|
-
*/
|
|
283
|
+
/** Request changes on a step's gated proposal; the step re-runs with the feedback folded in. */
|
|
321
284
|
requestStepChanges(workspaceId: string, executionId: string, approvalId: string, review: {
|
|
322
285
|
feedback?: string;
|
|
323
286
|
comments?: StepReviewComment[];
|
|
324
287
|
}): Promise<ExecutionInstance>;
|
|
325
|
-
/**
|
|
326
|
-
* Reject a step's gated proposal: the run stops entirely. The gate is marked
|
|
327
|
-
* `rejected` and the run is failed with a dedicated `rejected` failure kind, so
|
|
328
|
-
* the board surfaces it via the shared failure banner (block → `blocked`) with a
|
|
329
|
-
* Retry affordance. The parked durable run is woken so it observes the now-terminal
|
|
330
|
-
* status and stops (the workflow's advance loop no-ops on a non-running run).
|
|
331
|
-
* Idempotent — rejecting an already-terminal gate is a no-op.
|
|
332
|
-
*/
|
|
288
|
+
/** Reject a step's gated proposal; the run stops with a `rejected` failure. */
|
|
333
289
|
rejectStep(workspaceId: string, executionId: string, approvalId: string, reason?: string): Promise<ExecutionInstance>;
|
|
334
|
-
/**
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
*
|
|
338
|
-
* `reviewEffort` records, in the same call, how much review the PR actually needed. It is
|
|
339
|
-
* always optional: an untagged merge settles the track record with a null tag and nothing
|
|
340
|
-
* downstream breaks. The record settle runs AFTER the merge and is best-effort inside the
|
|
341
|
-
* service, so no part of this feature can fail or block a merge.
|
|
342
|
-
*/
|
|
290
|
+
/** Dispatch the human-review gate's fixer from a human's freeform instructions. */
|
|
291
|
+
requestHumanReviewFix(workspaceId: string, blockId: string, instructions: string): Promise<ExecutionInstance>;
|
|
292
|
+
/** Merge an open PR for real, moving the block from `pr_ready` to `done`. */
|
|
343
293
|
mergePr(workspaceId: string, blockId: string, reviewEffort?: ReviewEffort | null): Promise<Block>;
|
|
344
|
-
/**
|
|
345
|
-
* Record that a human DECLINED to merge — they dismissed the review card rather than acting on
|
|
346
|
-
* it, so the class's rollup counts a rejection instead of a forever-`pending_review` row.
|
|
347
|
-
*/
|
|
294
|
+
/** Record that a human DECLINED to merge (the review card was dismissed, not acted on). */
|
|
348
295
|
recordMergeRejection(workspaceId: string, executionId: string): Promise<void>;
|
|
349
296
|
/**
|
|
350
297
|
* Record a terminal agent failure: persist a structured {@link AgentFailure},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExecutionService.d.ts","sourceRoot":"","sources":["../../../src/modules/execution/ExecutionService.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,KAAK,EACL,iBAAiB,EACjB,kBAAkB,EAClB,qBAAqB,EACrB,eAAe,EACf,oBAAoB,EACpB,iBAAiB,EACjB,oBAAoB,EACpB,6BAA6B,EAG7B,iBAAiB,EAIlB,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"ExecutionService.d.ts","sourceRoot":"","sources":["../../../src/modules/execution/ExecutionService.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { allPullRequests, DEFAULT_COMPANION_MAX_ATTEMPTS, pipelineHasVisualStep, } from '@cat-factory/contracts';
|
|
2
|
-
import { companionFor
|
|
2
|
+
import { companionFor } from '@cat-factory/agents';
|
|
3
3
|
import { assertPipelineLaunchable } from '../pipelines/pipelineShape.js';
|
|
4
4
|
import { producerWasSkipped, shouldRunGatedStep } from './stepGating.logic.js';
|
|
5
5
|
import { resolveIndividualVendors, } from './individualVendors.logic.js';
|
|
6
6
|
import { assertFound, ConflictError, NotFoundError, RunContendedError, ReviewContendedError, ValidationError, } from '@cat-factory/kernel';
|
|
7
7
|
import { DEFAULT_RISK_POLICY, noopLogger, runBestEffort } from '@cat-factory/kernel';
|
|
8
|
-
import {
|
|
8
|
+
import { isTesterKind } from './ci.logic.js';
|
|
9
9
|
import { DEFAULT_FOLLOW_UP_MAX_LOOPS, FOLLOW_UP_PRODUCER_KIND } from './followUp.logic.js';
|
|
10
10
|
import { AgentContextBuilder } from './AgentContextBuilder.js';
|
|
11
11
|
import { CompanionController } from './CompanionController.js';
|
|
@@ -13,6 +13,7 @@ import { StepGraph } from './StepGraph.js';
|
|
|
13
13
|
import { RunStateMachine } from './RunStateMachine.js';
|
|
14
14
|
import { RunDispatcher } from './RunDispatcher.js';
|
|
15
15
|
import { RunAdmission } from './RunAdmission.js';
|
|
16
|
+
import { StepDecisionController } from './StepDecisionController.js';
|
|
16
17
|
import { inferTechnicalLabel } from './technical.logic.js';
|
|
17
18
|
import { MergeResolver } from './MergeResolver.js';
|
|
18
19
|
import { PostMergeBoardController } from './PostMergeBoardController.js';
|
|
@@ -32,7 +33,6 @@ import { InitiativeInterviewController } from './InitiativeInterviewController.j
|
|
|
32
33
|
import { DocInterviewController } from './DocInterviewController.js';
|
|
33
34
|
import { isReentrantDecisionResume } from './reentrancy.logic.js';
|
|
34
35
|
import { RunMergePolicy } from './RunMergePolicy.js';
|
|
35
|
-
import { isAsyncAgentExecutor } from '@cat-factory/kernel';
|
|
36
36
|
import { descendantIds } from '../board/board.logic.js';
|
|
37
37
|
import { requireWorkspace } from '@cat-factory/kernel';
|
|
38
38
|
import { buildResumedInstance, planResumedSteps, planRestartFromStep } from './retry.logic.js';
|
|
@@ -148,6 +148,8 @@ export class ExecutionService {
|
|
|
148
148
|
* / `resolveGatePollExhaustion` + the follow-up human-action API are thin pass-throughs.
|
|
149
149
|
*/
|
|
150
150
|
runDispatcher;
|
|
151
|
+
/** The human decision surface on a parked run (approve / reject / merge / …). */
|
|
152
|
+
stepDecisions;
|
|
151
153
|
constructor(dependencies) {
|
|
152
154
|
// Bind the whole deps object, then destructure what this constructor itself reads. The
|
|
153
155
|
// object is what the sibling factories below are handed, so a field they consume can never
|
|
@@ -361,6 +363,22 @@ export class ExecutionService {
|
|
|
361
363
|
this.subscriptionActivations = subscriptionActivationRepository;
|
|
362
364
|
this.pokeInitiativeLoop = pokeInitiativeLoop;
|
|
363
365
|
this.resolveWorkspaceModelDefault = resolveWorkspaceModelDefault;
|
|
366
|
+
this.stepDecisions = new StepDecisionController({
|
|
367
|
+
agentExecutor: this.agentExecutor,
|
|
368
|
+
agentKindRegistry: this.agentKindRegistry,
|
|
369
|
+
blockRepository: this.blockRepository,
|
|
370
|
+
clock: this.clock,
|
|
371
|
+
executionRepository: this.executionRepository,
|
|
372
|
+
mergePolicy: this.mergePolicy,
|
|
373
|
+
runDispatcher: this.runDispatcher,
|
|
374
|
+
runStateMachine: this.runStateMachine,
|
|
375
|
+
stepGraph: this.stepGraph,
|
|
376
|
+
workRunner: this.workRunner,
|
|
377
|
+
requireWorkspace: (ws) => this.requireWorkspace(ws),
|
|
378
|
+
requireBlock: (ws, id) => this.requireBlock(ws, id),
|
|
379
|
+
failRun: (ws, id, message, kind, detail, reason) => this.failRun(ws, id, message, kind, detail, reason),
|
|
380
|
+
finalizeMerge: (ws, blockId) => this.finalizeMerge(ws, blockId),
|
|
381
|
+
});
|
|
364
382
|
}
|
|
365
383
|
/**
|
|
366
384
|
* Assemble the post-merge board controller. A method rather than a literal in the constructor
|
|
@@ -907,50 +925,6 @@ export class ExecutionService {
|
|
|
907
925
|
// the companion iteration-cap gate, so they have a single home: {@link parkStepOnDecision},
|
|
908
926
|
// the `advanceRunPastGate`/`settleAdvancedGate` split (run under `mutateInstance`), and
|
|
909
927
|
// {@link dispatchIterationCap}.
|
|
910
|
-
/**
|
|
911
|
-
* Several gates park on a `step.approval` but are NOT generic prose approvals — they are
|
|
912
|
-
* driven by their own dedicated surface, never the generic
|
|
913
|
-
* approve/request-changes/reject resolvers (which would advance the run bypassing the
|
|
914
|
-
* loop). Guard those resolvers so a stray approve can't short-circuit any of them:
|
|
915
|
-
* the requirements/clarity review gates, the brainstorms, the human-testing and
|
|
916
|
-
* visual-confirmation gates, an interview-trait gate, a companion at its rework cap
|
|
917
|
-
* (`companion.exceeded`), the follow-up companion with undecided items, and a coder
|
|
918
|
-
* parked on the implementation-fork decision (whose approve would skip the build
|
|
919
|
-
* dispatch entirely).
|
|
920
|
-
*/
|
|
921
|
-
assertNotIterativeGate(step) {
|
|
922
|
-
if (step.agentKind === REQUIREMENTS_REVIEW_AGENT_KIND) {
|
|
923
|
-
throw new ConflictError('Resolve the requirements review through its review window, not the approval gate');
|
|
924
|
-
}
|
|
925
|
-
if (step.agentKind === CLARITY_REVIEW_AGENT_KIND) {
|
|
926
|
-
throw new ConflictError('Resolve the clarity review through its review window, not the approval gate');
|
|
927
|
-
}
|
|
928
|
-
if (step.agentKind === REQUIREMENTS_BRAINSTORM_AGENT_KIND ||
|
|
929
|
-
step.agentKind === ARCHITECTURE_BRAINSTORM_AGENT_KIND) {
|
|
930
|
-
throw new ConflictError('Resolve the brainstorm through its brainstorm window, not the approval gate');
|
|
931
|
-
}
|
|
932
|
-
if (step.agentKind === HUMAN_TEST_AGENT_KIND) {
|
|
933
|
-
throw new ConflictError('Resolve the human-testing gate through its window (confirm / request a fix), not the approval gate');
|
|
934
|
-
}
|
|
935
|
-
if (step.agentKind === VISUAL_CONFIRM_AGENT_KIND) {
|
|
936
|
-
throw new ConflictError('Resolve the visual-confirmation gate through its window (approve / request a fix), not the approval gate');
|
|
937
|
-
}
|
|
938
|
-
if (hasTrait(step.agentKind, INTERVIEW_GATE_TRAIT, this.agentKindRegistry)) {
|
|
939
|
-
throw new ConflictError('Resolve the interview through its interview window, not the approval gate');
|
|
940
|
-
}
|
|
941
|
-
if (step.companion?.exceeded) {
|
|
942
|
-
throw new ConflictError('Resolve this companion review through its iteration-cap prompt, not the approval gate');
|
|
943
|
-
}
|
|
944
|
-
if (step.followUps?.enabled && step.followUps.items.some((i) => i.status === 'pending')) {
|
|
945
|
-
throw new ConflictError('Resolve the follow-up companion through its window (file / send back / answer / dismiss), not the approval gate');
|
|
946
|
-
}
|
|
947
|
-
if (step.forkDecision?.status === 'awaiting_choice' ||
|
|
948
|
-
step.forkDecision?.status === 'answering') {
|
|
949
|
-
// Approving here would advance the run PAST the coder step with the build never run
|
|
950
|
-
// (the park sits between the proposer and the Coder dispatch).
|
|
951
|
-
throw new ConflictError('Resolve the implementation-fork decision through its fork window (choose an approach), not the approval gate');
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
928
|
/** Pick the brainstorm kind for a stage (the dedicated window drives both via the same loop). */
|
|
955
929
|
brainstormKindFor(stage) {
|
|
956
930
|
return stage === 'architecture'
|
|
@@ -1081,55 +1055,6 @@ export class ExecutionService {
|
|
|
1081
1055
|
// The clarity / human-testing / visual-confirmation gate-window actions now live on the
|
|
1082
1056
|
// per-feature sub-facades (`clarityReview` / `humanTest` / `visualConfirm`); see the getters
|
|
1083
1057
|
// above and {@link gate-window-facades}.
|
|
1084
|
-
/**
|
|
1085
|
-
* Dispatch the `fixer` against the human-review gate's PR branch from a human's freeform
|
|
1086
|
-
* instructions — bypassing the precheck + grace window. Parks a `pendingFix` on the gate step,
|
|
1087
|
-
* consumed on the gate's next poll (see {@link evaluateGate}) which dispatches the fixer with
|
|
1088
|
-
* the instructions folded in. A second request before the first is consumed simply replaces the
|
|
1089
|
-
* pending instructions. Throws when no human-review gate is currently parked.
|
|
1090
|
-
*
|
|
1091
|
-
* The run is re-driven via `workRunner.startRun` so the pending fix is picked up promptly even
|
|
1092
|
-
* when the driver had died (e.g. its durable advance job expired/was evicted before the stale-
|
|
1093
|
-
* run sweeper re-drove it) — `startRun` is idempotent for a live run (the exclusive advance
|
|
1094
|
-
* queue no-ops a duplicate send), so this only has an effect when no driver is currently
|
|
1095
|
-
* polling. A spend-paused run is left paused (it resumes through its own path).
|
|
1096
|
-
*/
|
|
1097
|
-
async requestHumanReviewFix(workspaceId, blockId, instructions) {
|
|
1098
|
-
const block = await this.blockRepository.get(workspaceId, blockId);
|
|
1099
|
-
if (!block?.executionId) {
|
|
1100
|
-
throw new ConflictError('No human-review gate is currently awaiting input');
|
|
1101
|
-
}
|
|
1102
|
-
// Optimistic-concurrency write: parking the `pendingFix` can race the gate's own poll
|
|
1103
|
-
// (the durable driver advancing the run), so re-read + re-apply on fresh state instead
|
|
1104
|
-
// of clobbering — the lost-update fix, same path as resolveDecision. The validation runs
|
|
1105
|
-
// inside the mutation so it sees the run as it stands at write time.
|
|
1106
|
-
const instance = await this.runStateMachine.mutateInstance(workspaceId, block.executionId, (inst) => {
|
|
1107
|
-
const step = inst.steps[inst.currentStep];
|
|
1108
|
-
if (!step || step.agentKind !== HUMAN_REVIEW_AGENT_KIND || !step.gate) {
|
|
1109
|
-
throw new ConflictError('No human-review gate is currently awaiting input');
|
|
1110
|
-
}
|
|
1111
|
-
// The fix is consumed by evaluateGate's pendingFix branch, which dispatches the fixer
|
|
1112
|
-
// ONLY when the gate's provider is wired AND there is an async executor to escalate to.
|
|
1113
|
-
// Reject up front when neither holds, instead of silently parking a pendingFix the gate
|
|
1114
|
-
// would discard on its pass-through (an unwired gate advances) — the caller must see the
|
|
1115
|
-
// failure, not a 200.
|
|
1116
|
-
const gate = this.runDispatcher.gateFor(step.agentKind);
|
|
1117
|
-
if (!gate?.wired() || !isAsyncAgentExecutor(this.agentExecutor)) {
|
|
1118
|
-
throw new ConflictError('The human-review gate cannot dispatch a fix on this deployment (no review provider or async executor configured)');
|
|
1119
|
-
}
|
|
1120
|
-
step.gate.pendingFix = { instructions, at: this.clock.now() };
|
|
1121
|
-
// Re-arm a decision-parked run so the re-driven loop polls instead of no-oping; a spend-
|
|
1122
|
-
// paused run stays paused.
|
|
1123
|
-
if (inst.status === 'blocked')
|
|
1124
|
-
inst.status = 'running';
|
|
1125
|
-
});
|
|
1126
|
-
await this.runStateMachine.emitInstance(workspaceId, instance);
|
|
1127
|
-
// Ensure a driver is active to consume the pending fix (idempotent for a live run).
|
|
1128
|
-
if (instance.status === 'running') {
|
|
1129
|
-
await this.workRunner.startRun(workspaceId, instance.id);
|
|
1130
|
-
}
|
|
1131
|
-
return instance;
|
|
1132
|
-
}
|
|
1133
1058
|
/**
|
|
1134
1059
|
* Merge a block's PR(s) for real, then mark it `done`. The remote merge happens FIRST (via
|
|
1135
1060
|
* the {@link PullRequestMerger} port) and only on its success does the block flip to `done`
|
|
@@ -1255,237 +1180,38 @@ export class ExecutionService {
|
|
|
1255
1180
|
applyModuleAssignment(workspaceId, taskId) {
|
|
1256
1181
|
return this.postMergeBoard.applyModuleAssignment(workspaceId, taskId);
|
|
1257
1182
|
}
|
|
1183
|
+
// ---- human decision surface (delegated) ---------------------------------
|
|
1184
|
+
// Resolve / approve / request-changes / reject / merge and the human-review fix request all
|
|
1185
|
+
// live on {@link StepDecisionController} — the decisions PEOPLE make about a parked run, as
|
|
1186
|
+
// opposed to the engine's own advance path. These stay here as thin delegates because the HTTP
|
|
1187
|
+
// + public-API controllers reach them through the engine facade.
|
|
1258
1188
|
/** Resolve a pending decision; the run's next step lets the agent finish it. */
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
// Optimistic-concurrency write: a second resolve (double-click) or a racing driver
|
|
1262
|
-
// poll can't clobber the chosen decision — the loser re-reads and re-applies.
|
|
1263
|
-
const instance = await this.runStateMachine.mutateInstance(workspaceId, executionId, async (inst) => {
|
|
1264
|
-
const step = inst.steps.find((s) => s.decision?.id === decisionId);
|
|
1265
|
-
if (!step || !step.decision)
|
|
1266
|
-
throw new NotFoundError('Decision', decisionId);
|
|
1267
|
-
step.decision.chosen = choice;
|
|
1268
|
-
this.stepGraph.startStep(step);
|
|
1269
|
-
if (inst.status === 'blocked')
|
|
1270
|
-
inst.status = 'running';
|
|
1271
|
-
await this.runStateMachine.updateBlockProgress(workspaceId, inst, 'in_progress');
|
|
1272
|
-
});
|
|
1273
|
-
// Wake the parked durable run, if any. The DB write above remains the source
|
|
1274
|
-
// of truth (so the backstop sweeper can still re-drive it); the signal is an
|
|
1275
|
-
// optimisation that lets the workflow continue immediately.
|
|
1276
|
-
await this.workRunner.signalDecision(workspaceId, instance.id, decisionId, choice);
|
|
1277
|
-
await this.runStateMachine.emitInstance(workspaceId, instance);
|
|
1278
|
-
return instance;
|
|
1189
|
+
resolveDecision(workspaceId, executionId, decisionId, choice) {
|
|
1190
|
+
return this.stepDecisions.resolveDecision(workspaceId, executionId, decisionId, choice);
|
|
1279
1191
|
}
|
|
1280
|
-
/**
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
* {@link resolveDecision}'s durable-wake but *advances* the pipeline instead of
|
|
1284
|
-
* re-running the step (the step is already done). Idempotent — re-approving an
|
|
1285
|
-
* already-approved gate is a no-op.
|
|
1286
|
-
*/
|
|
1287
|
-
async approveStep(workspaceId, executionId, approvalId, opts = {}) {
|
|
1288
|
-
await this.requireWorkspace(workspaceId);
|
|
1289
|
-
// Optimistic-concurrency write like resolveDecision/requestStepChanges: an approve
|
|
1290
|
-
// holding a stale snapshot (a racing reject, a driver poll, a terminal transition)
|
|
1291
|
-
// must re-read and re-validate rather than blind-write — otherwise it can resurrect
|
|
1292
|
-
// a run another writer already failed. The advance's in-memory half runs inside the
|
|
1293
|
-
// CAS; the non-idempotent side effects (block writes, driver signal, emit) run once
|
|
1294
|
-
// after, on the winning state.
|
|
1295
|
-
let stepIndex = -1;
|
|
1296
|
-
let alreadyApproved = false;
|
|
1297
|
-
const instance = await this.runStateMachine.mutateInstance(workspaceId, executionId, (inst) => {
|
|
1298
|
-
alreadyApproved = false;
|
|
1299
|
-
stepIndex = inst.steps.findIndex((s) => s.approval?.id === approvalId);
|
|
1300
|
-
const step = inst.steps[stepIndex];
|
|
1301
|
-
if (!step || !step.approval)
|
|
1302
|
-
throw new NotFoundError('Approval', approvalId);
|
|
1303
|
-
this.assertNotIterativeGate(step);
|
|
1304
|
-
if (step.approval.status === 'approved') {
|
|
1305
|
-
alreadyApproved = true;
|
|
1306
|
-
return;
|
|
1307
|
-
}
|
|
1308
|
-
if (step.approval.status === 'rejected') {
|
|
1309
|
-
throw new ConflictError(`Approval '${approvalId}' was rejected`);
|
|
1310
|
-
}
|
|
1311
|
-
if (inst.status === 'failed' || inst.status === 'done') {
|
|
1312
|
-
throw new ConflictError(`Execution '${executionId}' is already ${inst.status}`);
|
|
1313
|
-
}
|
|
1314
|
-
// A human edit to the proposal replaces the agent's text, so the revised
|
|
1315
|
-
// proposal is what downstream steps read (via priorOutputs). That only holds when the
|
|
1316
|
-
// output IS the agent's work product: a step whose output is a RENDERING of an
|
|
1317
|
-
// already-ingested artifact (the spec doc, the blueprint tree, the initiative plan —
|
|
1318
|
-
// see `reviewableArtifactOutput`) would take the edit into `step.output` while the
|
|
1319
|
-
// committed artifact stayed the ingested one, so the correction silently reaches
|
|
1320
|
-
// nothing. Refuse rather than accept-and-drop; the reviewer's route is "request
|
|
1321
|
-
// changes", which re-runs the producer with the correction as feedback.
|
|
1322
|
-
if (opts.proposal !== undefined) {
|
|
1323
|
-
if (step.outputIsRendered) {
|
|
1324
|
-
throw new ValidationError("This step's output is a rendering of the artifact it already produced, so edits " +
|
|
1325
|
-
'to it cannot change that artifact. Request changes instead — the step re-runs ' +
|
|
1326
|
-
'with your feedback.', { reason: 'proposal_not_editable' });
|
|
1327
|
-
}
|
|
1328
|
-
step.output = opts.proposal;
|
|
1329
|
-
step.approval.proposal = opts.proposal;
|
|
1330
|
-
}
|
|
1331
|
-
step.approval.status = 'approved';
|
|
1332
|
-
// A gate is never raised on the final step, but the shared advance stays defensive.
|
|
1333
|
-
this.runStateMachine.advanceRunPastGate(inst, stepIndex);
|
|
1334
|
-
});
|
|
1335
|
-
if (alreadyApproved)
|
|
1336
|
-
return instance;
|
|
1337
|
-
await this.runStateMachine.settleAdvancedGate(workspaceId, instance, stepIndex);
|
|
1338
|
-
return instance;
|
|
1192
|
+
/** Approve a step's gated proposal (optionally replacing it with the human's edit). */
|
|
1193
|
+
approveStep(workspaceId, executionId, approvalId, opts = {}) {
|
|
1194
|
+
return this.stepDecisions.approveStep(workspaceId, executionId, approvalId, opts);
|
|
1339
1195
|
}
|
|
1340
|
-
/**
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
* folded into the agent's context (see {@link AgentContextBuilder}). The run is left
|
|
1344
|
-
* `running` on the same step; on the re-run's completion the gate is raised
|
|
1345
|
-
* afresh. At least one of `feedback`/`comments` is expected (the controller
|
|
1346
|
-
* validates this), but an empty review is harmless — the agent simply re-runs.
|
|
1347
|
-
*/
|
|
1348
|
-
async requestStepChanges(workspaceId, executionId, approvalId, review) {
|
|
1349
|
-
await this.requireWorkspace(workspaceId);
|
|
1350
|
-
// Optimistic-concurrency write: two concurrent change-requests on the same gate
|
|
1351
|
-
// (the documented double-submit) can't both dispatch a re-run — the loser re-reads,
|
|
1352
|
-
// sees `changes_requested`, and is rejected below instead of clobbering.
|
|
1353
|
-
const instance = await this.runStateMachine.mutateInstance(workspaceId, executionId, (inst) => {
|
|
1354
|
-
const step = inst.steps.find((s) => s.approval?.id === approvalId);
|
|
1355
|
-
if (!step || !step.approval)
|
|
1356
|
-
throw new NotFoundError('Approval', approvalId);
|
|
1357
|
-
this.assertNotIterativeGate(step);
|
|
1358
|
-
if (step.approval.status === 'approved') {
|
|
1359
|
-
throw new ConflictError(`Approval '${approvalId}' is already approved`);
|
|
1360
|
-
}
|
|
1361
|
-
if (step.approval.status === 'rejected') {
|
|
1362
|
-
throw new ConflictError(`Approval '${approvalId}' was rejected`);
|
|
1363
|
-
}
|
|
1364
|
-
// A re-run is already in flight (and will raise a fresh gate on completion);
|
|
1365
|
-
// acting on this now-stale gate id would dispatch duplicate work.
|
|
1366
|
-
if (step.approval.status === 'changes_requested') {
|
|
1367
|
-
throw new ConflictError(`Approval '${approvalId}' is already being re-run`);
|
|
1368
|
-
}
|
|
1369
|
-
const stepIndex = inst.steps.findIndex((s) => s.approval?.id === approvalId);
|
|
1370
|
-
step.approval.status = 'changes_requested';
|
|
1371
|
-
step.approval.feedback = review.feedback;
|
|
1372
|
-
step.approval.comments = review.comments?.length ? review.comments : undefined;
|
|
1373
|
-
// A companion's gate reviews the PRODUCER's output, not the companion's own work:
|
|
1374
|
-
// requesting changes here must re-run the producer (with the human's feedback
|
|
1375
|
-
// folded in) and re-grade, NOT re-run the companion. Redirect the rework to the
|
|
1376
|
-
// nearest preceding step of one of the companion's target kinds.
|
|
1377
|
-
if (isCompanionKind(step.agentKind)) {
|
|
1378
|
-
const targets = companionTargets(step.agentKind);
|
|
1379
|
-
let producerIndex = -1;
|
|
1380
|
-
for (let i = stepIndex - 1; i >= 0; i--) {
|
|
1381
|
-
if (targets.includes(inst.steps[i].agentKind)) {
|
|
1382
|
-
producerIndex = i;
|
|
1383
|
-
break;
|
|
1384
|
-
}
|
|
1385
|
-
}
|
|
1386
|
-
const producer = producerIndex >= 0 ? inst.steps[producerIndex] : undefined;
|
|
1387
|
-
if (producer) {
|
|
1388
|
-
// Re-run the producer (with the human's feedback) and every step up to and
|
|
1389
|
-
// including the companion, then the companion re-grades. Does NOT touch the
|
|
1390
|
-
// companion's automatic-rework budget — a human-driven iteration is unbounded.
|
|
1391
|
-
const previousProposal = producer.output ?? step.approval.proposal;
|
|
1392
|
-
this.stepGraph.rerunProducerThrough(inst, producerIndex, stepIndex, {
|
|
1393
|
-
previousProposal,
|
|
1394
|
-
feedback: review.feedback ?? '',
|
|
1395
|
-
...(review.comments?.length ? { comments: review.comments } : {}),
|
|
1396
|
-
});
|
|
1397
|
-
if (inst.status === 'blocked')
|
|
1398
|
-
inst.status = 'running';
|
|
1399
|
-
return;
|
|
1400
|
-
}
|
|
1401
|
-
}
|
|
1402
|
-
// Drop the live job handle so the re-run dispatches fresh work rather than
|
|
1403
|
-
// re-attaching to the finished job (async steps); inline steps ignore this.
|
|
1404
|
-
step.jobId = undefined;
|
|
1405
|
-
// A requested re-run is a fresh execution: clear the prior timing so the next
|
|
1406
|
-
// start/finish times this attempt rather than spanning the human gate wait.
|
|
1407
|
-
step.startedAt = null;
|
|
1408
|
-
step.finishedAt = null;
|
|
1409
|
-
this.stepGraph.startStep(step);
|
|
1410
|
-
if (inst.status === 'blocked')
|
|
1411
|
-
inst.status = 'running';
|
|
1412
|
-
});
|
|
1413
|
-
await this.workRunner.signalDecision(workspaceId, instance.id, approvalId, 'changes_requested');
|
|
1414
|
-
await this.runStateMachine.emitInstance(workspaceId, instance);
|
|
1415
|
-
return instance;
|
|
1196
|
+
/** Request changes on a step's gated proposal; the step re-runs with the feedback folded in. */
|
|
1197
|
+
requestStepChanges(workspaceId, executionId, approvalId, review) {
|
|
1198
|
+
return this.stepDecisions.requestStepChanges(workspaceId, executionId, approvalId, review);
|
|
1416
1199
|
}
|
|
1417
|
-
/**
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
* the board surfaces it via the shared failure banner (block → `blocked`) with a
|
|
1421
|
-
* Retry affordance. The parked durable run is woken so it observes the now-terminal
|
|
1422
|
-
* status and stops (the workflow's advance loop no-ops on a non-running run).
|
|
1423
|
-
* Idempotent — rejecting an already-terminal gate is a no-op.
|
|
1424
|
-
*/
|
|
1425
|
-
async rejectStep(workspaceId, executionId, approvalId, reason) {
|
|
1426
|
-
await this.requireWorkspace(workspaceId);
|
|
1427
|
-
// Optimistic-concurrency write: a reject racing the durable driver (or a concurrent
|
|
1428
|
-
// resolve/request-changes on the same gate) re-reads and re-applies instead of
|
|
1429
|
-
// clobbering the other writer — the lost-update fix, same as resolveDecision.
|
|
1430
|
-
let alreadyRejected = false;
|
|
1431
|
-
const instance = await this.runStateMachine.mutateInstance(workspaceId, executionId, (inst) => {
|
|
1432
|
-
const step = inst.steps.find((s) => s.approval?.id === approvalId);
|
|
1433
|
-
if (!step || !step.approval)
|
|
1434
|
-
throw new NotFoundError('Approval', approvalId);
|
|
1435
|
-
this.assertNotIterativeGate(step);
|
|
1436
|
-
if (step.approval.status === 'approved') {
|
|
1437
|
-
throw new ConflictError(`Approval '${approvalId}' is already approved`);
|
|
1438
|
-
}
|
|
1439
|
-
// A re-run is in flight; this gate id is stale (a fresh one is raised on its
|
|
1440
|
-
// completion). Reject the current gate via that fresh id, not this one.
|
|
1441
|
-
if (step.approval.status === 'changes_requested') {
|
|
1442
|
-
throw new ConflictError(`Approval '${approvalId}' is being re-run`);
|
|
1443
|
-
}
|
|
1444
|
-
// Already rejected (and the run already failed): leave it as-is and skip failRun below.
|
|
1445
|
-
if (step.approval.status === 'rejected') {
|
|
1446
|
-
alreadyRejected = true;
|
|
1447
|
-
return;
|
|
1448
|
-
}
|
|
1449
|
-
step.approval.status = 'rejected';
|
|
1450
|
-
if (reason)
|
|
1451
|
-
step.approval.feedback = reason;
|
|
1452
|
-
});
|
|
1453
|
-
if (alreadyRejected)
|
|
1454
|
-
return instance;
|
|
1455
|
-
const message = reason
|
|
1456
|
-
? `A reviewer rejected the proposal: ${reason}`
|
|
1457
|
-
: 'A reviewer rejected the proposal, stopping the run.';
|
|
1458
|
-
// failRun persists the terminal failure + flips the block to `blocked` and emits.
|
|
1459
|
-
await this.failRun(workspaceId, executionId, message, 'rejected');
|
|
1460
|
-
// Wake the parked durable run; it re-reads the now-terminal status and stops.
|
|
1461
|
-
await this.workRunner.signalDecision(workspaceId, instance.id, approvalId, 'rejected');
|
|
1462
|
-
return assertFound(await this.executionRepository.get(workspaceId, executionId), 'Execution', executionId);
|
|
1200
|
+
/** Reject a step's gated proposal; the run stops with a `rejected` failure. */
|
|
1201
|
+
rejectStep(workspaceId, executionId, approvalId, reason) {
|
|
1202
|
+
return this.stepDecisions.rejectStep(workspaceId, executionId, approvalId, reason);
|
|
1463
1203
|
}
|
|
1464
|
-
/**
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
*
|
|
1468
|
-
* `reviewEffort` records, in the same call, how much review the PR actually needed. It is
|
|
1469
|
-
* always optional: an untagged merge settles the track record with a null tag and nothing
|
|
1470
|
-
* downstream breaks. The record settle runs AFTER the merge and is best-effort inside the
|
|
1471
|
-
* service, so no part of this feature can fail or block a merge.
|
|
1472
|
-
*/
|
|
1473
|
-
async mergePr(workspaceId, blockId, reviewEffort) {
|
|
1474
|
-
await this.requireWorkspace(workspaceId);
|
|
1475
|
-
const block = await this.requireBlock(workspaceId, blockId);
|
|
1476
|
-
if (block.status !== 'pr_ready') {
|
|
1477
|
-
throw new ConflictError(`Block '${blockId}' has no PR awaiting merge`, 'no_pr_to_merge');
|
|
1478
|
-
}
|
|
1479
|
-
await this.finalizeMerge(workspaceId, blockId);
|
|
1480
|
-
await this.mergePolicy.recordHumanMerge(workspaceId, blockId, reviewEffort);
|
|
1481
|
-
return this.requireBlock(workspaceId, blockId);
|
|
1204
|
+
/** Dispatch the human-review gate's fixer from a human's freeform instructions. */
|
|
1205
|
+
requestHumanReviewFix(workspaceId, blockId, instructions) {
|
|
1206
|
+
return this.stepDecisions.requestHumanReviewFix(workspaceId, blockId, instructions);
|
|
1482
1207
|
}
|
|
1483
|
-
/**
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1208
|
+
/** Merge an open PR for real, moving the block from `pr_ready` to `done`. */
|
|
1209
|
+
mergePr(workspaceId, blockId, reviewEffort) {
|
|
1210
|
+
return this.stepDecisions.mergePr(workspaceId, blockId, reviewEffort);
|
|
1211
|
+
}
|
|
1212
|
+
/** Record that a human DECLINED to merge (the review card was dismissed, not acted on). */
|
|
1487
1213
|
recordMergeRejection(workspaceId, executionId) {
|
|
1488
|
-
return this.
|
|
1214
|
+
return this.stepDecisions.recordMergeRejection(workspaceId, executionId);
|
|
1489
1215
|
}
|
|
1490
1216
|
/**
|
|
1491
1217
|
* Record a terminal agent failure: persist a structured {@link AgentFailure},
|