@revisium/orchestrator 0.2.0-alpha.3 → 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 +4 -4
- package/control-plane/bootstrap.config.json +108 -550
- package/control-plane/default-playbook/catalog/pipelines.json +446 -1123
- package/control-plane/default-playbook/catalog/roles.json +1 -1
- package/control-plane/default-playbook/catalog/run-profiles.json +338 -0
- package/control-plane/default-playbook/package.json +1 -1
- package/control-plane/default-playbook/playbook.json +2 -1
- package/control-plane/default-playbook/prompts/developer.md +6 -6
- package/control-plane/default-playbook/prompts/integrator.md +1 -1
- package/dist/__generated__/client/browser.js +18 -0
- package/dist/__generated__/client/browser.js.map +1 -0
- package/dist/__generated__/client/client.js +32 -0
- package/dist/__generated__/client/client.js.map +1 -0
- package/dist/__generated__/client/commonInputTypes.js +11 -0
- package/dist/__generated__/client/commonInputTypes.js.map +1 -0
- package/dist/__generated__/client/enums.js +19 -0
- package/dist/__generated__/client/enums.js.map +1 -0
- package/dist/__generated__/client/internal/class.js +53 -0
- package/dist/__generated__/client/internal/class.js.map +1 -0
- package/dist/__generated__/client/internal/prismaNamespace.js +450 -0
- package/dist/__generated__/client/internal/prismaNamespace.js.map +1 -0
- package/dist/__generated__/client/internal/prismaNamespaceBrowser.js +421 -0
- package/dist/__generated__/client/internal/prismaNamespaceBrowser.js.map +1 -0
- package/dist/__generated__/client/models/Branch.js +2 -0
- package/dist/__generated__/client/models/Branch.js.map +1 -0
- package/dist/__generated__/client/models/CostLedgerEntry.js +2 -0
- package/dist/__generated__/client/models/CostLedgerEntry.js.map +1 -0
- package/dist/__generated__/client/models/FileBlob.js +2 -0
- package/dist/__generated__/client/models/FileBlob.js.map +1 -0
- package/dist/__generated__/client/models/InboxItem.js +2 -0
- package/dist/__generated__/client/models/InboxItem.js.map +1 -0
- package/dist/__generated__/client/models/ProjectFileUsage.js +2 -0
- package/dist/__generated__/client/models/ProjectFileUsage.js.map +1 -0
- package/dist/__generated__/client/models/Revision.js +2 -0
- package/dist/__generated__/client/models/Revision.js.map +1 -0
- package/dist/__generated__/client/models/RevoProject.js +2 -0
- package/dist/__generated__/client/models/RevoProject.js.map +1 -0
- package/dist/__generated__/client/models/Row.js +2 -0
- package/dist/__generated__/client/models/Row.js.map +1 -0
- package/dist/__generated__/client/models/RunAttempt.js +2 -0
- package/dist/__generated__/client/models/RunAttempt.js.map +1 -0
- package/dist/__generated__/client/models/RunEvent.js +2 -0
- package/dist/__generated__/client/models/RunEvent.js.map +1 -0
- package/dist/__generated__/client/models/RunOutput.js +2 -0
- package/dist/__generated__/client/models/RunOutput.js.map +1 -0
- package/dist/__generated__/client/models/RunTask.js +2 -0
- package/dist/__generated__/client/models/RunTask.js.map +1 -0
- package/dist/__generated__/client/models/Table.js +2 -0
- package/dist/__generated__/client/models/Table.js.map +1 -0
- package/dist/__generated__/client/models/TableMigration.js +2 -0
- package/dist/__generated__/client/models/TableMigration.js.map +1 -0
- package/dist/__generated__/client/models/TaskRun.js +2 -0
- package/dist/__generated__/client/models/TaskRun.js.map +1 -0
- package/dist/__generated__/client/models.js +2 -0
- package/dist/__generated__/client/models.js.map +1 -0
- package/dist/api/graphql-api/graphql-ws/pubsub.module.js +2 -1
- package/dist/api/graphql-api/graphql-ws/pubsub.module.js.map +1 -1
- package/dist/api/graphql-api/graphql-ws/subscription-bridge.service.js +110 -28
- package/dist/api/graphql-api/graphql-ws/subscription-bridge.service.js.map +1 -1
- package/dist/api/graphql-api/graphql-ws/subscription-mappers.js +3 -1
- package/dist/api/graphql-api/graphql-ws/subscription-mappers.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/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/registerGraphqlEnums.js +6 -0
- package/dist/api/graphql-api/registerGraphqlEnums.js.map +1 -1
- package/dist/api/graphql-api/runs/inputs/create-run.input.js +17 -1
- 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 +11 -1
- package/dist/api/graphql-api/runs/inputs/simulate-route.input.js.map +1 -1
- package/dist/api/graphql-api/runs/model/run-workflow.model.js +5 -0
- package/dist/api/graphql-api/runs/model/run-workflow.model.js.map +1 -1
- package/dist/api/graphql-api/share/model/issue-action.model.js +7 -0
- package/dist/api/graphql-api/share/model/issue-action.model.js.map +1 -0
- package/dist/cli/commands/doctor-report.js +1 -13
- package/dist/cli/commands/doctor-report.js.map +1 -1
- package/dist/cli/commands/lifecycle.js +50 -65
- package/dist/cli/commands/lifecycle.js.map +1 -1
- package/dist/cli/commands/{revisium-helpers.js → process-helpers.js} +1 -24
- package/dist/cli/commands/process-helpers.js.map +1 -0
- package/dist/config.js +17 -55
- package/dist/config.js.map +1 -1
- package/dist/control-plane/bootstrap-json-validator.js +65 -0
- package/dist/control-plane/bootstrap-json-validator.js.map +1 -0
- package/dist/control-plane/bootstrap.js +25 -38
- package/dist/control-plane/bootstrap.js.map +1 -1
- package/dist/control-plane/change-notifications.js +45 -14
- package/dist/control-plane/change-notifications.js.map +1 -1
- package/dist/control-plane/data-access.js +1 -106
- package/dist/control-plane/data-access.js.map +1 -1
- package/dist/control-plane/default-playbook-policy.js +488 -94
- package/dist/control-plane/default-playbook-policy.js.map +1 -1
- package/dist/control-plane/definitions.js +4 -5
- package/dist/control-plane/definitions.js.map +1 -1
- package/dist/control-plane/engine-transport.js +383 -0
- package/dist/control-plane/engine-transport.js.map +1 -0
- package/dist/control-plane/inbox.js +15 -7
- package/dist/control-plane/inbox.js.map +1 -1
- package/dist/control-plane/index.js +0 -1
- package/dist/control-plane/index.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/merge-override-audit.js +2 -2
- package/dist/control-plane/merge-override-audit.js.map +1 -1
- package/dist/control-plane/pipeline-coverage-registry.js +486 -0
- package/dist/control-plane/pipeline-coverage-registry.js.map +1 -0
- package/dist/control-plane/query-types.js +2 -0
- package/dist/control-plane/query-types.js.map +1 -0
- package/dist/control-plane/run-profiles.js +175 -0
- package/dist/control-plane/run-profiles.js.map +1 -0
- package/dist/control-plane/schema-migration.js +0 -35
- package/dist/control-plane/schema-migration.js.map +1 -1
- package/dist/control-plane/seed-default-playbook.js +2 -31
- package/dist/control-plane/seed-default-playbook.js.map +1 -1
- package/dist/control-plane/tables.js +8 -0
- package/dist/control-plane/tables.js.map +1 -1
- package/dist/control-plane/transport.js +16 -0
- package/dist/control-plane/transport.js.map +1 -0
- package/dist/control-plane/versioned-meaning.js +72 -15
- package/dist/control-plane/versioned-meaning.js.map +1 -1
- package/dist/e2e/kit/agents.js +63 -8
- package/dist/e2e/kit/agents.js.map +1 -1
- package/dist/e2e/kit/assertions.js +15 -3
- package/dist/e2e/kit/assertions.js.map +1 -1
- package/dist/e2e/kit/crash.js +8 -4
- package/dist/e2e/kit/crash.js.map +1 -1
- package/dist/e2e/kit/drive.js +46 -11
- package/dist/e2e/kit/drive.js.map +1 -1
- package/dist/e2e/kit/env.js +17 -1
- package/dist/e2e/kit/env.js.map +1 -1
- package/dist/e2e/kit/fake-integrator.js +9 -22
- package/dist/e2e/kit/fake-integrator.js.map +1 -1
- package/dist/e2e/kit/gh-emulator.js +166 -26
- package/dist/e2e/kit/gh-emulator.js.map +1 -1
- package/dist/e2e/kit/harness.js +44 -10
- package/dist/e2e/kit/harness.js.map +1 -1
- package/dist/e2e/kit/index.js +2 -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 +207 -0
- package/dist/e2e/kit/scenario.js.map +1 -0
- package/dist/e2e/kit/scenarios.js +38 -45
- 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/engine/dbos.service.js +8 -1
- package/dist/engine/dbos.service.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/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/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/host/daemon.js +32 -8
- package/dist/host/daemon.js.map +1 -1
- package/dist/host/ensure-host.js +3 -3
- package/dist/host/ensure-host.js.map +1 -1
- package/dist/host/host-runtime.js.map +1 -1
- package/dist/host/host.lifecycle.js +3 -16
- package/dist/host/host.lifecycle.js.map +1 -1
- package/dist/http/graphql-host.js +17 -8
- package/dist/http/graphql-host.js.map +1 -1
- package/dist/mcp/mcp-capabilities.js +8 -2
- package/dist/mcp/mcp-capabilities.js.map +1 -1
- package/dist/mcp/mcp-facade.service.js +53 -10
- 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 +108 -1
- 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 +573 -105
- package/dist/pipeline/data-driven-task.workflow.js.map +1 -1
- package/dist/pipeline/pipeline.service.js +28 -12
- package/dist/pipeline/pipeline.service.js.map +1 -1
- package/dist/pipeline/route-contract.js +80 -39
- package/dist/pipeline/route-contract.js.map +1 -1
- package/dist/pipeline-core/condition-scopes.js +15 -0
- package/dist/pipeline-core/condition-scopes.js.map +1 -0
- package/dist/pipeline-core/index.js +2 -0
- package/dist/pipeline-core/index.js.map +1 -1
- package/dist/pipeline-core/kit/fixtures.js +81 -44
- package/dist/pipeline-core/kit/fixtures.js.map +1 -1
- package/dist/pipeline-core/materialize.js +221 -0
- package/dist/pipeline-core/materialize.js.map +1 -0
- package/dist/pipeline-core/types.js +5 -0
- package/dist/pipeline-core/types.js.map +1 -1
- package/dist/pipeline-core/validate-loops.js +2 -15
- package/dist/pipeline-core/validate-loops.js.map +1 -1
- package/dist/pipeline-core/validate-resilience.js +257 -0
- package/dist/pipeline-core/validate-resilience.js.map +1 -0
- package/dist/pipeline-core/validate.js +2 -0
- package/dist/pipeline-core/validate.js.map +1 -1
- package/dist/playbook/catalog-loader.js +40 -3
- package/dist/playbook/catalog-loader.js.map +1 -1
- package/dist/playbook/catalog-schema-validator.js +468 -0
- package/dist/playbook/catalog-schema-validator.js.map +1 -0
- package/dist/playbook/import-mapper.js +55 -3
- package/dist/playbook/import-mapper.js.map +1 -1
- package/dist/playbook/manifest.js +5 -0
- package/dist/playbook/manifest.js.map +1 -1
- package/dist/playbook/playbook-installer.js +17 -1
- package/dist/playbook/playbook-installer.js.map +1 -1
- package/dist/poller/pr-readiness-core.js +257 -93
- package/dist/poller/pr-readiness-core.js.map +1 -1
- package/dist/poller/pr-readiness.js +2 -2
- package/dist/poller/pr-readiness.js.map +1 -1
- package/dist/revisium/inbox.service.js +8 -8
- package/dist/revisium/inbox.service.js.map +1 -1
- package/dist/revisium/playbooks.service.js +311 -21
- package/dist/revisium/playbooks.service.js.map +1 -1
- package/dist/revisium/revisium.module.js +8 -8
- package/dist/revisium/revisium.module.js.map +1 -1
- package/dist/revisium/roles.service.js +3 -1
- package/dist/revisium/roles.service.js.map +1 -1
- package/dist/revisium/run.service.js +9 -8
- package/dist/revisium/run.service.js.map +1 -1
- package/dist/revisium/tokens.js +0 -1
- package/dist/revisium/tokens.js.map +1 -1
- package/dist/run/create-run.js +1 -4
- package/dist/run/create-run.js.map +1 -1
- package/dist/run/inspect-run.js +33 -29
- package/dist/run/inspect-run.js.map +1 -1
- package/dist/run/issue-ref.js +97 -2
- package/dist/run/issue-ref.js.map +1 -1
- package/dist/run/prisma-runtime-data-access.js +616 -0
- package/dist/run/prisma-runtime-data-access.js.map +1 -0
- package/dist/run/run-outputs.js +39 -14
- package/dist/run/run-outputs.js.map +1 -1
- package/dist/run/terminal-run-status.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 +692 -89
- package/dist/runners/integrator.js.map +1 -1
- package/dist/runners/worktree.service.js +10 -8
- package/dist/runners/worktree.service.js.map +1 -1
- package/dist/schema/ajv-errors.js +15 -0
- package/dist/schema/ajv-errors.js.map +1 -0
- package/dist/smoke/isolation.js +7 -16
- package/dist/smoke/isolation.js.map +1 -1
- package/dist/storage/ensure-storage.js +118 -0
- package/dist/storage/ensure-storage.js.map +1 -0
- package/dist/storage/revo-database.js +39 -0
- package/dist/storage/revo-database.js.map +1 -0
- package/dist/storage/revo-prisma.service.js +31 -0
- package/dist/storage/revo-prisma.service.js.map +1 -0
- package/dist/storage/revo-storage.module.js +18 -0
- package/dist/storage/revo-storage.module.js.map +1 -0
- package/dist/task-control-plane/pr-readiness.service.js +5 -2
- package/dist/task-control-plane/pr-readiness.service.js.map +1 -1
- package/dist/task-control-plane/run-watch.service.js +3 -6
- package/dist/task-control-plane/run-watch.service.js.map +1 -1
- package/dist/task-control-plane/task-control-plane-api.service.js +518 -85
- package/dist/task-control-plane/task-control-plane-api.service.js.map +1 -1
- package/dist/testing/runtime-data-access.js +175 -0
- package/dist/testing/runtime-data-access.js.map +1 -0
- package/dist/worker/build-context.js +178 -2
- package/dist/worker/build-context.js.map +1 -1
- package/dist/worker/claude-code-runner.js +23 -6
- package/dist/worker/claude-code-runner.js.map +1 -1
- package/dist/worker/codex-runner.js +6 -1
- package/dist/worker/codex-runner.js.map +1 -1
- package/package.json +20 -7
- package/prisma/migrations/20260707080500_initial_schema/migration.sql +259 -0
- package/prisma/migrations/20260707103000_prisma_runtime_tables/migration.sql +234 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +338 -0
- package/prisma.config.ts +11 -0
- package/dist/cli/commands/revisium-helpers.js.map +0 -1
- package/dist/control-plane/client-transport.js +0 -168
- package/dist/control-plane/client-transport.js.map +0 -1
- package/dist/engine/ensure-postgres.js +0 -46
- package/dist/engine/ensure-postgres.js.map +0 -1
- package/dist/host/ensure-revisium.js +0 -135
- package/dist/host/ensure-revisium.js.map +0 -1
- package/revisium.config.json +0 -10
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { step as coreStep, initialState, validateTemplate, InterpretError, selectJoinWinner, reduceJoinVerdict, } from '../pipeline-core/index.js';
|
|
2
|
-
import {
|
|
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';
|
|
6
|
-
import { normalizeIssueRef } from '../run/issue-ref.js';
|
|
6
|
+
import { normalizeIssueAction, normalizeIssueRef } from '../run/issue-ref.js';
|
|
7
7
|
import { RUNNER_IDLE_TIMEOUT_KIND, RUNNER_WALL_CLOCK_LIMIT_KIND, } from '../worker/process-executor.js';
|
|
8
8
|
export const RUN_PROGRESS_EVENT_KEY = 'run-progress';
|
|
9
|
+
export const INTEGRATOR_PROGRESS_EVENT_TYPES = ['integrate_succeeded', 'foreign_pr_adopted'];
|
|
9
10
|
const MAX_STEPS = 1_000;
|
|
10
11
|
const DEFAULT_RUNNER_TRANSIENT_MAX_ATTEMPTS = 2;
|
|
11
12
|
const DEFAULT_RUNNER_TRANSIENT_RETRY_BACKOFF_MS = 2_000;
|
|
13
|
+
const TRANSIENT_RETRY_GATE_OUTCOMES = ['retry', 'give_up'];
|
|
12
14
|
function readPositiveIntegerEnv(env, key, fallback) {
|
|
13
15
|
const raw = env[key];
|
|
14
16
|
if (raw === undefined)
|
|
@@ -44,11 +46,12 @@ function physicalAttemptFor(runId, stepKey, attemptNo) {
|
|
|
44
46
|
attemptId: `attempt_${fnv1a64Hex(attemptKey)}`,
|
|
45
47
|
};
|
|
46
48
|
}
|
|
47
|
-
function stepInputForAttempt(nodeId, inputs, attempt) {
|
|
49
|
+
function stepInputForAttempt(nodeId, inputs, attempt, retryContext) {
|
|
48
50
|
return {
|
|
49
51
|
nodeId,
|
|
50
52
|
attempt: { attemptNo: attempt.attemptNo, attemptId: attempt.attemptId },
|
|
51
53
|
...(Object.keys(inputs).length > 0 ? { inputs } : {}),
|
|
54
|
+
...(retryContext ? { retryContext } : {}),
|
|
52
55
|
};
|
|
53
56
|
}
|
|
54
57
|
function optionalTiming(timing) {
|
|
@@ -97,6 +100,19 @@ function nextOrdinal(byNode, nodeId) {
|
|
|
97
100
|
function isRecord(value) {
|
|
98
101
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
99
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
|
+
}
|
|
100
116
|
function producedChangeArtifact(value) {
|
|
101
117
|
if (!isRecord(value))
|
|
102
118
|
return undefined;
|
|
@@ -108,10 +124,12 @@ function producedChangeArtifact(value) {
|
|
|
108
124
|
if (typeof headSha !== 'string' || headSha.trim().length === 0)
|
|
109
125
|
return undefined;
|
|
110
126
|
const issueRef = normalizeArtifactIssueRef(candidate.issueRef);
|
|
127
|
+
const issueAction = normalizeArtifactIssueAction(candidate.issueAction);
|
|
111
128
|
return {
|
|
112
129
|
branch,
|
|
113
130
|
headSha,
|
|
114
131
|
...(issueRef ? { issueRef } : {}),
|
|
132
|
+
...(issueAction ? { issueAction } : {}),
|
|
115
133
|
...(typeof candidate.worktreePath === 'string' && candidate.worktreePath.trim() ? { worktreePath: candidate.worktreePath } : {}),
|
|
116
134
|
...(typeof candidate.artifactRef === 'string' && candidate.artifactRef.trim() ? { artifactRef: candidate.artifactRef } : {}),
|
|
117
135
|
...(typeof candidate.prNumber === 'number' && Number.isSafeInteger(candidate.prNumber) ? { prNumber: candidate.prNumber } : {}),
|
|
@@ -125,7 +143,15 @@ function normalizeArtifactIssueRef(value) {
|
|
|
125
143
|
return undefined;
|
|
126
144
|
}
|
|
127
145
|
}
|
|
128
|
-
function
|
|
146
|
+
function normalizeArtifactIssueAction(value) {
|
|
147
|
+
try {
|
|
148
|
+
return normalizeIssueAction(value, 'change.issueAction');
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function changeWithRunIssueContract(change, issueRef, issueAction) {
|
|
129
155
|
const normalized = { ...change };
|
|
130
156
|
if (issueRef) {
|
|
131
157
|
normalized.issueRef = issueRef;
|
|
@@ -133,6 +159,12 @@ function changeWithRunIssueRef(change, issueRef) {
|
|
|
133
159
|
else {
|
|
134
160
|
delete normalized.issueRef;
|
|
135
161
|
}
|
|
162
|
+
if (issueAction) {
|
|
163
|
+
normalized.issueAction = issueAction;
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
delete normalized.issueAction;
|
|
167
|
+
}
|
|
136
168
|
return normalized;
|
|
137
169
|
}
|
|
138
170
|
function producedChangeFromInputs(inputs) {
|
|
@@ -150,7 +182,16 @@ function mergeReadinessFromInputs(inputs) {
|
|
|
150
182
|
const headSha = value.headSha;
|
|
151
183
|
if (typeof headSha !== 'string' || headSha.trim().length === 0)
|
|
152
184
|
return undefined;
|
|
153
|
-
|
|
185
|
+
const override = isRecord(value.override) && value.override.accepted === true
|
|
186
|
+
? { accepted: true }
|
|
187
|
+
: undefined;
|
|
188
|
+
return { headSha, ...(override ? { override } : {}) };
|
|
189
|
+
}
|
|
190
|
+
function headShaFromPayload(value) {
|
|
191
|
+
if (!isRecord(value))
|
|
192
|
+
return undefined;
|
|
193
|
+
const headSha = value.headSha;
|
|
194
|
+
return typeof headSha === 'string' && headSha.trim().length > 0 ? headSha.trim() : undefined;
|
|
154
195
|
}
|
|
155
196
|
function attachProducedChange(output, change) {
|
|
156
197
|
if (isRecord(output))
|
|
@@ -162,9 +203,27 @@ function nodeProducesChange(node) {
|
|
|
162
203
|
node.produces?.name === 'change' &&
|
|
163
204
|
node.resultSchema === 'schema:change';
|
|
164
205
|
}
|
|
206
|
+
function recoveryContextAfterNode(nodeId, recoveryContext) {
|
|
207
|
+
if (!recoveryContext)
|
|
208
|
+
return undefined;
|
|
209
|
+
return nodeId === 'classifyRecovery' ? recoveryContext : null;
|
|
210
|
+
}
|
|
165
211
|
function runnerProducesWorktreeChanges(runnerId) {
|
|
166
212
|
return runnerId === 'claude-code' || runnerId === 'codex';
|
|
167
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
|
+
}
|
|
168
227
|
function artifactRefFromResult(result) {
|
|
169
228
|
const artifacts = result.artifacts;
|
|
170
229
|
const processArtifact = isRecord(artifacts) && isRecord(artifacts.process) ? artifacts.process : artifacts;
|
|
@@ -199,6 +258,8 @@ function transientKind(reason) {
|
|
|
199
258
|
return 'timeout';
|
|
200
259
|
if (/\b429\b|rate.?limit|session limit/i.test(reason))
|
|
201
260
|
return 'rate_limit';
|
|
261
|
+
if (/\b529\b|overloaded/i.test(reason))
|
|
262
|
+
return 'overloaded';
|
|
202
263
|
if (isLegacyRetryableCrashReason(reason))
|
|
203
264
|
return 'crash';
|
|
204
265
|
return 'unknown';
|
|
@@ -275,16 +336,71 @@ function gateVerdict(decision, outcomes) {
|
|
|
275
336
|
return outcomes[0];
|
|
276
337
|
return outcomes.length > 1 ? outcomes.at(-1) : undefined;
|
|
277
338
|
}
|
|
278
|
-
function
|
|
339
|
+
function gateResolutionMetadata(decision, fallbackInboxId) {
|
|
340
|
+
const decisionRecord = isRecord(decision) ? decision : {};
|
|
341
|
+
const answer = isRecord(decision.answer) ? decision.answer : {};
|
|
342
|
+
const note = decision.note ?? (typeof answer.note === 'string' ? answer.note : undefined);
|
|
343
|
+
const resolvedBy = decision.resolvedBy ?? (typeof answer.resolvedBy === 'string' ? answer.resolvedBy : '');
|
|
344
|
+
const resolvedAt = decision.resolvedAt ?? (typeof answer.resolvedAt === 'string' ? answer.resolvedAt : '');
|
|
345
|
+
const inboxId = decision.inboxId ?? (typeof answer.inboxId === 'string' ? answer.inboxId : fallbackInboxId);
|
|
346
|
+
const mergeOverrideAudit = answer.mergeOverrideAudit ?? decisionRecord['mergeOverrideAudit'];
|
|
347
|
+
const adoptionAudit = answer.adoptionAudit ?? decisionRecord['adoptionAudit'];
|
|
348
|
+
return {
|
|
349
|
+
...(note !== undefined ? { note } : {}),
|
|
350
|
+
resolvedBy,
|
|
351
|
+
resolvedAt,
|
|
352
|
+
...(inboxId !== undefined ? { inboxId } : {}),
|
|
353
|
+
...(mergeOverrideAudit !== undefined ? { mergeOverrideAudit } : {}),
|
|
354
|
+
...(adoptionAudit !== undefined ? { adoptionAudit } : {}),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
function gateResolutionOutput(decision, verdict, fallbackInboxId, trustedGateHeadSha) {
|
|
358
|
+
const resolution = gateResolutionMetadata(decision, fallbackInboxId);
|
|
279
359
|
return {
|
|
280
360
|
outcome: decision.outcome ?? verdict ?? decision.decision,
|
|
281
|
-
...(
|
|
282
|
-
resolvedBy:
|
|
283
|
-
resolvedAt:
|
|
284
|
-
inboxId:
|
|
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 } : {}),
|
|
285
368
|
...(decision.decision ? { decision: decision.decision } : {}),
|
|
286
369
|
};
|
|
287
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
|
+
}
|
|
288
404
|
function gateTopicFor(reason) {
|
|
289
405
|
if (/merge/i.test(reason))
|
|
290
406
|
return 'merge';
|
|
@@ -317,9 +433,29 @@ function gateArtifactView(row, as) {
|
|
|
317
433
|
payloadRef: `attempt:${row.attemptId ?? ''}`,
|
|
318
434
|
};
|
|
319
435
|
}
|
|
320
|
-
|
|
436
|
+
function freshMergeGateArtifact(decision, lastVerdict, lastProducedOutput) {
|
|
437
|
+
if (decision.nodeId !== 'mergeGate')
|
|
438
|
+
return undefined;
|
|
439
|
+
if (lastProducedOutput?.nodeId === 'mergeRecheck' && lastVerdict === 'clean')
|
|
440
|
+
return lastProducedOutput;
|
|
441
|
+
if (lastProducedOutput?.nodeId === 'mergeApproveReverify' && lastVerdict === 'recheck')
|
|
442
|
+
return lastProducedOutput;
|
|
443
|
+
return undefined;
|
|
444
|
+
}
|
|
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) {
|
|
321
455
|
const summary = { nodeId: decision.nodeId, outcomes: decision.outcomes };
|
|
322
|
-
const artRow =
|
|
456
|
+
const artRow = latestBlockedScriptArtifact(decision, recoveryContext)
|
|
457
|
+
?? freshMergeGateArtifact(decision, lastVerdict, lastProducedOutput)
|
|
458
|
+
?? resolveGateRow(decision.gatedArtifact, outputsByNode);
|
|
323
459
|
if (artRow)
|
|
324
460
|
summary.gatedArtifact = gateArtifactView(artRow, decision.gatedArtifact?.as);
|
|
325
461
|
const verdictRow = resolveGateRow(decision.verdictFrom, outputsByNode);
|
|
@@ -329,48 +465,133 @@ export function buildGateSummary(decision, outputsByNode, lastVerdict) {
|
|
|
329
465
|
summary.reviewerVerdict = { verdict: lastVerdict };
|
|
330
466
|
return summary;
|
|
331
467
|
}
|
|
468
|
+
function approvedMergeGateHeadSha(decision, verdict, summary) {
|
|
469
|
+
if (decision.nodeId !== 'mergeGate')
|
|
470
|
+
return undefined;
|
|
471
|
+
if (verdict === 'approved' || verdict === 'override_merge') {
|
|
472
|
+
return headShaFromPayload(summary.gatedArtifact?.payload) ?? null;
|
|
473
|
+
}
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
function approvedHeadMove(decision, approvedHeadSha, pointer, verdict) {
|
|
477
|
+
if (decision.nodeId !== 'mergeApproveReverify' || verdict !== 'clean' || approvedHeadSha === undefined)
|
|
478
|
+
return undefined;
|
|
479
|
+
const reverifyHeadSha = headShaFromPayload(pointer);
|
|
480
|
+
if (reverifyHeadSha === undefined || reverifyHeadSha === approvedHeadSha)
|
|
481
|
+
return undefined;
|
|
482
|
+
return { approvedHeadSha, reverifyHeadSha };
|
|
483
|
+
}
|
|
484
|
+
function reapprovalRequiredFeedback(pointer, approvedHeadSha, reverifyHeadSha) {
|
|
485
|
+
if (!isRecord(pointer))
|
|
486
|
+
return pointer;
|
|
487
|
+
const evidence = Array.isArray(pointer.evidence)
|
|
488
|
+
? pointer.evidence.filter((item) => typeof item === 'string')
|
|
489
|
+
: [];
|
|
490
|
+
return {
|
|
491
|
+
...pointer,
|
|
492
|
+
verdict: 'recheck',
|
|
493
|
+
evidence: [
|
|
494
|
+
...evidence,
|
|
495
|
+
`approved mergeGate headSha=${approvedHeadSha}; reverify headSha=${reverifyHeadSha}; reapproval required`,
|
|
496
|
+
],
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
function reopenNodeState(state, nodeId) {
|
|
500
|
+
return { ...state, activeNodeIds: new Set([nodeId]), status: 'running' };
|
|
501
|
+
}
|
|
502
|
+
function integratorResultPointer(result) {
|
|
503
|
+
return {
|
|
504
|
+
prUrl: result.prUrl,
|
|
505
|
+
branch: result.branch,
|
|
506
|
+
prNumber: result.prNumber,
|
|
507
|
+
headSha: result.headSha,
|
|
508
|
+
status: result.status,
|
|
509
|
+
...(result.issueRef ? { issueRef: result.issueRef } : {}),
|
|
510
|
+
...(result.foreignPr ? { foreignPr: true } : {}),
|
|
511
|
+
...(result.prAuthor ? { prAuthor: result.prAuthor } : {}),
|
|
512
|
+
...(result.integratorAccount ? { integratorAccount: result.integratorAccount } : {}),
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
function integratorProgressEventType(result) {
|
|
516
|
+
return result.foreignPr ? 'foreign_pr_adopted' : 'integrate_succeeded';
|
|
517
|
+
}
|
|
518
|
+
function mergeOverrideEventPayload(result) {
|
|
519
|
+
const audit = result.override.audit;
|
|
520
|
+
return {
|
|
521
|
+
actor: result.override.actor,
|
|
522
|
+
note: result.override.note,
|
|
523
|
+
reason: audit?.reason ?? result.override.reason ?? '',
|
|
524
|
+
risk: audit?.risk ?? '',
|
|
525
|
+
verificationResponsibility: audit?.verificationResponsibility ?? '',
|
|
526
|
+
headSha: audit?.headSha ?? result.headSha,
|
|
527
|
+
freshHeadSha: result.headSha,
|
|
528
|
+
prNumber: result.prNumber,
|
|
529
|
+
source: result.override.source,
|
|
530
|
+
overriddenFacts: result.override.facts,
|
|
531
|
+
replied: result.override.replied,
|
|
532
|
+
resolved: result.override.resolved,
|
|
533
|
+
...(result.override.reason ? { refusalReason: result.override.reason } : {}),
|
|
534
|
+
};
|
|
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
|
+
}
|
|
332
546
|
export function buildSystemScriptRegistry(deps) {
|
|
333
|
-
const { appendEvent, releaseWorktreeFn, integrateFn,
|
|
334
|
-
function buildIntegratorInput(runId, ctx, inputs) {
|
|
335
|
-
const { taskId, title, base, issueRef } = ctx;
|
|
547
|
+
const { appendEvent, releaseWorktreeFn, integrateFn, confirmMergeFn, pollPrFn, overrideMergeFn, respondThreadsFn, } = deps;
|
|
548
|
+
function buildIntegratorInput(runId, ctx, inputs, githubAccount) {
|
|
549
|
+
const { taskId, title, base, issueRef, issueAction } = ctx;
|
|
336
550
|
const change = producedChangeFromInputs(inputs);
|
|
337
551
|
const mergeReadiness = mergeReadinessFromInputs(inputs);
|
|
338
|
-
const changeForIntegrator = change ?
|
|
552
|
+
const changeForIntegrator = change ? changeWithRunIssueContract(change, issueRef, issueAction) : undefined;
|
|
339
553
|
return {
|
|
340
554
|
runId,
|
|
341
555
|
taskId,
|
|
342
556
|
title,
|
|
343
557
|
base,
|
|
558
|
+
...(githubAccount ? { githubAccount } : {}),
|
|
344
559
|
...(issueRef ? { issueRef } : {}),
|
|
560
|
+
...(issueAction ? { issueAction } : {}),
|
|
345
561
|
...(changeForIntegrator ? { change: changeForIntegrator } : {}),
|
|
346
562
|
...(inputs.triage === undefined ? {} : { triage: inputs.triage }),
|
|
563
|
+
...(inputs.gateResolution === undefined ? {} : { gateResolution: inputs.gateResolution }),
|
|
347
564
|
...(mergeReadiness ? { mergeReadiness } : {}),
|
|
348
565
|
};
|
|
349
566
|
}
|
|
350
567
|
function makeIntegratorScript(desc) {
|
|
351
|
-
return async ({ runId, decision, ctx,
|
|
352
|
-
const integratorInput = buildIntegratorInput(runId, ctx, inputs);
|
|
353
|
-
const binding = bindingByRef.get(decision.scriptRef) ?? bindingByRef.get('script:integrator');
|
|
354
|
-
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));
|
|
355
570
|
let result;
|
|
356
571
|
try {
|
|
357
|
-
result =
|
|
572
|
+
result = await desc.real(integratorInput);
|
|
358
573
|
}
|
|
359
574
|
catch (err) {
|
|
575
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
360
576
|
await appendEvent({
|
|
361
577
|
runId, taskId: ctx.taskId, stepId: '', stepKey,
|
|
362
578
|
type: 'step_failed',
|
|
363
|
-
payload: { scriptRef: decision.scriptRef, error:
|
|
579
|
+
payload: { scriptRef: decision.scriptRef, error: reason },
|
|
364
580
|
});
|
|
365
|
-
return { outcome: 'failed' };
|
|
581
|
+
return { outcome: 'failed', reason };
|
|
366
582
|
}
|
|
367
583
|
if ('needsHuman' in result) {
|
|
584
|
+
const pointer = {
|
|
585
|
+
reason: desc.blockedReason,
|
|
586
|
+
lesson: result.lesson,
|
|
587
|
+
nodeId: decision.nodeId,
|
|
588
|
+
};
|
|
368
589
|
await appendEvent({
|
|
369
590
|
runId, taskId: ctx.taskId, stepId: '', stepKey: 'pipeline',
|
|
370
591
|
type: 'pipeline_blocked',
|
|
371
|
-
payload:
|
|
592
|
+
payload: pointer,
|
|
372
593
|
});
|
|
373
|
-
return { outcome: 'blocked' };
|
|
594
|
+
return { outcome: 'blocked', pointer };
|
|
374
595
|
}
|
|
375
596
|
const { eventType, payload, pointer, verdict } = desc.mapSuccess(result);
|
|
376
597
|
await appendEvent({ runId, taskId: ctx.taskId, stepId: '', stepKey, type: eventType, payload });
|
|
@@ -378,40 +599,52 @@ export function buildSystemScriptRegistry(deps) {
|
|
|
378
599
|
};
|
|
379
600
|
}
|
|
380
601
|
const cleanupWorktree = async ({ runId, decision, ctx, stepKey }) => {
|
|
602
|
+
let releaseResult;
|
|
381
603
|
try {
|
|
382
|
-
await releaseWorktreeFn(runId, ctx.taskId);
|
|
604
|
+
releaseResult = await releaseWorktreeFn(runId, ctx.taskId);
|
|
605
|
+
}
|
|
606
|
+
catch (err) {
|
|
607
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
608
|
+
await appendEvent({
|
|
609
|
+
runId,
|
|
610
|
+
taskId: ctx.taskId,
|
|
611
|
+
stepId: '',
|
|
612
|
+
stepKey,
|
|
613
|
+
type: 'cleanup_failed',
|
|
614
|
+
payload: { nodeId: decision.nodeId, error, released: false },
|
|
615
|
+
});
|
|
616
|
+
return { outcome: 'ok', pointer: { released: false, error } };
|
|
617
|
+
}
|
|
618
|
+
if (!releaseResult.released) {
|
|
619
|
+
const pointer = {
|
|
620
|
+
released: false,
|
|
621
|
+
reason: releaseResult.reason,
|
|
622
|
+
worktreePath: releaseResult.worktreePath,
|
|
623
|
+
};
|
|
624
|
+
await appendEvent({
|
|
625
|
+
runId,
|
|
626
|
+
taskId: ctx.taskId,
|
|
627
|
+
stepId: '',
|
|
628
|
+
stepKey,
|
|
629
|
+
type: 'cleanup_failed',
|
|
630
|
+
payload: { nodeId: decision.nodeId, ...pointer },
|
|
631
|
+
});
|
|
632
|
+
return { outcome: 'ok', pointer };
|
|
383
633
|
}
|
|
384
|
-
catch { /* best-effort */ }
|
|
385
634
|
await appendEvent({ runId, taskId: ctx.taskId, stepId: '', stepKey, type: 'worktree_released', payload: { nodeId: decision.nodeId } });
|
|
386
635
|
return { outcome: 'ok', pointer: { released: true } };
|
|
387
636
|
};
|
|
388
637
|
const integratorScript = makeIntegratorScript({
|
|
389
638
|
real: integrateFn,
|
|
390
|
-
stub: runStub,
|
|
391
639
|
blockedReason: 'integrate',
|
|
392
640
|
mapSuccess: (result) => ({
|
|
393
|
-
eventType:
|
|
394
|
-
payload:
|
|
395
|
-
|
|
396
|
-
branch: result.branch,
|
|
397
|
-
prNumber: result.prNumber,
|
|
398
|
-
headSha: result.headSha,
|
|
399
|
-
status: result.status,
|
|
400
|
-
...(result.issueRef ? { issueRef: result.issueRef } : {}),
|
|
401
|
-
},
|
|
402
|
-
pointer: {
|
|
403
|
-
prUrl: result.prUrl,
|
|
404
|
-
branch: result.branch,
|
|
405
|
-
prNumber: result.prNumber,
|
|
406
|
-
headSha: result.headSha,
|
|
407
|
-
status: result.status,
|
|
408
|
-
...(result.issueRef ? { issueRef: result.issueRef } : {}),
|
|
409
|
-
},
|
|
641
|
+
eventType: integratorProgressEventType(result),
|
|
642
|
+
payload: integratorResultPointer(result),
|
|
643
|
+
pointer: integratorResultPointer(result),
|
|
410
644
|
}),
|
|
411
645
|
});
|
|
412
646
|
const confirmMergeScript = makeIntegratorScript({
|
|
413
647
|
real: confirmMergeFn,
|
|
414
|
-
stub: runConfirmStub,
|
|
415
648
|
blockedReason: 'confirm-merge',
|
|
416
649
|
mapSuccess: (result) => ({
|
|
417
650
|
eventType: 'merge_confirmed',
|
|
@@ -430,7 +663,6 @@ export function buildSystemScriptRegistry(deps) {
|
|
|
430
663
|
});
|
|
431
664
|
const pollPrScript = makeIntegratorScript({
|
|
432
665
|
real: pollPrFn,
|
|
433
|
-
stub: runPollStub,
|
|
434
666
|
blockedReason: 'poll-pr',
|
|
435
667
|
mapSuccess: (result) => ({
|
|
436
668
|
eventType: 'pr_polled',
|
|
@@ -447,9 +679,54 @@ export function buildSystemScriptRegistry(deps) {
|
|
|
447
679
|
verdict: result.verdict,
|
|
448
680
|
}),
|
|
449
681
|
});
|
|
682
|
+
const overrideMergeScript = async ({ runId, decision, ctx, launchBindings, stepKey, inputs }) => {
|
|
683
|
+
const integratorInput = buildIntegratorInput(runId, ctx, inputs, scriptGithubAccount(decision, launchBindings));
|
|
684
|
+
let result;
|
|
685
|
+
try {
|
|
686
|
+
result = await overrideMergeFn(integratorInput);
|
|
687
|
+
}
|
|
688
|
+
catch (err) {
|
|
689
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
690
|
+
await appendEvent({
|
|
691
|
+
runId, taskId: ctx.taskId, stepId: '', stepKey,
|
|
692
|
+
type: 'step_failed',
|
|
693
|
+
payload: { scriptRef: decision.scriptRef, error: reason },
|
|
694
|
+
});
|
|
695
|
+
return { outcome: 'failed', reason };
|
|
696
|
+
}
|
|
697
|
+
if ('needsHuman' in result) {
|
|
698
|
+
const pointer = { reason: 'override-merge', lesson: result.lesson, nodeId: decision.nodeId };
|
|
699
|
+
await appendEvent({
|
|
700
|
+
runId, taskId: ctx.taskId, stepId: '', stepKey: 'pipeline',
|
|
701
|
+
type: 'pipeline_blocked',
|
|
702
|
+
payload: pointer,
|
|
703
|
+
});
|
|
704
|
+
return { outcome: 'blocked', pointer };
|
|
705
|
+
}
|
|
706
|
+
if (result.override.replied > 0 || result.override.resolved > 0) {
|
|
707
|
+
await appendEvent({
|
|
708
|
+
runId,
|
|
709
|
+
taskId: ctx.taskId,
|
|
710
|
+
stepId: '',
|
|
711
|
+
stepKey,
|
|
712
|
+
type: 'threads_responded',
|
|
713
|
+
payload: { replied: result.override.replied, resolved: result.override.resolved },
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
if (result.override.accepted || result.verdict !== 'merged') {
|
|
717
|
+
await appendEvent({
|
|
718
|
+
runId,
|
|
719
|
+
taskId: ctx.taskId,
|
|
720
|
+
stepId: '',
|
|
721
|
+
stepKey,
|
|
722
|
+
type: result.override.accepted ? 'merge_overridden' : 'merge_override_refused',
|
|
723
|
+
payload: mergeOverrideEventPayload(result),
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
return { outcome: 'ok', pointer: result, verdict: result.verdict };
|
|
727
|
+
};
|
|
450
728
|
const respondThreadsScript = makeIntegratorScript({
|
|
451
729
|
real: respondThreadsFn,
|
|
452
|
-
stub: runRespondStub,
|
|
453
730
|
blockedReason: 'respond-threads',
|
|
454
731
|
mapSuccess: (result) => ({
|
|
455
732
|
eventType: 'threads_responded',
|
|
@@ -461,8 +738,8 @@ export function buildSystemScriptRegistry(deps) {
|
|
|
461
738
|
['script:cleanupWorktree', cleanupWorktree],
|
|
462
739
|
['script:confirmMerge', confirmMergeScript],
|
|
463
740
|
['script:pollPr', pollPrScript],
|
|
741
|
+
['script:overrideMerge', overrideMergeScript],
|
|
464
742
|
['script:respondThreads', respondThreadsScript],
|
|
465
|
-
// Unknown refs fall through to this entry, matching the original else-branch behavior.
|
|
466
743
|
['script:integrator', integratorScript],
|
|
467
744
|
]);
|
|
468
745
|
}
|
|
@@ -501,7 +778,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
501
778
|
}
|
|
502
779
|
async function recordOutput(runId, node, ordinal, attemptId, output, outputsByNode) {
|
|
503
780
|
if (!('produces' in node) || !node.produces)
|
|
504
|
-
return;
|
|
781
|
+
return undefined;
|
|
505
782
|
const row = {
|
|
506
783
|
runId,
|
|
507
784
|
nodeId: node.id,
|
|
@@ -515,6 +792,23 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
515
792
|
list.push(row);
|
|
516
793
|
outputsByNode.set(node.id, list);
|
|
517
794
|
await appendRunOutput(row);
|
|
795
|
+
return row;
|
|
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;
|
|
518
812
|
}
|
|
519
813
|
return async function dataDrivenTaskImpl(runId, opts) {
|
|
520
814
|
try {
|
|
@@ -537,8 +831,9 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
537
831
|
if (diagnostics.length > 0) {
|
|
538
832
|
throw new Error(`PINNED_TEMPLATE_INVALID: ${template.pipelineId} — ${diagnostics.map((d) => d.code).join(', ')}`);
|
|
539
833
|
}
|
|
540
|
-
const { taskId, title, base, issueRef } = await loadRunTaskContext(runId);
|
|
541
|
-
const live = route.roleBindings.some((b) => runnerNeedsLivePreflight(b.resolvedRunnerId))
|
|
834
|
+
const { taskId, title, base, issueRef, issueAction } = await loadRunTaskContext(runId);
|
|
835
|
+
const live = route.roleBindings.some((b) => runnerNeedsLivePreflight(b.resolvedRunnerId)) ||
|
|
836
|
+
templateRequiresLiveWorktree(template);
|
|
542
837
|
if (live) {
|
|
543
838
|
const pf = await preflightFn(taskId, base);
|
|
544
839
|
if ('needsHuman' in pf) {
|
|
@@ -548,27 +843,27 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
548
843
|
if (live) {
|
|
549
844
|
await createWorktreeFn(runId, taskId, title, base, issueRef);
|
|
550
845
|
}
|
|
551
|
-
return runGraph(runId, opts, taskId, title, base, issueRef, live);
|
|
846
|
+
return runGraph(runId, opts, taskId, title, base, issueRef, issueAction, live);
|
|
552
847
|
}
|
|
553
|
-
async function runGraph(runId, opts, taskId, title, base, issueRef, live) {
|
|
848
|
+
async function runGraph(runId, opts, taskId, title, base, issueRef, issueAction, live) {
|
|
554
849
|
const { route, template, runnerRetryPolicy } = opts;
|
|
555
850
|
const bindingByRef = new Map();
|
|
556
851
|
for (const binding of route.roleBindings) {
|
|
557
852
|
bindingByRef.set(`role:${binding.roleId}`, binding);
|
|
558
|
-
bindingByRef.set(`script:${binding.roleId}`, binding);
|
|
559
853
|
bindingByRef.set(binding.roleId, binding);
|
|
560
|
-
if (runnerUsesRealIntegrator(binding.resolvedRunnerId) && !bindingByRef.has('script:integrator')) {
|
|
561
|
-
bindingByRef.set('script:integrator', binding);
|
|
562
|
-
}
|
|
563
854
|
}
|
|
564
|
-
const
|
|
855
|
+
const launchBindings = route.launchBindings ?? [];
|
|
565
856
|
let state = initialState(template);
|
|
566
857
|
let lastResult;
|
|
567
858
|
let lastVerdict = '';
|
|
859
|
+
let lastProducedOutput;
|
|
860
|
+
let recoveryContext;
|
|
568
861
|
let lastFailureReason = '';
|
|
862
|
+
let approvedMergeGateHeadShaValue;
|
|
569
863
|
let stepCount = 0;
|
|
570
864
|
const effectOrdinalByNode = new Map();
|
|
571
865
|
const outputsByNode = new Map();
|
|
866
|
+
const agentQuestionRetryContextByNode = new Map();
|
|
572
867
|
for (let i = 0; i < MAX_STEPS; i++) {
|
|
573
868
|
const { state: nextState, decision } = coreStep(template, state, lastResult);
|
|
574
869
|
state = nextState;
|
|
@@ -577,10 +872,13 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
577
872
|
return await finish(runId, decision.status, lastVerdict, stepCount, lastFailureReason);
|
|
578
873
|
}
|
|
579
874
|
const eff = await applyDecision(decision, {
|
|
580
|
-
runId, template, state, bindingByRef,
|
|
581
|
-
effectOrdinalByNode, outputsByNode, runnerRetryPolicy,
|
|
875
|
+
runId, template, state, bindingByRef, launchBindings, taskId, title, base, issueRef, issueAction,
|
|
876
|
+
effectOrdinalByNode, outputsByNode, runnerRetryPolicy, agentQuestionRetryContextByNode,
|
|
582
877
|
live,
|
|
583
878
|
lastVerdict,
|
|
879
|
+
lastProducedOutput,
|
|
880
|
+
recoveryContext,
|
|
881
|
+
approvedMergeGateHeadSha: approvedMergeGateHeadShaValue,
|
|
584
882
|
});
|
|
585
883
|
stepCount += eff.stepDelta;
|
|
586
884
|
if (eff.terminal) {
|
|
@@ -591,12 +889,30 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
591
889
|
await deps.setProgress?.(runId, progressCursor(state, eff.lastResult));
|
|
592
890
|
}
|
|
593
891
|
lastResult = eff.lastResult;
|
|
594
|
-
|
|
595
|
-
|
|
892
|
+
lastVerdict = eff.lastVerdict ?? lastVerdict;
|
|
893
|
+
lastProducedOutput = eff.producedOutput ?? lastProducedOutput;
|
|
894
|
+
if ('recoveryContext' in eff)
|
|
895
|
+
recoveryContext = eff.recoveryContext ?? undefined;
|
|
596
896
|
lastFailureReason = eff.failureReason ?? '';
|
|
897
|
+
if (eff.approvedMergeGateHeadSha !== undefined) {
|
|
898
|
+
approvedMergeGateHeadShaValue = eff.approvedMergeGateHeadSha ?? undefined;
|
|
899
|
+
}
|
|
597
900
|
}
|
|
598
901
|
throw new InterpretError(`data-driven ${template.pipelineId} did not terminate within ${MAX_STEPS} steps (template loop bug)`);
|
|
599
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
|
+
}
|
|
600
916
|
function branchTemplateForJoin(template, joinId) {
|
|
601
917
|
const join = resolveNode(template, joinId);
|
|
602
918
|
if (join.kind !== 'join')
|
|
@@ -648,20 +964,15 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
648
964
|
};
|
|
649
965
|
let lastResult;
|
|
650
966
|
let lastVerdict = '';
|
|
967
|
+
let lastProducedOutput;
|
|
968
|
+
let recoveryContext;
|
|
651
969
|
let stepDelta = 0;
|
|
652
970
|
for (let i = 0; i < MAX_STEPS; i++) {
|
|
653
971
|
const next = coreStep(branchTemplate, state, lastResult);
|
|
654
972
|
state = next.state;
|
|
655
973
|
if (next.decision.type === 'complete') {
|
|
656
|
-
if (next.decision.status !== 'succeeded') {
|
|
657
|
-
throw new InterpretError(`fork ${decision.nodeId} branch ${branch.id} completed ${next.decision.status} before join ${decision.joinId}`);
|
|
658
|
-
}
|
|
659
974
|
return {
|
|
660
|
-
arrival:
|
|
661
|
-
branchId: branch.id,
|
|
662
|
-
seq,
|
|
663
|
-
...(lastVerdict ? { verdict: lastVerdict } : {}),
|
|
664
|
-
},
|
|
975
|
+
arrival: forkBranchArrival(decision, branch, seq, next.decision.status, lastVerdict),
|
|
665
976
|
stepDelta,
|
|
666
977
|
};
|
|
667
978
|
}
|
|
@@ -670,15 +981,17 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
670
981
|
template: branchTemplate,
|
|
671
982
|
state,
|
|
672
983
|
lastVerdict,
|
|
984
|
+
lastProducedOutput,
|
|
985
|
+
recoveryContext,
|
|
673
986
|
});
|
|
674
987
|
stepDelta += eff.stepDelta;
|
|
675
988
|
if (eff.terminal)
|
|
676
989
|
return { terminal: eff.terminal, stepDelta };
|
|
677
|
-
|
|
678
|
-
state = eff.stateOverride;
|
|
990
|
+
state = eff.stateOverride ?? state;
|
|
679
991
|
lastResult = eff.lastResult;
|
|
680
|
-
|
|
681
|
-
|
|
992
|
+
lastVerdict = eff.lastVerdict ?? lastVerdict;
|
|
993
|
+
lastProducedOutput = eff.producedOutput ?? lastProducedOutput;
|
|
994
|
+
recoveryContext = recoveryContextFromEffect(recoveryContext, eff);
|
|
682
995
|
}
|
|
683
996
|
throw new InterpretError(`fork ${decision.nodeId} branch ${branch.id} did not reach join ${decision.joinId} within ${MAX_STEPS} steps`);
|
|
684
997
|
}
|
|
@@ -691,15 +1004,25 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
691
1004
|
const stepKey = stepKeyFor(node.id, ordinal);
|
|
692
1005
|
const resolved = resolveConsumes(node, ctx.outputsByNode);
|
|
693
1006
|
if ('missing' in resolved) {
|
|
1007
|
+
const reason = `${REVO_INPUT_MISSING}: required input ${resolved.missing} was not produced`;
|
|
694
1008
|
await appendEvent({
|
|
695
1009
|
runId, taskId, stepId: '', stepKey, type: 'step_failed',
|
|
696
|
-
payload: { nodeId: node.id, error:
|
|
1010
|
+
payload: { nodeId: node.id, error: reason },
|
|
697
1011
|
});
|
|
698
|
-
return { lastResult: { outcome: 'failed', errorCode: REVO_INPUT_MISSING }, lastVerdict: 'failed', stepDelta: 1 };
|
|
1012
|
+
return { lastResult: { outcome: 'failed', errorCode: REVO_INPUT_MISSING }, lastVerdict: 'failed', failureReason: reason, stepDelta: 1 };
|
|
699
1013
|
}
|
|
700
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
|
+
}
|
|
701
1023
|
if ('blocked' in result) {
|
|
702
1024
|
if (result.recovery) {
|
|
1025
|
+
ctx.agentQuestionRetryContextByNode.delete(node.id);
|
|
703
1026
|
const recovery = await awaitVerificationRecoveryGate(runId, ctx, result.recovery);
|
|
704
1027
|
return {
|
|
705
1028
|
lastResult: undefined,
|
|
@@ -711,6 +1034,22 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
711
1034
|
stepDelta: result.attemptsMade,
|
|
712
1035
|
};
|
|
713
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);
|
|
714
1053
|
return {
|
|
715
1054
|
lastResult: undefined,
|
|
716
1055
|
terminal: {
|
|
@@ -723,18 +1062,22 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
723
1062
|
};
|
|
724
1063
|
}
|
|
725
1064
|
if (result.failed) {
|
|
1065
|
+
ctx.agentQuestionRetryContextByNode.delete(node.id);
|
|
726
1066
|
await appendEvent({
|
|
727
1067
|
runId, taskId, stepId: '', stepKey, type: 'step_failed',
|
|
728
1068
|
idempotencyKey: result.attemptId,
|
|
729
|
-
payload: { nodeId: node.id, error: result.errorCode },
|
|
1069
|
+
payload: { nodeId: node.id, error: result.errorCode, reason: result.reason },
|
|
730
1070
|
});
|
|
731
1071
|
return { lastResult: { outcome: 'failed', errorCode: result.errorCode }, lastVerdict: 'failed', failureReason: result.reason, stepDelta: result.attemptsMade };
|
|
732
1072
|
}
|
|
733
|
-
|
|
1073
|
+
ctx.agentQuestionRetryContextByNode.delete(node.id);
|
|
1074
|
+
const producedOutput = await recordOutput(runId, node, ordinal, result.attemptId, result.output, ctx.outputsByNode);
|
|
734
1075
|
const verdict = result.verdict;
|
|
735
1076
|
return {
|
|
736
1077
|
lastResult: { outcome: 'succeeded', ...(verdict ? { verdict } : {}) },
|
|
737
1078
|
...(verdict ? { lastVerdict: verdict } : {}),
|
|
1079
|
+
...(producedOutput ? { producedOutput } : {}),
|
|
1080
|
+
...(ctx.recoveryContext ? { recoveryContext: recoveryContextAfterNode(node.id, ctx.recoveryContext) } : {}),
|
|
738
1081
|
stepDelta: result.attemptsMade,
|
|
739
1082
|
};
|
|
740
1083
|
}
|
|
@@ -743,30 +1086,64 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
743
1086
|
const ordinal = nextOrdinal(ctx.effectOrdinalByNode, node.id);
|
|
744
1087
|
const resolved = resolveConsumes(node, ctx.outputsByNode);
|
|
745
1088
|
if ('missing' in resolved) {
|
|
1089
|
+
const reason = `${REVO_INPUT_MISSING}: required input ${resolved.missing} was not produced`;
|
|
746
1090
|
await appendEvent({
|
|
747
1091
|
runId, taskId, stepId: '', stepKey: stepKeyFor(node.id, ordinal), type: 'step_failed',
|
|
748
|
-
payload: { nodeId: node.id, error:
|
|
1092
|
+
payload: { nodeId: node.id, error: reason },
|
|
749
1093
|
});
|
|
750
|
-
return { lastResult: { outcome: 'failed', errorCode: REVO_INPUT_MISSING }, lastVerdict: 'failed', stepDelta: 1 };
|
|
1094
|
+
return { lastResult: { outcome: 'failed', errorCode: REVO_INPUT_MISSING }, lastVerdict: 'failed', failureReason: reason, stepDelta: 1 };
|
|
751
1095
|
}
|
|
752
|
-
const scriptResult = await invokeScript(runId, decision, { taskId, title, base, issueRef: ctx.issueRef }, 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);
|
|
753
1097
|
if (scriptResult.outcome === 'blocked') {
|
|
754
|
-
|
|
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
|
+
};
|
|
755
1107
|
}
|
|
756
1108
|
if (scriptResult.outcome === 'failed') {
|
|
757
|
-
|
|
1109
|
+
const reason = scriptResult.reason ? `${REVO_SCRIPT_FAILED}: ${scriptResult.reason}` : REVO_SCRIPT_FAILED;
|
|
1110
|
+
return { lastResult: { outcome: 'failed', errorCode: REVO_SCRIPT_FAILED }, lastVerdict: 'failed', recoveryContext: null, failureReason: reason, stepDelta: 1 };
|
|
1111
|
+
}
|
|
1112
|
+
let pointer = scriptResult.pointer;
|
|
1113
|
+
let sv = scriptResult.verdict;
|
|
1114
|
+
let stateOverride;
|
|
1115
|
+
const headMove = approvedHeadMove(decision, ctx.approvedMergeGateHeadSha, pointer, sv);
|
|
1116
|
+
if (headMove) {
|
|
1117
|
+
pointer = reapprovalRequiredFeedback(pointer, headMove.approvedHeadSha, headMove.reverifyHeadSha);
|
|
1118
|
+
sv = 'recheck';
|
|
1119
|
+
stateOverride = reopenNodeState(ctx.state, 'mergeGate');
|
|
758
1120
|
}
|
|
759
|
-
await recordOutput(runId, node, ordinal, stepKeyFor(node.id, ordinal),
|
|
760
|
-
|
|
761
|
-
|
|
1121
|
+
const producedOutput = await recordOutput(runId, node, ordinal, stepKeyFor(node.id, ordinal), pointer, ctx.outputsByNode);
|
|
1122
|
+
return {
|
|
1123
|
+
lastResult: stateOverride ? undefined : { outcome: 'succeeded', ...(sv ? { verdict: sv } : {}) },
|
|
1124
|
+
...(sv ? { lastVerdict: sv } : {}),
|
|
1125
|
+
...(producedOutput ? { producedOutput } : {}),
|
|
1126
|
+
...(ctx.recoveryContext ? { recoveryContext: recoveryContextAfterNode(node.id, ctx.recoveryContext) } : {}),
|
|
1127
|
+
...(stateOverride ? { stateOverride } : {}),
|
|
1128
|
+
stepDelta: 1,
|
|
1129
|
+
};
|
|
762
1130
|
}
|
|
763
1131
|
case 'awaitGate': {
|
|
764
1132
|
const topic = gateTopicFor(decision.reason);
|
|
765
1133
|
const ordinal = nextOrdinal(ctx.effectOrdinalByNode, decision.nodeId);
|
|
766
|
-
const
|
|
1134
|
+
const summary = buildGateSummary(decision, ctx.outputsByNode, ctx.lastVerdict, ctx.lastProducedOutput, ctx.recoveryContext);
|
|
1135
|
+
const human = await awaitHuman(runId, topic, stepKeyFor(decision.nodeId, ordinal), `${decision.reason} approval`, summary, decision.outcomes);
|
|
767
1136
|
const verdict = gateVerdict(human, decision.outcomes);
|
|
768
|
-
|
|
769
|
-
|
|
1137
|
+
const approvedHeadSha = approvedMergeGateHeadSha(decision, verdict, summary);
|
|
1138
|
+
const producedOutput = await recordOutput(runId, resolveNode(template, decision.nodeId), ordinal, stepKeyFor(decision.nodeId, ordinal), gateResolutionOutput(human, verdict, stepKeyFor(decision.nodeId, ordinal), approvedHeadSha), ctx.outputsByNode);
|
|
1139
|
+
return {
|
|
1140
|
+
lastResult: verdict ? { verdict } : {},
|
|
1141
|
+
...(verdict ? { lastVerdict: verdict } : {}),
|
|
1142
|
+
...(producedOutput ? { producedOutput } : {}),
|
|
1143
|
+
...(decision.nodeId === 'recoveryGate' ? { recoveryContext: null } : {}),
|
|
1144
|
+
...(approvedHeadSha !== undefined ? { approvedMergeGateHeadSha: approvedHeadSha } : {}),
|
|
1145
|
+
stepDelta: 0,
|
|
1146
|
+
};
|
|
770
1147
|
}
|
|
771
1148
|
case 'fork': {
|
|
772
1149
|
await appendEvent({
|
|
@@ -783,9 +1160,43 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
783
1160
|
return { lastResult: {}, stepDelta: 0 };
|
|
784
1161
|
}
|
|
785
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
|
+
}
|
|
786
1188
|
async function invokeRole(runId, decision, node, ctx, inputs, stepKey) {
|
|
787
1189
|
const binding = resolveRoleBinding(ctx, decision);
|
|
788
|
-
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
|
+
});
|
|
789
1200
|
}
|
|
790
1201
|
function resolveRoleBinding(ctx, decision) {
|
|
791
1202
|
const binding = ctx.bindingByRef.get(decision.roleRef);
|
|
@@ -797,20 +1208,24 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
797
1208
|
async function invokeRoleAttempts(input) {
|
|
798
1209
|
const { runId, decision, node, ctx, inputs, stepKey, binding } = input;
|
|
799
1210
|
const attemptIds = [];
|
|
1211
|
+
const retryContext = input.retryContext;
|
|
800
1212
|
for (let attemptNo = 1; attemptNo <= ctx.runnerRetryPolicy.maxAttempts; attemptNo++) {
|
|
801
1213
|
const physicalAttempt = physicalAttemptFor(runId, stepKey, attemptNo);
|
|
802
1214
|
attemptIds.push(physicalAttempt.attemptId);
|
|
803
|
-
const
|
|
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);
|
|
804
1217
|
const needsHuman = await maybeHandleNeedsHumanRoleResult({
|
|
805
1218
|
...input,
|
|
806
1219
|
attemptIds,
|
|
807
1220
|
physicalAttempt,
|
|
808
1221
|
result,
|
|
809
1222
|
});
|
|
810
|
-
if (needsHuman
|
|
811
|
-
|
|
812
|
-
|
|
1223
|
+
if (needsHuman) {
|
|
1224
|
+
if ('action' in needsHuman) {
|
|
1225
|
+
continue;
|
|
1226
|
+
}
|
|
813
1227
|
return needsHuman;
|
|
1228
|
+
}
|
|
814
1229
|
const failed = roleValidationFailure(ctx.template, node, result, physicalAttempt);
|
|
815
1230
|
if (failed)
|
|
816
1231
|
return failed;
|
|
@@ -830,6 +1245,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
830
1245
|
nodeId: node.id,
|
|
831
1246
|
attemptId: physicalAttempt.attemptId,
|
|
832
1247
|
...(ctx.issueRef ? { issueRef: ctx.issueRef } : {}),
|
|
1248
|
+
...(ctx.issueAction ? { issueAction: ctx.issueAction } : {}),
|
|
833
1249
|
...(artifactRef ? { artifactRef } : {}),
|
|
834
1250
|
});
|
|
835
1251
|
output = attachProducedChange(output, change);
|
|
@@ -870,17 +1286,57 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
870
1286
|
}
|
|
871
1287
|
const transient = transientRunnerFailure(result);
|
|
872
1288
|
if (transient === undefined) {
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
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 };
|
|
879
1299
|
}
|
|
880
1300
|
return handleTransientRoleResult({ ...input, transient });
|
|
881
1301
|
}
|
|
882
1302
|
return undefined;
|
|
883
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
|
+
}
|
|
884
1340
|
async function handleTransientRoleResult(input) {
|
|
885
1341
|
const { runId, node, ctx, stepKey, attemptIds, physicalAttempt, transient } = input;
|
|
886
1342
|
const policy = ctx.runnerRetryPolicy;
|
|
@@ -898,7 +1354,7 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
898
1354
|
});
|
|
899
1355
|
if (policy.backoffMs > 0)
|
|
900
1356
|
await deps.sleep(policy.backoffMs);
|
|
901
|
-
return 'retry';
|
|
1357
|
+
return { action: 'retry' };
|
|
902
1358
|
}
|
|
903
1359
|
const retry = runnerRetryBlockPayload({
|
|
904
1360
|
transient,
|
|
@@ -1021,9 +1477,21 @@ export function makeDataDrivenTask(runStepFn, deps) {
|
|
|
1021
1477
|
lesson: `Human selected ${outcome}; recovery is recorded but no Revo worktree patch was adopted by the main session.`,
|
|
1022
1478
|
};
|
|
1023
1479
|
}
|
|
1024
|
-
async function invokeScript(runId, decision, ctx, bindingByRef, stepKey, inputs) {
|
|
1025
|
-
const handler = scriptRegistry.get(decision.scriptRef)
|
|
1026
|
-
|
|
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 });
|
|
1027
1495
|
}
|
|
1028
1496
|
async function finish(runId, status, verdict, steps, failureReason = '') {
|
|
1029
1497
|
if (status === 'succeeded') {
|