@revisium/orchestrator 0.1.0-alpha.8 → 0.1.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.
- package/README.md +150 -130
- package/control-plane/default-playbook/catalog/pipelines.json +280 -11
- package/control-plane/default-playbook/package.json +1 -1
- package/control-plane/default-playbook/prompts/developer.md +1 -0
- package/control-plane/default-playbook/prompts/triager.md +3 -0
- package/dist/api/graphql-api/graphql-ws/subscription-mappers.js +3 -0
- package/dist/api/graphql-api/graphql-ws/subscription-mappers.js.map +1 -1
- package/dist/api/graphql-api/pr/inputs/pr-readiness.input.js +6 -0
- package/dist/api/graphql-api/pr/inputs/pr-readiness.input.js.map +1 -1
- package/dist/api/graphql-api/pr/model/pr-readiness.model.js +30 -0
- package/dist/api/graphql-api/pr/model/pr-readiness.model.js.map +1 -1
- package/dist/api/graphql-api/runs/inputs/create-run.input.js +6 -0
- package/dist/api/graphql-api/runs/inputs/create-run.input.js.map +1 -1
- package/dist/api/graphql-api/runs/model/run.model.js +6 -0
- package/dist/api/graphql-api/runs/model/run.model.js.map +1 -1
- package/dist/api/graphql-api/share/model/issue-ref.model.js +53 -0
- package/dist/api/graphql-api/share/model/issue-ref.model.js.map +1 -0
- package/dist/cli/commands/lifecycle.js +113 -8
- package/dist/cli/commands/lifecycle.js.map +1 -1
- package/dist/cli/commands/mcp.js +8 -1
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/process-tree.js +42 -0
- package/dist/cli/commands/process-tree.js.map +1 -1
- package/dist/cli/commands/rogue-reaper.js +63 -0
- package/dist/cli/commands/rogue-reaper.js.map +1 -0
- package/dist/config.js +9 -0
- package/dist/config.js.map +1 -1
- package/dist/control-plane/bootstrap.js +14 -1
- package/dist/control-plane/bootstrap.js.map +1 -1
- package/dist/control-plane/client-transport.js +1 -1
- package/dist/control-plane/client-transport.js.map +1 -1
- package/dist/control-plane/default-playbook-policy.js +593 -0
- package/dist/control-plane/default-playbook-policy.js.map +1 -0
- package/dist/control-plane/seed-default-playbook.js +77 -8
- package/dist/control-plane/seed-default-playbook.js.map +1 -1
- package/dist/e2e/kit/agents.js +15 -21
- package/dist/e2e/kit/agents.js.map +1 -1
- package/dist/e2e/kit/assertions.js +18 -13
- package/dist/e2e/kit/assertions.js.map +1 -1
- package/dist/e2e/kit/fake-integrator.js +11 -2
- package/dist/e2e/kit/fake-integrator.js.map +1 -1
- package/dist/e2e/kit/gh-emulator.js +2 -1
- package/dist/e2e/kit/gh-emulator.js.map +1 -1
- package/dist/e2e/kit/git-target-repo.js +1 -1
- package/dist/engine/dbos.service.js +3 -0
- package/dist/engine/dbos.service.js.map +1 -1
- package/dist/features/pr/queries/impl/get-pr-readiness.query.js.map +1 -1
- package/dist/features/runs/commands/impl/create-run.command.js.map +1 -1
- package/dist/features/runs/queries/handlers/runs-query.handlers.js +1 -0
- package/dist/features/runs/queries/handlers/runs-query.handlers.js.map +1 -1
- package/dist/host/daemon.js +14 -1
- package/dist/host/daemon.js.map +1 -1
- package/dist/host/host-runtime.js +20 -2
- package/dist/host/host-runtime.js.map +1 -1
- package/dist/mcp/mcp-capabilities.js +16 -1
- package/dist/mcp/mcp-capabilities.js.map +1 -1
- package/dist/mcp/mcp-facade.service.js +325 -12
- package/dist/mcp/mcp-facade.service.js.map +1 -1
- package/dist/mcp/mcp-tools.js +58 -12
- package/dist/mcp/mcp-tools.js.map +1 -1
- package/dist/observability/activity-signal.js +107 -0
- package/dist/observability/activity-signal.js.map +1 -0
- package/dist/observability/index.js +1 -0
- package/dist/observability/index.js.map +1 -1
- package/dist/pipeline/data-driven-task.workflow.js +549 -44
- package/dist/pipeline/data-driven-task.workflow.js.map +1 -1
- package/dist/pipeline/pipeline.service.js +195 -101
- package/dist/pipeline/pipeline.service.js.map +1 -1
- package/dist/pipeline/route-contract.js +4 -3
- package/dist/pipeline/route-contract.js.map +1 -1
- package/dist/pipeline-core/interpret.js +2 -0
- package/dist/pipeline-core/interpret.js.map +1 -1
- package/dist/pipeline-core/kit/builders.js +2 -0
- package/dist/pipeline-core/kit/builders.js.map +1 -1
- package/dist/pipeline-core/kit/fixtures.js +72 -21
- package/dist/pipeline-core/kit/fixtures.js.map +1 -1
- package/dist/pipeline-core/types.js +2 -0
- package/dist/pipeline-core/types.js.map +1 -1
- package/dist/pipeline-core/validate-dataflow.js +32 -0
- package/dist/pipeline-core/validate-dataflow.js.map +1 -1
- package/dist/playbook/import-mapper.js +8 -1
- package/dist/playbook/import-mapper.js.map +1 -1
- package/dist/poller/pr-readiness-core.js +299 -28
- package/dist/poller/pr-readiness-core.js.map +1 -1
- package/dist/revisium/playbooks.service.js +17 -2
- package/dist/revisium/playbooks.service.js.map +1 -1
- package/dist/revisium/run.service.js +1 -0
- package/dist/revisium/run.service.js.map +1 -1
- package/dist/run/append-event.js +8 -5
- package/dist/run/append-event.js.map +1 -1
- package/dist/run/create-run.js +5 -1
- package/dist/run/create-run.js.map +1 -1
- package/dist/run/inspect-run.js +30 -4
- package/dist/run/inspect-run.js.map +1 -1
- package/dist/run/issue-ref.js +88 -0
- package/dist/run/issue-ref.js.map +1 -0
- package/dist/runners/claude-code.service.js +2 -2
- package/dist/runners/claude-code.service.js.map +1 -1
- package/dist/runners/codex.service.js +2 -2
- package/dist/runners/codex.service.js.map +1 -1
- package/dist/runners/integrator-branch-naming.js +33 -13
- package/dist/runners/integrator-branch-naming.js.map +1 -1
- package/dist/runners/integrator.js +285 -72
- package/dist/runners/integrator.js.map +1 -1
- package/dist/runners/worktree.service.js +2 -2
- package/dist/runners/worktree.service.js.map +1 -1
- package/dist/task-control-plane/pr-readiness.service.js +15 -13
- package/dist/task-control-plane/pr-readiness.service.js.map +1 -1
- package/dist/task-control-plane/run-watch.service.js +512 -0
- package/dist/task-control-plane/run-watch.service.js.map +1 -0
- package/dist/task-control-plane/task-control-plane-api.service.js +277 -25
- package/dist/task-control-plane/task-control-plane-api.service.js.map +1 -1
- package/dist/worker/artifact-store.js +21 -3
- package/dist/worker/artifact-store.js.map +1 -1
- package/dist/worker/build-context.js +19 -3
- package/dist/worker/build-context.js.map +1 -1
- package/dist/worker/claude-code-runner.js +76 -15
- package/dist/worker/claude-code-runner.js.map +1 -1
- package/dist/worker/codex-runner.js +40 -13
- package/dist/worker/codex-runner.js.map +1 -1
- package/dist/worker/process-executor.js +149 -20
- package/dist/worker/process-executor.js.map +1 -1
- package/dist/worker/runner-common.js +29 -0
- package/dist/worker/runner-common.js.map +1 -1
- package/dist/worker/runner.js +16 -1
- package/dist/worker/runner.js.map +1 -1
- package/package.json +1 -1
|
@@ -29,8 +29,63 @@
|
|
|
29
29
|
*/
|
|
30
30
|
import { step as coreStep, initialState, validateTemplate, InterpretError, } from '../pipeline-core/index.js';
|
|
31
31
|
import { runnerNeedsLivePreflight, runnerUsesRealIntegrator } from './route-contract.js';
|
|
32
|
+
import { redactEventPayload } from '../run/append-event.js';
|
|
33
|
+
import { redactSecrets } from '../control-plane/inbox.js';
|
|
34
|
+
import { fnv1a64Hex } from '../control-plane/steps.js';
|
|
35
|
+
import { normalizeIssueRef } from '../run/issue-ref.js';
|
|
36
|
+
import { RUNNER_IDLE_TIMEOUT_KIND, RUNNER_WALL_CLOCK_LIMIT_KIND, } from '../worker/process-executor.js';
|
|
32
37
|
export const RUN_PROGRESS_EVENT_KEY = 'run-progress';
|
|
33
38
|
const MAX_STEPS = 1_000; // a VALID template terminates; guards a data/loop authoring mistake at runtime.
|
|
39
|
+
const DEFAULT_RUNNER_TRANSIENT_MAX_ATTEMPTS = 2;
|
|
40
|
+
const DEFAULT_RUNNER_TRANSIENT_RETRY_BACKOFF_MS = 2_000;
|
|
41
|
+
function readPositiveIntegerEnv(env, key, fallback) {
|
|
42
|
+
const raw = env[key];
|
|
43
|
+
if (raw === undefined)
|
|
44
|
+
return fallback;
|
|
45
|
+
if (!/^\d+$/.test(raw))
|
|
46
|
+
throw new Error(`${key} must be a positive integer`);
|
|
47
|
+
const parsed = Number(raw);
|
|
48
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0)
|
|
49
|
+
throw new Error(`${key} must be a positive integer`);
|
|
50
|
+
return parsed;
|
|
51
|
+
}
|
|
52
|
+
function readNonNegativeIntegerEnv(env, key, fallback) {
|
|
53
|
+
const raw = env[key];
|
|
54
|
+
if (raw === undefined)
|
|
55
|
+
return fallback;
|
|
56
|
+
if (!/^\d+$/.test(raw))
|
|
57
|
+
throw new Error(`${key} must be a non-negative integer`);
|
|
58
|
+
const parsed = Number(raw);
|
|
59
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0)
|
|
60
|
+
throw new Error(`${key} must be a non-negative integer`);
|
|
61
|
+
return parsed;
|
|
62
|
+
}
|
|
63
|
+
export function resolveRunnerTransientRetryPolicy(env = process.env) {
|
|
64
|
+
return {
|
|
65
|
+
maxAttempts: readPositiveIntegerEnv(env, 'REVO_RUNNER_TRANSIENT_MAX_ATTEMPTS', DEFAULT_RUNNER_TRANSIENT_MAX_ATTEMPTS),
|
|
66
|
+
backoffMs: readNonNegativeIntegerEnv(env, 'REVO_RUNNER_TRANSIENT_RETRY_BACKOFF_MS', DEFAULT_RUNNER_TRANSIENT_RETRY_BACKOFF_MS),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function physicalAttemptFor(runId, stepKey, attemptNo) {
|
|
70
|
+
const attemptKey = `${runId}|${stepKey}|${attemptNo}`;
|
|
71
|
+
return {
|
|
72
|
+
attemptNo,
|
|
73
|
+
attemptId: `attempt_${fnv1a64Hex(attemptKey)}`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function stepInputForAttempt(nodeId, inputs, attempt) {
|
|
77
|
+
return {
|
|
78
|
+
nodeId,
|
|
79
|
+
attempt: { attemptNo: attempt.attemptNo, attemptId: attempt.attemptId },
|
|
80
|
+
...(Object.keys(inputs).length > 0 ? { inputs } : {}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function optionalTiming(timing) {
|
|
84
|
+
return timing === undefined ? {} : { timing };
|
|
85
|
+
}
|
|
86
|
+
function optionalFailureKind(failureKind) {
|
|
87
|
+
return failureKind === undefined ? {} : { failureKind };
|
|
88
|
+
}
|
|
34
89
|
/**
|
|
35
90
|
* Reserved engine error codes (matched only by a node's `catch`, §3/§6).
|
|
36
91
|
*
|
|
@@ -46,6 +101,31 @@ const REVO_SCRIPT_FAILED = 'revo.ScriptFailed';
|
|
|
46
101
|
const REVO_SCRIPT_BLOCKED = 'revo.ScriptBlocked';
|
|
47
102
|
const REVO_RESULT_INVALID = 'revo.ResultInvalid';
|
|
48
103
|
const REVO_INPUT_MISSING = 'revo.InputMissing';
|
|
104
|
+
function invalidRoleResult(reason, physicalAttempt) {
|
|
105
|
+
return {
|
|
106
|
+
failed: true,
|
|
107
|
+
errorCode: REVO_RESULT_INVALID,
|
|
108
|
+
reason,
|
|
109
|
+
attemptId: physicalAttempt.attemptId,
|
|
110
|
+
attemptsMade: physicalAttempt.attemptNo,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function runnerBlockReason(transient) {
|
|
114
|
+
if (transient.failureKind)
|
|
115
|
+
return transient.failureKind;
|
|
116
|
+
return `runner-transient-failure:${transient.transientKind}`;
|
|
117
|
+
}
|
|
118
|
+
function runnerBlockLesson(transient) {
|
|
119
|
+
const safe = String(redactEventPayload(transient.reason));
|
|
120
|
+
if (transient.failureKind)
|
|
121
|
+
return `${transient.failureKind}: ${safe || 'runner failed'}`;
|
|
122
|
+
return `runner-transient-failure (${transient.transientKind}): ${safe || 'runner failed'}`;
|
|
123
|
+
}
|
|
124
|
+
function pipelineBlockedPayload(reason, lesson, retry) {
|
|
125
|
+
if (retry === undefined)
|
|
126
|
+
return { reason, lesson };
|
|
127
|
+
return { ...retry };
|
|
128
|
+
}
|
|
49
129
|
/** Per-node execution stepKey: the bare nodeId on the first entry (stable ids for existing tests), an
|
|
50
130
|
* ordinal-suffixed key on loop re-entries so attempts/events/outputs are distinct per iteration (0016
|
|
51
131
|
* §4.1 — fixes the latent 0015 stepKey-reuse collision). */
|
|
@@ -62,6 +142,125 @@ function nextOrdinal(byNode, nodeId) {
|
|
|
62
142
|
function isRecord(value) {
|
|
63
143
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
64
144
|
}
|
|
145
|
+
function producedChangeArtifact(value) {
|
|
146
|
+
if (!isRecord(value))
|
|
147
|
+
return undefined;
|
|
148
|
+
const candidate = isRecord(value.change) ? value.change : value;
|
|
149
|
+
const branch = candidate.branch;
|
|
150
|
+
const headSha = candidate.headSha;
|
|
151
|
+
if (typeof branch !== 'string' || branch.trim().length === 0)
|
|
152
|
+
return undefined;
|
|
153
|
+
if (typeof headSha !== 'string' || headSha.trim().length === 0)
|
|
154
|
+
return undefined;
|
|
155
|
+
const issueRef = normalizeArtifactIssueRef(candidate.issueRef);
|
|
156
|
+
return {
|
|
157
|
+
branch,
|
|
158
|
+
headSha,
|
|
159
|
+
...(issueRef ? { issueRef } : {}),
|
|
160
|
+
...(typeof candidate.worktreePath === 'string' && candidate.worktreePath.trim() ? { worktreePath: candidate.worktreePath } : {}),
|
|
161
|
+
...(typeof candidate.artifactRef === 'string' && candidate.artifactRef.trim() ? { artifactRef: candidate.artifactRef } : {}),
|
|
162
|
+
...(typeof candidate.prNumber === 'number' && Number.isSafeInteger(candidate.prNumber) ? { prNumber: candidate.prNumber } : {}),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function normalizeArtifactIssueRef(value) {
|
|
166
|
+
try {
|
|
167
|
+
return normalizeIssueRef(value, 'change.issueRef');
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function changeWithRunIssueRef(change, issueRef) {
|
|
174
|
+
const normalized = { ...change };
|
|
175
|
+
if (issueRef) {
|
|
176
|
+
normalized.issueRef = issueRef;
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
delete normalized.issueRef;
|
|
180
|
+
}
|
|
181
|
+
return normalized;
|
|
182
|
+
}
|
|
183
|
+
function producedChangeFromInputs(inputs) {
|
|
184
|
+
for (const key of ['reviewChange', 'ciChange', 'reworkChange', 'developerChange', 'change']) {
|
|
185
|
+
const artifact = producedChangeArtifact(inputs[key]);
|
|
186
|
+
if (artifact)
|
|
187
|
+
return artifact;
|
|
188
|
+
}
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
function mergeReadinessFromInputs(inputs) {
|
|
192
|
+
const value = inputs.mergeReadiness;
|
|
193
|
+
if (!isRecord(value))
|
|
194
|
+
return undefined;
|
|
195
|
+
const headSha = value.headSha;
|
|
196
|
+
if (typeof headSha !== 'string' || headSha.trim().length === 0)
|
|
197
|
+
return undefined;
|
|
198
|
+
return { headSha };
|
|
199
|
+
}
|
|
200
|
+
function attachProducedChange(output, change) {
|
|
201
|
+
if (isRecord(output))
|
|
202
|
+
return { ...output, change };
|
|
203
|
+
return { summary: output, change };
|
|
204
|
+
}
|
|
205
|
+
function nodeProducesChange(node) {
|
|
206
|
+
return (node.kind === 'agent' || node.kind === 'script') &&
|
|
207
|
+
node.produces?.name === 'change' &&
|
|
208
|
+
node.resultSchema === 'schema:change';
|
|
209
|
+
}
|
|
210
|
+
function runnerProducesWorktreeChanges(runnerId) {
|
|
211
|
+
return runnerId === 'claude-code' || runnerId === 'codex';
|
|
212
|
+
}
|
|
213
|
+
function artifactRefFromResult(result) {
|
|
214
|
+
const artifacts = result.artifacts;
|
|
215
|
+
const processArtifact = isRecord(artifacts) && isRecord(artifacts.process) ? artifacts.process : artifacts;
|
|
216
|
+
if (!isRecord(processArtifact))
|
|
217
|
+
return undefined;
|
|
218
|
+
return typeof processArtifact.ref === 'string' ? processArtifact.ref : undefined;
|
|
219
|
+
}
|
|
220
|
+
function publicTimeoutFailureKind(value) {
|
|
221
|
+
return value === RUNNER_IDLE_TIMEOUT_KIND || value === RUNNER_WALL_CLOCK_LIMIT_KIND
|
|
222
|
+
? value
|
|
223
|
+
: undefined;
|
|
224
|
+
}
|
|
225
|
+
function transientRunnerFailure(result) {
|
|
226
|
+
const output = result.output;
|
|
227
|
+
if (!isRecord(output) || output.error !== 'runner_failed')
|
|
228
|
+
return undefined;
|
|
229
|
+
const reason = typeof output.reason === 'string' ? output.reason : '';
|
|
230
|
+
const failureKind = publicTimeoutFailureKind(output.failureKind);
|
|
231
|
+
const legacyKind = transientKind(reason);
|
|
232
|
+
const retryableCandidate = output.retryableCandidate !== false;
|
|
233
|
+
return {
|
|
234
|
+
reason,
|
|
235
|
+
transientKind: failureKind ? 'timeout' : legacyKind,
|
|
236
|
+
retryableCandidate,
|
|
237
|
+
retryable: retryableCandidate && (failureKind !== undefined || legacyKind !== 'unknown'),
|
|
238
|
+
...optionalFailureKind(failureKind),
|
|
239
|
+
...optionalTiming(output.timing),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
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
|
+
function transientKind(reason) {
|
|
248
|
+
if (/exceeded\s*\d+\s*ms|timed?\s*out|\btimeout\b/i.test(reason))
|
|
249
|
+
return 'timeout';
|
|
250
|
+
if (/\b429\b|rate.?limit|session limit/i.test(reason))
|
|
251
|
+
return 'rate_limit';
|
|
252
|
+
if (isLegacyRetryableCrashReason(reason))
|
|
253
|
+
return 'crash';
|
|
254
|
+
return 'unknown';
|
|
255
|
+
}
|
|
256
|
+
function isLegacyRetryableCrashReason(reason) {
|
|
257
|
+
if (/\b(auth|credential|permission|forbidden|denied|unauthori[sz]ed|config|schema|malformed|parseable|invalid|RUNNER_NOT_IMPLEMENTED|not wired|unknown runner|requires an OpenAI|ENOENT|not found)\b/i
|
|
258
|
+
.test(reason)) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
return /\b(signal|crash(?:ed)?|killed|ECONNRESET|ETIMEDOUT|EAI_AGAIN|socket hang up|ENOMEM|EAGAIN)\b/i
|
|
262
|
+
.test(reason);
|
|
263
|
+
}
|
|
65
264
|
/** Extract the DOMAIN verdict from the only supported source: top-level AttemptResult.verdict. */
|
|
66
265
|
function domainVerdictOf(result) {
|
|
67
266
|
if (typeof result.verdict === 'string' && result.verdict.trim().length > 0) {
|
|
@@ -136,6 +335,66 @@ function gateTopicFor(reason) {
|
|
|
136
335
|
return 'question';
|
|
137
336
|
return 'plan';
|
|
138
337
|
}
|
|
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
|
+
export const GATE_ARTIFACT_MAX = 16_000;
|
|
345
|
+
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
|
+
function resolveGateRow(ref, outputsByNode) {
|
|
348
|
+
if (!ref)
|
|
349
|
+
return undefined;
|
|
350
|
+
const produced = outputsByNode.get(ref.node) ?? [];
|
|
351
|
+
if (produced.length === 0)
|
|
352
|
+
return undefined;
|
|
353
|
+
if (typeof ref.iteration === 'number')
|
|
354
|
+
return produced.find((o) => o.ordinal === ref.iteration);
|
|
355
|
+
return produced[produced.length - 1]; // 'latest' (and 'all' → the most recent for an inline view)
|
|
356
|
+
}
|
|
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
|
+
function gateArtifactView(row, as) {
|
|
365
|
+
const base = { nodeId: row.nodeId, name: as ?? row.name, schemaRef: row.schemaRef };
|
|
366
|
+
const safe = redactEventPayload(redactSecrets(row.payload) ?? null);
|
|
367
|
+
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
|
+
if (Buffer.byteLength(serialized, 'utf8') <= GATE_ARTIFACT_MAX)
|
|
370
|
+
return { ...base, payload: safe };
|
|
371
|
+
return {
|
|
372
|
+
...base,
|
|
373
|
+
truncated: true,
|
|
374
|
+
preview: serialized.slice(0, GATE_PREVIEW_CHARS),
|
|
375
|
+
payloadRef: `attempt:${row.attemptId ?? ''}`,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
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
|
+
export function buildGateSummary(decision, outputsByNode, lastVerdict) {
|
|
385
|
+
const summary = { nodeId: decision.nodeId, outcomes: decision.outcomes };
|
|
386
|
+
const artRow = resolveGateRow(decision.gatedArtifact, outputsByNode);
|
|
387
|
+
if (artRow)
|
|
388
|
+
summary.gatedArtifact = gateArtifactView(artRow, decision.gatedArtifact?.as);
|
|
389
|
+
const verdictRow = resolveGateRow(decision.verdictFrom, outputsByNode);
|
|
390
|
+
if (verdictRow)
|
|
391
|
+
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
|
+
else if (!decision.verdictFrom && lastVerdict)
|
|
395
|
+
summary.reviewerVerdict = { verdict: lastVerdict };
|
|
396
|
+
return summary;
|
|
397
|
+
}
|
|
139
398
|
/**
|
|
140
399
|
* makeDataDrivenTask — DBOS-free factory for the dataDrivenTask async function (C1).
|
|
141
400
|
*
|
|
@@ -143,7 +402,7 @@ function gateTopicFor(reason) {
|
|
|
143
402
|
* production and tests. Returns a plain async function with the workflow signature.
|
|
144
403
|
*/
|
|
145
404
|
export function makeDataDrivenTask(runStepFn, deps) {
|
|
146
|
-
const { appendEvent, appendRunOutput, awaitHuman, completeRun, failRun, blockRun, loadRunTaskContext, integrateFn, runStub, confirmMergeFn, runConfirmStub, pollPrFn, runPollStub, respondThreadsFn, runRespondStub, preflightFn, createWorktreeFn, releaseWorktreeFn } = deps;
|
|
405
|
+
const { appendEvent, appendRunOutput, awaitHuman, completeRun, failRun, blockRun, loadRunTaskContext, integrateFn, runStub, confirmMergeFn, runConfirmStub, pollPrFn, runPollStub, respondThreadsFn, runRespondStub, captureChangeFn, preflightFn, createWorktreeFn, releaseWorktreeFn } = deps;
|
|
147
406
|
/** Resolve a node's `consumes` from the workflow-local output accumulator — NOT live Revisium reads
|
|
148
407
|
* (0016 §6 / consensus M4: a live read on replay can see rows written past the replay point). */
|
|
149
408
|
function resolveConsumes(node, outputsByNode) {
|
|
@@ -177,7 +436,8 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
177
436
|
return { inputs };
|
|
178
437
|
}
|
|
179
438
|
/** Record a node's produced output to the workflow-local accumulator + Revisium (when `produces`). */
|
|
180
|
-
|
|
439
|
+
// (gate-summary helpers live at module scope below — they are pure over outputsByNode.)
|
|
440
|
+
async function recordOutput(runId, node, ordinal, attemptId, output, outputsByNode) {
|
|
181
441
|
if (!('produces' in node) || !node.produces)
|
|
182
442
|
return;
|
|
183
443
|
const row = {
|
|
@@ -187,7 +447,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
187
447
|
name: node.produces.name,
|
|
188
448
|
schemaRef: ('resultSchema' in node && node.resultSchema) || '',
|
|
189
449
|
payload: output,
|
|
190
|
-
attemptId
|
|
450
|
+
attemptId,
|
|
191
451
|
};
|
|
192
452
|
const list = outputsByNode.get(node.id) ?? [];
|
|
193
453
|
list.push(row);
|
|
@@ -220,7 +480,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
220
480
|
if (diagnostics.length > 0) {
|
|
221
481
|
throw new Error(`PINNED_TEMPLATE_INVALID: ${template.pipelineId} — ${diagnostics.map((d) => d.code).join(', ')}`);
|
|
222
482
|
}
|
|
223
|
-
const { taskId, title, base } = await loadRunTaskContext(runId);
|
|
483
|
+
const { taskId, title, base, issueRef } = await loadRunTaskContext(runId);
|
|
224
484
|
// B5/B7 — live preflight: one memoized DBOS step, evaluated exactly once BEFORE the graph runs.
|
|
225
485
|
// Skipped entirely when every selected binding resolves to a stub/script runner (mirrors the old
|
|
226
486
|
// engine's gate). A `needsHuman` preflight blocks the run (clean/base invariant unmet) and surfaces
|
|
@@ -229,7 +489,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
229
489
|
const live = route.roleBindings.some((b) => runnerNeedsLivePreflight(b.resolvedRunnerId));
|
|
230
490
|
if (live) {
|
|
231
491
|
// Preflight runs against the BASE checkout (resolveTaskCwd), BEFORE the worktree exists — its
|
|
232
|
-
// fetch + clean/
|
|
492
|
+
// fetch + clean/base-availability checks protect the user's base repo. Ordering is load-bearing.
|
|
233
493
|
const pf = await preflightFn(taskId, base);
|
|
234
494
|
if ('needsHuman' in pf) {
|
|
235
495
|
return await blockWithLesson(runId, taskId, 'preflight', pf.lesson, 0);
|
|
@@ -247,9 +507,9 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
247
507
|
let result;
|
|
248
508
|
try {
|
|
249
509
|
if (live) {
|
|
250
|
-
await createWorktreeFn(runId, taskId, title, base);
|
|
510
|
+
await createWorktreeFn(runId, taskId, title, base, issueRef);
|
|
251
511
|
}
|
|
252
|
-
result = await runGraph(runId, opts, taskId, title, base);
|
|
512
|
+
result = await runGraph(runId, opts, taskId, title, base, issueRef, live);
|
|
253
513
|
return result;
|
|
254
514
|
}
|
|
255
515
|
finally {
|
|
@@ -264,8 +524,8 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
264
524
|
}
|
|
265
525
|
}
|
|
266
526
|
/** The pipeline-core interpretation loop, extracted so the worktree `finally` in runBody wraps it. */
|
|
267
|
-
async function runGraph(runId, opts, taskId, title, base) {
|
|
268
|
-
const { route, template } = opts;
|
|
527
|
+
async function runGraph(runId, opts, taskId, title, base, issueRef, live) {
|
|
528
|
+
const { route, template, runnerRetryPolicy } = opts;
|
|
269
529
|
// Capability resolution map (GENERIC — no role-ids in the engine): roleRef/scriptRef → route binding.
|
|
270
530
|
// The route's bindings are authoritative for runner dispatch (and durable on recovery via the DBOS
|
|
271
531
|
// workflow input). A `role:<id>` handle resolves to the binding whose roleId matches `<id>`. A
|
|
@@ -300,14 +560,25 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
300
560
|
return await finish(runId, decision.status, lastVerdict, stepCount, lastFailureReason);
|
|
301
561
|
}
|
|
302
562
|
const eff = await applyDecision(decision, {
|
|
303
|
-
runId, template, bindingByRef, executionProfile, taskId, title, base,
|
|
304
|
-
effectOrdinalByNode, outputsByNode,
|
|
563
|
+
runId, template, bindingByRef, executionProfile, taskId, title, base, issueRef,
|
|
564
|
+
effectOrdinalByNode, outputsByNode, runnerRetryPolicy,
|
|
565
|
+
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
|
+
lastVerdict,
|
|
305
569
|
});
|
|
570
|
+
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
|
+
if (eff.terminal) {
|
|
576
|
+
return await blockWithLesson(runId, taskId, eff.terminal.reason, eff.terminal.lesson, stepCount, eff.terminal.retry);
|
|
577
|
+
}
|
|
306
578
|
lastResult = eff.lastResult;
|
|
307
579
|
if (eff.lastVerdict !== undefined)
|
|
308
580
|
lastVerdict = eff.lastVerdict;
|
|
309
581
|
lastFailureReason = eff.failureReason ?? '';
|
|
310
|
-
stepCount += eff.stepDelta;
|
|
311
582
|
}
|
|
312
583
|
throw new InterpretError(`data-driven ${template.pipelineId} did not terminate within ${MAX_STEPS} steps (template loop bug)`);
|
|
313
584
|
}
|
|
@@ -335,19 +606,35 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
335
606
|
return { lastResult: { outcome: 'failed', errorCode: REVO_INPUT_MISSING }, lastVerdict: 'failed', stepDelta: 1 };
|
|
336
607
|
}
|
|
337
608
|
const result = await invokeRole(runId, decision, node, ctx, resolved.inputs, stepKey);
|
|
609
|
+
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
|
+
return {
|
|
614
|
+
lastResult: undefined,
|
|
615
|
+
terminal: {
|
|
616
|
+
status: 'blocked',
|
|
617
|
+
reason: result.reason,
|
|
618
|
+
lesson: result.lesson,
|
|
619
|
+
...(result.retry ? { retry: result.retry } : {}),
|
|
620
|
+
},
|
|
621
|
+
stepDelta: result.attemptsMade,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
338
624
|
if (result.failed) {
|
|
339
625
|
await appendEvent({
|
|
340
626
|
runId, taskId, stepId: '', stepKey, type: 'step_failed',
|
|
627
|
+
idempotencyKey: result.attemptId,
|
|
341
628
|
payload: { nodeId: node.id, error: result.errorCode },
|
|
342
629
|
});
|
|
343
|
-
return { lastResult: { outcome: 'failed', errorCode: result.errorCode }, lastVerdict: 'failed', failureReason: result.reason, stepDelta:
|
|
630
|
+
return { lastResult: { outcome: 'failed', errorCode: result.errorCode }, lastVerdict: 'failed', failureReason: result.reason, stepDelta: result.attemptsMade };
|
|
344
631
|
}
|
|
345
|
-
await recordOutput(runId, node, ordinal, result.output, ctx.outputsByNode);
|
|
632
|
+
await recordOutput(runId, node, ordinal, result.attemptId, result.output, ctx.outputsByNode);
|
|
346
633
|
const verdict = result.verdict;
|
|
347
634
|
return {
|
|
348
635
|
lastResult: { outcome: 'succeeded', ...(verdict ? { verdict } : {}) },
|
|
349
636
|
...(verdict ? { lastVerdict: verdict } : {}),
|
|
350
|
-
stepDelta:
|
|
637
|
+
stepDelta: result.attemptsMade,
|
|
351
638
|
};
|
|
352
639
|
}
|
|
353
640
|
case 'invokeScript': {
|
|
@@ -363,7 +650,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
363
650
|
});
|
|
364
651
|
return { lastResult: { outcome: 'failed', errorCode: REVO_INPUT_MISSING }, lastVerdict: 'failed', stepDelta: 1 };
|
|
365
652
|
}
|
|
366
|
-
const scriptResult = await invokeScript(runId, decision, { taskId, title, base }, bindingByRef, stepKeyFor(node.id, ordinal), resolved.inputs);
|
|
653
|
+
const scriptResult = await invokeScript(runId, decision, { taskId, title, base, issueRef: ctx.issueRef }, bindingByRef, stepKeyFor(node.id, ordinal), resolved.inputs);
|
|
367
654
|
// A blocked script (needsHuman) routes via revo.ScriptBlocked → a `blocked` terminal; a thrown
|
|
368
655
|
// script routes via revo.ScriptFailed → a `failed` terminal (§6 catch). The lesson-bearing
|
|
369
656
|
// pipeline_blocked is emitted inside invokeScript for the block path (parity with the old engine).
|
|
@@ -373,7 +660,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
373
660
|
if (scriptResult.outcome === 'failed') {
|
|
374
661
|
return { lastResult: { outcome: 'failed', errorCode: REVO_SCRIPT_FAILED }, lastVerdict: 'failed', stepDelta: 1 };
|
|
375
662
|
}
|
|
376
|
-
await recordOutput(runId, node, ordinal, scriptResult.pointer, ctx.outputsByNode);
|
|
663
|
+
await recordOutput(runId, node, ordinal, stepKeyFor(node.id, ordinal), scriptResult.pointer, ctx.outputsByNode);
|
|
377
664
|
// A classifying script (pollPr, plan 0018) surfaces a DOMAIN verdict so the next `choice` can
|
|
378
665
|
// route on it (§8). Scripts with a single fixed `next` (integrator/confirmMerge) carry none.
|
|
379
666
|
const sv = scriptResult.verdict;
|
|
@@ -384,10 +671,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
384
671
|
// Per-entry gate key (nodeId#ordinal) so a re-entered gate (e.g. a question gate looped in the
|
|
385
672
|
// review phase) gets a DISTINCT inbox row instead of colliding on `runId|topic` (§3.2 audit).
|
|
386
673
|
const ordinal = nextOrdinal(ctx.effectOrdinalByNode, decision.nodeId);
|
|
387
|
-
const human = await awaitHuman(runId, topic, stepKeyFor(decision.nodeId, ordinal), `${decision.reason} approval`,
|
|
388
|
-
nodeId: decision.nodeId,
|
|
389
|
-
outcomes: decision.outcomes,
|
|
390
|
-
});
|
|
674
|
+
const human = await awaitHuman(runId, topic, stepKeyFor(decision.nodeId, ordinal), `${decision.reason} approval`, buildGateSummary(decision, ctx.outputsByNode, ctx.lastVerdict));
|
|
391
675
|
const verdict = gateVerdict(human, decision.outcomes);
|
|
392
676
|
return { lastResult: verdict ? { verdict } : {}, ...(verdict ? { lastVerdict: verdict } : {}), stepDelta: 0 };
|
|
393
677
|
}
|
|
@@ -417,29 +701,200 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
417
701
|
// ── Effect executors ─────────────────────────────────────────────────────────
|
|
418
702
|
/** invokeRole → dispatch via the existing runStep DBOS step; validate result vs resultSchema. */
|
|
419
703
|
async function invokeRole(runId, decision, node, ctx, inputs, stepKey) {
|
|
704
|
+
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
|
+
return invokeRoleAttempts({ runId, decision, node, ctx, inputs, stepKey, binding });
|
|
709
|
+
}
|
|
710
|
+
function resolveRoleBinding(ctx, decision) {
|
|
420
711
|
const binding = ctx.bindingByRef.get(decision.roleRef);
|
|
421
|
-
if (
|
|
712
|
+
if (binding === undefined) {
|
|
422
713
|
// A VALID template's caps resolve at run start; an unresolved roleRef is a fatal config gap.
|
|
423
714
|
throw new Error(`CAPABILITY_UNRESOLVED: roleRef ${decision.roleRef} has no route binding`);
|
|
424
715
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
716
|
+
return binding;
|
|
717
|
+
}
|
|
718
|
+
async function invokeRoleAttempts(input) {
|
|
719
|
+
const { runId, decision, node, ctx, inputs, stepKey, binding } = input;
|
|
720
|
+
const attemptIds = [];
|
|
721
|
+
for (let attemptNo = 1; attemptNo <= ctx.runnerRetryPolicy.maxAttempts; attemptNo++) {
|
|
722
|
+
const physicalAttempt = physicalAttemptFor(runId, stepKey, attemptNo);
|
|
723
|
+
attemptIds.push(physicalAttempt.attemptId);
|
|
724
|
+
const result = await runStepFn(runId, binding.rowId, stepKey, stepInputForAttempt(decision.nodeId, inputs, physicalAttempt), binding.resolvedRunnerId, ctx.executionProfile, physicalAttempt);
|
|
725
|
+
const needsHuman = await maybeHandleNeedsHumanRoleResult({
|
|
726
|
+
...input,
|
|
727
|
+
attemptIds,
|
|
728
|
+
physicalAttempt,
|
|
729
|
+
result,
|
|
730
|
+
});
|
|
731
|
+
if (needsHuman === 'retry')
|
|
732
|
+
continue;
|
|
733
|
+
if (needsHuman)
|
|
734
|
+
return needsHuman;
|
|
735
|
+
const failed = roleValidationFailure(ctx.template, node, result, physicalAttempt);
|
|
736
|
+
if (failed)
|
|
737
|
+
return failed;
|
|
738
|
+
let output = result.output;
|
|
739
|
+
const hasProducedChange = producedChangeArtifact(output) !== undefined;
|
|
740
|
+
const shouldCaptureChange = nodeProducesChange(node) &&
|
|
741
|
+
!hasProducedChange &&
|
|
742
|
+
ctx.live &&
|
|
743
|
+
runnerProducesWorktreeChanges(binding.resolvedRunnerId);
|
|
744
|
+
if (shouldCaptureChange) {
|
|
745
|
+
const artifactRef = artifactRefFromResult(result);
|
|
746
|
+
const change = await captureChangeFn({
|
|
747
|
+
runId,
|
|
748
|
+
taskId: ctx.taskId,
|
|
749
|
+
title: ctx.title,
|
|
750
|
+
base: ctx.base,
|
|
751
|
+
nodeId: node.id,
|
|
752
|
+
attemptId: physicalAttempt.attemptId,
|
|
753
|
+
...(ctx.issueRef ? { issueRef: ctx.issueRef } : {}),
|
|
754
|
+
...(artifactRef ? { artifactRef } : {}),
|
|
755
|
+
});
|
|
756
|
+
output = attachProducedChange(output, change);
|
|
757
|
+
}
|
|
758
|
+
return {
|
|
759
|
+
failed: false,
|
|
760
|
+
verdict: domainVerdictOf(result),
|
|
761
|
+
output,
|
|
762
|
+
attemptId: physicalAttempt.attemptId,
|
|
763
|
+
attemptsMade: attemptNo,
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
throw new InterpretError(`runner retry loop for ${stepKey} exceeded maxAttempts`);
|
|
767
|
+
}
|
|
768
|
+
async function maybeHandleNeedsHumanRoleResult(input) {
|
|
769
|
+
const { result, node, physicalAttempt } = input;
|
|
432
770
|
if (result.needsHuman) {
|
|
433
|
-
|
|
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
|
+
const transient = transientRunnerFailure(result);
|
|
778
|
+
if (transient === undefined) {
|
|
779
|
+
const safeLesson = String(redactEventPayload(result.lesson ?? `agent ${node.id} reported needsHuman`));
|
|
780
|
+
return { blocked: true, reason: 'agent-needs-human', lesson: safeLesson, attemptsMade: physicalAttempt.attemptNo };
|
|
781
|
+
}
|
|
782
|
+
return handleTransientRoleResult({ ...input, transient });
|
|
434
783
|
}
|
|
435
|
-
|
|
436
|
-
|
|
784
|
+
return undefined;
|
|
785
|
+
}
|
|
786
|
+
async function handleTransientRoleResult(input) {
|
|
787
|
+
const { runId, node, ctx, stepKey, attemptIds, physicalAttempt, transient } = input;
|
|
788
|
+
const policy = ctx.runnerRetryPolicy;
|
|
789
|
+
if (shouldRetryTransient(transient, physicalAttempt, policy)) {
|
|
790
|
+
const nextAttempt = physicalAttemptFor(runId, stepKey, physicalAttempt.attemptNo + 1);
|
|
791
|
+
await appendRunnerRetryScheduled({
|
|
792
|
+
runId,
|
|
793
|
+
taskId: ctx.taskId,
|
|
794
|
+
stepKey,
|
|
795
|
+
nodeId: node.id,
|
|
796
|
+
failedAttempt: physicalAttempt,
|
|
797
|
+
nextAttempt,
|
|
798
|
+
policy,
|
|
799
|
+
transient,
|
|
800
|
+
});
|
|
801
|
+
if (policy.backoffMs > 0)
|
|
802
|
+
await deps.sleep(policy.backoffMs);
|
|
803
|
+
return 'retry';
|
|
437
804
|
}
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
805
|
+
const retry = runnerRetryBlockPayload({
|
|
806
|
+
transient,
|
|
807
|
+
attemptIds,
|
|
808
|
+
lastAttempt: physicalAttempt,
|
|
809
|
+
policy,
|
|
810
|
+
attemptsExhausted: transient.retryable && physicalAttempt.attemptNo >= policy.maxAttempts,
|
|
811
|
+
});
|
|
812
|
+
if (retry.attemptsExhausted) {
|
|
813
|
+
await appendRunnerRetryExhausted({
|
|
814
|
+
runId,
|
|
815
|
+
taskId: ctx.taskId,
|
|
816
|
+
stepKey,
|
|
817
|
+
nodeId: node.id,
|
|
818
|
+
retry,
|
|
819
|
+
idempotencyKey: physicalAttempt.attemptId,
|
|
820
|
+
});
|
|
441
821
|
}
|
|
442
|
-
return {
|
|
822
|
+
return {
|
|
823
|
+
blocked: true,
|
|
824
|
+
reason: retry.reason,
|
|
825
|
+
lesson: retry.lesson,
|
|
826
|
+
retry,
|
|
827
|
+
attemptsMade: physicalAttempt.attemptNo,
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
function shouldRetryTransient(transient, attempt, policy) {
|
|
831
|
+
return transient.retryable && attempt.attemptNo < policy.maxAttempts;
|
|
832
|
+
}
|
|
833
|
+
function roleValidationFailure(template, node, result, physicalAttempt) {
|
|
834
|
+
if (resultSatisfiesSchema(node, result)) {
|
|
835
|
+
const verdictProblem = resultVerdictProblem(template, node, result);
|
|
836
|
+
if (verdictProblem)
|
|
837
|
+
return invalidRoleResult(verdictProblem, physicalAttempt);
|
|
838
|
+
return undefined;
|
|
839
|
+
}
|
|
840
|
+
return invalidRoleResult(`${REVO_RESULT_INVALID}: node ${node.id} result did not satisfy resultSchema ${String('resultSchema' in node ? node.resultSchema : '')}`, physicalAttempt);
|
|
841
|
+
}
|
|
842
|
+
function runnerRetryBlockPayload(input) {
|
|
843
|
+
const { transient, attemptIds, lastAttempt, policy, attemptsExhausted } = input;
|
|
844
|
+
const reason = runnerBlockReason(transient);
|
|
845
|
+
const lesson = runnerBlockLesson(transient);
|
|
846
|
+
return {
|
|
847
|
+
attemptsExhausted,
|
|
848
|
+
attemptsMade: lastAttempt.attemptNo,
|
|
849
|
+
maxAttempts: policy.maxAttempts,
|
|
850
|
+
attemptIds: [...attemptIds],
|
|
851
|
+
lastAttemptId: lastAttempt.attemptId,
|
|
852
|
+
reason,
|
|
853
|
+
lesson,
|
|
854
|
+
...optionalFailureKind(transient.failureKind),
|
|
855
|
+
transientKind: transient.transientKind,
|
|
856
|
+
...optionalTiming(transient.timing),
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
async function appendRunnerRetryScheduled(input) {
|
|
860
|
+
const { runId, taskId, stepKey, nodeId, failedAttempt, nextAttempt, policy, transient } = input;
|
|
861
|
+
await appendEvent({
|
|
862
|
+
runId,
|
|
863
|
+
taskId,
|
|
864
|
+
stepId: '',
|
|
865
|
+
stepKey,
|
|
866
|
+
type: 'runner_retry_scheduled',
|
|
867
|
+
idempotencyKey: failedAttempt.attemptId,
|
|
868
|
+
payload: {
|
|
869
|
+
nodeId,
|
|
870
|
+
failedAttemptNo: failedAttempt.attemptNo,
|
|
871
|
+
failedAttemptId: failedAttempt.attemptId,
|
|
872
|
+
nextAttemptNo: nextAttempt.attemptNo,
|
|
873
|
+
nextAttemptId: nextAttempt.attemptId,
|
|
874
|
+
maxAttempts: policy.maxAttempts,
|
|
875
|
+
backoffMs: policy.backoffMs,
|
|
876
|
+
reason: runnerBlockReason(transient),
|
|
877
|
+
lesson: runnerBlockLesson(transient),
|
|
878
|
+
...optionalFailureKind(transient.failureKind),
|
|
879
|
+
transientKind: transient.transientKind,
|
|
880
|
+
...optionalTiming(transient.timing),
|
|
881
|
+
},
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
async function appendRunnerRetryExhausted(input) {
|
|
885
|
+
const { runId, taskId, stepKey, nodeId, retry, idempotencyKey } = input;
|
|
886
|
+
await appendEvent({
|
|
887
|
+
runId,
|
|
888
|
+
taskId,
|
|
889
|
+
stepId: '',
|
|
890
|
+
stepKey,
|
|
891
|
+
type: 'runner_retry_exhausted',
|
|
892
|
+
idempotencyKey,
|
|
893
|
+
payload: {
|
|
894
|
+
nodeId,
|
|
895
|
+
...retry,
|
|
896
|
+
},
|
|
897
|
+
});
|
|
443
898
|
}
|
|
444
899
|
/**
|
|
445
900
|
* invokeScript → the built-in system SCRIPT library. The only built-in v1 script is the integrator
|
|
@@ -461,7 +916,20 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
461
916
|
const binding = bindingByRef.get(decision.scriptRef) ?? bindingByRef.get('script:integrator');
|
|
462
917
|
// respondThreads consumes `triage` (plan 0018) — ride the hydrated input on the integrator input so
|
|
463
918
|
// the live script can reply/resolve the triaged threads without a live Revisium read.
|
|
464
|
-
const
|
|
919
|
+
const change = producedChangeFromInputs(inputs);
|
|
920
|
+
const mergeReadiness = mergeReadinessFromInputs(inputs);
|
|
921
|
+
const issueRef = ctx.issueRef;
|
|
922
|
+
const changeForIntegrator = change ? changeWithRunIssueRef(change, issueRef) : undefined;
|
|
923
|
+
const integratorInput = {
|
|
924
|
+
runId,
|
|
925
|
+
taskId: ctx.taskId,
|
|
926
|
+
title: ctx.title,
|
|
927
|
+
base: ctx.base,
|
|
928
|
+
...(issueRef ? { issueRef } : {}),
|
|
929
|
+
...(changeForIntegrator ? { change: changeForIntegrator } : {}),
|
|
930
|
+
...(inputs.triage === undefined ? {} : { triage: inputs.triage }),
|
|
931
|
+
...(mergeReadiness ? { mergeReadiness } : {}),
|
|
932
|
+
};
|
|
465
933
|
// A script node whose resolved runner mechanically performs the merge uses the REAL script;
|
|
466
934
|
// otherwise the pure stub (zero git/gh). Absent a binding (template-only script), default to stub.
|
|
467
935
|
const useReal = !!binding && runnerUsesRealIntegrator(binding.resolvedRunnerId);
|
|
@@ -513,9 +981,21 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
513
981
|
stepId: '',
|
|
514
982
|
stepKey,
|
|
515
983
|
type: 'merge_confirmed',
|
|
516
|
-
payload: {
|
|
984
|
+
payload: {
|
|
985
|
+
prNumber: merged.prNumber,
|
|
986
|
+
prUrl: merged.prUrl,
|
|
987
|
+
...(merged.issueRef ? { issueRef: merged.issueRef } : {}),
|
|
988
|
+
},
|
|
517
989
|
});
|
|
518
|
-
return {
|
|
990
|
+
return {
|
|
991
|
+
outcome: 'ok',
|
|
992
|
+
pointer: {
|
|
993
|
+
merged: true,
|
|
994
|
+
prNumber: merged.prNumber,
|
|
995
|
+
prUrl: merged.prUrl,
|
|
996
|
+
...(merged.issueRef ? { issueRef: merged.issueRef } : {}),
|
|
997
|
+
},
|
|
998
|
+
};
|
|
519
999
|
}
|
|
520
1000
|
if (isPollPr) {
|
|
521
1001
|
// pollPr CLASSIFIES the feedback: its verdict (review_changes/ci_changes/clean) routes the prRouter
|
|
@@ -527,7 +1007,15 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
527
1007
|
stepId: '',
|
|
528
1008
|
stepKey,
|
|
529
1009
|
type: 'pr_polled',
|
|
530
|
-
payload: {
|
|
1010
|
+
payload: {
|
|
1011
|
+
prNumber: feedback.prNumber,
|
|
1012
|
+
headSha: feedback.headSha,
|
|
1013
|
+
verdict: feedback.verdict,
|
|
1014
|
+
evidence: feedback.evidence,
|
|
1015
|
+
ciFailures: feedback.ciFailures.length,
|
|
1016
|
+
reviewThreads: feedback.reviewThreads.length,
|
|
1017
|
+
...(feedback.issueRef ? { issueRef: feedback.issueRef } : {}),
|
|
1018
|
+
},
|
|
531
1019
|
});
|
|
532
1020
|
return { outcome: 'ok', pointer: feedback, verdict: feedback.verdict };
|
|
533
1021
|
}
|
|
@@ -550,9 +1038,26 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
550
1038
|
stepId: '',
|
|
551
1039
|
stepKey,
|
|
552
1040
|
type: 'integrate_succeeded',
|
|
553
|
-
payload: {
|
|
1041
|
+
payload: {
|
|
1042
|
+
prUrl: integrated.prUrl,
|
|
1043
|
+
branch: integrated.branch,
|
|
1044
|
+
prNumber: integrated.prNumber,
|
|
1045
|
+
headSha: integrated.headSha,
|
|
1046
|
+
status: integrated.status,
|
|
1047
|
+
...(integrated.issueRef ? { issueRef: integrated.issueRef } : {}),
|
|
1048
|
+
},
|
|
554
1049
|
});
|
|
555
|
-
return {
|
|
1050
|
+
return {
|
|
1051
|
+
outcome: 'ok',
|
|
1052
|
+
pointer: {
|
|
1053
|
+
prUrl: integrated.prUrl,
|
|
1054
|
+
branch: integrated.branch,
|
|
1055
|
+
prNumber: integrated.prNumber,
|
|
1056
|
+
headSha: integrated.headSha,
|
|
1057
|
+
status: integrated.status,
|
|
1058
|
+
...(integrated.issueRef ? { issueRef: integrated.issueRef } : {}),
|
|
1059
|
+
},
|
|
1060
|
+
};
|
|
556
1061
|
}
|
|
557
1062
|
/** Terminal: finish the run in Revisium per the core's terminal status. */
|
|
558
1063
|
async function finish(runId, status, verdict, steps, failureReason = '') {
|
|
@@ -581,14 +1086,14 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
581
1086
|
* boundary) + marks the Revisium run-row blocked, then returns a `blocked` result WITHOUT entering the
|
|
582
1087
|
* graph. Mirrors the old engine's `blockPipeline({ reason })`.
|
|
583
1088
|
*/
|
|
584
|
-
async function blockWithLesson(runId, taskId, reason, lesson, steps) {
|
|
1089
|
+
async function blockWithLesson(runId, taskId, reason, lesson, steps, retry) {
|
|
585
1090
|
await appendEvent({
|
|
586
1091
|
runId,
|
|
587
1092
|
taskId,
|
|
588
1093
|
stepId: '',
|
|
589
1094
|
stepKey: 'pipeline',
|
|
590
1095
|
type: 'pipeline_blocked',
|
|
591
|
-
payload:
|
|
1096
|
+
payload: pipelineBlockedPayload(reason, lesson, retry),
|
|
592
1097
|
});
|
|
593
1098
|
await blockRun(runId, { actor: 'pipeline', source: `data-driven-${reason}`, reason });
|
|
594
1099
|
return { runId, status: 'blocked', verdict: 'blocked', steps };
|