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