@revisium/orchestrator 0.1.0-alpha.9 → 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 +143 -157
- package/control-plane/default-playbook/catalog/pipelines.json +217 -8
- 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/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 +12 -8
- 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/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/mcp/mcp-capabilities.js +14 -1
- package/dist/mcp/mcp-capabilities.js.map +1 -1
- package/dist/mcp/mcp-facade.service.js +306 -11
- package/dist/mcp/mcp-facade.service.js.map +1 -1
- package/dist/mcp/mcp-tools.js +41 -14
- 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 +482 -40
- 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/kit/fixtures.js +72 -21
- package/dist/pipeline-core/kit/fixtures.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 +10 -3
- 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 +13 -8
- package/dist/runners/integrator-branch-naming.js.map +1 -1
- package/dist/runners/integrator.js +254 -89
- 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 +234 -3
- package/dist/task-control-plane/run-watch.service.js.map +1 -1
- package/dist/task-control-plane/task-control-plane-api.service.js +272 -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
|
@@ -31,8 +31,61 @@ import { step as coreStep, initialState, validateTemplate, InterpretError, } fro
|
|
|
31
31
|
import { runnerNeedsLivePreflight, runnerUsesRealIntegrator } from './route-contract.js';
|
|
32
32
|
import { redactEventPayload } from '../run/append-event.js';
|
|
33
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';
|
|
34
37
|
export const RUN_PROGRESS_EVENT_KEY = 'run-progress';
|
|
35
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
|
+
}
|
|
36
89
|
/**
|
|
37
90
|
* Reserved engine error codes (matched only by a node's `catch`, §3/§6).
|
|
38
91
|
*
|
|
@@ -48,6 +101,31 @@ const REVO_SCRIPT_FAILED = 'revo.ScriptFailed';
|
|
|
48
101
|
const REVO_SCRIPT_BLOCKED = 'revo.ScriptBlocked';
|
|
49
102
|
const REVO_RESULT_INVALID = 'revo.ResultInvalid';
|
|
50
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
|
+
}
|
|
51
129
|
/** Per-node execution stepKey: the bare nodeId on the first entry (stable ids for existing tests), an
|
|
52
130
|
* ordinal-suffixed key on loop re-entries so attempts/events/outputs are distinct per iteration (0016
|
|
53
131
|
* §4.1 — fixes the latent 0015 stepKey-reuse collision). */
|
|
@@ -64,6 +142,125 @@ function nextOrdinal(byNode, nodeId) {
|
|
|
64
142
|
function isRecord(value) {
|
|
65
143
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
66
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
|
+
}
|
|
67
264
|
/** Extract the DOMAIN verdict from the only supported source: top-level AttemptResult.verdict. */
|
|
68
265
|
function domainVerdictOf(result) {
|
|
69
266
|
if (typeof result.verdict === 'string' && result.verdict.trim().length > 0) {
|
|
@@ -205,7 +402,7 @@ export function buildGateSummary(decision, outputsByNode, lastVerdict) {
|
|
|
205
402
|
* production and tests. Returns a plain async function with the workflow signature.
|
|
206
403
|
*/
|
|
207
404
|
export function makeDataDrivenTask(runStepFn, deps) {
|
|
208
|
-
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;
|
|
209
406
|
/** Resolve a node's `consumes` from the workflow-local output accumulator — NOT live Revisium reads
|
|
210
407
|
* (0016 §6 / consensus M4: a live read on replay can see rows written past the replay point). */
|
|
211
408
|
function resolveConsumes(node, outputsByNode) {
|
|
@@ -240,7 +437,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
240
437
|
}
|
|
241
438
|
/** Record a node's produced output to the workflow-local accumulator + Revisium (when `produces`). */
|
|
242
439
|
// (gate-summary helpers live at module scope below — they are pure over outputsByNode.)
|
|
243
|
-
async function recordOutput(runId, node, ordinal, output, outputsByNode) {
|
|
440
|
+
async function recordOutput(runId, node, ordinal, attemptId, output, outputsByNode) {
|
|
244
441
|
if (!('produces' in node) || !node.produces)
|
|
245
442
|
return;
|
|
246
443
|
const row = {
|
|
@@ -250,7 +447,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
250
447
|
name: node.produces.name,
|
|
251
448
|
schemaRef: ('resultSchema' in node && node.resultSchema) || '',
|
|
252
449
|
payload: output,
|
|
253
|
-
attemptId
|
|
450
|
+
attemptId,
|
|
254
451
|
};
|
|
255
452
|
const list = outputsByNode.get(node.id) ?? [];
|
|
256
453
|
list.push(row);
|
|
@@ -283,7 +480,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
283
480
|
if (diagnostics.length > 0) {
|
|
284
481
|
throw new Error(`PINNED_TEMPLATE_INVALID: ${template.pipelineId} — ${diagnostics.map((d) => d.code).join(', ')}`);
|
|
285
482
|
}
|
|
286
|
-
const { taskId, title, base } = await loadRunTaskContext(runId);
|
|
483
|
+
const { taskId, title, base, issueRef } = await loadRunTaskContext(runId);
|
|
287
484
|
// B5/B7 — live preflight: one memoized DBOS step, evaluated exactly once BEFORE the graph runs.
|
|
288
485
|
// Skipped entirely when every selected binding resolves to a stub/script runner (mirrors the old
|
|
289
486
|
// engine's gate). A `needsHuman` preflight blocks the run (clean/base invariant unmet) and surfaces
|
|
@@ -292,7 +489,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
292
489
|
const live = route.roleBindings.some((b) => runnerNeedsLivePreflight(b.resolvedRunnerId));
|
|
293
490
|
if (live) {
|
|
294
491
|
// Preflight runs against the BASE checkout (resolveTaskCwd), BEFORE the worktree exists — its
|
|
295
|
-
// fetch + clean/
|
|
492
|
+
// fetch + clean/base-availability checks protect the user's base repo. Ordering is load-bearing.
|
|
296
493
|
const pf = await preflightFn(taskId, base);
|
|
297
494
|
if ('needsHuman' in pf) {
|
|
298
495
|
return await blockWithLesson(runId, taskId, 'preflight', pf.lesson, 0);
|
|
@@ -310,9 +507,9 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
310
507
|
let result;
|
|
311
508
|
try {
|
|
312
509
|
if (live) {
|
|
313
|
-
await createWorktreeFn(runId, taskId, title, base);
|
|
510
|
+
await createWorktreeFn(runId, taskId, title, base, issueRef);
|
|
314
511
|
}
|
|
315
|
-
result = await runGraph(runId, opts, taskId, title, base);
|
|
512
|
+
result = await runGraph(runId, opts, taskId, title, base, issueRef, live);
|
|
316
513
|
return result;
|
|
317
514
|
}
|
|
318
515
|
finally {
|
|
@@ -327,8 +524,8 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
327
524
|
}
|
|
328
525
|
}
|
|
329
526
|
/** The pipeline-core interpretation loop, extracted so the worktree `finally` in runBody wraps it. */
|
|
330
|
-
async function runGraph(runId, opts, taskId, title, base) {
|
|
331
|
-
const { route, template } = opts;
|
|
527
|
+
async function runGraph(runId, opts, taskId, title, base, issueRef, live) {
|
|
528
|
+
const { route, template, runnerRetryPolicy } = opts;
|
|
332
529
|
// Capability resolution map (GENERIC — no role-ids in the engine): roleRef/scriptRef → route binding.
|
|
333
530
|
// The route's bindings are authoritative for runner dispatch (and durable on recovery via the DBOS
|
|
334
531
|
// workflow input). A `role:<id>` handle resolves to the binding whose roleId matches `<id>`. A
|
|
@@ -363,17 +560,25 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
363
560
|
return await finish(runId, decision.status, lastVerdict, stepCount, lastFailureReason);
|
|
364
561
|
}
|
|
365
562
|
const eff = await applyDecision(decision, {
|
|
366
|
-
runId, template, bindingByRef, executionProfile, taskId, title, base,
|
|
367
|
-
effectOrdinalByNode, outputsByNode,
|
|
563
|
+
runId, template, bindingByRef, executionProfile, taskId, title, base, issueRef,
|
|
564
|
+
effectOrdinalByNode, outputsByNode, runnerRetryPolicy,
|
|
565
|
+
live,
|
|
368
566
|
// The verdict from the PRIOR effect (the reviewer/poller that routed into this node). At an
|
|
369
567
|
// awaitGate this is the routing verdict that opened the gate → D3's default reviewerVerdict.
|
|
370
568
|
lastVerdict,
|
|
371
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
|
+
}
|
|
372
578
|
lastResult = eff.lastResult;
|
|
373
579
|
if (eff.lastVerdict !== undefined)
|
|
374
580
|
lastVerdict = eff.lastVerdict;
|
|
375
581
|
lastFailureReason = eff.failureReason ?? '';
|
|
376
|
-
stepCount += eff.stepDelta;
|
|
377
582
|
}
|
|
378
583
|
throw new InterpretError(`data-driven ${template.pipelineId} did not terminate within ${MAX_STEPS} steps (template loop bug)`);
|
|
379
584
|
}
|
|
@@ -401,19 +606,35 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
401
606
|
return { lastResult: { outcome: 'failed', errorCode: REVO_INPUT_MISSING }, lastVerdict: 'failed', stepDelta: 1 };
|
|
402
607
|
}
|
|
403
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
|
+
}
|
|
404
624
|
if (result.failed) {
|
|
405
625
|
await appendEvent({
|
|
406
626
|
runId, taskId, stepId: '', stepKey, type: 'step_failed',
|
|
627
|
+
idempotencyKey: result.attemptId,
|
|
407
628
|
payload: { nodeId: node.id, error: result.errorCode },
|
|
408
629
|
});
|
|
409
|
-
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 };
|
|
410
631
|
}
|
|
411
|
-
await recordOutput(runId, node, ordinal, result.output, ctx.outputsByNode);
|
|
632
|
+
await recordOutput(runId, node, ordinal, result.attemptId, result.output, ctx.outputsByNode);
|
|
412
633
|
const verdict = result.verdict;
|
|
413
634
|
return {
|
|
414
635
|
lastResult: { outcome: 'succeeded', ...(verdict ? { verdict } : {}) },
|
|
415
636
|
...(verdict ? { lastVerdict: verdict } : {}),
|
|
416
|
-
stepDelta:
|
|
637
|
+
stepDelta: result.attemptsMade,
|
|
417
638
|
};
|
|
418
639
|
}
|
|
419
640
|
case 'invokeScript': {
|
|
@@ -429,7 +650,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
429
650
|
});
|
|
430
651
|
return { lastResult: { outcome: 'failed', errorCode: REVO_INPUT_MISSING }, lastVerdict: 'failed', stepDelta: 1 };
|
|
431
652
|
}
|
|
432
|
-
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);
|
|
433
654
|
// A blocked script (needsHuman) routes via revo.ScriptBlocked → a `blocked` terminal; a thrown
|
|
434
655
|
// script routes via revo.ScriptFailed → a `failed` terminal (§6 catch). The lesson-bearing
|
|
435
656
|
// pipeline_blocked is emitted inside invokeScript for the block path (parity with the old engine).
|
|
@@ -439,7 +660,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
439
660
|
if (scriptResult.outcome === 'failed') {
|
|
440
661
|
return { lastResult: { outcome: 'failed', errorCode: REVO_SCRIPT_FAILED }, lastVerdict: 'failed', stepDelta: 1 };
|
|
441
662
|
}
|
|
442
|
-
await recordOutput(runId, node, ordinal, scriptResult.pointer, ctx.outputsByNode);
|
|
663
|
+
await recordOutput(runId, node, ordinal, stepKeyFor(node.id, ordinal), scriptResult.pointer, ctx.outputsByNode);
|
|
443
664
|
// A classifying script (pollPr, plan 0018) surfaces a DOMAIN verdict so the next `choice` can
|
|
444
665
|
// route on it (§8). Scripts with a single fixed `next` (integrator/confirmMerge) carry none.
|
|
445
666
|
const sv = scriptResult.verdict;
|
|
@@ -480,29 +701,200 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
480
701
|
// ── Effect executors ─────────────────────────────────────────────────────────
|
|
481
702
|
/** invokeRole → dispatch via the existing runStep DBOS step; validate result vs resultSchema. */
|
|
482
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) {
|
|
483
711
|
const binding = ctx.bindingByRef.get(decision.roleRef);
|
|
484
|
-
if (
|
|
712
|
+
if (binding === undefined) {
|
|
485
713
|
// A VALID template's caps resolve at run start; an unresolved roleRef is a fatal config gap.
|
|
486
714
|
throw new Error(`CAPABILITY_UNRESOLVED: roleRef ${decision.roleRef} has no route binding`);
|
|
487
715
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
const
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
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;
|
|
495
770
|
if (result.needsHuman) {
|
|
496
|
-
|
|
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 });
|
|
497
783
|
}
|
|
498
|
-
|
|
499
|
-
|
|
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';
|
|
500
804
|
}
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
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
|
+
});
|
|
821
|
+
}
|
|
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;
|
|
504
839
|
}
|
|
505
|
-
return {
|
|
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
|
+
});
|
|
506
898
|
}
|
|
507
899
|
/**
|
|
508
900
|
* invokeScript → the built-in system SCRIPT library. The only built-in v1 script is the integrator
|
|
@@ -524,7 +916,20 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
524
916
|
const binding = bindingByRef.get(decision.scriptRef) ?? bindingByRef.get('script:integrator');
|
|
525
917
|
// respondThreads consumes `triage` (plan 0018) — ride the hydrated input on the integrator input so
|
|
526
918
|
// the live script can reply/resolve the triaged threads without a live Revisium read.
|
|
527
|
-
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
|
+
};
|
|
528
933
|
// A script node whose resolved runner mechanically performs the merge uses the REAL script;
|
|
529
934
|
// otherwise the pure stub (zero git/gh). Absent a binding (template-only script), default to stub.
|
|
530
935
|
const useReal = !!binding && runnerUsesRealIntegrator(binding.resolvedRunnerId);
|
|
@@ -576,9 +981,21 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
576
981
|
stepId: '',
|
|
577
982
|
stepKey,
|
|
578
983
|
type: 'merge_confirmed',
|
|
579
|
-
payload: {
|
|
984
|
+
payload: {
|
|
985
|
+
prNumber: merged.prNumber,
|
|
986
|
+
prUrl: merged.prUrl,
|
|
987
|
+
...(merged.issueRef ? { issueRef: merged.issueRef } : {}),
|
|
988
|
+
},
|
|
580
989
|
});
|
|
581
|
-
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
|
+
};
|
|
582
999
|
}
|
|
583
1000
|
if (isPollPr) {
|
|
584
1001
|
// pollPr CLASSIFIES the feedback: its verdict (review_changes/ci_changes/clean) routes the prRouter
|
|
@@ -590,7 +1007,15 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
590
1007
|
stepId: '',
|
|
591
1008
|
stepKey,
|
|
592
1009
|
type: 'pr_polled',
|
|
593
|
-
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
|
+
},
|
|
594
1019
|
});
|
|
595
1020
|
return { outcome: 'ok', pointer: feedback, verdict: feedback.verdict };
|
|
596
1021
|
}
|
|
@@ -613,9 +1038,26 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
613
1038
|
stepId: '',
|
|
614
1039
|
stepKey,
|
|
615
1040
|
type: 'integrate_succeeded',
|
|
616
|
-
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
|
+
},
|
|
617
1049
|
});
|
|
618
|
-
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
|
+
};
|
|
619
1061
|
}
|
|
620
1062
|
/** Terminal: finish the run in Revisium per the core's terminal status. */
|
|
621
1063
|
async function finish(runId, status, verdict, steps, failureReason = '') {
|
|
@@ -644,14 +1086,14 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
644
1086
|
* boundary) + marks the Revisium run-row blocked, then returns a `blocked` result WITHOUT entering the
|
|
645
1087
|
* graph. Mirrors the old engine's `blockPipeline({ reason })`.
|
|
646
1088
|
*/
|
|
647
|
-
async function blockWithLesson(runId, taskId, reason, lesson, steps) {
|
|
1089
|
+
async function blockWithLesson(runId, taskId, reason, lesson, steps, retry) {
|
|
648
1090
|
await appendEvent({
|
|
649
1091
|
runId,
|
|
650
1092
|
taskId,
|
|
651
1093
|
stepId: '',
|
|
652
1094
|
stepKey: 'pipeline',
|
|
653
1095
|
type: 'pipeline_blocked',
|
|
654
|
-
payload:
|
|
1096
|
+
payload: pipelineBlockedPayload(reason, lesson, retry),
|
|
655
1097
|
});
|
|
656
1098
|
await blockRun(runId, { actor: 'pipeline', source: `data-driven-${reason}`, reason });
|
|
657
1099
|
return { runId, status: 'blocked', verdict: 'blocked', steps };
|