@revisium/orchestrator 0.1.0 → 0.2.0-alpha.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 (212) hide show
  1. package/README.md +78 -0
  2. package/control-plane/bootstrap.config.json +43 -1
  3. package/control-plane/default-playbook/catalog/pipelines.json +941 -3
  4. package/control-plane/default-playbook/catalog/roles.json +54 -0
  5. package/dist/api/graphql-api/graphql-ws/pubsub.module.js +0 -1
  6. package/dist/api/graphql-api/graphql-ws/pubsub.module.js.map +1 -1
  7. package/dist/api/graphql-api/graphql-ws/run-progress-subscription-poller.service.js +0 -1
  8. package/dist/api/graphql-api/graphql-ws/run-progress-subscription-poller.service.js.map +1 -1
  9. package/dist/api/graphql-api/graphql-ws/subscription-bridge.service.js +0 -1
  10. package/dist/api/graphql-api/graphql-ws/subscription-bridge.service.js.map +1 -1
  11. package/dist/api/graphql-api/pr/model/pr-readiness.model.js +0 -4
  12. package/dist/api/graphql-api/pr/model/pr-readiness.model.js.map +1 -1
  13. package/dist/cli/commands/doctor-report.js +0 -18
  14. package/dist/cli/commands/doctor-report.js.map +1 -1
  15. package/dist/cli/commands/lifecycle.js +3 -71
  16. package/dist/cli/commands/lifecycle.js.map +1 -1
  17. package/dist/cli/commands/mcp.js +0 -14
  18. package/dist/cli/commands/mcp.js.map +1 -1
  19. package/dist/cli/commands/process-tree.js +4 -26
  20. package/dist/cli/commands/process-tree.js.map +1 -1
  21. package/dist/cli/commands/revisium-helpers.js +0 -6
  22. package/dist/cli/commands/revisium-helpers.js.map +1 -1
  23. package/dist/cli/commands/rogue-reaper.js +0 -34
  24. package/dist/cli/commands/rogue-reaper.js.map +1 -1
  25. package/dist/cli/index.js +0 -4
  26. package/dist/cli/index.js.map +1 -1
  27. package/dist/cli/program.js +0 -5
  28. package/dist/cli/program.js.map +1 -1
  29. package/dist/config.js +0 -25
  30. package/dist/config.js.map +1 -1
  31. package/dist/control-plane/bootstrap.js +0 -28
  32. package/dist/control-plane/bootstrap.js.map +1 -1
  33. package/dist/control-plane/change-notifications.js +0 -1
  34. package/dist/control-plane/change-notifications.js.map +1 -1
  35. package/dist/control-plane/client-transport.js +0 -11
  36. package/dist/control-plane/client-transport.js.map +1 -1
  37. package/dist/control-plane/definitions.js +8 -17
  38. package/dist/control-plane/definitions.js.map +1 -1
  39. package/dist/control-plane/inbox.js +2 -120
  40. package/dist/control-plane/inbox.js.map +1 -1
  41. package/dist/control-plane/resolve-cwd.js +0 -61
  42. package/dist/control-plane/resolve-cwd.js.map +1 -1
  43. package/dist/control-plane/schema-migration.js +0 -9
  44. package/dist/control-plane/schema-migration.js.map +1 -1
  45. package/dist/control-plane/seed-default-playbook.js +5 -70
  46. package/dist/control-plane/seed-default-playbook.js.map +1 -1
  47. package/dist/control-plane/steps.js +0 -15
  48. package/dist/control-plane/steps.js.map +1 -1
  49. package/dist/e2e/kit/assertions.js +1 -1
  50. package/dist/e2e/kit/assertions.js.map +1 -1
  51. package/dist/engine/dbos.service.js +9 -179
  52. package/dist/engine/dbos.service.js.map +1 -1
  53. package/dist/engine/ensure-postgres.js +0 -48
  54. package/dist/engine/ensure-postgres.js.map +1 -1
  55. package/dist/host/daemon.js +2 -21
  56. package/dist/host/daemon.js.map +1 -1
  57. package/dist/host/dbos-identity.js +0 -31
  58. package/dist/host/dbos-identity.js.map +1 -1
  59. package/dist/host/ensure-host.js +1 -37
  60. package/dist/host/ensure-host.js.map +1 -1
  61. package/dist/host/ensure-revisium.js +1 -135
  62. package/dist/host/ensure-revisium.js.map +1 -1
  63. package/dist/host/host-runtime.js +0 -28
  64. package/dist/host/host-runtime.js.map +1 -1
  65. package/dist/host/host.lifecycle.js +0 -27
  66. package/dist/host/host.lifecycle.js.map +1 -1
  67. package/dist/host/queue-ownership.js +0 -42
  68. package/dist/host/queue-ownership.js.map +1 -1
  69. package/dist/host/queue-poller-census.js +1 -28
  70. package/dist/host/queue-poller-census.js.map +1 -1
  71. package/dist/http/graphql-host.module.js +0 -3
  72. package/dist/http/graphql-host.module.js.map +1 -1
  73. package/dist/mcp/mcp-capabilities.js +19 -14
  74. package/dist/mcp/mcp-capabilities.js.map +1 -1
  75. package/dist/mcp/mcp-facade.service.js +41 -23
  76. package/dist/mcp/mcp-facade.service.js.map +1 -1
  77. package/dist/mcp/mcp-http.service.js +0 -18
  78. package/dist/mcp/mcp-http.service.js.map +1 -1
  79. package/dist/mcp/mcp-tools.js +18 -40
  80. package/dist/mcp/mcp-tools.js.map +1 -1
  81. package/dist/observability/activity-signal.js +0 -4
  82. package/dist/observability/activity-signal.js.map +1 -1
  83. package/dist/observability/agent-activity-reporter.js +0 -4
  84. package/dist/observability/agent-activity-reporter.js.map +1 -1
  85. package/dist/pipeline/await-human.js +0 -40
  86. package/dist/pipeline/await-human.js.map +1 -1
  87. package/dist/pipeline/data-driven-task.workflow.js +106 -236
  88. package/dist/pipeline/data-driven-task.workflow.js.map +1 -1
  89. package/dist/pipeline/data-driven-template.js +0 -25
  90. package/dist/pipeline/data-driven-template.js.map +1 -1
  91. package/dist/pipeline/pipeline.module.js +0 -12
  92. package/dist/pipeline/pipeline.module.js.map +1 -1
  93. package/dist/pipeline/pipeline.service.js +10 -99
  94. package/dist/pipeline/pipeline.service.js.map +1 -1
  95. package/dist/pipeline-core/index.js +1 -13
  96. package/dist/pipeline-core/index.js.map +1 -1
  97. package/dist/pipeline-core/interpret.js +21 -94
  98. package/dist/pipeline-core/interpret.js.map +1 -1
  99. package/dist/pipeline-core/kit/assertions.js +0 -19
  100. package/dist/pipeline-core/kit/assertions.js.map +1 -1
  101. package/dist/pipeline-core/kit/builders.js +1 -21
  102. package/dist/pipeline-core/kit/builders.js.map +1 -1
  103. package/dist/pipeline-core/kit/drive.js +1 -31
  104. package/dist/pipeline-core/kit/drive.js.map +1 -1
  105. package/dist/pipeline-core/kit/fixtures.js +8 -82
  106. package/dist/pipeline-core/kit/fixtures.js.map +1 -1
  107. package/dist/pipeline-core/kit/index.js +0 -8
  108. package/dist/pipeline-core/kit/index.js.map +1 -1
  109. package/dist/pipeline-core/types.js +13 -52
  110. package/dist/pipeline-core/types.js.map +1 -1
  111. package/dist/pipeline-core/validate-capability.js +0 -3
  112. package/dist/pipeline-core/validate-capability.js.map +1 -1
  113. package/dist/pipeline-core/validate-conflicts.js +0 -9
  114. package/dist/pipeline-core/validate-conflicts.js.map +1 -1
  115. package/dist/pipeline-core/validate-dataflow.js +0 -17
  116. package/dist/pipeline-core/validate-dataflow.js.map +1 -1
  117. package/dist/pipeline-core/validate-diff.js +3 -36
  118. package/dist/pipeline-core/validate-diff.js.map +1 -1
  119. package/dist/pipeline-core/validate-edges.js +0 -6
  120. package/dist/pipeline-core/validate-edges.js.map +1 -1
  121. package/dist/pipeline-core/validate-graph.js +0 -11
  122. package/dist/pipeline-core/validate-graph.js.map +1 -1
  123. package/dist/pipeline-core/validate-loops.js +37 -44
  124. package/dist/pipeline-core/validate-loops.js.map +1 -1
  125. package/dist/pipeline-core/validate-parallel.js +9 -15
  126. package/dist/pipeline-core/validate-parallel.js.map +1 -1
  127. package/dist/pipeline-core/validate-sink.js +0 -7
  128. package/dist/pipeline-core/validate-sink.js.map +1 -1
  129. package/dist/pipeline-core/validate-topology.js +1 -20
  130. package/dist/pipeline-core/validate-topology.js.map +1 -1
  131. package/dist/pipeline-core/validate-verdict.js +52 -7
  132. package/dist/pipeline-core/validate-verdict.js.map +1 -1
  133. package/dist/pipeline-core/validate.js +5 -29
  134. package/dist/pipeline-core/validate.js.map +1 -1
  135. package/dist/playbook/catalog-loader.js +8 -3
  136. package/dist/playbook/catalog-loader.js.map +1 -1
  137. package/dist/playbook/import-mapper.js +1 -9
  138. package/dist/playbook/import-mapper.js.map +1 -1
  139. package/dist/poller/pr-readiness-core.js +0 -74
  140. package/dist/poller/pr-readiness-core.js.map +1 -1
  141. package/dist/revisium/inbox.service.js +0 -24
  142. package/dist/revisium/inbox.service.js.map +1 -1
  143. package/dist/revisium/playbooks.service.js +2 -4
  144. package/dist/revisium/playbooks.service.js.map +1 -1
  145. package/dist/revisium/revisium.module.js +0 -12
  146. package/dist/revisium/revisium.module.js.map +1 -1
  147. package/dist/revisium/roles.service.js +0 -6
  148. package/dist/revisium/roles.service.js.map +1 -1
  149. package/dist/revisium/run.service.js +1 -75
  150. package/dist/revisium/run.service.js.map +1 -1
  151. package/dist/revisium/tokens.js +0 -4
  152. package/dist/revisium/tokens.js.map +1 -1
  153. package/dist/run/append-event.js +0 -34
  154. package/dist/run/append-event.js.map +1 -1
  155. package/dist/run/block-run.js.map +1 -1
  156. package/dist/run/cancel-run.js +0 -33
  157. package/dist/run/cancel-run.js.map +1 -1
  158. package/dist/run/complete-run.js.map +1 -1
  159. package/dist/run/create-run.js +0 -7
  160. package/dist/run/create-run.js.map +1 -1
  161. package/dist/run/fail-run.js +0 -3
  162. package/dist/run/fail-run.js.map +1 -1
  163. package/dist/run/inspect-run.js +0 -26
  164. package/dist/run/inspect-run.js.map +1 -1
  165. package/dist/run/run-outputs.js +0 -19
  166. package/dist/run/run-outputs.js.map +1 -1
  167. package/dist/run/terminal-run-status.js +0 -12
  168. package/dist/run/terminal-run-status.js.map +1 -1
  169. package/dist/runners/claude-code.service.js +0 -11
  170. package/dist/runners/claude-code.service.js.map +1 -1
  171. package/dist/runners/codex.service.js +0 -9
  172. package/dist/runners/codex.service.js.map +1 -1
  173. package/dist/runners/gh-identity.js +0 -52
  174. package/dist/runners/gh-identity.js.map +1 -1
  175. package/dist/runners/integrator-branch-naming.js +3 -11
  176. package/dist/runners/integrator-branch-naming.js.map +1 -1
  177. package/dist/runners/integrator-git.js +0 -25
  178. package/dist/runners/integrator-git.js.map +1 -1
  179. package/dist/runners/integrator-remote.js +0 -2
  180. package/dist/runners/integrator-remote.js.map +1 -1
  181. package/dist/runners/integrator-types.js +0 -7
  182. package/dist/runners/integrator-types.js.map +1 -1
  183. package/dist/runners/integrator.js +3 -167
  184. package/dist/runners/integrator.js.map +1 -1
  185. package/dist/runners/runner.module.js +0 -6
  186. package/dist/runners/runner.module.js.map +1 -1
  187. package/dist/runners/tokens.js +0 -4
  188. package/dist/runners/tokens.js.map +1 -1
  189. package/dist/runners/worktree.service.js +1 -17
  190. package/dist/runners/worktree.service.js.map +1 -1
  191. package/dist/task-control-plane/run-watch.service.js +49 -173
  192. package/dist/task-control-plane/run-watch.service.js.map +1 -1
  193. package/dist/task-control-plane/task-control-plane-api.service.js +15 -17
  194. package/dist/task-control-plane/task-control-plane-api.service.js.map +1 -1
  195. package/dist/worker/build-context.js +0 -16
  196. package/dist/worker/build-context.js.map +1 -1
  197. package/dist/worker/claude-code-runner.js +9 -42
  198. package/dist/worker/claude-code-runner.js.map +1 -1
  199. package/dist/worker/codex-runner.js +88 -22
  200. package/dist/worker/codex-runner.js.map +1 -1
  201. package/dist/worker/git-worktree-manager.js +20 -67
  202. package/dist/worker/git-worktree-manager.js.map +1 -1
  203. package/dist/worker/process-executor.js +0 -12
  204. package/dist/worker/process-executor.js.map +1 -1
  205. package/dist/worker/result-envelope.js +33 -49
  206. package/dist/worker/result-envelope.js.map +1 -1
  207. package/dist/worker/runner-dispatch.js +0 -3
  208. package/dist/worker/runner-dispatch.js.map +1 -1
  209. package/dist/worker/runner.js.map +1 -1
  210. package/dist/worker/stub-runner.js +0 -7
  211. package/dist/worker/stub-runner.js.map +1 -1
  212. package/package.json +2 -2
@@ -1,33 +1,4 @@
1
- /**
2
- * data-driven-task.workflow.ts — the DBOS effect-adapter for a DATA-DRIVEN pipeline (plan 0015 §10).
3
- *
4
- * Runs the pure `pipeline-core` graph on REAL DBOS. As of slice 3 this is the SOLE pipeline engine:
5
- * selection routes EVERY pipeline here (TaskControlPlaneApiService), executing the state-machine
6
- * template pinned for the run; a pipeline without a valid template FAILS LOUD at selection. The old
7
- * hardcoded `developTask` workflow + its role→phase classifiers were removed.
8
- *
9
- * INVARIANT: `src/pipeline/*` imports NO `@dbos-inc/dbos-sdk` (M1 — DBOS sealed). All DBOS interaction
10
- * goes through the generic DbosService verbs (registerStep/registerWorkflow/awaitDecision) injected as
11
- * deps. This file is registered via the engine seam in PipelineService.
12
- *
13
- * GENERIC ENGINE: zero role-ids / pipeline shapes here. `roleRef`/`scriptRef` are opaque capability
14
- * handles resolved against the run's route bindings + the existing runner machinery (§1/§10).
15
- *
16
- * The loop (§10):
17
- * { state, decision } = core.step(template, state, lastResult)
18
- * → execute `decision` as a durable DBOS step (runner / human gate / integrator / fork)
19
- * → validate result vs the node's `resultSchema` + redact at this boundary (reuse runStep/append)
20
- * → record the result as the next `lastResult`
21
- * → repeat until `decision.type === 'complete'` → finish the run.
22
- *
23
- * RECOVERY: DBOS replays the workflow body; `core.step` is deterministic and every effect is a durable
24
- * memoized DBOS step, so a replay re-derives the identical Decision sequence and consumes the recorded
25
- * step results — no live race, no duplicate effects (deterministic ids + ROW_CONFLICT no-op).
26
- *
27
- * C1 architecture: the body is an extracted DBOS-free builder (`makeDataDrivenTask`) registered via the
28
- * engine seam, so tests exercise the SAME production logic directly with a plain step fn.
29
- */
30
- import { step as coreStep, initialState, validateTemplate, InterpretError, } from '../pipeline-core/index.js';
1
+ import { step as coreStep, initialState, validateTemplate, InterpretError, selectJoinWinner, reduceJoinVerdict, } from '../pipeline-core/index.js';
31
2
  import { runnerNeedsLivePreflight, runnerUsesRealIntegrator } from './route-contract.js';
32
3
  import { redactEventPayload } from '../run/append-event.js';
33
4
  import { redactSecrets } from '../control-plane/inbox.js';
@@ -35,7 +6,7 @@ import { fnv1a64Hex } from '../control-plane/steps.js';
35
6
  import { normalizeIssueRef } from '../run/issue-ref.js';
36
7
  import { RUNNER_IDLE_TIMEOUT_KIND, RUNNER_WALL_CLOCK_LIMIT_KIND, } from '../worker/process-executor.js';
37
8
  export const RUN_PROGRESS_EVENT_KEY = 'run-progress';
38
- const MAX_STEPS = 1_000; // a VALID template terminates; guards a data/loop authoring mistake at runtime.
9
+ const MAX_STEPS = 1_000;
39
10
  const DEFAULT_RUNNER_TRANSIENT_MAX_ATTEMPTS = 2;
40
11
  const DEFAULT_RUNNER_TRANSIENT_RETRY_BACKOFF_MS = 2_000;
41
12
  function readPositiveIntegerEnv(env, key, fallback) {
@@ -86,17 +57,6 @@ function optionalTiming(timing) {
86
57
  function optionalFailureKind(failureKind) {
87
58
  return failureKind === undefined ? {} : { failureKind };
88
59
  }
89
- /**
90
- * Reserved engine error codes (matched only by a node's `catch`, §3/§6).
91
- *
92
- * A built-in script (the integrator) has TWO distinct failure modes the routing data discriminates:
93
- * - `revo.ScriptBlocked` — the script needs a human (nothing-to-integrate, ambiguous PRs, a refused
94
- * pinned identity, a non-JSON `pr view`). NOT a crash; a `catch` routes it to a `blocked` terminal,
95
- * and the adapter surfaces the human-readable lesson on a `pipeline_blocked` event (parity with the
96
- * old engine's `blockPipeline({ reason:'integrate' })`).
97
- * - `revo.ScriptFailed` — the script THREW (a gh/push error). A `catch` routes it to a `failed`
98
- * terminal (parity with the old engine's top-level catch → failRun).
99
- */
100
60
  const REVO_SCRIPT_FAILED = 'revo.ScriptFailed';
101
61
  const REVO_SCRIPT_BLOCKED = 'revo.ScriptBlocked';
102
62
  const REVO_RESULT_INVALID = 'revo.ResultInvalid';
@@ -126,14 +86,9 @@ function pipelineBlockedPayload(reason, lesson, retry) {
126
86
  return { reason, lesson };
127
87
  return { ...retry };
128
88
  }
129
- /** Per-node execution stepKey: the bare nodeId on the first entry (stable ids for existing tests), an
130
- * ordinal-suffixed key on loop re-entries so attempts/events/outputs are distinct per iteration (0016
131
- * §4.1 — fixes the latent 0015 stepKey-reuse collision). */
132
89
  function stepKeyFor(nodeId, ordinal) {
133
90
  return ordinal <= 1 ? nodeId : `${nodeId}#${ordinal}`;
134
91
  }
135
- /** Next per-(run,node) execution ordinal (1-based). Deterministic on DBOS replay: the runBody loop
136
- * re-runs identically (coreStep is pure, effects are memoized), so the accumulator is rebuilt 1:1. */
137
92
  function nextOrdinal(byNode, nodeId) {
138
93
  const n = (byNode.get(nodeId) ?? 0) + 1;
139
94
  byNode.set(nodeId, n);
@@ -239,11 +194,6 @@ function transientRunnerFailure(result) {
239
194
  ...optionalTiming(output.timing),
240
195
  };
241
196
  }
242
- /**
243
- * Classify a transient runner failure into a SHORT, machine-readable kind so a consumer can tell
244
- * timeout / rate-limit / crash apart from `pipeline_blocked.payload.reason` alone (the full detail
245
- * stays in the lesson). Checked timeout-first so a "exited 1 (timeout)" message reads as a timeout.
246
- */
247
197
  function transientKind(reason) {
248
198
  if (/exceeded\s*\d+\s*ms|timed?\s*out|\btimeout\b/i.test(reason))
249
199
  return 'timeout';
@@ -261,7 +211,6 @@ function isLegacyRetryableCrashReason(reason) {
261
211
  return /\b(signal|crash(?:ed)?|killed|ECONNRESET|ETIMEDOUT|EAI_AGAIN|socket hang up|ENOMEM|EAGAIN)\b/i
262
212
  .test(reason);
263
213
  }
264
- /** Extract the DOMAIN verdict from the only supported source: top-level AttemptResult.verdict. */
265
214
  function domainVerdictOf(result) {
266
215
  if (typeof result.verdict === 'string' && result.verdict.trim().length > 0) {
267
216
  return result.verdict.trim().toLowerCase();
@@ -280,12 +229,6 @@ function resultVerdictProblem(template, node, result) {
280
229
  }
281
230
  return undefined;
282
231
  }
283
- /**
284
- * Validate a recorded result against a node's `resultSchema` at the ADAPTER boundary (§10). MVP
285
- * contract (resolves §14 Q3 — resultSchema is a DATA handle, validated structurally here): a declared
286
- * `resultSchema` requires a non-empty object/string output. Token redaction already happens inside
287
- * `runStep`/`appendEvent`; this guards a malformed effect result → `revo.ResultInvalid` (§6 precedence).
288
- */
289
232
  function resultSatisfiesSchema(node, result) {
290
233
  if (!('resultSchema' in node) || !node.resultSchema)
291
234
  return true;
@@ -306,28 +249,11 @@ function progressCursor(state, lastResult) {
306
249
  ...(lastResult ? { lastResult } : {}),
307
250
  };
308
251
  }
309
- /**
310
- * Map an approve/reject human decision onto a DOMAIN verdict the template's gate `outcomes` can route.
311
- * The core never sees approve/reject — it routes on a domain label (§8). Approve → the first declared
312
- * outcome (the "proceed" label by template convention, e.g. `approved`); reject → fail-closed to a
313
- * NON-first outcome if one exists (e.g. `changes_requested`), else undefined so the gate's `default`
314
- * (typically a `blocked` terminal) fires. This keeps gate semantics 100% in the routing data (§6/§8).
315
- */
316
252
  function gateVerdict(decision, outcomes) {
317
253
  if (decision.decision === 'approve')
318
254
  return outcomes[0];
319
- // reject: prefer a declared "rework/changes" outcome; else let the default branch route (blocked).
320
255
  return outcomes.length > 1 ? outcomes.at(-1) : undefined;
321
256
  }
322
- /**
323
- * Stable gate topic from a node's reason. The topic is the DBOS recv channel AND part of the gate
324
- * inbox id (`runId|topic`), so DISTINCT gates in one pipeline MUST map to DISTINCT topics — otherwise
325
- * a second gate's `recv` collides with the first's already-consumed message and the run hangs (plan
326
- * 0018: the `review-question` gate would otherwise reuse the `plan` topic of the plan gate).
327
- * merge-review → 'merge'
328
- * review-question→ 'question'
329
- * (anything else)→ 'plan' (the plan-review gate)
330
- */
331
257
  function gateTopicFor(reason) {
332
258
  if (/merge/i.test(reason))
333
259
  return 'merge';
@@ -335,15 +261,8 @@ function gateTopicFor(reason) {
335
261
  return 'question';
336
262
  return 'plan';
337
263
  }
338
- // D3 — enrich the gate inbox row with the artifact under review + the reviewer verdict, inline, so an
339
- // approver decides without digging the agent log. `pushInbox` has no size cap (unlike run-outputs'
340
- // PAYLOAD_MAX), so the artifact is budgeted HERE: over-budget → a head preview + a payload_ref locator,
341
- // never the full payload. The inbox id is keyed by runId|gateKey only, so a larger context does not
342
- // change it → pushInbox stays idempotent on replay. The payload is also secret/token redacted at this
343
- // build site (pushInbox only masks secret-NAMED keys, not token SHAPES in free-text) — see gateArtifactView.
344
264
  export const GATE_ARTIFACT_MAX = 16_000;
345
265
  export const GATE_PREVIEW_CHARS = 4_000;
346
- /** Latest (or pinned-ordinal) output row for a gate ref, or undefined if the producer has not run. */
347
266
  function resolveGateRow(ref, outputsByNode) {
348
267
  if (!ref)
349
268
  return undefined;
@@ -352,20 +271,12 @@ function resolveGateRow(ref, outputsByNode) {
352
271
  return undefined;
353
272
  if (typeof ref.iteration === 'number')
354
273
  return produced.find((o) => o.ordinal === ref.iteration);
355
- return produced[produced.length - 1]; // 'latest' (and 'all' → the most recent for an inline view)
274
+ return produced[produced.length - 1];
356
275
  }
357
- /**
358
- * Inline artifact view with the 16KB budget. Secrets + token shapes are scrubbed BEFORE inlining or
359
- * previewing — mirroring run-outputs.ts (the sibling persist boundary): pushInbox only masks
360
- * secret-NAMED keys, so a token shape (`ghp_…`) in a free-text field (e.g. pollPr's prFeedback) would
361
- * otherwise persist verbatim. Over-budget → a head preview (of the redacted serialization) + an
362
- * `attempt:` locator (the full artifact is recoverable from that attempt's agent log).
363
- */
364
276
  function gateArtifactView(row, as) {
365
277
  const base = { nodeId: row.nodeId, name: as ?? row.name, schemaRef: row.schemaRef };
366
278
  const safe = redactEventPayload(redactSecrets(row.payload) ?? null);
367
279
  const serialized = JSON.stringify(safe ?? null);
368
- // Budget on BYTES (not UTF-16 length) so a multi-byte payload can't slip past the cap.
369
280
  if (Buffer.byteLength(serialized, 'utf8') <= GATE_ARTIFACT_MAX)
370
281
  return { ...base, payload: safe };
371
282
  return {
@@ -375,12 +286,6 @@ function gateArtifactView(row, as) {
375
286
  payloadRef: `attempt:${row.attemptId ?? ''}`,
376
287
  };
377
288
  }
378
- /**
379
- * Build the enriched gate inbox summary from the workflow-local outputs (replay-safe — rebuilt
380
- * identically, 0016 §6). `verdictFrom` resolves a node's verdict output; when it is NOT specified, the
381
- * verdict defaults to the routing verdict that opened the gate (`lastVerdict`). Routing is unaffected —
382
- * purely informational.
383
- */
384
289
  export function buildGateSummary(decision, outputsByNode, lastVerdict) {
385
290
  const summary = { nodeId: decision.nodeId, outcomes: decision.outcomes };
386
291
  const artRow = resolveGateRow(decision.gatedArtifact, outputsByNode);
@@ -389,22 +294,12 @@ export function buildGateSummary(decision, outputsByNode, lastVerdict) {
389
294
  const verdictRow = resolveGateRow(decision.verdictFrom, outputsByNode);
390
295
  if (verdictRow)
391
296
  summary.reviewerVerdict = gateArtifactView(verdictRow);
392
- // Only fall back to the routing verdict when no verdictFrom was REQUESTED — a specified-but-unresolved
393
- // verdictFrom must not silently present the routing verdict as if it came from that source.
394
297
  else if (!decision.verdictFrom && lastVerdict)
395
298
  summary.reviewerVerdict = { verdict: lastVerdict };
396
299
  return summary;
397
300
  }
398
- /**
399
- * makeDataDrivenTask — DBOS-free factory for the dataDrivenTask async function (C1).
400
- *
401
- * Receives the (DBOS-wrapped in prod, plain in tests) `runStepFn` so the body is IDENTICAL across
402
- * production and tests. Returns a plain async function with the workflow signature.
403
- */
404
301
  export function makeDataDrivenTask(runStepFn, deps) {
405
302
  const { appendEvent, appendRunOutput, awaitHuman, completeRun, failRun, blockRun, loadRunTaskContext, integrateFn, runStub, confirmMergeFn, runConfirmStub, pollPrFn, runPollStub, respondThreadsFn, runRespondStub, captureChangeFn, preflightFn, createWorktreeFn, releaseWorktreeFn } = deps;
406
- /** Resolve a node's `consumes` from the workflow-local output accumulator — NOT live Revisium reads
407
- * (0016 §6 / consensus M4: a live read on replay can see rows written past the replay point). */
408
303
  function resolveConsumes(node, outputsByNode) {
409
304
  const refs = 'consumes' in node ? (node.consumes ?? []) : [];
410
305
  const inputs = {};
@@ -435,8 +330,6 @@ export function makeDataDrivenTask(runStepFn, deps) {
435
330
  }
436
331
  return { inputs };
437
332
  }
438
- /** Record a node's produced output to the workflow-local accumulator + Revisium (when `produces`). */
439
- // (gate-summary helpers live at module scope below — they are pure over outputsByNode.)
440
333
  async function recordOutput(runId, node, ordinal, attemptId, output, outputsByNode) {
441
334
  if (!('produces' in node) || !node.produces)
442
335
  return;
@@ -459,9 +352,6 @@ export function makeDataDrivenTask(runStepFn, deps) {
459
352
  return await runBody(runId, opts);
460
353
  }
461
354
  catch (err) {
462
- // TERMINAL failure: mark the Revisium run-row `failed` (idempotent, event-first) before
463
- // re-throwing so DBOS still records the workflow ERROR (progress truth) and the run-row stops
464
- // lying. The run's terminal-failure surfacing contract (0008 #2).
465
355
  const reason = err instanceof Error ? err.message : String(err);
466
356
  try {
467
357
  await failRun(runId, reason);
@@ -474,64 +364,25 @@ export function makeDataDrivenTask(runStepFn, deps) {
474
364
  };
475
365
  async function runBody(runId, opts) {
476
366
  const { route, template } = opts;
477
- // Defense-in-depth: the pinned template is validated at the boundary (§9 — pipeline-core is the
478
- // authoritative validator even though selection already validated it). A broken pin fails the run.
479
367
  const diagnostics = validateTemplate(template).filter((d) => d.severity === 'error');
480
368
  if (diagnostics.length > 0) {
481
369
  throw new Error(`PINNED_TEMPLATE_INVALID: ${template.pipelineId} — ${diagnostics.map((d) => d.code).join(', ')}`);
482
370
  }
483
371
  const { taskId, title, base, issueRef } = await loadRunTaskContext(runId);
484
- // B5/B7 — live preflight: one memoized DBOS step, evaluated exactly once BEFORE the graph runs.
485
- // Skipped entirely when every selected binding resolves to a stub/script runner (mirrors the old
486
- // engine's gate). A `needsHuman` preflight blocks the run (clean/base invariant unmet) and surfaces
487
- // the lesson — the run never enters the graph. This is an engine-level guard (infrastructure), not a
488
- // template node: it is cross-cutting and identical for every pinned pipeline (zero role-ids/shapes).
489
372
  const live = route.roleBindings.some((b) => runnerNeedsLivePreflight(b.resolvedRunnerId));
490
373
  if (live) {
491
- // Preflight runs against the BASE checkout (resolveTaskCwd), BEFORE the worktree exists — its
492
- // fetch + clean/base-availability checks protect the user's base repo. Ordering is load-bearing.
493
374
  const pf = await preflightFn(taskId, base);
494
375
  if ('needsHuman' in pf) {
495
376
  return await blockWithLesson(runId, taskId, 'preflight', pf.lesson, 0);
496
377
  }
497
378
  }
498
- // Per-run worktree (plan 0017): create AFTER a passing preflight and BEFORE any live effect, so all
499
- // repo-touching steps (developer/rework + integrator) resolve to the isolated worktree (keyed by
500
- // runId) — never the shared base checkout. Skipped for non-live runs. The `finally` releases it at a
501
- // SUCCEEDED terminal (the PR is merged via confirmMerge → branch is in base → worktree disposable) and
502
- // on a throw (failure); it KEEPS the worktree on a `blocked` terminal (confirmMerge blocks when the PR
503
- // isn't merged yet — the tree must survive for rework / a manual merge; reclaimed by cleanup_worktree
504
- // + the host-start sweep). It does NOT run while parked at a gate (awaitHuman suspends the live
505
- // workflow via `recv`, so runBody never returns there). Create is INSIDE the try so the release also
506
- // cleans up a create that partially built the worktree before throwing (codex/CodeRabbit).
507
- let result;
508
- try {
509
- if (live) {
510
- await createWorktreeFn(runId, taskId, title, base, issueRef);
511
- }
512
- result = await runGraph(runId, opts, taskId, title, base, issueRef, live);
513
- return result;
514
- }
515
- finally {
516
- if (live && result?.status !== 'blocked') {
517
- try {
518
- await releaseWorktreeFn(runId, taskId);
519
- }
520
- catch (releaseErr) {
521
- console.warn(`[data-driven] worktree release for ${runId} failed (orphan; pruned later): ${String(releaseErr)}`);
522
- }
523
- }
379
+ if (live) {
380
+ await createWorktreeFn(runId, taskId, title, base, issueRef);
524
381
  }
382
+ return runGraph(runId, opts, taskId, title, base, issueRef, live);
525
383
  }
526
- /** The pipeline-core interpretation loop, extracted so the worktree `finally` in runBody wraps it. */
527
384
  async function runGraph(runId, opts, taskId, title, base, issueRef, live) {
528
385
  const { route, template, runnerRetryPolicy } = opts;
529
- // Capability resolution map (GENERIC — no role-ids in the engine): roleRef/scriptRef → route binding.
530
- // The route's bindings are authoritative for runner dispatch (and durable on recovery via the DBOS
531
- // workflow input). A `role:<id>` handle resolves to the binding whose roleId matches `<id>`. A
532
- // `script:<id>` handle likewise resolves by roleId; additionally the canonical `script:integrator`
533
- // handle resolves to whichever binding's RESOLVED runner mechanically performs the merge (runner-
534
- // wins, D7) so the integrator script dispatches real-vs-stub exactly like the hardcoded path.
535
386
  const bindingByRef = new Map();
536
387
  for (const binding of route.roleBindings) {
537
388
  bindingByRef.set(`role:${binding.roleId}`, binding);
@@ -547,9 +398,6 @@ export function makeDataDrivenTask(runStepFn, deps) {
547
398
  let lastVerdict = '';
548
399
  let lastFailureReason = '';
549
400
  let stepCount = 0;
550
- // Workflow-local dataflow state (0016 §4.1/§6): per-node execution ordinals + produced outputs.
551
- // Both are rebuilt deterministically on DBOS replay (the loop re-runs identically); the adapter
552
- // hydrates consumers from `outputsByNode`, never from a live Revisium read.
553
401
  const effectOrdinalByNode = new Map();
554
402
  const outputsByNode = new Map();
555
403
  for (let i = 0; i < MAX_STEPS; i++) {
@@ -560,21 +408,19 @@ export function makeDataDrivenTask(runStepFn, deps) {
560
408
  return await finish(runId, decision.status, lastVerdict, stepCount, lastFailureReason);
561
409
  }
562
410
  const eff = await applyDecision(decision, {
563
- runId, template, bindingByRef, executionProfile, taskId, title, base, issueRef,
411
+ runId, template, state, bindingByRef, executionProfile, taskId, title, base, issueRef,
564
412
  effectOrdinalByNode, outputsByNode, runnerRetryPolicy,
565
413
  live,
566
- // The verdict from the PRIOR effect (the reviewer/poller that routed into this node). At an
567
- // awaitGate this is the routing verdict that opened the gate → D3's default reviewerVerdict.
568
414
  lastVerdict,
569
415
  });
570
416
  stepCount += eff.stepDelta;
571
- // Engine-level short-circuit (agent needsHuman, classified by invokeRole): block the run at a
572
- // visible, lesson-bearing terminal WITHOUT routing through the core's onFailure:'abort' machinery
573
- // (which would fail the run). blockWithLesson emits the single `pipeline_blocked` (token-redacted
574
- // at the persist boundary) + marks the run-row blocked — the same guard the preflight block uses.
575
417
  if (eff.terminal) {
576
418
  return await blockWithLesson(runId, taskId, eff.terminal.reason, eff.terminal.lesson, stepCount, eff.terminal.retry);
577
419
  }
420
+ if (eff.stateOverride) {
421
+ state = eff.stateOverride;
422
+ await deps.setProgress?.(runId, progressCursor(state, eff.lastResult));
423
+ }
578
424
  lastResult = eff.lastResult;
579
425
  if (eff.lastVerdict !== undefined)
580
426
  lastVerdict = eff.lastVerdict;
@@ -582,11 +428,91 @@ export function makeDataDrivenTask(runStepFn, deps) {
582
428
  }
583
429
  throw new InterpretError(`data-driven ${template.pipelineId} did not terminate within ${MAX_STEPS} steps (template loop bug)`);
584
430
  }
585
- /**
586
- * Execute ONE non-terminal Decision as a durable effect and return the next `lastResult`/`lastVerdict`
587
- * + the step-count delta. Extracted from the `runBody` loop so each is small + independently testable
588
- * (the loop just threads the result; this is the per-kind dispatch).
589
- */
431
+ function branchTemplateForJoin(template, joinId) {
432
+ const join = resolveNode(template, joinId);
433
+ if (join.kind !== 'join')
434
+ throw new InterpretError(`fork target ${joinId} is not a join (${join.kind})`);
435
+ return {
436
+ ...template,
437
+ nodes: {
438
+ ...template.nodes,
439
+ [joinId]: { id: joinId, kind: 'terminal', status: 'succeeded' },
440
+ },
441
+ };
442
+ }
443
+ async function executeForkBranches(decision, ctx) {
444
+ const branchTemplate = branchTemplateForJoin(ctx.template, decision.joinId);
445
+ const results = await Promise.all(decision.branches.map((branch, idx) => executeForkBranch(branchTemplate, decision, branch, idx + 1, ctx)));
446
+ const stepDelta = results.reduce((sum, result) => sum + result.stepDelta, 0);
447
+ const terminal = results.find((result) => result.terminal)?.terminal;
448
+ if (terminal)
449
+ return { lastResult: undefined, terminal, stepDelta };
450
+ const arrivals = results.flatMap((result) => result.arrival ? [result.arrival] : []);
451
+ if (arrivals.length !== decision.branches.length) {
452
+ throw new InterpretError(`fork ${decision.nodeId} expected ${decision.branches.length} branch arrivals, got ${arrivals.length}`);
453
+ }
454
+ const lastResult = { joinArrivals: arrivals };
455
+ const winner = selectJoinWinner(decision.mode, arrivals, decision.joinId);
456
+ const join = resolveNode(ctx.template, decision.joinId);
457
+ if (join.kind !== 'join')
458
+ throw new InterpretError(`fork target ${decision.joinId} is not a join (${join.kind})`);
459
+ const verdict = reduceJoinVerdict(join, arrivals, winner);
460
+ return {
461
+ lastResult,
462
+ ...(verdict !== undefined ? { lastVerdict: verdict } : {}),
463
+ stateOverride: {
464
+ ...ctx.state,
465
+ activeNodeIds: new Set([decision.joinId]),
466
+ status: 'running',
467
+ lastResult,
468
+ },
469
+ stepDelta,
470
+ };
471
+ }
472
+ async function executeForkBranch(branchTemplate, decision, branch, seq, ctx) {
473
+ let state = {
474
+ ...ctx.state,
475
+ activeNodeIds: new Set([branch.entry]),
476
+ scopedCounters: { ...ctx.state.scopedCounters },
477
+ status: 'running',
478
+ lastResult: undefined,
479
+ };
480
+ let lastResult;
481
+ let lastVerdict = '';
482
+ let stepDelta = 0;
483
+ for (let i = 0; i < MAX_STEPS; i++) {
484
+ const next = coreStep(branchTemplate, state, lastResult);
485
+ state = next.state;
486
+ if (next.decision.type === 'complete') {
487
+ if (next.decision.status !== 'succeeded') {
488
+ throw new InterpretError(`fork ${decision.nodeId} branch ${branch.id} completed ${next.decision.status} before join ${decision.joinId}`);
489
+ }
490
+ return {
491
+ arrival: {
492
+ branchId: branch.id,
493
+ seq,
494
+ ...(lastVerdict ? { verdict: lastVerdict } : {}),
495
+ },
496
+ stepDelta,
497
+ };
498
+ }
499
+ const eff = await applyDecision(next.decision, {
500
+ ...ctx,
501
+ template: branchTemplate,
502
+ state,
503
+ lastVerdict,
504
+ });
505
+ stepDelta += eff.stepDelta;
506
+ if (eff.terminal)
507
+ return { terminal: eff.terminal, stepDelta };
508
+ if (eff.stateOverride)
509
+ state = eff.stateOverride;
510
+ lastResult = eff.lastResult;
511
+ if (eff.lastVerdict !== undefined)
512
+ lastVerdict = eff.lastVerdict;
513
+ }
514
+ throw new InterpretError(`fork ${decision.nodeId} branch ${branch.id} did not reach join ${decision.joinId} within ${MAX_STEPS} steps`);
515
+ }
590
516
  async function applyDecision(decision, ctx) {
591
517
  const { runId, template, bindingByRef, taskId, title, base } = ctx;
592
518
  switch (decision.type) {
@@ -596,9 +522,6 @@ export function makeDataDrivenTask(runStepFn, deps) {
596
522
  const stepKey = stepKeyFor(node.id, ordinal);
597
523
  const resolved = resolveConsumes(node, ctx.outputsByNode);
598
524
  if ('missing' in resolved) {
599
- // A required upstream output is absent → fail-loud as a WIRING fault (0016 §6 / consensus M3):
600
- // a dedicated step_failed names the missing (node, as), distinct from a domain `blocker`. The
601
- // node's default onFailure='abort' then routes to a failed terminal (the run fails loud).
602
525
  await appendEvent({
603
526
  runId, taskId, stepId: '', stepKey, type: 'step_failed',
604
527
  payload: { nodeId: node.id, error: `${REVO_INPUT_MISSING}: required input ${resolved.missing} was not produced` },
@@ -607,9 +530,6 @@ export function makeDataDrivenTask(runStepFn, deps) {
607
530
  }
608
531
  const result = await invokeRole(runId, decision, node, ctx, resolved.inputs, stepKey);
609
532
  if ('blocked' in result) {
610
- // needsHuman (deliberate agent block OR transient runner failure): short-circuit to a visible
611
- // `blocked` terminal carrying the per-kind lesson (runGraph emits the pipeline_blocked + blocks
612
- // the run-row via blockWithLesson). NOT a failed/aborted run — the block is recoverable.
613
533
  return {
614
534
  lastResult: undefined,
615
535
  terminal: {
@@ -640,8 +560,6 @@ export function makeDataDrivenTask(runStepFn, deps) {
640
560
  case 'invokeScript': {
641
561
  const node = resolveNode(template, decision.nodeId);
642
562
  const ordinal = nextOrdinal(ctx.effectOrdinalByNode, node.id);
643
- // A script node may `consumes` upstream data (plan 0018: respondThreads ← triage). Hydrate it
644
- // from the workflow-local accumulator (same seam as agents), fail-loud on a missing required input.
645
563
  const resolved = resolveConsumes(node, ctx.outputsByNode);
646
564
  if ('missing' in resolved) {
647
565
  await appendEvent({
@@ -651,9 +569,6 @@ export function makeDataDrivenTask(runStepFn, deps) {
651
569
  return { lastResult: { outcome: 'failed', errorCode: REVO_INPUT_MISSING }, lastVerdict: 'failed', stepDelta: 1 };
652
570
  }
653
571
  const scriptResult = await invokeScript(runId, decision, { taskId, title, base, issueRef: ctx.issueRef }, bindingByRef, stepKeyFor(node.id, ordinal), resolved.inputs);
654
- // A blocked script (needsHuman) routes via revo.ScriptBlocked → a `blocked` terminal; a thrown
655
- // script routes via revo.ScriptFailed → a `failed` terminal (§6 catch). The lesson-bearing
656
- // pipeline_blocked is emitted inside invokeScript for the block path (parity with the old engine).
657
572
  if (scriptResult.outcome === 'blocked') {
658
573
  return { lastResult: { outcome: 'failed', errorCode: REVO_SCRIPT_BLOCKED }, lastVerdict: 'blocked', stepDelta: 1 };
659
574
  }
@@ -661,24 +576,17 @@ export function makeDataDrivenTask(runStepFn, deps) {
661
576
  return { lastResult: { outcome: 'failed', errorCode: REVO_SCRIPT_FAILED }, lastVerdict: 'failed', stepDelta: 1 };
662
577
  }
663
578
  await recordOutput(runId, node, ordinal, stepKeyFor(node.id, ordinal), scriptResult.pointer, ctx.outputsByNode);
664
- // A classifying script (pollPr, plan 0018) surfaces a DOMAIN verdict so the next `choice` can
665
- // route on it (§8). Scripts with a single fixed `next` (integrator/confirmMerge) carry none.
666
579
  const sv = scriptResult.verdict;
667
580
  return { lastResult: { outcome: 'succeeded', ...(sv ? { verdict: sv } : {}) }, ...(sv ? { lastVerdict: sv } : {}), stepDelta: 1 };
668
581
  }
669
582
  case 'awaitGate': {
670
583
  const topic = gateTopicFor(decision.reason);
671
- // Per-entry gate key (nodeId#ordinal) so a re-entered gate (e.g. a question gate looped in the
672
- // review phase) gets a DISTINCT inbox row instead of colliding on `runId|topic` (§3.2 audit).
673
584
  const ordinal = nextOrdinal(ctx.effectOrdinalByNode, decision.nodeId);
674
585
  const human = await awaitHuman(runId, topic, stepKeyFor(decision.nodeId, ordinal), `${decision.reason} approval`, buildGateSummary(decision, ctx.outputsByNode, ctx.lastVerdict));
675
586
  const verdict = gateVerdict(human, decision.outcomes);
676
587
  return { lastResult: verdict ? { verdict } : {}, ...(verdict ? { lastVerdict: verdict } : {}), stepDelta: 0 };
677
588
  }
678
589
  case 'fork': {
679
- // Fork/join is supported by the core; the MVP feature-development pipeline has none. Record a
680
- // deterministic barrier arrival per branch (verdict undefined) so an `all` join proceeds. A richer
681
- // concurrent-branch executor (DBOS child workflows) is a later slice (§14 Q1).
682
590
  await appendEvent({
683
591
  runId,
684
592
  taskId,
@@ -687,30 +595,19 @@ export function makeDataDrivenTask(runStepFn, deps) {
687
595
  type: 'pipeline_fork',
688
596
  payload: { nodeId: decision.nodeId, branches: decision.branches.map((b) => b.id), joinId: decision.joinId },
689
597
  });
690
- return {
691
- lastResult: { joinArrivals: decision.branches.map((b, idx) => ({ branchId: b.id, seq: idx + 1 })) },
692
- stepDelta: 0,
693
- };
598
+ return executeForkBranches(decision, ctx);
694
599
  }
695
600
  case 'startTimer':
696
- // `wait` nodes are rare (§1) and unused by the MVP templates; a durable timer executor is a later
697
- // slice. Treat as an immediate (recorded) resume so a template using it still advances.
698
601
  return { lastResult: {}, stepDelta: 0 };
699
602
  }
700
603
  }
701
- // ── Effect executors ─────────────────────────────────────────────────────────
702
- /** invokeRole → dispatch via the existing runStep DBOS step; validate result vs resultSchema. */
703
604
  async function invokeRole(runId, decision, node, ctx, inputs, stepKey) {
704
605
  const binding = resolveRoleBinding(ctx, decision);
705
- // stepKey is ordinal-aware (0016 §4.1): distinct per loop iteration, stable across replay. The
706
- // hydrated `inputs` (consumed upstream outputs) ride in stepInput → the runner renders them as a
707
- // `## Inputs (from previous steps)` prompt section (build-context).
708
606
  return invokeRoleAttempts({ runId, decision, node, ctx, inputs, stepKey, binding });
709
607
  }
710
608
  function resolveRoleBinding(ctx, decision) {
711
609
  const binding = ctx.bindingByRef.get(decision.roleRef);
712
610
  if (binding === undefined) {
713
- // A VALID template's caps resolve at run start; an unresolved roleRef is a fatal config gap.
714
611
  throw new Error(`CAPABILITY_UNRESOLVED: roleRef ${decision.roleRef} has no route binding`);
715
612
  }
716
613
  return binding;
@@ -721,7 +618,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
721
618
  for (let attemptNo = 1; attemptNo <= ctx.runnerRetryPolicy.maxAttempts; attemptNo++) {
722
619
  const physicalAttempt = physicalAttemptFor(runId, stepKey, attemptNo);
723
620
  attemptIds.push(physicalAttempt.attemptId);
724
- const result = await runStepFn(runId, binding.rowId, stepKey, stepInputForAttempt(decision.nodeId, inputs, physicalAttempt), binding.resolvedRunnerId, ctx.executionProfile, physicalAttempt);
621
+ const result = await runStepFn(runId, binding.rowId, stepKey, stepInputForAttempt(decision.nodeId, inputs, physicalAttempt), binding.resolvedRunnerId, ctx.executionProfile, physicalAttempt, ctx.template.verdicts.domain);
725
622
  const needsHuman = await maybeHandleNeedsHumanRoleResult({
726
623
  ...input,
727
624
  attemptIds,
@@ -768,12 +665,6 @@ export function makeDataDrivenTask(runStepFn, deps) {
768
665
  async function maybeHandleNeedsHumanRoleResult(input) {
769
666
  const { result, node, physicalAttempt } = input;
770
667
  if (result.needsHuman) {
771
- // A `needsHuman` result is NOT a wiring fault — it is a recoverable human-block. Route it to a
772
- // visible `blocked` terminal (like a blocked SCRIPT), never a ResultInvalid abort (which permanently
773
- // kills the run). TWO distinct things produce needsHuman; both block, but the human must see WHICH:
774
- // - DELIBERATE: the agent self-reported `needsHuman:true` (the result-envelope contract). It carries
775
- // the agent's own `output` + `lesson` — a genuine "I need a human decision".
776
- // - TRANSIENT: runStep wraps a runner-process failure as `output.error === 'runner_failed'`.
777
668
  const transient = transientRunnerFailure(result);
778
669
  if (transient === undefined) {
779
670
  const safeLesson = String(redactEventPayload(result.lesson ?? `agent ${node.id} reported needsHuman`));
@@ -896,26 +787,19 @@ export function makeDataDrivenTask(runStepFn, deps) {
896
787
  },
897
788
  });
898
789
  }
899
- /**
900
- * invokeScript → the built-in system SCRIPT library. The only built-in v1 script is the integrator
901
- * (`script:integrator`); it dispatches to the real integrator (live) or the stub (script) exactly as
902
- * the hardcoded path does — runner-wins via the resolved binding (D7).
903
- *
904
- * Two distinct outcomes the routing data discriminates (parity with the old engine):
905
- * - `blocked`: the integrator needs a human (nothing to integrate, ambiguous PRs, refused identity,
906
- * non-JSON pr view). We emit a `pipeline_blocked` event carrying the human-readable lesson (so the
907
- * human sees WHY — the persist boundary redacts any token), then the node's `catch[revo.ScriptBlocked]`
908
- * routes to a `blocked` terminal.
909
- * - `failed`: the integrator THREW (a gh/push error). `catch[revo.ScriptFailed]` routes to a `failed`
910
- * terminal; the run's top-level catch failRuns (the thrown reason is recorded, redacted).
911
- */
912
790
  async function invokeScript(runId, decision, ctx, bindingByRef, stepKey, inputs) {
791
+ if (decision.scriptRef === 'script:cleanupWorktree') {
792
+ try {
793
+ await releaseWorktreeFn(runId, ctx.taskId);
794
+ }
795
+ catch { /* best-effort */ }
796
+ await appendEvent({ runId, taskId: ctx.taskId, stepId: '', stepKey, type: 'worktree_released', payload: { nodeId: decision.nodeId } });
797
+ return { outcome: 'ok', pointer: { released: true } };
798
+ }
913
799
  const isConfirmMerge = decision.scriptRef === 'script:confirmMerge';
914
800
  const isPollPr = decision.scriptRef === 'script:pollPr';
915
801
  const isRespondThreads = decision.scriptRef === 'script:respondThreads';
916
802
  const binding = bindingByRef.get(decision.scriptRef) ?? bindingByRef.get('script:integrator');
917
- // respondThreads consumes `triage` (plan 0018) — ride the hydrated input on the integrator input so
918
- // the live script can reply/resolve the triaged threads without a live Revisium read.
919
803
  const change = producedChangeFromInputs(inputs);
920
804
  const mergeReadiness = mergeReadinessFromInputs(inputs);
921
805
  const issueRef = ctx.issueRef;
@@ -930,8 +814,6 @@ export function makeDataDrivenTask(runStepFn, deps) {
930
814
  ...(inputs.triage === undefined ? {} : { triage: inputs.triage }),
931
815
  ...(mergeReadiness ? { mergeReadiness } : {}),
932
816
  };
933
- // A script node whose resolved runner mechanically performs the merge uses the REAL script;
934
- // otherwise the pure stub (zero git/gh). Absent a binding (template-only script), default to stub.
935
817
  const useReal = !!binding && runnerUsesRealIntegrator(binding.resolvedRunnerId);
936
818
  let result;
937
819
  try {
@@ -960,8 +842,6 @@ export function makeDataDrivenTask(runStepFn, deps) {
960
842
  return { outcome: 'failed' };
961
843
  }
962
844
  if ('needsHuman' in result) {
963
- // Surface the blocking reason as pipeline_blocked (the persist boundary redacts any token, D15) so
964
- // the human sees WHY the script could not proceed — exactly as the old engine's blockPipeline.
965
845
  const reason = isConfirmMerge ? 'confirm-merge' : isPollPr ? 'poll-pr' : isRespondThreads ? 'respond-threads' : 'integrate';
966
846
  await appendEvent({
967
847
  runId,
@@ -998,8 +878,6 @@ export function makeDataDrivenTask(runStepFn, deps) {
998
878
  };
999
879
  }
1000
880
  if (isPollPr) {
1001
- // pollPr CLASSIFIES the feedback: its verdict (review_changes/ci_changes/clean) routes the prRouter
1002
- // choice (§8 — a script may emit a domain verdict the routing data acts on, same as an agent).
1003
881
  const feedback = result;
1004
882
  await appendEvent({
1005
883
  runId,
@@ -1059,7 +937,6 @@ export function makeDataDrivenTask(runStepFn, deps) {
1059
937
  },
1060
938
  };
1061
939
  }
1062
- /** Terminal: finish the run in Revisium per the core's terminal status. */
1063
940
  async function finish(runId, status, verdict, steps, failureReason = '') {
1064
941
  if (status === 'succeeded') {
1065
942
  await completeRun(runId, { actor: 'pipeline', source: 'data-driven-complete', verdict, iterations: steps });
@@ -1080,12 +957,6 @@ export function makeDataDrivenTask(runStepFn, deps) {
1080
957
  }
1081
958
  return { runId, status, verdict, steps };
1082
959
  }
1083
- /**
1084
- * Block the run early with a human-readable lesson (the engine-level preflight guard — there is no
1085
- * graph node for it). Emits a lesson-bearing `pipeline_blocked` (token-redacted at the persist
1086
- * boundary) + marks the Revisium run-row blocked, then returns a `blocked` result WITHOUT entering the
1087
- * graph. Mirrors the old engine's `blockPipeline({ reason })`.
1088
- */
1089
960
  async function blockWithLesson(runId, taskId, reason, lesson, steps, retry) {
1090
961
  await appendEvent({
1091
962
  runId,
@@ -1099,7 +970,6 @@ export function makeDataDrivenTask(runStepFn, deps) {
1099
970
  return { runId, status: 'blocked', verdict: 'blocked', steps };
1100
971
  }
1101
972
  }
1102
- /** Resolve a node id against the template (a VALID template never dangles — guarded defensively). */
1103
973
  function resolveNode(template, nodeId) {
1104
974
  const node = template.nodes[nodeId];
1105
975
  if (!node)