@revisium/orchestrator 0.2.0-alpha.4 → 0.2.0-alpha.5

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 (147) hide show
  1. package/README.md +1 -1
  2. package/control-plane/bootstrap.config.json +5 -1
  3. package/control-plane/default-playbook/catalog/pipelines.json +51 -0
  4. package/control-plane/default-playbook/catalog/roles.json +1 -1
  5. package/control-plane/default-playbook/catalog/run-profiles.json +130 -0
  6. package/control-plane/default-playbook/prompts/integrator.md +1 -1
  7. package/dist/__generated__/client/internal/class.js +4 -4
  8. package/dist/__generated__/client/internal/class.js.map +1 -1
  9. package/dist/__generated__/client/internal/prismaNamespace.js +0 -1
  10. package/dist/__generated__/client/internal/prismaNamespace.js.map +1 -1
  11. package/dist/__generated__/client/internal/prismaNamespaceBrowser.js +0 -1
  12. package/dist/__generated__/client/internal/prismaNamespaceBrowser.js.map +1 -1
  13. package/dist/api/graphql-api/inbox/inputs/resolve-gate.input.js +9 -0
  14. package/dist/api/graphql-api/inbox/inputs/resolve-gate.input.js.map +1 -1
  15. package/dist/api/graphql-api/method/inputs/create-run-profile.input.js +54 -0
  16. package/dist/api/graphql-api/method/inputs/create-run-profile.input.js.map +1 -0
  17. package/dist/api/graphql-api/method/inputs/deprecate-run-profile.input.js +37 -0
  18. package/dist/api/graphql-api/method/inputs/deprecate-run-profile.input.js.map +1 -0
  19. package/dist/api/graphql-api/method/inputs/get-run-profile.input.js +32 -0
  20. package/dist/api/graphql-api/method/inputs/get-run-profile.input.js.map +1 -0
  21. package/dist/api/graphql-api/method/inputs/list-run-profiles.input.js +33 -0
  22. package/dist/api/graphql-api/method/inputs/list-run-profiles.input.js.map +1 -0
  23. package/dist/api/graphql-api/method/inputs/update-run-profile.input.js +59 -0
  24. package/dist/api/graphql-api/method/inputs/update-run-profile.input.js.map +1 -0
  25. package/dist/api/graphql-api/method/inputs/validate-run-profile.input.js +33 -0
  26. package/dist/api/graphql-api/method/inputs/validate-run-profile.input.js.map +1 -0
  27. package/dist/api/graphql-api/method/method.resolver.js +76 -1
  28. package/dist/api/graphql-api/method/method.resolver.js.map +1 -1
  29. package/dist/api/graphql-api/method/model/run-profile-connection.model.js +16 -0
  30. package/dist/api/graphql-api/method/model/run-profile-connection.model.js.map +1 -0
  31. package/dist/api/graphql-api/method/model/run-profile-status.model.js +6 -0
  32. package/dist/api/graphql-api/method/model/run-profile-status.model.js.map +1 -0
  33. package/dist/api/graphql-api/method/model/run-profile.model.js +79 -0
  34. package/dist/api/graphql-api/method/model/run-profile.model.js.map +1 -0
  35. package/dist/api/graphql-api/registerGraphqlEnums.js +4 -0
  36. package/dist/api/graphql-api/registerGraphqlEnums.js.map +1 -1
  37. package/dist/api/graphql-api/runs/inputs/create-run.input.js +6 -7
  38. package/dist/api/graphql-api/runs/inputs/create-run.input.js.map +1 -1
  39. package/dist/api/graphql-api/runs/inputs/simulate-route.input.js +6 -7
  40. package/dist/api/graphql-api/runs/inputs/simulate-route.input.js.map +1 -1
  41. package/dist/control-plane/default-playbook-policy.js +2 -11
  42. package/dist/control-plane/default-playbook-policy.js.map +1 -1
  43. package/dist/control-plane/engine-transport.js +2 -0
  44. package/dist/control-plane/engine-transport.js.map +1 -1
  45. package/dist/control-plane/json-fields.js +1 -1
  46. package/dist/control-plane/json-fields.js.map +1 -1
  47. package/dist/control-plane/pipeline-coverage-registry.js +26 -18
  48. package/dist/control-plane/pipeline-coverage-registry.js.map +1 -1
  49. package/dist/control-plane/run-profiles.js +101 -15
  50. package/dist/control-plane/run-profiles.js.map +1 -1
  51. package/dist/control-plane/transport.js.map +1 -1
  52. package/dist/control-plane/versioned-meaning.js +18 -3
  53. package/dist/control-plane/versioned-meaning.js.map +1 -1
  54. package/dist/e2e/kit/agents.js +3 -2
  55. package/dist/e2e/kit/agents.js.map +1 -1
  56. package/dist/e2e/kit/assertions.js +2 -2
  57. package/dist/e2e/kit/crash.js +8 -4
  58. package/dist/e2e/kit/crash.js.map +1 -1
  59. package/dist/e2e/kit/drive.js.map +1 -1
  60. package/dist/e2e/kit/fake-integrator.js +3 -36
  61. package/dist/e2e/kit/fake-integrator.js.map +1 -1
  62. package/dist/e2e/kit/gh-emulator.js +8 -2
  63. package/dist/e2e/kit/gh-emulator.js.map +1 -1
  64. package/dist/e2e/kit/index.js +1 -0
  65. package/dist/e2e/kit/index.js.map +1 -1
  66. package/dist/e2e/kit/run-profiles.js +50 -0
  67. package/dist/e2e/kit/run-profiles.js.map +1 -0
  68. package/dist/e2e/kit/scenario.js +48 -6
  69. package/dist/e2e/kit/scenario.js.map +1 -1
  70. package/dist/e2e/kit/scenarios.js +26 -27
  71. package/dist/e2e/kit/scenarios.js.map +1 -1
  72. package/dist/e2e/recovery-crash-child.js +2 -2
  73. package/dist/e2e/recovery-crash-child.js.map +1 -1
  74. package/dist/e2e/recovery-dd-crash-child.js +8 -6
  75. package/dist/e2e/recovery-dd-crash-child.js.map +1 -1
  76. package/dist/features/inbox/commands/impl/resolve-gate.command.js.map +1 -1
  77. package/dist/features/method/commands/handlers/method-command.handlers.js +64 -0
  78. package/dist/features/method/commands/handlers/method-command.handlers.js.map +1 -0
  79. package/dist/features/method/commands/impl/create-run-profile.command.js +7 -0
  80. package/dist/features/method/commands/impl/create-run-profile.command.js.map +1 -0
  81. package/dist/features/method/commands/impl/deprecate-run-profile.command.js +7 -0
  82. package/dist/features/method/commands/impl/deprecate-run-profile.command.js.map +1 -0
  83. package/dist/features/method/commands/impl/update-run-profile.command.js +7 -0
  84. package/dist/features/method/commands/impl/update-run-profile.command.js.map +1 -0
  85. package/dist/features/method/commands/index.js +6 -1
  86. package/dist/features/method/commands/index.js.map +1 -1
  87. package/dist/features/method/method-api.service.js +31 -3
  88. package/dist/features/method/method-api.service.js.map +1 -1
  89. package/dist/features/method/queries/handlers/method-query.handlers.js +58 -1
  90. package/dist/features/method/queries/handlers/method-query.handlers.js.map +1 -1
  91. package/dist/features/method/queries/impl/get-run-profile.query.js +7 -0
  92. package/dist/features/method/queries/impl/get-run-profile.query.js.map +1 -0
  93. package/dist/features/method/queries/impl/list-run-profiles.query.js +7 -0
  94. package/dist/features/method/queries/impl/list-run-profiles.query.js.map +1 -0
  95. package/dist/features/method/queries/impl/validate-run-profile.query.js +7 -0
  96. package/dist/features/method/queries/impl/validate-run-profile.query.js.map +1 -0
  97. package/dist/features/method/queries/index.js +4 -1
  98. package/dist/features/method/queries/index.js.map +1 -1
  99. package/dist/features/runs/commands/impl/create-run.command.js.map +1 -1
  100. package/dist/features/runs/queries/impl/simulate-route.query.js.map +1 -1
  101. package/dist/features/shared/connection.js +6 -3
  102. package/dist/features/shared/connection.js.map +1 -1
  103. package/dist/mcp/mcp-capabilities.js +5 -0
  104. package/dist/mcp/mcp-capabilities.js.map +1 -1
  105. package/dist/mcp/mcp-facade.service.js +34 -12
  106. package/dist/mcp/mcp-facade.service.js.map +1 -1
  107. package/dist/mcp/mcp-tool-result.js +22 -0
  108. package/dist/mcp/mcp-tool-result.js.map +1 -0
  109. package/dist/mcp/mcp-tools.js +90 -17
  110. package/dist/mcp/mcp-tools.js.map +1 -1
  111. package/dist/pipeline/await-human.js +27 -8
  112. package/dist/pipeline/await-human.js.map +1 -1
  113. package/dist/pipeline/data-driven-task.workflow.js +318 -69
  114. package/dist/pipeline/data-driven-task.workflow.js.map +1 -1
  115. package/dist/pipeline/pipeline.service.js +4 -10
  116. package/dist/pipeline/pipeline.service.js.map +1 -1
  117. package/dist/pipeline/route-contract.js +22 -101
  118. package/dist/pipeline/route-contract.js.map +1 -1
  119. package/dist/playbook/catalog-loader.js +1 -1
  120. package/dist/playbook/catalog-loader.js.map +1 -1
  121. package/dist/playbook/catalog-schema-validator.js +69 -39
  122. package/dist/playbook/catalog-schema-validator.js.map +1 -1
  123. package/dist/playbook/import-mapper.js +20 -9
  124. package/dist/playbook/import-mapper.js.map +1 -1
  125. package/dist/revisium/playbooks.service.js +208 -13
  126. package/dist/revisium/playbooks.service.js.map +1 -1
  127. package/dist/run/create-run.js +0 -3
  128. package/dist/run/create-run.js.map +1 -1
  129. package/dist/run/inspect-run.js +1 -1
  130. package/dist/run/inspect-run.js.map +1 -1
  131. package/dist/run/prisma-runtime-data-access.js +0 -3
  132. package/dist/run/prisma-runtime-data-access.js.map +1 -1
  133. package/dist/runners/gh-identity.js +29 -7
  134. package/dist/runners/gh-identity.js.map +1 -1
  135. package/dist/runners/integrator.js +73 -71
  136. package/dist/runners/integrator.js.map +1 -1
  137. package/dist/storage/ensure-storage.js +2 -1
  138. package/dist/storage/ensure-storage.js.map +1 -1
  139. package/dist/task-control-plane/task-control-plane-api.service.js +410 -159
  140. package/dist/task-control-plane/task-control-plane-api.service.js.map +1 -1
  141. package/dist/worker/claude-code-runner.js +1 -3
  142. package/dist/worker/claude-code-runner.js.map +1 -1
  143. package/package.json +1 -1
  144. package/prisma/migrations/20260707103000_prisma_runtime_tables/migration.sql +0 -1
  145. package/prisma/schema.prisma +16 -17
  146. package/dist/api/graphql-api/runs/inputs/execution-profile.input.js +0 -85
  147. package/dist/api/graphql-api/runs/inputs/execution-profile.input.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  import { step as coreStep, initialState, validateTemplate, InterpretError, selectJoinWinner, reduceJoinVerdict, } from '../pipeline-core/index.js';
2
- import { resolveLaunchOverrides, runnerNeedsLivePreflight, runnerUsesRealIntegrator } from './route-contract.js';
2
+ import { resolveLaunchOverrides, runnerNeedsLivePreflight } from './route-contract.js';
3
3
  import { redactEventPayload } from '../run/append-event.js';
4
4
  import { redactSecrets } from '../control-plane/inbox.js';
5
5
  import { fnv1a64Hex } from '../control-plane/steps.js';
@@ -10,6 +10,7 @@ export const INTEGRATOR_PROGRESS_EVENT_TYPES = ['integrate_succeeded', 'foreign_
10
10
  const MAX_STEPS = 1_000;
11
11
  const DEFAULT_RUNNER_TRANSIENT_MAX_ATTEMPTS = 2;
12
12
  const DEFAULT_RUNNER_TRANSIENT_RETRY_BACKOFF_MS = 2_000;
13
+ const TRANSIENT_RETRY_GATE_OUTCOMES = ['retry', 'give_up'];
13
14
  function readPositiveIntegerEnv(env, key, fallback) {
14
15
  const raw = env[key];
15
16
  if (raw === undefined)
@@ -45,11 +46,12 @@ function physicalAttemptFor(runId, stepKey, attemptNo) {
45
46
  attemptId: `attempt_${fnv1a64Hex(attemptKey)}`,
46
47
  };
47
48
  }
48
- function stepInputForAttempt(nodeId, inputs, attempt) {
49
+ function stepInputForAttempt(nodeId, inputs, attempt, retryContext) {
49
50
  return {
50
51
  nodeId,
51
52
  attempt: { attemptNo: attempt.attemptNo, attemptId: attempt.attemptId },
52
53
  ...(Object.keys(inputs).length > 0 ? { inputs } : {}),
54
+ ...(retryContext ? { retryContext } : {}),
53
55
  };
54
56
  }
55
57
  function optionalTiming(timing) {
@@ -98,6 +100,19 @@ function nextOrdinal(byNode, nodeId) {
98
100
  function isRecord(value) {
99
101
  return value !== null && typeof value === 'object' && !Array.isArray(value);
100
102
  }
103
+ function transientRetryGateOutcome(decision) {
104
+ const answer = isRecord(decision.answer) ? decision.answer : {};
105
+ let outcome = '';
106
+ if (typeof decision.outcome === 'string') {
107
+ outcome = decision.outcome;
108
+ }
109
+ else if (typeof answer.outcome === 'string') {
110
+ outcome = answer.outcome;
111
+ }
112
+ if (outcome === 'retry')
113
+ return 'retry';
114
+ return 'give_up';
115
+ }
101
116
  function producedChangeArtifact(value) {
102
117
  if (!isRecord(value))
103
118
  return undefined;
@@ -188,9 +203,27 @@ function nodeProducesChange(node) {
188
203
  node.produces?.name === 'change' &&
189
204
  node.resultSchema === 'schema:change';
190
205
  }
206
+ function recoveryContextAfterNode(nodeId, recoveryContext) {
207
+ if (!recoveryContext)
208
+ return undefined;
209
+ return nodeId === 'classifyRecovery' ? recoveryContext : null;
210
+ }
191
211
  function runnerProducesWorktreeChanges(runnerId) {
192
212
  return runnerId === 'claude-code' || runnerId === 'codex';
193
213
  }
214
+ const LIVE_WORKTREE_SCRIPT_REFS = new Set([
215
+ 'script:integrator',
216
+ 'script:confirmMerge',
217
+ 'script:pollPr',
218
+ 'script:overrideMerge',
219
+ 'script:respondThreads',
220
+ ]);
221
+ function scriptRequiresLiveWorktree(scriptRef) {
222
+ return LIVE_WORKTREE_SCRIPT_REFS.has(scriptRef);
223
+ }
224
+ function templateRequiresLiveWorktree(template) {
225
+ return Object.values(template.nodes).some((node) => node.kind === 'script' && scriptRequiresLiveWorktree(node.scriptRef));
226
+ }
194
227
  function artifactRefFromResult(result) {
195
228
  const artifacts = result.artifacts;
196
229
  const processArtifact = isRecord(artifacts) && isRecord(artifacts.process) ? artifacts.process : artifacts;
@@ -225,6 +258,8 @@ function transientKind(reason) {
225
258
  return 'timeout';
226
259
  if (/\b429\b|rate.?limit|session limit/i.test(reason))
227
260
  return 'rate_limit';
261
+ if (/\b529\b|overloaded/i.test(reason))
262
+ return 'overloaded';
228
263
  if (isLegacyRetryableCrashReason(reason))
229
264
  return 'crash';
230
265
  return 'unknown';
@@ -301,7 +336,7 @@ function gateVerdict(decision, outcomes) {
301
336
  return outcomes[0];
302
337
  return outcomes.length > 1 ? outcomes.at(-1) : undefined;
303
338
  }
304
- function gateResolutionOutput(decision, verdict, fallbackInboxId, trustedGateHeadSha) {
339
+ function gateResolutionMetadata(decision, fallbackInboxId) {
305
340
  const decisionRecord = isRecord(decision) ? decision : {};
306
341
  const answer = isRecord(decision.answer) ? decision.answer : {};
307
342
  const note = decision.note ?? (typeof answer.note === 'string' ? answer.note : undefined);
@@ -311,17 +346,61 @@ function gateResolutionOutput(decision, verdict, fallbackInboxId, trustedGateHea
311
346
  const mergeOverrideAudit = answer.mergeOverrideAudit ?? decisionRecord['mergeOverrideAudit'];
312
347
  const adoptionAudit = answer.adoptionAudit ?? decisionRecord['adoptionAudit'];
313
348
  return {
314
- outcome: decision.outcome ?? verdict ?? decision.decision,
315
349
  ...(note !== undefined ? { note } : {}),
316
350
  resolvedBy,
317
351
  resolvedAt,
318
- inboxId,
319
- ...(trustedGateHeadSha !== undefined ? { trustedGateHeadSha } : {}),
352
+ ...(inboxId !== undefined ? { inboxId } : {}),
320
353
  ...(mergeOverrideAudit !== undefined ? { mergeOverrideAudit } : {}),
321
354
  ...(adoptionAudit !== undefined ? { adoptionAudit } : {}),
355
+ };
356
+ }
357
+ function gateResolutionOutput(decision, verdict, fallbackInboxId, trustedGateHeadSha) {
358
+ const resolution = gateResolutionMetadata(decision, fallbackInboxId);
359
+ return {
360
+ outcome: decision.outcome ?? verdict ?? decision.decision,
361
+ ...(resolution.note !== undefined ? { note: resolution.note } : {}),
362
+ resolvedBy: resolution.resolvedBy,
363
+ resolvedAt: resolution.resolvedAt,
364
+ inboxId: resolution.inboxId ?? fallbackInboxId,
365
+ ...(trustedGateHeadSha !== undefined ? { trustedGateHeadSha } : {}),
366
+ ...(resolution.mergeOverrideAudit !== undefined ? { mergeOverrideAudit: resolution.mergeOverrideAudit } : {}),
367
+ ...(resolution.adoptionAudit !== undefined ? { adoptionAudit: resolution.adoptionAudit } : {}),
322
368
  ...(decision.decision ? { decision: decision.decision } : {}),
323
369
  };
324
370
  }
371
+ function nonEmptyString(value) {
372
+ if (typeof value !== 'string')
373
+ return undefined;
374
+ const trimmed = value.trim();
375
+ return trimmed.length > 0 ? trimmed : undefined;
376
+ }
377
+ function isSyntheticGateTimeoutFallback(decision) {
378
+ const answer = isRecord(decision.answer) ? decision.answer : {};
379
+ return decision.decision === 'reject' && answer.reason === 'gate-timeout';
380
+ }
381
+ function agentQuestionRetryContext(decision, nodeId, safeLesson) {
382
+ if (isSyntheticGateTimeoutFallback(decision))
383
+ return undefined;
384
+ const resolution = gateResolutionMetadata(decision);
385
+ const inboxId = nonEmptyString(resolution.inboxId);
386
+ const resolvedBy = nonEmptyString(resolution.resolvedBy);
387
+ if (!inboxId || !resolvedBy)
388
+ return undefined;
389
+ return {
390
+ kind: 'agent_question',
391
+ nodeId,
392
+ answer: decision.answer,
393
+ lesson: safeLesson,
394
+ inboxId,
395
+ resolvedBy,
396
+ };
397
+ }
398
+ function unresolvedAgentQuestionLesson(nodeId, safeLesson, decision) {
399
+ const reason = isSyntheticGateTimeoutFallback(decision)
400
+ ? 'agent question timed out'
401
+ : 'agent question resolution is missing inboxId or resolvedBy';
402
+ return `${reason} for ${nodeId}; original question: ${safeLesson}`;
403
+ }
325
404
  function gateTopicFor(reason) {
326
405
  if (/merge/i.test(reason))
327
406
  return 'merge';
@@ -363,9 +442,19 @@ function freshMergeGateArtifact(decision, lastVerdict, lastProducedOutput) {
363
442
  return lastProducedOutput;
364
443
  return undefined;
365
444
  }
366
- export function buildGateSummary(decision, outputsByNode, lastVerdict, lastProducedOutput) {
445
+ function latestBlockedScriptArtifact(decision, recoveryContext) {
446
+ if (decision.nodeId !== 'recoveryGate')
447
+ return undefined;
448
+ const payload = recoveryContext?.payload;
449
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload))
450
+ return undefined;
451
+ const record = payload;
452
+ return typeof record.lesson === 'string' && typeof record.reason === 'string' ? recoveryContext : undefined;
453
+ }
454
+ export function buildGateSummary(decision, outputsByNode, lastVerdict, lastProducedOutput, recoveryContext) {
367
455
  const summary = { nodeId: decision.nodeId, outcomes: decision.outcomes };
368
- const artRow = freshMergeGateArtifact(decision, lastVerdict, lastProducedOutput)
456
+ const artRow = latestBlockedScriptArtifact(decision, recoveryContext)
457
+ ?? freshMergeGateArtifact(decision, lastVerdict, lastProducedOutput)
369
458
  ?? resolveGateRow(decision.gatedArtifact, outputsByNode);
370
459
  if (artRow)
371
460
  summary.gatedArtifact = gateArtifactView(artRow, decision.gatedArtifact?.as);
@@ -444,9 +533,19 @@ function mergeOverrideEventPayload(result) {
444
533
  ...(result.override.reason ? { refusalReason: result.override.reason } : {}),
445
534
  };
446
535
  }
536
+ function scriptGithubAccount(decision, launchBindings) {
537
+ if (!launchBindings)
538
+ return undefined;
539
+ for (let index = launchBindings.length - 1; index >= 0; index -= 1) {
540
+ const binding = launchBindings[index];
541
+ if (binding.match.nodeId === decision.nodeId)
542
+ return binding.accounts?.github;
543
+ }
544
+ return undefined;
545
+ }
447
546
  export function buildSystemScriptRegistry(deps) {
448
- const { appendEvent, releaseWorktreeFn, integrateFn, runStub, confirmMergeFn, runConfirmStub, pollPrFn, runPollStub, overrideMergeFn, runOverrideStub, respondThreadsFn, runRespondStub, } = deps;
449
- function buildIntegratorInput(runId, ctx, inputs) {
547
+ const { appendEvent, releaseWorktreeFn, integrateFn, confirmMergeFn, pollPrFn, overrideMergeFn, respondThreadsFn, } = deps;
548
+ function buildIntegratorInput(runId, ctx, inputs, githubAccount) {
450
549
  const { taskId, title, base, issueRef, issueAction } = ctx;
451
550
  const change = producedChangeFromInputs(inputs);
452
551
  const mergeReadiness = mergeReadinessFromInputs(inputs);
@@ -456,6 +555,7 @@ export function buildSystemScriptRegistry(deps) {
456
555
  taskId,
457
556
  title,
458
557
  base,
558
+ ...(githubAccount ? { githubAccount } : {}),
459
559
  ...(issueRef ? { issueRef } : {}),
460
560
  ...(issueAction ? { issueAction } : {}),
461
561
  ...(changeForIntegrator ? { change: changeForIntegrator } : {}),
@@ -465,13 +565,11 @@ export function buildSystemScriptRegistry(deps) {
465
565
  };
466
566
  }
467
567
  function makeIntegratorScript(desc) {
468
- return async ({ runId, decision, ctx, bindingByRef, stepKey, inputs }) => {
469
- const integratorInput = buildIntegratorInput(runId, ctx, inputs);
470
- const binding = bindingByRef.get(decision.scriptRef) ?? bindingByRef.get('script:integrator');
471
- const useReal = !!binding && runnerUsesRealIntegrator(binding.resolvedRunnerId);
568
+ return async ({ runId, decision, ctx, launchBindings, stepKey, inputs }) => {
569
+ const integratorInput = buildIntegratorInput(runId, ctx, inputs, scriptGithubAccount(decision, launchBindings));
472
570
  let result;
473
571
  try {
474
- result = useReal ? await desc.real(integratorInput) : desc.stub(integratorInput);
572
+ result = await desc.real(integratorInput);
475
573
  }
476
574
  catch (err) {
477
575
  const reason = err instanceof Error ? err.message : String(err);
@@ -483,12 +581,17 @@ export function buildSystemScriptRegistry(deps) {
483
581
  return { outcome: 'failed', reason };
484
582
  }
485
583
  if ('needsHuman' in result) {
584
+ const pointer = {
585
+ reason: desc.blockedReason,
586
+ lesson: result.lesson,
587
+ nodeId: decision.nodeId,
588
+ };
486
589
  await appendEvent({
487
590
  runId, taskId: ctx.taskId, stepId: '', stepKey: 'pipeline',
488
591
  type: 'pipeline_blocked',
489
- payload: { reason: desc.blockedReason, lesson: result.lesson, nodeId: decision.nodeId },
592
+ payload: pointer,
490
593
  });
491
- return { outcome: 'blocked' };
594
+ return { outcome: 'blocked', pointer };
492
595
  }
493
596
  const { eventType, payload, pointer, verdict } = desc.mapSuccess(result);
494
597
  await appendEvent({ runId, taskId: ctx.taskId, stepId: '', stepKey, type: eventType, payload });
@@ -533,7 +636,6 @@ export function buildSystemScriptRegistry(deps) {
533
636
  };
534
637
  const integratorScript = makeIntegratorScript({
535
638
  real: integrateFn,
536
- stub: runStub,
537
639
  blockedReason: 'integrate',
538
640
  mapSuccess: (result) => ({
539
641
  eventType: integratorProgressEventType(result),
@@ -543,7 +645,6 @@ export function buildSystemScriptRegistry(deps) {
543
645
  });
544
646
  const confirmMergeScript = makeIntegratorScript({
545
647
  real: confirmMergeFn,
546
- stub: runConfirmStub,
547
648
  blockedReason: 'confirm-merge',
548
649
  mapSuccess: (result) => ({
549
650
  eventType: 'merge_confirmed',
@@ -562,7 +663,6 @@ export function buildSystemScriptRegistry(deps) {
562
663
  });
563
664
  const pollPrScript = makeIntegratorScript({
564
665
  real: pollPrFn,
565
- stub: runPollStub,
566
666
  blockedReason: 'poll-pr',
567
667
  mapSuccess: (result) => ({
568
668
  eventType: 'pr_polled',
@@ -579,13 +679,11 @@ export function buildSystemScriptRegistry(deps) {
579
679
  verdict: result.verdict,
580
680
  }),
581
681
  });
582
- const overrideMergeScript = async ({ runId, decision, ctx, bindingByRef, stepKey, inputs }) => {
583
- const integratorInput = buildIntegratorInput(runId, ctx, inputs);
584
- const binding = bindingByRef.get(decision.scriptRef) ?? bindingByRef.get('script:integrator');
585
- const useReal = !!binding && runnerUsesRealIntegrator(binding.resolvedRunnerId);
682
+ const overrideMergeScript = async ({ runId, decision, ctx, launchBindings, stepKey, inputs }) => {
683
+ const integratorInput = buildIntegratorInput(runId, ctx, inputs, scriptGithubAccount(decision, launchBindings));
586
684
  let result;
587
685
  try {
588
- result = useReal ? await overrideMergeFn(integratorInput) : runOverrideStub(integratorInput);
686
+ result = await overrideMergeFn(integratorInput);
589
687
  }
590
688
  catch (err) {
591
689
  const reason = err instanceof Error ? err.message : String(err);
@@ -597,12 +695,13 @@ export function buildSystemScriptRegistry(deps) {
597
695
  return { outcome: 'failed', reason };
598
696
  }
599
697
  if ('needsHuman' in result) {
698
+ const pointer = { reason: 'override-merge', lesson: result.lesson, nodeId: decision.nodeId };
600
699
  await appendEvent({
601
700
  runId, taskId: ctx.taskId, stepId: '', stepKey: 'pipeline',
602
701
  type: 'pipeline_blocked',
603
- payload: { reason: 'override-merge', lesson: result.lesson, nodeId: decision.nodeId },
702
+ payload: pointer,
604
703
  });
605
- return { outcome: 'blocked' };
704
+ return { outcome: 'blocked', pointer };
606
705
  }
607
706
  if (result.override.replied > 0 || result.override.resolved > 0) {
608
707
  await appendEvent({
@@ -628,7 +727,6 @@ export function buildSystemScriptRegistry(deps) {
628
727
  };
629
728
  const respondThreadsScript = makeIntegratorScript({
630
729
  real: respondThreadsFn,
631
- stub: runRespondStub,
632
730
  blockedReason: 'respond-threads',
633
731
  mapSuccess: (result) => ({
634
732
  eventType: 'threads_responded',
@@ -642,7 +740,6 @@ export function buildSystemScriptRegistry(deps) {
642
740
  ['script:pollPr', pollPrScript],
643
741
  ['script:overrideMerge', overrideMergeScript],
644
742
  ['script:respondThreads', respondThreadsScript],
645
- // Unknown refs fall through to this entry, matching the original else-branch behavior.
646
743
  ['script:integrator', integratorScript],
647
744
  ]);
648
745
  }
@@ -697,6 +794,22 @@ export function makeDataDrivenTask(runStepFn, deps) {
697
794
  await appendRunOutput(row);
698
795
  return row;
699
796
  }
797
+ async function recordRecoveryContext(runId, node, ordinal, attemptId, payload, outputsByNode) {
798
+ const row = {
799
+ runId,
800
+ nodeId: node.id,
801
+ ordinal,
802
+ name: 'recoveryContext',
803
+ schemaRef: 'schema:recoveryContext',
804
+ payload,
805
+ attemptId,
806
+ };
807
+ const list = outputsByNode.get(node.id) ?? [];
808
+ list.push(row);
809
+ outputsByNode.set(node.id, list);
810
+ await appendRunOutput(row);
811
+ return row;
812
+ }
700
813
  return async function dataDrivenTaskImpl(runId, opts) {
701
814
  try {
702
815
  return await runBody(runId, opts);
@@ -719,7 +832,8 @@ export function makeDataDrivenTask(runStepFn, deps) {
719
832
  throw new Error(`PINNED_TEMPLATE_INVALID: ${template.pipelineId} — ${diagnostics.map((d) => d.code).join(', ')}`);
720
833
  }
721
834
  const { taskId, title, base, issueRef, issueAction } = await loadRunTaskContext(runId);
722
- const live = route.roleBindings.some((b) => runnerNeedsLivePreflight(b.resolvedRunnerId));
835
+ const live = route.roleBindings.some((b) => runnerNeedsLivePreflight(b.resolvedRunnerId)) ||
836
+ templateRequiresLiveWorktree(template);
723
837
  if (live) {
724
838
  const pf = await preflightFn(taskId, base);
725
839
  if ('needsHuman' in pf) {
@@ -736,22 +850,20 @@ export function makeDataDrivenTask(runStepFn, deps) {
736
850
  const bindingByRef = new Map();
737
851
  for (const binding of route.roleBindings) {
738
852
  bindingByRef.set(`role:${binding.roleId}`, binding);
739
- bindingByRef.set(`script:${binding.roleId}`, binding);
740
853
  bindingByRef.set(binding.roleId, binding);
741
- if (runnerUsesRealIntegrator(binding.resolvedRunnerId) && !bindingByRef.has('script:integrator')) {
742
- bindingByRef.set('script:integrator', binding);
743
- }
744
854
  }
745
- const executionProfile = route.executionProfile;
855
+ const launchBindings = route.launchBindings ?? [];
746
856
  let state = initialState(template);
747
857
  let lastResult;
748
858
  let lastVerdict = '';
749
859
  let lastProducedOutput;
860
+ let recoveryContext;
750
861
  let lastFailureReason = '';
751
862
  let approvedMergeGateHeadShaValue;
752
863
  let stepCount = 0;
753
864
  const effectOrdinalByNode = new Map();
754
865
  const outputsByNode = new Map();
866
+ const agentQuestionRetryContextByNode = new Map();
755
867
  for (let i = 0; i < MAX_STEPS; i++) {
756
868
  const { state: nextState, decision } = coreStep(template, state, lastResult);
757
869
  state = nextState;
@@ -760,11 +872,12 @@ export function makeDataDrivenTask(runStepFn, deps) {
760
872
  return await finish(runId, decision.status, lastVerdict, stepCount, lastFailureReason);
761
873
  }
762
874
  const eff = await applyDecision(decision, {
763
- runId, template, state, bindingByRef, executionProfile, taskId, title, base, issueRef, issueAction,
764
- effectOrdinalByNode, outputsByNode, runnerRetryPolicy,
875
+ runId, template, state, bindingByRef, launchBindings, taskId, title, base, issueRef, issueAction,
876
+ effectOrdinalByNode, outputsByNode, runnerRetryPolicy, agentQuestionRetryContextByNode,
765
877
  live,
766
878
  lastVerdict,
767
879
  lastProducedOutput,
880
+ recoveryContext,
768
881
  approvedMergeGateHeadSha: approvedMergeGateHeadShaValue,
769
882
  });
770
883
  stepCount += eff.stepDelta;
@@ -778,6 +891,8 @@ export function makeDataDrivenTask(runStepFn, deps) {
778
891
  lastResult = eff.lastResult;
779
892
  lastVerdict = eff.lastVerdict ?? lastVerdict;
780
893
  lastProducedOutput = eff.producedOutput ?? lastProducedOutput;
894
+ if ('recoveryContext' in eff)
895
+ recoveryContext = eff.recoveryContext ?? undefined;
781
896
  lastFailureReason = eff.failureReason ?? '';
782
897
  if (eff.approvedMergeGateHeadSha !== undefined) {
783
898
  approvedMergeGateHeadShaValue = eff.approvedMergeGateHeadSha ?? undefined;
@@ -785,6 +900,19 @@ export function makeDataDrivenTask(runStepFn, deps) {
785
900
  }
786
901
  throw new InterpretError(`data-driven ${template.pipelineId} did not terminate within ${MAX_STEPS} steps (template loop bug)`);
787
902
  }
903
+ function forkBranchArrival(decision, branch, seq, status, lastVerdict) {
904
+ if (status !== 'succeeded') {
905
+ throw new InterpretError(`fork ${decision.nodeId} branch ${branch.id} completed ${status} before join ${decision.joinId}`);
906
+ }
907
+ return {
908
+ branchId: branch.id,
909
+ seq,
910
+ ...(lastVerdict ? { verdict: lastVerdict } : {}),
911
+ };
912
+ }
913
+ function recoveryContextFromEffect(current, effect) {
914
+ return 'recoveryContext' in effect ? effect.recoveryContext ?? undefined : current;
915
+ }
788
916
  function branchTemplateForJoin(template, joinId) {
789
917
  const join = resolveNode(template, joinId);
790
918
  if (join.kind !== 'join')
@@ -837,20 +965,14 @@ export function makeDataDrivenTask(runStepFn, deps) {
837
965
  let lastResult;
838
966
  let lastVerdict = '';
839
967
  let lastProducedOutput;
968
+ let recoveryContext;
840
969
  let stepDelta = 0;
841
970
  for (let i = 0; i < MAX_STEPS; i++) {
842
971
  const next = coreStep(branchTemplate, state, lastResult);
843
972
  state = next.state;
844
973
  if (next.decision.type === 'complete') {
845
- if (next.decision.status !== 'succeeded') {
846
- throw new InterpretError(`fork ${decision.nodeId} branch ${branch.id} completed ${next.decision.status} before join ${decision.joinId}`);
847
- }
848
974
  return {
849
- arrival: {
850
- branchId: branch.id,
851
- seq,
852
- ...(lastVerdict ? { verdict: lastVerdict } : {}),
853
- },
975
+ arrival: forkBranchArrival(decision, branch, seq, next.decision.status, lastVerdict),
854
976
  stepDelta,
855
977
  };
856
978
  }
@@ -860,16 +982,16 @@ export function makeDataDrivenTask(runStepFn, deps) {
860
982
  state,
861
983
  lastVerdict,
862
984
  lastProducedOutput,
985
+ recoveryContext,
863
986
  });
864
987
  stepDelta += eff.stepDelta;
865
988
  if (eff.terminal)
866
989
  return { terminal: eff.terminal, stepDelta };
867
- if (eff.stateOverride)
868
- state = eff.stateOverride;
990
+ state = eff.stateOverride ?? state;
869
991
  lastResult = eff.lastResult;
870
- if (eff.lastVerdict !== undefined)
871
- lastVerdict = eff.lastVerdict;
992
+ lastVerdict = eff.lastVerdict ?? lastVerdict;
872
993
  lastProducedOutput = eff.producedOutput ?? lastProducedOutput;
994
+ recoveryContext = recoveryContextFromEffect(recoveryContext, eff);
873
995
  }
874
996
  throw new InterpretError(`fork ${decision.nodeId} branch ${branch.id} did not reach join ${decision.joinId} within ${MAX_STEPS} steps`);
875
997
  }
@@ -890,8 +1012,17 @@ export function makeDataDrivenTask(runStepFn, deps) {
890
1012
  return { lastResult: { outcome: 'failed', errorCode: REVO_INPUT_MISSING }, lastVerdict: 'failed', failureReason: reason, stepDelta: 1 };
891
1013
  }
892
1014
  const result = await invokeRole(runId, decision, node, ctx, resolved.inputs, stepKey);
1015
+ if ('question' in result) {
1016
+ ctx.agentQuestionRetryContextByNode.set(node.id, result.retryContext);
1017
+ return {
1018
+ lastResult: undefined,
1019
+ stateOverride: reopenNodeState(ctx.state, node.id),
1020
+ stepDelta: result.attemptsMade,
1021
+ };
1022
+ }
893
1023
  if ('blocked' in result) {
894
1024
  if (result.recovery) {
1025
+ ctx.agentQuestionRetryContextByNode.delete(node.id);
895
1026
  const recovery = await awaitVerificationRecoveryGate(runId, ctx, result.recovery);
896
1027
  return {
897
1028
  lastResult: undefined,
@@ -903,6 +1034,22 @@ export function makeDataDrivenTask(runStepFn, deps) {
903
1034
  stepDelta: result.attemptsMade,
904
1035
  };
905
1036
  }
1037
+ if (result.retry?.attemptsExhausted) {
1038
+ const retryOutcome = await awaitTransientRetryGate(runId, ctx, {
1039
+ node,
1040
+ stepKey,
1041
+ binding: resolveRoleBinding(ctx, decision),
1042
+ retry: result.retry,
1043
+ });
1044
+ if (retryOutcome === 'retry') {
1045
+ return {
1046
+ lastResult: undefined,
1047
+ stateOverride: reopenNodeState(ctx.state, node.id),
1048
+ stepDelta: result.attemptsMade,
1049
+ };
1050
+ }
1051
+ }
1052
+ ctx.agentQuestionRetryContextByNode.delete(node.id);
906
1053
  return {
907
1054
  lastResult: undefined,
908
1055
  terminal: {
@@ -915,6 +1062,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
915
1062
  };
916
1063
  }
917
1064
  if (result.failed) {
1065
+ ctx.agentQuestionRetryContextByNode.delete(node.id);
918
1066
  await appendEvent({
919
1067
  runId, taskId, stepId: '', stepKey, type: 'step_failed',
920
1068
  idempotencyKey: result.attemptId,
@@ -922,12 +1070,14 @@ export function makeDataDrivenTask(runStepFn, deps) {
922
1070
  });
923
1071
  return { lastResult: { outcome: 'failed', errorCode: result.errorCode }, lastVerdict: 'failed', failureReason: result.reason, stepDelta: result.attemptsMade };
924
1072
  }
1073
+ ctx.agentQuestionRetryContextByNode.delete(node.id);
925
1074
  const producedOutput = await recordOutput(runId, node, ordinal, result.attemptId, result.output, ctx.outputsByNode);
926
1075
  const verdict = result.verdict;
927
1076
  return {
928
1077
  lastResult: { outcome: 'succeeded', ...(verdict ? { verdict } : {}) },
929
1078
  ...(verdict ? { lastVerdict: verdict } : {}),
930
1079
  ...(producedOutput ? { producedOutput } : {}),
1080
+ ...(ctx.recoveryContext ? { recoveryContext: recoveryContextAfterNode(node.id, ctx.recoveryContext) } : {}),
931
1081
  stepDelta: result.attemptsMade,
932
1082
  };
933
1083
  }
@@ -943,13 +1093,21 @@ export function makeDataDrivenTask(runStepFn, deps) {
943
1093
  });
944
1094
  return { lastResult: { outcome: 'failed', errorCode: REVO_INPUT_MISSING }, lastVerdict: 'failed', failureReason: reason, stepDelta: 1 };
945
1095
  }
946
- const scriptResult = await invokeScript(runId, decision, { taskId, title, base, issueRef: ctx.issueRef, issueAction: ctx.issueAction }, bindingByRef, stepKeyFor(node.id, ordinal), resolved.inputs);
1096
+ const scriptResult = await invokeScript(runId, decision, { taskId, title, base, issueRef: ctx.issueRef, issueAction: ctx.issueAction }, bindingByRef, ctx.launchBindings, stepKeyFor(node.id, ordinal), resolved.inputs);
947
1097
  if (scriptResult.outcome === 'blocked') {
948
- return { lastResult: { outcome: 'failed', errorCode: REVO_SCRIPT_BLOCKED }, lastVerdict: 'blocked', stepDelta: 1 };
1098
+ const recoveryContext = scriptResult.pointer === undefined
1099
+ ? undefined
1100
+ : await recordRecoveryContext(runId, node, ordinal, stepKeyFor(node.id, ordinal), scriptResult.pointer, ctx.outputsByNode);
1101
+ return {
1102
+ lastResult: { outcome: 'failed', errorCode: REVO_SCRIPT_BLOCKED },
1103
+ lastVerdict: 'blocked',
1104
+ ...(recoveryContext ? { recoveryContext } : {}),
1105
+ stepDelta: 1,
1106
+ };
949
1107
  }
950
1108
  if (scriptResult.outcome === 'failed') {
951
1109
  const reason = scriptResult.reason ? `${REVO_SCRIPT_FAILED}: ${scriptResult.reason}` : REVO_SCRIPT_FAILED;
952
- return { lastResult: { outcome: 'failed', errorCode: REVO_SCRIPT_FAILED }, lastVerdict: 'failed', failureReason: reason, stepDelta: 1 };
1110
+ return { lastResult: { outcome: 'failed', errorCode: REVO_SCRIPT_FAILED }, lastVerdict: 'failed', recoveryContext: null, failureReason: reason, stepDelta: 1 };
953
1111
  }
954
1112
  let pointer = scriptResult.pointer;
955
1113
  let sv = scriptResult.verdict;
@@ -965,6 +1123,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
965
1123
  lastResult: stateOverride ? undefined : { outcome: 'succeeded', ...(sv ? { verdict: sv } : {}) },
966
1124
  ...(sv ? { lastVerdict: sv } : {}),
967
1125
  ...(producedOutput ? { producedOutput } : {}),
1126
+ ...(ctx.recoveryContext ? { recoveryContext: recoveryContextAfterNode(node.id, ctx.recoveryContext) } : {}),
968
1127
  ...(stateOverride ? { stateOverride } : {}),
969
1128
  stepDelta: 1,
970
1129
  };
@@ -972,7 +1131,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
972
1131
  case 'awaitGate': {
973
1132
  const topic = gateTopicFor(decision.reason);
974
1133
  const ordinal = nextOrdinal(ctx.effectOrdinalByNode, decision.nodeId);
975
- const summary = buildGateSummary(decision, ctx.outputsByNode, ctx.lastVerdict, ctx.lastProducedOutput);
1134
+ const summary = buildGateSummary(decision, ctx.outputsByNode, ctx.lastVerdict, ctx.lastProducedOutput, ctx.recoveryContext);
976
1135
  const human = await awaitHuman(runId, topic, stepKeyFor(decision.nodeId, ordinal), `${decision.reason} approval`, summary, decision.outcomes);
977
1136
  const verdict = gateVerdict(human, decision.outcomes);
978
1137
  const approvedHeadSha = approvedMergeGateHeadSha(decision, verdict, summary);
@@ -981,6 +1140,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
981
1140
  lastResult: verdict ? { verdict } : {},
982
1141
  ...(verdict ? { lastVerdict: verdict } : {}),
983
1142
  ...(producedOutput ? { producedOutput } : {}),
1143
+ ...(decision.nodeId === 'recoveryGate' ? { recoveryContext: null } : {}),
984
1144
  ...(approvedHeadSha !== undefined ? { approvedMergeGateHeadSha: approvedHeadSha } : {}),
985
1145
  stepDelta: 0,
986
1146
  };
@@ -1000,9 +1160,43 @@ export function makeDataDrivenTask(runStepFn, deps) {
1000
1160
  return { lastResult: {}, stepDelta: 0 };
1001
1161
  }
1002
1162
  }
1163
+ async function awaitTransientRetryGate(runId, ctx, input) {
1164
+ const outcomes = [...TRANSIENT_RETRY_GATE_OUTCOMES];
1165
+ const decision = await awaitHuman(runId, 'retry', `transientRetry:${input.stepKey}`, `Retry ${input.node.id} after transient runner failure`, {
1166
+ kind: 'transient_retry',
1167
+ runId,
1168
+ taskId: ctx.taskId,
1169
+ nodeId: input.node.id,
1170
+ step: input.stepKey,
1171
+ role: input.binding.rowId,
1172
+ runner: input.binding.resolvedRunnerId,
1173
+ reason: input.retry.reason,
1174
+ lesson: input.retry.lesson,
1175
+ attemptsExhausted: input.retry.attemptsExhausted,
1176
+ attemptsMade: input.retry.attemptsMade,
1177
+ maxAttempts: input.retry.maxAttempts,
1178
+ attemptIds: input.retry.attemptIds,
1179
+ lastAttemptId: input.retry.lastAttemptId,
1180
+ ...(input.retry.failureKind ? { failureKind: input.retry.failureKind } : {}),
1181
+ ...(input.retry.transientKind ? { transientKind: input.retry.transientKind } : {}),
1182
+ ...(input.retry.timing !== undefined ? { timing: input.retry.timing } : {}),
1183
+ reconcile: { default: 'keep', supported: ['keep'] },
1184
+ outcomes,
1185
+ }, outcomes);
1186
+ return transientRetryGateOutcome(decision);
1187
+ }
1003
1188
  async function invokeRole(runId, decision, node, ctx, inputs, stepKey) {
1004
1189
  const binding = resolveRoleBinding(ctx, decision);
1005
- return invokeRoleAttempts({ runId, decision, node, ctx, inputs, stepKey, binding });
1190
+ return invokeRoleAttempts({
1191
+ runId,
1192
+ decision,
1193
+ node,
1194
+ ctx,
1195
+ inputs,
1196
+ stepKey,
1197
+ binding,
1198
+ retryContext: ctx.agentQuestionRetryContextByNode.get(node.id),
1199
+ });
1006
1200
  }
1007
1201
  function resolveRoleBinding(ctx, decision) {
1008
1202
  const binding = ctx.bindingByRef.get(decision.roleRef);
@@ -1014,21 +1208,24 @@ export function makeDataDrivenTask(runStepFn, deps) {
1014
1208
  async function invokeRoleAttempts(input) {
1015
1209
  const { runId, decision, node, ctx, inputs, stepKey, binding } = input;
1016
1210
  const attemptIds = [];
1211
+ const retryContext = input.retryContext;
1017
1212
  for (let attemptNo = 1; attemptNo <= ctx.runnerRetryPolicy.maxAttempts; attemptNo++) {
1018
1213
  const physicalAttempt = physicalAttemptFor(runId, stepKey, attemptNo);
1019
1214
  attemptIds.push(physicalAttempt.attemptId);
1020
- const launchOverrides = resolveLaunchOverrides(binding, decision.nodeId, ctx.executionProfile);
1021
- const result = await runStepFn(runId, binding.rowId, stepKey, stepInputForAttempt(decision.nodeId, inputs, physicalAttempt), binding.resolvedRunnerId, ctx.executionProfile, physicalAttempt, ctx.template.verdicts.domain, launchOverrides);
1215
+ const launchOverrides = resolveLaunchOverrides(binding, decision.nodeId, ctx.launchBindings);
1216
+ const result = await runStepFn(runId, binding.rowId, stepKey, stepInputForAttempt(decision.nodeId, inputs, physicalAttempt, retryContext), binding.resolvedRunnerId, physicalAttempt, ctx.template.verdicts.domain, launchOverrides);
1022
1217
  const needsHuman = await maybeHandleNeedsHumanRoleResult({
1023
1218
  ...input,
1024
1219
  attemptIds,
1025
1220
  physicalAttempt,
1026
1221
  result,
1027
1222
  });
1028
- if (needsHuman === 'retry')
1029
- continue;
1030
- if (needsHuman)
1223
+ if (needsHuman) {
1224
+ if ('action' in needsHuman) {
1225
+ continue;
1226
+ }
1031
1227
  return needsHuman;
1228
+ }
1032
1229
  const failed = roleValidationFailure(ctx.template, node, result, physicalAttempt);
1033
1230
  if (failed)
1034
1231
  return failed;
@@ -1089,17 +1286,57 @@ export function makeDataDrivenTask(runStepFn, deps) {
1089
1286
  }
1090
1287
  const transient = transientRunnerFailure(result);
1091
1288
  if (transient === undefined) {
1092
- return {
1093
- blocked: true,
1094
- reason: 'agent-needs-human',
1095
- lesson: safeLesson,
1096
- attemptsMade: physicalAttempt.attemptNo,
1097
- };
1289
+ const question = await awaitAgentQuestion({ ...input, safeLesson, physicalAttempt });
1290
+ if ('unresolved' in question) {
1291
+ return {
1292
+ blocked: true,
1293
+ reason: question.reason,
1294
+ lesson: question.lesson,
1295
+ attemptsMade: physicalAttempt.attemptNo,
1296
+ };
1297
+ }
1298
+ return { question: true, retryContext: question, attemptsMade: physicalAttempt.attemptNo };
1098
1299
  }
1099
1300
  return handleTransientRoleResult({ ...input, transient });
1100
1301
  }
1101
1302
  return undefined;
1102
1303
  }
1304
+ async function awaitAgentQuestion(input) {
1305
+ const { runId, node, ctx, stepKey, binding, physicalAttempt, safeLesson } = input;
1306
+ const question = await awaitHuman(runId, 'question', `agentQuestion:${stepKey}:attempt${physicalAttempt.attemptNo}`, `${node.id} question`, {
1307
+ kind: 'agent_question',
1308
+ runId,
1309
+ taskId: ctx.taskId,
1310
+ nodeId: node.id,
1311
+ step: stepKey,
1312
+ role: binding.rowId,
1313
+ runner: binding.resolvedRunnerId,
1314
+ lesson: safeLesson,
1315
+ attemptId: physicalAttempt.attemptId,
1316
+ }, undefined, 'question');
1317
+ const retryContext = agentQuestionRetryContext(question, node.id, safeLesson);
1318
+ if (retryContext === undefined) {
1319
+ return {
1320
+ unresolved: true,
1321
+ reason: 'agent-question-unresolved',
1322
+ lesson: unresolvedAgentQuestionLesson(node.id, safeLesson, question),
1323
+ };
1324
+ }
1325
+ await appendEvent({
1326
+ runId,
1327
+ taskId: ctx.taskId,
1328
+ stepId: '',
1329
+ stepKey,
1330
+ type: 'agent_question_resolved',
1331
+ idempotencyKey: `${physicalAttempt.attemptId}:question`,
1332
+ payload: {
1333
+ nodeId: node.id,
1334
+ inboxId: retryContext.inboxId,
1335
+ resolvedBy: retryContext.resolvedBy,
1336
+ },
1337
+ });
1338
+ return retryContext;
1339
+ }
1103
1340
  async function handleTransientRoleResult(input) {
1104
1341
  const { runId, node, ctx, stepKey, attemptIds, physicalAttempt, transient } = input;
1105
1342
  const policy = ctx.runnerRetryPolicy;
@@ -1117,7 +1354,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
1117
1354
  });
1118
1355
  if (policy.backoffMs > 0)
1119
1356
  await deps.sleep(policy.backoffMs);
1120
- return 'retry';
1357
+ return { action: 'retry' };
1121
1358
  }
1122
1359
  const retry = runnerRetryBlockPayload({
1123
1360
  transient,
@@ -1240,9 +1477,21 @@ export function makeDataDrivenTask(runStepFn, deps) {
1240
1477
  lesson: `Human selected ${outcome}; recovery is recorded but no Revo worktree patch was adopted by the main session.`,
1241
1478
  };
1242
1479
  }
1243
- async function invokeScript(runId, decision, ctx, bindingByRef, stepKey, inputs) {
1244
- const handler = scriptRegistry.get(decision.scriptRef) ?? scriptRegistry.get('script:integrator');
1245
- return handler({ runId, decision, ctx, bindingByRef, stepKey, inputs });
1480
+ async function invokeScript(runId, decision, ctx, bindingByRef, launchBindings, stepKey, inputs) {
1481
+ const handler = scriptRegistry.get(decision.scriptRef);
1482
+ if (!handler) {
1483
+ const reason = `script handler is not registered: ${decision.scriptRef}`;
1484
+ await appendEvent({
1485
+ runId,
1486
+ taskId: ctx.taskId,
1487
+ stepId: '',
1488
+ stepKey,
1489
+ type: 'step_failed',
1490
+ payload: { scriptRef: decision.scriptRef, error: reason },
1491
+ });
1492
+ return { outcome: 'failed', reason };
1493
+ }
1494
+ return handler({ runId, decision, ctx, bindingByRef, launchBindings, stepKey, inputs });
1246
1495
  }
1247
1496
  async function finish(runId, status, verdict, steps, failureReason = '') {
1248
1497
  if (status === 'succeeded') {