@cat-factory/orchestration 0.192.0 → 0.194.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.
Files changed (117) hide show
  1. package/dist/container/dependencies.d.ts +5 -1
  2. package/dist/container/dependencies.d.ts.map +1 -1
  3. package/dist/container/engine-collaborators.d.ts.map +1 -1
  4. package/dist/container/engine-collaborators.js +4 -7
  5. package/dist/container/engine-collaborators.js.map +1 -1
  6. package/dist/container/execution-service.d.ts.map +1 -1
  7. package/dist/container/execution-service.js +3 -0
  8. package/dist/container/execution-service.js.map +1 -1
  9. package/dist/container/inline-model-deps.d.ts +20 -0
  10. package/dist/container/inline-model-deps.d.ts.map +1 -0
  11. package/dist/container/inline-model-deps.js +38 -0
  12. package/dist/container/inline-model-deps.js.map +1 -0
  13. package/dist/container/modules.d.ts.map +1 -1
  14. package/dist/container/modules.js +23 -39
  15. package/dist/container/modules.js.map +1 -1
  16. package/dist/container/review-modules.d.ts.map +1 -1
  17. package/dist/container/review-modules.js +10 -28
  18. package/dist/container/review-modules.js.map +1 -1
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +1 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/inlineBlockModel.d.ts +51 -0
  24. package/dist/inlineBlockModel.d.ts.map +1 -0
  25. package/dist/inlineBlockModel.js +31 -0
  26. package/dist/inlineBlockModel.js.map +1 -0
  27. package/dist/modules/board/BoardService.d.ts +2 -0
  28. package/dist/modules/board/BoardService.d.ts.map +1 -1
  29. package/dist/modules/board/BoardService.js +4 -0
  30. package/dist/modules/board/BoardService.js.map +1 -1
  31. package/dist/modules/board/publicBoardReads.d.ts +12 -0
  32. package/dist/modules/board/publicBoardReads.d.ts.map +1 -1
  33. package/dist/modules/board/publicBoardReads.js +19 -4
  34. package/dist/modules/board/publicBoardReads.js.map +1 -1
  35. package/dist/modules/bugHunt/BugHuntAssessorService.d.ts +8 -4
  36. package/dist/modules/bugHunt/BugHuntAssessorService.d.ts.map +1 -1
  37. package/dist/modules/bugHunt/BugHuntAssessorService.js +6 -10
  38. package/dist/modules/bugHunt/BugHuntAssessorService.js.map +1 -1
  39. package/dist/modules/docInterview/DocInterviewService.d.ts +8 -4
  40. package/dist/modules/docInterview/DocInterviewService.d.ts.map +1 -1
  41. package/dist/modules/docInterview/DocInterviewService.js +4 -13
  42. package/dist/modules/docInterview/DocInterviewService.js.map +1 -1
  43. package/dist/modules/execution/AgentContextBuilder.d.ts +14 -12
  44. package/dist/modules/execution/AgentContextBuilder.d.ts.map +1 -1
  45. package/dist/modules/execution/AgentContextBuilder.js +8 -28
  46. package/dist/modules/execution/AgentContextBuilder.js.map +1 -1
  47. package/dist/modules/execution/AgentDispatchController.d.ts +115 -0
  48. package/dist/modules/execution/AgentDispatchController.d.ts.map +1 -0
  49. package/dist/modules/execution/AgentDispatchController.js +307 -0
  50. package/dist/modules/execution/AgentDispatchController.js.map +1 -0
  51. package/dist/modules/execution/ExecutionService.d.ts +18 -105
  52. package/dist/modules/execution/ExecutionService.d.ts.map +1 -1
  53. package/dist/modules/execution/ExecutionService.js +70 -589
  54. package/dist/modules/execution/ExecutionService.js.map +1 -1
  55. package/dist/modules/execution/ExecutionServiceDependencies.d.ts +19 -3
  56. package/dist/modules/execution/ExecutionServiceDependencies.d.ts.map +1 -1
  57. package/dist/modules/execution/ForkChatService.d.ts +8 -4
  58. package/dist/modules/execution/ForkChatService.d.ts.map +1 -1
  59. package/dist/modules/execution/ForkChatService.js +4 -13
  60. package/dist/modules/execution/ForkChatService.js.map +1 -1
  61. package/dist/modules/execution/IterationCapController.d.ts +57 -0
  62. package/dist/modules/execution/IterationCapController.d.ts.map +1 -0
  63. package/dist/modules/execution/IterationCapController.js +141 -0
  64. package/dist/modules/execution/IterationCapController.js.map +1 -0
  65. package/dist/modules/execution/JudgeService.d.ts +8 -4
  66. package/dist/modules/execution/JudgeService.d.ts.map +1 -1
  67. package/dist/modules/execution/JudgeService.js +4 -13
  68. package/dist/modules/execution/JudgeService.js.map +1 -1
  69. package/dist/modules/execution/RunAdmission.d.ts +1 -1
  70. package/dist/modules/execution/RunAdmission.d.ts.map +1 -1
  71. package/dist/modules/execution/RunAdmission.js +5 -2
  72. package/dist/modules/execution/RunAdmission.js.map +1 -1
  73. package/dist/modules/execution/RunDispatcher.d.ts +31 -195
  74. package/dist/modules/execution/RunDispatcher.d.ts.map +1 -1
  75. package/dist/modules/execution/RunDispatcher.js +47 -313
  76. package/dist/modules/execution/RunDispatcher.js.map +1 -1
  77. package/dist/modules/execution/RunDispatcherDependencies.d.ts +142 -0
  78. package/dist/modules/execution/RunDispatcherDependencies.d.ts.map +1 -0
  79. package/dist/modules/execution/RunDispatcherDependencies.js +27 -0
  80. package/dist/modules/execution/RunDispatcherDependencies.js.map +1 -0
  81. package/dist/modules/execution/RunLifecycleController.d.ts +141 -0
  82. package/dist/modules/execution/RunLifecycleController.d.ts.map +1 -0
  83. package/dist/modules/execution/RunLifecycleController.js +489 -0
  84. package/dist/modules/execution/RunLifecycleController.js.map +1 -0
  85. package/dist/modules/execution/TesterQualityReviewService.d.ts +8 -4
  86. package/dist/modules/execution/TesterQualityReviewService.d.ts.map +1 -1
  87. package/dist/modules/execution/TesterQualityReviewService.js +4 -13
  88. package/dist/modules/execution/TesterQualityReviewService.js.map +1 -1
  89. package/dist/modules/execution/dispatchPromptSettings.d.ts +42 -2
  90. package/dist/modules/execution/dispatchPromptSettings.d.ts.map +1 -1
  91. package/dist/modules/execution/dispatchPromptSettings.js +37 -0
  92. package/dist/modules/execution/dispatchPromptSettings.js.map +1 -1
  93. package/dist/modules/execution/run-action-controllers.d.ts +18 -0
  94. package/dist/modules/execution/run-action-controllers.d.ts.map +1 -0
  95. package/dist/modules/execution/run-action-controllers.js +19 -0
  96. package/dist/modules/execution/run-action-controllers.js.map +1 -0
  97. package/dist/modules/execution/run-context-admission.d.ts +1 -1
  98. package/dist/modules/execution/run-context-admission.d.ts.map +1 -1
  99. package/dist/modules/execution/run-context-admission.js +2 -0
  100. package/dist/modules/execution/run-context-admission.js.map +1 -1
  101. package/dist/modules/initiative/InitiativeInterviewService.d.ts +8 -4
  102. package/dist/modules/initiative/InitiativeInterviewService.d.ts.map +1 -1
  103. package/dist/modules/initiative/InitiativeInterviewService.js +4 -13
  104. package/dist/modules/initiative/InitiativeInterviewService.js.map +1 -1
  105. package/dist/modules/kaizen/KaizenService.d.ts +14 -9
  106. package/dist/modules/kaizen/KaizenService.d.ts.map +1 -1
  107. package/dist/modules/kaizen/KaizenService.js +11 -16
  108. package/dist/modules/kaizen/KaizenService.js.map +1 -1
  109. package/dist/modules/modelPresets/ModelPresetService.d.ts +54 -2
  110. package/dist/modules/modelPresets/ModelPresetService.d.ts.map +1 -1
  111. package/dist/modules/modelPresets/ModelPresetService.js +103 -4
  112. package/dist/modules/modelPresets/ModelPresetService.js.map +1 -1
  113. package/dist/modules/review/IterativeReviewService.d.ts +8 -4
  114. package/dist/modules/review/IterativeReviewService.d.ts.map +1 -1
  115. package/dist/modules/review/IterativeReviewService.js +4 -13
  116. package/dist/modules/review/IterativeReviewService.js.map +1 -1
  117. package/package.json +11 -11
@@ -1,20 +1,16 @@
1
- import { allPullRequests, DEFAULT_COMPANION_MAX_ATTEMPTS, pipelineHasVisualStep, } from '@cat-factory/contracts';
2
- import { companionFor } from '@cat-factory/agents';
3
- import { assertPipelineLaunchable } from '../pipelines/pipelineShape.js';
1
+ import { allPullRequests } from '@cat-factory/contracts';
4
2
  import { producerWasSkipped, shouldRunGatedStep } from './stepGating.logic.js';
5
3
  import { resolveIndividualVendors, } from './individualVendors.logic.js';
6
- import { assertFound, ConflictError, NotFoundError, RunContendedError, ReviewContendedError, ValidationError, } from '@cat-factory/kernel';
7
- import { DEFAULT_RISK_POLICY, noopLogger, runBestEffort } from '@cat-factory/kernel';
8
- import { isTesterKind } from './ci.logic.js';
9
- import { DEFAULT_FOLLOW_UP_MAX_LOOPS, FOLLOW_UP_PRODUCER_KIND } from './followUp.logic.js';
4
+ import { assertFound, RunContendedError, ReviewContendedError, } from '@cat-factory/kernel';
5
+ import { noopLogger, runBestEffort } from '@cat-factory/kernel';
10
6
  import { AgentContextBuilder } from './AgentContextBuilder.js';
11
7
  import { CompanionController } from './CompanionController.js';
12
8
  import { StepGraph } from './StepGraph.js';
13
- import { claimLiveRunOrConflict, handOffLiveRun } from './runStart.js';
14
9
  import { RunStateMachine } from './RunStateMachine.js';
15
10
  import { RunDispatcher } from './RunDispatcher.js';
16
11
  import { RunAdmission } from './RunAdmission.js';
17
12
  import { StepDecisionController } from './StepDecisionController.js';
13
+ import { buildRunActionControllers } from './run-action-controllers.js';
18
14
  import { inferTechnicalLabel } from './technical.logic.js';
19
15
  import { MergeResolver } from './MergeResolver.js';
20
16
  import { PostMergeBoardController } from './PostMergeBoardController.js';
@@ -27,16 +23,13 @@ import { PrReviewController } from './PrReviewController.js';
27
23
  import { BrainstormActions, ClarityReviewActions, RequirementReviewActions, } from './gate-window-facades.js';
28
24
  import { TesterController } from './TesterController.js';
29
25
  import { RalphController } from './RalphController.js';
30
- import { isRalphKind, resolveRalphConfig, seedRalphState } from './ralph.logic.js';
31
26
  import { HumanTestController } from './HumanTestController.js';
32
27
  import { VisualConfirmationController } from './VisualConfirmationController.js';
33
28
  import { InitiativeInterviewController } from './InitiativeInterviewController.js';
34
29
  import { DocInterviewController } from './DocInterviewController.js';
35
30
  import { isReentrantDecisionResume } from './reentrancy.logic.js';
36
31
  import { RunMergePolicy } from './RunMergePolicy.js';
37
- import { descendantIds } from '../board/board.logic.js';
38
32
  import { requireWorkspace } from '@cat-factory/kernel';
39
- import { buildResumedInstance, planResumedSteps, planRestartFromStep } from './retry.logic.js';
40
33
  import { PrVerificationReportController } from './PrVerificationReportController.js';
41
34
  /**
42
35
  * The execution engine. It orchestrates a pipeline of agent-performed steps and
@@ -153,7 +146,8 @@ export class ExecutionService {
153
146
  * their drops without a null-check. `noopLogger` when a facade wired none.
154
147
  */
155
148
  log;
156
- subscriptionActivations;
149
+ // No `subscriptionActivations` field: its only reader is the run-lifecycle surface, which now
150
+ // takes the repository straight off the deps object (see {@link RunLifecycleController}).
157
151
  pokeInitiativeLoop;
158
152
  resolveWorkspaceModelDefault;
159
153
  /**
@@ -166,6 +160,13 @@ export class ExecutionService {
166
160
  runDispatcher;
167
161
  /** The human decision surface on a parked run (approve / reject / merge / …). */
168
162
  stepDecisions;
163
+ /**
164
+ * The surfaces a HUMAN drives on a whole run: its lifecycle (launch / re-launch / resume / end,
165
+ * which share the claim-then-hand-off order) and the three-way iteration-cap resolution both
166
+ * automatic-rework gates park for. Extracted so this service keeps the per-step machine; the
167
+ * public methods that reach them are thin pass-throughs.
168
+ */
169
+ runActions;
169
170
  /** Maintains the run's verification report on its PR (a hook on step settlement). */
170
171
  prVerificationReport;
171
172
  constructor(dependencies) {
@@ -263,7 +264,7 @@ export class ExecutionService {
263
264
  clock,
264
265
  clockNow: () => this.clock.now(),
265
266
  resolveRiskPolicy: (ws, block) => this.resolveRiskPolicy(ws, block),
266
- dispatchIterationCap: (ws, blockId, choice, handlers) => this.dispatchIterationCap(ws, blockId, choice, handlers),
267
+ dispatchIterationCap: (ws, blockId, choice, handlers) => this.runActions.iterationCap.dispatchIterationCap(ws, blockId, choice, handlers),
267
268
  testerQualityReviewer,
268
269
  environmentProvisioning,
269
270
  environmentTeardown,
@@ -320,7 +321,6 @@ export class ExecutionService {
320
321
  this.notifications = notificationService;
321
322
  this.issueWriteback = issueWriteback;
322
323
  this.log = logger ?? noopLogger;
323
- this.subscriptionActivations = subscriptionActivationRepository;
324
324
  this.pokeInitiativeLoop = pokeInitiativeLoop;
325
325
  this.resolveWorkspaceModelDefault = resolveWorkspaceModelDefault;
326
326
  this.stepDecisions = new StepDecisionController({
@@ -339,6 +339,28 @@ export class ExecutionService {
339
339
  failRun: (ws, id, message, kind, detail, reason) => this.failRun(ws, id, message, kind, detail, reason),
340
340
  finalizeMerge: (ws, blockId) => this.finalizeMerge(ws, blockId),
341
341
  });
342
+ // The two surfaces a HUMAN drives on a whole run (its lifecycle + the iteration-cap
343
+ // resolution), built as one pair by the sibling factory: the cap gate's `stop-reset` branch is
344
+ // a run cancel, so it is bound to the lifecycle controller there rather than back through this
345
+ // instance. The engine methods they reach into are passed BOUND, as with the gate windows.
346
+ this.runActions = buildRunActionControllers({
347
+ admission: this.admission,
348
+ blockRepository: this.blockRepository,
349
+ clock: this.clock,
350
+ contextBuilder: this.contextBuilder,
351
+ events: this.events,
352
+ executionRepository: this.executionRepository,
353
+ idGenerator: this.idGenerator,
354
+ pipelineRepository: this.pipelineRepository,
355
+ runStateMachine: this.runStateMachine,
356
+ stepGraph: this.stepGraph,
357
+ workRunner: this.workRunner,
358
+ subscriptionActivations: subscriptionActivationRepository,
359
+ requireWorkspace: (ws) => this.requireWorkspace(ws),
360
+ requireBlock: (ws, id) => this.requireBlock(ws, id),
361
+ failRun: (ws, id, message, kind, detail, reason) => this.failRun(ws, id, message, kind, detail, reason),
362
+ inferBlockTechnical: (ws, block, producer, companionStep) => this.inferBlockTechnical(ws, block, producer, companionStep),
363
+ });
342
364
  }
343
365
  /**
344
366
  * Re-compose and re-publish a run's verification report OUT OF BAND, after the run itself has
@@ -504,198 +526,6 @@ export class ExecutionService {
504
526
  const resolveDefault = this.resolveWorkspaceModelDefault;
505
527
  return resolveIndividualVendors(blockModelId, agentKinds, resolveDefault ? (kind) => resolveDefault(workspaceId, kind, modelPresetId) : undefined, hasPersonalSubscription);
506
528
  }
507
- /** Start a pipeline against a block, replacing any prior run on it. */
508
- async start(workspaceId, blockId, pipelineId, options = {}) {
509
- const { initiatedBy, activate, origin = 'manual', intakeOrigin, gatesOverride } = options;
510
- await this.requireWorkspace(workspaceId);
511
- const block = await this.requireBlock(workspaceId, blockId);
512
- const pipeline = assertFound(await this.pipelineRepository.get(workspaceId, pipelineId), 'Pipeline', pipelineId);
513
- // Launch-constraint gate (start-only, NOT part of the shared retry re-validation): reject a
514
- // manual start of a recurring-only pipeline (or a scheduled fire of a one-off-only one), and
515
- // a bug-intake pipeline that isn't recurring. Before any side effects.
516
- assertPipelineLaunchable(pipeline.agentKinds, pipeline.availability, origin, pipeline.enabled);
517
- // Per-run gate override must be parallel to the pipeline's steps (one boolean per step,
518
- // original-index-aligned like `pipeline.gates`). A mismatch means a preset's review mapping
519
- // is out of step with the pipeline it targets — reject up front, before any side effects.
520
- if (gatesOverride && gatesOverride.length !== pipeline.agentKinds.length) {
521
- throw new ValidationError(`Gate override has ${gatesOverride.length} entr${gatesOverride.length === 1 ? 'y' : 'ies'} but pipeline '${pipeline.id}' has ${pipeline.agentKinds.length} step(s).`);
522
- }
523
- // Shared config/resource preconditions (pipeline shape, frame type, tester infra, binary
524
- // storage, agent backend, provider/preset satisfiability, budget) — the SAME gate a retry
525
- // runs, so the two can't drift. See assertRunnable.
526
- await this.admission.assertRunnable(workspaceId, block, pipeline, initiatedBy);
527
- // A Ralph-loop step needs a programmatic completion command (its exit condition); refuse to
528
- // start a misconfigured run rather than dispatch a validation-less coding pass that never
529
- // gates. The command is a per-task agent-config value (the SPA also requires it at creation).
530
- if (pipeline.agentKinds.some(isRalphKind) &&
531
- !resolveRalphConfig(block.agentConfig).validationCommand) {
532
- throw new ValidationError('A Ralph loop task needs a validation command (its completion criterion) before it can ' +
533
- 'start. Set one in the task configuration.');
534
- }
535
- // START-ONLY gates below: a retry REPLACES the failed run rather than adding a new one, so
536
- // the concurrency limit doesn't apply to it, and a re-drive of an already-started task isn't
537
- // re-gated on its dependencies.
538
- // Enforce the workspace's per-service running-task limit (off by default) — a clear,
539
- // actionable error before any side effects, so the human knows why the start was refused.
540
- await this.admission.assertWithinTaskLimit(workspaceId, block);
541
- // Hard dependency gate: a task cannot start while any block it `dependsOn` is unfinished
542
- // (not yet `done`/merged). Enforced server-side so it holds for manual starts, recurring
543
- // fires, auto-start propagation and direct API calls alike — the frontend's runnable
544
- // check is only a hint. Before any side effects so nothing is torn down on a refusal.
545
- await this.admission.assertDependenciesMet(workspaceId, block);
546
- // Mint the activation next: if the credential can't be unlocked, fail before
547
- // tearing down the block's prior run or creating a new one.
548
- const executionId = this.idGenerator.next('exec');
549
- await activate?.(executionId);
550
- // Read the block's prior run once: a manual re-start of an already-running block REPLACES
551
- // it (the board offers "start" on a live block), so we pass its id to `insertLive` as the
552
- // `replaceId` it supersedes atomically. A genuinely-CONCURRENT second start reads the SAME
553
- // prior (or none), so only one insert wins and the other is rejected 409 — the loser's
554
- // `replaceId` deletes only what it read, never the winner's fresh row (see insertLive).
555
- const prior = await this.executionRepository.getByBlock(workspaceId, blockId);
556
- // Replacing the block's prior run: clear its per-run activation now (it never reaches
557
- // the terminal cleanup in emitInstance when it's still running), so a replaced run's
558
- // system-encrypted token copy doesn't linger to its TTL. Keyed by the OLD run id, so
559
- // the activation just minted for the new run is untouched.
560
- if (this.subscriptionActivations && prior && prior.id !== executionId) {
561
- // Best-effort + idempotent, mirroring the terminal cleanup in RunStateMachine.emit: a
562
- // failure here must never derail the start. In mothership mode this repo is remote and
563
- // `deleteByExecution` is not yet allow-listed (it throws `unknown_method`), so an
564
- // unguarded call would otherwise break re-running any block; the TTL sweep reclaims the
565
- // stale activation row as the backstop.
566
- try {
567
- await this.subscriptionActivations.deleteByExecution(prior.id);
568
- }
569
- catch {
570
- // Swallow — see above.
571
- }
572
- }
573
- // NB: do NOT `deleteByBlock` here — `claimLiveRunOrConflict` (below) atomically clears the
574
- // block's terminal rows AND the `prior` run it replaces, then inserts the new live run, so a
575
- // concurrent double-start is rejected by the live-run index instead of both wiping each
576
- // other's row (see insertLive).
577
- // Build the run only from the ENABLED steps. A step the pipeline marked
578
- // `enabled[i] === false` is kept in the saved pipeline (so it can be toggled back
579
- // on later) but skipped here entirely. Gates/thresholds are read by the kind's
580
- // ORIGINAL index `i`, so they stay aligned to the kind even when earlier steps are
581
- // skipped; the first SURVIVING step is the one that starts working.
582
- const steps = pipeline.agentKinds
583
- .map((kind, i) => ({ kind, i }))
584
- .filter(({ i }) => pipeline.enabled?.[i] !== false)
585
- .map(({ kind, i }, position) => {
586
- const companionDef = companionFor(kind);
587
- return {
588
- agentKind: kind,
589
- state: position === 0 ? 'working' : 'pending',
590
- progress: 0,
591
- decision: null,
592
- // A gated step pauses for human approval once its proposal is ready (see
593
- // recordStepResult). A per-run override (the initiative-preset seam) wins over the
594
- // pipeline's own gate for this step; else the pipeline definition at run start. Both
595
- // read by the step's ORIGINAL index `i`, so they stay aligned to the kind even when
596
- // earlier steps are disabled.
597
- requiresApproval: gatesOverride?.[i] ?? pipeline.gates?.[i] ?? false,
598
- approval: null,
599
- // A consensus-enabled step runs through the multi-model mechanism (the consensus
600
- // executor reads this off the context). Copied from the pipeline at run start.
601
- ...(pipeline.consensus?.[i] ? { consensus: pipeline.consensus[i] } : {}),
602
- // Estimate gating: when set+enabled the step is skipped at runtime unless the
603
- // block estimate (written by an earlier task-estimator step) meets the threshold.
604
- ...(pipeline.gating?.[i] ? { gating: pipeline.gating[i] } : {}),
605
- // The extensible per-step options bag (the new home for per-step parameters — see
606
- // stepOptionsSchema). Copied from the pipeline at run start, keyed by the step's
607
- // ORIGINAL index `i`, so it stays aligned to the kind even when earlier steps are
608
- // disabled. Today it carries the requirements-review `autoRecommend` toggle.
609
- ...(pipeline.stepOptions?.[i] ? { stepOptions: pipeline.stepOptions[i] } : {}),
610
- // A companion step carries its quality bar + rework budget, seeded from the
611
- // pipeline's per-step threshold (else the companion's default).
612
- ...(companionDef
613
- ? {
614
- companion: {
615
- threshold: pipeline.thresholds?.[i] ?? companionDef.defaultThreshold,
616
- maxAttempts: DEFAULT_COMPANION_MAX_ATTEMPTS,
617
- attempts: 0,
618
- verdicts: [],
619
- },
620
- }
621
- : {}),
622
- // The Follow-up companion is on by default for a `coder` step; the pipeline's
623
- // per-step `followUps[i] === false` toggle disables it. Seeded empty here; the
624
- // harness streams items in as the Coder surfaces them (see pollAgentJob).
625
- ...(kind === FOLLOW_UP_PRODUCER_KIND && pipeline.followUps?.[i] !== false
626
- ? {
627
- followUps: {
628
- enabled: true,
629
- items: [],
630
- loops: 0,
631
- maxLoops: DEFAULT_FOLLOW_UP_MAX_LOOPS,
632
- },
633
- }
634
- : {}),
635
- // The test quality-control companion is on by default for a Tester step; the
636
- // pipeline's per-step `testerQuality[i].enabled === false` disables it. `maxAttempts`
637
- // is seeded with the default ceiling here and refreshed from the task's resolved
638
- // merge preset on the first report (TesterController). Optional estimate gating is
639
- // carried through so it can be evaluated against the block estimate at gate time.
640
- ...(isTesterKind(kind) && pipeline.testerQuality?.[i]?.enabled !== false
641
- ? {
642
- testerQuality: {
643
- enabled: true,
644
- attempts: 0,
645
- maxAttempts: DEFAULT_RISK_POLICY.maxTesterQualityIterations,
646
- verdicts: [],
647
- ...(pipeline.testerQuality?.[i]?.gating
648
- ? { gating: pipeline.testerQuality[i].gating }
649
- : {}),
650
- },
651
- }
652
- : {}),
653
- // A `ralph` step carries its persistent-loop state — the iteration count, the budget,
654
- // and the programmatic completion command — seeded from the block's per-task agent
655
- // config. Riding the persisted step is what lets a mid-loop run survive a restart
656
- // (both durable drivers + sweepers re-drive from it). See ralph.logic.ts.
657
- ...(isRalphKind(kind)
658
- ? { ralph: seedRalphState(resolveRalphConfig(block.agentConfig)) }
659
- : {}),
660
- };
661
- });
662
- if (steps.length === 0) {
663
- throw new ValidationError('Pipeline has no enabled steps to run.');
664
- }
665
- // For a visual (UI-test) pipeline on a frontend frame, resolve its backend bindings ONCE at
666
- // start and stamp both the resolved bindings and the non-fatal advisories (duplicate env vars,
667
- // or a partial-live set of bound services) on the run. The bindings are a frozen snapshot so
668
- // the SPA's run/step detail projects what the run ACTUALLY drove against (truthful after the
669
- // envs are torn down). Only paid for a visual pipeline — the same condition the tester infra
670
- // gate keys off — so a plain backend run does no extra env read. Absent → no notes/bindings.
671
- const frontendRun = pipelineHasVisualStep({ agentKinds: pipeline.agentKinds })
672
- ? await this.contextBuilder.resolveFrontendRunInfo(workspaceId, block)
673
- : undefined;
674
- const instance = {
675
- id: executionId,
676
- blockId,
677
- pipelineId: pipeline.id,
678
- pipelineName: pipeline.name,
679
- steps,
680
- currentStep: 0,
681
- status: 'running',
682
- initiatedBy: initiatedBy ?? null,
683
- // Only a headless start carries an explicit intake origin; `ui` is the read-time
684
- // default, so an ordinary board/schedule start stores nothing extra.
685
- ...(intakeOrigin != null ? { intakeOrigin } : {}),
686
- createdAt: this.clock.now(),
687
- ...(frontendRun?.notes.length ? { notes: frontendRun.notes } : {}),
688
- ...(frontendRun?.bindings.length ? { frontendBindings: frontendRun.bindings } : {}),
689
- };
690
- await claimLiveRunOrConflict(this.runStartDeps, workspaceId, instance, prior?.id);
691
- await this.blockRepository.update(workspaceId, blockId, {
692
- status: 'in_progress',
693
- progress: 0,
694
- executionId: instance.id,
695
- });
696
- await handOffLiveRun(this.runStartDeps, workspaceId, instance, block);
697
- return instance;
698
- }
699
529
  /**
700
530
  * Advance a single run by exactly one step and report what happened. This is
701
531
  * the durable driver's entry point: it reloads the run from storage (so it is
@@ -944,126 +774,9 @@ export class ExecutionService {
944
774
  ? this.architectureBrainstormKind
945
775
  : this.requirementsBrainstormKind;
946
776
  }
947
- /**
948
- * Route an iteration-cap resolution to its gate-specific handlers. `stop-reset` is
949
- * uniform across gates: cancel the run and return the block to phase zero (editable),
950
- * keeping whatever reference artifact each gate persists (the requirements doc on its
951
- * own table; a companion's producer output on its branch). Shared by the requirements
952
- * gate (`requirementsReview.resolveExceeded`, via {@link ReviewGateController}) and the
953
- * companion gate ({@link resolveCompanionExceeded}) so the three-way choice lives in one place.
954
- */
955
- async dispatchIterationCap(workspaceId, blockId, choice, handlers) {
956
- if (choice === 'extra-round') {
957
- await handlers.extraRound();
958
- }
959
- else if (choice === 'proceed') {
960
- await handlers.proceed();
961
- }
962
- else {
963
- // stop-reset: tear down the run + reset the block to phase zero (editable).
964
- await this.cancel(workspaceId, blockId);
965
- }
966
- }
967
- /**
968
- * Resolve a companion step parked at its automatic-rework cap (`companion.exceeded`):
969
- * grant one more round, proceed accepting the producer's current output, or stop the
970
- * task and reset it to phase zero. The companion mirror of the requirements
971
- * iteration-cap resolution (`requirementsReview.resolveExceeded`), sharing the iteration-cap dispatch + the
972
- * gate-resume plumbing. Idempotent — an already-resolved gate returns the instance
973
- * unchanged. Scoped by execution + approval id (the execution controller surface),
974
- * since a companion gate is not block-addressed like the requirements window.
975
- */
976
- async resolveCompanionExceeded(workspaceId, executionId, approvalId, choice) {
977
- await this.requireWorkspace(workspaceId);
978
- // Optimistic-concurrency human-action write (race-audit 2.2 controller-half): both non-cancel
979
- // branches persist under `mutateInstance` (load fresh → re-find the gate → mutate → CAS), so a
980
- // concurrent driver poll — or a `stopRun`/`cancel` racing this resolve — can't be clobbered by a
981
- // blind full-row upsert, and a cancelled run is never resurrected. The pure in-memory mutation
982
- // runs inside the CAS; the non-idempotent side effects (block writes, `technical` inference,
983
- // driver signal, emit) run once after, on the winning snapshot — the same pure/side-effect split
984
- // `approveStep` and the review gate-resume use. The validation snapshot below gives a fast
985
- // 404/409 (and the idempotent already-resolved early return).
986
- const snapshot = assertFound(await this.executionRepository.get(workspaceId, executionId), 'Execution', executionId);
987
- const snapStep = snapshot.steps.find((s) => s.approval?.id === approvalId);
988
- if (!snapStep || !snapStep.approval)
989
- throw new NotFoundError('Approval', approvalId);
990
- if (!snapStep.companion?.exceeded) {
991
- throw new ConflictError(`Approval '${approvalId}' is not a companion iteration-cap gate`);
992
- }
993
- if (snapStep.approval.status === 'approved')
994
- return snapshot;
995
- // The state the caller sees: the winning post-mutation snapshot for extra-round/proceed, or
996
- // the pre-cancel snapshot for stop-reset (the run row is deleted, so there's nothing to re-read).
997
- let result = snapshot;
998
- await this.dispatchIterationCap(workspaceId, snapshot.blockId, choice, {
999
- // Grant one more automatic rework: raise the budget by one, clear the cap flag, then loop
1000
- // the producer back through the companion to re-grade (`loopCompanionProducer` re-arms the
1001
- // run `running`). The last verdict's feedback drives the rework.
1002
- extraRound: async () => {
1003
- let signalId;
1004
- const persisted = await this.runStateMachine.mutateInstance(workspaceId, executionId, (inst) => {
1005
- const i = inst.steps.findIndex((s) => s.approval?.id === approvalId);
1006
- const s = inst.steps[i];
1007
- if (!s?.companion || !s.approval)
1008
- throw new NotFoundError('Approval', approvalId);
1009
- // Another writer already resolved this gate: no-op (idempotent) and skip the signal.
1010
- if (s.approval.status === 'approved') {
1011
- signalId = undefined;
1012
- return;
1013
- }
1014
- s.companion.maxAttempts += 1;
1015
- s.companion.exceeded = undefined;
1016
- const producer = inst.steps[this.stepGraph.companionProducerIndex(inst, i)];
1017
- // Capture the approval id BEFORE `loopCompanionProducer`: it resets the companion
1018
- // step for re-run (`resetStepForRerun`), which NULLS `s.approval`, so reading
1019
- // `s.approval.id` after would throw. The signal targets the gate's original approval.
1020
- signalId = s.approval.id;
1021
- this.stepGraph.loopCompanionProducer(inst, i, {
1022
- previousProposal: producer?.output ?? '',
1023
- feedback: s.companion.verdicts.at(-1)?.feedback ?? '',
1024
- });
1025
- });
1026
- result = persisted;
1027
- if (!signalId)
1028
- return;
1029
- await this.runStateMachine.updateBlockProgress(workspaceId, persisted, 'in_progress');
1030
- await this.workRunner.signalDecision(workspaceId, persisted.id, signalId, 'extra-round');
1031
- await this.runStateMachine.emitInstance(workspaceId, persisted);
1032
- },
1033
- // Proceed: accept the producer's current output and advance past the gate.
1034
- proceed: async () => {
1035
- let stepIndex = -1;
1036
- const persisted = await this.runStateMachine.mutateInstance(workspaceId, executionId, (inst) => {
1037
- stepIndex = inst.steps.findIndex((s) => s.approval?.id === approvalId);
1038
- const s = inst.steps[stepIndex];
1039
- if (!s?.companion || !s.approval)
1040
- throw new NotFoundError('Approval', approvalId);
1041
- if (s.approval.status === 'approved') {
1042
- stepIndex = -1;
1043
- return;
1044
- }
1045
- s.companion.exceeded = undefined;
1046
- s.approval.status = 'approved';
1047
- this.runStateMachine.advanceRunPastGate(inst, stepIndex);
1048
- });
1049
- result = persisted;
1050
- if (stepIndex === -1)
1051
- return;
1052
- // The spec-companion never reached its automatic PASS branch, but both signals are
1053
- // persisted (the producer's `noBusinessSpecs` + this step's `technicalCorroborated`),
1054
- // so infer the block's `technical` label here too — best-effort, human-authority
1055
- // preserved — before settling the advance.
1056
- const step = persisted.steps[stepIndex];
1057
- if (step.agentKind === 'spec-companion') {
1058
- const producer = persisted.steps[this.stepGraph.companionProducerIndex(persisted, stepIndex)];
1059
- const block = await this.blockRepository.get(workspaceId, persisted.blockId);
1060
- if (producer && block)
1061
- await this.inferBlockTechnical(workspaceId, block, producer, step);
1062
- }
1063
- await this.runStateMachine.settleAdvancedGate(workspaceId, persisted, stepIndex);
1064
- },
1065
- });
1066
- return result;
777
+ /** @see IterationCapController.resolveCompanionExceeded */
778
+ resolveCompanionExceeded(workspaceId, executionId, approvalId, choice) {
779
+ return this.runActions.iterationCap.resolveCompanionExceeded(workspaceId, executionId, approvalId, choice);
1067
780
  }
1068
781
  // The clarity / human-testing / visual-confirmation gate-window actions now live on the
1069
782
  // per-feature sub-facades (`clarityReview` / `humanTest` / `visualConfirm`); see the getters
@@ -1237,269 +950,37 @@ export class ExecutionService {
1237
950
  failRun(workspaceId, executionId, message, kind = 'agent', detail = null, reason = null) {
1238
951
  return this.runStateMachine.failRun(workspaceId, executionId, message, kind, detail, reason);
1239
952
  }
1240
- /**
1241
- * Retry a failed run: re-drive the same pipeline on the same block, **resuming
1242
- * from the step that actually failed** rather than restarting from step 0. The
1243
- * steps that already completed are preserved (so a `coder` failure in `pl_full`
1244
- * doesn't re-run the human-gated `requirements`/`architect` steps before it);
1245
- * the failed step and everything after it are reset to a clean, re-runnable
1246
- * state. Only a `failed` run can be retried.
1247
- *
1248
- * A fresh instance id is minted because the durable runner addresses one
1249
- * Workflows instance per execution id and the failed one is terminal — the new
1250
- * instance simply starts with `currentStep` pointed at the failed step, so the
1251
- * driver advances forward from there and never re-issues the completed steps'
1252
- * work. Mirrors {@link BootstrapService.retry}; both are reached via the unified
1253
- * `POST /agent-runs/:id/retry` endpoint.
1254
- */
1255
- async retry(workspaceId, executionId,
1256
- /** The retrying user (their personal subscription is used for individual-usage
1257
- * models). Falls back to the original initiator when omitted. */
1258
- initiatedBy,
1259
- /** Mint the per-run personal-credential activation (see {@link start}). */
1260
- activate) {
1261
- await this.requireWorkspace(workspaceId);
1262
- const previous = assertFound(await this.executionRepository.get(workspaceId, executionId), 'Execution', executionId);
1263
- if (previous.status !== 'failed') {
1264
- throw new ConflictError(`Only a failed run can be retried (run is '${previous.status}').`, 'run_not_retryable', { status: previous.status });
1265
- }
1266
- const block = await this.requireBlock(workspaceId, previous.blockId);
1267
- // Run the SAME config/resource preconditions start() does (shape, frame type, tester infra,
1268
- // binary storage, agent backend, provider/preset satisfiability, budget), so a retry can't
1269
- // silently proceed on a config a fresh start would refuse — the drift that let a
1270
- // subscription-only preset fail mid-run against the routing default. Validated over the
1271
- // STORED steps (what the retry actually re-drives), not the current pipeline definition, so
1272
- // an out-of-band pipeline edit can't skew the gate and a deleted pipeline needs no special
1273
- // case. Before any side effects.
1274
- await this.admission.assertRunnable(workspaceId, block, this.admission.runnableShapeOf(previous.steps), initiatedBy ?? previous.initiatedBy);
1275
- const { steps, currentStep } = planResumedSteps(previous);
1276
- // Mint the activation before replacing the failed run, so a bad password aborts
1277
- // the retry without losing the retryable terminal run.
1278
- const newId = this.idGenerator.next('exec');
1279
- const replaceId = previous.id;
1280
- await activate?.(newId);
1281
- // Replace the terminal failed run for this block with the resumed one (single run per
1282
- // block, matching the board's by-block projection). This mints a FRESH run id; the
1283
- // atomic `claimLiveRunOrConflict` below replaces `previous` (via `replaceId`) and clears
1284
- // any terminal rows in the SAME transaction, so a concurrent double-retry is serialised by
1285
- // the live-run index (the loser gets a 409) instead of both deleting-then-inserting.
1286
- const instance = buildResumedInstance({
1287
- previous,
1288
- id: newId,
1289
- plan: { steps, currentStep },
1290
- initiatedBy,
1291
- now: this.clock.now(),
1292
- });
1293
- await claimLiveRunOrConflict(this.runStartDeps, workspaceId, instance, replaceId);
1294
- const done = steps.filter((s) => s.state === 'done').length;
1295
- await this.blockRepository.update(workspaceId, previous.blockId, {
1296
- status: 'in_progress',
1297
- progress: steps.length > 0 ? done / steps.length : 0,
1298
- executionId: instance.id,
1299
- });
1300
- await handOffLiveRun(this.runStartDeps, workspaceId, instance, block);
1301
- return instance;
1302
- }
1303
- /**
1304
- * Restart a run from a human-chosen step: re-run from `fromStepIndex` onward,
1305
- * regardless of how far the run had progressed (a `done`, `failed`, `blocked`,
1306
- * `paused` or still-`running` run are all valid sources). Unlike {@link retry}
1307
- * (which resumes at the first FAILURE) this rewinds to an arbitrary step the user
1308
- * picked — so it can re-run steps that already completed.
1309
- *
1310
- * What is preserved vs reset:
1311
- * - Steps BEFORE `fromStepIndex` keep their `output`/approval/timing untouched, so
1312
- * the engine still hands the restarted step its predecessors' work as
1313
- * `priorOutputs` (and their resolved `decisions`) — a useful handoff.
1314
- * - The chosen step and every later one are reset to a clean, re-runnable state,
1315
- * dropping each step's iteration counters (companion attempts, gate/test attempts,
1316
- * eviction recoveries) so the restart starts those loops from zero.
1317
- * - A block's incorporated requirements are NOT touched: they live on the
1318
- * requirement-review record, so a restarted spec-writer/coder still receives the
1319
- * incorporated document (or the base description when none was generated). When the
1320
- * chosen step is the `requirements-review` gate ITSELF, re-running it mints a fresh
1321
- * iteration-1 review (the reviewer's `review()` replaces the prior one), which is
1322
- * exactly the "reset the iterations counter from this step" semantics.
1323
- *
1324
- * Like {@link retry} a fresh instance id is minted (the durable runner addresses one
1325
- * driver per execution id). Any still-live driver/container for the run being
1326
- * replaced is torn down first, so restarting a RUNNING run never orphans a container
1327
- * or a parked Workflows instance.
1328
- */
1329
- async restartFromStep(workspaceId, executionId, fromStepIndex,
1330
- /** The restarting user (their personal subscription is used for individual-usage
1331
- * models). Falls back to the original initiator when omitted. */
1332
- initiatedBy,
1333
- /** Mint the per-run personal-credential activation (see {@link start}). */
1334
- activate) {
1335
- await this.requireWorkspace(workspaceId);
1336
- const previous = assertFound(await this.executionRepository.get(workspaceId, executionId), 'Execution', executionId);
1337
- const block = await this.requireBlock(workspaceId, previous.blockId);
1338
- if (!Number.isInteger(fromStepIndex) ||
1339
- fromStepIndex < 0 ||
1340
- fromStepIndex >= previous.steps.length) {
1341
- throw new ValidationError(`Step ${fromStepIndex} is out of range for this run (it has ${previous.steps.length} step(s)).`);
1342
- }
1343
- // Run the SAME config/resource preconditions start()/retry() do, over the STORED steps this
1344
- // restart re-drives (frame type, tester infra, binary storage, agent backend, provider/preset
1345
- // satisfiability, budget). A restart re-dispatches provider-bearing steps just like a retry,
1346
- // so it must be gated identically — otherwise a run whose preset can't run every step (e.g. a
1347
- // subscription-only model an inline reviewer can't drive) strands mid-run instead of being
1348
- // refused up front. Before any teardown/side effects.
1349
- await this.admission.assertRunnable(workspaceId, block, this.admission.runnableShapeOf(previous.steps), initiatedBy ?? previous.initiatedBy);
1350
- // Tear down whatever was driving the run we're about to replace — its per-run
1351
- // container AND its durable driver — before minting the restart. A `done`/`failed`
1352
- // run is already terminal (a no-op teardown), but a still-`running` run would
1353
- // otherwise leak a container and a live Workflows/pg-boss driver.
1354
- await this.runStateMachine.stopRunContainer(workspaceId, previous);
1355
- await this.workRunner.cancelRun(workspaceId, executionId);
1356
- const { steps, currentStep } = planRestartFromStep(previous, fromStepIndex);
1357
- // Mint the activation before replacing the prior run, so a bad password aborts the
1358
- // restart without losing the source run.
1359
- const newId = this.idGenerator.next('exec');
1360
- const replaceId = previous.id;
1361
- await activate?.(newId);
1362
- // Like retry(), this mints a FRESH run id. `claimLiveRunOrConflict` atomically supersedes
1363
- // the torn-down source run (`replaceId`, which here may still be LIVE — running/paused/
1364
- // blocked) and clears terminal rows in one transaction, so a concurrent start that already
1365
- // created a NEW live run for the block loses (409) instead of being silently clobbered.
1366
- const instance = buildResumedInstance({
1367
- previous,
1368
- id: newId,
1369
- plan: { steps, currentStep },
1370
- initiatedBy,
1371
- now: this.clock.now(),
1372
- });
1373
- await claimLiveRunOrConflict(this.runStartDeps, workspaceId, instance, replaceId);
1374
- const done = steps.filter((s) => s.state === 'done').length;
1375
- await this.blockRepository.update(workspaceId, previous.blockId, {
1376
- status: 'in_progress',
1377
- progress: steps.length > 0 ? done / steps.length : 0,
1378
- executionId: instance.id,
1379
- });
1380
- await handOffLiveRun(this.runStartDeps, workspaceId, instance, block);
1381
- return instance;
1382
- }
1383
- /**
1384
- * The bound callbacks the two run-start funnels (`runStart.ts`) need, so they depend on no
1385
- * concrete repository or service. Those funnels own the ORDER between a run's atomic claim and
1386
- * its hand-off and are documented there; every start path calls both, writing the block state
1387
- * that path owns in between.
1388
- */
1389
- get runStartDeps() {
1390
- return {
1391
- insertLive: (ws, instance, options) => this.executionRepository.insertLive(ws, instance, options),
1392
- startRun: (ws, id) => this.workRunner.startRun(ws, id),
1393
- emitInstance: (ws, instance) => this.runStateMachine.emitInstance(ws, instance),
1394
- publishRunStarted: (ws, instance, block) => this.runStateMachine.publishRunStarted(ws, instance, block),
1395
- };
1396
- }
1397
- /**
1398
- * Resume every run paused by the spend safeguard in this workspace. Flips them
1399
- * back to `running` and re-drives the durable runner. If the budget is still
1400
- * exhausted the spend gate will simply pause them again on their next step.
1401
- */
1402
- async resumePaused(workspaceId) {
1403
- await this.requireWorkspace(workspaceId);
1404
- // Lean projection: only the paused runs' ids are needed to re-drive them — no `detail` decode.
1405
- const live = await this.executionRepository.listLive(workspaceId);
1406
- const paused = live.filter((e) => e.status === 'paused');
1407
- for (const p of paused) {
1408
- // Optimistic-concurrency write: only flip + re-drive a run that is STILL paused at
1409
- // write time, so a resume racing the driver (or a concurrent resume) can't clobber a
1410
- // run another writer already advanced. A vanished/contended run is skipped (the next
1411
- // sweep retries) rather than failing the whole batch.
1412
- let flipped = false;
1413
- const resumed = await this.runStateMachine
1414
- .mutateInstance(workspaceId, p.id, (inst) => {
1415
- flipped = inst.status === 'paused';
1416
- if (flipped)
1417
- inst.status = 'running';
1418
- })
1419
- .catch(() => null);
1420
- if (resumed && flipped) {
1421
- // `startRun` re-drives runners that re-create the run from scratch (pg-boss re-enqueues
1422
- // the same id). On Cloudflare the paused run's Workflows instance is still ALIVE parked
1423
- // on a `waitForEvent`, so `startRun`'s `create` no-ops there; `signalResume` delivers the
1424
- // event that wakes it immediately instead of waiting out the periodic budget re-check.
1425
- await this.workRunner.startRun(workspaceId, resumed.id);
1426
- await this.workRunner.signalResume?.(workspaceId, resumed.id);
1427
- await this.runStateMachine.emitInstance(workspaceId, resumed);
1428
- }
1429
- }
1430
- // Clear the workspace-scoped `budget_paused` card now the pause is being lifted (F3). If the
1431
- // budget is still exhausted a resumed run re-pauses and re-raises it on its next step.
1432
- await this.runStateMachine.clearBudgetPaused(workspaceId);
1433
- return this.executionRepository.listByWorkspace(workspaceId);
1434
- }
1435
- /** Cancel the run on a block, returning it to `planned`. */
1436
- async cancel(workspaceId, blockId) {
1437
- await this.requireWorkspace(workspaceId);
1438
- await this.requireBlock(workspaceId, blockId);
1439
- // Tear down the durable run (if any) AND its per-run container before removing
1440
- // the record, so a cancel never leaves a container running until its watchdog.
1441
- const existing = await this.executionRepository.getByBlock(workspaceId, blockId);
1442
- if (existing) {
1443
- await this.runStateMachine.stopRunContainer(workspaceId, existing);
1444
- await this.workRunner.cancelRun(workspaceId, existing.id);
1445
- }
1446
- await this.executionRepository.deleteByBlock(workspaceId, blockId);
1447
- await this.blockRepository.update(workspaceId, blockId, {
1448
- status: 'planned',
1449
- progress: 0,
1450
- executionId: null,
1451
- });
1452
- // The run record is gone and the block is back to planned; the client can't
1453
- // reconstruct that from a per-instance event, so signal a coarse refresh. Name the block
1454
- // so the refresh fans out to every board mounting its shared service.
1455
- await this.events.boardChanged(workspaceId, 'cancel', blockId);
1456
- return this.requireBlock(workspaceId, blockId);
1457
- }
1458
- /**
1459
- * Explicitly stop a *running* run by id (the unified `POST /agent-runs/:id/stop`
1460
- * surface): kill its per-run container, tear down the durable driver, then record
1461
- * a terminal `cancelled` failure so the board shows the run stopped (with retry)
1462
- * rather than spinning forever. Idempotent — a run already terminal is returned
1463
- * as-is. `opts.reason`/`opts.kind` let the orphan sweep reuse this with its own
1464
- * wording instead of the user-facing default.
1465
- */
1466
- async stopRun(workspaceId, executionId, opts = {}) {
1467
- await this.requireWorkspace(workspaceId);
1468
- const instance = assertFound(await this.executionRepository.get(workspaceId, executionId), 'Execution', executionId);
1469
- if (instance.status === 'failed' || instance.status === 'done')
1470
- return instance;
1471
- await this.runStateMachine.stopRunContainer(workspaceId, instance);
1472
- await this.workRunner.cancelRun(workspaceId, executionId);
1473
- await this.failRun(workspaceId, executionId, opts.reason ?? 'Stopped by the user.', opts.kind ?? 'cancelled');
1474
- return assertFound(await this.executionRepository.get(workspaceId, executionId), 'Execution', executionId);
1475
- }
1476
- /**
1477
- * Tear down every run under a block subtree — kill each container, terminate each
1478
- * durable driver, and delete the run record — so deleting a service/module never
1479
- * orphans a container or a Workflows instance. Best-effort and silent: the board
1480
- * delete that follows emits the coarse refresh, so no per-run event is needed.
1481
- *
1482
- * Returns the workspace block list it loaded so the immediately-following `removeBlock`
1483
- * can reuse it instead of re-listing the whole board (this teardown deletes only run
1484
- * records, never blocks, so the list is still current) — see {@link PreloadedBlocks}.
1485
- */
1486
- async teardownForBlockTree(workspaceId, rootId) {
1487
- const blocks = await this.blockRepository.listByWorkspace(workspaceId);
1488
- // Resolve every run in one query and index by block id, rather than a per-block
1489
- // getByBlock (N+1) over the whole subtree.
1490
- const runsByBlock = new Map((await this.executionRepository.listByWorkspace(workspaceId)).map((run) => [
1491
- run.blockId,
1492
- run,
1493
- ]));
1494
- for (const blockId of descendantIds(blocks, rootId)) {
1495
- const run = runsByBlock.get(blockId);
1496
- if (!run)
1497
- continue;
1498
- await this.runStateMachine.stopRunContainer(workspaceId, run);
1499
- await this.workRunner.cancelRun(workspaceId, run.id);
1500
- await this.executionRepository.deleteByBlock(workspaceId, blockId);
1501
- }
1502
- return { workspaceId, blocks };
953
+ // ---- run-lifecycle pass-throughs ----------------------------------------
954
+ // Launching, re-launching, resuming and ending a run all live on
955
+ // {@link RunLifecycleController} (they share the claim-then-hand-off order documented there);
956
+ // these thin delegations keep this service the single surface the HTTP layer talks to.
957
+ /** @see RunLifecycleController.start */
958
+ start(workspaceId, blockId, pipelineId, options = {}) {
959
+ return this.runActions.lifecycle.start(workspaceId, blockId, pipelineId, options);
960
+ }
961
+ /** @see RunLifecycleController.retry */
962
+ retry(workspaceId, executionId, initiatedBy, activate) {
963
+ return this.runActions.lifecycle.retry(workspaceId, executionId, initiatedBy, activate);
964
+ }
965
+ /** @see RunLifecycleController.restartFromStep */
966
+ restartFromStep(workspaceId, executionId, fromStepIndex, initiatedBy, activate) {
967
+ return this.runActions.lifecycle.restartFromStep(workspaceId, executionId, fromStepIndex, initiatedBy, activate);
968
+ }
969
+ /** @see RunLifecycleController.resumePaused */
970
+ resumePaused(workspaceId) {
971
+ return this.runActions.lifecycle.resumePaused(workspaceId);
972
+ }
973
+ /** @see RunLifecycleController.cancel */
974
+ cancel(workspaceId, blockId) {
975
+ return this.runActions.lifecycle.cancel(workspaceId, blockId);
976
+ }
977
+ /** @see RunLifecycleController.stopRun */
978
+ stopRun(workspaceId, executionId, opts = {}) {
979
+ return this.runActions.lifecycle.stopRun(workspaceId, executionId, opts);
980
+ }
981
+ /** @see RunLifecycleController.teardownForBlockTree */
982
+ teardownForBlockTree(workspaceId, rootId) {
983
+ return this.runActions.lifecycle.teardownForBlockTree(workspaceId, rootId);
1503
984
  }
1504
985
  }
1505
986
  //# sourceMappingURL=ExecutionService.js.map