@cat-factory/orchestration 0.35.1 → 0.36.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.
@@ -9,7 +9,7 @@ import { assertFound, ConflictError, getErrorMessage, isModelUsable, NotFoundErr
9
9
  import { DEFAULT_MERGE_PRESET } from '@cat-factory/kernel';
10
10
  import { CONFLICTS_AGENT_KIND, MERGER_AGENT_KIND, REQUIREMENTS_REVIEW_AGENT_KIND, CLARITY_REVIEW_AGENT_KIND, REQUIREMENTS_BRAINSTORM_AGENT_KIND, ARCHITECTURE_BRAINSTORM_AGENT_KIND, BUG_INVESTIGATOR_AGENT_KIND, TRACKER_AGENT_KIND, ANALYSIS_AGENT_KIND, TESTER_AGENT_KIND, UI_TESTER_AGENT_KIND, isTesterKind, HUMAN_TEST_AGENT_KIND, VISUAL_CONFIRM_AGENT_KIND, HUMAN_REVIEW_AGENT_KIND, BLUEPRINTS_AGENT_KIND, SPEC_WRITER_AGENT_KIND, } from './ci.logic.js';
11
11
  import { DEFAULT_FOLLOW_UP_MAX_LOOPS, FOLLOW_UP_PRODUCER_KIND, followUpsToSendBack, hasPendingFollowUps, renderFollowUpRework, shouldLoopCoder, } from './followUp.logic.js';
12
- import { AgentContextBuilder } from './AgentContextBuilder.js';
12
+ import { AgentContextBuilder, } from './AgentContextBuilder.js';
13
13
  import { CompanionController } from './CompanionController.js';
14
14
  import { inferTechnicalLabel } from './technical.logic.js';
15
15
  import { MergeResolver } from './MergeResolver.js';
@@ -17,9 +17,10 @@ import { ReviewGateController } from './ReviewGateController.js';
17
17
  import { TesterController } from './TesterController.js';
18
18
  import { HumanTestController } from './HumanTestController.js';
19
19
  import { VisualConfirmationController } from './VisualConfirmationController.js';
20
+ import { FALLTHROUGH_STEP_HANDLER_ORDER, } from './step-handler-registry.js';
20
21
  import { getProvider, recordGateAttempt, registeredGateFactories, registeredStepResolverFactories, requireProvider, } from '@cat-factory/kernel';
21
22
  import { isAsyncAgentExecutor } from '@cat-factory/kernel';
22
- import { isDeployStep } from '@cat-factory/integrations';
23
+ import { isDeployStep, DEPLOYER_AGENT_KIND } from '@cat-factory/integrations';
23
24
  import { dependenciesMet, descendantIds, serviceOf, unmetDependencies, } from '../board/board.logic.js';
24
25
  import { requireWorkspace } from '@cat-factory/kernel';
25
26
  import { planResumedSteps, planRestartFromStep } from './retry.logic.js';
@@ -147,7 +148,12 @@ export class ExecutionService {
147
148
  * {@link stepResolverFor} and {@link StepCompletionResolver}.
148
149
  */
149
150
  stepResolverCache;
150
- constructor({ workspaceRepository, blockRepository, pipelineRepository, executionRepository, accountRepository, idGenerator, clock, agentExecutor, workRunner, executionEventPublisher, boardService, spendService, documentRepository, taskRepository, requirementReviewRepository, requirementReviewService, kaizenScheduler, clarityReviewRepository, clarityReviewService, brainstormServices, brainstormSessionRepository, fragmentResolver, environmentProvisioning, environmentTeardown, branchUpdater, blueprintReconciler, notificationService, binaryArtifactStore, workspaceSettingsService, llmObservability, pullRequestMerger, mergePresetRepository, ticketTrackerProvider, issueWriteback, subscriptionActivationRepository, resolveWorkspaceModelDefault, resolveProviderCapabilities, localTestInfraSupported, resolveRunRepoContext, resolveTesterFallbackDefault, resolveRequireEnvironmentProvider, assertAgentBackendConfigured, runInitiatorScope, }) {
151
+ /**
152
+ * Lazily-built, order-sorted per-step-kind handler list. See {@link dispatchStepHandler}
153
+ * and {@link StepHandler}. Engine-internal (no public registration seam).
154
+ */
155
+ stepHandlerCache;
156
+ 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, binaryArtifactStore, workspaceSettingsService, llmObservability, pullRequestMerger, mergePresetRepository, ticketTrackerProvider, issueWriteback, subscriptionActivationRepository, resolveWorkspaceModelDefault, resolveProviderCapabilities, localTestInfraSupported, resolveRunRepoContext, resolveTesterFallbackDefault, resolveRequireEnvironmentProvider, assertAgentBackendConfigured, runInitiatorScope, }) {
151
157
  this.runInitiatorScope = runInitiatorScope ?? ((_initiatedBy, fn) => fn());
152
158
  this.workspaceRepository = workspaceRepository;
153
159
  this.blockRepository = blockRepository;
@@ -173,6 +179,7 @@ export class ExecutionService {
173
179
  blockRepository,
174
180
  accountRepository,
175
181
  documents: documentRepository,
182
+ documentUrlResolver,
176
183
  tasks: taskRepository,
177
184
  requirementReviews: requirementReviewRepository,
178
185
  clarityReviews: clarityReviewRepository,
@@ -810,21 +817,29 @@ export class ExecutionService {
810
817
  if (step.gating?.enabled && !shouldRunGatedStep(block.estimate, step.gating)) {
811
818
  return this.skipGatedStep(workspaceId, instance, step, isFinalStep);
812
819
  }
813
- // A `deployer` step provisions an ephemeral environment deterministically via
814
- // the provider no LLM, no token usage — when the integration is wired.
815
- // Otherwise it falls through to the normal agent path.
816
- if (this.environmentProvisioning && isDeployStep(step.agentKind)) {
817
- const result = await this.runDeployer(workspaceId, instance, block, options);
818
- return this.recordStepResult(workspaceId, instance, step, isFinalStep, result);
819
- }
820
- // A `tracker` step files a GitHub issue / Jira ticket from the preceding
821
- // `analysis` output (the tech-debt pipeline) — no LLM of its own. It is a
822
- // pass-through when no tracker provider is wired or none is configured for the
823
- // workspace. See {@link runTracker}.
824
- if (step.agentKind === TRACKER_AGENT_KIND) {
825
- const result = await this.runTracker(workspaceId, instance, block);
826
- return this.recordStepResult(workspaceId, instance, step, isFinalStep, result);
827
- }
820
+ // The fixed run-lifecycle preamble is done; hand the per-kind work to the
821
+ // engine-internal StepHandler registry (the first handler whose `canHandle` claims
822
+ // this step). See {@link dispatchStepHandler} / {@link runStepBody}.
823
+ return this.dispatchStepHandler({
824
+ workspaceId,
825
+ instance,
826
+ step,
827
+ block,
828
+ isFinalStep,
829
+ options,
830
+ });
831
+ }
832
+ /**
833
+ * The per-step-kind body run by the StepHandler registry once {@link stepInstance}'s
834
+ * preamble has completed. Phase 0 of the ExecutionService split: a single fallthrough
835
+ * handler delegates the ENTIRE body here unchanged (so dispatch is wired with zero
836
+ * behaviour change); later phases lift each branch below into its own handler and shrink
837
+ * this method until only the generic container/inline-agent tail remains.
838
+ */
839
+ async runStepBody(ctx) {
840
+ 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.)
828
843
  // A `requirements-review` step runs the inline reviewer and parks for the dedicated
829
844
  // review window, driving the iterative answer → incorporate → re-review loop. NOT a
830
845
  // container/prose agent. Pass-through when the reviewer isn't wired. The clarity gate
@@ -2055,6 +2070,65 @@ export class ExecutionService {
2055
2070
  * startup import side effect (see {@link gateFor} for the same caching caveat). See
2056
2071
  * {@link StepCompletionResolver}.
2057
2072
  */
2073
+ /**
2074
+ * Dispatch a step (whose preamble already ran in {@link stepInstance}) to the first
2075
+ * registered {@link StepHandler} whose `canHandle` claims it, ordered by `order`. The
2076
+ * fallthrough handler claims everything, so this always resolves to a handler.
2077
+ */
2078
+ dispatchStepHandler(ctx) {
2079
+ if (!this.stepHandlerCache)
2080
+ this.stepHandlerCache = this.buildStepHandlerRegistry();
2081
+ const handler = this.stepHandlerCache.find((h) => h.canHandle(ctx));
2082
+ // The fallthrough handler's `canHandle` is unconditional, so this is unreachable; it
2083
+ // exists only to satisfy the type and to fail loudly if that invariant is ever broken.
2084
+ if (!handler)
2085
+ throw new Error(`No step handler for agentKind "${ctx.step.agentKind}"`);
2086
+ return handler.handle(ctx);
2087
+ }
2088
+ /**
2089
+ * Build the order-sorted per-step-kind handler list, mirroring
2090
+ * {@link buildStepResolverRegistry} (built-ins constructed inline, closing over `this`).
2091
+ * Engine-internal: there is no public `registerStepHandler` seam. Phase 0 registers only
2092
+ * the generic fallthrough; later phases prepend more-specific handlers with lower `order`.
2093
+ */
2094
+ buildStepHandlerRegistry() {
2095
+ const handlers = [
2096
+ // A `deployer` step provisions an ephemeral environment deterministically via the
2097
+ // provider — no LLM, no token usage — when the integration is wired. Unwired, its
2098
+ // `canHandle` is false so the step falls through to the generic agent path.
2099
+ {
2100
+ kind: DEPLOYER_AGENT_KIND,
2101
+ order: 100,
2102
+ canHandle: ({ step }) => !!this.environmentProvisioning && isDeployStep(step.agentKind),
2103
+ handle: async ({ workspaceId, instance, step, block, isFinalStep, options }) => {
2104
+ const result = await this.runDeployer(workspaceId, instance, block, options);
2105
+ return this.recordStepResult(workspaceId, instance, step, isFinalStep, result);
2106
+ },
2107
+ },
2108
+ // A `tracker` step files a GitHub issue / Jira ticket from the preceding `analysis`
2109
+ // output (the tech-debt pipeline) — no LLM of its own. It is a pass-through when no
2110
+ // tracker provider is wired or none is configured for the workspace (handled inside
2111
+ // {@link runTracker}, which still records a result), so it always claims the step.
2112
+ {
2113
+ kind: TRACKER_AGENT_KIND,
2114
+ order: 110,
2115
+ canHandle: ({ step }) => step.agentKind === TRACKER_AGENT_KIND,
2116
+ handle: async ({ workspaceId, instance, step, block, isFinalStep }) => {
2117
+ const result = await this.runTracker(workspaceId, instance, block);
2118
+ return this.recordStepResult(workspaceId, instance, step, isFinalStep, result);
2119
+ },
2120
+ },
2121
+ // The generic container/inline-agent fallthrough — claims every step no more-specific
2122
+ // handler did (today's `runStepBody` tail). Highest order so it always runs last.
2123
+ {
2124
+ kind: '*',
2125
+ order: FALLTHROUGH_STEP_HANDLER_ORDER,
2126
+ canHandle: () => true,
2127
+ handle: (ctx) => this.runStepBody(ctx),
2128
+ },
2129
+ ];
2130
+ return handlers.sort((a, b) => a.order - b.order);
2131
+ }
2058
2132
  stepResolverFor(agentKind) {
2059
2133
  if (!this.stepResolverCache)
2060
2134
  this.stepResolverCache = this.buildStepResolverRegistry();