@cat-factory/orchestration 0.70.1 → 0.71.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 (80) hide show
  1. package/dist/container.d.ts +23 -2
  2. package/dist/container.d.ts.map +1 -1
  3. package/dist/container.js +43 -10
  4. package/dist/container.js.map +1 -1
  5. package/dist/modules/clarity/ClarityReviewService.d.ts +21 -1
  6. package/dist/modules/clarity/ClarityReviewService.d.ts.map +1 -1
  7. package/dist/modules/clarity/ClarityReviewService.js +27 -1
  8. package/dist/modules/clarity/ClarityReviewService.js.map +1 -1
  9. package/dist/modules/clarity/clarity.logic.d.ts +8 -0
  10. package/dist/modules/clarity/clarity.logic.d.ts.map +1 -1
  11. package/dist/modules/clarity/clarity.logic.js +23 -0
  12. package/dist/modules/clarity/clarity.logic.js.map +1 -1
  13. package/dist/modules/execution/AgentContextBuilder.d.ts +3 -0
  14. package/dist/modules/execution/AgentContextBuilder.d.ts.map +1 -1
  15. package/dist/modules/execution/AgentContextBuilder.js +2 -1
  16. package/dist/modules/execution/AgentContextBuilder.js.map +1 -1
  17. package/dist/modules/execution/ExecutionService.d.ts +60 -10
  18. package/dist/modules/execution/ExecutionService.d.ts.map +1 -1
  19. package/dist/modules/execution/ExecutionService.js +192 -27
  20. package/dist/modules/execution/ExecutionService.js.map +1 -1
  21. package/dist/modules/execution/MergeResolver.d.ts +21 -2
  22. package/dist/modules/execution/MergeResolver.d.ts.map +1 -1
  23. package/dist/modules/execution/MergeResolver.js +9 -3
  24. package/dist/modules/execution/MergeResolver.js.map +1 -1
  25. package/dist/modules/execution/RunDispatcher.d.ts +33 -1
  26. package/dist/modules/execution/RunDispatcher.d.ts.map +1 -1
  27. package/dist/modules/execution/RunDispatcher.js +144 -14
  28. package/dist/modules/execution/RunDispatcher.js.map +1 -1
  29. package/dist/modules/execution/RunStateMachine.d.ts +5 -2
  30. package/dist/modules/execution/RunStateMachine.d.ts.map +1 -1
  31. package/dist/modules/execution/RunStateMachine.js +4 -1
  32. package/dist/modules/execution/RunStateMachine.js.map +1 -1
  33. package/dist/modules/execution/bugInvestigation.logic.d.ts +3 -0
  34. package/dist/modules/execution/bugInvestigation.logic.d.ts.map +1 -0
  35. package/dist/modules/execution/bugInvestigation.logic.js +44 -0
  36. package/dist/modules/execution/bugInvestigation.logic.js.map +1 -0
  37. package/dist/modules/execution/mergeOrder.logic.d.ts +23 -0
  38. package/dist/modules/execution/mergeOrder.logic.d.ts.map +1 -0
  39. package/dist/modules/execution/mergeOrder.logic.js +30 -0
  40. package/dist/modules/execution/mergeOrder.logic.js.map +1 -0
  41. package/dist/modules/initiative/InitiativeLoopService.d.ts +7 -1
  42. package/dist/modules/initiative/InitiativeLoopService.d.ts.map +1 -1
  43. package/dist/modules/initiative/InitiativeLoopService.js +16 -3
  44. package/dist/modules/initiative/InitiativeLoopService.js.map +1 -1
  45. package/dist/modules/initiative/InitiativeService.d.ts +15 -1
  46. package/dist/modules/initiative/InitiativeService.d.ts.map +1 -1
  47. package/dist/modules/initiative/InitiativeService.js +46 -1
  48. package/dist/modules/initiative/InitiativeService.js.map +1 -1
  49. package/dist/modules/initiative/initiative.logic.d.ts +59 -1
  50. package/dist/modules/initiative/initiative.logic.d.ts.map +1 -1
  51. package/dist/modules/initiative/initiative.logic.js +241 -19
  52. package/dist/modules/initiative/initiative.logic.js.map +1 -1
  53. package/dist/modules/pipelines/PipelineService.d.ts.map +1 -1
  54. package/dist/modules/pipelines/PipelineService.js +15 -1
  55. package/dist/modules/pipelines/PipelineService.js.map +1 -1
  56. package/dist/modules/pipelines/pipelineShape.d.ts +15 -0
  57. package/dist/modules/pipelines/pipelineShape.d.ts.map +1 -1
  58. package/dist/modules/pipelines/pipelineShape.js +14 -4
  59. package/dist/modules/pipelines/pipelineShape.js.map +1 -1
  60. package/dist/modules/recurring/RecurringPipelineService.d.ts +18 -0
  61. package/dist/modules/recurring/RecurringPipelineService.d.ts.map +1 -1
  62. package/dist/modules/recurring/RecurringPipelineService.js +50 -4
  63. package/dist/modules/recurring/RecurringPipelineService.js.map +1 -1
  64. package/dist/modules/review/IterativeReviewService.d.ts +12 -0
  65. package/dist/modules/review/IterativeReviewService.d.ts.map +1 -1
  66. package/dist/modules/review/IterativeReviewService.js +23 -5
  67. package/dist/modules/review/IterativeReviewService.js.map +1 -1
  68. package/dist/modules/sandbox/SandboxRunService.d.ts +3 -0
  69. package/dist/modules/sandbox/SandboxRunService.d.ts.map +1 -1
  70. package/dist/modules/sandbox/SandboxRunService.js +1 -1
  71. package/dist/modules/sandbox/SandboxRunService.js.map +1 -1
  72. package/dist/modules/sandbox/SandboxService.d.ts +3 -0
  73. package/dist/modules/sandbox/SandboxService.d.ts.map +1 -1
  74. package/dist/modules/sandbox/SandboxService.js +3 -3
  75. package/dist/modules/sandbox/SandboxService.js.map +1 -1
  76. package/dist/validation/validateRegistrations.d.ts +9 -3
  77. package/dist/validation/validateRegistrations.d.ts.map +1 -1
  78. package/dist/validation/validateRegistrations.js +7 -7
  79. package/dist/validation/validateRegistrations.js.map +1 -1
  80. package/package.json +11 -11
@@ -1,12 +1,14 @@
1
1
  import { ConflictError, DEFAULT_MERGE_PRESET, getErrorMessage, getProvider, INITIATIVE_ANALYST_AGENT_KIND, INITIATIVE_COMMITTER_AGENT_KIND, INITIATIVE_INTERVIEWER_AGENT_KIND, INITIATIVE_PLANNER_AGENT_KIND, isAsyncAgentExecutor, NotFoundError, parseLocalModelId, recordGateAttempt, registeredGateFactories, registeredStepResolverFactories, requireProvider, sameSubtasks, } from '@cat-factory/kernel';
2
2
  import { frontendOriginsForService, parseBlueprintService, parseSpecDoc, } from '@cat-factory/contracts';
3
- import { blueprintPostOp, commitInitiativeTracker, isCompanionKind, isContainerBackedCompanion, moduleSlug, registeredAgentStep, registeredPostOps, registeredPreOps, runRepoOps, specPostOp, TASK_ESTIMATOR_AGENT_KIND, } from '@cat-factory/agents';
3
+ import { blueprintPostOp, commitInitiativeTracker, isCompanionKind, isContainerBackedCompanion, moduleSlug, runRepoOps, specPostOp, TASK_ESTIMATOR_AGENT_KIND, } from '@cat-factory/agents';
4
4
  import { DEPLOYER_AGENT_KIND, isDeployStep } from '@cat-factory/integrations';
5
+ import { BUG_INTAKE_AGENT_KIND } from '../pipelines/pipelineShape.js';
5
6
  import { coerceTaskEstimate, summarizeEstimate } from '../estimation/estimate.logic.js';
6
7
  import { reviewableArtifactOutput } from './artifact-review.logic.js';
7
8
  import { deployEvictionEpoch, deployJobId, orderProvisionTargets } from './deployer.logic.js';
9
+ import { renderInvestigationDigest } from './bugInvestigation.logic.js';
8
10
  import { frameOf, validInvolvedServiceFrames } from './frame.logic.js';
9
- import { ANALYSIS_AGENT_KIND, ARCHITECTURE_BRAINSTORM_AGENT_KIND, BLUEPRINTS_AGENT_KIND, CLARITY_REVIEW_AGENT_KIND, CONFLICTS_AGENT_KIND, HUMAN_TEST_AGENT_KIND, isTesterKind, MERGER_AGENT_KIND, REQUIREMENTS_BRAINSTORM_AGENT_KIND, REQUIREMENTS_REVIEW_AGENT_KIND, SPEC_WRITER_AGENT_KIND, TESTER_AGENT_KIND, TRACKER_AGENT_KIND, UI_TESTER_AGENT_KIND, VISUAL_CONFIRM_AGENT_KIND, } from './ci.logic.js';
11
+ import { ANALYSIS_AGENT_KIND, ARCHITECTURE_BRAINSTORM_AGENT_KIND, BLUEPRINTS_AGENT_KIND, BUG_INVESTIGATOR_AGENT_KIND, CLARITY_REVIEW_AGENT_KIND, CONFLICTS_AGENT_KIND, HUMAN_TEST_AGENT_KIND, isTesterKind, MERGER_AGENT_KIND, REQUIREMENTS_BRAINSTORM_AGENT_KIND, REQUIREMENTS_REVIEW_AGENT_KIND, SPEC_WRITER_AGENT_KIND, TESTER_AGENT_KIND, TRACKER_AGENT_KIND, UI_TESTER_AGENT_KIND, VISUAL_CONFIRM_AGENT_KIND, } from './ci.logic.js';
10
12
  import { followUpsToSendBack, hasPendingFollowUps, renderFollowUpRework, shouldLoopCoder, } from './followUp.logic.js';
11
13
  import { agentFailureKindFromCause, classifyAgentFailure, isContainerEvictionError, isTransientEviction, MAX_EVICTION_RECOVERIES, MAX_TRANSIENT_EVICTION_RECOVERIES, } from './job.logic.js';
12
14
  import { AgentContextBuilder } from './AgentContextBuilder.js';
@@ -97,6 +99,7 @@ export class RunDispatcher {
97
99
  blockRepository;
98
100
  executionRepository;
99
101
  agentExecutor;
102
+ agentKindRegistry;
100
103
  workRunner;
101
104
  events;
102
105
  idGenerator;
@@ -120,6 +123,7 @@ export class RunDispatcher {
120
123
  environmentProvisioning;
121
124
  ticketTrackerProvider;
122
125
  issueWriteback;
126
+ bugIntakeService;
123
127
  notificationService;
124
128
  blueprintReconciler;
125
129
  initiativeService;
@@ -139,6 +143,7 @@ export class RunDispatcher {
139
143
  this.blockRepository = deps.blockRepository;
140
144
  this.executionRepository = deps.executionRepository;
141
145
  this.agentExecutor = deps.agentExecutor;
146
+ this.agentKindRegistry = deps.agentKindRegistry;
142
147
  this.workRunner = deps.workRunner;
143
148
  this.events = deps.events;
144
149
  this.idGenerator = deps.idGenerator;
@@ -162,6 +167,7 @@ export class RunDispatcher {
162
167
  this.environmentProvisioning = deps.environmentProvisioning;
163
168
  this.ticketTrackerProvider = deps.ticketTrackerProvider;
164
169
  this.issueWriteback = deps.issueWriteback;
170
+ this.bugIntakeService = deps.bugIntakeService;
165
171
  this.notificationService = deps.notificationService;
166
172
  this.blueprintReconciler = deps.blueprintReconciler;
167
173
  this.initiativeService = deps.initiativeService;
@@ -902,23 +908,28 @@ export class RunDispatcher {
902
908
  step.rework = undefined;
903
909
  // A repo-operating step (the container "implementer" agent) opened a PR for
904
910
  // its work. Record it on the block so the board can surface and link to it,
905
- // regardless of whether this is the final step.
906
- if (result.pullRequest) {
911
+ // regardless of whether this is the final step. A multi-repo run
912
+ // (service-connections phase 3) additionally reports the PRs it opened in the
913
+ // connected involved-service repos; record them beside the own-service PR (they may
914
+ // even arrive when the own service was a no-op and only a peer changed).
915
+ if (result.pullRequest || result.peerPullRequests?.length) {
907
916
  // Read the block before the update so we can tell whether this PR is newly
908
917
  // opened (vs. the same PR re-reported by a re-run/retry of the coder step).
909
918
  const priorBlock = this.issueWriteback
910
919
  ? await this.blockRepository.get(workspaceId, instance.blockId)
911
920
  : null;
912
921
  await this.blockRepository.update(workspaceId, instance.blockId, {
913
- pullRequest: result.pullRequest,
922
+ ...(result.pullRequest ? { pullRequest: result.pullRequest } : {}),
923
+ ...(result.peerPullRequests?.length ? { peerPullRequests: result.peerPullRequests } : {}),
914
924
  });
915
925
  // Best-effort writeback: comment on the task's linked tracker issue(s) that a
916
- // PR opened. Only when the PR is newly recorded — a retry that re-reports the
917
- // same PR must not re-comment (the tracker comment is not idempotent). Gated
918
- // inside the provider by the workspace setting + per-task override;
919
- // fire-and-forget so a tracker outage never fails the run.
926
+ // PR opened. Only for the OWN-service PR, and only when it is newly recorded — a
927
+ // retry that re-reports the same PR must not re-comment (the tracker comment is not
928
+ // idempotent). Gated inside the provider by the workspace setting + per-task
929
+ // override; fire-and-forget so a tracker outage never fails the run.
920
930
  if (this.issueWriteback &&
921
931
  priorBlock &&
932
+ result.pullRequest &&
922
933
  priorBlock.pullRequest?.url !== result.pullRequest.url) {
923
934
  await this.issueWriteback
924
935
  .onPullRequestOpened(workspaceId, priorBlock, result.pullRequest)
@@ -1548,6 +1559,89 @@ export class RunDispatcher {
1548
1559
  return { output: `Could not file a tracking ticket: ${getErrorMessage(error)}` };
1549
1560
  }
1550
1561
  }
1562
+ /**
1563
+ * Run a `bug-intake` step — the recurring bug-triage pipeline's inbound dual of `tracker`
1564
+ * (design §3). Pull ONE matching open issue from the schedule's configured tracker board,
1565
+ * claim it (import + replace-link onto the reused block, mark it in-progress + comment), and
1566
+ * seed the block's title/description from it so every downstream step works THAT bug. When
1567
+ * nothing matches — or no task source is wired — the run completes SUCCESSFULLY with every
1568
+ * remaining step skipped (there is nothing to investigate / reproduce / fix), no notification.
1569
+ * Best-effort throughout: the intake helper never throws (a tracker outage resolves to a
1570
+ * no-op), and the pickup writeback is fire-and-forget.
1571
+ */
1572
+ async runBugIntake(workspaceId, instance, step, block, isFinalStep) {
1573
+ const outcome = this.bugIntakeService
1574
+ ? await this.bugIntakeService.pickForBlock(workspaceId, block.id)
1575
+ : { picked: null, summary: 'Issue intake is not configured on this deployment.' };
1576
+ if (!outcome.picked) {
1577
+ return this.completeRunSkippingRemaining(workspaceId, instance, step, outcome.summary);
1578
+ }
1579
+ const pickup = outcome.picked;
1580
+ // Seed the reused recurring block from the picked issue so each fire works a different bug
1581
+ // through the same block (the same block-seeding `createTaskFromIssue` does, applied in place).
1582
+ // Clear the previous fire's peer PRs too — this fire works a DIFFERENT bug, so a prior bug's
1583
+ // connected-repo PRs must not linger on the block. (The own-service `pullRequest` is overwritten
1584
+ // by this run's coder step before any step reads it; it is a non-nullable `BlockPatch` field, so
1585
+ // it cannot be cleared here anyway.)
1586
+ await this.blockRepository.update(workspaceId, block.id, {
1587
+ title: pickup.seedTitle,
1588
+ description: pickup.seedDescription,
1589
+ peerPullRequests: [],
1590
+ });
1591
+ // Best-effort: claim the issue where it was filed (in-progress mark + "taken by cat-factory"
1592
+ // comment). Fire-and-forget — a tracker hiccup must never fail the run, mirroring the PR
1593
+ // open/merge writeback hooks; and unlike them this is NOT gated on the writeback settings.
1594
+ if (this.issueWriteback) {
1595
+ await this.issueWriteback
1596
+ .onIssuePickedUp(workspaceId, block.id, pickup.inProgressLabel ? { inProgressLabel: pickup.inProgressLabel } : {})
1597
+ .catch(() => { });
1598
+ }
1599
+ return this.recordStepResult(workspaceId, instance, step, isFinalStep, {
1600
+ output: pickup.summary,
1601
+ });
1602
+ }
1603
+ /**
1604
+ * Complete the run successfully after a `bug-intake` step found no issue to work: record the
1605
+ * intake step's own no-match output (it SUCCEEDED — it made the decision), then mark every
1606
+ * REMAINING step `skipped` and finalize the reused block `done`, with NO notification (the
1607
+ * outcome is visible in the schedule's run history).
1608
+ *
1609
+ * The block is finalized `done` DIRECTLY here rather than through `RunStateMachine.finalizeBlock`:
1610
+ * for a mergerless task block (every bug-triage pipeline) finalizeBlock's terminal branch treats
1611
+ * the run as "work complete but unmerged" — it flips the block `pr_ready` and raises a
1612
+ * `pipeline_complete` "confirm + merge the PR" notification. This fire did NO work and opened NO
1613
+ * PR, so that card would be spurious (and its payload would reference a STALE PR carried over from
1614
+ * a prior fire). Setting the terminal status inline keeps the no-op silent, as documented.
1615
+ */
1616
+ async completeRunSkippingRemaining(workspaceId, instance, step, summary) {
1617
+ step.output = summary;
1618
+ step.progress = 1;
1619
+ step.subtasks = undefined;
1620
+ this.stepGraph.finishStep(step);
1621
+ for (let i = instance.currentStep + 1; i < instance.steps.length; i++) {
1622
+ const remaining = instance.steps[i];
1623
+ if (!remaining)
1624
+ continue;
1625
+ remaining.skipped = true;
1626
+ remaining.output = '';
1627
+ remaining.progress = 1;
1628
+ remaining.subtasks = undefined;
1629
+ this.stepGraph.finishStep(remaining);
1630
+ }
1631
+ instance.currentStep = instance.steps.length - 1;
1632
+ instance.status = 'done';
1633
+ const block = await this.blockRepository.get(workspaceId, instance.blockId);
1634
+ if (block && block.status !== 'done') {
1635
+ await this.blockRepository.update(workspaceId, instance.blockId, {
1636
+ status: 'done',
1637
+ progress: 1,
1638
+ });
1639
+ }
1640
+ await this.executionRepository.upsert(workspaceId, instance);
1641
+ await this.runStateMachine.emitInstance(workspaceId, instance);
1642
+ await this.runStateMachine.stopRunContainer(workspaceId, instance);
1643
+ return { kind: 'done' };
1644
+ }
1551
1645
  /**
1552
1646
  * Persist an APPROVED initiative plan for an `initiative-committer` step: flip the
1553
1647
  * entity to `executing` and mirror the tracker into the repo's default branch
@@ -1686,13 +1780,13 @@ export class RunDispatcher {
1686
1780
  * unchanged without the feature. A throwing op propagates to fail the step.
1687
1781
  */
1688
1782
  async runRegisteredPreOps(workspaceId, block, step, context) {
1689
- const ops = registeredPreOps(step.agentKind);
1783
+ const ops = this.agentKindRegistry.preOps(step.agentKind);
1690
1784
  if (ops.length === 0)
1691
1785
  return;
1692
1786
  const runRepo = await this.resolveRunRepo(workspaceId, block.id);
1693
1787
  if (!runRepo)
1694
1788
  return;
1695
- const branch = await this.resolveRepoOpBranch(registeredAgentStep(step.agentKind), block, runRepo);
1789
+ const branch = await this.resolveRepoOpBranch(this.agentKindRegistry.agentStep(step.agentKind), block, runRepo);
1696
1790
  await runRepoOps(ops, { repo: runRepo.repo, context, branch });
1697
1791
  }
1698
1792
  /**
@@ -1717,7 +1811,7 @@ export class RunDispatcher {
1717
1811
  * {@link runRegisteredPreOps}; the agent's {@link AgentRunResult} is threaded through.
1718
1812
  */
1719
1813
  async runRegisteredPostOps(workspaceId, instance, step, isFinalStep, result) {
1720
- const registered = registeredPostOps(step.agentKind);
1814
+ const registered = this.agentKindRegistry.postOps(step.agentKind);
1721
1815
  const builtIn = this.builtInPostOps(step.agentKind);
1722
1816
  if (registered.length === 0 && builtIn.length === 0)
1723
1817
  return;
@@ -1730,7 +1824,7 @@ export class RunDispatcher {
1730
1824
  const context = await this.contextBuilder.buildContext(workspaceId, instance, step, isFinalStep, block);
1731
1825
  // Registered (custom) kinds resolve their branch from their declared clone target.
1732
1826
  if (registered.length > 0) {
1733
- const branch = await this.resolveRepoOpBranch(registeredAgentStep(step.agentKind), block, runRepo);
1827
+ const branch = await this.resolveRepoOpBranch(this.agentKindRegistry.agentStep(step.agentKind), block, runRepo);
1734
1828
  await runRepoOps(registered, { repo: runRepo.repo, context, branch, result });
1735
1829
  }
1736
1830
  // Built-in (migrated) kinds resolve their branch to MATCH their container dispatch
@@ -1837,6 +1931,17 @@ export class RunDispatcher {
1837
1931
  return this.recordStepResult(workspaceId, instance, step, isFinalStep, result);
1838
1932
  },
1839
1933
  },
1934
+ // A `bug-intake` step (the recurring bug-triage pipeline) pulls ONE matching open issue from
1935
+ // the schedule's tracker board, claims it, and seeds the reused block from it — no LLM of its
1936
+ // own, the inbound dual of `tracker`. On no match (or no task source wired) it completes the
1937
+ // run successfully, skipping every remaining step. Handled entirely inside
1938
+ // {@link runBugIntake}, so it always claims the step.
1939
+ {
1940
+ kind: BUG_INTAKE_AGENT_KIND,
1941
+ order: 111,
1942
+ canHandle: ({ step }) => step.agentKind === BUG_INTAKE_AGENT_KIND,
1943
+ handle: ({ workspaceId, instance, step, block, isFinalStep }) => this.runBugIntake(workspaceId, instance, step, block, isFinalStep),
1944
+ },
1840
1945
  // The `initiative-interviewer` step interviews the human on goals/constraints — an
1841
1946
  // inline LLM gate that PARKS the planning run on a decision-wait while they answer
1842
1947
  // through the planning window, then synthesizes the brief onto the entity and advances
@@ -2113,6 +2218,22 @@ export class RunDispatcher {
2113
2218
  // whether the single-actor estimator or the consensus ranked-scoring variant produced
2114
2219
  // the JSON. Running at the post-completion slot keeps the summary in `step.output`
2115
2220
  // before the approval gate reads it as the proposal.
2221
+ // A `bug-investigator` step returns its STRUCTURED triage as `result.custom` (kept on
2222
+ // `step.custom` for the generic-structured view + the clarity gate's structured read).
2223
+ // Render a prose digest into `step.output` at the post-completion slot so downstream
2224
+ // steps (estimator / repro-test / coder) read the investigation via `priorOutputs`
2225
+ // (which carries only `step.output`), and the clarity gate's investigation-prose context
2226
+ // sees it too. An unparseable result leaves the agent's raw reply on `step.output`.
2227
+ {
2228
+ kind: BUG_INVESTIGATOR_AGENT_KIND,
2229
+ phase: 'post-completion',
2230
+ applies: (result) => result.custom !== undefined,
2231
+ resolve: async ({ result }) => {
2232
+ const digest = renderInvestigationDigest(result.custom);
2233
+ if (digest)
2234
+ return { output: digest };
2235
+ },
2236
+ },
2116
2237
  {
2117
2238
  kind: TASK_ESTIMATOR_AGENT_KIND,
2118
2239
  phase: 'post-completion',
@@ -2224,6 +2345,12 @@ export class RunDispatcher {
2224
2345
  const gateState = step.gate;
2225
2346
  const probe = await this.runInitiatorScope(instance.initiatedBy, () => gate.probe(workspaceId, block.id, gateState));
2226
2347
  step.gate.headSha = probe.headSha;
2348
+ // Multi-repo (service-connections phase 4): the CI / conflicts gates aggregate across every
2349
+ // PR the task opened; persist the per-repo head shas (and, for the conflicts gate, which repo
2350
+ // conflicted) so the run-detail UI can group checks by service and the conflict-resolver can
2351
+ // target the conflicted repo.
2352
+ step.gate.headShas = probe.headShas ?? null;
2353
+ step.gate.conflictTarget = probe.conflictTarget ?? null;
2227
2354
  // Persist the precheck outcome so the run-detail UI can surface why the gate is
2228
2355
  // looping (the failing checks / conflict reason) — detail that was previously fed
2229
2356
  // only to the helper agent and then discarded.
@@ -2244,7 +2371,10 @@ export class RunDispatcher {
2244
2371
  return { kind: 'awaiting_gate', stepIndex: instance.currentStep };
2245
2372
  }
2246
2373
  // probe.status === 'fail'.
2247
- const canEscalate = isAsyncAgentExecutor(this.agentExecutor);
2374
+ // A gate can decline escalation for a failure its helper can't fix (e.g. the conflicts
2375
+ // gate on a PEER-repo conflict it has no resolver for) — go straight to give-up instead
2376
+ // of burning the attempt budget on a helper that can't touch the problem.
2377
+ const canEscalate = isAsyncAgentExecutor(this.agentExecutor) && probe.escalatable !== false;
2248
2378
  if (canEscalate && step.gate.attempts < step.gate.maxAttempts) {
2249
2379
  return this.dispatchGateHelper(workspaceId, instance, step, block, isFinalStep, gate, probe.failureSummary);
2250
2380
  }