@revisium/orchestrator 0.1.0 → 0.2.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -0
- package/control-plane/bootstrap.config.json +43 -1
- package/control-plane/default-playbook/catalog/pipelines.json +1250 -7
- package/control-plane/default-playbook/catalog/roles.json +54 -0
- package/dist/api/graphql-api/graphql-ws/pubsub.module.js +0 -1
- package/dist/api/graphql-api/graphql-ws/pubsub.module.js.map +1 -1
- package/dist/api/graphql-api/graphql-ws/run-progress-subscription-poller.service.js +0 -1
- package/dist/api/graphql-api/graphql-ws/run-progress-subscription-poller.service.js.map +1 -1
- package/dist/api/graphql-api/graphql-ws/subscription-bridge.service.js +0 -1
- package/dist/api/graphql-api/graphql-ws/subscription-bridge.service.js.map +1 -1
- package/dist/api/graphql-api/inbox/inbox.resolver.js +12 -0
- package/dist/api/graphql-api/inbox/inbox.resolver.js.map +1 -1
- package/dist/api/graphql-api/inbox/inputs/resolve-gate.input.js +37 -0
- package/dist/api/graphql-api/inbox/inputs/resolve-gate.input.js.map +1 -0
- package/dist/api/graphql-api/pr/model/pr-readiness.model.js +0 -4
- package/dist/api/graphql-api/pr/model/pr-readiness.model.js.map +1 -1
- package/dist/cli/commands/doctor-report.js +0 -18
- package/dist/cli/commands/doctor-report.js.map +1 -1
- package/dist/cli/commands/lifecycle.js +3 -71
- package/dist/cli/commands/lifecycle.js.map +1 -1
- package/dist/cli/commands/mcp.js +0 -14
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/process-tree.js +4 -26
- package/dist/cli/commands/process-tree.js.map +1 -1
- package/dist/cli/commands/revisium-helpers.js +0 -6
- package/dist/cli/commands/revisium-helpers.js.map +1 -1
- package/dist/cli/commands/rogue-reaper.js +0 -34
- package/dist/cli/commands/rogue-reaper.js.map +1 -1
- package/dist/cli/index.js +0 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/program.js +0 -5
- package/dist/cli/program.js.map +1 -1
- package/dist/config.js +0 -25
- package/dist/config.js.map +1 -1
- package/dist/control-plane/bootstrap.js +0 -28
- package/dist/control-plane/bootstrap.js.map +1 -1
- package/dist/control-plane/change-notifications.js +0 -1
- package/dist/control-plane/change-notifications.js.map +1 -1
- package/dist/control-plane/client-transport.js +0 -11
- package/dist/control-plane/client-transport.js.map +1 -1
- package/dist/control-plane/default-playbook-policy.js +64 -4
- package/dist/control-plane/default-playbook-policy.js.map +1 -1
- package/dist/control-plane/definitions.js +8 -17
- package/dist/control-plane/definitions.js.map +1 -1
- package/dist/control-plane/inbox.js +2 -120
- package/dist/control-plane/inbox.js.map +1 -1
- package/dist/control-plane/resolve-cwd.js +0 -61
- package/dist/control-plane/resolve-cwd.js.map +1 -1
- package/dist/control-plane/schema-migration.js +0 -9
- package/dist/control-plane/schema-migration.js.map +1 -1
- package/dist/control-plane/seed-default-playbook.js +5 -70
- package/dist/control-plane/seed-default-playbook.js.map +1 -1
- package/dist/control-plane/steps.js +0 -15
- package/dist/control-plane/steps.js.map +1 -1
- package/dist/e2e/kit/assertions.js +1 -1
- package/dist/e2e/kit/assertions.js.map +1 -1
- package/dist/engine/dbos.service.js +9 -179
- package/dist/engine/dbos.service.js.map +1 -1
- package/dist/engine/ensure-postgres.js +0 -48
- package/dist/engine/ensure-postgres.js.map +1 -1
- package/dist/features/inbox/commands/handlers/inbox-command.handlers.js +16 -0
- package/dist/features/inbox/commands/handlers/inbox-command.handlers.js.map +1 -1
- package/dist/features/inbox/commands/impl/resolve-gate.command.js +7 -0
- package/dist/features/inbox/commands/impl/resolve-gate.command.js.map +1 -0
- package/dist/features/inbox/commands/index.js +2 -1
- package/dist/features/inbox/commands/index.js.map +1 -1
- package/dist/features/inbox/inbox-api.service.js +4 -0
- package/dist/features/inbox/inbox-api.service.js.map +1 -1
- package/dist/host/daemon.js +2 -21
- package/dist/host/daemon.js.map +1 -1
- package/dist/host/dbos-identity.js +0 -31
- package/dist/host/dbos-identity.js.map +1 -1
- package/dist/host/ensure-host.js +1 -37
- package/dist/host/ensure-host.js.map +1 -1
- package/dist/host/ensure-revisium.js +1 -135
- package/dist/host/ensure-revisium.js.map +1 -1
- package/dist/host/host-runtime.js +0 -28
- package/dist/host/host-runtime.js.map +1 -1
- package/dist/host/host.lifecycle.js +0 -27
- package/dist/host/host.lifecycle.js.map +1 -1
- package/dist/host/queue-ownership.js +0 -42
- package/dist/host/queue-ownership.js.map +1 -1
- package/dist/host/queue-poller-census.js +1 -28
- package/dist/host/queue-poller-census.js.map +1 -1
- package/dist/http/graphql-host.module.js +0 -3
- package/dist/http/graphql-host.module.js.map +1 -1
- package/dist/mcp/mcp-capabilities.js +20 -14
- package/dist/mcp/mcp-capabilities.js.map +1 -1
- package/dist/mcp/mcp-facade.service.js +44 -23
- package/dist/mcp/mcp-facade.service.js.map +1 -1
- package/dist/mcp/mcp-http.service.js +0 -18
- package/dist/mcp/mcp-http.service.js.map +1 -1
- package/dist/mcp/mcp-tools.js +28 -40
- package/dist/mcp/mcp-tools.js.map +1 -1
- package/dist/observability/activity-signal.js +0 -4
- package/dist/observability/activity-signal.js.map +1 -1
- package/dist/observability/agent-activity-reporter.js +0 -4
- package/dist/observability/agent-activity-reporter.js.map +1 -1
- package/dist/pipeline/await-human.js +13 -43
- package/dist/pipeline/await-human.js.map +1 -1
- package/dist/pipeline/data-driven-task.workflow.js +121 -238
- package/dist/pipeline/data-driven-task.workflow.js.map +1 -1
- package/dist/pipeline/data-driven-template.js +0 -25
- package/dist/pipeline/data-driven-template.js.map +1 -1
- package/dist/pipeline/pipeline.module.js +0 -12
- package/dist/pipeline/pipeline.module.js.map +1 -1
- package/dist/pipeline/pipeline.service.js +10 -99
- package/dist/pipeline/pipeline.service.js.map +1 -1
- package/dist/pipeline-core/index.js +1 -13
- package/dist/pipeline-core/index.js.map +1 -1
- package/dist/pipeline-core/interpret.js +21 -94
- package/dist/pipeline-core/interpret.js.map +1 -1
- package/dist/pipeline-core/kit/assertions.js +0 -19
- package/dist/pipeline-core/kit/assertions.js.map +1 -1
- package/dist/pipeline-core/kit/builders.js +2 -21
- package/dist/pipeline-core/kit/builders.js.map +1 -1
- package/dist/pipeline-core/kit/drive.js +1 -31
- package/dist/pipeline-core/kit/drive.js.map +1 -1
- package/dist/pipeline-core/kit/fixtures.js +26 -86
- package/dist/pipeline-core/kit/fixtures.js.map +1 -1
- package/dist/pipeline-core/kit/index.js +0 -8
- package/dist/pipeline-core/kit/index.js.map +1 -1
- package/dist/pipeline-core/types.js +13 -52
- package/dist/pipeline-core/types.js.map +1 -1
- package/dist/pipeline-core/validate-capability.js +0 -3
- package/dist/pipeline-core/validate-capability.js.map +1 -1
- package/dist/pipeline-core/validate-conflicts.js +0 -9
- package/dist/pipeline-core/validate-conflicts.js.map +1 -1
- package/dist/pipeline-core/validate-dataflow.js +2 -21
- package/dist/pipeline-core/validate-dataflow.js.map +1 -1
- package/dist/pipeline-core/validate-diff.js +3 -36
- package/dist/pipeline-core/validate-diff.js.map +1 -1
- package/dist/pipeline-core/validate-edges.js +0 -6
- package/dist/pipeline-core/validate-edges.js.map +1 -1
- package/dist/pipeline-core/validate-graph.js +0 -11
- package/dist/pipeline-core/validate-graph.js.map +1 -1
- package/dist/pipeline-core/validate-loops.js +37 -44
- package/dist/pipeline-core/validate-loops.js.map +1 -1
- package/dist/pipeline-core/validate-parallel.js +9 -15
- package/dist/pipeline-core/validate-parallel.js.map +1 -1
- package/dist/pipeline-core/validate-sink.js +0 -7
- package/dist/pipeline-core/validate-sink.js.map +1 -1
- package/dist/pipeline-core/validate-topology.js +1 -20
- package/dist/pipeline-core/validate-topology.js.map +1 -1
- package/dist/pipeline-core/validate-verdict.js +52 -7
- package/dist/pipeline-core/validate-verdict.js.map +1 -1
- package/dist/pipeline-core/validate.js +5 -29
- package/dist/pipeline-core/validate.js.map +1 -1
- package/dist/playbook/catalog-loader.js +8 -3
- package/dist/playbook/catalog-loader.js.map +1 -1
- package/dist/playbook/import-mapper.js +1 -9
- package/dist/playbook/import-mapper.js.map +1 -1
- package/dist/poller/pr-readiness-core.js +0 -74
- package/dist/poller/pr-readiness-core.js.map +1 -1
- package/dist/revisium/inbox.service.js +0 -24
- package/dist/revisium/inbox.service.js.map +1 -1
- package/dist/revisium/playbooks.service.js +2 -4
- package/dist/revisium/playbooks.service.js.map +1 -1
- package/dist/revisium/revisium.module.js +0 -12
- package/dist/revisium/revisium.module.js.map +1 -1
- package/dist/revisium/roles.service.js +0 -6
- package/dist/revisium/roles.service.js.map +1 -1
- package/dist/revisium/run.service.js +1 -75
- package/dist/revisium/run.service.js.map +1 -1
- package/dist/revisium/tokens.js +0 -4
- package/dist/revisium/tokens.js.map +1 -1
- package/dist/run/append-event.js +0 -34
- package/dist/run/append-event.js.map +1 -1
- package/dist/run/block-run.js.map +1 -1
- package/dist/run/cancel-run.js +0 -33
- package/dist/run/cancel-run.js.map +1 -1
- package/dist/run/complete-run.js.map +1 -1
- package/dist/run/create-run.js +0 -7
- package/dist/run/create-run.js.map +1 -1
- package/dist/run/fail-run.js +0 -3
- package/dist/run/fail-run.js.map +1 -1
- package/dist/run/inspect-run.js +0 -26
- package/dist/run/inspect-run.js.map +1 -1
- package/dist/run/run-outputs.js +0 -19
- package/dist/run/run-outputs.js.map +1 -1
- package/dist/run/terminal-run-status.js +0 -12
- package/dist/run/terminal-run-status.js.map +1 -1
- package/dist/runners/claude-code.service.js +0 -11
- package/dist/runners/claude-code.service.js.map +1 -1
- package/dist/runners/codex.service.js +0 -9
- package/dist/runners/codex.service.js.map +1 -1
- package/dist/runners/gh-identity.js +0 -52
- package/dist/runners/gh-identity.js.map +1 -1
- package/dist/runners/integrator-branch-naming.js +3 -11
- package/dist/runners/integrator-branch-naming.js.map +1 -1
- package/dist/runners/integrator-git.js +0 -25
- package/dist/runners/integrator-git.js.map +1 -1
- package/dist/runners/integrator-remote.js +0 -2
- package/dist/runners/integrator-remote.js.map +1 -1
- package/dist/runners/integrator-types.js +0 -7
- package/dist/runners/integrator-types.js.map +1 -1
- package/dist/runners/integrator.js +44 -176
- package/dist/runners/integrator.js.map +1 -1
- package/dist/runners/runner.module.js +0 -6
- package/dist/runners/runner.module.js.map +1 -1
- package/dist/runners/tokens.js +0 -4
- package/dist/runners/tokens.js.map +1 -1
- package/dist/runners/worktree.service.js +1 -17
- package/dist/runners/worktree.service.js.map +1 -1
- package/dist/task-control-plane/run-watch.service.js +50 -174
- package/dist/task-control-plane/run-watch.service.js.map +1 -1
- package/dist/task-control-plane/task-control-plane-api.service.js +134 -36
- package/dist/task-control-plane/task-control-plane-api.service.js.map +1 -1
- package/dist/worker/build-context.js +0 -16
- package/dist/worker/build-context.js.map +1 -1
- package/dist/worker/claude-code-runner.js +9 -42
- package/dist/worker/claude-code-runner.js.map +1 -1
- package/dist/worker/codex-runner.js +88 -22
- package/dist/worker/codex-runner.js.map +1 -1
- package/dist/worker/git-worktree-manager.js +20 -67
- package/dist/worker/git-worktree-manager.js.map +1 -1
- package/dist/worker/process-executor.js +0 -12
- package/dist/worker/process-executor.js.map +1 -1
- package/dist/worker/result-envelope.js +33 -49
- package/dist/worker/result-envelope.js.map +1 -1
- package/dist/worker/runner-dispatch.js +0 -3
- package/dist/worker/runner-dispatch.js.map +1 -1
- package/dist/worker/runner.js.map +1 -1
- package/dist/worker/stub-runner.js +0 -7
- package/dist/worker/stub-runner.js.map +1 -1
- 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;
|
|
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);
|
|
@@ -181,7 +136,7 @@ function changeWithRunIssueRef(change, issueRef) {
|
|
|
181
136
|
return normalized;
|
|
182
137
|
}
|
|
183
138
|
function producedChangeFromInputs(inputs) {
|
|
184
|
-
for (const key of ['reviewChange', 'ciChange', 'reworkChange', 'developerChange', 'change']) {
|
|
139
|
+
for (const key of ['reviewChange', 'ciChange', 'stuckReworkChange', 'reworkChange', 'developerChange', 'change']) {
|
|
185
140
|
const artifact = producedChangeArtifact(inputs[key]);
|
|
186
141
|
if (artifact)
|
|
187
142
|
return artifact;
|
|
@@ -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,23 @@ 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) {
|
|
253
|
+
if (decision.outcome)
|
|
254
|
+
return outcomes.includes(decision.outcome) ? decision.outcome : undefined;
|
|
317
255
|
if (decision.decision === 'approve')
|
|
318
256
|
return outcomes[0];
|
|
319
|
-
// reject: prefer a declared "rework/changes" outcome; else let the default branch route (blocked).
|
|
320
257
|
return outcomes.length > 1 ? outcomes.at(-1) : undefined;
|
|
321
258
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
259
|
+
function gateResolutionOutput(decision, verdict, fallbackInboxId) {
|
|
260
|
+
return {
|
|
261
|
+
outcome: decision.outcome ?? verdict ?? decision.decision,
|
|
262
|
+
...(decision.note !== undefined ? { note: decision.note } : {}),
|
|
263
|
+
resolvedBy: decision.resolvedBy ?? '',
|
|
264
|
+
resolvedAt: decision.resolvedAt ?? '',
|
|
265
|
+
inboxId: decision.inboxId ?? fallbackInboxId,
|
|
266
|
+
...(decision.decision ? { decision: decision.decision } : {}),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
331
269
|
function gateTopicFor(reason) {
|
|
332
270
|
if (/merge/i.test(reason))
|
|
333
271
|
return 'merge';
|
|
@@ -335,15 +273,8 @@ function gateTopicFor(reason) {
|
|
|
335
273
|
return 'question';
|
|
336
274
|
return 'plan';
|
|
337
275
|
}
|
|
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
276
|
export const GATE_ARTIFACT_MAX = 16_000;
|
|
345
277
|
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
278
|
function resolveGateRow(ref, outputsByNode) {
|
|
348
279
|
if (!ref)
|
|
349
280
|
return undefined;
|
|
@@ -352,20 +283,12 @@ function resolveGateRow(ref, outputsByNode) {
|
|
|
352
283
|
return undefined;
|
|
353
284
|
if (typeof ref.iteration === 'number')
|
|
354
285
|
return produced.find((o) => o.ordinal === ref.iteration);
|
|
355
|
-
return produced[produced.length - 1];
|
|
286
|
+
return produced[produced.length - 1];
|
|
356
287
|
}
|
|
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
288
|
function gateArtifactView(row, as) {
|
|
365
289
|
const base = { nodeId: row.nodeId, name: as ?? row.name, schemaRef: row.schemaRef };
|
|
366
290
|
const safe = redactEventPayload(redactSecrets(row.payload) ?? null);
|
|
367
291
|
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
292
|
if (Buffer.byteLength(serialized, 'utf8') <= GATE_ARTIFACT_MAX)
|
|
370
293
|
return { ...base, payload: safe };
|
|
371
294
|
return {
|
|
@@ -375,12 +298,6 @@ function gateArtifactView(row, as) {
|
|
|
375
298
|
payloadRef: `attempt:${row.attemptId ?? ''}`,
|
|
376
299
|
};
|
|
377
300
|
}
|
|
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
301
|
export function buildGateSummary(decision, outputsByNode, lastVerdict) {
|
|
385
302
|
const summary = { nodeId: decision.nodeId, outcomes: decision.outcomes };
|
|
386
303
|
const artRow = resolveGateRow(decision.gatedArtifact, outputsByNode);
|
|
@@ -389,22 +306,12 @@ export function buildGateSummary(decision, outputsByNode, lastVerdict) {
|
|
|
389
306
|
const verdictRow = resolveGateRow(decision.verdictFrom, outputsByNode);
|
|
390
307
|
if (verdictRow)
|
|
391
308
|
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
309
|
else if (!decision.verdictFrom && lastVerdict)
|
|
395
310
|
summary.reviewerVerdict = { verdict: lastVerdict };
|
|
396
311
|
return summary;
|
|
397
312
|
}
|
|
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
313
|
export function makeDataDrivenTask(runStepFn, deps) {
|
|
405
314
|
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
315
|
function resolveConsumes(node, outputsByNode) {
|
|
409
316
|
const refs = 'consumes' in node ? (node.consumes ?? []) : [];
|
|
410
317
|
const inputs = {};
|
|
@@ -435,8 +342,6 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
435
342
|
}
|
|
436
343
|
return { inputs };
|
|
437
344
|
}
|
|
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
345
|
async function recordOutput(runId, node, ordinal, attemptId, output, outputsByNode) {
|
|
441
346
|
if (!('produces' in node) || !node.produces)
|
|
442
347
|
return;
|
|
@@ -459,9 +364,6 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
459
364
|
return await runBody(runId, opts);
|
|
460
365
|
}
|
|
461
366
|
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
367
|
const reason = err instanceof Error ? err.message : String(err);
|
|
466
368
|
try {
|
|
467
369
|
await failRun(runId, reason);
|
|
@@ -474,64 +376,25 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
474
376
|
};
|
|
475
377
|
async function runBody(runId, opts) {
|
|
476
378
|
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
379
|
const diagnostics = validateTemplate(template).filter((d) => d.severity === 'error');
|
|
480
380
|
if (diagnostics.length > 0) {
|
|
481
381
|
throw new Error(`PINNED_TEMPLATE_INVALID: ${template.pipelineId} — ${diagnostics.map((d) => d.code).join(', ')}`);
|
|
482
382
|
}
|
|
483
383
|
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
384
|
const live = route.roleBindings.some((b) => runnerNeedsLivePreflight(b.resolvedRunnerId));
|
|
490
385
|
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
386
|
const pf = await preflightFn(taskId, base);
|
|
494
387
|
if ('needsHuman' in pf) {
|
|
495
388
|
return await blockWithLesson(runId, taskId, 'preflight', pf.lesson, 0);
|
|
496
389
|
}
|
|
497
390
|
}
|
|
498
|
-
|
|
499
|
-
|
|
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
|
-
}
|
|
391
|
+
if (live) {
|
|
392
|
+
await createWorktreeFn(runId, taskId, title, base, issueRef);
|
|
524
393
|
}
|
|
394
|
+
return runGraph(runId, opts, taskId, title, base, issueRef, live);
|
|
525
395
|
}
|
|
526
|
-
/** The pipeline-core interpretation loop, extracted so the worktree `finally` in runBody wraps it. */
|
|
527
396
|
async function runGraph(runId, opts, taskId, title, base, issueRef, live) {
|
|
528
397
|
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
398
|
const bindingByRef = new Map();
|
|
536
399
|
for (const binding of route.roleBindings) {
|
|
537
400
|
bindingByRef.set(`role:${binding.roleId}`, binding);
|
|
@@ -547,9 +410,6 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
547
410
|
let lastVerdict = '';
|
|
548
411
|
let lastFailureReason = '';
|
|
549
412
|
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
413
|
const effectOrdinalByNode = new Map();
|
|
554
414
|
const outputsByNode = new Map();
|
|
555
415
|
for (let i = 0; i < MAX_STEPS; i++) {
|
|
@@ -560,21 +420,19 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
560
420
|
return await finish(runId, decision.status, lastVerdict, stepCount, lastFailureReason);
|
|
561
421
|
}
|
|
562
422
|
const eff = await applyDecision(decision, {
|
|
563
|
-
runId, template, bindingByRef, executionProfile, taskId, title, base, issueRef,
|
|
423
|
+
runId, template, state, bindingByRef, executionProfile, taskId, title, base, issueRef,
|
|
564
424
|
effectOrdinalByNode, outputsByNode, runnerRetryPolicy,
|
|
565
425
|
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
426
|
lastVerdict,
|
|
569
427
|
});
|
|
570
428
|
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
429
|
if (eff.terminal) {
|
|
576
430
|
return await blockWithLesson(runId, taskId, eff.terminal.reason, eff.terminal.lesson, stepCount, eff.terminal.retry);
|
|
577
431
|
}
|
|
432
|
+
if (eff.stateOverride) {
|
|
433
|
+
state = eff.stateOverride;
|
|
434
|
+
await deps.setProgress?.(runId, progressCursor(state, eff.lastResult));
|
|
435
|
+
}
|
|
578
436
|
lastResult = eff.lastResult;
|
|
579
437
|
if (eff.lastVerdict !== undefined)
|
|
580
438
|
lastVerdict = eff.lastVerdict;
|
|
@@ -582,11 +440,91 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
582
440
|
}
|
|
583
441
|
throw new InterpretError(`data-driven ${template.pipelineId} did not terminate within ${MAX_STEPS} steps (template loop bug)`);
|
|
584
442
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
443
|
+
function branchTemplateForJoin(template, joinId) {
|
|
444
|
+
const join = resolveNode(template, joinId);
|
|
445
|
+
if (join.kind !== 'join')
|
|
446
|
+
throw new InterpretError(`fork target ${joinId} is not a join (${join.kind})`);
|
|
447
|
+
return {
|
|
448
|
+
...template,
|
|
449
|
+
nodes: {
|
|
450
|
+
...template.nodes,
|
|
451
|
+
[joinId]: { id: joinId, kind: 'terminal', status: 'succeeded' },
|
|
452
|
+
},
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
async function executeForkBranches(decision, ctx) {
|
|
456
|
+
const branchTemplate = branchTemplateForJoin(ctx.template, decision.joinId);
|
|
457
|
+
const results = await Promise.all(decision.branches.map((branch, idx) => executeForkBranch(branchTemplate, decision, branch, idx + 1, ctx)));
|
|
458
|
+
const stepDelta = results.reduce((sum, result) => sum + result.stepDelta, 0);
|
|
459
|
+
const terminal = results.find((result) => result.terminal)?.terminal;
|
|
460
|
+
if (terminal)
|
|
461
|
+
return { lastResult: undefined, terminal, stepDelta };
|
|
462
|
+
const arrivals = results.flatMap((result) => result.arrival ? [result.arrival] : []);
|
|
463
|
+
if (arrivals.length !== decision.branches.length) {
|
|
464
|
+
throw new InterpretError(`fork ${decision.nodeId} expected ${decision.branches.length} branch arrivals, got ${arrivals.length}`);
|
|
465
|
+
}
|
|
466
|
+
const lastResult = { joinArrivals: arrivals };
|
|
467
|
+
const winner = selectJoinWinner(decision.mode, arrivals, decision.joinId);
|
|
468
|
+
const join = resolveNode(ctx.template, decision.joinId);
|
|
469
|
+
if (join.kind !== 'join')
|
|
470
|
+
throw new InterpretError(`fork target ${decision.joinId} is not a join (${join.kind})`);
|
|
471
|
+
const verdict = reduceJoinVerdict(join, arrivals, winner);
|
|
472
|
+
return {
|
|
473
|
+
lastResult,
|
|
474
|
+
...(verdict !== undefined ? { lastVerdict: verdict } : {}),
|
|
475
|
+
stateOverride: {
|
|
476
|
+
...ctx.state,
|
|
477
|
+
activeNodeIds: new Set([decision.joinId]),
|
|
478
|
+
status: 'running',
|
|
479
|
+
lastResult,
|
|
480
|
+
},
|
|
481
|
+
stepDelta,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
async function executeForkBranch(branchTemplate, decision, branch, seq, ctx) {
|
|
485
|
+
let state = {
|
|
486
|
+
...ctx.state,
|
|
487
|
+
activeNodeIds: new Set([branch.entry]),
|
|
488
|
+
scopedCounters: { ...ctx.state.scopedCounters },
|
|
489
|
+
status: 'running',
|
|
490
|
+
lastResult: undefined,
|
|
491
|
+
};
|
|
492
|
+
let lastResult;
|
|
493
|
+
let lastVerdict = '';
|
|
494
|
+
let stepDelta = 0;
|
|
495
|
+
for (let i = 0; i < MAX_STEPS; i++) {
|
|
496
|
+
const next = coreStep(branchTemplate, state, lastResult);
|
|
497
|
+
state = next.state;
|
|
498
|
+
if (next.decision.type === 'complete') {
|
|
499
|
+
if (next.decision.status !== 'succeeded') {
|
|
500
|
+
throw new InterpretError(`fork ${decision.nodeId} branch ${branch.id} completed ${next.decision.status} before join ${decision.joinId}`);
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
arrival: {
|
|
504
|
+
branchId: branch.id,
|
|
505
|
+
seq,
|
|
506
|
+
...(lastVerdict ? { verdict: lastVerdict } : {}),
|
|
507
|
+
},
|
|
508
|
+
stepDelta,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
const eff = await applyDecision(next.decision, {
|
|
512
|
+
...ctx,
|
|
513
|
+
template: branchTemplate,
|
|
514
|
+
state,
|
|
515
|
+
lastVerdict,
|
|
516
|
+
});
|
|
517
|
+
stepDelta += eff.stepDelta;
|
|
518
|
+
if (eff.terminal)
|
|
519
|
+
return { terminal: eff.terminal, stepDelta };
|
|
520
|
+
if (eff.stateOverride)
|
|
521
|
+
state = eff.stateOverride;
|
|
522
|
+
lastResult = eff.lastResult;
|
|
523
|
+
if (eff.lastVerdict !== undefined)
|
|
524
|
+
lastVerdict = eff.lastVerdict;
|
|
525
|
+
}
|
|
526
|
+
throw new InterpretError(`fork ${decision.nodeId} branch ${branch.id} did not reach join ${decision.joinId} within ${MAX_STEPS} steps`);
|
|
527
|
+
}
|
|
590
528
|
async function applyDecision(decision, ctx) {
|
|
591
529
|
const { runId, template, bindingByRef, taskId, title, base } = ctx;
|
|
592
530
|
switch (decision.type) {
|
|
@@ -596,9 +534,6 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
596
534
|
const stepKey = stepKeyFor(node.id, ordinal);
|
|
597
535
|
const resolved = resolveConsumes(node, ctx.outputsByNode);
|
|
598
536
|
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
537
|
await appendEvent({
|
|
603
538
|
runId, taskId, stepId: '', stepKey, type: 'step_failed',
|
|
604
539
|
payload: { nodeId: node.id, error: `${REVO_INPUT_MISSING}: required input ${resolved.missing} was not produced` },
|
|
@@ -607,9 +542,6 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
607
542
|
}
|
|
608
543
|
const result = await invokeRole(runId, decision, node, ctx, resolved.inputs, stepKey);
|
|
609
544
|
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
545
|
return {
|
|
614
546
|
lastResult: undefined,
|
|
615
547
|
terminal: {
|
|
@@ -640,8 +572,6 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
640
572
|
case 'invokeScript': {
|
|
641
573
|
const node = resolveNode(template, decision.nodeId);
|
|
642
574
|
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
575
|
const resolved = resolveConsumes(node, ctx.outputsByNode);
|
|
646
576
|
if ('missing' in resolved) {
|
|
647
577
|
await appendEvent({
|
|
@@ -651,9 +581,6 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
651
581
|
return { lastResult: { outcome: 'failed', errorCode: REVO_INPUT_MISSING }, lastVerdict: 'failed', stepDelta: 1 };
|
|
652
582
|
}
|
|
653
583
|
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
584
|
if (scriptResult.outcome === 'blocked') {
|
|
658
585
|
return { lastResult: { outcome: 'failed', errorCode: REVO_SCRIPT_BLOCKED }, lastVerdict: 'blocked', stepDelta: 1 };
|
|
659
586
|
}
|
|
@@ -661,24 +588,18 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
661
588
|
return { lastResult: { outcome: 'failed', errorCode: REVO_SCRIPT_FAILED }, lastVerdict: 'failed', stepDelta: 1 };
|
|
662
589
|
}
|
|
663
590
|
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
591
|
const sv = scriptResult.verdict;
|
|
667
592
|
return { lastResult: { outcome: 'succeeded', ...(sv ? { verdict: sv } : {}) }, ...(sv ? { lastVerdict: sv } : {}), stepDelta: 1 };
|
|
668
593
|
}
|
|
669
594
|
case 'awaitGate': {
|
|
670
595
|
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
596
|
const ordinal = nextOrdinal(ctx.effectOrdinalByNode, decision.nodeId);
|
|
674
|
-
const human = await awaitHuman(runId, topic, stepKeyFor(decision.nodeId, ordinal), `${decision.reason} approval`, buildGateSummary(decision, ctx.outputsByNode, ctx.lastVerdict));
|
|
597
|
+
const human = await awaitHuman(runId, topic, stepKeyFor(decision.nodeId, ordinal), `${decision.reason} approval`, buildGateSummary(decision, ctx.outputsByNode, ctx.lastVerdict), decision.outcomes);
|
|
675
598
|
const verdict = gateVerdict(human, decision.outcomes);
|
|
599
|
+
await recordOutput(runId, resolveNode(template, decision.nodeId), ordinal, stepKeyFor(decision.nodeId, ordinal), gateResolutionOutput(human, verdict, stepKeyFor(decision.nodeId, ordinal)), ctx.outputsByNode);
|
|
676
600
|
return { lastResult: verdict ? { verdict } : {}, ...(verdict ? { lastVerdict: verdict } : {}), stepDelta: 0 };
|
|
677
601
|
}
|
|
678
602
|
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
603
|
await appendEvent({
|
|
683
604
|
runId,
|
|
684
605
|
taskId,
|
|
@@ -687,30 +608,19 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
687
608
|
type: 'pipeline_fork',
|
|
688
609
|
payload: { nodeId: decision.nodeId, branches: decision.branches.map((b) => b.id), joinId: decision.joinId },
|
|
689
610
|
});
|
|
690
|
-
return
|
|
691
|
-
lastResult: { joinArrivals: decision.branches.map((b, idx) => ({ branchId: b.id, seq: idx + 1 })) },
|
|
692
|
-
stepDelta: 0,
|
|
693
|
-
};
|
|
611
|
+
return executeForkBranches(decision, ctx);
|
|
694
612
|
}
|
|
695
613
|
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
614
|
return { lastResult: {}, stepDelta: 0 };
|
|
699
615
|
}
|
|
700
616
|
}
|
|
701
|
-
// ── Effect executors ─────────────────────────────────────────────────────────
|
|
702
|
-
/** invokeRole → dispatch via the existing runStep DBOS step; validate result vs resultSchema. */
|
|
703
617
|
async function invokeRole(runId, decision, node, ctx, inputs, stepKey) {
|
|
704
618
|
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
619
|
return invokeRoleAttempts({ runId, decision, node, ctx, inputs, stepKey, binding });
|
|
709
620
|
}
|
|
710
621
|
function resolveRoleBinding(ctx, decision) {
|
|
711
622
|
const binding = ctx.bindingByRef.get(decision.roleRef);
|
|
712
623
|
if (binding === undefined) {
|
|
713
|
-
// A VALID template's caps resolve at run start; an unresolved roleRef is a fatal config gap.
|
|
714
624
|
throw new Error(`CAPABILITY_UNRESOLVED: roleRef ${decision.roleRef} has no route binding`);
|
|
715
625
|
}
|
|
716
626
|
return binding;
|
|
@@ -721,7 +631,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
721
631
|
for (let attemptNo = 1; attemptNo <= ctx.runnerRetryPolicy.maxAttempts; attemptNo++) {
|
|
722
632
|
const physicalAttempt = physicalAttemptFor(runId, stepKey, attemptNo);
|
|
723
633
|
attemptIds.push(physicalAttempt.attemptId);
|
|
724
|
-
const result = await runStepFn(runId, binding.rowId, stepKey, stepInputForAttempt(decision.nodeId, inputs, physicalAttempt), binding.resolvedRunnerId, ctx.executionProfile, physicalAttempt);
|
|
634
|
+
const result = await runStepFn(runId, binding.rowId, stepKey, stepInputForAttempt(decision.nodeId, inputs, physicalAttempt), binding.resolvedRunnerId, ctx.executionProfile, physicalAttempt, ctx.template.verdicts.domain);
|
|
725
635
|
const needsHuman = await maybeHandleNeedsHumanRoleResult({
|
|
726
636
|
...input,
|
|
727
637
|
attemptIds,
|
|
@@ -768,12 +678,6 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
768
678
|
async function maybeHandleNeedsHumanRoleResult(input) {
|
|
769
679
|
const { result, node, physicalAttempt } = input;
|
|
770
680
|
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
681
|
const transient = transientRunnerFailure(result);
|
|
778
682
|
if (transient === undefined) {
|
|
779
683
|
const safeLesson = String(redactEventPayload(result.lesson ?? `agent ${node.id} reported needsHuman`));
|
|
@@ -896,26 +800,19 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
896
800
|
},
|
|
897
801
|
});
|
|
898
802
|
}
|
|
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
803
|
async function invokeScript(runId, decision, ctx, bindingByRef, stepKey, inputs) {
|
|
804
|
+
if (decision.scriptRef === 'script:cleanupWorktree') {
|
|
805
|
+
try {
|
|
806
|
+
await releaseWorktreeFn(runId, ctx.taskId);
|
|
807
|
+
}
|
|
808
|
+
catch { /* best-effort */ }
|
|
809
|
+
await appendEvent({ runId, taskId: ctx.taskId, stepId: '', stepKey, type: 'worktree_released', payload: { nodeId: decision.nodeId } });
|
|
810
|
+
return { outcome: 'ok', pointer: { released: true } };
|
|
811
|
+
}
|
|
913
812
|
const isConfirmMerge = decision.scriptRef === 'script:confirmMerge';
|
|
914
813
|
const isPollPr = decision.scriptRef === 'script:pollPr';
|
|
915
814
|
const isRespondThreads = decision.scriptRef === 'script:respondThreads';
|
|
916
815
|
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
816
|
const change = producedChangeFromInputs(inputs);
|
|
920
817
|
const mergeReadiness = mergeReadinessFromInputs(inputs);
|
|
921
818
|
const issueRef = ctx.issueRef;
|
|
@@ -930,8 +827,6 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
930
827
|
...(inputs.triage === undefined ? {} : { triage: inputs.triage }),
|
|
931
828
|
...(mergeReadiness ? { mergeReadiness } : {}),
|
|
932
829
|
};
|
|
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
830
|
const useReal = !!binding && runnerUsesRealIntegrator(binding.resolvedRunnerId);
|
|
936
831
|
let result;
|
|
937
832
|
try {
|
|
@@ -960,8 +855,6 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
960
855
|
return { outcome: 'failed' };
|
|
961
856
|
}
|
|
962
857
|
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
858
|
const reason = isConfirmMerge ? 'confirm-merge' : isPollPr ? 'poll-pr' : isRespondThreads ? 'respond-threads' : 'integrate';
|
|
966
859
|
await appendEvent({
|
|
967
860
|
runId,
|
|
@@ -998,8 +891,6 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
998
891
|
};
|
|
999
892
|
}
|
|
1000
893
|
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
894
|
const feedback = result;
|
|
1004
895
|
await appendEvent({
|
|
1005
896
|
runId,
|
|
@@ -1059,7 +950,6 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
1059
950
|
},
|
|
1060
951
|
};
|
|
1061
952
|
}
|
|
1062
|
-
/** Terminal: finish the run in Revisium per the core's terminal status. */
|
|
1063
953
|
async function finish(runId, status, verdict, steps, failureReason = '') {
|
|
1064
954
|
if (status === 'succeeded') {
|
|
1065
955
|
await completeRun(runId, { actor: 'pipeline', source: 'data-driven-complete', verdict, iterations: steps });
|
|
@@ -1080,12 +970,6 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
1080
970
|
}
|
|
1081
971
|
return { runId, status, verdict, steps };
|
|
1082
972
|
}
|
|
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
973
|
async function blockWithLesson(runId, taskId, reason, lesson, steps, retry) {
|
|
1090
974
|
await appendEvent({
|
|
1091
975
|
runId,
|
|
@@ -1099,7 +983,6 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
1099
983
|
return { runId, status: 'blocked', verdict: 'blocked', steps };
|
|
1100
984
|
}
|
|
1101
985
|
}
|
|
1102
|
-
/** Resolve a node id against the template (a VALID template never dangles — guarded defensively). */
|
|
1103
986
|
function resolveNode(template, nodeId) {
|
|
1104
987
|
const node = template.nodes[nodeId];
|
|
1105
988
|
if (!node)
|