@atolis-hq/wake 0.2.96 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/activities/agent/agent-activity-definition.js +26 -0
- package/dist/src/activities/agent/agent-activity.js +171 -0
- package/dist/src/activities/agent/agent-result.js +149 -0
- package/dist/src/activities/contracts/activity.js +10 -0
- package/dist/src/activities/contracts/config.js +2 -0
- package/dist/src/activities/contracts/event-schema.js +302 -0
- package/dist/src/activities/contracts/events.js +42 -0
- package/dist/src/activities/contracts/identifiers.js +29 -0
- package/dist/src/activities/contracts/registry.js +71 -0
- package/dist/src/activities/contracts/streams.js +17 -0
- package/dist/src/activities/contracts/vocabulary.js +58 -0
- package/dist/src/activities/index.js +21 -0
- package/dist/src/activities/pr/activity-support.js +111 -0
- package/dist/src/activities/pr/application.js +158 -0
- package/dist/src/activities/pr/approve.js +85 -0
- package/dist/src/activities/pr/capability.js +11 -0
- package/dist/src/activities/pr/decision-claim.js +76 -0
- package/dist/src/activities/pr/event-drafts.js +66 -0
- package/dist/src/activities/pr/intent.js +37 -0
- package/dist/src/activities/pr/merge-authority-gate.js +14 -0
- package/dist/src/activities/pr/merge.js +110 -0
- package/dist/src/activities/pr/policy.js +135 -0
- package/dist/src/activities/pr/projection.js +130 -0
- package/dist/src/activities/pr/vocabulary.js +30 -0
- package/dist/src/activities/review/authorization.js +22 -0
- package/dist/src/activities/review/contracts.js +22 -0
- package/dist/src/activities/review/signals.js +3 -0
- package/dist/src/bootstrap/analytics-projection.js +26 -0
- package/dist/src/bootstrap/board-projection.js +337 -0
- package/dist/src/bootstrap/composition-root.js +382 -0
- package/dist/src/bootstrap/config/load-config.js +51 -0
- package/dist/src/bootstrap/config/root-schema.js +134 -0
- package/dist/src/bootstrap/external-ref.js +12 -0
- package/dist/src/bootstrap/fake-provider-files.js +44 -0
- package/dist/src/bootstrap/fake-scenarios.js +22 -0
- package/dist/src/bootstrap/index.js +48 -0
- package/dist/src/bootstrap/initialise.js +434 -0
- package/dist/src/bootstrap/paths.js +15 -0
- package/dist/src/bootstrap/projection-runtime.js +27 -0
- package/dist/src/bootstrap/runner-quota-reporter.js +21 -0
- package/dist/src/bootstrap/runner-registry.js +30 -0
- package/dist/src/bootstrap/schedule-checkpoint-store.js +49 -0
- package/dist/src/bootstrap/self-update-application.js +195 -0
- package/dist/src/bootstrap/self-update-failure-log.js +33 -0
- package/dist/src/bootstrap/source-update-port.js +47 -0
- package/dist/src/bootstrap/status-publish-activity.js +42 -0
- package/dist/src/bootstrap/surface-api-applications.js +325 -0
- package/dist/src/bootstrap/surface-api-execution-applications.js +95 -0
- package/dist/src/bootstrap/surface-api-metadata.js +12 -0
- package/dist/src/bootstrap/surface-api-projection-pages.js +22 -0
- package/dist/src/bootstrap/surface-api-run-context.js +5 -0
- package/dist/src/bootstrap/surface-api-transcripts.js +57 -0
- package/dist/src/bootstrap/surface-api-work-applications.js +199 -0
- package/dist/src/bootstrap/surface-applications.js +7 -0
- package/dist/src/bootstrap/surface-cli-applications.js +701 -0
- package/dist/src/bootstrap/update-ledger.js +61 -0
- package/dist/src/bootstrap/update-maintenance-lease.js +176 -0
- package/dist/src/{version.js → bootstrap/version.js} +35 -51
- package/dist/src/control-plane/application/advance-once.js +184 -0
- package/dist/src/control-plane/application/control-plane-projection.js +42 -0
- package/dist/src/control-plane/application/control-plane-service.js +42 -0
- package/dist/src/control-plane/application/intake-pipeline.js +29 -0
- package/dist/src/control-plane/application/runner-control-service.js +66 -0
- package/dist/src/control-plane/application/runner-pipeline.js +57 -0
- package/dist/src/control-plane/application/schedule-service.js +47 -0
- package/dist/src/control-plane/application/work-cancellation-policy.js +31 -0
- package/dist/src/control-plane/contracts/commands.js +10 -0
- package/dist/src/control-plane/contracts/config.js +22 -0
- package/dist/src/control-plane/contracts/events.js +81 -0
- package/dist/src/control-plane/contracts/streams.js +5 -0
- package/dist/src/control-plane/domain/dispatch-policy.js +15 -0
- package/dist/src/control-plane/domain/quota-policy.js +26 -0
- package/dist/src/control-plane/domain/runner-quota.js +35 -0
- package/dist/src/control-plane/domain/schedule-policy.js +46 -0
- package/dist/src/control-plane/index.js +20 -0
- package/dist/src/control-plane/infrastructure/intake-host.js +24 -0
- package/dist/src/control-plane/infrastructure/resident-host.js +45 -0
- package/dist/src/control-plane/infrastructure/tick-host.js +35 -0
- package/dist/src/execution/application/activation-claim.js +50 -0
- package/dist/src/execution/application/active-run-cancellation.js +14 -0
- package/dist/src/execution/application/execution-activity.js +97 -0
- package/dist/src/execution/application/execution-projection.js +17 -0
- package/dist/src/execution/application/execution-service.js +372 -0
- package/dist/src/execution/application/execution-validation.js +26 -0
- package/dist/src/execution/application/recovery-service.js +212 -0
- package/dist/src/execution/application/run-lifecycle.js +115 -0
- package/dist/src/execution/application/run-liveness-service.js +80 -0
- package/dist/src/execution/application/run-repository.js +27 -0
- package/dist/src/execution/contracts/commands.js +1 -0
- package/dist/src/execution/contracts/config.js +35 -0
- package/dist/src/execution/contracts/control-plane.js +1 -0
- package/dist/src/execution/contracts/event-factory.js +53 -0
- package/dist/src/execution/contracts/events.js +274 -0
- package/dist/src/execution/contracts/identifiers.js +5 -0
- package/dist/src/execution/contracts/liveness.js +1 -0
- package/dist/src/execution/contracts/results.js +1 -0
- package/dist/src/execution/contracts/runner.js +15 -0
- package/dist/src/execution/contracts/streams.js +10 -0
- package/dist/src/execution/contracts/views.js +1 -0
- package/dist/src/execution/contracts/vocabulary.js +43 -0
- package/dist/src/execution/contracts/workspace.js +1 -0
- package/dist/src/execution/domain/run-result.js +34 -0
- package/dist/src/execution/domain/run.js +142 -0
- package/dist/src/execution/index.js +31 -0
- package/dist/src/execution/infrastructure/agent-runner-adapter.js +114 -0
- package/dist/src/execution/infrastructure/process-execution.js +29 -0
- package/dist/src/execution/infrastructure/prompt-templates.js +42 -0
- package/dist/src/execution/infrastructure/runners/claude.js +120 -0
- package/dist/src/execution/infrastructure/runners/codex.js +143 -0
- package/dist/src/execution/infrastructure/runners/command.js +4 -0
- package/dist/src/execution/infrastructure/runners/cursor.js +69 -0
- package/dist/src/execution/infrastructure/runners/fake-scenarios.js +99 -0
- package/dist/src/execution/infrastructure/runners/fake.js +65 -0
- package/dist/src/execution/infrastructure/runners/registry.js +26 -0
- package/dist/src/execution/infrastructure/transcript-store.js +223 -0
- package/dist/src/execution/infrastructure/transcripts.js +11 -0
- package/dist/src/execution/infrastructure/workspace/fake-workspace.js +19 -0
- package/dist/src/execution/infrastructure/workspace/git-workspace.js +241 -0
- package/dist/src/integrations/application/agent-run-publication-reactor.js +111 -0
- package/dist/src/integrations/application/artifact-registration-reactor.js +159 -0
- package/dist/src/integrations/application/poll-service.js +25 -0
- package/dist/src/integrations/application/terminal-agent-run-report.js +40 -0
- package/dist/src/integrations/application/work-admission.js +31 -0
- package/dist/src/integrations/application/work-conclusion.js +19 -0
- package/dist/src/integrations/contracts/artifact-events.js +60 -0
- package/dist/src/integrations/contracts/artifact-vocabulary.js +9 -0
- package/dist/src/integrations/contracts/config.js +14 -0
- package/dist/src/integrations/contracts/identifiers.js +5 -0
- package/dist/src/integrations/contracts/intake-rules.js +14 -0
- package/dist/src/integrations/contracts/intake.js +1 -0
- package/dist/src/integrations/contracts/outcome-vocabulary.js +7 -0
- package/dist/src/integrations/contracts/provider.js +37 -0
- package/dist/src/integrations/contracts/streams.js +10 -0
- package/dist/src/integrations/delivery/application/delivery-outcome-reactor.js +75 -0
- package/dist/src/integrations/delivery/application/delivery-projector.js +165 -0
- package/dist/src/integrations/delivery/application/delivery-service.js +173 -0
- package/dist/src/integrations/delivery/contracts/commands.js +1 -0
- package/dist/src/integrations/delivery/contracts/config.js +1 -0
- package/dist/src/integrations/delivery/contracts/event-factory.js +18 -0
- package/dist/src/integrations/delivery/contracts/events.js +86 -0
- package/dist/src/integrations/delivery/contracts/intents.js +89 -0
- package/dist/src/integrations/delivery/contracts/views.js +1 -0
- package/dist/src/integrations/delivery/contracts/vocabulary.js +22 -0
- package/dist/src/integrations/fake/durable-delivery-provider.js +48 -0
- package/dist/src/integrations/fake/external-sink.js +9 -0
- package/dist/src/integrations/fake/external-source.js +44 -0
- package/dist/src/integrations/fake/inbound-translator.js +130 -0
- package/dist/src/integrations/fake/provider.js +95 -0
- package/dist/src/integrations/github/application/agent-context-reader.js +54 -0
- package/dist/src/integrations/github/application/agent-run-comment.js +94 -0
- package/dist/src/integrations/github/application/comment-history-reader.js +43 -0
- package/dist/src/integrations/github/application/inbound-context.js +10 -0
- package/dist/src/integrations/github/application/inbound-review-signals.js +159 -0
- package/dist/src/integrations/github/application/inbound-translator.js +231 -0
- package/dist/src/integrations/github/application/inbound-watch-gate-signals.js +84 -0
- package/dist/src/integrations/github/application/intake-policy.js +23 -0
- package/dist/src/integrations/github/application/outbound-translator.js +47 -0
- package/dist/src/integrations/github/application/poll-service.js +29 -0
- package/dist/src/integrations/github/application/pull-request-translation.js +10 -0
- package/dist/src/integrations/github/application/resource-links.js +8 -0
- package/dist/src/integrations/github/application/review-command-translator.js +16 -0
- package/dist/src/integrations/github/application/wake-labels.js +95 -0
- package/dist/src/integrations/github/contracts/config.js +47 -0
- package/dist/src/integrations/github/contracts/events.js +127 -0
- package/dist/src/integrations/github/contracts/external-key.js +19 -0
- package/dist/src/integrations/github/contracts/payloads.js +9 -0
- package/dist/src/integrations/github/contracts/vocabulary.js +55 -0
- package/dist/src/integrations/github/index.js +31 -0
- package/dist/src/integrations/github/infrastructure/client-reads.js +214 -0
- package/dist/src/integrations/github/infrastructure/client.js +132 -0
- package/dist/src/integrations/github/infrastructure/content-fingerprint.js +10 -0
- package/dist/src/integrations/github/infrastructure/delivery.js +31 -0
- package/dist/src/integrations/github/infrastructure/etag-cache.js +56 -0
- package/dist/src/integrations/github/infrastructure/gh-auth.js +19 -0
- package/dist/src/integrations/github/infrastructure/issue-source.js +109 -0
- package/dist/src/integrations/github/infrastructure/pr-source.js +186 -0
- package/dist/src/integrations/github/infrastructure/review-source.js +70 -0
- package/dist/src/integrations/github/infrastructure/source.js +108 -0
- package/dist/src/integrations/github/provider.js +96 -0
- package/dist/src/integrations/index.js +30 -0
- package/dist/src/kernel/contracts/checkpoint-store.js +1 -0
- package/dist/src/kernel/contracts/clock.js +1 -0
- package/dist/src/kernel/contracts/commands.js +1 -0
- package/dist/src/kernel/contracts/event-journal.js +2 -0
- package/dist/src/kernel/contracts/event-schema.js +54 -0
- package/dist/src/kernel/contracts/events.js +11 -0
- package/dist/src/kernel/contracts/id-generator.js +1 -0
- package/dist/src/kernel/contracts/identifiers.js +8 -0
- package/dist/src/kernel/contracts/projection-store.js +1 -0
- package/dist/src/kernel/contracts/relations.js +1 -0
- package/dist/src/kernel/contracts/schema.js +17 -0
- package/dist/src/kernel/contracts/vocabulary.js +6 -0
- package/dist/src/kernel/domain/event-envelope.js +29 -0
- package/dist/src/kernel/domain/match-mode.js +11 -0
- package/dist/src/kernel/domain/relation.js +10 -0
- package/dist/src/kernel/index.js +17 -0
- package/dist/src/{lib/clock.js → kernel/infrastructure/system-clock.js} +3 -3
- package/dist/src/kernel/infrastructure/ulid-id-generator.js +9 -0
- package/dist/src/main.js +137 -1030
- package/dist/src/orchestration/application/accept-activity-outcome.js +25 -0
- package/dist/src/orchestration/application/accept-signal.js +35 -0
- package/dist/src/orchestration/application/advance-workflow.js +160 -0
- package/dist/src/orchestration/application/coordination-claims.js +107 -0
- package/dist/src/orchestration/application/group-budget-recorder.js +43 -0
- package/dist/src/orchestration/application/orchestration-projection.js +20 -0
- package/dist/src/orchestration/application/orchestration-repository.js +36 -0
- package/dist/src/orchestration/application/orchestration-service.js +82 -0
- package/dist/src/orchestration/application/request-child.js +118 -0
- package/dist/src/orchestration/application/signal-reactor.js +17 -0
- package/dist/src/orchestration/application/start-workflow.js +51 -0
- package/dist/src/orchestration/application/watch-reactor.js +82 -0
- package/dist/src/orchestration/contracts/activity-outcome.js +18 -0
- package/dist/src/orchestration/contracts/commands.js +1 -0
- package/dist/src/orchestration/contracts/config.js +128 -0
- package/dist/src/orchestration/contracts/event-decoder.js +263 -0
- package/dist/src/orchestration/contracts/events.js +41 -0
- package/dist/src/orchestration/contracts/identifiers.js +35 -0
- package/dist/src/orchestration/contracts/streams.js +38 -0
- package/dist/src/orchestration/contracts/views.js +1 -0
- package/dist/src/orchestration/contracts/vocabulary.js +30 -0
- package/dist/src/orchestration/domain/activation-policy.js +55 -0
- package/dist/src/orchestration/domain/approval-defaults.js +17 -0
- package/dist/src/orchestration/domain/child-policy.js +63 -0
- package/dist/src/orchestration/domain/compiler.js +162 -0
- package/dist/src/orchestration/domain/coordination-events.js +25 -0
- package/dist/src/orchestration/domain/decision-events.js +47 -0
- package/dist/src/orchestration/domain/interpreter.js +49 -0
- package/dist/src/orchestration/domain/operator-retry-policy.js +34 -0
- package/dist/src/orchestration/domain/retry-policy.js +26 -0
- package/dist/src/orchestration/domain/signal-policy.js +120 -0
- package/dist/src/orchestration/domain/supplemental-policy.js +61 -0
- package/dist/src/orchestration/domain/transition.js +55 -0
- package/dist/src/orchestration/domain/workflow-graph.js +41 -0
- package/dist/src/orchestration/domain/workflow-instance-events.js +236 -0
- package/dist/src/orchestration/domain/workflow-instance.js +41 -0
- package/dist/src/orchestration/domain/workflow-selector.js +21 -0
- package/dist/src/orchestration/index.js +19 -0
- package/dist/src/persistence/application/projection-runner.js +79 -0
- package/dist/src/persistence/filesystem/file-checkpoint-store.js +35 -0
- package/dist/src/persistence/filesystem/file-event-journal.js +143 -0
- package/dist/src/persistence/filesystem/file-lock.js +91 -0
- package/dist/src/persistence/filesystem/file-projection-store.js +64 -0
- package/dist/src/persistence/index.js +8 -0
- package/dist/src/persistence/memory/in-memory-checkpoint-store.js +14 -0
- package/dist/src/persistence/memory/in-memory-event-journal.js +88 -0
- package/dist/src/persistence/memory/in-memory-projection-store.js +17 -0
- package/dist/src/resources/application/lookup-projections.js +48 -0
- package/dist/src/resources/application/resource-lookup.js +16 -0
- package/dist/src/resources/application/resource-projections.js +81 -0
- package/dist/src/resources/application/resource-repository.js +18 -0
- package/dist/src/resources/application/resource-service.js +83 -0
- package/dist/src/resources/contracts/commands.js +1 -0
- package/dist/src/resources/contracts/config.js +2 -0
- package/dist/src/resources/contracts/events.js +83 -0
- package/dist/src/resources/contracts/identifiers.js +15 -0
- package/dist/src/resources/contracts/streams.js +6 -0
- package/dist/src/resources/contracts/views.js +1 -0
- package/dist/src/resources/contracts/vocabulary.js +67 -0
- package/dist/src/resources/domain/correlation.js +8 -0
- package/dist/src/resources/domain/resource.js +58 -0
- package/dist/src/resources/index.js +14 -0
- package/dist/src/surfaces/api/contracts/activities.js +1 -0
- package/dist/src/surfaces/api/contracts/board.js +2 -0
- package/dist/src/surfaces/api/contracts/common.js +46 -0
- package/dist/src/surfaces/api/contracts/control-plane.js +5 -0
- package/dist/src/surfaces/api/contracts/events.js +1 -0
- package/dist/src/surfaces/api/contracts/execution.js +1 -0
- package/dist/src/surfaces/api/contracts/index.js +13 -0
- package/dist/src/surfaces/api/contracts/observability.js +1 -0
- package/dist/src/surfaces/api/contracts/orchestration.js +1 -0
- package/dist/src/surfaces/api/contracts/resources.js +1 -0
- package/dist/src/surfaces/api/contracts/status.js +1 -0
- package/dist/src/surfaces/api/contracts/system.js +1 -0
- package/dist/src/surfaces/api/contracts/transport-values.js +39 -0
- package/dist/src/surfaces/api/contracts/work.js +17 -0
- package/dist/src/surfaces/api/http-server.js +121 -0
- package/dist/src/surfaces/api/presenters/activities.js +1 -0
- package/dist/src/surfaces/api/presenters/board.js +3 -0
- package/dist/src/surfaces/api/presenters/control-plane.js +1 -0
- package/dist/src/surfaces/api/presenters/events.js +1 -0
- package/dist/src/surfaces/api/presenters/execution.js +59 -0
- package/dist/src/surfaces/api/presenters/observability.js +1 -0
- package/dist/src/surfaces/api/presenters/orchestration.js +19 -0
- package/dist/src/surfaces/api/presenters/resources.js +15 -0
- package/dist/src/surfaces/api/presenters/status.js +3 -0
- package/dist/src/surfaces/api/presenters/system.js +14 -0
- package/dist/src/surfaces/api/presenters/work.js +16 -0
- package/dist/src/surfaces/api/problem-details.js +9 -0
- package/dist/src/surfaces/api/router.js +13 -0
- package/dist/src/surfaces/api/routes/activities.js +1 -0
- package/dist/src/surfaces/api/routes/applications.js +1 -0
- package/dist/src/surfaces/api/routes/commands.js +82 -0
- package/dist/src/surfaces/api/routes/control-plane.js +6 -0
- package/dist/src/surfaces/api/routes/events.js +1 -0
- package/dist/src/surfaces/api/routes/execution.js +8 -0
- package/dist/src/surfaces/api/routes/index.js +18 -0
- package/dist/src/surfaces/api/routes/observability.js +1 -0
- package/dist/src/surfaces/api/routes/orchestration.js +1 -0
- package/dist/src/surfaces/api/routes/pagination.js +78 -0
- package/dist/src/surfaces/api/routes/read.js +135 -0
- package/dist/src/surfaces/api/routes/resources.js +1 -0
- package/dist/src/surfaces/api/routes/responses.js +67 -0
- package/dist/src/surfaces/api/routes/system.js +1 -0
- package/dist/src/surfaces/api/routes/work.js +9 -0
- package/dist/src/surfaces/cli/commands/api.js +1 -0
- package/dist/src/surfaces/cli/commands/audit.js +10 -0
- package/dist/src/surfaces/cli/commands/correlate.js +2 -0
- package/dist/src/surfaces/cli/commands/doctor.js +6 -0
- package/dist/src/surfaces/cli/commands/init.js +39 -0
- package/dist/src/surfaces/cli/commands/sandbox-entrypoint.js +33 -0
- package/dist/src/surfaces/cli/commands/sandbox-setup.js +29 -0
- package/dist/src/surfaces/cli/commands/sandbox.js +105 -0
- package/dist/src/surfaces/cli/commands/self-update.js +38 -0
- package/dist/src/surfaces/cli/commands/smoke.js +16 -0
- package/dist/src/surfaces/cli/commands/start.js +1 -0
- package/dist/src/surfaces/cli/commands/stop.js +13 -0
- package/dist/src/surfaces/cli/commands/tick.js +1 -0
- package/dist/src/surfaces/cli/commands/ui.js +2 -0
- package/dist/src/surfaces/cli/commands/validate-state.js +5 -0
- package/dist/src/surfaces/cli/infrastructure/docker-cli.js +221 -0
- package/dist/src/surfaces/cli/infrastructure/process-log.js +86 -0
- package/dist/src/surfaces/cli/main.js +140 -0
- package/dist/src/surfaces/cli/usage.js +33 -0
- package/dist/src/surfaces/contracts/config.js +34 -0
- package/dist/src/surfaces/index.js +47 -0
- package/dist/src/surfaces/web-assets/assets/index-Bfn4FwZG.css +1 -0
- package/dist/src/surfaces/web-assets/assets/index-ipB6JPGU.js +1090 -0
- package/dist/src/surfaces/web-assets/index.html +13 -0
- package/dist/src/surfaces/web-host/asset-source.js +1 -0
- package/dist/src/surfaces/web-host/packaged-assets.js +44 -0
- package/dist/src/work/application/work-projection.js +59 -0
- package/dist/src/work/application/work-repository.js +18 -0
- package/dist/src/work/application/work-service.js +117 -0
- package/dist/src/work/contracts/commands.js +1 -0
- package/dist/src/work/contracts/config.js +2 -0
- package/dist/src/work/contracts/events.js +103 -0
- package/dist/src/work/contracts/identifiers.js +5 -0
- package/dist/src/work/contracts/streams.js +6 -0
- package/dist/src/work/contracts/views.js +1 -0
- package/dist/src/work/contracts/vocabulary.js +6 -0
- package/dist/src/work/domain/work-item.js +89 -0
- package/dist/src/work/index.js +11 -0
- package/docker/Dockerfile +10 -6
- package/docker/Dockerfile.packaged +3 -3
- package/package.json +29 -4
- package/prompts/implement.md +2 -2
- package/templates/SETUP.md +7 -7
- package/dist/src/adapters/claude/claude-runner.js +0 -529
- package/dist/src/adapters/codex/codex-runner.js +0 -503
- package/dist/src/adapters/cursor/cursor-runner.js +0 -418
- package/dist/src/adapters/docker/docker-cli.js +0 -150
- package/dist/src/adapters/fake/fake-artifact-verifier.js +0 -14
- package/dist/src/adapters/fake/fake-github-pull-request-activity-source.js +0 -77
- package/dist/src/adapters/fake/fake-resource-index.js +0 -21
- package/dist/src/adapters/fake/fake-runner.js +0 -77
- package/dist/src/adapters/fake/fake-ticketing-system.js +0 -185
- package/dist/src/adapters/fake/fake-workspace-manager.js +0 -81
- package/dist/src/adapters/fs/resource-index.js +0 -126
- package/dist/src/adapters/fs/self-update-ledger.js +0 -17
- package/dist/src/adapters/fs/state-store.js +0 -775
- package/dist/src/adapters/git/git-workspace-manager.js +0 -289
- package/dist/src/adapters/github/github-artifact-verifier.js +0 -38
- package/dist/src/adapters/github/github-auth.js +0 -16
- package/dist/src/adapters/github/github-client.js +0 -249
- package/dist/src/adapters/github/github-etag-cache.js +0 -57
- package/dist/src/adapters/github/github-issues-work-source.js +0 -692
- package/dist/src/adapters/github/github-pull-request-activity-source.js +0 -584
- package/dist/src/adapters/github/github-pull-request-merge-actor.js +0 -45
- package/dist/src/adapters/http/ui-assets.js +0 -1188
- package/dist/src/adapters/http/ui-data.js +0 -1107
- package/dist/src/adapters/http/ui-server.js +0 -458
- package/dist/src/adapters/runner/cli-command.js +0 -73
- package/dist/src/adapters/runner/prompt-templates.js +0 -76
- package/dist/src/adapters/runner/runner-cli-adapter.js +0 -112
- package/dist/src/adapters/runner/runner-registry.js +0 -139
- package/dist/src/adapters/runner/runtime-events.js +0 -32
- package/dist/src/adapters/runner/stage-prompt.js +0 -268
- package/dist/src/adapters/runner/transcripts.js +0 -25
- package/dist/src/cli/audit-command.js +0 -36
- package/dist/src/cli/correlate-command.js +0 -62
- package/dist/src/cli/doctor-command.js +0 -48
- package/dist/src/cli/init-command.js +0 -18
- package/dist/src/cli/sandbox-command.js +0 -258
- package/dist/src/cli/sandbox-entrypoint-command.js +0 -88
- package/dist/src/cli/sandbox-exec-logging.js +0 -7
- package/dist/src/cli/sandbox-resume.js +0 -79
- package/dist/src/cli/sandbox-setup-command.js +0 -31
- package/dist/src/cli/scaffold-assets.js +0 -100
- package/dist/src/cli/self-update-command.js +0 -198
- package/dist/src/cli/startup-preflight.js +0 -134
- package/dist/src/cli/stop-command.js +0 -46
- package/dist/src/cli/ui-command.js +0 -27
- package/dist/src/config/defaults.js +0 -10
- package/dist/src/config/discover-config-files.js +0 -11
- package/dist/src/config/load-config.js +0 -69
- package/dist/src/config/split-config.js +0 -14
- package/dist/src/core/active-run-recovery.js +0 -44
- package/dist/src/core/approval-intents.js +0 -16
- package/dist/src/core/audit-events.js +0 -82
- package/dist/src/core/control-plane.js +0 -192
- package/dist/src/core/event-builders.js +0 -153
- package/dist/src/core/event-resolver.js +0 -249
- package/dist/src/core/lifecycle-service.js +0 -8
- package/dist/src/core/live-execution.js +0 -81
- package/dist/src/core/outbox.js +0 -198
- package/dist/src/core/policy-engine.js +0 -389
- package/dist/src/core/projection-updater.js +0 -693
- package/dist/src/core/quota-backoff.js +0 -69
- package/dist/src/core/run-lease.js +0 -38
- package/dist/src/core/scheduled-workflow-source.js +0 -199
- package/dist/src/core/sink-router.js +0 -107
- package/dist/src/core/stale-run-reconciler.js +0 -352
- package/dist/src/core/tick-runner.js +0 -2365
- package/dist/src/core/workspace-cleanup.js +0 -117
- package/dist/src/domain/branch-naming.js +0 -14
- package/dist/src/domain/custom-commands.js +0 -58
- package/dist/src/domain/event-types.js +0 -158
- package/dist/src/domain/manual-labels.js +0 -1
- package/dist/src/domain/resource-uri.js +0 -38
- package/dist/src/domain/runner-routing.js +0 -127
- package/dist/src/domain/runtime-events.js +0 -36
- package/dist/src/domain/schema.js +0 -1233
- package/dist/src/domain/sources.js +0 -16
- package/dist/src/domain/stages.js +0 -36
- package/dist/src/domain/work-item-labels.js +0 -48
- package/dist/src/domain/work-item-lifecycle.js +0 -10
- package/dist/src/domain/work-item-status.js +0 -33
- package/dist/src/domain/workflows.js +0 -159
- package/dist/src/lib/deep-merge.js +0 -14
- package/dist/src/lib/detached-process-logging.js +0 -46
- package/dist/src/lib/event-log.js +0 -21
- package/dist/src/lib/format.js +0 -41
- package/dist/src/lib/json-file.js +0 -23
- package/dist/src/lib/lock.js +0 -181
- package/dist/src/lib/log-rotation.js +0 -42
- package/dist/src/lib/paths.js +0 -52
- package/dist/src/lib/process-identity.js +0 -54
- package/dist/src/lib/state-health.js +0 -30
- package/dist/src/lib/work-id.js +0 -19
- package/dist/src/lib/yaml-file.js +0 -20
- /package/dist/src/{core → activities/pr}/contracts.js +0 -0
- /package/dist/src/{domain/types.js → control-plane/contracts/views.js} +0 -0
|
@@ -1,2365 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import { readFile } from 'node:fs/promises';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { createLifecycleService } from './lifecycle-service.js';
|
|
5
|
-
import { createPolicyEngine } from './policy-engine.js';
|
|
6
|
-
import { createProjectionUpdater } from './projection-updater.js';
|
|
7
|
-
import { acquireFileLock } from '../lib/lock.js';
|
|
8
|
-
import { CORRELATION_REGISTERED_EVENT, CORRELATION_PRIMARY_CONFLICT_EVENT, PR_AUTO_MERGE_ENABLED_EVENT, PR_REVIEW_APPROVED_EVENT, PUBLISH_INTENT_REQUESTED_EVENT, RUN_CLAIMED_EVENT, RUN_COMPLETED_EVENT, } from '../domain/event-types.js';
|
|
9
|
-
import { parseRunnerArtifacts, parseRunnerResult } from '../domain/schema.js';
|
|
10
|
-
import { maxConfiguredRunnerTimeoutMs, resolveRunnerRouting } from '../domain/runner-routing.js';
|
|
11
|
-
import { FROZEN_WORK_ITEM_LABEL, isWorkItemDeleted, isWorkItemRunnable, } from '../domain/work-item-lifecycle.js';
|
|
12
|
-
import { isMeaningfulRuntimeEvent } from '../domain/runtime-events.js';
|
|
13
|
-
import { chooseAction as chooseWorkflowAction, entryStage as workflowEntryStage, isKnownWorkflowStage, workflowChangedBlockReason, workflowForProjection, workflowNameForProjection, } from '../domain/workflows.js';
|
|
14
|
-
import { labelsForWorkItem, SCHEDULED_WORKFLOW_LABEL, } from '../domain/work-item-labels.js';
|
|
15
|
-
import { createEventEnvelope } from '../lib/event-log.js';
|
|
16
|
-
import { branchNameForIssue } from '../domain/branch-naming.js';
|
|
17
|
-
import { customCommandWorkspace, isCustomCommandAction } from '../domain/custom-commands.js';
|
|
18
|
-
import { resolveQuotaPauseUntil } from './quota-backoff.js';
|
|
19
|
-
import { createLabelsEvent, createPublishIntentEvent } from './event-builders.js';
|
|
20
|
-
import { previousMatchingSlot } from './scheduled-workflow-source.js';
|
|
21
|
-
import { createOutbox } from './outbox.js';
|
|
22
|
-
import { createEventResolver } from './event-resolver.js';
|
|
23
|
-
import { createStaleRunReconciler } from './stale-run-reconciler.js';
|
|
24
|
-
import { createWorkspaceCleanup } from './workspace-cleanup.js';
|
|
25
|
-
import { createAgentExecution } from './live-execution.js';
|
|
26
|
-
import { createAutonomousDecisionAuditEvent, workflowRevision as computeWorkflowRevision, } from './audit-events.js';
|
|
27
|
-
import { createRunLease, isRunLeaseExpired, renewRunLease, runLeaseRenewalIntervalMs, } from './run-lease.js';
|
|
28
|
-
import { currentProcessIdentity, processIdentityMatches } from '../lib/process-identity.js';
|
|
29
|
-
import { readJsonFile, writeJsonFile } from '../lib/json-file.js';
|
|
30
|
-
import { isMissingPathError } from '../lib/state-health.js';
|
|
31
|
-
const prReviewApprovalMarker = '<!-- wake:pr-review-approved -->';
|
|
32
|
-
const prReviewChangesMarker = '<!-- wake:pr-review-changes-requested -->';
|
|
33
|
-
const activeRunSourceRefreshIntervalMs = 1_000;
|
|
34
|
-
function latestHumanCommentId(candidate) {
|
|
35
|
-
const human = candidate.comments.filter((c) => !c.isBotAuthored);
|
|
36
|
-
return human.at(-1)?.id;
|
|
37
|
-
}
|
|
38
|
-
// Matched as a token at the start of a (trimmed) line, mirroring the
|
|
39
|
-
// /approved and /changes commands in policy-engine.ts.
|
|
40
|
-
const interruptCommandPattern = /^\/interrupt\b/i;
|
|
41
|
-
// Plain comments during a run are additional context for the next turn, not
|
|
42
|
-
// a signal to abandon the current attempt - only an explicit /interrupt
|
|
43
|
-
// should cancel an in-flight run, per the PR #411 follow-up discussion.
|
|
44
|
-
function newHumanCommentsSince(snapshot, refreshed) {
|
|
45
|
-
const knownIds = new Set(snapshot.comments.map((comment) => comment.id));
|
|
46
|
-
return refreshed.comments.filter((comment) => !comment.isBotAuthored && !knownIds.has(comment.id));
|
|
47
|
-
}
|
|
48
|
-
function requestsInterrupt(comments) {
|
|
49
|
-
return comments.some((comment) => comment.body.split(/\r?\n/).some((line) => interruptCommandPattern.test(line.trim())));
|
|
50
|
-
}
|
|
51
|
-
function latestActionableCommentId(candidate) {
|
|
52
|
-
const handledCommentId = typeof candidate.context.lastHandledCommentId === 'string'
|
|
53
|
-
? candidate.context.lastHandledCommentId
|
|
54
|
-
: undefined;
|
|
55
|
-
const lastBotIndex = candidate.comments.reduce((acc, comment, index) => {
|
|
56
|
-
return comment.isBotAuthored ? index : acc;
|
|
57
|
-
}, -1);
|
|
58
|
-
const latestComment = candidate.comments.slice(lastBotIndex).at(-1);
|
|
59
|
-
if (latestComment?.id !== handledCommentId &&
|
|
60
|
-
latestComment?.isBotAuthored === true &&
|
|
61
|
-
latestComment.resourceUri !== undefined &&
|
|
62
|
-
latestComment.body.includes(prReviewChangesMarker)) {
|
|
63
|
-
return latestComment.id;
|
|
64
|
-
}
|
|
65
|
-
return latestHumanCommentId(candidate);
|
|
66
|
-
}
|
|
67
|
-
function projectedSourceRevision(projection) {
|
|
68
|
-
const latestCommentUpdatedAt = projection.comments
|
|
69
|
-
.map((comment) => comment.updatedAt)
|
|
70
|
-
.sort()
|
|
71
|
-
.at(-1);
|
|
72
|
-
return latestCommentUpdatedAt === undefined
|
|
73
|
-
? `${projection.issue.repo}#${projection.issue.number}@${projection.issue.updatedAt}`
|
|
74
|
-
: `${projection.issue.repo}#${projection.issue.number}@${projection.issue.updatedAt};comments@${latestCommentUpdatedAt}`;
|
|
75
|
-
}
|
|
76
|
-
function stableJson(value) {
|
|
77
|
-
if (value === null || typeof value !== 'object') {
|
|
78
|
-
return JSON.stringify(value);
|
|
79
|
-
}
|
|
80
|
-
if (Array.isArray(value)) {
|
|
81
|
-
return `[${value.map((entry) => stableJson(entry)).join(',')}]`;
|
|
82
|
-
}
|
|
83
|
-
const record = value;
|
|
84
|
-
return `{${Object.keys(record)
|
|
85
|
-
.sort()
|
|
86
|
-
.map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`)
|
|
87
|
-
.join(',')}}`;
|
|
88
|
-
}
|
|
89
|
-
function sha256(value) {
|
|
90
|
-
return `sha256:${createHash('sha256').update(stableJson(value)).digest('hex')}`;
|
|
91
|
-
}
|
|
92
|
-
function isLateralReadOnlyAction(action, config) {
|
|
93
|
-
return isCustomCommandAction(action, config);
|
|
94
|
-
}
|
|
95
|
-
function isEventFromWatcherRun(event) {
|
|
96
|
-
return (typeof event.payload === 'object' &&
|
|
97
|
-
event.payload !== null &&
|
|
98
|
-
'watcherRun' in event.payload &&
|
|
99
|
-
event.payload.watcherRun === true);
|
|
100
|
-
}
|
|
101
|
-
function shouldPublishRunResult(input) {
|
|
102
|
-
if (input.failureClass === 'quota') {
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
if (input.failureClass === undefined || input.failureClass === 'task') {
|
|
106
|
-
return true;
|
|
107
|
-
}
|
|
108
|
-
return input.failureClass !== input.previousFailureClass;
|
|
109
|
-
}
|
|
110
|
-
function hasConfirmedExternalSideEffect(projection) {
|
|
111
|
-
return projection.correlatedResources.some((resource) => /^[a-z0-9-]+:pr:/.test(resource.resourceUri));
|
|
112
|
-
}
|
|
113
|
-
function failurePhaseForRecord(record) {
|
|
114
|
-
if (record.metadata?.failureSource === 'wake-workspace-validation') {
|
|
115
|
-
return 'workspace-validation';
|
|
116
|
-
}
|
|
117
|
-
if (record.agentPid !== undefined || record.agentProcessStartedAt !== undefined) {
|
|
118
|
-
return 'running';
|
|
119
|
-
}
|
|
120
|
-
if (record.lifecycle === 'PROCESS_STARTING' || record.lifecycle === 'RUNNING') {
|
|
121
|
-
return 'process-starting';
|
|
122
|
-
}
|
|
123
|
-
if (record.lifecycle === 'PREPARING') {
|
|
124
|
-
return 'workspace-prep';
|
|
125
|
-
}
|
|
126
|
-
return 'unknown';
|
|
127
|
-
}
|
|
128
|
-
function isWorkspaceValidationFailure(error) {
|
|
129
|
-
return (error !== null &&
|
|
130
|
-
typeof error === 'object' &&
|
|
131
|
-
'failureSource' in error &&
|
|
132
|
-
error.failureSource === 'wake-workspace-validation');
|
|
133
|
-
}
|
|
134
|
-
function classifyFailedRun(input) {
|
|
135
|
-
const processStarted = input.record.agentPid !== undefined || input.record.agentProcessStartedAt !== undefined;
|
|
136
|
-
const workspaceChanged = input.workspacePath !== undefined;
|
|
137
|
-
const externalSideEffects = hasConfirmedExternalSideEffect(input.projection)
|
|
138
|
-
? 'confirmed'
|
|
139
|
-
: processStarted
|
|
140
|
-
? 'unknown'
|
|
141
|
-
: 'none';
|
|
142
|
-
const failurePhase = input.failurePhase ??
|
|
143
|
-
((input.envelope === 'degraded' || input.envelope === 'missing') && input.sentinel === 'FAILED'
|
|
144
|
-
? 'result-parsing'
|
|
145
|
-
: failurePhaseForRecord(input.record));
|
|
146
|
-
let retrySafety;
|
|
147
|
-
if (input.failureClass === 'task') {
|
|
148
|
-
retrySafety = 'NOT_RETRYABLE';
|
|
149
|
-
}
|
|
150
|
-
else if (externalSideEffects === 'confirmed' || workspaceChanged) {
|
|
151
|
-
retrySafety = 'REQUIRES_RECONCILIATION';
|
|
152
|
-
}
|
|
153
|
-
else if (processStarted) {
|
|
154
|
-
retrySafety = 'SAFE_TO_RESUME';
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
retrySafety = 'SAFE_TO_RETRY';
|
|
158
|
-
}
|
|
159
|
-
return {
|
|
160
|
-
failurePhase,
|
|
161
|
-
processStarted,
|
|
162
|
-
workspaceChanged,
|
|
163
|
-
externalSideEffects,
|
|
164
|
-
retrySafety,
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
export function createTickRunner(deps) {
|
|
168
|
-
const policy = createPolicyEngine();
|
|
169
|
-
const lifecycle = createLifecycleService();
|
|
170
|
-
const projectionUpdater = createProjectionUpdater({
|
|
171
|
-
stateStore: deps.stateStore,
|
|
172
|
-
resourceIndex: deps.resourceIndex,
|
|
173
|
-
config: deps.config,
|
|
174
|
-
});
|
|
175
|
-
const { deliverOutboundEvent, retryUnconfirmedDeliveries, suppressOutboundEvent } = createOutbox({
|
|
176
|
-
clock: deps.clock,
|
|
177
|
-
stateStore: deps.stateStore,
|
|
178
|
-
projectionUpdater,
|
|
179
|
-
...(deps.outboundSink === undefined ? {} : { outboundSink: deps.outboundSink }),
|
|
180
|
-
});
|
|
181
|
-
const { ingestInboundEvents } = createEventResolver({
|
|
182
|
-
clock: deps.clock,
|
|
183
|
-
config: deps.config,
|
|
184
|
-
stateStore: deps.stateStore,
|
|
185
|
-
resourceIndex: deps.resourceIndex,
|
|
186
|
-
projectionUpdater,
|
|
187
|
-
qualifiesForMint: policy.qualifiesForMint,
|
|
188
|
-
});
|
|
189
|
-
const { reconcileStaleRunningRecords } = createStaleRunReconciler({
|
|
190
|
-
config: deps.config,
|
|
191
|
-
stateStore: deps.stateStore,
|
|
192
|
-
projectionUpdater,
|
|
193
|
-
runnerTimeoutMs,
|
|
194
|
-
isRunningRecordActive,
|
|
195
|
-
deliverOutboundEvent,
|
|
196
|
-
});
|
|
197
|
-
const { cleanupClosedIssueWorkspaces, sweepExpiredTranscriptDirs } = createWorkspaceCleanup({
|
|
198
|
-
clock: deps.clock,
|
|
199
|
-
config: deps.config,
|
|
200
|
-
stateStore: deps.stateStore,
|
|
201
|
-
workspaceManager: deps.workspaceManager,
|
|
202
|
-
projectionUpdater,
|
|
203
|
-
});
|
|
204
|
-
const ownerInstanceId = `instance-${process.pid}-${Date.now()}`;
|
|
205
|
-
function isAwaitingApproval(projection) {
|
|
206
|
-
// pendingApprovalAction persists through a changes-requested review round
|
|
207
|
-
// within the same gated cycle (that fold path never touches it), so it's
|
|
208
|
-
// the faithful "is this item currently gated on human sign-off" signal —
|
|
209
|
-
// context.status alone flips between 'awaiting-approval' and
|
|
210
|
-
// 'changes-requested' within the same gated cycle and can't be used here.
|
|
211
|
-
// The lastRunSentinel fallback tolerates a projection folded before
|
|
212
|
-
// pendingApprovalAction was reliably set on every gated DONE.
|
|
213
|
-
return (typeof projection.context.pendingApprovalAction === 'string' ||
|
|
214
|
-
projection.context.lastRunSentinel === 'AWAITING_APPROVAL');
|
|
215
|
-
}
|
|
216
|
-
// Always reads the current projection at write time (never a threaded
|
|
217
|
-
// local like a stale `workflowName`/`claimedStage`), so a mid-run
|
|
218
|
-
// WORKFLOW_SELECTED_EVENT or freeze/schedule change can never be
|
|
219
|
-
// stamped with a label reflecting a snapshot from before it took effect.
|
|
220
|
-
async function currentLabelsForWorkItem(workItemKey, fallback) {
|
|
221
|
-
const projection = (await deps.stateStore.readIssueState(workItemKey)) ?? fallback;
|
|
222
|
-
return labelsForWorkItem(projection, deps.config);
|
|
223
|
-
}
|
|
224
|
-
function hasLabel(projection, label) {
|
|
225
|
-
return projection.issue.labels.includes(label);
|
|
226
|
-
}
|
|
227
|
-
function approvedMergePolicyForStage(stage) {
|
|
228
|
-
// Schema defaults materialize a disabled merge block whenever onSuccess is
|
|
229
|
-
// set (e.g. approve-only watchers); treat it as no merge policy at all.
|
|
230
|
-
const merge = stage?.watch?.find((watch) => watch.onSuccess?.merge !== undefined)?.onSuccess?.merge ?? null;
|
|
231
|
-
return merge !== null && (merge.approve || merge.autoMerge) ? merge : null;
|
|
232
|
-
}
|
|
233
|
-
function escapeRegex(input) {
|
|
234
|
-
return input.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
235
|
-
}
|
|
236
|
-
function globPatternMatches(pattern, path) {
|
|
237
|
-
const regex = new RegExp(`^${pattern.split('*').map(escapeRegex).join('.*')}$`);
|
|
238
|
-
return regex.test(path);
|
|
239
|
-
}
|
|
240
|
-
function reviewerMessageFromApprovalComment(body) {
|
|
241
|
-
return body
|
|
242
|
-
.replaceAll(prReviewApprovalMarker, '')
|
|
243
|
-
.replaceAll('<!-- wake:pr-review-changes-requested -->', '')
|
|
244
|
-
.trim();
|
|
245
|
-
}
|
|
246
|
-
async function evaluatePrMergeRisk(input) {
|
|
247
|
-
const incumbent = await deps.resourceIndex.resolve(input.targetResourceUri);
|
|
248
|
-
if (incumbent !== undefined && incumbent !== input.projection.workItemKey) {
|
|
249
|
-
return {
|
|
250
|
-
passed: false,
|
|
251
|
-
reason: `Merge policy blocked this PR because ${input.targetResourceUri} is primary-correlated to ${incumbent}, not ${input.projection.workItemKey}.`,
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
const blockedLabel = input.policy.blockedLabels.find((label) => input.projection.issue.labels.includes(label));
|
|
255
|
-
if (blockedLabel !== undefined) {
|
|
256
|
-
return {
|
|
257
|
-
passed: false,
|
|
258
|
-
reason: `Merge policy blocked this PR because the work item has blocked label "${blockedLabel}".`,
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
if (deps.prMergeActor === undefined) {
|
|
262
|
-
return {
|
|
263
|
-
passed: false,
|
|
264
|
-
reason: 'Merge policy blocked this PR because no pull request merge actor is configured.',
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
const filesChanged = await deps.prMergeActor.listChangedFiles(input.targetResourceUri);
|
|
268
|
-
if (input.policy.maxFilesChanged !== undefined &&
|
|
269
|
-
filesChanged.length > input.policy.maxFilesChanged) {
|
|
270
|
-
return {
|
|
271
|
-
passed: false,
|
|
272
|
-
reason: `Merge policy blocked this PR because it changes ${filesChanged.length} files, exceeding maxFilesChanged ${input.policy.maxFilesChanged}.`,
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
for (const pattern of input.policy.blockedPaths) {
|
|
276
|
-
const blockedPath = filesChanged.find((path) => globPatternMatches(pattern, path));
|
|
277
|
-
if (blockedPath !== undefined) {
|
|
278
|
-
return {
|
|
279
|
-
passed: false,
|
|
280
|
-
reason: `Merge policy blocked this PR because changed path "${blockedPath}" matches blockedPaths pattern "${pattern}".`,
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
return { passed: true, filesChanged };
|
|
285
|
-
}
|
|
286
|
-
async function publishPrMergePolicyBlock(input) {
|
|
287
|
-
const commentId = input.approvalResolution.triggeringCommentId;
|
|
288
|
-
const targetResourceUri = input.approvalResolution.targetResourceUri;
|
|
289
|
-
if (commentId === undefined || targetResourceUri === undefined) {
|
|
290
|
-
return { status: 'idle' };
|
|
291
|
-
}
|
|
292
|
-
const runId = `merge-gate-blocked-${commentId.replace(/[^a-z0-9]+/gi, '-')}`;
|
|
293
|
-
const occurredAt = eventStampNow();
|
|
294
|
-
const blockedEvent = createEventEnvelope({
|
|
295
|
-
eventId: `${runId}-completed`,
|
|
296
|
-
workItemKey: input.projection.workItemKey,
|
|
297
|
-
streamScope: 'work-item',
|
|
298
|
-
direction: 'internal',
|
|
299
|
-
sourceSystem: 'wake',
|
|
300
|
-
sourceEventType: RUN_COMPLETED_EVENT,
|
|
301
|
-
sourceRefs: {
|
|
302
|
-
repo: input.projection.issue.repo,
|
|
303
|
-
issueNumber: input.projection.issue.number,
|
|
304
|
-
runId,
|
|
305
|
-
resourceUri: targetResourceUri,
|
|
306
|
-
},
|
|
307
|
-
occurredAt,
|
|
308
|
-
ingestedAt: occurredAt,
|
|
309
|
-
trigger: 'immediate',
|
|
310
|
-
payload: {
|
|
311
|
-
action: input.approvalResolution.pendingAction,
|
|
312
|
-
sentinel: 'BLOCKED',
|
|
313
|
-
runId,
|
|
314
|
-
reason: 'pr-review-merge-policy-blocked',
|
|
315
|
-
blockReason: 'pr-review-merge-policy-blocked',
|
|
316
|
-
handledCommentId: commentId,
|
|
317
|
-
body: input.reason,
|
|
318
|
-
},
|
|
319
|
-
});
|
|
320
|
-
const appended = await deps.stateStore.appendEventEnvelope(blockedEvent);
|
|
321
|
-
await projectionUpdater.rebuildFromEvents([appended]);
|
|
322
|
-
const reviewerMessage = reviewerMessageFromApprovalComment(input.approvalResolution.triggeringCommentBody ?? '');
|
|
323
|
-
const body = [reviewerMessage, input.reason].filter((part) => part.length > 0).join('\n\n');
|
|
324
|
-
await deliverOutboundEvent(createEventEnvelope({
|
|
325
|
-
eventId: `${runId}-publish-intent`,
|
|
326
|
-
workItemKey: input.projection.workItemKey,
|
|
327
|
-
streamScope: 'work-item',
|
|
328
|
-
direction: 'outbound',
|
|
329
|
-
sourceSystem: 'wake',
|
|
330
|
-
sourceEventType: PUBLISH_INTENT_REQUESTED_EVENT,
|
|
331
|
-
sourceRefs: {
|
|
332
|
-
repo: input.projection.issue.repo,
|
|
333
|
-
issueNumber: input.projection.issue.number,
|
|
334
|
-
runId,
|
|
335
|
-
resourceUri: targetResourceUri,
|
|
336
|
-
},
|
|
337
|
-
occurredAt,
|
|
338
|
-
ingestedAt: occurredAt,
|
|
339
|
-
trigger: 'context-only',
|
|
340
|
-
payload: {
|
|
341
|
-
kind: 'question',
|
|
342
|
-
origin: input.projection.origin ?? 'github',
|
|
343
|
-
body,
|
|
344
|
-
action: input.approvalResolution.pendingAction,
|
|
345
|
-
sentinel: 'BLOCKED',
|
|
346
|
-
runId,
|
|
347
|
-
idempotencyKey: `${commentId}:pr-review-merge-policy-blocked`,
|
|
348
|
-
deliveryState: 'PENDING',
|
|
349
|
-
},
|
|
350
|
-
derivedHints: { stage: input.projection.wake.stage },
|
|
351
|
-
}));
|
|
352
|
-
await deliverOutboundEvent(createLabelsEvent({
|
|
353
|
-
projection: input.projection,
|
|
354
|
-
runId,
|
|
355
|
-
...(await currentLabelsForWorkItem(input.projection.workItemKey, input.projection)),
|
|
356
|
-
occurredAt,
|
|
357
|
-
}));
|
|
358
|
-
return {
|
|
359
|
-
status: 'processed',
|
|
360
|
-
runId,
|
|
361
|
-
sentinel: 'BLOCKED',
|
|
362
|
-
nextStage: null,
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
function describeMergeActorError(error) {
|
|
366
|
-
return error instanceof Error ? error.message : String(error);
|
|
367
|
-
}
|
|
368
|
-
async function performApprovedPrMergeActions(input) {
|
|
369
|
-
if (deps.prMergeActor === undefined ||
|
|
370
|
-
input.approvalResolution.targetResourceUri === undefined ||
|
|
371
|
-
input.approvalResolution.triggeringCommentId === undefined) {
|
|
372
|
-
return { blocked: false };
|
|
373
|
-
}
|
|
374
|
-
const targetResourceUri = input.approvalResolution.targetResourceUri;
|
|
375
|
-
const commentId = input.approvalResolution.triggeringCommentId.replace(/[^a-z0-9]+/gi, '-');
|
|
376
|
-
const reviewBody = reviewerMessageFromApprovalComment(input.approvalResolution.triggeringCommentBody ?? '');
|
|
377
|
-
// approve and autoMerge are independent: one failing doesn't stop the
|
|
378
|
-
// other from being attempted. If autoMerge succeeds, a failed approval is
|
|
379
|
-
// not a policy block on its own; GitHub can reject self-approval while
|
|
380
|
-
// still accepting auto-merge for repos that do not require review.
|
|
381
|
-
const approvalBlockedReasons = [];
|
|
382
|
-
const autoMergeBlockedReasons = [];
|
|
383
|
-
const approvedEventId = `pr-merge-approved-${commentId}`;
|
|
384
|
-
if (input.mergePolicy.approve &&
|
|
385
|
-
(await deps.stateStore.readEventEnvelope(approvedEventId)) === null) {
|
|
386
|
-
try {
|
|
387
|
-
await deps.prMergeActor.approve(targetResourceUri, reviewBody);
|
|
388
|
-
const occurredAt = eventStampNow();
|
|
389
|
-
await deps.stateStore.appendEventEnvelope(createEventEnvelope({
|
|
390
|
-
eventId: approvedEventId,
|
|
391
|
-
workItemKey: input.projection.workItemKey,
|
|
392
|
-
streamScope: 'work-item',
|
|
393
|
-
direction: 'internal',
|
|
394
|
-
sourceSystem: 'wake',
|
|
395
|
-
sourceEventType: PR_REVIEW_APPROVED_EVENT,
|
|
396
|
-
sourceRefs: {
|
|
397
|
-
resourceUri: targetResourceUri,
|
|
398
|
-
commentId: input.approvalResolution.triggeringCommentId,
|
|
399
|
-
},
|
|
400
|
-
occurredAt,
|
|
401
|
-
ingestedAt: occurredAt,
|
|
402
|
-
trigger: 'context-only',
|
|
403
|
-
payload: {
|
|
404
|
-
idempotencyKey: `${input.approvalResolution.triggeringCommentId}:pr-review-approval`,
|
|
405
|
-
},
|
|
406
|
-
}));
|
|
407
|
-
}
|
|
408
|
-
catch (error) {
|
|
409
|
-
approvalBlockedReasons.push(`Merge policy blocked the approval step: ${describeMergeActorError(error)}`);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
const autoMergeEventId = `pr-auto-merge-enabled-${commentId}`;
|
|
413
|
-
let autoMergeSucceeded = false;
|
|
414
|
-
if (input.mergePolicy.autoMerge &&
|
|
415
|
-
(await deps.stateStore.readEventEnvelope(autoMergeEventId)) === null) {
|
|
416
|
-
try {
|
|
417
|
-
await deps.prMergeActor.enableAutoMerge(targetResourceUri, input.mergePolicy.mergeMethod);
|
|
418
|
-
const occurredAt = eventStampNow();
|
|
419
|
-
await deps.stateStore.appendEventEnvelope(createEventEnvelope({
|
|
420
|
-
eventId: autoMergeEventId,
|
|
421
|
-
workItemKey: input.projection.workItemKey,
|
|
422
|
-
streamScope: 'work-item',
|
|
423
|
-
direction: 'internal',
|
|
424
|
-
sourceSystem: 'wake',
|
|
425
|
-
sourceEventType: PR_AUTO_MERGE_ENABLED_EVENT,
|
|
426
|
-
sourceRefs: {
|
|
427
|
-
resourceUri: targetResourceUri,
|
|
428
|
-
commentId: input.approvalResolution.triggeringCommentId,
|
|
429
|
-
},
|
|
430
|
-
occurredAt,
|
|
431
|
-
ingestedAt: occurredAt,
|
|
432
|
-
trigger: 'context-only',
|
|
433
|
-
payload: {
|
|
434
|
-
idempotencyKey: `${input.approvalResolution.triggeringCommentId}:pr-auto-merge`,
|
|
435
|
-
},
|
|
436
|
-
}));
|
|
437
|
-
autoMergeSucceeded = true;
|
|
438
|
-
}
|
|
439
|
-
catch (error) {
|
|
440
|
-
autoMergeBlockedReasons.push(`Merge policy blocked the auto-merge step: ${describeMergeActorError(error)}`);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
else if (input.mergePolicy.autoMerge) {
|
|
444
|
-
autoMergeSucceeded = true;
|
|
445
|
-
}
|
|
446
|
-
const blockedReasons = autoMergeSucceeded && input.mergePolicy.autoMerge
|
|
447
|
-
? autoMergeBlockedReasons
|
|
448
|
-
: [...approvalBlockedReasons, ...autoMergeBlockedReasons];
|
|
449
|
-
if (blockedReasons.length > 0) {
|
|
450
|
-
return { blocked: true, reason: blockedReasons.join(' ') };
|
|
451
|
-
}
|
|
452
|
-
// Approval failed but auto-merge covered it (e.g. GitHub rejecting
|
|
453
|
-
// self-approval on the bot's own PR) - not a policy block, but the
|
|
454
|
-
// failure should still leave a visible trail instead of vanishing
|
|
455
|
-
// silently, since it may also be a real permissions/config problem.
|
|
456
|
-
if (approvalBlockedReasons.length > 0) {
|
|
457
|
-
const occurredAt = eventStampNow();
|
|
458
|
-
await deliverOutboundEvent(createEventEnvelope({
|
|
459
|
-
eventId: `pr-merge-approval-note-${commentId}`,
|
|
460
|
-
workItemKey: input.projection.workItemKey,
|
|
461
|
-
streamScope: 'work-item',
|
|
462
|
-
direction: 'outbound',
|
|
463
|
-
sourceSystem: 'wake',
|
|
464
|
-
sourceEventType: PUBLISH_INTENT_REQUESTED_EVENT,
|
|
465
|
-
sourceRefs: {
|
|
466
|
-
repo: input.projection.issue.repo,
|
|
467
|
-
issueNumber: input.projection.issue.number,
|
|
468
|
-
resourceUri: targetResourceUri,
|
|
469
|
-
},
|
|
470
|
-
occurredAt,
|
|
471
|
-
ingestedAt: occurredAt,
|
|
472
|
-
trigger: 'context-only',
|
|
473
|
-
payload: {
|
|
474
|
-
kind: 'status-update',
|
|
475
|
-
origin: input.projection.origin ?? 'github',
|
|
476
|
-
body: `Approval step did not block the merge, but did not succeed on its own: ${approvalBlockedReasons.join(' ')}`,
|
|
477
|
-
idempotencyKey: `${commentId}:pr-merge-approval-note`,
|
|
478
|
-
deliveryState: 'PENDING',
|
|
479
|
-
},
|
|
480
|
-
derivedHints: { stage: input.projection.wake.stage },
|
|
481
|
-
}));
|
|
482
|
-
}
|
|
483
|
-
return { blocked: false };
|
|
484
|
-
}
|
|
485
|
-
// The one deterministic approval transition: /approved, wake:auto, and a
|
|
486
|
-
// watcher child's onSuccess.approve all resolve a pending approval through
|
|
487
|
-
// this same event shape, so replay folds them identically.
|
|
488
|
-
async function applyApprovalTransition(input) {
|
|
489
|
-
const nextStage = lifecycle.nextStageFromSentinel(input.projection.wake.stage, 'DONE', input.workflow);
|
|
490
|
-
if (nextStage === null) {
|
|
491
|
-
return null;
|
|
492
|
-
}
|
|
493
|
-
const approvalCompletedEvent = createEventEnvelope({
|
|
494
|
-
eventId: `${input.approvalId}-completed`,
|
|
495
|
-
workItemKey: input.projection.workItemKey,
|
|
496
|
-
streamScope: 'work-item',
|
|
497
|
-
direction: 'internal',
|
|
498
|
-
sourceSystem: 'wake',
|
|
499
|
-
sourceEventType: RUN_COMPLETED_EVENT,
|
|
500
|
-
sourceRefs: {
|
|
501
|
-
repo: input.projection.issue.repo,
|
|
502
|
-
issueNumber: input.projection.issue.number,
|
|
503
|
-
runId: input.approvalId,
|
|
504
|
-
},
|
|
505
|
-
occurredAt: input.approvedAt,
|
|
506
|
-
ingestedAt: input.approvedAt,
|
|
507
|
-
trigger: 'immediate',
|
|
508
|
-
payload: {
|
|
509
|
-
action: input.pendingAction,
|
|
510
|
-
sentinel: 'DONE',
|
|
511
|
-
nextStage,
|
|
512
|
-
runId: input.approvalId,
|
|
513
|
-
reason: input.reason,
|
|
514
|
-
...input.payloadExtras,
|
|
515
|
-
},
|
|
516
|
-
});
|
|
517
|
-
await deps.stateStore.appendEventEnvelope(approvalCompletedEvent);
|
|
518
|
-
await projectionUpdater.rebuildFromEvents([approvalCompletedEvent]);
|
|
519
|
-
await deliverOutboundEvent(createLabelsEvent({
|
|
520
|
-
projection: input.projection,
|
|
521
|
-
runId: input.approvalId,
|
|
522
|
-
...(await currentLabelsForWorkItem(input.projection.workItemKey, input.projection)),
|
|
523
|
-
occurredAt: input.approvedAt,
|
|
524
|
-
}));
|
|
525
|
-
return { nextStage };
|
|
526
|
-
}
|
|
527
|
-
// Folds a human /changes reply onto the parent's context via the same
|
|
528
|
-
// `changesRequested` RUN_COMPLETED payload shape a rejecting review watcher
|
|
529
|
-
// uses (projection-updater.ts) — same fold, same bounding against
|
|
530
|
-
// config.retry.maxChangesRequestedRetries, so a human bouncing /changes
|
|
531
|
-
// repeatedly escalates to 'blocked' exactly like an auto-revise loop would.
|
|
532
|
-
async function applyChangesRequestedTransition(input) {
|
|
533
|
-
const currentCount = typeof input.projection.context.changesRequestedCount === 'number' &&
|
|
534
|
-
Number.isInteger(input.projection.context.changesRequestedCount)
|
|
535
|
-
? input.projection.context.changesRequestedCount
|
|
536
|
-
: 0;
|
|
537
|
-
const escalated = currentCount + 1 > deps.config.retry.maxChangesRequestedRetries;
|
|
538
|
-
const event = createEventEnvelope({
|
|
539
|
-
eventId: `${input.requestId}-changes-requested`,
|
|
540
|
-
workItemKey: input.projection.workItemKey,
|
|
541
|
-
streamScope: 'work-item',
|
|
542
|
-
direction: 'internal',
|
|
543
|
-
sourceSystem: 'wake',
|
|
544
|
-
sourceEventType: RUN_COMPLETED_EVENT,
|
|
545
|
-
sourceRefs: {
|
|
546
|
-
repo: input.projection.issue.repo,
|
|
547
|
-
issueNumber: input.projection.issue.number,
|
|
548
|
-
runId: input.requestId,
|
|
549
|
-
},
|
|
550
|
-
occurredAt: input.requestedAt,
|
|
551
|
-
ingestedAt: input.requestedAt,
|
|
552
|
-
trigger: 'immediate',
|
|
553
|
-
payload: {
|
|
554
|
-
changesRequested: true,
|
|
555
|
-
runId: input.requestId,
|
|
556
|
-
reason: 'human:changes-requested',
|
|
557
|
-
...(input.feedbackBody === undefined ? {} : { reviewFeedbackBody: input.feedbackBody }),
|
|
558
|
-
...(input.triggeringCommentId === undefined
|
|
559
|
-
? {}
|
|
560
|
-
: { handledCommentId: input.triggeringCommentId }),
|
|
561
|
-
},
|
|
562
|
-
});
|
|
563
|
-
await deps.stateStore.appendEventEnvelope(event);
|
|
564
|
-
await projectionUpdater.rebuildFromEvents([event]);
|
|
565
|
-
return { escalated };
|
|
566
|
-
}
|
|
567
|
-
// Closes the loop on #82's review feedback: rather than scrape a PR link
|
|
568
|
-
// out of the agent's free text, the agent emits a `wake-artifacts` fence
|
|
569
|
-
// (domain/schema.ts's parseRunnerArtifacts) and Wake verifies each claim
|
|
570
|
-
// against the real provider before trusting it. No verifier configured (no
|
|
571
|
-
// GitHub source) means no claim is ever trusted — this is a no-op, not a
|
|
572
|
-
// silent "believe the agent" fallback.
|
|
573
|
-
//
|
|
574
|
-
// Uses a plain appendEventEnvelope + rebuildFromEvents pair, not
|
|
575
|
-
// deliverOutboundEvent: wake.correlation.registered is not a publish-intent
|
|
576
|
-
// type, so attemptDelivery's outbound sink call would be a no-op anyway —
|
|
577
|
-
// this matches the same plain-append pattern used elsewhere for
|
|
578
|
-
// internal-only events (see buildOriginCorrelationEvents' callers).
|
|
579
|
-
async function registerReportedArtifacts(input) {
|
|
580
|
-
if (deps.artifactVerifier === undefined) {
|
|
581
|
-
return;
|
|
582
|
-
}
|
|
583
|
-
const { artifacts } = parseRunnerArtifacts(input.runnerResult.result);
|
|
584
|
-
for (const artifact of artifacts) {
|
|
585
|
-
const verified = await deps.artifactVerifier.verify(artifact, {
|
|
586
|
-
branch: input.branch,
|
|
587
|
-
repo: input.projection.issue.repo,
|
|
588
|
-
});
|
|
589
|
-
if (verified === null) {
|
|
590
|
-
continue;
|
|
591
|
-
}
|
|
592
|
-
const event = createEventEnvelope({
|
|
593
|
-
eventId: `${input.runId}-artifact-${artifact.kind}-${verified.resourceUri.replace(/[^a-z0-9]+/gi, '-')}`,
|
|
594
|
-
workItemKey: input.projection.workItemKey,
|
|
595
|
-
streamScope: 'work-item',
|
|
596
|
-
direction: 'internal',
|
|
597
|
-
sourceSystem: 'wake',
|
|
598
|
-
sourceEventType: CORRELATION_REGISTERED_EVENT,
|
|
599
|
-
sourceRefs: { runId: input.runId },
|
|
600
|
-
occurredAt: input.occurredAt,
|
|
601
|
-
ingestedAt: input.occurredAt,
|
|
602
|
-
trigger: 'context-only',
|
|
603
|
-
payload: {
|
|
604
|
-
resourceUri: verified.resourceUri,
|
|
605
|
-
role: 'implementation',
|
|
606
|
-
relation: 'primary',
|
|
607
|
-
provenance: 'agent-reported',
|
|
608
|
-
registeredBy: input.runId,
|
|
609
|
-
idempotencyKey: `${input.runId}:artifact-registration:${verified.resourceUri}`,
|
|
610
|
-
},
|
|
611
|
-
});
|
|
612
|
-
const appended = await deps.stateStore.appendEventEnvelope(event);
|
|
613
|
-
await projectionUpdater.rebuildFromEvents([appended]);
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
// Events are stamped by reading the clock at the moment of stamping, never
|
|
617
|
-
// from a frozen tick-start snapshot. `tickStartedAt` is the tick's *decision*
|
|
618
|
-
// clock (policy/staleness), and reusing it to date events inverts them
|
|
619
|
-
// against the work source's own poll-time ingestedAt — pollEvents() runs
|
|
620
|
-
// after tickStartedAt is captured, so in production every polled upsert is
|
|
621
|
-
// LATER than the tick's start. An event dated before the upsert that creates
|
|
622
|
-
// the projection it folds into sorts ahead of it in rebuildFromEvents' global
|
|
623
|
-
// replay, folds against `current === null`, and is silently dropped. Reading
|
|
624
|
-
// per event also stays correct once ticks work items in parallel, where a
|
|
625
|
-
// shared per-tick snapshot would tie every concurrent event on ingestedAt and
|
|
626
|
-
// leave append order as the only discriminator.
|
|
627
|
-
function eventStampNow() {
|
|
628
|
-
return deps.clock.now().toISOString();
|
|
629
|
-
}
|
|
630
|
-
async function appendAuditEvent(input) {
|
|
631
|
-
await deps.stateStore.appendEventEnvelope(createAutonomousDecisionAuditEvent(input));
|
|
632
|
-
}
|
|
633
|
-
// The watchlist is every resource currently correlated to an open work
|
|
634
|
-
// item, deduplicated by exact resourceUri. It is derived once per tick from
|
|
635
|
-
// the pre-poll projection snapshot (see runTick's ordering note) and handed
|
|
636
|
-
// to every configured WorkSource. Core never interprets these URIs — it's
|
|
637
|
-
// each source's own job to recognize and filter down to the resourceUri
|
|
638
|
-
// shapes it understands (e.g. the GitHub PR source only polls entries
|
|
639
|
-
// starting with `github:pr:`, ignoring everything else, including its own
|
|
640
|
-
// review-thread correlations) and never core's (CLAUDE.md: "Core compares
|
|
641
|
-
// resourceUri strings for equality and never parses a locator").
|
|
642
|
-
function deriveWatchlist(projections) {
|
|
643
|
-
const seen = new Set();
|
|
644
|
-
const watch = [];
|
|
645
|
-
for (const projection of projections) {
|
|
646
|
-
if (projection.issue.state !== 'open' || isWorkItemDeleted(projection)) {
|
|
647
|
-
continue;
|
|
648
|
-
}
|
|
649
|
-
for (const resource of projection.correlatedResources) {
|
|
650
|
-
if (seen.has(resource.resourceUri)) {
|
|
651
|
-
continue;
|
|
652
|
-
}
|
|
653
|
-
seen.add(resource.resourceUri);
|
|
654
|
-
watch.push({ resourceUri: resource.resourceUri });
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
return watch;
|
|
658
|
-
}
|
|
659
|
-
// Runs against every open, non-deleted item every tick (not just ones with
|
|
660
|
-
// an eligible next action) so a stale label self-heals even on a parked
|
|
661
|
-
// item nothing else would otherwise re-check — this is also where
|
|
662
|
-
// wake:frozen/wake:scheduled-workflow get their only reconciliation
|
|
663
|
-
// coverage, since freeze/unfreeze no longer writes labels inline (§6).
|
|
664
|
-
async function markPendingActionableIssues(projections) {
|
|
665
|
-
const activeRunWorkItemKeys = new Set();
|
|
666
|
-
const now = deps.clock.now();
|
|
667
|
-
for (const record of await deps.stateStore.listRunRecordSummaries()) {
|
|
668
|
-
if (record.status === 'running' && (await isRunningRecordActive(record, now))) {
|
|
669
|
-
activeRunWorkItemKeys.add(record.workItemKey);
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
for (const projection of projections) {
|
|
673
|
-
if (isWorkItemDeleted(projection) || activeRunWorkItemKeys.has(projection.workItemKey)) {
|
|
674
|
-
continue;
|
|
675
|
-
}
|
|
676
|
-
const labels = labelsForWorkItem(projection, deps.config);
|
|
677
|
-
const matchesDesired = hasLabel(projection, labels.statusLabel) &&
|
|
678
|
-
hasLabel(projection, labels.stageLabel) &&
|
|
679
|
-
hasLabel(projection, labels.workflowLabel) &&
|
|
680
|
-
(labels.frozenLabel === undefined || hasLabel(projection, labels.frozenLabel)) &&
|
|
681
|
-
(labels.scheduledLabel === undefined || hasLabel(projection, labels.scheduledLabel));
|
|
682
|
-
// A toggle label (frozen/scheduled) whose desired state is "absent"
|
|
683
|
-
// still needs to be checked for stray presence — the family-prefix
|
|
684
|
-
// labels above don't need this since removing them is folded into
|
|
685
|
-
// "does the current one match" by construction.
|
|
686
|
-
const hasStrayToggleLabel = (labels.frozenLabel === undefined && hasLabel(projection, FROZEN_WORK_ITEM_LABEL)) ||
|
|
687
|
-
(labels.scheduledLabel === undefined && hasLabel(projection, SCHEDULED_WORKFLOW_LABEL));
|
|
688
|
-
if (matchesDesired && !hasStrayToggleLabel) {
|
|
689
|
-
continue;
|
|
690
|
-
}
|
|
691
|
-
await deliverOutboundEvent(createLabelsEvent({
|
|
692
|
-
projection,
|
|
693
|
-
runId: `pending-${projection.workItemKey}-${deps.clock.now().getTime()}`,
|
|
694
|
-
...labels,
|
|
695
|
-
occurredAt: eventStampNow(),
|
|
696
|
-
}));
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
function runnerTimeoutMs() {
|
|
700
|
-
return maxConfiguredRunnerTimeoutMs(deps.config);
|
|
701
|
-
}
|
|
702
|
-
async function promptHashForAction(action) {
|
|
703
|
-
const promptsRoot = deps.config.paths.promptsRoot;
|
|
704
|
-
if (promptsRoot === undefined) {
|
|
705
|
-
return sha256({ action, status: 'not-configured' });
|
|
706
|
-
}
|
|
707
|
-
for (const suffix of ['.md', '.start.md', '.resume.md']) {
|
|
708
|
-
const path = join(promptsRoot, `${action}${suffix}`);
|
|
709
|
-
try {
|
|
710
|
-
return `sha256:${createHash('sha256')
|
|
711
|
-
.update(await readFile(path, 'utf8'))
|
|
712
|
-
.digest('hex')}`;
|
|
713
|
-
}
|
|
714
|
-
catch {
|
|
715
|
-
// Try the next supported prompt template name.
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
return sha256({ action, status: 'missing' });
|
|
719
|
-
}
|
|
720
|
-
function triggerEventIdForRun(input) {
|
|
721
|
-
if (input.watcherTrigger?.kind === 'event') {
|
|
722
|
-
return input.watcherTrigger.eventId;
|
|
723
|
-
}
|
|
724
|
-
return input.recentEvents.at(-1)?.eventId;
|
|
725
|
-
}
|
|
726
|
-
async function createRunInputSnapshot(input) {
|
|
727
|
-
const validation = input.workspaceValidation !== null &&
|
|
728
|
-
typeof input.workspaceValidation === 'object' &&
|
|
729
|
-
!Array.isArray(input.workspaceValidation)
|
|
730
|
-
? input.workspaceValidation
|
|
731
|
-
: undefined;
|
|
732
|
-
const repositoryHead = typeof validation?.baseRevision === 'string' ? validation.baseRevision : undefined;
|
|
733
|
-
const workspaceHead = typeof validation?.headRevision === 'string' ? validation.headRevision : undefined;
|
|
734
|
-
const triggerEventId = triggerEventIdForRun({
|
|
735
|
-
...(input.watcherTrigger === undefined ? {} : { watcherTrigger: input.watcherTrigger }),
|
|
736
|
-
recentEvents: input.recentEvents,
|
|
737
|
-
});
|
|
738
|
-
return deps.stateStore.writeRunInputSnapshot({
|
|
739
|
-
schemaVersion: 1,
|
|
740
|
-
snapshotId: `${input.runId}-input`,
|
|
741
|
-
runId: input.runId,
|
|
742
|
-
createdAt: input.createdAt,
|
|
743
|
-
action: input.action,
|
|
744
|
-
workflowName: input.workflowName,
|
|
745
|
-
claimedStage: input.claimedStage,
|
|
746
|
-
projectionVersion: input.projection.wake.syncedAt,
|
|
747
|
-
sourceUpdatedAt: input.projection.issue.updatedAt,
|
|
748
|
-
sourceRevision: input.sourceRevision,
|
|
749
|
-
...(triggerEventId === undefined ? {} : { triggerEventId }),
|
|
750
|
-
...(input.recentEvents.at(-1)?.eventId === undefined
|
|
751
|
-
? {}
|
|
752
|
-
: { handledThroughEventId: input.recentEvents.at(-1).eventId }),
|
|
753
|
-
workflowHash: await computeWorkflowRevision({
|
|
754
|
-
config: deps.config,
|
|
755
|
-
workflowName: input.workflowName,
|
|
756
|
-
workflow: input.workflow,
|
|
757
|
-
action: input.action,
|
|
758
|
-
}),
|
|
759
|
-
promptHash: await promptHashForAction(input.action),
|
|
760
|
-
...(repositoryHead === undefined ? {} : { repositoryHead }),
|
|
761
|
-
...(workspaceHead === undefined ? {} : { workspaceHead }),
|
|
762
|
-
runnerConfigurationHash: sha256({
|
|
763
|
-
routing: input.routing,
|
|
764
|
-
runner: deps.config.runners[input.routing.runnerName],
|
|
765
|
-
}),
|
|
766
|
-
projection: input.projection,
|
|
767
|
-
recentEvents: input.recentEvents,
|
|
768
|
-
});
|
|
769
|
-
}
|
|
770
|
-
// Counted from durable run records (never an in-memory counter, per the
|
|
771
|
-
// "tick is a pure function of durable state" invariant), so this holds
|
|
772
|
-
// across process restarts and is a backstop independent of any specific
|
|
773
|
-
// dispatch-eligibility bug (stuck watcher, misconfigured cron, a dedupe gap
|
|
774
|
-
// not yet caught) - it caps total damage rather than preventing a cause.
|
|
775
|
-
async function exceedsDispatchRateLimit(now) {
|
|
776
|
-
const { windowMs, maxDispatches } = deps.config.scheduler.dispatchRateLimit;
|
|
777
|
-
const windowStartMs = now.getTime() - windowMs;
|
|
778
|
-
const runRecords = await deps.stateStore.listRunRecordSummaries();
|
|
779
|
-
const recentCount = runRecords.reduce((count, record) => {
|
|
780
|
-
const startedAtMs = Date.parse(record.startedAt);
|
|
781
|
-
return Number.isFinite(startedAtMs) &&
|
|
782
|
-
startedAtMs >= windowStartMs &&
|
|
783
|
-
startedAtMs <= now.getTime()
|
|
784
|
-
? count + 1
|
|
785
|
-
: count;
|
|
786
|
-
}, 0);
|
|
787
|
-
return recentCount >= maxDispatches;
|
|
788
|
-
}
|
|
789
|
-
function cancellationReasonForIneligibleProjection(projection) {
|
|
790
|
-
if (projection === null || projection.issue.state !== 'open') {
|
|
791
|
-
return 'CANCELED_BY_SOURCE_CLOSED';
|
|
792
|
-
}
|
|
793
|
-
const requiredAssignees = deps.config.sources.github.policy.requiredAssignees;
|
|
794
|
-
if (requiredAssignees.length > 0 &&
|
|
795
|
-
!requiredAssignees.some((login) => projection.issue.assignees.includes(login))) {
|
|
796
|
-
return 'CANCELED_BY_UNASSIGNMENT';
|
|
797
|
-
}
|
|
798
|
-
const workflow = workflowForProjection(projection, deps.config);
|
|
799
|
-
if (workflow === null || !isKnownWorkflowStage(projection.wake.stage, workflow)) {
|
|
800
|
-
return 'CANCELED_BY_WORKFLOW_CHANGE';
|
|
801
|
-
}
|
|
802
|
-
return 'CANCELED_BY_SUPERSEDING_EVENT';
|
|
803
|
-
}
|
|
804
|
-
async function isRunningRecordActive(record, now) {
|
|
805
|
-
if (record.lease !== undefined && !isRunLeaseExpired(record, now)) {
|
|
806
|
-
return true;
|
|
807
|
-
}
|
|
808
|
-
return (processIdentityMatches({
|
|
809
|
-
pid: record.agentPid,
|
|
810
|
-
processStartedAt: record.agentProcessStartedAt,
|
|
811
|
-
}) ||
|
|
812
|
-
processIdentityMatches({
|
|
813
|
-
pid: record.workerPid,
|
|
814
|
-
processStartedAt: record.workerProcessStartedAt,
|
|
815
|
-
}));
|
|
816
|
-
}
|
|
817
|
-
async function hasSchedulerCapacity(now) {
|
|
818
|
-
const runRecords = await deps.stateStore.listRunRecordSummaries();
|
|
819
|
-
for (const record of runRecords) {
|
|
820
|
-
if (record.status !== 'running') {
|
|
821
|
-
continue;
|
|
822
|
-
}
|
|
823
|
-
if (await isRunningRecordActive(record, now)) {
|
|
824
|
-
return false;
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
return true;
|
|
828
|
-
}
|
|
829
|
-
async function parkConfigDriftedProjections(projections) {
|
|
830
|
-
let parked = false;
|
|
831
|
-
for (const projection of projections) {
|
|
832
|
-
if (projection.wake.blockReason === workflowChangedBlockReason) {
|
|
833
|
-
continue;
|
|
834
|
-
}
|
|
835
|
-
const workflow = workflowForProjection(projection, deps.config);
|
|
836
|
-
if (workflow !== null && isKnownWorkflowStage(projection.wake.stage, workflow)) {
|
|
837
|
-
continue;
|
|
838
|
-
}
|
|
839
|
-
const occurredAt = eventStampNow();
|
|
840
|
-
const eventId = `workflow-changed-${projection.workItemKey}-${deps.clock.now().getTime()}`;
|
|
841
|
-
const event = createEventEnvelope({
|
|
842
|
-
eventId,
|
|
843
|
-
workItemKey: projection.workItemKey,
|
|
844
|
-
streamScope: 'work-item',
|
|
845
|
-
direction: 'internal',
|
|
846
|
-
sourceSystem: 'wake',
|
|
847
|
-
sourceEventType: RUN_COMPLETED_EVENT,
|
|
848
|
-
sourceRefs: {
|
|
849
|
-
repo: projection.issue.repo,
|
|
850
|
-
issueNumber: projection.issue.number,
|
|
851
|
-
runId: eventId,
|
|
852
|
-
},
|
|
853
|
-
occurredAt,
|
|
854
|
-
ingestedAt: occurredAt,
|
|
855
|
-
trigger: 'immediate',
|
|
856
|
-
payload: {
|
|
857
|
-
sentinel: 'BLOCKED',
|
|
858
|
-
runId: eventId,
|
|
859
|
-
reason: workflowChangedBlockReason,
|
|
860
|
-
blockReason: workflowChangedBlockReason,
|
|
861
|
-
body: `Workflow configuration changed; stored workflow or stage is no longer configured.`,
|
|
862
|
-
},
|
|
863
|
-
});
|
|
864
|
-
await deps.stateStore.appendEventEnvelope(event);
|
|
865
|
-
await projectionUpdater.rebuildFromEvents([event]);
|
|
866
|
-
await deliverOutboundEvent(createLabelsEvent({
|
|
867
|
-
projection,
|
|
868
|
-
runId: eventId,
|
|
869
|
-
...(await currentLabelsForWorkItem(projection.workItemKey, projection)),
|
|
870
|
-
occurredAt,
|
|
871
|
-
}));
|
|
872
|
-
parked = true;
|
|
873
|
-
}
|
|
874
|
-
return parked;
|
|
875
|
-
}
|
|
876
|
-
async function runIntakeTick() {
|
|
877
|
-
const lock = await acquireFileLock(deps.stateStore.paths.tickLockFile, {
|
|
878
|
-
staleAfterMs: Math.min(runnerTimeoutMs(), 5 * 60 * 1000),
|
|
879
|
-
});
|
|
880
|
-
if (!lock.acquired) {
|
|
881
|
-
return { status: 'locked' };
|
|
882
|
-
}
|
|
883
|
-
try {
|
|
884
|
-
const tickStartedAt = deps.clock.now();
|
|
885
|
-
await reconcileStaleRunningRecords(tickStartedAt);
|
|
886
|
-
await retryUnconfirmedDeliveries();
|
|
887
|
-
const watchlistProjections = await deps.stateStore.listIssueStates();
|
|
888
|
-
const inboundEvents = await ingestInboundEvents(await deps.workSource.pollEvents({ watch: deriveWatchlist(watchlistProjections) }));
|
|
889
|
-
for (const event of inboundEvents) {
|
|
890
|
-
const trigger = event.payload.trigger;
|
|
891
|
-
const workflowName = typeof event.payload.workflow === 'string' ? event.payload.workflow : undefined;
|
|
892
|
-
const workflow = workflowName === undefined ? undefined : deps.config.workflows[workflowName];
|
|
893
|
-
if (trigger?.kind !== 'schedule' || workflowName === undefined || workflow === undefined) {
|
|
894
|
-
continue;
|
|
895
|
-
}
|
|
896
|
-
const timestamp = eventStampNow();
|
|
897
|
-
await appendAuditEvent({
|
|
898
|
-
eventId: `${event.eventId}-audit-trigger-fired`,
|
|
899
|
-
decisionType: 'trigger.fired',
|
|
900
|
-
workItemKey: event.workItemKey,
|
|
901
|
-
runId: event.eventId,
|
|
902
|
-
workflowRevision: await computeWorkflowRevision({
|
|
903
|
-
config: deps.config,
|
|
904
|
-
workflowName,
|
|
905
|
-
workflow,
|
|
906
|
-
}),
|
|
907
|
-
inputsConsidered: {
|
|
908
|
-
workflow: workflowName,
|
|
909
|
-
schedule: workflow.trigger?.schedule,
|
|
910
|
-
trigger,
|
|
911
|
-
sourceEventId: event.eventId,
|
|
912
|
-
},
|
|
913
|
-
outcome: {
|
|
914
|
-
fired: true,
|
|
915
|
-
sourceEventType: event.sourceEventType,
|
|
916
|
-
},
|
|
917
|
-
timestamp,
|
|
918
|
-
sourceRefs: event.sourceRefs,
|
|
919
|
-
});
|
|
920
|
-
}
|
|
921
|
-
const projections = await deps.stateStore.listIssueStates();
|
|
922
|
-
await cleanupClosedIssueWorkspaces(projections);
|
|
923
|
-
await sweepExpiredTranscriptDirs();
|
|
924
|
-
// Runs every tick, not only when this tick happened to poll a fresh
|
|
925
|
-
// inbound event — a stale label on a parked item (nothing else ever
|
|
926
|
-
// re-checks it once there's no next action) otherwise never self-heals.
|
|
927
|
-
await markPendingActionableIssues(projections);
|
|
928
|
-
return {
|
|
929
|
-
status: inboundEvents.length > 0 ? 'processed' : 'idle',
|
|
930
|
-
};
|
|
931
|
-
}
|
|
932
|
-
finally {
|
|
933
|
-
await lock.release();
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
function watcherStatus(projection) {
|
|
937
|
-
const sentinel = projection.context.lastRunSentinel;
|
|
938
|
-
if (isAwaitingApproval(projection))
|
|
939
|
-
return 'awaiting-approval';
|
|
940
|
-
if (sentinel === 'REJECTED')
|
|
941
|
-
return 'rejected';
|
|
942
|
-
if (sentinel === 'BLOCKED')
|
|
943
|
-
return 'blocked';
|
|
944
|
-
if (sentinel === 'FAILED')
|
|
945
|
-
return 'failed';
|
|
946
|
-
if (sentinel === 'DONE')
|
|
947
|
-
return 'done';
|
|
948
|
-
return 'pending';
|
|
949
|
-
}
|
|
950
|
-
function watcherKey(input) {
|
|
951
|
-
return [
|
|
952
|
-
input.workItemKey,
|
|
953
|
-
input.parentWorkflowName,
|
|
954
|
-
input.parentStage,
|
|
955
|
-
String(input.watcherIndex),
|
|
956
|
-
]
|
|
957
|
-
.join('__')
|
|
958
|
-
.replace(/[^A-Za-z0-9._-]/g, '_');
|
|
959
|
-
}
|
|
960
|
-
function watcherStateFile(key) {
|
|
961
|
-
return join(deps.stateStore.paths.dataRoot, 'watchers', `${key}.json`);
|
|
962
|
-
}
|
|
963
|
-
async function readWatcherState(key) {
|
|
964
|
-
try {
|
|
965
|
-
const raw = await readJsonFile(watcherStateFile(key));
|
|
966
|
-
if (raw === null || typeof raw !== 'object')
|
|
967
|
-
return null;
|
|
968
|
-
const record = raw;
|
|
969
|
-
return record.schemaVersion === 1 &&
|
|
970
|
-
record.key === key &&
|
|
971
|
-
typeof record.updatedAt === 'string'
|
|
972
|
-
? {
|
|
973
|
-
schemaVersion: 1,
|
|
974
|
-
key,
|
|
975
|
-
...(typeof record.lastDispatchedEventId === 'string'
|
|
976
|
-
? { lastDispatchedEventId: record.lastDispatchedEventId }
|
|
977
|
-
: {}),
|
|
978
|
-
...(typeof record.lastDispatchedSlot === 'string'
|
|
979
|
-
? { lastDispatchedSlot: record.lastDispatchedSlot }
|
|
980
|
-
: {}),
|
|
981
|
-
...(typeof record.failureCount === 'number' &&
|
|
982
|
-
Number.isInteger(record.failureCount) &&
|
|
983
|
-
record.failureCount >= 0
|
|
984
|
-
? { failureCount: record.failureCount }
|
|
985
|
-
: {}),
|
|
986
|
-
updatedAt: record.updatedAt,
|
|
987
|
-
}
|
|
988
|
-
: null;
|
|
989
|
-
}
|
|
990
|
-
catch (error) {
|
|
991
|
-
if (isMissingPathError(error))
|
|
992
|
-
return null;
|
|
993
|
-
throw error;
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
async function writeWatcherState(key, patch) {
|
|
997
|
-
const current = await readWatcherState(key);
|
|
998
|
-
await writeJsonFile(watcherStateFile(key), {
|
|
999
|
-
schemaVersion: 1,
|
|
1000
|
-
key,
|
|
1001
|
-
...(current?.lastDispatchedEventId === undefined
|
|
1002
|
-
? {}
|
|
1003
|
-
: { lastDispatchedEventId: current.lastDispatchedEventId }),
|
|
1004
|
-
...(current?.lastDispatchedSlot === undefined
|
|
1005
|
-
? {}
|
|
1006
|
-
: { lastDispatchedSlot: current.lastDispatchedSlot }),
|
|
1007
|
-
...(current?.failureCount === undefined ? {} : { failureCount: current.failureCount }),
|
|
1008
|
-
...patch,
|
|
1009
|
-
updatedAt: deps.clock.now().toISOString(),
|
|
1010
|
-
});
|
|
1011
|
-
}
|
|
1012
|
-
function watcherCursorPatch(trigger) {
|
|
1013
|
-
return {
|
|
1014
|
-
...(trigger.kind === 'event'
|
|
1015
|
-
? { lastDispatchedEventId: trigger.eventId }
|
|
1016
|
-
: { lastDispatchedSlot: trigger.slot }),
|
|
1017
|
-
failureCount: 0,
|
|
1018
|
-
};
|
|
1019
|
-
}
|
|
1020
|
-
function isRetryEligibleWatcherFailure(input) {
|
|
1021
|
-
return (input.sentinel === 'FAILED' &&
|
|
1022
|
-
(input.retrySafety === 'SAFE_TO_RETRY' || input.retrySafety === 'SAFE_TO_RESUME'));
|
|
1023
|
-
}
|
|
1024
|
-
async function updateWatcherStateAfterCompletion(input) {
|
|
1025
|
-
if (input.key === undefined || input.trigger === undefined)
|
|
1026
|
-
return;
|
|
1027
|
-
if (isRetryEligibleWatcherFailure(input)) {
|
|
1028
|
-
const current = await readWatcherState(input.key);
|
|
1029
|
-
const failureCount = (current?.failureCount ?? 0) + 1;
|
|
1030
|
-
if (failureCount < deps.config.retry.maxFailureRetries) {
|
|
1031
|
-
await writeWatcherState(input.key, { failureCount });
|
|
1032
|
-
return;
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
await writeWatcherState(input.key, watcherCursorPatch(input.trigger));
|
|
1036
|
-
}
|
|
1037
|
-
async function nextWatcherDispatch(projections, now) {
|
|
1038
|
-
for (const projection of projections) {
|
|
1039
|
-
// A closed issue can still carry a stale status label (e.g. squash-merged
|
|
1040
|
-
// outside Wake's own merge gate, before label reconciliation caught up),
|
|
1041
|
-
// so this must be checked explicitly - unlike deriveWatchlist and
|
|
1042
|
-
// cancellationReasonForIneligibleProjection, watcher dispatch has no
|
|
1043
|
-
// other path that excludes closed issues. Without this, a closed issue
|
|
1044
|
-
// whose last-seen local status matches `watcher.while.status` (e.g.
|
|
1045
|
-
// awaiting-approval) re-fires its watcher workflow every tick forever.
|
|
1046
|
-
if (projection.issue.state !== 'open' || !isWorkItemRunnable(projection))
|
|
1047
|
-
continue;
|
|
1048
|
-
const parentWorkflow = workflowForProjection(projection, deps.config);
|
|
1049
|
-
if (parentWorkflow === null)
|
|
1050
|
-
continue;
|
|
1051
|
-
const parentWorkflowName = workflowNameForProjection(projection, deps.config);
|
|
1052
|
-
const stage = parentWorkflow.stages[projection.wake.stage];
|
|
1053
|
-
if (stage === undefined)
|
|
1054
|
-
continue;
|
|
1055
|
-
for (const [watcherIndex, watcher] of (stage.watch ?? []).entries()) {
|
|
1056
|
-
if (!watcher.while.status.includes(watcherStatus(projection)))
|
|
1057
|
-
continue;
|
|
1058
|
-
const key = watcherKey({
|
|
1059
|
-
workItemKey: projection.workItemKey,
|
|
1060
|
-
parentWorkflowName,
|
|
1061
|
-
parentStage: projection.wake.stage,
|
|
1062
|
-
watcherIndex,
|
|
1063
|
-
});
|
|
1064
|
-
const state = await readWatcherState(key);
|
|
1065
|
-
if (watcher.on !== undefined) {
|
|
1066
|
-
const events = await deps.stateStore.listRecentEventEnvelopes({
|
|
1067
|
-
workItemKey: projection.workItemKey,
|
|
1068
|
-
direction: 'internal',
|
|
1069
|
-
limit: 200,
|
|
1070
|
-
});
|
|
1071
|
-
const matchingEvents = events
|
|
1072
|
-
.filter((event) => watcher.on.event.includes(event.sourceEventType))
|
|
1073
|
-
.filter((event) => !isEventFromWatcherRun(event))
|
|
1074
|
-
.sort((left, right) => left.ingestedAt.localeCompare(right.ingestedAt));
|
|
1075
|
-
const cursorIndex = state?.lastDispatchedEventId === undefined
|
|
1076
|
-
? -1
|
|
1077
|
-
: matchingEvents.findIndex((event) => event.eventId === state.lastDispatchedEventId);
|
|
1078
|
-
const undispatched = matchingEvents.slice(cursorIndex + 1);
|
|
1079
|
-
const next = (state?.failureCount ?? 0) > 0 ? undispatched.at(0) : undispatched.at(-1);
|
|
1080
|
-
if (next !== undefined) {
|
|
1081
|
-
return {
|
|
1082
|
-
projection,
|
|
1083
|
-
parentWorkflowName,
|
|
1084
|
-
parentStage: projection.wake.stage,
|
|
1085
|
-
watcherIndex,
|
|
1086
|
-
targetWorkflowName: watcher.workflow,
|
|
1087
|
-
trigger: { kind: 'event', eventId: next.eventId },
|
|
1088
|
-
};
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
if (watcher.schedule !== undefined) {
|
|
1092
|
-
const slot = previousMatchingSlot({
|
|
1093
|
-
cron: watcher.schedule.cron,
|
|
1094
|
-
now,
|
|
1095
|
-
...(state?.lastDispatchedSlot === undefined ? {} : { after: state.lastDispatchedSlot }),
|
|
1096
|
-
});
|
|
1097
|
-
if (slot !== null) {
|
|
1098
|
-
return {
|
|
1099
|
-
projection,
|
|
1100
|
-
parentWorkflowName,
|
|
1101
|
-
parentStage: projection.wake.stage,
|
|
1102
|
-
watcherIndex,
|
|
1103
|
-
targetWorkflowName: watcher.workflow,
|
|
1104
|
-
trigger: { kind: 'schedule', slot },
|
|
1105
|
-
};
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
return null;
|
|
1111
|
-
}
|
|
1112
|
-
async function resolvePrReviewTarget(input) {
|
|
1113
|
-
if (deps.artifactVerifier === undefined) {
|
|
1114
|
-
return null;
|
|
1115
|
-
}
|
|
1116
|
-
const { artifacts } = parseRunnerArtifacts(input.runnerResult.result);
|
|
1117
|
-
for (const artifact of artifacts) {
|
|
1118
|
-
const verified = await deps.artifactVerifier.verify(artifact, {
|
|
1119
|
-
branch: branchNameForIssue(input.projection.issue.number),
|
|
1120
|
-
repo: input.projection.issue.repo,
|
|
1121
|
-
});
|
|
1122
|
-
if (verified === null) {
|
|
1123
|
-
continue;
|
|
1124
|
-
}
|
|
1125
|
-
const incumbent = await deps.resourceIndex.resolve(verified.resourceUri);
|
|
1126
|
-
if (incumbent !== undefined && incumbent !== input.projection.workItemKey) {
|
|
1127
|
-
const conflict = createEventEnvelope({
|
|
1128
|
-
eventId: `${input.runId}-pr-review-primary-conflict`,
|
|
1129
|
-
workItemKey: input.projection.workItemKey,
|
|
1130
|
-
streamScope: 'work-item',
|
|
1131
|
-
direction: 'internal',
|
|
1132
|
-
sourceSystem: 'wake',
|
|
1133
|
-
sourceEventType: CORRELATION_PRIMARY_CONFLICT_EVENT,
|
|
1134
|
-
sourceRefs: { runId: input.runId },
|
|
1135
|
-
occurredAt: input.occurredAt,
|
|
1136
|
-
ingestedAt: input.occurredAt,
|
|
1137
|
-
trigger: 'context-only',
|
|
1138
|
-
payload: {
|
|
1139
|
-
resourceUri: verified.resourceUri,
|
|
1140
|
-
incumbentWorkItemKey: incumbent,
|
|
1141
|
-
},
|
|
1142
|
-
});
|
|
1143
|
-
const appended = await deps.stateStore.appendEventEnvelope(conflict);
|
|
1144
|
-
await projectionUpdater.rebuildFromEvents([appended]);
|
|
1145
|
-
return null;
|
|
1146
|
-
}
|
|
1147
|
-
if (incumbent === undefined) {
|
|
1148
|
-
const event = createEventEnvelope({
|
|
1149
|
-
eventId: `${input.runId}-pr-review-artifact-${verified.resourceUri.replace(/[^a-z0-9]+/gi, '-')}`,
|
|
1150
|
-
workItemKey: input.projection.workItemKey,
|
|
1151
|
-
streamScope: 'work-item',
|
|
1152
|
-
direction: 'internal',
|
|
1153
|
-
sourceSystem: 'wake',
|
|
1154
|
-
sourceEventType: CORRELATION_REGISTERED_EVENT,
|
|
1155
|
-
sourceRefs: { runId: input.runId },
|
|
1156
|
-
occurredAt: input.occurredAt,
|
|
1157
|
-
ingestedAt: input.occurredAt,
|
|
1158
|
-
trigger: 'context-only',
|
|
1159
|
-
payload: {
|
|
1160
|
-
resourceUri: verified.resourceUri,
|
|
1161
|
-
role: 'implementation',
|
|
1162
|
-
relation: 'primary',
|
|
1163
|
-
provenance: 'agent-reported',
|
|
1164
|
-
registeredBy: input.runId,
|
|
1165
|
-
idempotencyKey: `${input.runId}:pr-review-artifact-registration:${verified.resourceUri}`,
|
|
1166
|
-
},
|
|
1167
|
-
});
|
|
1168
|
-
const appended = await deps.stateStore.appendEventEnvelope(event);
|
|
1169
|
-
await projectionUpdater.rebuildFromEvents([appended]);
|
|
1170
|
-
}
|
|
1171
|
-
return verified.resourceUri;
|
|
1172
|
-
}
|
|
1173
|
-
return null;
|
|
1174
|
-
}
|
|
1175
|
-
async function runRunnerTick() {
|
|
1176
|
-
const lock = await acquireFileLock(deps.stateStore.paths.runnerLockFile, {
|
|
1177
|
-
staleAfterMs: Math.min(runnerTimeoutMs(), 5 * 60 * 1000),
|
|
1178
|
-
});
|
|
1179
|
-
if (!lock.acquired) {
|
|
1180
|
-
return { status: 'locked' };
|
|
1181
|
-
}
|
|
1182
|
-
try {
|
|
1183
|
-
// The tick's *decision* clock: one consistent "now" for staleness and
|
|
1184
|
-
// policy, so a single tick's decisions can't disagree with themselves.
|
|
1185
|
-
// It is deliberately NOT an event-stamping clock — see eventStampNow().
|
|
1186
|
-
// Its only remaining uses are the run records' startedAt (run records
|
|
1187
|
-
// are not events and take no part in the rebuild fold).
|
|
1188
|
-
const tickStartedAt = deps.clock.now();
|
|
1189
|
-
const nowIso = tickStartedAt.toISOString();
|
|
1190
|
-
await reconcileStaleRunningRecords(tickStartedAt);
|
|
1191
|
-
const projections = await deps.stateStore.listIssueStates();
|
|
1192
|
-
if (await parkConfigDriftedProjections(projections)) {
|
|
1193
|
-
return { status: 'processed' };
|
|
1194
|
-
}
|
|
1195
|
-
let candidate = projections.find((issue) => isWorkItemRunnable(issue) &&
|
|
1196
|
-
policy.resolveNextEligibleAction(issue, deps.config) !== null);
|
|
1197
|
-
const watcherDispatch = candidate === undefined ? await nextWatcherDispatch(projections, tickStartedAt) : null;
|
|
1198
|
-
candidate ??= watcherDispatch?.projection;
|
|
1199
|
-
let watcherStateKeyForRun;
|
|
1200
|
-
let watcherTriggerForRun;
|
|
1201
|
-
const watcherRun = watcherDispatch !== null;
|
|
1202
|
-
if (candidate === undefined) {
|
|
1203
|
-
return { status: 'idle' };
|
|
1204
|
-
}
|
|
1205
|
-
if (await exceedsDispatchRateLimit(tickStartedAt)) {
|
|
1206
|
-
const rateLimitedWorkflowName = workflowNameForProjection(candidate, deps.config);
|
|
1207
|
-
const rateLimitedWorkflow = workflowForProjection(candidate, deps.config);
|
|
1208
|
-
const blockedAt = eventStampNow();
|
|
1209
|
-
await appendAuditEvent({
|
|
1210
|
-
eventId: `dispatch-rate-limited-${candidate.workItemKey}-${tickStartedAt.getTime()}`,
|
|
1211
|
-
decisionType: 'dispatch.rate-limited',
|
|
1212
|
-
workItemKey: candidate.workItemKey,
|
|
1213
|
-
runId: `dispatch-rate-limited-${candidate.workItemKey}-${tickStartedAt.getTime()}`,
|
|
1214
|
-
workflowRevision: rateLimitedWorkflow === null
|
|
1215
|
-
? 'sha256:unavailable'
|
|
1216
|
-
: await computeWorkflowRevision({
|
|
1217
|
-
config: deps.config,
|
|
1218
|
-
workflowName: rateLimitedWorkflowName,
|
|
1219
|
-
workflow: rateLimitedWorkflow,
|
|
1220
|
-
}),
|
|
1221
|
-
inputsConsidered: {
|
|
1222
|
-
windowMs: deps.config.scheduler.dispatchRateLimit.windowMs,
|
|
1223
|
-
maxDispatches: deps.config.scheduler.dispatchRateLimit.maxDispatches,
|
|
1224
|
-
watcherRun,
|
|
1225
|
-
},
|
|
1226
|
-
outcome: { dispatched: false, reason: 'dispatch-rate-limit-exceeded' },
|
|
1227
|
-
timestamp: blockedAt,
|
|
1228
|
-
sourceRefs: { repo: candidate.issue.repo, issueNumber: candidate.issue.number },
|
|
1229
|
-
});
|
|
1230
|
-
return { status: 'idle' };
|
|
1231
|
-
}
|
|
1232
|
-
let sourceRevision = projectedSourceRevision(candidate);
|
|
1233
|
-
let refresh;
|
|
1234
|
-
try {
|
|
1235
|
-
refresh = await deps.workSource.refreshForDispatch?.({ projection: candidate });
|
|
1236
|
-
}
|
|
1237
|
-
catch {
|
|
1238
|
-
return { status: 'idle' };
|
|
1239
|
-
}
|
|
1240
|
-
if (refresh !== undefined && refresh !== null) {
|
|
1241
|
-
if (refresh.sourceExists === false) {
|
|
1242
|
-
return { status: 'idle' };
|
|
1243
|
-
}
|
|
1244
|
-
sourceRevision = refresh.sourceRevision;
|
|
1245
|
-
if (refresh.events.length > 0) {
|
|
1246
|
-
await ingestInboundEvents(refresh.events);
|
|
1247
|
-
candidate = (await deps.stateStore.readIssueState(candidate.workItemKey)) ?? candidate;
|
|
1248
|
-
}
|
|
1249
|
-
}
|
|
1250
|
-
if (!watcherRun && policy.resolveNextEligibleAction(candidate, deps.config) === null) {
|
|
1251
|
-
return { status: 'idle' };
|
|
1252
|
-
}
|
|
1253
|
-
const workflow = workflowForProjection(candidate, deps.config);
|
|
1254
|
-
if (workflow === null) {
|
|
1255
|
-
return { status: 'idle' };
|
|
1256
|
-
}
|
|
1257
|
-
let workflowName = workflowNameForProjection(candidate, deps.config);
|
|
1258
|
-
let action;
|
|
1259
|
-
let command;
|
|
1260
|
-
let claimedStage = candidate.wake.stage;
|
|
1261
|
-
let workspaceMode = 'none';
|
|
1262
|
-
let promptContextOverrides;
|
|
1263
|
-
if (watcherDispatch !== null) {
|
|
1264
|
-
const targetWorkflow = deps.config.workflows[watcherDispatch.targetWorkflowName];
|
|
1265
|
-
if (targetWorkflow === undefined) {
|
|
1266
|
-
return { status: 'idle' };
|
|
1267
|
-
}
|
|
1268
|
-
const entryStageName = workflowEntryStage(targetWorkflow);
|
|
1269
|
-
const entryStage = targetWorkflow.stages[entryStageName];
|
|
1270
|
-
if (entryStage === undefined) {
|
|
1271
|
-
return { status: 'idle' };
|
|
1272
|
-
}
|
|
1273
|
-
action = entryStage.action ?? entryStageName;
|
|
1274
|
-
claimedStage = entryStageName;
|
|
1275
|
-
workspaceMode = entryStage.workspace;
|
|
1276
|
-
// The triggering event's own body (e.g. a refine plan comment) is
|
|
1277
|
-
// durable and already local the moment this fires — projection.comments
|
|
1278
|
-
// only gets it once a later GitHub poll echoes it back, which a watcher
|
|
1279
|
-
// dispatched off the same-tick completion event can easily outrace.
|
|
1280
|
-
const triggeringEvent = watcherDispatch.trigger.kind === 'event'
|
|
1281
|
-
? await deps.stateStore.readEventEnvelope(watcherDispatch.trigger.eventId)
|
|
1282
|
-
: null;
|
|
1283
|
-
const triggeringBody = triggeringEvent?.payload.body;
|
|
1284
|
-
promptContextOverrides = {
|
|
1285
|
-
...entryStage.promptContext,
|
|
1286
|
-
...(typeof triggeringBody === 'string'
|
|
1287
|
-
? { parentPendingReviewBody: triggeringBody }
|
|
1288
|
-
: {}),
|
|
1289
|
-
};
|
|
1290
|
-
workflowName = watcherDispatch.targetWorkflowName;
|
|
1291
|
-
watcherStateKeyForRun = watcherKey({
|
|
1292
|
-
workItemKey: watcherDispatch.projection.workItemKey,
|
|
1293
|
-
parentWorkflowName: watcherDispatch.parentWorkflowName,
|
|
1294
|
-
parentStage: watcherDispatch.parentStage,
|
|
1295
|
-
watcherIndex: watcherDispatch.watcherIndex,
|
|
1296
|
-
});
|
|
1297
|
-
watcherTriggerForRun = watcherDispatch.trigger;
|
|
1298
|
-
}
|
|
1299
|
-
else if (isAwaitingApproval(candidate)) {
|
|
1300
|
-
const customCommandRequest = policy.resolveCustomCommandRequest(candidate, deps.config);
|
|
1301
|
-
if (customCommandRequest !== null) {
|
|
1302
|
-
action = customCommandRequest.action;
|
|
1303
|
-
command = customCommandRequest.command;
|
|
1304
|
-
claimedStage = candidate.wake.stage;
|
|
1305
|
-
workspaceMode = customCommandRequest.workspace;
|
|
1306
|
-
}
|
|
1307
|
-
else {
|
|
1308
|
-
const approvalResolution = policy.resolveApprovalTransition(candidate);
|
|
1309
|
-
if (approvalResolution === null) {
|
|
1310
|
-
const changesRequestedAction = policy.resolveChangesRequestedAction(candidate);
|
|
1311
|
-
if (changesRequestedAction !== null) {
|
|
1312
|
-
action = changesRequestedAction;
|
|
1313
|
-
const workflowAction = chooseWorkflowAction(candidate, workflow);
|
|
1314
|
-
claimedStage = workflowAction?.stage ?? candidate.wake.stage;
|
|
1315
|
-
workspaceMode = workflowAction?.workspace ?? 'none';
|
|
1316
|
-
promptContextOverrides = {
|
|
1317
|
-
...workflowAction?.promptContext,
|
|
1318
|
-
...(typeof candidate.context.changesRequestedFeedback === 'string'
|
|
1319
|
-
? { parentPendingReviewBody: candidate.context.changesRequestedFeedback }
|
|
1320
|
-
: {}),
|
|
1321
|
-
};
|
|
1322
|
-
}
|
|
1323
|
-
else {
|
|
1324
|
-
const reviewAction = policy.resolvePendingReviewFeedback(candidate);
|
|
1325
|
-
if (reviewAction === null) {
|
|
1326
|
-
return { status: 'idle' };
|
|
1327
|
-
}
|
|
1328
|
-
action = reviewAction;
|
|
1329
|
-
const workflowAction = chooseWorkflowAction(candidate, workflow);
|
|
1330
|
-
claimedStage = workflowAction?.stage ?? candidate.wake.stage;
|
|
1331
|
-
workspaceMode = workflowAction?.workspace ?? 'none';
|
|
1332
|
-
promptContextOverrides = workflowAction?.promptContext;
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
else if (approvalResolution.approved) {
|
|
1336
|
-
const mergePolicy = approvedMergePolicyForStage(workflow.stages[candidate.wake.stage]);
|
|
1337
|
-
if (approvalResolution.targetResourceUri !== undefined && mergePolicy !== null) {
|
|
1338
|
-
const risk = await evaluatePrMergeRisk({
|
|
1339
|
-
projection: candidate,
|
|
1340
|
-
targetResourceUri: approvalResolution.targetResourceUri,
|
|
1341
|
-
policy: mergePolicy,
|
|
1342
|
-
});
|
|
1343
|
-
if (!risk.passed) {
|
|
1344
|
-
return await publishPrMergePolicyBlock({
|
|
1345
|
-
projection: candidate,
|
|
1346
|
-
approvalResolution,
|
|
1347
|
-
reason: risk.reason,
|
|
1348
|
-
workflowName,
|
|
1349
|
-
});
|
|
1350
|
-
}
|
|
1351
|
-
const mergeActionsResult = await performApprovedPrMergeActions({
|
|
1352
|
-
projection: candidate,
|
|
1353
|
-
approvalResolution,
|
|
1354
|
-
mergePolicy,
|
|
1355
|
-
});
|
|
1356
|
-
if (mergeActionsResult.blocked) {
|
|
1357
|
-
return await publishPrMergePolicyBlock({
|
|
1358
|
-
projection: candidate,
|
|
1359
|
-
approvalResolution,
|
|
1360
|
-
reason: mergeActionsResult.reason,
|
|
1361
|
-
workflowName,
|
|
1362
|
-
});
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
|
-
const approvalId = `approval-${candidate.issue.number}-${deps.clock.now().getTime()}`;
|
|
1366
|
-
const approvedAt = deps.clock.now().toISOString();
|
|
1367
|
-
const automaticApproval = approvalResolution.automatic === true;
|
|
1368
|
-
const applied = await applyApprovalTransition({
|
|
1369
|
-
projection: candidate,
|
|
1370
|
-
pendingAction: approvalResolution.pendingAction,
|
|
1371
|
-
approvalId,
|
|
1372
|
-
approvedAt,
|
|
1373
|
-
reason: automaticApproval ? 'auto:approved' : 'human:approved',
|
|
1374
|
-
workflow,
|
|
1375
|
-
workflowName,
|
|
1376
|
-
payloadExtras: automaticApproval
|
|
1377
|
-
? {
|
|
1378
|
-
autoResolution: {
|
|
1379
|
-
kind: 'awaiting-approval',
|
|
1380
|
-
classification: 'auto-approval',
|
|
1381
|
-
reasoning: approvalResolution.reason,
|
|
1382
|
-
},
|
|
1383
|
-
}
|
|
1384
|
-
: {
|
|
1385
|
-
handledCommentId: approvalResolution.triggeringCommentId ?? latestHumanCommentId(candidate),
|
|
1386
|
-
},
|
|
1387
|
-
});
|
|
1388
|
-
if (applied === null) {
|
|
1389
|
-
return { status: 'idle' };
|
|
1390
|
-
}
|
|
1391
|
-
if (automaticApproval) {
|
|
1392
|
-
await appendAuditEvent({
|
|
1393
|
-
eventId: `${approvalId}-audit-auto-resolution`,
|
|
1394
|
-
decisionType: 'approval.auto-resolved',
|
|
1395
|
-
workItemKey: candidate.workItemKey,
|
|
1396
|
-
runId: approvalId,
|
|
1397
|
-
workflowRevision: await computeWorkflowRevision({
|
|
1398
|
-
config: deps.config,
|
|
1399
|
-
workflowName,
|
|
1400
|
-
workflow,
|
|
1401
|
-
action: approvalResolution.pendingAction,
|
|
1402
|
-
}),
|
|
1403
|
-
inputsConsidered: {
|
|
1404
|
-
labels: candidate.issue.labels,
|
|
1405
|
-
pendingAction: approvalResolution.pendingAction,
|
|
1406
|
-
pendingApprovalAllowAutoApproval: candidate.context.pendingApprovalAllowAutoApproval === true,
|
|
1407
|
-
},
|
|
1408
|
-
outcome: {
|
|
1409
|
-
approved: true,
|
|
1410
|
-
nextStage: applied.nextStage,
|
|
1411
|
-
reason: approvalResolution.reason,
|
|
1412
|
-
},
|
|
1413
|
-
timestamp: approvedAt,
|
|
1414
|
-
sourceRefs: {
|
|
1415
|
-
repo: candidate.issue.repo,
|
|
1416
|
-
issueNumber: candidate.issue.number,
|
|
1417
|
-
},
|
|
1418
|
-
});
|
|
1419
|
-
}
|
|
1420
|
-
return {
|
|
1421
|
-
status: 'processed',
|
|
1422
|
-
runId: approvalId,
|
|
1423
|
-
sentinel: 'DONE',
|
|
1424
|
-
nextStage: applied.nextStage,
|
|
1425
|
-
};
|
|
1426
|
-
}
|
|
1427
|
-
else {
|
|
1428
|
-
const changesRequestId = `changes-requested-${candidate.issue.number}-${deps.clock.now().getTime()}`;
|
|
1429
|
-
const requestedAt = deps.clock.now().toISOString();
|
|
1430
|
-
const changesRequestedTriggeringCommentId = approvalResolution.triggeringCommentId ?? latestHumanCommentId(candidate);
|
|
1431
|
-
const { escalated } = await applyChangesRequestedTransition({
|
|
1432
|
-
projection: candidate,
|
|
1433
|
-
requestId: changesRequestId,
|
|
1434
|
-
requestedAt,
|
|
1435
|
-
...(approvalResolution.triggeringCommentBody === undefined
|
|
1436
|
-
? {}
|
|
1437
|
-
: { feedbackBody: approvalResolution.triggeringCommentBody }),
|
|
1438
|
-
...(changesRequestedTriggeringCommentId === undefined
|
|
1439
|
-
? {}
|
|
1440
|
-
: { triggeringCommentId: changesRequestedTriggeringCommentId }),
|
|
1441
|
-
});
|
|
1442
|
-
if (escalated) {
|
|
1443
|
-
return { status: 'idle' };
|
|
1444
|
-
}
|
|
1445
|
-
action = approvalResolution.pendingAction;
|
|
1446
|
-
const workflowAction = chooseWorkflowAction(candidate, workflow);
|
|
1447
|
-
claimedStage = workflowAction?.stage ?? candidate.wake.stage;
|
|
1448
|
-
workspaceMode = workflowAction?.workspace ?? 'none';
|
|
1449
|
-
promptContextOverrides = {
|
|
1450
|
-
...workflowAction?.promptContext,
|
|
1451
|
-
...(approvalResolution.changesRequested === true &&
|
|
1452
|
-
approvalResolution.triggeringCommentBody !== undefined
|
|
1453
|
-
? { parentPendingReviewBody: approvalResolution.triggeringCommentBody }
|
|
1454
|
-
: {}),
|
|
1455
|
-
};
|
|
1456
|
-
}
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
else {
|
|
1460
|
-
const workflowAction = chooseWorkflowAction(candidate, workflow);
|
|
1461
|
-
// Retry takes priority over the stage's fresh default action.
|
|
1462
|
-
// chooseWorkflowAction almost always returns a non-null action for
|
|
1463
|
-
// any valid stage (e.g. 'implement'), so checking it first would
|
|
1464
|
-
// silently discard chooseRetryActionAfterHumanReply's decision
|
|
1465
|
-
// whenever a FAILED/BLOCKED run left a lateral action (like
|
|
1466
|
-
// `revise`) unfinished with a fresh human reply waiting — instead
|
|
1467
|
-
// of resuming that action, it would restart the stage from
|
|
1468
|
-
// scratch (#258 follow-up incident: a FAILED `revise` run fell
|
|
1469
|
-
// back to a full fresh `implement` run and lost the PR-feedback
|
|
1470
|
-
// context).
|
|
1471
|
-
const nextAction = policy.resolveCustomCommandRequest(candidate, deps.config)?.action ??
|
|
1472
|
-
policy.chooseRetryActionAfterHumanReply(candidate, workflow) ??
|
|
1473
|
-
workflowAction?.action ??
|
|
1474
|
-
null;
|
|
1475
|
-
if (nextAction === null) {
|
|
1476
|
-
return { status: 'idle' };
|
|
1477
|
-
}
|
|
1478
|
-
action = nextAction;
|
|
1479
|
-
command = policy.resolveCustomCommandRequest(candidate, deps.config)?.command;
|
|
1480
|
-
claimedStage = workflowAction?.stage ?? candidate.wake.stage;
|
|
1481
|
-
workspaceMode =
|
|
1482
|
-
customCommandWorkspace(action, deps.config) ?? workflowAction?.workspace ?? 'none';
|
|
1483
|
-
promptContextOverrides = workflowAction?.promptContext;
|
|
1484
|
-
}
|
|
1485
|
-
// Resolve routing (with sideways fallback across quota-paused runners,
|
|
1486
|
-
// #67) before claiming a run, so a fully-paused tier costs nothing more
|
|
1487
|
-
// than an idle tick instead of a claimed-but-doomed run record.
|
|
1488
|
-
const ledgerAtStart = await deps.stateStore.readLedger();
|
|
1489
|
-
const routing = resolveRunnerRouting({
|
|
1490
|
-
config: deps.config,
|
|
1491
|
-
stage: claimedStage,
|
|
1492
|
-
action,
|
|
1493
|
-
workflowName,
|
|
1494
|
-
...(command === undefined ? {} : { command }),
|
|
1495
|
-
now: tickStartedAt,
|
|
1496
|
-
...(ledgerAtStart === null ? {} : { ledger: ledgerAtStart }),
|
|
1497
|
-
});
|
|
1498
|
-
if (routing === null) {
|
|
1499
|
-
return { status: 'idle' };
|
|
1500
|
-
}
|
|
1501
|
-
if (!(await hasSchedulerCapacity(deps.clock.now()))) {
|
|
1502
|
-
return { status: 'idle' };
|
|
1503
|
-
}
|
|
1504
|
-
const runId = `run-${candidate.issue.number}-${deps.clock.now().getTime()}`;
|
|
1505
|
-
const lease = createRunLease({ clock: deps.clock, ownerInstanceId });
|
|
1506
|
-
const workerIdentity = currentProcessIdentity();
|
|
1507
|
-
const runningRecord = {
|
|
1508
|
-
schemaVersion: 1,
|
|
1509
|
-
runId,
|
|
1510
|
-
workItemKey: candidate.workItemKey,
|
|
1511
|
-
repo: candidate.issue.repo,
|
|
1512
|
-
issueNumber: candidate.issue.number,
|
|
1513
|
-
action,
|
|
1514
|
-
lifecycle: 'CREATED',
|
|
1515
|
-
status: 'running',
|
|
1516
|
-
startedAt: nowIso,
|
|
1517
|
-
routing,
|
|
1518
|
-
lease,
|
|
1519
|
-
workerPid: workerIdentity.pid,
|
|
1520
|
-
workerProcessStartedAt: workerIdentity.processStartedAt,
|
|
1521
|
-
metadata: {
|
|
1522
|
-
sourceRevision,
|
|
1523
|
-
...(watcherRun
|
|
1524
|
-
? {
|
|
1525
|
-
watcher: true,
|
|
1526
|
-
watcherWorkflow: workflowName,
|
|
1527
|
-
watcherStateKey: watcherStateKeyForRun,
|
|
1528
|
-
watcherTrigger: watcherTriggerForRun,
|
|
1529
|
-
}
|
|
1530
|
-
: {}),
|
|
1531
|
-
},
|
|
1532
|
-
};
|
|
1533
|
-
await deps.stateStore.writeRunRecord(runningRecord);
|
|
1534
|
-
async function transitionRunLifecycle(lifecycle) {
|
|
1535
|
-
await deps.stateStore.writeRunRecord({
|
|
1536
|
-
...(await deps.stateStore.readRunRecord(runId)),
|
|
1537
|
-
lifecycle,
|
|
1538
|
-
});
|
|
1539
|
-
}
|
|
1540
|
-
async function appendRuntimeEvent(event) {
|
|
1541
|
-
const timestamp = event.timestamp ?? deps.clock.now().toISOString();
|
|
1542
|
-
await deps.stateStore.updateRunRecordIf(runId, {
|
|
1543
|
-
expect: (record) => record.status === 'running' &&
|
|
1544
|
-
record.lease?.leaseId === lease.leaseId &&
|
|
1545
|
-
record.lease.ownerInstanceId === ownerInstanceId,
|
|
1546
|
-
update: (record) => {
|
|
1547
|
-
const runtimeEvents = record.runtimeEvents ?? [];
|
|
1548
|
-
const sequence = event.sequence ?? runtimeEvents.length;
|
|
1549
|
-
const normalizedEvent = {
|
|
1550
|
-
...event,
|
|
1551
|
-
timestamp,
|
|
1552
|
-
sequence,
|
|
1553
|
-
};
|
|
1554
|
-
return {
|
|
1555
|
-
...record,
|
|
1556
|
-
runtimeEvents: [...runtimeEvents, normalizedEvent],
|
|
1557
|
-
...(isMeaningfulRuntimeEvent(normalizedEvent)
|
|
1558
|
-
? { lastMeaningfulActivityAt: normalizedEvent.timestamp }
|
|
1559
|
-
: {}),
|
|
1560
|
-
};
|
|
1561
|
-
},
|
|
1562
|
-
});
|
|
1563
|
-
}
|
|
1564
|
-
const claimedAt = eventStampNow();
|
|
1565
|
-
const claimedEvent = createEventEnvelope({
|
|
1566
|
-
eventId: `${runId}-claimed`,
|
|
1567
|
-
workItemKey: candidate.workItemKey,
|
|
1568
|
-
streamScope: 'work-item',
|
|
1569
|
-
direction: 'internal',
|
|
1570
|
-
sourceSystem: 'wake',
|
|
1571
|
-
sourceEventType: RUN_CLAIMED_EVENT,
|
|
1572
|
-
sourceRefs: {
|
|
1573
|
-
repo: candidate.issue.repo,
|
|
1574
|
-
issueNumber: candidate.issue.number,
|
|
1575
|
-
runId,
|
|
1576
|
-
},
|
|
1577
|
-
occurredAt: claimedAt,
|
|
1578
|
-
ingestedAt: claimedAt,
|
|
1579
|
-
trigger: 'immediate',
|
|
1580
|
-
payload: {
|
|
1581
|
-
action,
|
|
1582
|
-
priorStage: candidate.wake.stage,
|
|
1583
|
-
...(watcherRun ? {} : { claimedStage }),
|
|
1584
|
-
sourceRevision,
|
|
1585
|
-
...(watcherRun ? { watcherRun: true, watcherTrigger: watcherTriggerForRun } : {}),
|
|
1586
|
-
},
|
|
1587
|
-
});
|
|
1588
|
-
await deps.stateStore.appendEventEnvelope(claimedEvent);
|
|
1589
|
-
if (watcherRun && watcherDispatch !== null && watcherTriggerForRun !== undefined) {
|
|
1590
|
-
await appendAuditEvent({
|
|
1591
|
-
eventId: `${runId}-audit-watcher-dispatched`,
|
|
1592
|
-
decisionType: 'watcher.dispatched',
|
|
1593
|
-
workItemKey: candidate.workItemKey,
|
|
1594
|
-
runId,
|
|
1595
|
-
workflowRevision: await computeWorkflowRevision({
|
|
1596
|
-
config: deps.config,
|
|
1597
|
-
workflowName,
|
|
1598
|
-
workflow: deps.config.workflows[workflowName] ?? workflow,
|
|
1599
|
-
action,
|
|
1600
|
-
}),
|
|
1601
|
-
inputsConsidered: {
|
|
1602
|
-
parentWorkflowName: watcherDispatch.parentWorkflowName,
|
|
1603
|
-
parentStage: watcherDispatch.parentStage,
|
|
1604
|
-
watcherIndex: watcherDispatch.watcherIndex,
|
|
1605
|
-
watcherStatus: watcherStatus(candidate),
|
|
1606
|
-
trigger: watcherTriggerForRun,
|
|
1607
|
-
},
|
|
1608
|
-
outcome: {
|
|
1609
|
-
dispatched: true,
|
|
1610
|
-
targetWorkflowName: workflowName,
|
|
1611
|
-
action,
|
|
1612
|
-
claimedStage,
|
|
1613
|
-
},
|
|
1614
|
-
timestamp: claimedAt,
|
|
1615
|
-
sourceRefs: {
|
|
1616
|
-
repo: candidate.issue.repo,
|
|
1617
|
-
issueNumber: candidate.issue.number,
|
|
1618
|
-
},
|
|
1619
|
-
});
|
|
1620
|
-
}
|
|
1621
|
-
await transitionRunLifecycle('CLAIMED');
|
|
1622
|
-
await projectionUpdater.rebuildFromEvents([claimedEvent]);
|
|
1623
|
-
// Watcher runs execute a *different* workflow's stage (e.g. plan-review)
|
|
1624
|
-
// against the same parent projection, not a stage transition of the
|
|
1625
|
-
// parent's own workflow — writing labels here would stamp the child
|
|
1626
|
-
// workflow's stage/workflow onto the parent's GitHub issue. The parent's
|
|
1627
|
-
// labels must only ever reflect the parent's own action (see the mirrored
|
|
1628
|
-
// guard on the completion path below).
|
|
1629
|
-
if (!watcherRun) {
|
|
1630
|
-
await deliverOutboundEvent(createLabelsEvent({
|
|
1631
|
-
projection: candidate,
|
|
1632
|
-
runId,
|
|
1633
|
-
...(await currentLabelsForWorkItem(candidate.workItemKey, candidate)),
|
|
1634
|
-
occurredAt: eventStampNow(),
|
|
1635
|
-
}));
|
|
1636
|
-
}
|
|
1637
|
-
let leaseRenewalTimer;
|
|
1638
|
-
function startLeaseRenewal() {
|
|
1639
|
-
leaseRenewalTimer = setInterval(() => {
|
|
1640
|
-
void renewRunLease({
|
|
1641
|
-
stateStore: deps.stateStore,
|
|
1642
|
-
runId,
|
|
1643
|
-
leaseId: lease.leaseId,
|
|
1644
|
-
ownerInstanceId,
|
|
1645
|
-
clock: deps.clock,
|
|
1646
|
-
});
|
|
1647
|
-
}, runLeaseRenewalIntervalMs);
|
|
1648
|
-
}
|
|
1649
|
-
function delayActiveRunRefresh() {
|
|
1650
|
-
return new Promise((resolve) => {
|
|
1651
|
-
const timer = setTimeout(resolve, activeRunSourceRefreshIntervalMs);
|
|
1652
|
-
timer.unref?.();
|
|
1653
|
-
});
|
|
1654
|
-
}
|
|
1655
|
-
const recentEvents = await deps.stateStore.listEventEnvelopesForWorkItem(candidate.workItemKey, 6);
|
|
1656
|
-
const activeCandidate = candidate;
|
|
1657
|
-
const runnerProjection = watcherRun
|
|
1658
|
-
? {
|
|
1659
|
-
...activeCandidate,
|
|
1660
|
-
wake: {
|
|
1661
|
-
...activeCandidate.wake,
|
|
1662
|
-
sessionId: undefined,
|
|
1663
|
-
sessionCli: undefined,
|
|
1664
|
-
},
|
|
1665
|
-
}
|
|
1666
|
-
: activeCandidate;
|
|
1667
|
-
const inputSnapshot = await createRunInputSnapshot({
|
|
1668
|
-
runId,
|
|
1669
|
-
createdAt: deps.clock.now().toISOString(),
|
|
1670
|
-
action,
|
|
1671
|
-
workflowName,
|
|
1672
|
-
workflow: deps.config.workflows[workflowName] ?? workflow,
|
|
1673
|
-
claimedStage,
|
|
1674
|
-
projection: runnerProjection,
|
|
1675
|
-
recentEvents,
|
|
1676
|
-
sourceRevision,
|
|
1677
|
-
routing,
|
|
1678
|
-
...(watcherTriggerForRun === undefined ? {} : { watcherTrigger: watcherTriggerForRun }),
|
|
1679
|
-
});
|
|
1680
|
-
await deps.stateStore.writeRunRecord({
|
|
1681
|
-
...(await deps.stateStore.readRunRecord(runId)),
|
|
1682
|
-
inputSnapshotId: inputSnapshot.snapshotId,
|
|
1683
|
-
});
|
|
1684
|
-
try {
|
|
1685
|
-
await transitionRunLifecycle('PREPARING');
|
|
1686
|
-
const prepareResult = workspaceMode === 'branch'
|
|
1687
|
-
? await deps.workspaceManager.prepareWorkspace({
|
|
1688
|
-
workId: candidate.workItemKey,
|
|
1689
|
-
repo: candidate.issue.repo,
|
|
1690
|
-
issueNumber: candidate.issue.number,
|
|
1691
|
-
})
|
|
1692
|
-
: workspaceMode === 'read-only'
|
|
1693
|
-
? await deps.workspaceManager.prepareReadOnlyClone({
|
|
1694
|
-
repo: candidate.issue.repo,
|
|
1695
|
-
})
|
|
1696
|
-
: {};
|
|
1697
|
-
const { workspacePath } = prepareResult;
|
|
1698
|
-
const mergeConflictDetected = 'mergeConflictDetected' in prepareResult ? prepareResult.mergeConflictDetected : false;
|
|
1699
|
-
const upstreamChanges = 'upstreamChanges' in prepareResult && typeof prepareResult.upstreamChanges === 'string'
|
|
1700
|
-
? prepareResult.upstreamChanges
|
|
1701
|
-
: undefined;
|
|
1702
|
-
const preExistingUncommittedChanges = 'preExistingUncommittedChanges' in prepareResult
|
|
1703
|
-
? prepareResult.preExistingUncommittedChanges
|
|
1704
|
-
: false;
|
|
1705
|
-
const preparedRecord = (await deps.stateStore.readRunRecord(runId));
|
|
1706
|
-
await deps.stateStore.writeRunRecord({
|
|
1707
|
-
...preparedRecord,
|
|
1708
|
-
lifecycle: 'PROCESS_STARTING',
|
|
1709
|
-
metadata: {
|
|
1710
|
-
...preparedRecord.metadata,
|
|
1711
|
-
...(workspacePath === undefined ? {} : { workspacePath }),
|
|
1712
|
-
workspaceMode,
|
|
1713
|
-
inputSnapshotId: inputSnapshot.snapshotId,
|
|
1714
|
-
...(prepareResult.validation === undefined
|
|
1715
|
-
? {}
|
|
1716
|
-
: { workspaceValidation: prepareResult.validation }),
|
|
1717
|
-
},
|
|
1718
|
-
});
|
|
1719
|
-
await transitionRunLifecycle('RUNNING');
|
|
1720
|
-
startLeaseRenewal();
|
|
1721
|
-
let executionFinished = false;
|
|
1722
|
-
let cancellationReason = null;
|
|
1723
|
-
const runnerInput = {
|
|
1724
|
-
action,
|
|
1725
|
-
projection: runnerProjection,
|
|
1726
|
-
recentEvents,
|
|
1727
|
-
config: deps.config,
|
|
1728
|
-
runId,
|
|
1729
|
-
routing,
|
|
1730
|
-
workspaceMode,
|
|
1731
|
-
...(promptContextOverrides === undefined ? {} : { promptContextOverrides }),
|
|
1732
|
-
...(workspacePath === undefined ? {} : { workspacePath }),
|
|
1733
|
-
...(mergeConflictDetected ? { mergeConflictDetected: true } : {}),
|
|
1734
|
-
...(upstreamChanges === undefined ? {} : { upstreamChanges }),
|
|
1735
|
-
...(preExistingUncommittedChanges ? { preExistingUncommittedChanges: true } : {}),
|
|
1736
|
-
onProcessStart: async (identity) => {
|
|
1737
|
-
await deps.stateStore.updateRunRecordIf(runId, {
|
|
1738
|
-
expect: (record) => record.status === 'running' &&
|
|
1739
|
-
record.lease?.leaseId === lease.leaseId &&
|
|
1740
|
-
record.lease.ownerInstanceId === ownerInstanceId,
|
|
1741
|
-
update: (record) => ({
|
|
1742
|
-
...record,
|
|
1743
|
-
agentPid: identity.pid,
|
|
1744
|
-
agentProcessStartedAt: identity.processStartedAt,
|
|
1745
|
-
}),
|
|
1746
|
-
});
|
|
1747
|
-
},
|
|
1748
|
-
onRuntimeEvent: appendRuntimeEvent,
|
|
1749
|
-
};
|
|
1750
|
-
const execution = deps.runner.start === undefined
|
|
1751
|
-
? createAgentExecution(runnerInput, (liveInput) => deps.runner.run(liveInput))
|
|
1752
|
-
: await deps.runner.start(runnerInput);
|
|
1753
|
-
async function persistCancellationRequest(reason) {
|
|
1754
|
-
const requestedAt = deps.clock.now().toISOString();
|
|
1755
|
-
await deps.stateStore.updateRunRecordIf(runId, {
|
|
1756
|
-
expect: (record) => record.status === 'running' &&
|
|
1757
|
-
record.lease?.leaseId === lease.leaseId &&
|
|
1758
|
-
record.lease.ownerInstanceId === ownerInstanceId,
|
|
1759
|
-
update: (record) => ({
|
|
1760
|
-
...record,
|
|
1761
|
-
metadata: {
|
|
1762
|
-
...record.metadata,
|
|
1763
|
-
cancellation: {
|
|
1764
|
-
reason,
|
|
1765
|
-
requestedAt,
|
|
1766
|
-
source: 'active-source-reconciliation',
|
|
1767
|
-
},
|
|
1768
|
-
},
|
|
1769
|
-
}),
|
|
1770
|
-
});
|
|
1771
|
-
}
|
|
1772
|
-
async function superviseActiveSource() {
|
|
1773
|
-
if (deps.workSource.refreshForDispatch === undefined) {
|
|
1774
|
-
return null;
|
|
1775
|
-
}
|
|
1776
|
-
while (!executionFinished) {
|
|
1777
|
-
await delayActiveRunRefresh();
|
|
1778
|
-
if (executionFinished) {
|
|
1779
|
-
return null;
|
|
1780
|
-
}
|
|
1781
|
-
let activeRefresh;
|
|
1782
|
-
try {
|
|
1783
|
-
activeRefresh = await deps.workSource.refreshForDispatch({
|
|
1784
|
-
projection: activeCandidate,
|
|
1785
|
-
});
|
|
1786
|
-
}
|
|
1787
|
-
catch {
|
|
1788
|
-
continue;
|
|
1789
|
-
}
|
|
1790
|
-
if (activeRefresh === null) {
|
|
1791
|
-
continue;
|
|
1792
|
-
}
|
|
1793
|
-
if (activeRefresh.events.length > 0) {
|
|
1794
|
-
await ingestInboundEvents(activeRefresh.events);
|
|
1795
|
-
}
|
|
1796
|
-
const refreshedProjection = (await deps.stateStore.readIssueState(activeCandidate.workItemKey)) ??
|
|
1797
|
-
activeCandidate;
|
|
1798
|
-
const newHumanComments = newHumanCommentsSince(inputSnapshot.projection, refreshedProjection);
|
|
1799
|
-
if (requestsInterrupt(newHumanComments)) {
|
|
1800
|
-
const reason = 'CANCELED_BY_SUPERSEDING_EVENT';
|
|
1801
|
-
cancellationReason = reason;
|
|
1802
|
-
await persistCancellationRequest(reason);
|
|
1803
|
-
await execution.cancel(reason);
|
|
1804
|
-
return reason;
|
|
1805
|
-
}
|
|
1806
|
-
const ineligible = activeRefresh.sourceExists === false ||
|
|
1807
|
-
!policy.isEligible(refreshedProjection, deps.config);
|
|
1808
|
-
if (!ineligible) {
|
|
1809
|
-
continue;
|
|
1810
|
-
}
|
|
1811
|
-
const reason = cancellationReasonForIneligibleProjection(activeRefresh.sourceExists === false ? null : refreshedProjection);
|
|
1812
|
-
cancellationReason = reason;
|
|
1813
|
-
await persistCancellationRequest(reason);
|
|
1814
|
-
await execution.cancel(reason);
|
|
1815
|
-
return reason;
|
|
1816
|
-
}
|
|
1817
|
-
return null;
|
|
1818
|
-
}
|
|
1819
|
-
const runnerResult = await Promise.race([
|
|
1820
|
-
execution.result.finally(() => {
|
|
1821
|
-
executionFinished = true;
|
|
1822
|
-
}),
|
|
1823
|
-
superviseActiveSource().then(async (reason) => {
|
|
1824
|
-
if (reason === null) {
|
|
1825
|
-
return execution.result;
|
|
1826
|
-
}
|
|
1827
|
-
return execution.result;
|
|
1828
|
-
}),
|
|
1829
|
-
]);
|
|
1830
|
-
clearInterval(leaseRenewalTimer);
|
|
1831
|
-
leaseRenewalTimer = undefined;
|
|
1832
|
-
const parsedRunnerResult = parseRunnerResult(runnerResult.result);
|
|
1833
|
-
const sentinel = parsedRunnerResult.status;
|
|
1834
|
-
// The approval gate is pure policy, never the agent's word choice —
|
|
1835
|
-
// a DONE on a stage configured with skipApproval: false is gated
|
|
1836
|
-
// regardless of what the agent wrote (ADR 0002).
|
|
1837
|
-
const skipApproval = runnerResult.metadata?.skipApproval;
|
|
1838
|
-
const approvalGated = sentinel === 'DONE' && skipApproval === false;
|
|
1839
|
-
// A canceled run must not advance the stage regardless of what the
|
|
1840
|
-
// runner echoed back — the snapshot it acted on was superseded.
|
|
1841
|
-
const nextStage = cancellationReason !== null
|
|
1842
|
-
? null
|
|
1843
|
-
: approvalGated
|
|
1844
|
-
? null
|
|
1845
|
-
: isLateralReadOnlyAction(action, deps.config) && sentinel === 'DONE'
|
|
1846
|
-
? null
|
|
1847
|
-
: lifecycle.nextStageFromSentinel(claimedStage, sentinel, workflow);
|
|
1848
|
-
const finishedAt = deps.clock.now().toISOString();
|
|
1849
|
-
let workspaceBookkeeping;
|
|
1850
|
-
if (workspacePath !== undefined) {
|
|
1851
|
-
try {
|
|
1852
|
-
workspaceBookkeeping = {
|
|
1853
|
-
status: 'recorded',
|
|
1854
|
-
result: await deps.workspaceManager.recordWorkspaceBookkeeping({ workspacePath }),
|
|
1855
|
-
};
|
|
1856
|
-
}
|
|
1857
|
-
catch (error) {
|
|
1858
|
-
workspaceBookkeeping = {
|
|
1859
|
-
status: 'failed',
|
|
1860
|
-
failureSource: 'wake-workspace-bookkeeping',
|
|
1861
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1862
|
-
};
|
|
1863
|
-
}
|
|
1864
|
-
}
|
|
1865
|
-
let prReviewTargetResourceUri = null;
|
|
1866
|
-
if (watcherRun) {
|
|
1867
|
-
// Artifact correlation is intentionally scoped to any watcher-dispatched
|
|
1868
|
-
// run, not to an action literally named "pr-review".
|
|
1869
|
-
prReviewTargetResourceUri = await resolvePrReviewTarget({
|
|
1870
|
-
projection: candidate,
|
|
1871
|
-
runId,
|
|
1872
|
-
runnerResult,
|
|
1873
|
-
occurredAt: finishedAt,
|
|
1874
|
-
});
|
|
1875
|
-
await appendAuditEvent({
|
|
1876
|
-
eventId: `${runId}-audit-review-verdict`,
|
|
1877
|
-
decisionType: 'review.verdict',
|
|
1878
|
-
workItemKey: candidate.workItemKey,
|
|
1879
|
-
runId,
|
|
1880
|
-
workflowRevision: await computeWorkflowRevision({
|
|
1881
|
-
config: deps.config,
|
|
1882
|
-
workflowName,
|
|
1883
|
-
workflow: deps.config.workflows[workflowName] ?? workflow,
|
|
1884
|
-
action,
|
|
1885
|
-
}),
|
|
1886
|
-
inputsConsidered: {
|
|
1887
|
-
sourceRevision,
|
|
1888
|
-
watcherTrigger: watcherTriggerForRun,
|
|
1889
|
-
verifiedTargetResourceUri: prReviewTargetResourceUri,
|
|
1890
|
-
},
|
|
1891
|
-
outcome: {
|
|
1892
|
-
sentinel,
|
|
1893
|
-
verdict: prReviewTargetResourceUri === null
|
|
1894
|
-
? 'uncertain'
|
|
1895
|
-
: sentinel === 'DONE'
|
|
1896
|
-
? 'approved'
|
|
1897
|
-
: sentinel === 'REJECTED'
|
|
1898
|
-
? 'changes-requested'
|
|
1899
|
-
: 'uncertain',
|
|
1900
|
-
reasoning: parsedRunnerResult.body,
|
|
1901
|
-
runner: {
|
|
1902
|
-
model: runnerResult.model,
|
|
1903
|
-
cli: runnerResult.cli,
|
|
1904
|
-
},
|
|
1905
|
-
},
|
|
1906
|
-
timestamp: finishedAt,
|
|
1907
|
-
sourceRefs: {
|
|
1908
|
-
repo: candidate.issue.repo,
|
|
1909
|
-
issueNumber: candidate.issue.number,
|
|
1910
|
-
},
|
|
1911
|
-
});
|
|
1912
|
-
}
|
|
1913
|
-
else if (workspaceMode === 'branch') {
|
|
1914
|
-
await registerReportedArtifacts({
|
|
1915
|
-
projection: candidate,
|
|
1916
|
-
runId,
|
|
1917
|
-
runnerResult,
|
|
1918
|
-
branch: branchNameForIssue(candidate.issue.number),
|
|
1919
|
-
occurredAt: finishedAt,
|
|
1920
|
-
});
|
|
1921
|
-
}
|
|
1922
|
-
const failedRunnerName = runnerResult.routing?.runnerName ?? routing.runnerName;
|
|
1923
|
-
const existingRunners = ledgerAtStart?.runners ?? {};
|
|
1924
|
-
if (runnerResult.failureClass === 'quota') {
|
|
1925
|
-
const failureCount = (existingRunners[failedRunnerName]?.failureCount ?? 0) + 1;
|
|
1926
|
-
const quotaPause = resolveQuotaPauseUntil({
|
|
1927
|
-
result: runnerResult.result,
|
|
1928
|
-
now: new Date(finishedAt),
|
|
1929
|
-
failureCount,
|
|
1930
|
-
});
|
|
1931
|
-
await deps.stateStore.writeLedger({
|
|
1932
|
-
schemaVersion: 1,
|
|
1933
|
-
runners: {
|
|
1934
|
-
...existingRunners,
|
|
1935
|
-
[failedRunnerName]: {
|
|
1936
|
-
pausedUntil: quotaPause.pausedUntil,
|
|
1937
|
-
pausedUntilSource: quotaPause.source,
|
|
1938
|
-
failureCount,
|
|
1939
|
-
lastFailureAt: finishedAt,
|
|
1940
|
-
},
|
|
1941
|
-
},
|
|
1942
|
-
});
|
|
1943
|
-
}
|
|
1944
|
-
else if ((existingRunners[failedRunnerName]?.failureCount ?? 0) > 0) {
|
|
1945
|
-
await deps.stateStore.writeLedger({
|
|
1946
|
-
schemaVersion: 1,
|
|
1947
|
-
runners: {
|
|
1948
|
-
...existingRunners,
|
|
1949
|
-
[failedRunnerName]: { failureCount: 0 },
|
|
1950
|
-
},
|
|
1951
|
-
});
|
|
1952
|
-
}
|
|
1953
|
-
const resultMetadata = {
|
|
1954
|
-
...runnerResult.metadata,
|
|
1955
|
-
envelope: parsedRunnerResult.envelope,
|
|
1956
|
-
...(cancellationReason === null ? {} : { cancellationReason }),
|
|
1957
|
-
...(runnerResult.failureClass === undefined
|
|
1958
|
-
? {}
|
|
1959
|
-
: { failureClass: runnerResult.failureClass }),
|
|
1960
|
-
...(runnerResult.routing === undefined ? {} : { routing: runnerResult.routing }),
|
|
1961
|
-
};
|
|
1962
|
-
const executionOutcome = cancellationReason !== null
|
|
1963
|
-
? cancellationReason
|
|
1964
|
-
: runnerResult.failureClass === 'quota'
|
|
1965
|
-
? 'QUOTA_EXHAUSTED'
|
|
1966
|
-
: runnerResult.failureClass === 'infra'
|
|
1967
|
-
? 'PROCESS_FAILED'
|
|
1968
|
-
: 'COMPLETED';
|
|
1969
|
-
// Canceled runs don't produce a meaningful workflow outcome; the input
|
|
1970
|
-
// they acted on was superseded so the sentinel is not authoritative.
|
|
1971
|
-
const workflowOutcome = cancellationReason !== null
|
|
1972
|
-
? undefined
|
|
1973
|
-
: sentinel === 'DONE'
|
|
1974
|
-
? approvalGated
|
|
1975
|
-
? 'AWAITING_APPROVAL'
|
|
1976
|
-
: 'DONE'
|
|
1977
|
-
: sentinel === 'REJECTED'
|
|
1978
|
-
? 'CHANGES_REQUESTED'
|
|
1979
|
-
: sentinel === 'BLOCKED'
|
|
1980
|
-
? 'BLOCKED'
|
|
1981
|
-
: undefined;
|
|
1982
|
-
await transitionRunLifecycle('FINALISING');
|
|
1983
|
-
const finalisingRecord = (await deps.stateStore.readRunRecord(runId));
|
|
1984
|
-
const failureContext = sentinel === 'FAILED'
|
|
1985
|
-
? classifyFailedRun({
|
|
1986
|
-
projection: candidate,
|
|
1987
|
-
record: finalisingRecord,
|
|
1988
|
-
failureClass: runnerResult.failureClass ?? 'task',
|
|
1989
|
-
...(workspacePath === undefined ? {} : { workspacePath }),
|
|
1990
|
-
envelope: parsedRunnerResult.envelope,
|
|
1991
|
-
sentinel,
|
|
1992
|
-
})
|
|
1993
|
-
: undefined;
|
|
1994
|
-
await deps.stateStore.writeRunRecord({
|
|
1995
|
-
...finalisingRecord,
|
|
1996
|
-
lifecycle: 'TERMINAL',
|
|
1997
|
-
status: sentinel === 'DONE'
|
|
1998
|
-
? approvalGated
|
|
1999
|
-
? 'awaiting-approval'
|
|
2000
|
-
: 'completed'
|
|
2001
|
-
: sentinel === 'REJECTED'
|
|
2002
|
-
? 'rejected'
|
|
2003
|
-
: sentinel === 'BLOCKED'
|
|
2004
|
-
? 'blocked'
|
|
2005
|
-
: 'failed',
|
|
2006
|
-
finishedAt,
|
|
2007
|
-
sessionId: runnerResult.session_id,
|
|
2008
|
-
sentinel,
|
|
2009
|
-
executionOutcome,
|
|
2010
|
-
...(workflowOutcome !== undefined ? { workflowOutcome } : {}),
|
|
2011
|
-
...(failureContext === undefined ? {} : failureContext),
|
|
2012
|
-
summary: parsedRunnerResult.body,
|
|
2013
|
-
...(runnerResult.routing === undefined ? {} : { routing: runnerResult.routing }),
|
|
2014
|
-
...(runnerResult.tokenUsage === undefined ? {} : { tokenUsage: runnerResult.tokenUsage }),
|
|
2015
|
-
metadata: {
|
|
2016
|
-
...finalisingRecord.metadata,
|
|
2017
|
-
...(workspaceBookkeeping === undefined ? {} : { workspaceBookkeeping }),
|
|
2018
|
-
...resultMetadata,
|
|
2019
|
-
},
|
|
2020
|
-
});
|
|
2021
|
-
// Computed before the payload so a rejecting review verdict (either
|
|
2022
|
-
// the PR-artifact-verified path or a plain onSuccess.approve watcher
|
|
2023
|
-
// with no PR) can fold context.status = 'changes-requested' on the
|
|
2024
|
-
// parent via the same RUN_COMPLETED event, instead of requiring a
|
|
2025
|
-
// separate marker comment round-trip (§5).
|
|
2026
|
-
const watcherSuccessPolicy = watcherRun && watcherDispatch !== null
|
|
2027
|
-
? deps.config.workflows[watcherDispatch.parentWorkflowName]?.stages[watcherDispatch.parentStage]?.watch?.[watcherDispatch.watcherIndex]?.onSuccess
|
|
2028
|
-
: undefined;
|
|
2029
|
-
const isReviewRejection = watcherRun &&
|
|
2030
|
-
sentinel === 'REJECTED' &&
|
|
2031
|
-
(prReviewTargetResourceUri !== null || watcherSuccessPolicy?.approve === true);
|
|
2032
|
-
const runCompletedEvent = createEventEnvelope({
|
|
2033
|
-
eventId: `${runId}-completed`,
|
|
2034
|
-
workItemKey: candidate.workItemKey,
|
|
2035
|
-
streamScope: 'work-item',
|
|
2036
|
-
direction: 'internal',
|
|
2037
|
-
sourceSystem: 'wake',
|
|
2038
|
-
sourceEventType: RUN_COMPLETED_EVENT,
|
|
2039
|
-
sourceRefs: {
|
|
2040
|
-
repo: candidate.issue.repo,
|
|
2041
|
-
issueNumber: candidate.issue.number,
|
|
2042
|
-
runId,
|
|
2043
|
-
},
|
|
2044
|
-
occurredAt: finishedAt,
|
|
2045
|
-
ingestedAt: finishedAt,
|
|
2046
|
-
trigger: 'immediate',
|
|
2047
|
-
payload: {
|
|
2048
|
-
action,
|
|
2049
|
-
sentinel,
|
|
2050
|
-
approvalGated,
|
|
2051
|
-
allowAutoApproval: runnerResult.metadata?.allowAutoApproval === true,
|
|
2052
|
-
...(nextStage !== null ? { nextStage } : {}),
|
|
2053
|
-
runId,
|
|
2054
|
-
sessionId: runnerResult.session_id,
|
|
2055
|
-
sessionCli: runnerResult.cli,
|
|
2056
|
-
workspacePath,
|
|
2057
|
-
reason: cancellationReason === null
|
|
2058
|
-
? `runner:${sentinel.toLowerCase()}`
|
|
2059
|
-
: `runner:${String(cancellationReason).toLowerCase()}`,
|
|
2060
|
-
...(runnerResult.routing === undefined ? {} : { routing: runnerResult.routing }),
|
|
2061
|
-
...(runnerResult.failureClass === undefined
|
|
2062
|
-
? {}
|
|
2063
|
-
: { failureClass: runnerResult.failureClass }),
|
|
2064
|
-
...(failureContext === undefined ? {} : failureContext),
|
|
2065
|
-
// Only mark the triggering comment handled when the run reached the
|
|
2066
|
-
// agent and produced a real outcome. Quota/infra failures are transient
|
|
2067
|
-
// blips; canceled runs acted on a superseded snapshot — in both cases
|
|
2068
|
-
// leave handledCommentId unset so the next tick can retry (S9).
|
|
2069
|
-
...(runnerResult.failureClass === 'quota' ||
|
|
2070
|
-
runnerResult.failureClass === 'infra' ||
|
|
2071
|
-
cancellationReason !== null
|
|
2072
|
-
? {}
|
|
2073
|
-
: { handledCommentId: latestActionableCommentId(inputSnapshot.projection) }),
|
|
2074
|
-
body: parsedRunnerResult.body,
|
|
2075
|
-
envelope: parsedRunnerResult.envelope,
|
|
2076
|
-
executionOutcome,
|
|
2077
|
-
...(workflowOutcome !== undefined ? { workflowOutcome } : {}),
|
|
2078
|
-
...(watcherRun ? { watcherRun: true, watcherTrigger: watcherTriggerForRun } : {}),
|
|
2079
|
-
...(isReviewRejection
|
|
2080
|
-
? { changesRequested: true, reviewFeedbackBody: parsedRunnerResult.body }
|
|
2081
|
-
: {}),
|
|
2082
|
-
},
|
|
2083
|
-
});
|
|
2084
|
-
await deps.stateStore.appendEventEnvelope(runCompletedEvent);
|
|
2085
|
-
await projectionUpdater.rebuildFromEvents([runCompletedEvent]);
|
|
2086
|
-
await updateWatcherStateAfterCompletion({
|
|
2087
|
-
key: watcherStateKeyForRun,
|
|
2088
|
-
trigger: watcherTriggerForRun,
|
|
2089
|
-
sentinel,
|
|
2090
|
-
retrySafety: failureContext?.retrySafety,
|
|
2091
|
-
});
|
|
2092
|
-
if (!watcherRun) {
|
|
2093
|
-
await deliverOutboundEvent(createLabelsEvent({
|
|
2094
|
-
projection: candidate,
|
|
2095
|
-
runId,
|
|
2096
|
-
...(await currentLabelsForWorkItem(candidate.workItemKey, candidate)),
|
|
2097
|
-
occurredAt: finishedAt,
|
|
2098
|
-
}));
|
|
2099
|
-
}
|
|
2100
|
-
const publishIntent = createPublishIntentEvent({
|
|
2101
|
-
projection: candidate,
|
|
2102
|
-
runId,
|
|
2103
|
-
action,
|
|
2104
|
-
runnerResult,
|
|
2105
|
-
parsedRunnerResult,
|
|
2106
|
-
sentinel,
|
|
2107
|
-
approvalGated,
|
|
2108
|
-
occurredAt: finishedAt,
|
|
2109
|
-
startedAt: nowIso,
|
|
2110
|
-
...(workspacePath === undefined ? {} : { workspacePath }),
|
|
2111
|
-
...(typeof candidate.context.lastFailureClass === 'string'
|
|
2112
|
-
? { previousFailureClass: candidate.context.lastFailureClass }
|
|
2113
|
-
: {}),
|
|
2114
|
-
});
|
|
2115
|
-
if (watcherRun) {
|
|
2116
|
-
// Watcher-dispatched runs own PR verdict delivery and correlation
|
|
2117
|
-
// registration regardless of the target workflow/action name.
|
|
2118
|
-
const pendingApprovalAction = candidate.context.pendingApprovalAction;
|
|
2119
|
-
if (prReviewTargetResourceUri !== null &&
|
|
2120
|
-
(sentinel === 'DONE' || sentinel === 'REJECTED')) {
|
|
2121
|
-
await deliverOutboundEvent({
|
|
2122
|
-
...publishIntent,
|
|
2123
|
-
sourceRefs: {
|
|
2124
|
-
...publishIntent.sourceRefs,
|
|
2125
|
-
resourceUri: prReviewTargetResourceUri,
|
|
2126
|
-
},
|
|
2127
|
-
payload: {
|
|
2128
|
-
...publishIntent.payload,
|
|
2129
|
-
kind: 'status-update',
|
|
2130
|
-
body: sentinel === 'DONE'
|
|
2131
|
-
? `${parsedRunnerResult.body}\n\n${prReviewApprovalMarker}`
|
|
2132
|
-
: `${parsedRunnerResult.body}\n\n<!-- wake:pr-review-changes-requested -->`,
|
|
2133
|
-
idempotencyKey: `${runId}:pr-review-verdict-comment`,
|
|
2134
|
-
},
|
|
2135
|
-
});
|
|
2136
|
-
}
|
|
2137
|
-
else if (watcherDispatch !== null &&
|
|
2138
|
-
watcherSuccessPolicy?.approve === true &&
|
|
2139
|
-
(sentinel === 'DONE' ||
|
|
2140
|
-
sentinel === 'REJECTED' ||
|
|
2141
|
-
sentinel === 'FAILED' ||
|
|
2142
|
-
sentinel === 'BLOCKED')) {
|
|
2143
|
-
// No PR surface to carry the verdict comment: the child's sentinel
|
|
2144
|
-
// is its verdict. Publish the review body for every verdict so the
|
|
2145
|
-
// human sees why; only an ungated DONE resolves the parent's
|
|
2146
|
-
// pending gate (checked below via isAwaitingApproval).
|
|
2147
|
-
await deliverOutboundEvent(publishIntent);
|
|
2148
|
-
const approvalId = `${runId}-parent-approval`;
|
|
2149
|
-
const parentWorkflow = deps.config.workflows[watcherDispatch.parentWorkflowName];
|
|
2150
|
-
if (sentinel === 'DONE' &&
|
|
2151
|
-
isAwaitingApproval(candidate) &&
|
|
2152
|
-
typeof pendingApprovalAction === 'string' &&
|
|
2153
|
-
parentWorkflow !== undefined &&
|
|
2154
|
-
(await deps.stateStore.readEventEnvelope(`${approvalId}-completed`)) === null) {
|
|
2155
|
-
const approvedAt = eventStampNow();
|
|
2156
|
-
const applied = await applyApprovalTransition({
|
|
2157
|
-
projection: candidate,
|
|
2158
|
-
pendingAction: pendingApprovalAction,
|
|
2159
|
-
approvalId,
|
|
2160
|
-
approvedAt,
|
|
2161
|
-
reason: 'watcher:approved',
|
|
2162
|
-
workflow: parentWorkflow,
|
|
2163
|
-
workflowName: watcherDispatch.parentWorkflowName,
|
|
2164
|
-
});
|
|
2165
|
-
if (applied !== null) {
|
|
2166
|
-
await appendAuditEvent({
|
|
2167
|
-
eventId: `${approvalId}-audit-watcher-approval`,
|
|
2168
|
-
decisionType: 'approval.watcher-resolved',
|
|
2169
|
-
workItemKey: candidate.workItemKey,
|
|
2170
|
-
runId,
|
|
2171
|
-
workflowRevision: await computeWorkflowRevision({
|
|
2172
|
-
config: deps.config,
|
|
2173
|
-
workflowName: watcherDispatch.parentWorkflowName,
|
|
2174
|
-
workflow: parentWorkflow,
|
|
2175
|
-
action: pendingApprovalAction,
|
|
2176
|
-
}),
|
|
2177
|
-
inputsConsidered: {
|
|
2178
|
-
watcherWorkflow: watcherDispatch.targetWorkflowName,
|
|
2179
|
-
pendingAction: pendingApprovalAction,
|
|
2180
|
-
childRunId: runId,
|
|
2181
|
-
childSentinel: sentinel,
|
|
2182
|
-
},
|
|
2183
|
-
outcome: { approved: true, nextStage: applied.nextStage },
|
|
2184
|
-
timestamp: approvedAt,
|
|
2185
|
-
sourceRefs: {
|
|
2186
|
-
repo: candidate.issue.repo,
|
|
2187
|
-
issueNumber: candidate.issue.number,
|
|
2188
|
-
},
|
|
2189
|
-
});
|
|
2190
|
-
}
|
|
2191
|
-
}
|
|
2192
|
-
}
|
|
2193
|
-
else {
|
|
2194
|
-
await suppressOutboundEvent(publishIntent, {
|
|
2195
|
-
suppressedPublishReason: 'pr-review-no-actionable-verdict',
|
|
2196
|
-
});
|
|
2197
|
-
}
|
|
2198
|
-
}
|
|
2199
|
-
else if (shouldPublishRunResult({
|
|
2200
|
-
failureClass: runnerResult.failureClass,
|
|
2201
|
-
previousFailureClass: candidate.context.lastFailureClass,
|
|
2202
|
-
})) {
|
|
2203
|
-
await deliverOutboundEvent(publishIntent);
|
|
2204
|
-
}
|
|
2205
|
-
else {
|
|
2206
|
-
await suppressOutboundEvent(publishIntent, {
|
|
2207
|
-
suppressedPublishReason: runnerResult.failureClass === 'quota' ? 'quota-failure' : 'repeated-infra-failure',
|
|
2208
|
-
});
|
|
2209
|
-
}
|
|
2210
|
-
return {
|
|
2211
|
-
status: 'processed',
|
|
2212
|
-
runId,
|
|
2213
|
-
sentinel,
|
|
2214
|
-
nextStage,
|
|
2215
|
-
};
|
|
2216
|
-
}
|
|
2217
|
-
catch (err) {
|
|
2218
|
-
clearInterval(leaseRenewalTimer);
|
|
2219
|
-
const finishedAt = deps.clock.now().toISOString();
|
|
2220
|
-
const sentinel = 'FAILED';
|
|
2221
|
-
const failedRecord = (await deps.stateStore.readRunRecord(runId)) ?? runningRecord;
|
|
2222
|
-
const failedRecordMetadata = failedRecord.metadata;
|
|
2223
|
-
const failedRecordWorkspacePath = typeof failedRecordMetadata?.workspacePath === 'string'
|
|
2224
|
-
? failedRecordMetadata.workspacePath
|
|
2225
|
-
: undefined;
|
|
2226
|
-
const failureContext = classifyFailedRun({
|
|
2227
|
-
projection: candidate,
|
|
2228
|
-
record: failedRecord,
|
|
2229
|
-
failureClass: 'infra',
|
|
2230
|
-
...(isWorkspaceValidationFailure(err)
|
|
2231
|
-
? { failurePhase: 'workspace-validation' }
|
|
2232
|
-
: {}),
|
|
2233
|
-
...(failedRecordWorkspacePath === undefined
|
|
2234
|
-
? {}
|
|
2235
|
-
: { workspacePath: failedRecordWorkspacePath }),
|
|
2236
|
-
});
|
|
2237
|
-
await deps.stateStore.writeRunRecord({
|
|
2238
|
-
...failedRecord,
|
|
2239
|
-
lifecycle: 'TERMINAL',
|
|
2240
|
-
status: 'failed',
|
|
2241
|
-
finishedAt,
|
|
2242
|
-
sentinel,
|
|
2243
|
-
executionOutcome: 'PROCESS_FAILED',
|
|
2244
|
-
...failureContext,
|
|
2245
|
-
summary: err instanceof Error ? err.message : String(err),
|
|
2246
|
-
metadata: {
|
|
2247
|
-
...failedRecord.metadata,
|
|
2248
|
-
failureClass: 'infra',
|
|
2249
|
-
...(isWorkspaceValidationFailure(err)
|
|
2250
|
-
? { failureSource: 'wake-workspace-validation' }
|
|
2251
|
-
: {}),
|
|
2252
|
-
...failureContext,
|
|
2253
|
-
},
|
|
2254
|
-
});
|
|
2255
|
-
const runCompletedEvent = createEventEnvelope({
|
|
2256
|
-
eventId: `${runId}-completed`,
|
|
2257
|
-
workItemKey: candidate.workItemKey,
|
|
2258
|
-
streamScope: 'work-item',
|
|
2259
|
-
direction: 'internal',
|
|
2260
|
-
sourceSystem: 'wake',
|
|
2261
|
-
sourceEventType: RUN_COMPLETED_EVENT,
|
|
2262
|
-
sourceRefs: {
|
|
2263
|
-
repo: candidate.issue.repo,
|
|
2264
|
-
issueNumber: candidate.issue.number,
|
|
2265
|
-
runId,
|
|
2266
|
-
},
|
|
2267
|
-
occurredAt: finishedAt,
|
|
2268
|
-
ingestedAt: finishedAt,
|
|
2269
|
-
trigger: 'immediate',
|
|
2270
|
-
payload: {
|
|
2271
|
-
action,
|
|
2272
|
-
sentinel,
|
|
2273
|
-
runId,
|
|
2274
|
-
reason: 'runner:infrastructure-error',
|
|
2275
|
-
failureClass: 'infra',
|
|
2276
|
-
...failureContext,
|
|
2277
|
-
executionOutcome: 'PROCESS_FAILED',
|
|
2278
|
-
// Deliberately omit handledCommentId: an infra blip (CLI crash, timeout,
|
|
2279
|
-
// network error) never reached the agent, so it isn't an answer to the
|
|
2280
|
-
// triggering comment. Leaving it unset lets the next tick retry the same
|
|
2281
|
-
// request instead of silently eating it (S9).
|
|
2282
|
-
// Mirrors the happy-path completion payload below: without this flag,
|
|
2283
|
-
// projection-updater.ts folds sentinel/workflowOutcome onto the
|
|
2284
|
-
// *parent's* context, even though this failure belongs to a watcher's
|
|
2285
|
-
// own child-workflow run, not the parent's own action.
|
|
2286
|
-
...(watcherRun ? { watcherRun: true, watcherTrigger: watcherTriggerForRun } : {}),
|
|
2287
|
-
},
|
|
2288
|
-
});
|
|
2289
|
-
await deps.stateStore.appendEventEnvelope(runCompletedEvent);
|
|
2290
|
-
await projectionUpdater.rebuildFromEvents([runCompletedEvent]);
|
|
2291
|
-
await updateWatcherStateAfterCompletion({
|
|
2292
|
-
key: watcherStateKeyForRun,
|
|
2293
|
-
trigger: watcherTriggerForRun,
|
|
2294
|
-
sentinel,
|
|
2295
|
-
retrySafety: failureContext.retrySafety,
|
|
2296
|
-
});
|
|
2297
|
-
// See the matching guard on the claim path above: a watcher run's
|
|
2298
|
-
// failure is not the parent's own action failing, so it must not
|
|
2299
|
-
// stamp the child workflow's stage/workflow onto the parent's labels.
|
|
2300
|
-
if (!watcherRun) {
|
|
2301
|
-
await deliverOutboundEvent(createLabelsEvent({
|
|
2302
|
-
projection: candidate,
|
|
2303
|
-
runId,
|
|
2304
|
-
...(await currentLabelsForWorkItem(candidate.workItemKey, candidate)),
|
|
2305
|
-
occurredAt: finishedAt,
|
|
2306
|
-
}));
|
|
2307
|
-
}
|
|
2308
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
2309
|
-
const infraFailureResult = {
|
|
2310
|
-
result: errorMessage,
|
|
2311
|
-
model: 'unknown',
|
|
2312
|
-
cli: 'unknown',
|
|
2313
|
-
failureClass: 'infra',
|
|
2314
|
-
};
|
|
2315
|
-
const parsedInfraFailureResult = parseRunnerResult(infraFailureResult.result);
|
|
2316
|
-
const failurePublishIntent = createPublishIntentEvent({
|
|
2317
|
-
projection: candidate,
|
|
2318
|
-
runId,
|
|
2319
|
-
action,
|
|
2320
|
-
runnerResult: infraFailureResult,
|
|
2321
|
-
parsedRunnerResult: parsedInfraFailureResult,
|
|
2322
|
-
sentinel,
|
|
2323
|
-
occurredAt: finishedAt,
|
|
2324
|
-
startedAt: nowIso,
|
|
2325
|
-
...(typeof candidate.context.lastFailureClass === 'string'
|
|
2326
|
-
? { previousFailureClass: candidate.context.lastFailureClass }
|
|
2327
|
-
: {}),
|
|
2328
|
-
});
|
|
2329
|
-
if (shouldPublishRunResult({
|
|
2330
|
-
failureClass: 'infra',
|
|
2331
|
-
previousFailureClass: candidate.context.lastFailureClass,
|
|
2332
|
-
})) {
|
|
2333
|
-
await deliverOutboundEvent(failurePublishIntent);
|
|
2334
|
-
}
|
|
2335
|
-
else {
|
|
2336
|
-
await suppressOutboundEvent(failurePublishIntent, {
|
|
2337
|
-
suppressedPublishReason: 'repeated-infra-failure',
|
|
2338
|
-
});
|
|
2339
|
-
}
|
|
2340
|
-
return {
|
|
2341
|
-
status: 'processed',
|
|
2342
|
-
runId,
|
|
2343
|
-
sentinel,
|
|
2344
|
-
nextStage: null,
|
|
2345
|
-
};
|
|
2346
|
-
}
|
|
2347
|
-
}
|
|
2348
|
-
finally {
|
|
2349
|
-
await lock.release();
|
|
2350
|
-
}
|
|
2351
|
-
}
|
|
2352
|
-
return {
|
|
2353
|
-
runIntakeTick,
|
|
2354
|
-
runRunnerTick,
|
|
2355
|
-
async runTick() {
|
|
2356
|
-
await deps.stateStore.assertStateHealthy();
|
|
2357
|
-
const intakeResult = await runIntakeTick();
|
|
2358
|
-
if (intakeResult.status === 'locked') {
|
|
2359
|
-
return intakeResult;
|
|
2360
|
-
}
|
|
2361
|
-
const runnerResult = await runRunnerTick();
|
|
2362
|
-
return runnerResult;
|
|
2363
|
-
},
|
|
2364
|
-
};
|
|
2365
|
-
}
|