@cat-factory/orchestration 0.36.5 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/container.d.ts +6 -4
- package/dist/container.d.ts.map +1 -1
- package/dist/container.js.map +1 -1
- package/dist/modules/artifacts/artifactRetention.d.ts +8 -5
- package/dist/modules/artifacts/artifactRetention.d.ts.map +1 -1
- package/dist/modules/artifacts/artifactRetention.js +10 -4
- package/dist/modules/artifacts/artifactRetention.js.map +1 -1
- package/dist/modules/execution/ExecutionService.d.ts +33 -12
- package/dist/modules/execution/ExecutionService.d.ts.map +1 -1
- package/dist/modules/execution/ExecutionService.js +243 -129
- package/dist/modules/execution/ExecutionService.js.map +1 -1
- package/dist/modules/execution/VisualConfirmationController.d.ts +13 -4
- package/dist/modules/execution/VisualConfirmationController.d.ts.map +1 -1
- package/dist/modules/execution/VisualConfirmationController.js +22 -11
- package/dist/modules/execution/VisualConfirmationController.js.map +1 -1
- package/dist/modules/execution/step-handler-registry.d.ts +22 -1
- package/dist/modules/execution/step-handler-registry.d.ts.map +1 -1
- package/package.json +10 -10
|
@@ -65,6 +65,16 @@ function parseRepoFromPullUrl(url) {
|
|
|
65
65
|
return undefined;
|
|
66
66
|
return { owner: match[1], repo: match[2] };
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* The inline review/brainstorm gate kinds, all driven through the {@link ReviewGateController}
|
|
70
|
+
* by the engine's `review-gate` StepHandler. Kept in sync with the handler's `switch`.
|
|
71
|
+
*/
|
|
72
|
+
const REVIEW_GATE_AGENT_KINDS = new Set([
|
|
73
|
+
REQUIREMENTS_REVIEW_AGENT_KIND,
|
|
74
|
+
CLARITY_REVIEW_AGENT_KIND,
|
|
75
|
+
REQUIREMENTS_BRAINSTORM_AGENT_KIND,
|
|
76
|
+
ARCHITECTURE_BRAINSTORM_AGENT_KIND,
|
|
77
|
+
]);
|
|
68
78
|
/**
|
|
69
79
|
* The execution engine. It orchestrates a pipeline of agent-performed steps and
|
|
70
80
|
* is fully deterministic: `advanceInstance` moves one run forward by exactly one
|
|
@@ -153,7 +163,12 @@ export class ExecutionService {
|
|
|
153
163
|
* and {@link StepHandler}. Engine-internal (no public registration seam).
|
|
154
164
|
*/
|
|
155
165
|
stepHandlerCache;
|
|
156
|
-
|
|
166
|
+
/**
|
|
167
|
+
* Lazily-built, order-sorted completion-path interceptor list (companion/tester verdict
|
|
168
|
+
* short-circuits). See {@link dispatchStepCompletionInterceptor}. Engine-internal.
|
|
169
|
+
*/
|
|
170
|
+
stepCompletionInterceptorCache;
|
|
171
|
+
constructor({ workspaceRepository, blockRepository, pipelineRepository, executionRepository, accountRepository, idGenerator, clock, agentExecutor, workRunner, executionEventPublisher, boardService, spendService, documentRepository, documentUrlResolver, taskRepository, requirementReviewRepository, requirementReviewService, kaizenScheduler, clarityReviewRepository, clarityReviewService, brainstormServices, brainstormSessionRepository, fragmentResolver, environmentProvisioning, environmentTeardown, branchUpdater, blueprintReconciler, notificationService, resolveBinaryArtifactStore, workspaceSettingsService, llmObservability, pullRequestMerger, mergePresetRepository, ticketTrackerProvider, issueWriteback, subscriptionActivationRepository, resolveWorkspaceModelDefault, resolveProviderCapabilities, localTestInfraSupported, resolveRunRepoContext, resolveTesterFallbackDefault, resolveRequireEnvironmentProvider, assertAgentBackendConfigured, runInitiatorScope, }) {
|
|
157
172
|
this.runInitiatorScope = runInitiatorScope ?? ((_initiatedBy, fn) => fn());
|
|
158
173
|
this.workspaceRepository = workspaceRepository;
|
|
159
174
|
this.blockRepository = blockRepository;
|
|
@@ -271,7 +286,7 @@ export class ExecutionService {
|
|
|
271
286
|
agentExecutor,
|
|
272
287
|
contextBuilder: this.contextBuilder,
|
|
273
288
|
notificationService,
|
|
274
|
-
...(
|
|
289
|
+
...(resolveBinaryArtifactStore ? { resolveBinaryArtifactStore } : {}),
|
|
275
290
|
resolveMergePreset: (ws, block) => this.resolveMergePreset(ws, block),
|
|
276
291
|
parkStepOnDecision: (ws, i, s, p) => this.parkStepOnDecision(ws, i, s, p),
|
|
277
292
|
finishStep: (s) => this.finishStep(s),
|
|
@@ -819,7 +834,7 @@ export class ExecutionService {
|
|
|
819
834
|
}
|
|
820
835
|
// The fixed run-lifecycle preamble is done; hand the per-kind work to the
|
|
821
836
|
// engine-internal StepHandler registry (the first handler whose `canHandle` claims
|
|
822
|
-
// this step). See {@link dispatchStepHandler} / {@link
|
|
837
|
+
// this step). See {@link dispatchStepHandler} / {@link handleAgentStep}.
|
|
823
838
|
return this.dispatchStepHandler({
|
|
824
839
|
workspaceId,
|
|
825
840
|
instance,
|
|
@@ -830,77 +845,16 @@ export class ExecutionService {
|
|
|
830
845
|
});
|
|
831
846
|
}
|
|
832
847
|
/**
|
|
833
|
-
* The
|
|
834
|
-
*
|
|
835
|
-
*
|
|
836
|
-
*
|
|
837
|
-
*
|
|
848
|
+
* The generic container/inline-agent step — the lowest-priority StepHandler, claiming
|
|
849
|
+
* every step no more-specific handler did (coder, architect, spec-writer, merger,
|
|
850
|
+
* task-estimator, the container-backed companions, …). Builds the agent context, runs the
|
|
851
|
+
* kind's pre-ops, then either dispatches an async container job and parks (the durable
|
|
852
|
+
* driver polls between sleeps) or runs the inline LLM call and records the result. This is
|
|
853
|
+
* what the dispatch chain falls through to; all the deterministic / gate / inline-review
|
|
854
|
+
* kinds are claimed earlier by their own handlers (see {@link buildStepHandlerRegistry}).
|
|
838
855
|
*/
|
|
839
|
-
async
|
|
856
|
+
async handleAgentStep(ctx) {
|
|
840
857
|
const { workspaceId, instance, step, block, isFinalStep, options } = ctx;
|
|
841
|
-
// (The `deployer` and `tracker` steps are handled by their own StepHandlers — see
|
|
842
|
-
// {@link buildStepHandlerRegistry} — so they no longer branch here.)
|
|
843
|
-
// A `requirements-review` step runs the inline reviewer and parks for the dedicated
|
|
844
|
-
// review window, driving the iterative answer → incorporate → re-review loop. NOT a
|
|
845
|
-
// container/prose agent. Pass-through when the reviewer isn't wired. The clarity gate
|
|
846
|
-
// shares the SAME flow (only the subject + persisted doc differ); both run through the
|
|
847
|
-
// {@link ReviewGateController}, parameterised by their {@link ReviewKind}.
|
|
848
|
-
if (step.agentKind === REQUIREMENTS_REVIEW_AGENT_KIND) {
|
|
849
|
-
return this.reviewGate.evaluate(this.requirementsKind, workspaceId, instance, step, block, isFinalStep);
|
|
850
|
-
}
|
|
851
|
-
// A `clarity-review` step triages the block's bug report (optionally enriched by an
|
|
852
|
-
// upstream `bug-investigator` step) and parks for the dedicated review window, driving
|
|
853
|
-
// the same iterative loop as the requirements gate. NOT a container/prose agent.
|
|
854
|
-
// Pass-through when the reviewer isn't wired.
|
|
855
|
-
if (step.agentKind === CLARITY_REVIEW_AGENT_KIND) {
|
|
856
|
-
return this.reviewGate.evaluate(this.clarityKind, workspaceId, instance, step, block, isFinalStep);
|
|
857
|
-
}
|
|
858
|
-
// The two brainstorm (structured-dialogue) gates run the inline option-generator and park
|
|
859
|
-
// for the dedicated brainstorm window, driving the same iterative loop as the requirements
|
|
860
|
-
// gate. NOT container/prose agents. Pass-through when the brainstorm module isn't wired.
|
|
861
|
-
if (step.agentKind === REQUIREMENTS_BRAINSTORM_AGENT_KIND) {
|
|
862
|
-
return this.reviewGate.evaluate(this.requirementsBrainstormKind, workspaceId, instance, step, block, isFinalStep);
|
|
863
|
-
}
|
|
864
|
-
if (step.agentKind === ARCHITECTURE_BRAINSTORM_AGENT_KIND) {
|
|
865
|
-
return this.reviewGate.evaluate(this.architectureBrainstormKind, workspaceId, instance, step, block, isFinalStep);
|
|
866
|
-
}
|
|
867
|
-
// A `human-test` gate spins up an ephemeral environment and PARKS for a human to
|
|
868
|
-
// validate the change in a live URL before the run continues — NOT a container/prose
|
|
869
|
-
// agent and NOT a programmatic polling gate (the human is the verdict). It also drives
|
|
870
|
-
// the same helpers the other gates use on demand: the Tester's `fixer` (from findings)
|
|
871
|
-
// and the `conflict-resolver` (after a conflicting pull-main). Degrades to a manual
|
|
872
|
-
// (no-env) mode when no ephemeral-environment provider is wired. See {@link HumanTestController}.
|
|
873
|
-
if (step.agentKind === HUMAN_TEST_AGENT_KIND) {
|
|
874
|
-
return this.humanTestController.evaluate(workspaceId, instance, step, block, isFinalStep);
|
|
875
|
-
}
|
|
876
|
-
// A `visual-confirmation` gate gathers the UI tester's screenshots + the uploaded
|
|
877
|
-
// reference designs and PARKS for a human to review actual-vs-reference, then on demand
|
|
878
|
-
// dispatches the Tester's `fixer`. Passes through (auto-advances) when no binary-artifact
|
|
879
|
-
// store is wired. See {@link VisualConfirmationController}.
|
|
880
|
-
if (step.agentKind === VISUAL_CONFIRM_AGENT_KIND) {
|
|
881
|
-
return this.visualConfirmationController.evaluate(workspaceId, instance, step, block, isFinalStep);
|
|
882
|
-
}
|
|
883
|
-
// A polling gate step (`ci` / `conflicts`) runs a programmatic precheck and only
|
|
884
|
-
// escalates to a helper container agent (`ci-fixer` / `conflict-resolver`) on a
|
|
885
|
-
// negative verdict — no LLM of its own. Pass-through when the gate's provider is
|
|
886
|
-
// not wired. One generic machine drives every gate; see {@link evaluateGate}.
|
|
887
|
-
const gate = this.gateFor(step.agentKind);
|
|
888
|
-
if (gate) {
|
|
889
|
-
return this.evaluateGate(workspaceId, instance, step, block, isFinalStep, gate);
|
|
890
|
-
}
|
|
891
|
-
// A companion step grades the nearest preceding producer of one of its target
|
|
892
|
-
// kinds, looping it back for automatic rework below the threshold (and failing
|
|
893
|
-
// the run once the budget is spent) before any human gate. See evaluateCompanion.
|
|
894
|
-
//
|
|
895
|
-
// INLINE companions (architect-companion / spec-companion) run their LLM grading right
|
|
896
|
-
// here. CONTAINER-backed companions (reviewer / doc-reviewer) instead fall through to the
|
|
897
|
-
// generic async container dispatch below — they clone the producer's PR branch and review
|
|
898
|
-
// the REAL repository — and their verdict is resolved in `recordStepResult` via
|
|
899
|
-
// `companionController.resolveContainerVerdict` (which runs the SAME threshold / rework
|
|
900
|
-
// loop). A summary-only review is useless; the container reviewer reads the actual diff.
|
|
901
|
-
if (isCompanionKind(step.agentKind) && !isContainerBackedCompanion(step.agentKind)) {
|
|
902
|
-
return this.companionController.evaluate(workspaceId, instance, step, block, isFinalStep, options);
|
|
903
|
-
}
|
|
904
858
|
// Async (container) steps don't block: dispatch the job and park. The durable
|
|
905
859
|
// driver polls `pollAgentJob` between sleeps so the run can span far longer
|
|
906
860
|
// than a single durable step's timeout, while each step stays short. A set
|
|
@@ -1549,26 +1503,21 @@ export class ExecutionService {
|
|
|
1549
1503
|
await this.emitInstance(workspaceId, instance);
|
|
1550
1504
|
return { kind: 'awaiting_decision', decisionId: step.decision.id };
|
|
1551
1505
|
}
|
|
1552
|
-
//
|
|
1553
|
-
//
|
|
1554
|
-
//
|
|
1555
|
-
//
|
|
1556
|
-
//
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
if (isTesterKind(step.agentKind) && result.testReport !== undefined) {
|
|
1568
|
-
const looped = await this.testerController.resolveTesterResult(workspaceId, instance, step, result);
|
|
1569
|
-
if (looped)
|
|
1570
|
-
return looped;
|
|
1571
|
-
}
|
|
1506
|
+
// Completion-path interceptors short-circuit before the normal finish/advance for the
|
|
1507
|
+
// few kinds whose verdict drives run flow: a container-backed companion applies its
|
|
1508
|
+
// threshold/rework/human-gate loop, and a Tester re-runs its `fixer` on a withheld
|
|
1509
|
+
// greenlight. A non-null outcome replaces the normal completion; null (a Tester
|
|
1510
|
+
// greenlight, or a companion whose block can't be loaded) falls through. See
|
|
1511
|
+
// {@link buildStepCompletionInterceptors}.
|
|
1512
|
+
const intercepted = await this.dispatchStepCompletionInterceptor({
|
|
1513
|
+
workspaceId,
|
|
1514
|
+
instance,
|
|
1515
|
+
step,
|
|
1516
|
+
isFinalStep,
|
|
1517
|
+
result,
|
|
1518
|
+
});
|
|
1519
|
+
if (intercepted)
|
|
1520
|
+
return intercepted;
|
|
1572
1521
|
// The step completed.
|
|
1573
1522
|
step.output = result.output ?? '';
|
|
1574
1523
|
// Surface a registered custom kind's structured JSON on the step so the SPA's
|
|
@@ -1612,38 +1561,25 @@ export class ExecutionService {
|
|
|
1612
1561
|
.catch(() => { });
|
|
1613
1562
|
}
|
|
1614
1563
|
}
|
|
1615
|
-
//
|
|
1616
|
-
//
|
|
1617
|
-
//
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
//
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
await
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
}
|
|
1635
|
-
// A `task-estimator` step emits a JSON triage (complexity/risk/impact). Parse it
|
|
1636
|
-
// tolerantly, persist it on the block (used to gate consensus steps + surfaced in
|
|
1637
|
-
// the UI), and replace the raw JSON output with a readable summary. An unparseable
|
|
1638
|
-
// estimate leaves the block untouched and keeps the raw output (no run failure).
|
|
1639
|
-
// The estimate works the same whether the single-actor estimator or the consensus
|
|
1640
|
-
// ranked-scoring variant produced the JSON — both land here.
|
|
1641
|
-
if (step.agentKind === TASK_ESTIMATOR_AGENT_KIND) {
|
|
1642
|
-
const estimate = coerceTaskEstimate(step.output, result.model ?? step.model ?? null, this.clock.now());
|
|
1643
|
-
if (estimate) {
|
|
1644
|
-
await this.blockRepository.update(workspaceId, instance.blockId, { estimate });
|
|
1645
|
-
step.output = summarizeEstimate(estimate);
|
|
1646
|
-
}
|
|
1564
|
+
// Run any POST-COMPLETION resolver registered for this step kind (blueprint/spec
|
|
1565
|
+
// ingestion, task-estimate persistence). It reshapes the agent's structured result into
|
|
1566
|
+
// domain state and may replace `step.output` (the estimator's readable summary). Its
|
|
1567
|
+
// POSITION is load-bearing — it runs after the output is recorded but BEFORE the
|
|
1568
|
+
// reviewable-output rendering and the follow-up/approval gates read `step.output`, so it
|
|
1569
|
+
// sits exactly where the old inline ingestion branches did. See
|
|
1570
|
+
// {@link buildStepResolverRegistry} and {@link StepCompletionResolver.phase}.
|
|
1571
|
+
const postCompletionResolver = this.stepResolverFor(step.agentKind);
|
|
1572
|
+
if (postCompletionResolver?.phase === 'post-completion' &&
|
|
1573
|
+
(postCompletionResolver.applies?.(result) ?? true)) {
|
|
1574
|
+
const resolution = await postCompletionResolver.resolve({
|
|
1575
|
+
workspaceId,
|
|
1576
|
+
instance,
|
|
1577
|
+
step,
|
|
1578
|
+
result,
|
|
1579
|
+
isFinalStep,
|
|
1580
|
+
});
|
|
1581
|
+
if (resolution?.output !== undefined)
|
|
1582
|
+
step.output = resolution.output;
|
|
1647
1583
|
}
|
|
1648
1584
|
// A producer that emits a STRUCTURED ARTIFACT (the spec doc, the blueprint tree, …)
|
|
1649
1585
|
// returns its raw Pi transcript summary as `result.output` — useless for review.
|
|
@@ -1702,7 +1638,9 @@ export class ExecutionService {
|
|
|
1702
1638
|
// tells `finalizeBlock` to leave it alone.
|
|
1703
1639
|
const resolver = this.stepResolverFor(step.agentKind);
|
|
1704
1640
|
let resolverOwnsTerminalStatus = false;
|
|
1705
|
-
if (resolver &&
|
|
1641
|
+
if (resolver &&
|
|
1642
|
+
(resolver.phase ?? 'terminal') === 'terminal' &&
|
|
1643
|
+
(resolver.applies?.(result) ?? true)) {
|
|
1706
1644
|
const resolution = await resolver.resolve({
|
|
1707
1645
|
workspaceId,
|
|
1708
1646
|
instance,
|
|
@@ -2118,17 +2056,144 @@ export class ExecutionService {
|
|
|
2118
2056
|
return this.recordStepResult(workspaceId, instance, step, isFinalStep, result);
|
|
2119
2057
|
},
|
|
2120
2058
|
},
|
|
2121
|
-
// The
|
|
2122
|
-
//
|
|
2059
|
+
// The `requirements-review` / `clarity-review` / `requirements-brainstorm` /
|
|
2060
|
+
// `architecture-brainstorm` steps are inline reviewers that park for their dedicated
|
|
2061
|
+
// window and drive the iterative answer → incorporate → re-review loop — NOT
|
|
2062
|
+
// container/prose agents. All four run through the {@link ReviewGateController},
|
|
2063
|
+
// parameterised by their {@link ReviewKind} (the per-case `switch` binds each kind's
|
|
2064
|
+
// review type so `evaluate`'s generic infers). Pass-through when the service isn't wired.
|
|
2123
2065
|
{
|
|
2124
|
-
kind: '
|
|
2066
|
+
kind: 'review-gate',
|
|
2067
|
+
order: 120,
|
|
2068
|
+
canHandle: ({ step }) => REVIEW_GATE_AGENT_KINDS.has(step.agentKind),
|
|
2069
|
+
handle: ({ workspaceId, instance, step, block, isFinalStep }) => {
|
|
2070
|
+
switch (step.agentKind) {
|
|
2071
|
+
case REQUIREMENTS_REVIEW_AGENT_KIND:
|
|
2072
|
+
return this.reviewGate.evaluate(this.requirementsKind, workspaceId, instance, step, block, isFinalStep);
|
|
2073
|
+
case CLARITY_REVIEW_AGENT_KIND:
|
|
2074
|
+
return this.reviewGate.evaluate(this.clarityKind, workspaceId, instance, step, block, isFinalStep);
|
|
2075
|
+
case REQUIREMENTS_BRAINSTORM_AGENT_KIND:
|
|
2076
|
+
return this.reviewGate.evaluate(this.requirementsBrainstormKind, workspaceId, instance, step, block, isFinalStep);
|
|
2077
|
+
case ARCHITECTURE_BRAINSTORM_AGENT_KIND:
|
|
2078
|
+
return this.reviewGate.evaluate(this.architectureBrainstormKind, workspaceId, instance, step, block, isFinalStep);
|
|
2079
|
+
// `canHandle` admits only the kinds in REVIEW_GATE_AGENT_KINDS, so every member
|
|
2080
|
+
// must have an explicit case above. Throw loudly if the two ever drift (a new
|
|
2081
|
+
// review kind added to the Set without a case here) rather than silently routing
|
|
2082
|
+
// it to the wrong reviewer.
|
|
2083
|
+
default:
|
|
2084
|
+
throw new Error(`Unhandled review-gate agentKind "${step.agentKind}"`);
|
|
2085
|
+
}
|
|
2086
|
+
},
|
|
2087
|
+
},
|
|
2088
|
+
// A `human-test` gate spins up an ephemeral environment and PARKS for a human to
|
|
2089
|
+
// validate the change in a live URL — NOT a container/prose agent and NOT a
|
|
2090
|
+
// programmatic polling gate (the human is the verdict). Degrades to a manual (no-env)
|
|
2091
|
+
// mode when no ephemeral-environment provider is wired. See {@link HumanTestController}.
|
|
2092
|
+
{
|
|
2093
|
+
kind: HUMAN_TEST_AGENT_KIND,
|
|
2094
|
+
order: 130,
|
|
2095
|
+
canHandle: ({ step }) => step.agentKind === HUMAN_TEST_AGENT_KIND,
|
|
2096
|
+
handle: ({ workspaceId, instance, step, block, isFinalStep }) => this.humanTestController.evaluate(workspaceId, instance, step, block, isFinalStep),
|
|
2097
|
+
},
|
|
2098
|
+
// A `visual-confirmation` gate gathers the UI tester's screenshots + uploaded reference
|
|
2099
|
+
// designs and PARKS for a human to review actual-vs-reference, then on demand dispatches
|
|
2100
|
+
// the Tester's `fixer`. Passes through when no binary-artifact store is wired.
|
|
2101
|
+
// See {@link VisualConfirmationController}.
|
|
2102
|
+
{
|
|
2103
|
+
kind: VISUAL_CONFIRM_AGENT_KIND,
|
|
2104
|
+
order: 140,
|
|
2105
|
+
canHandle: ({ step }) => step.agentKind === VISUAL_CONFIRM_AGENT_KIND,
|
|
2106
|
+
handle: ({ workspaceId, instance, step, block, isFinalStep }) => this.visualConfirmationController.evaluate(workspaceId, instance, step, block, isFinalStep),
|
|
2107
|
+
},
|
|
2108
|
+
// A polling gate step (`ci` / `conflicts` / `post-release-health` / `human-review`) runs
|
|
2109
|
+
// a programmatic precheck and only escalates to a helper container agent on a negative
|
|
2110
|
+
// verdict — no LLM of its own. Pass-through when the gate's provider is not wired. One
|
|
2111
|
+
// generic machine drives every gate; see {@link evaluateGate}. `canHandle` is the gate
|
|
2112
|
+
// registry lookup, so this claims exactly the registered gate kinds.
|
|
2113
|
+
{
|
|
2114
|
+
kind: 'polling-gate',
|
|
2115
|
+
order: 150,
|
|
2116
|
+
canHandle: ({ step }) => this.gateFor(step.agentKind) !== undefined,
|
|
2117
|
+
handle: ({ workspaceId, instance, step, block, isFinalStep }) => this.evaluateGate(workspaceId, instance, step, block, isFinalStep, this.gateFor(step.agentKind)),
|
|
2118
|
+
},
|
|
2119
|
+
// An INLINE companion (architect-companion / spec-companion) grades the nearest
|
|
2120
|
+
// preceding producer right here and loops it back for automatic rework below the
|
|
2121
|
+
// threshold before any human gate. CONTAINER-backed companions (reviewer / doc-reviewer)
|
|
2122
|
+
// do NOT match — they fall through to the generic async container dispatch and have their
|
|
2123
|
+
// verdict resolved by the completion interceptor instead. See {@link CompanionController}.
|
|
2124
|
+
{
|
|
2125
|
+
kind: 'inline-companion',
|
|
2126
|
+
order: 160,
|
|
2127
|
+
canHandle: ({ step }) => isCompanionKind(step.agentKind) && !isContainerBackedCompanion(step.agentKind),
|
|
2128
|
+
handle: ({ workspaceId, instance, step, block, isFinalStep, options }) => this.companionController.evaluate(workspaceId, instance, step, block, isFinalStep, options),
|
|
2129
|
+
},
|
|
2130
|
+
// The generic container/inline-agent step — claims every step no more-specific handler
|
|
2131
|
+
// did. Highest order so it always runs last. See {@link handleAgentStep}.
|
|
2132
|
+
{
|
|
2133
|
+
kind: 'agent',
|
|
2125
2134
|
order: FALLTHROUGH_STEP_HANDLER_ORDER,
|
|
2126
2135
|
canHandle: () => true,
|
|
2127
|
-
handle: (ctx) => this.
|
|
2136
|
+
handle: (ctx) => this.handleAgentStep(ctx),
|
|
2128
2137
|
},
|
|
2129
2138
|
];
|
|
2130
2139
|
return handlers.sort((a, b) => a.order - b.order);
|
|
2131
2140
|
}
|
|
2141
|
+
/**
|
|
2142
|
+
* Run the first completion-path interceptor that claims this finished step, returning its
|
|
2143
|
+
* short-circuit {@link AdvanceResult} (the companion verdict loop / tester re-test) or
|
|
2144
|
+
* `null` to let `recordStepResult`'s normal finish/advance spine run. Engine-internal,
|
|
2145
|
+
* mirroring {@link dispatchStepHandler}.
|
|
2146
|
+
*/
|
|
2147
|
+
async dispatchStepCompletionInterceptor(ctx) {
|
|
2148
|
+
if (!this.stepCompletionInterceptorCache) {
|
|
2149
|
+
this.stepCompletionInterceptorCache = this.buildStepCompletionInterceptors();
|
|
2150
|
+
}
|
|
2151
|
+
for (const interceptor of this.stepCompletionInterceptorCache) {
|
|
2152
|
+
if (interceptor.canIntercept(ctx)) {
|
|
2153
|
+
const outcome = await interceptor.intercept(ctx);
|
|
2154
|
+
if (outcome)
|
|
2155
|
+
return outcome;
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
return null;
|
|
2159
|
+
}
|
|
2160
|
+
/**
|
|
2161
|
+
* Build the order-sorted completion-path interceptors (companion / tester verdict
|
|
2162
|
+
* short-circuits), mirroring {@link buildStepHandlerRegistry} — built-ins constructed
|
|
2163
|
+
* inline closing over `this`, no public registration seam.
|
|
2164
|
+
*/
|
|
2165
|
+
buildStepCompletionInterceptors() {
|
|
2166
|
+
const interceptors = [
|
|
2167
|
+
// A container-backed companion (reviewer / doc-reviewer) just finished reviewing the
|
|
2168
|
+
// real repository on the producer's PR branch and returned its verdict as
|
|
2169
|
+
// `result.custom`. Hand it to the companion loop, which parses the verdict and applies
|
|
2170
|
+
// the SAME threshold / rework / human-gate handling an inline companion gets. Routed
|
|
2171
|
+
// here (not the normal step completion) so the verdict drives the loop instead of being
|
|
2172
|
+
// recorded as plain output. Falls through (returns null) when the block can't be loaded.
|
|
2173
|
+
{
|
|
2174
|
+
kind: 'companion-verdict',
|
|
2175
|
+
order: 100,
|
|
2176
|
+
canIntercept: ({ step }) => isCompanionKind(step.agentKind) && isContainerBackedCompanion(step.agentKind),
|
|
2177
|
+
intercept: async ({ workspaceId, instance, step, isFinalStep, result }) => {
|
|
2178
|
+
const companionBlock = await this.blockRepository.get(workspaceId, instance.blockId);
|
|
2179
|
+
if (!companionBlock)
|
|
2180
|
+
return null;
|
|
2181
|
+
return this.companionController.resolveContainerVerdict(workspaceId, instance, step, companionBlock, isFinalStep, result);
|
|
2182
|
+
},
|
|
2183
|
+
},
|
|
2184
|
+
// A `tester` step returned a structured report. On a withheld greenlight we do NOT
|
|
2185
|
+
// finish the step: loop the `fixer` (within the attempt budget) and re-test, mirroring
|
|
2186
|
+
// the CI gate. A greenlight (or no provider) returns null and falls through to the
|
|
2187
|
+
// normal finish/advance below. Records the report on the step either way.
|
|
2188
|
+
{
|
|
2189
|
+
kind: 'tester-verdict',
|
|
2190
|
+
order: 110,
|
|
2191
|
+
canIntercept: ({ step, result }) => isTesterKind(step.agentKind) && result.testReport !== undefined,
|
|
2192
|
+
intercept: ({ workspaceId, instance, step, result }) => this.testerController.resolveTesterResult(workspaceId, instance, step, result),
|
|
2193
|
+
},
|
|
2194
|
+
];
|
|
2195
|
+
return interceptors.sort((a, b) => a.order - b.order);
|
|
2196
|
+
}
|
|
2132
2197
|
stepResolverFor(agentKind) {
|
|
2133
2198
|
if (!this.stepResolverCache)
|
|
2134
2199
|
this.stepResolverCache = this.buildStepResolverRegistry();
|
|
@@ -2151,6 +2216,55 @@ export class ExecutionService {
|
|
|
2151
2216
|
return { ownsTerminalStatus: true };
|
|
2152
2217
|
},
|
|
2153
2218
|
},
|
|
2219
|
+
// POST-COMPLETION resolvers — run at the early slot (after output is recorded, before
|
|
2220
|
+
// the follow-up/approval gates), reshaping the agent's structured result into domain
|
|
2221
|
+
// state. Lifted verbatim from the old inline `recordStepResult` branches.
|
|
2222
|
+
//
|
|
2223
|
+
// A Blueprinter step produced a fresh service decomposition. Validate it with the
|
|
2224
|
+
// authoritative schema (a bad payload must never touch the board), then reconcile it
|
|
2225
|
+
// in place onto the run's service frame.
|
|
2226
|
+
{
|
|
2227
|
+
kind: BLUEPRINTS_AGENT_KIND,
|
|
2228
|
+
phase: 'post-completion',
|
|
2229
|
+
resolve: async ({ workspaceId, instance, result }) => {
|
|
2230
|
+
if (result.blueprintService !== undefined) {
|
|
2231
|
+
await this.ingestBlueprint(workspaceId, instance.blockId, result.blueprintService);
|
|
2232
|
+
}
|
|
2233
|
+
},
|
|
2234
|
+
},
|
|
2235
|
+
// A spec-writer step produced the service's unified specification (`spec.json`) and
|
|
2236
|
+
// committed it to the implementation branch — strict-validate it then nudge clients —
|
|
2237
|
+
// and reports its BUSINESS-vs-TECHNICAL determination. "No business specs" (a purely
|
|
2238
|
+
// technical task) is a valid outcome the spec-companion's convergence later combines
|
|
2239
|
+
// with its `technicalCorroborated` verdict; recorded even when false so a re-run
|
|
2240
|
+
// reflects the latest.
|
|
2241
|
+
{
|
|
2242
|
+
kind: SPEC_WRITER_AGENT_KIND,
|
|
2243
|
+
phase: 'post-completion',
|
|
2244
|
+
resolve: async ({ workspaceId, step, result }) => {
|
|
2245
|
+
if (result.spec !== undefined)
|
|
2246
|
+
await this.ingestSpec(workspaceId, result.spec);
|
|
2247
|
+
step.noBusinessSpecs = result.noBusinessSpecs === true;
|
|
2248
|
+
},
|
|
2249
|
+
},
|
|
2250
|
+
// A `task-estimator` step emits a JSON triage (complexity/risk/impact). Parse it
|
|
2251
|
+
// tolerantly, persist it on the block (used to gate consensus steps + surfaced in the
|
|
2252
|
+
// UI), and replace the raw JSON output with a readable summary. An unparseable estimate
|
|
2253
|
+
// leaves the block untouched and keeps the raw output (no run failure). Works the same
|
|
2254
|
+
// whether the single-actor estimator or the consensus ranked-scoring variant produced
|
|
2255
|
+
// the JSON. Running at the post-completion slot keeps the summary in `step.output`
|
|
2256
|
+
// before the approval gate reads it as the proposal.
|
|
2257
|
+
{
|
|
2258
|
+
kind: TASK_ESTIMATOR_AGENT_KIND,
|
|
2259
|
+
phase: 'post-completion',
|
|
2260
|
+
resolve: async ({ workspaceId, instance, step, result }) => {
|
|
2261
|
+
const estimate = coerceTaskEstimate(step.output ?? '', result.model ?? step.model ?? null, this.clock.now());
|
|
2262
|
+
if (estimate) {
|
|
2263
|
+
await this.blockRepository.update(workspaceId, instance.blockId, { estimate });
|
|
2264
|
+
return { output: summarizeEstimate(estimate) };
|
|
2265
|
+
}
|
|
2266
|
+
},
|
|
2267
|
+
},
|
|
2154
2268
|
];
|
|
2155
2269
|
const map = new Map(resolvers.map((r) => [r.kind, r]));
|
|
2156
2270
|
// Merge deployment-registered resolvers, mirroring the gate registry below. A
|