lastlight 0.1.15 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +265 -118
- package/agent-context/rules.md +62 -9
- package/agent-context/security.md +69 -0
- package/config/default.yaml +89 -0
- package/deploy/.env.production.example +20 -2
- package/deploy/entrypoint.sh +16 -17
- package/deploy/native/README.md +160 -0
- package/deploy/native/install.sh +110 -0
- package/deploy/native/lastlight.env.example +95 -0
- package/deploy/native/lastlight.service +50 -0
- package/deploy/sandbox-entrypoint.sh +59 -42
- package/dist/admin/auth.d.ts +1 -1
- package/dist/admin/auth.js +3 -1
- package/dist/admin/auth.js.map +1 -1
- package/dist/admin/chat-session-reader.d.ts +11 -14
- package/dist/admin/chat-session-reader.js +27 -51
- package/dist/admin/chat-session-reader.js.map +1 -1
- package/dist/admin/docker.d.ts +37 -0
- package/dist/admin/docker.js +86 -1
- package/dist/admin/docker.js.map +1 -1
- package/dist/admin/index.js +38 -3
- package/dist/admin/index.js.map +1 -1
- package/dist/admin/log-search.test.d.ts +1 -0
- package/dist/admin/log-search.test.js +78 -0
- package/dist/admin/log-search.test.js.map +1 -0
- package/dist/admin/routes.d.ts +52 -3
- package/dist/admin/routes.js +780 -63
- package/dist/admin/routes.js.map +1 -1
- package/dist/admin/routes.test.js +680 -14
- package/dist/admin/routes.test.js.map +1 -1
- package/dist/admin/server-logs.test.d.ts +1 -0
- package/dist/admin/server-logs.test.js +58 -0
- package/dist/admin/server-logs.test.js.map +1 -0
- package/dist/admin/sessions.d.ts +26 -39
- package/dist/admin/sessions.js +86 -310
- package/dist/admin/sessions.js.map +1 -1
- package/dist/admin/sessions.test.d.ts +1 -0
- package/dist/admin/sessions.test.js +78 -0
- package/dist/admin/sessions.test.js.map +1 -0
- package/dist/admin/version.d.ts +15 -0
- package/dist/admin/version.js +73 -0
- package/dist/admin/version.js.map +1 -0
- package/dist/admin/version.test.d.ts +1 -0
- package/dist/admin/version.test.js +71 -0
- package/dist/admin/version.test.js.map +1 -0
- package/dist/cli-config.d.ts +55 -0
- package/dist/cli-config.js +119 -0
- package/dist/cli-config.js.map +1 -0
- package/dist/cli-config.test.d.ts +1 -0
- package/dist/cli-config.test.js +92 -0
- package/dist/cli-config.test.js.map +1 -0
- package/dist/cli-format.d.ts +19 -0
- package/dist/cli-format.js +107 -0
- package/dist/cli-format.js.map +1 -0
- package/dist/cli-server.d.ts +73 -0
- package/dist/cli-server.js +347 -0
- package/dist/cli-server.js.map +1 -0
- package/dist/cli-server.test.d.ts +1 -0
- package/dist/cli-server.test.js +38 -0
- package/dist/cli-server.test.js.map +1 -0
- package/dist/cli-timeline.d.ts +35 -0
- package/dist/cli-timeline.js +373 -0
- package/dist/cli-timeline.js.map +1 -0
- package/dist/cli-timeline.test.d.ts +1 -0
- package/dist/cli-timeline.test.js +104 -0
- package/dist/cli-timeline.test.js.map +1 -0
- package/dist/cli.d.ts +0 -19
- package/dist/cli.js +910 -194
- package/dist/cli.js.map +1 -1
- package/dist/config-overlay.test.d.ts +1 -0
- package/dist/config-overlay.test.js +112 -0
- package/dist/config-overlay.test.js.map +1 -0
- package/dist/config-resolve.d.ts +28 -0
- package/dist/config-resolve.js +47 -0
- package/dist/config-resolve.js.map +1 -0
- package/dist/config-resolve.test.d.ts +1 -0
- package/dist/config-resolve.test.js +68 -0
- package/dist/config-resolve.test.js.map +1 -0
- package/dist/config.d.ts +62 -49
- package/dist/config.js +452 -76
- package/dist/config.js.map +1 -1
- package/dist/config.test.js +201 -32
- package/dist/config.test.js.map +1 -1
- package/dist/connectors/github-webhook.js +83 -5
- package/dist/connectors/github-webhook.js.map +1 -1
- package/dist/connectors/github-webhook.test.d.ts +1 -0
- package/dist/connectors/github-webhook.test.js +156 -0
- package/dist/connectors/github-webhook.test.js.map +1 -0
- package/dist/connectors/messaging/session-manager.d.ts +18 -0
- package/dist/connectors/messaging/session-manager.js +83 -2
- package/dist/connectors/messaging/session-manager.js.map +1 -1
- package/dist/connectors/messaging/session-manager.test.d.ts +1 -0
- package/dist/connectors/messaging/session-manager.test.js +144 -0
- package/dist/connectors/messaging/session-manager.test.js.map +1 -0
- package/dist/connectors/slack/connector.d.ts +9 -0
- package/dist/connectors/slack/connector.js +15 -0
- package/dist/connectors/slack/connector.js.map +1 -1
- package/dist/connectors/slack/mrkdwn.js +81 -0
- package/dist/connectors/slack/mrkdwn.js.map +1 -1
- package/dist/connectors/slack/mrkdwn.test.js +49 -0
- package/dist/connectors/slack/mrkdwn.test.js.map +1 -1
- package/dist/connectors/types.d.ts +1 -1
- package/dist/cron/fanout.d.ts +33 -0
- package/dist/cron/fanout.js +55 -0
- package/dist/cron/fanout.js.map +1 -0
- package/dist/cron/fanout.test.d.ts +1 -0
- package/dist/cron/fanout.test.js +73 -0
- package/dist/cron/fanout.test.js.map +1 -0
- package/dist/cron/jobs.d.ts +5 -0
- package/dist/cron/jobs.js +10 -3
- package/dist/cron/jobs.js.map +1 -1
- package/dist/cron/scheduler.d.ts +12 -0
- package/dist/cron/scheduler.js +27 -1
- package/dist/cron/scheduler.js.map +1 -1
- package/dist/engine/agent-executor.d.ts +158 -0
- package/dist/engine/agent-executor.js +1140 -0
- package/dist/engine/agent-executor.js.map +1 -0
- package/dist/engine/agent-executor.test.d.ts +1 -0
- package/dist/engine/agent-executor.test.js +395 -0
- package/dist/engine/agent-executor.test.js.map +1 -0
- package/dist/engine/chat-runner.d.ts +123 -0
- package/dist/engine/chat-runner.js +379 -0
- package/dist/engine/chat-runner.js.map +1 -0
- package/dist/engine/chat-runner.test.d.ts +1 -0
- package/dist/engine/chat-runner.test.js +104 -0
- package/dist/engine/chat-runner.test.js.map +1 -0
- package/dist/engine/chat-skills.d.ts +45 -0
- package/dist/engine/chat-skills.js +184 -0
- package/dist/engine/chat-skills.js.map +1 -0
- package/dist/engine/chat-skills.test.d.ts +1 -0
- package/dist/engine/chat-skills.test.js +20 -0
- package/dist/engine/chat-skills.test.js.map +1 -0
- package/dist/engine/chat.d.ts +20 -23
- package/dist/engine/chat.js +251 -155
- package/dist/engine/chat.js.map +1 -1
- package/dist/engine/chat.test.d.ts +1 -0
- package/dist/engine/chat.test.js +42 -0
- package/dist/engine/chat.test.js.map +1 -0
- package/dist/engine/classifier.d.ts +35 -2
- package/dist/engine/classifier.js +170 -31
- package/dist/engine/classifier.js.map +1 -1
- package/dist/engine/classifier.test.d.ts +1 -0
- package/dist/engine/classifier.test.js +115 -0
- package/dist/engine/classifier.test.js.map +1 -0
- package/dist/engine/dispatcher.d.ts +60 -0
- package/dist/engine/dispatcher.js +601 -0
- package/dist/engine/dispatcher.js.map +1 -0
- package/dist/engine/dispatcher.test.d.ts +1 -0
- package/dist/engine/dispatcher.test.js +511 -0
- package/dist/engine/dispatcher.test.js.map +1 -0
- package/dist/engine/event-shim.d.ts +117 -0
- package/dist/engine/event-shim.js +435 -0
- package/dist/engine/event-shim.js.map +1 -0
- package/dist/engine/event-shim.test.d.ts +1 -0
- package/dist/engine/event-shim.test.js +194 -0
- package/dist/engine/event-shim.test.js.map +1 -0
- package/dist/engine/git-auth.d.ts +13 -17
- package/dist/engine/git-auth.js +99 -42
- package/dist/engine/git-auth.js.map +1 -1
- package/dist/engine/git-auth.test.d.ts +1 -0
- package/dist/engine/git-auth.test.js +117 -0
- package/dist/engine/git-auth.test.js.map +1 -0
- package/dist/engine/github-app-client.d.ts +7 -0
- package/dist/engine/github-app-client.js +16 -0
- package/dist/engine/github-app-client.js.map +1 -0
- package/dist/engine/github-app-client.test.d.ts +1 -0
- package/dist/engine/github-app-client.test.js +44 -0
- package/dist/engine/github-app-client.test.js.map +1 -0
- package/dist/engine/github-tools.d.ts +12 -0
- package/dist/engine/github-tools.js +261 -0
- package/dist/engine/github-tools.js.map +1 -0
- package/dist/engine/github.d.ts +1027 -43
- package/dist/engine/github.js +153 -14
- package/dist/engine/github.js.map +1 -1
- package/dist/engine/llm.d.ts +47 -0
- package/dist/engine/llm.js +221 -0
- package/dist/engine/llm.js.map +1 -0
- package/dist/engine/llm.test.d.ts +1 -0
- package/dist/engine/llm.test.js +189 -0
- package/dist/engine/llm.test.js.map +1 -0
- package/dist/engine/profiles.d.ts +249 -0
- package/dist/engine/profiles.js +42 -0
- package/dist/engine/profiles.js.map +1 -0
- package/dist/engine/router.d.ts +12 -6
- package/dist/engine/router.js +339 -81
- package/dist/engine/router.js.map +1 -1
- package/dist/engine/router.test.js +428 -78
- package/dist/engine/router.test.js.map +1 -1
- package/dist/engine/screen.d.ts +41 -0
- package/dist/engine/screen.js +93 -0
- package/dist/engine/screen.js.map +1 -0
- package/dist/engine/screen.test.d.ts +1 -0
- package/dist/engine/screen.test.js +82 -0
- package/dist/engine/screen.test.js.map +1 -0
- package/dist/index.js +377 -432
- package/dist/index.js.map +1 -1
- package/dist/managed-repos.d.ts +7 -9
- package/dist/managed-repos.js +12 -15
- package/dist/managed-repos.js.map +1 -1
- package/dist/managed-repos.test.js +24 -19
- package/dist/managed-repos.test.js.map +1 -1
- package/dist/notify/index.d.ts +15 -0
- package/dist/notify/index.js +6 -0
- package/dist/notify/index.js.map +1 -0
- package/dist/notify/model.d.ts +57 -0
- package/dist/notify/model.js +89 -0
- package/dist/notify/model.js.map +1 -0
- package/dist/notify/model.test.d.ts +1 -0
- package/dist/notify/model.test.js +109 -0
- package/dist/notify/model.test.js.map +1 -0
- package/dist/notify/notifier.d.ts +17 -0
- package/dist/notify/notifier.js +87 -0
- package/dist/notify/notifier.js.map +1 -0
- package/dist/notify/notifier.test.d.ts +1 -0
- package/dist/notify/notifier.test.js +100 -0
- package/dist/notify/notifier.test.js.map +1 -0
- package/dist/notify/render.d.ts +21 -0
- package/dist/notify/render.js +59 -0
- package/dist/notify/render.js.map +1 -0
- package/dist/notify/render.test.d.ts +1 -0
- package/dist/notify/render.test.js +65 -0
- package/dist/notify/render.test.js.map +1 -0
- package/dist/notify/transports/github.d.ts +26 -0
- package/dist/notify/transports/github.js +26 -0
- package/dist/notify/transports/github.js.map +1 -0
- package/dist/notify/transports/slack.d.ts +26 -0
- package/dist/notify/transports/slack.js +28 -0
- package/dist/notify/transports/slack.js.map +1 -0
- package/dist/notify/transports.test.d.ts +1 -0
- package/dist/notify/transports.test.js +71 -0
- package/dist/notify/transports.test.js.map +1 -0
- package/dist/notify/types.d.ts +95 -0
- package/dist/notify/types.js +16 -0
- package/dist/notify/types.js.map +1 -0
- package/dist/sandbox/docker-compose.test.d.ts +1 -0
- package/dist/sandbox/docker-compose.test.js +128 -0
- package/dist/sandbox/docker-compose.test.js.map +1 -0
- package/dist/sandbox/docker.d.ts +97 -9
- package/dist/sandbox/docker.js +192 -41
- package/dist/sandbox/docker.js.map +1 -1
- package/dist/sandbox/docker.test.d.ts +1 -0
- package/dist/sandbox/docker.test.js +137 -0
- package/dist/sandbox/docker.test.js.map +1 -0
- package/dist/sandbox/egress-allowlist.d.ts +69 -0
- package/dist/sandbox/egress-allowlist.js +164 -0
- package/dist/sandbox/egress-allowlist.js.map +1 -0
- package/dist/sandbox/egress-allowlist.test.d.ts +1 -0
- package/dist/sandbox/egress-allowlist.test.js +65 -0
- package/dist/sandbox/egress-allowlist.test.js.map +1 -0
- package/dist/sandbox/egress-firewall-config.d.ts +127 -0
- package/dist/sandbox/egress-firewall-config.js +434 -0
- package/dist/sandbox/egress-firewall-config.js.map +1 -0
- package/dist/sandbox/egress-firewall-config.test.d.ts +1 -0
- package/dist/sandbox/egress-firewall-config.test.js +244 -0
- package/dist/sandbox/egress-firewall-config.test.js.map +1 -0
- package/dist/sandbox/images.d.ts +23 -0
- package/dist/sandbox/images.js +55 -0
- package/dist/sandbox/images.js.map +1 -0
- package/dist/sandbox/index.d.ts +82 -3
- package/dist/sandbox/index.js +223 -93
- package/dist/sandbox/index.js.map +1 -1
- package/dist/sandbox/index.test.d.ts +1 -0
- package/dist/sandbox/index.test.js +157 -0
- package/dist/sandbox/index.test.js.map +1 -0
- package/dist/session-log.d.ts +63 -0
- package/dist/session-log.js +290 -0
- package/dist/session-log.js.map +1 -0
- package/dist/session-log.test.d.ts +1 -0
- package/dist/session-log.test.js +85 -0
- package/dist/session-log.test.js.map +1 -0
- package/dist/setup.d.ts +30 -1
- package/dist/setup.js +241 -25
- package/dist/setup.js.map +1 -1
- package/dist/setup.test.js +138 -6
- package/dist/setup.test.js.map +1 -1
- package/dist/state/approval-store.d.ts +86 -0
- package/dist/state/approval-store.js +140 -0
- package/dist/state/approval-store.js.map +1 -0
- package/dist/state/build-assets.d.ts +50 -0
- package/dist/state/build-assets.js +154 -0
- package/dist/state/build-assets.js.map +1 -0
- package/dist/state/build-assets.test.d.ts +1 -0
- package/dist/state/build-assets.test.js +106 -0
- package/dist/state/build-assets.test.js.map +1 -0
- package/dist/state/db.d.ts +65 -333
- package/dist/state/db.js +112 -835
- package/dist/state/db.js.map +1 -1
- package/dist/state/db.test.js +127 -91
- package/dist/state/db.test.js.map +1 -1
- package/dist/state/execution-store.d.ts +257 -0
- package/dist/state/execution-store.js +527 -0
- package/dist/state/execution-store.js.map +1 -0
- package/dist/state/migrate.d.ts +15 -0
- package/dist/state/migrate.js +175 -0
- package/dist/state/migrate.js.map +1 -0
- package/dist/state/workflow-run-store.d.ts +195 -0
- package/dist/state/workflow-run-store.js +363 -0
- package/dist/state/workflow-run-store.js.map +1 -0
- package/dist/state/workflow-run-store.test.d.ts +1 -0
- package/dist/state/workflow-run-store.test.js +264 -0
- package/dist/state/workflow-run-store.test.js.map +1 -0
- package/dist/telemetry/index.d.ts +38 -0
- package/dist/telemetry/index.js +253 -0
- package/dist/telemetry/index.js.map +1 -0
- package/dist/telemetry/index.test.d.ts +1 -0
- package/dist/telemetry/index.test.js +73 -0
- package/dist/telemetry/index.test.js.map +1 -0
- package/dist/telemetry/pi-events.d.ts +12 -0
- package/dist/telemetry/pi-events.js +134 -0
- package/dist/telemetry/pi-events.js.map +1 -0
- package/dist/telemetry/pi-events.test.d.ts +1 -0
- package/dist/telemetry/pi-events.test.js +69 -0
- package/dist/telemetry/pi-events.test.js.map +1 -0
- package/dist/workflows/dag.d.ts +13 -1
- package/dist/workflows/dag.js +7 -3
- package/dist/workflows/dag.js.map +1 -1
- package/dist/workflows/dag.test.js +22 -0
- package/dist/workflows/dag.test.js.map +1 -1
- package/dist/workflows/golden-build.test.d.ts +1 -0
- package/dist/workflows/golden-build.test.js +45 -0
- package/dist/workflows/golden-build.test.js.map +1 -0
- package/dist/workflows/loader-overlay.test.d.ts +1 -0
- package/dist/workflows/loader-overlay.test.js +101 -0
- package/dist/workflows/loader-overlay.test.js.map +1 -0
- package/dist/workflows/loader.d.ts +36 -11
- package/dist/workflows/loader.js +311 -87
- package/dist/workflows/loader.js.map +1 -1
- package/dist/workflows/loader.test.js +114 -10
- package/dist/workflows/loader.test.js.map +1 -1
- package/dist/workflows/loop-eval.js +2 -0
- package/dist/workflows/loop-eval.js.map +1 -1
- package/dist/workflows/loop-eval.test.js +14 -0
- package/dist/workflows/loop-eval.test.js.map +1 -1
- package/dist/workflows/phase-executor.d.ts +145 -0
- package/dist/workflows/phase-executor.js +691 -0
- package/dist/workflows/phase-executor.js.map +1 -0
- package/dist/workflows/phase-executor.test.d.ts +1 -0
- package/dist/workflows/phase-executor.test.js +434 -0
- package/dist/workflows/phase-executor.test.js.map +1 -0
- package/dist/workflows/phase-ref.d.ts +44 -0
- package/dist/workflows/phase-ref.js +78 -0
- package/dist/workflows/phase-ref.js.map +1 -0
- package/dist/workflows/phase-ref.test.d.ts +1 -0
- package/dist/workflows/phase-ref.test.js +24 -0
- package/dist/workflows/phase-ref.test.js.map +1 -0
- package/dist/workflows/resume.d.ts +5 -11
- package/dist/workflows/resume.js +88 -7
- package/dist/workflows/resume.js.map +1 -1
- package/dist/workflows/runner.d.ts +34 -27
- package/dist/workflows/runner.js +252 -930
- package/dist/workflows/runner.js.map +1 -1
- package/dist/workflows/runner.test.js +346 -35
- package/dist/workflows/runner.test.js.map +1 -1
- package/dist/workflows/schema.d.ts +52 -10
- package/dist/workflows/schema.js +147 -7
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/simple.d.ts +67 -3
- package/dist/workflows/simple.js +248 -87
- package/dist/workflows/simple.js.map +1 -1
- package/dist/workflows/simple.test.d.ts +1 -0
- package/dist/workflows/simple.test.js +107 -0
- package/dist/workflows/simple.test.js.map +1 -0
- package/dist/workflows/templates.d.ts +29 -0
- package/dist/workflows/templates.js +42 -4
- package/dist/workflows/templates.js.map +1 -1
- package/dist/workflows/templates.test.js +103 -0
- package/dist/workflows/templates.test.js.map +1 -1
- package/dist/workflows/triggers.d.ts +21 -0
- package/dist/workflows/triggers.js +44 -0
- package/dist/workflows/triggers.js.map +1 -0
- package/dist/workflows/verdict.d.ts +19 -0
- package/dist/workflows/verdict.js +30 -0
- package/dist/workflows/verdict.js.map +1 -0
- package/dist/workflows/verdict.test.d.ts +1 -0
- package/dist/workflows/verdict.test.js +44 -0
- package/dist/workflows/verdict.test.js.map +1 -0
- package/dist/worktree/manager.d.ts +1 -1
- package/dist/worktree/manager.js +14 -14
- package/dist/worktree/manager.js.map +1 -1
- package/dist/worktree/manager.test.d.ts +1 -0
- package/dist/worktree/manager.test.js +87 -0
- package/dist/worktree/manager.test.js.map +1 -0
- package/docker-compose.yml +206 -5
- package/package.json +19 -9
- package/sandbox.Dockerfile +97 -18
- package/skills/README.md +40 -0
- package/skills/browser-qa/SKILL.md +193 -0
- package/skills/browser-qa/scripts/agent-browser.mjs +380 -0
- package/skills/building/SKILL.md +95 -0
- package/skills/chat/SKILL.md +30 -11
- package/skills/code-review/SKILL.md +59 -0
- package/skills/debug-production/SKILL.md +108 -0
- package/skills/demo/SKILL.md +194 -0
- package/skills/demo/scripts/compose-demo.sh +205 -0
- package/skills/issue-answer/SKILL.md +90 -0
- package/skills/issue-comment/SKILL.md +38 -24
- package/skills/issue-triage/SKILL.md +103 -37
- package/skills/issue-triage/references/AGENT-BRIEF.md +61 -0
- package/skills/pr-comment/SKILL.md +64 -0
- package/skills/pr-review/SKILL.md +65 -49
- package/skills/qa-test/SKILL.md +97 -0
- package/skills/repo-health/SKILL.md +48 -45
- package/skills/security-feedback/SKILL.md +173 -0
- package/skills/security-feedback/references/templates.md +77 -0
- package/skills/security-review/SKILL.md +213 -0
- package/skills/security-review/references/issue-format.md +302 -0
- package/skills/verify/SKILL.md +118 -0
- package/workflows/answer.yaml +37 -0
- package/workflows/build.yaml +32 -22
- package/workflows/cron-security.yaml +6 -0
- package/workflows/demo.yaml +55 -0
- package/workflows/explore.yaml +9 -0
- package/workflows/pr-comment.yaml +16 -0
- package/workflows/pr-fix.yaml +3 -0
- package/workflows/pr-review.yaml +4 -1
- package/workflows/prompts/answer.md +67 -0
- package/workflows/prompts/architect.md +24 -7
- package/workflows/prompts/demo.md +100 -0
- package/workflows/prompts/executor.md +29 -20
- package/workflows/prompts/explore-ask.md +51 -22
- package/workflows/prompts/explore-publish.md +2 -2
- package/workflows/prompts/explore-read.md +24 -10
- package/workflows/prompts/explore-synthesize.md +15 -8
- package/workflows/prompts/fix.md +9 -10
- package/workflows/prompts/guardrails.md +26 -8
- package/workflows/prompts/pr-fix.md +6 -7
- package/workflows/prompts/pr.md +11 -10
- package/workflows/prompts/qa-browser.md +95 -0
- package/workflows/prompts/qa-synth.md +44 -0
- package/workflows/prompts/qa-test.md +60 -0
- package/workflows/prompts/re-reviewer.md +8 -9
- package/workflows/prompts/reviewer.md +10 -9
- package/workflows/prompts/verify-browser.md +93 -0
- package/workflows/prompts/verify-synth.md +44 -0
- package/workflows/prompts/verify.md +63 -0
- package/workflows/qa-test.yaml +87 -0
- package/workflows/security-feedback.yaml +25 -0
- package/workflows/security-review.yaml +29 -0
- package/workflows/verify.yaml +85 -0
- package/deploy/mcp-config.tmpl.json +0 -14
- package/dist/cron/rate-limits.d.ts +0 -26
- package/dist/cron/rate-limits.js +0 -193
- package/dist/cron/rate-limits.js.map +0 -1
- package/dist/engine/executor.d.ts +0 -75
- package/dist/engine/executor.js +0 -292
- package/dist/engine/executor.js.map +0 -1
- package/mcp-github-app/package.json +0 -18
- package/mcp-github-app/src/auth.js +0 -66
- package/mcp-github-app/src/github.js +0 -371
- package/mcp-github-app/src/index.js +0 -534
- package/skills/devops/webhook-subscriptions/SKILL.md +0 -180
- package/skills/github/DESCRIPTION.md +0 -3
- package/skills/github/codebase-inspection/SKILL.md +0 -115
- package/skills/github/github-auth/SKILL.md +0 -54
- package/skills/github/github-auth/scripts/gh-env.sh +0 -66
- package/skills/github/github-code-review/SKILL.md +0 -480
- package/skills/github/github-code-review/references/review-output-template.md +0 -74
- package/skills/github/github-issues/SKILL.md +0 -369
- package/skills/github/github-issues/templates/bug-report.md +0 -35
- package/skills/github/github-issues/templates/feature-request.md +0 -31
- package/skills/github/github-pr-workflow/SKILL.md +0 -395
- package/skills/github/github-pr-workflow/references/ci-troubleshooting.md +0 -183
- package/skills/github/github-pr-workflow/references/conventional-commits.md +0 -71
- package/skills/github/github-pr-workflow/templates/pr-body-bugfix.md +0 -35
- package/skills/github/github-pr-workflow/templates/pr-body-feature.md +0 -33
- package/skills/github/github-repo-management/SKILL.md +0 -515
- package/skills/github/github-repo-management/references/github-api-cheatsheet.md +0 -161
- package/skills/github-orchestrator/SKILL.md +0 -103
- package/skills/mcp/DESCRIPTION.md +0 -3
- package/skills/mcp/mcporter/SKILL.md +0 -122
- package/skills/mcp/native-mcp/SKILL.md +0 -356
- package/skills/software-development/architect/SKILL.md +0 -154
- package/skills/software-development/assure-guardrails/SKILL.md +0 -239
- package/skills/software-development/plan/SKILL.md +0 -64
- package/skills/software-development/requesting-code-review/SKILL.md +0 -291
- package/skills/software-development/subagent-driven-development/SKILL.md +0 -433
- package/skills/software-development/systematic-debugging/SKILL.md +0 -366
- package/skills/software-development/test-driven-development/SKILL.md +0 -342
package/dist/workflows/runner.js
CHANGED
|
@@ -1,87 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { execSync } from "child_process";
|
|
3
|
-
import { readFileSync } from "fs";
|
|
4
|
-
import { join, resolve } from "path";
|
|
5
|
-
import { executeAgent } from "../engine/executor.js";
|
|
6
|
-
import { resolveModel } from "../config.js";
|
|
7
|
-
import { listRunningContainers } from "../admin/docker.js";
|
|
1
|
+
import { resolveModel, resolveVariant } from "../config.js";
|
|
8
2
|
import { loadPromptTemplate } from "./loader.js";
|
|
9
3
|
import { renderTemplate } from "./templates.js";
|
|
10
|
-
import { evalUntilExpression } from "./loop-eval.js";
|
|
11
4
|
import { buildDag, getReadyNodes, getNodesToSkip, isComplete } from "./dag.js";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
function
|
|
18
|
-
for (const base of [resolve("skills"), resolve(".claude/skills")]) {
|
|
19
|
-
try {
|
|
20
|
-
return readFileSync(join(base, skillName, "SKILL.md"), "utf-8");
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
/* try next path */
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
throw new Error(`Skill not found: skills/${skillName}/SKILL.md`);
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Build the agent prompt for a phase, handling both `prompt:` (template file)
|
|
30
|
-
* and `skill:` (SKILL.md reference) phase definitions.
|
|
31
|
-
*
|
|
32
|
-
* Skill phases produce the same prompt shape the legacy executeSkill used:
|
|
33
|
-
* "Follow these skill instructions:\n\n<SKILL.md>\n\nContext:\n<key: value lines>"
|
|
34
|
-
* Template variables ({{owner}}, {{issueNumber}}, etc.) are still rendered in
|
|
35
|
-
* the SKILL.md content so skills can reference workflow context if they want.
|
|
36
|
-
*/
|
|
37
|
-
function buildPhasePrompt(phase, ctx, extraCtx) {
|
|
38
|
-
const fullCtx = extraCtx ? { ...ctx, ...extraCtx } : ctx;
|
|
39
|
-
if (phase.skill) {
|
|
40
|
-
const skillContent = loadSkillInstructions(phase.skill);
|
|
41
|
-
const renderedSkill = renderTemplate(skillContent, fullCtx);
|
|
42
|
-
// Build a context block from the workflow context — same shape that the
|
|
43
|
-
// legacy executeSkill produced, so existing skill instructions still work.
|
|
44
|
-
const contextLines = Object.entries(fullCtx)
|
|
45
|
-
.filter(([, v]) => v !== undefined && v !== null)
|
|
46
|
-
.map(([k, v]) => `${k}: ${typeof v === "object" ? JSON.stringify(v) : v}`)
|
|
47
|
-
.join("\n");
|
|
48
|
-
return `Follow these skill instructions:\n\n${renderedSkill}\n\nContext:\n${contextLines}`;
|
|
49
|
-
}
|
|
50
|
-
if (phase.prompt) {
|
|
51
|
-
const template = loadPromptTemplate(phase.prompt);
|
|
52
|
-
return renderTemplate(template, fullCtx);
|
|
53
|
-
}
|
|
54
|
-
throw new Error(`Phase "${phase.name}" has neither prompt: nor skill: — cannot build prompt`);
|
|
55
|
-
}
|
|
56
|
-
// ── Phase-level deduplication ────────────────────────────────────────────────
|
|
57
|
-
/**
|
|
58
|
-
* Check if a sandbox container is actually running for a given taskId prefix.
|
|
59
|
-
*/
|
|
60
|
-
async function isContainerAlive(taskId) {
|
|
61
|
-
try {
|
|
62
|
-
const containers = await listRunningContainers();
|
|
63
|
-
return containers.some((c) => c.taskId === taskId);
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Check if an error was caused by manual termination.
|
|
71
|
-
*/
|
|
72
|
-
export function isTerminated(error) {
|
|
73
|
-
if (!error)
|
|
74
|
-
return false;
|
|
75
|
-
const lower = error.toLowerCase();
|
|
76
|
-
return (lower.includes("terminated") ||
|
|
77
|
-
lower.includes("killed") ||
|
|
78
|
-
lower.includes("exit undefined") ||
|
|
79
|
-
(lower.includes("container") && lower.includes("not running")));
|
|
80
|
-
}
|
|
81
|
-
function pickResult(r) {
|
|
82
|
-
return { success: r.success, output: r.output, error: r.error };
|
|
83
|
-
}
|
|
84
|
-
function gitAccessProfileForWorkflow(workflowName) {
|
|
5
|
+
import { qaImageAvailable, SANDBOX_IMAGE_QA } from "../sandbox/images.js";
|
|
6
|
+
import { PhaseExecutor, isTerminated, } from "./phase-executor.js";
|
|
7
|
+
import { collapseDetail } from "../notify/render.js";
|
|
8
|
+
// `isTerminated` used to live here; re-exported for API stability.
|
|
9
|
+
export { isTerminated };
|
|
10
|
+
export function gitAccessProfileForWorkflow(workflowName) {
|
|
85
11
|
switch (workflowName) {
|
|
86
12
|
case "build":
|
|
87
13
|
case "pr-fix":
|
|
@@ -90,159 +16,58 @@ function gitAccessProfileForWorkflow(workflowName) {
|
|
|
90
16
|
return "review-write";
|
|
91
17
|
case "issue-triage":
|
|
92
18
|
case "issue-comment":
|
|
19
|
+
case "pr-comment":
|
|
93
20
|
case "explore":
|
|
21
|
+
case "answer":
|
|
22
|
+
case "security-review":
|
|
23
|
+
// verify / qa-test / demo read the repo and post a findings/demo comment —
|
|
24
|
+
// they never push code, so issues-write (contents:read + issues:write) is
|
|
25
|
+
// enough.
|
|
26
|
+
case "verify":
|
|
27
|
+
case "qa-test":
|
|
28
|
+
case "demo":
|
|
94
29
|
return "issues-write";
|
|
30
|
+
case "security-feedback":
|
|
31
|
+
return "repo-write";
|
|
95
32
|
default:
|
|
96
33
|
return "read";
|
|
97
34
|
}
|
|
98
35
|
}
|
|
99
|
-
function gitSandboxAccessForWorkflow(workflowName, owner, repo) {
|
|
36
|
+
export function gitSandboxAccessForWorkflow(workflowName, owner, repo, prePopulateBranch, runId) {
|
|
100
37
|
const profile = gitAccessProfileForWorkflow(workflowName);
|
|
101
38
|
return {
|
|
102
39
|
owner,
|
|
103
40
|
repo,
|
|
104
41
|
profile,
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (status === "running") {
|
|
117
|
-
const alive = await isContainerAlive(taskId);
|
|
118
|
-
if (alive) {
|
|
119
|
-
console.log(`[runner] Phase ${phaseName} is already running (container alive) — skipping`);
|
|
120
|
-
return { skipped: true, reason: "running" };
|
|
121
|
-
}
|
|
122
|
-
console.log(`[runner] Phase ${phaseName} was running but container is dead — cleaning up`);
|
|
123
|
-
db.markStaleAsFailed(dedupKey, triggerId, workflowRunId);
|
|
124
|
-
}
|
|
125
|
-
else if (status === "done") {
|
|
126
|
-
console.log(`[runner] Phase ${phaseName} already completed successfully — skipping`);
|
|
127
|
-
return { skipped: true, reason: "done" };
|
|
128
|
-
}
|
|
129
|
-
const executionId = randomUUID();
|
|
130
|
-
db.recordStart({
|
|
131
|
-
id: executionId,
|
|
132
|
-
triggerType: "webhook",
|
|
133
|
-
triggerId,
|
|
134
|
-
skill: dedupKey,
|
|
135
|
-
repo: undefined,
|
|
136
|
-
issueNumber: undefined,
|
|
137
|
-
startedAt: new Date().toISOString(),
|
|
138
|
-
workflowRunId,
|
|
139
|
-
});
|
|
140
|
-
const phaseConfig = modelOverride ? { ...config, model: modelOverride } : config;
|
|
141
|
-
const result = await executeAgent(prompt, phaseConfig, {
|
|
142
|
-
taskId,
|
|
143
|
-
githubAccess,
|
|
144
|
-
// Persist the session id as soon as it arrives so the dashboard can
|
|
145
|
-
// show live agent logs for an in-flight phase, not just completed ones.
|
|
146
|
-
onSessionId: (sessionId) => {
|
|
147
|
-
try {
|
|
148
|
-
db.recordSessionId(executionId, sessionId);
|
|
149
|
-
}
|
|
150
|
-
catch (err) {
|
|
151
|
-
console.warn(`[runner] Failed to persist session id mid-run for ${phaseName}:`, err);
|
|
152
|
-
}
|
|
153
|
-
},
|
|
154
|
-
});
|
|
155
|
-
db.recordFinish(executionId, {
|
|
156
|
-
success: result.success,
|
|
157
|
-
error: result.error,
|
|
158
|
-
turns: result.turns,
|
|
159
|
-
durationMs: result.durationMs,
|
|
160
|
-
sessionId: result.sessionId,
|
|
161
|
-
costUsd: result.costUsd,
|
|
162
|
-
inputTokens: result.inputTokens,
|
|
163
|
-
cacheCreationInputTokens: result.cacheCreationInputTokens,
|
|
164
|
-
cacheReadInputTokens: result.cacheReadInputTokens,
|
|
165
|
-
outputTokens: result.outputTokens,
|
|
166
|
-
apiDurationMs: result.apiDurationMs,
|
|
167
|
-
stopReason: result.stopReason,
|
|
168
|
-
});
|
|
169
|
-
return { result, skipped: false };
|
|
170
|
-
}
|
|
171
|
-
const phaseConfig = modelOverride ? { ...config, model: modelOverride } : config;
|
|
172
|
-
const result = await executeAgent(prompt, phaseConfig, { taskId, githubAccess });
|
|
173
|
-
return { result, skipped: false };
|
|
174
|
-
}
|
|
175
|
-
// ── Resume logic ─────────────────────────────────────────────────────────────
|
|
176
|
-
/**
|
|
177
|
-
* Resolve a recorded phase name to its index in `definition.phases`.
|
|
178
|
-
* Handles the generated iteration labels the runner writes for looping phases:
|
|
179
|
-
*
|
|
180
|
-
* `${phase}_iter_${n}` — generic_loop iteration
|
|
181
|
-
* `${phase}_fix_${n}` — reviewer-style loop fix cycle
|
|
182
|
-
* `${phase}_${n}` — reviewer-style loop re-review cycle
|
|
183
|
-
*
|
|
184
|
-
* Returns -1 when the name doesn't match any phase (unknown/untracked labels
|
|
185
|
-
* like `waiting_approval` or user set_phase values such as `complete`).
|
|
186
|
-
*/
|
|
187
|
-
export function phaseIndexInDefinition(definition, name) {
|
|
188
|
-
const exact = definition.phases.findIndex((p) => p.name === name);
|
|
189
|
-
if (exact >= 0)
|
|
190
|
-
return exact;
|
|
191
|
-
const tryStrip = (re) => {
|
|
192
|
-
const m = name.match(re);
|
|
193
|
-
if (!m)
|
|
194
|
-
return -1;
|
|
195
|
-
return definition.phases.findIndex((p) => p.name === m[1]);
|
|
42
|
+
// Never forward the App PEM into sandboxes. The harness already mints a
|
|
43
|
+
// profile-scoped token and forwards it as GITHUB_TOKEN, so the agent gets
|
|
44
|
+
// github tools in static-token mode without the App private key ever
|
|
45
|
+
// entering the sandbox.
|
|
46
|
+
allowMcpAppAuth: false,
|
|
47
|
+
prePopulateBranch,
|
|
48
|
+
runId,
|
|
49
|
+
// Read-only workflows never need git history — clone at --depth 1. Only
|
|
50
|
+
// the code-pushing profiles (build / pr-fix / security-feedback) keep the
|
|
51
|
+
// deeper clone for rebase/amend headroom.
|
|
52
|
+
shallow: profile !== "repo-write",
|
|
196
53
|
};
|
|
197
|
-
const iterIdx = tryStrip(/^(.*)_iter_\d+$/);
|
|
198
|
-
if (iterIdx >= 0)
|
|
199
|
-
return iterIdx;
|
|
200
|
-
const fixIdx = tryStrip(/^(.*)_fix_\d+$/);
|
|
201
|
-
if (fixIdx >= 0)
|
|
202
|
-
return fixIdx;
|
|
203
|
-
const cycleIdx = tryStrip(/^(.*)_\d+$/);
|
|
204
|
-
if (cycleIdx >= 0)
|
|
205
|
-
return cycleIdx;
|
|
206
|
-
return -1;
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* Given the phase the runner last completed, return the name of the phase the
|
|
210
|
-
* runner should run next. Returns `null` when there is no next phase (i.e. the
|
|
211
|
-
* workflow is done).
|
|
212
|
-
*/
|
|
213
|
-
export function nextPhaseAfter(definition, completedPhase) {
|
|
214
|
-
const idx = phaseIndexInDefinition(definition, completedPhase);
|
|
215
|
-
if (idx < 0 || idx >= definition.phases.length - 1)
|
|
216
|
-
return null;
|
|
217
|
-
return definition.phases[idx + 1].name;
|
|
218
|
-
}
|
|
219
|
-
/**
|
|
220
|
-
* Find the phase that owns an approval gate with the given name — either via
|
|
221
|
-
* `phase.approval_gate` or `phase.loop.approval_gate`. Returns `null` when no
|
|
222
|
-
* phase declares that gate. Gate names should be unique within a workflow.
|
|
223
|
-
*/
|
|
224
|
-
function findGateOwningPhase(definition, gateName) {
|
|
225
|
-
for (const p of definition.phases) {
|
|
226
|
-
if (p.approval_gate === gateName)
|
|
227
|
-
return p.name;
|
|
228
|
-
if (p.loop?.approval_gate === gateName)
|
|
229
|
-
return p.name;
|
|
230
|
-
}
|
|
231
|
-
return null;
|
|
232
|
-
}
|
|
233
|
-
// ── Main workflow runner ─────────────────────────────────────────────────────
|
|
234
|
-
/** Returns true if any phase declares explicit dependencies — triggers DAG execution path. */
|
|
235
|
-
function hasDependencies(definition) {
|
|
236
|
-
return definition.phases.some((p) => p.depends_on && p.depends_on.length > 0);
|
|
237
54
|
}
|
|
55
|
+
// ── Unified workflow scheduler ───────────────────────────────────────────────
|
|
238
56
|
/**
|
|
239
57
|
* Run an agent workflow defined by a YAML definition.
|
|
240
|
-
*
|
|
241
|
-
*
|
|
58
|
+
*
|
|
59
|
+
* Every workflow executes as a DAG: workflows that declare no `depends_on`
|
|
60
|
+
* are run as a synthesized chain (each phase depends on the one before it),
|
|
61
|
+
* reproducing the old linear semantics including the failure cascade. Ready
|
|
62
|
+
* nodes are executed **one at a time in declaration order** (sequential).
|
|
63
|
+
* Each node's body — context / standard agent / reviewer-loop / generic-loop,
|
|
64
|
+
* plus approval and reply gates — lives in {@link PhaseExecutor}; the
|
|
65
|
+
* scheduler here owns the DAG, the `phases[]`/`outputs{}` accumulation, and
|
|
66
|
+
* the in-memory node status.
|
|
242
67
|
*/
|
|
243
|
-
export async function runWorkflow(definition, ctx, config, callbacks, db, models, approvalConfig, workflowId) {
|
|
68
|
+
export async function runWorkflow(definition, ctx, config, callbacks, db, models, approvalConfig, workflowId, variants) {
|
|
244
69
|
const phases = [];
|
|
245
|
-
const
|
|
70
|
+
const outputs = {};
|
|
246
71
|
const { taskId } = ctx;
|
|
247
72
|
// Slack-originated runs carry an explicit `slack:` trigger id — everything
|
|
248
73
|
// else (GitHub webhook, CLI) uses the legacy owner/repo#N shape.
|
|
@@ -252,25 +77,73 @@ export async function runWorkflow(definition, ctx, config, callbacks, db, models
|
|
|
252
77
|
// iteration at the right index and templates can read {{scratch.*}}.
|
|
253
78
|
const scratch = ctx.scratch
|
|
254
79
|
? { ...ctx.scratch }
|
|
255
|
-
: (db && workflowId ? { ...(db.
|
|
80
|
+
: (db && workflowId ? { ...(db.runs.getRun(workflowId)?.scratch ?? {}) } : {});
|
|
256
81
|
ctx.scratch = scratch;
|
|
257
|
-
const
|
|
82
|
+
const prePopulateBranch = typeof ctx.prePopulateBranch === "string"
|
|
83
|
+
? ctx.prePopulateBranch
|
|
84
|
+
: undefined;
|
|
85
|
+
const githubAccess = gitSandboxAccessForWorkflow(definition.name, ctx.owner, ctx.repo, prePopulateBranch, workflowId);
|
|
258
86
|
const notify = callbacks.postComment || (async () => { });
|
|
87
|
+
const reporter = callbacks.reporter;
|
|
259
88
|
const onStart = callbacks.onPhaseStart || (async () => { });
|
|
260
89
|
const onEnd = callbacks.onPhaseEnd || (async () => { });
|
|
90
|
+
// Terminal step key — dynamic loop steps (re-review / fix cycles) are
|
|
91
|
+
// inserted just above it so the checklist reads top-to-bottom in run order.
|
|
92
|
+
const lastPhaseKey = [...definition.phases]
|
|
93
|
+
.reverse()
|
|
94
|
+
.find((p) => (p.type ?? "agent") !== "context")?.name;
|
|
261
95
|
const modelFor = (taskType) => models ? resolveModel(models, taskType) : undefined;
|
|
262
|
-
|
|
96
|
+
const variantFor = (taskType) => variants ? resolveVariant(variants, taskType) : undefined;
|
|
97
|
+
/** Render a prompt template with current context + outputs. */
|
|
263
98
|
const renderPrompt = (promptPath, extraCtx) => {
|
|
264
99
|
const template = loadPromptTemplate(promptPath);
|
|
265
|
-
return renderTemplate(template, { ...ctx, phaseOutputs, ...(extraCtx || {}) });
|
|
100
|
+
return renderTemplate(template, { ...ctx, phaseOutputs: outputs, ...(extraCtx || {}) });
|
|
266
101
|
};
|
|
267
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* Render a YAML message template and post it as a *standalone* message.
|
|
104
|
+
* Routes through `reporter.note()` when the in-place checklist is active,
|
|
105
|
+
* else the legacy `postComment`.
|
|
106
|
+
*/
|
|
268
107
|
const notifyMessage = async (template, extraCtx) => {
|
|
269
108
|
if (!template)
|
|
270
109
|
return;
|
|
271
|
-
const rendered = renderTemplate(template, { ...ctx, phaseOutputs, ...(extraCtx || {}) });
|
|
272
|
-
if (rendered.trim())
|
|
110
|
+
const rendered = renderTemplate(template, { ...ctx, phaseOutputs: outputs, ...(extraCtx || {}) });
|
|
111
|
+
if (!rendered.trim())
|
|
112
|
+
return;
|
|
113
|
+
if (reporter)
|
|
114
|
+
await reporter.note(rendered);
|
|
115
|
+
else
|
|
116
|
+
await notify(rendered);
|
|
117
|
+
};
|
|
118
|
+
/** Post a pre-rendered standalone message (already-built string). */
|
|
119
|
+
const postNote = async (text) => {
|
|
120
|
+
if (!text.trim())
|
|
121
|
+
return;
|
|
122
|
+
if (reporter)
|
|
123
|
+
await reporter.note(text);
|
|
124
|
+
else
|
|
125
|
+
await notify(text);
|
|
126
|
+
};
|
|
127
|
+
/** Transition a checklist step (and optionally render a YAML message detail). */
|
|
128
|
+
const reportStep = async (key, status, template, extraCtx, opts) => {
|
|
129
|
+
const rendered = template
|
|
130
|
+
? renderTemplate(template, { ...ctx, phaseOutputs: outputs, ...(extraCtx || {}) }).trim()
|
|
131
|
+
: "";
|
|
132
|
+
if (reporter) {
|
|
133
|
+
const detail = collapseDetail(rendered);
|
|
134
|
+
if (opts?.insert) {
|
|
135
|
+
const step = { key, label: opts.label ?? key, status, detail };
|
|
136
|
+
await reporter.insertStep(step, opts.insertBefore ?? lastPhaseKey);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
await reporter.step(key, status, detail);
|
|
140
|
+
}
|
|
141
|
+
if (opts?.alsoNote && rendered)
|
|
142
|
+
await reporter.note(rendered);
|
|
143
|
+
}
|
|
144
|
+
else if (rendered) {
|
|
273
145
|
await notify(rendered);
|
|
146
|
+
}
|
|
274
147
|
};
|
|
275
148
|
/** Persist a phase transition to the DB workflow run. */
|
|
276
149
|
const persistPhase = (phase, summary) => {
|
|
@@ -281,758 +154,207 @@ export async function runWorkflow(definition, ctx, config, callbacks, db, models
|
|
|
281
154
|
success: true,
|
|
282
155
|
summary,
|
|
283
156
|
};
|
|
284
|
-
db.
|
|
157
|
+
db.runs.appendPhase(workflowId, phase, entry);
|
|
285
158
|
}
|
|
286
159
|
};
|
|
287
160
|
/** Mark the workflow run as failed. */
|
|
288
161
|
const failWorkflow = (errorMsg) => {
|
|
289
162
|
if (db && workflowId) {
|
|
290
|
-
db.
|
|
163
|
+
db.runs.finishRun(workflowId, "failed", { error: errorMsg });
|
|
291
164
|
}
|
|
292
165
|
};
|
|
293
|
-
// Determine resume point from the DB row's currentPhase. If the row doesn't
|
|
294
|
-
// exist (no DB), default to running everything from the first phase.
|
|
295
|
-
let resumeFromIdx = 0;
|
|
296
|
-
if (db && workflowId) {
|
|
297
|
-
const run = db.getWorkflowRun(workflowId);
|
|
298
|
-
if (run?.currentPhase) {
|
|
299
|
-
const next = nextPhaseAfter(definition, run.currentPhase);
|
|
300
|
-
if (next) {
|
|
301
|
-
const idx = phaseIndexInDefinition(definition, next);
|
|
302
|
-
if (idx >= 0)
|
|
303
|
-
resumeFromIdx = idx;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
const shouldRun = (phaseName) => {
|
|
308
|
-
const idx = phaseIndexInDefinition(definition, phaseName);
|
|
309
|
-
// Unknown phases always run — they're not tracked in the definition order.
|
|
310
|
-
if (idx === -1)
|
|
311
|
-
return true;
|
|
312
|
-
return idx >= resumeFromIdx;
|
|
313
|
-
};
|
|
314
166
|
/** Should an approval gate with this name actually pause the workflow? */
|
|
315
167
|
const gateEnabled = (gateName) => !!gateName && approvalConfig?.[gateName] === true;
|
|
316
|
-
// ──
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
168
|
+
// ── Collaborators ───────────────────────────────────────────────────────────
|
|
169
|
+
const runScope = {
|
|
170
|
+
definition,
|
|
171
|
+
ctx,
|
|
172
|
+
config,
|
|
173
|
+
taskId,
|
|
174
|
+
triggerId,
|
|
175
|
+
githubAccess,
|
|
176
|
+
scratch,
|
|
177
|
+
db,
|
|
178
|
+
workflowId,
|
|
179
|
+
};
|
|
180
|
+
const phaseReporter = {
|
|
181
|
+
onStart,
|
|
182
|
+
onEnd,
|
|
183
|
+
step: reportStep,
|
|
184
|
+
message: notifyMessage,
|
|
185
|
+
postNote,
|
|
186
|
+
persistPhase,
|
|
187
|
+
failWorkflow,
|
|
188
|
+
};
|
|
189
|
+
const phaseResolver = {
|
|
190
|
+
modelFor,
|
|
191
|
+
variantFor,
|
|
192
|
+
renderPrompt,
|
|
193
|
+
gateEnabled,
|
|
194
|
+
};
|
|
195
|
+
const executor = new PhaseExecutor(runScope, phaseReporter, phaseResolver);
|
|
196
|
+
// ── Schedule ─────────────────────────────────────────────────────────────────
|
|
197
|
+
const dag = buildDag(definition.phases, { chainIfNoDeps: true });
|
|
198
|
+
// Phase lookup + the backend this run is actually executing on. Used by the
|
|
199
|
+
// `requires_sandbox` gate below — config.sandbox is undefined when no backend
|
|
200
|
+
// override is set, which resolves to gondolin (see agent-executor).
|
|
201
|
+
const phaseByName = new Map(definition.phases.map((p) => [p.name, p]));
|
|
202
|
+
const activeBackend = config.sandbox ?? "gondolin";
|
|
203
|
+
while (!isComplete(dag)) {
|
|
204
|
+
// Honour a cancel that landed during the previous phase's execution.
|
|
205
|
+
if (db && workflowId) {
|
|
206
|
+
const latest = db.runs.getRun(workflowId);
|
|
207
|
+
if (latest?.status === "cancelled") {
|
|
208
|
+
console.log(`[runner] ${definition.name} cancelled — stopping`);
|
|
209
|
+
return { success: false, phases };
|
|
334
210
|
}
|
|
335
|
-
continue;
|
|
336
211
|
}
|
|
337
|
-
//
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
212
|
+
// Skip nodes whose trigger rule fails (deps terminal, rule unsatisfied).
|
|
213
|
+
// This is how a failure cascades to the end of a chain as skips. Skips are
|
|
214
|
+
// recorded in the executions ledger — the single source of truth the
|
|
215
|
+
// dashboard derives phase status from.
|
|
216
|
+
const toSkip = getNodesToSkip(dag);
|
|
217
|
+
for (const node of toSkip) {
|
|
218
|
+
node.status = "skipped";
|
|
219
|
+
phases.push({ phase: node.name, success: true, output: "Skipped (trigger rule not satisfied)" });
|
|
220
|
+
db?.executions.recordSkippedPhase(`${definition.name}:${node.name}`, triggerId, workflowId, githubAccess.repo);
|
|
221
|
+
await reportStep(node.name, "skipped");
|
|
341
222
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
359
|
-
await onStart(reviewLabel);
|
|
360
|
-
await notifyMessage(loop.messages?.on_cycle_start, { cycle: fixCycles + 1, maxCycles: MAX_CYCLES });
|
|
361
|
-
// Choose prompt: first cycle uses phase.prompt or phase.skill (via
|
|
362
|
-
// buildPhasePrompt), subsequent cycles always use the re_review_prompt
|
|
363
|
-
// template path defined in loop.on_request_changes.
|
|
364
|
-
const reviewPrompt = fixCycles === 0
|
|
365
|
-
? buildPhasePrompt(phase, { ...ctx, phaseOutputs }, { fixCycle: fixCycles })
|
|
366
|
-
: renderPrompt(loop.on_request_changes.re_review_prompt, { fixCycle: fixCycles });
|
|
367
|
-
// Resolve model
|
|
368
|
-
const reviewModelRaw = phase.model ? renderTemplate(phase.model, ctx) : undefined;
|
|
369
|
-
const reviewModel = reviewModelRaw || modelFor(phaseName);
|
|
370
|
-
const rr = await runPhase(definition.name, reviewLabel, taskId, triggerId, reviewPrompt, config, db, reviewModel, workflowId, githubAccess);
|
|
371
|
-
if (rr.skipped) {
|
|
372
|
-
if (rr.reason === "running") {
|
|
373
|
-
await notifyMessage(phase.messages?.on_skipped_done);
|
|
374
|
-
return { success: false, phases };
|
|
375
|
-
}
|
|
376
|
-
approved = true;
|
|
377
|
-
phases.push({ phase: reviewLabel, success: true, output: "Already completed" });
|
|
378
|
-
break;
|
|
379
|
-
}
|
|
380
|
-
phases.push({ phase: reviewLabel, ...pickResult(rr.result) });
|
|
381
|
-
await onEnd(reviewLabel, phases[phases.length - 1]);
|
|
382
|
-
// Parse verdict
|
|
383
|
-
const reviewerOutput = (rr.result.output || "").trim();
|
|
384
|
-
const verdictMarker = reviewerOutput.match(/^\s*VERDICT:\s*(APPROVED|REQUEST_CHANGES)\s*$/im);
|
|
385
|
-
let isApproved;
|
|
386
|
-
if (verdictMarker) {
|
|
387
|
-
isApproved = verdictMarker[1].toUpperCase() === "APPROVED";
|
|
388
|
-
}
|
|
389
|
-
else {
|
|
390
|
-
const upper = reviewerOutput.toUpperCase();
|
|
391
|
-
const hasRequestChanges = /\bREQUEST_CHANGES\b/.test(upper);
|
|
392
|
-
isApproved = !hasRequestChanges && /^APPROVED\b/.test(upper);
|
|
393
|
-
console.warn(`[runner] Reviewer output missing VERDICT: marker — using fallback detection (isApproved=${isApproved})`);
|
|
394
|
-
}
|
|
395
|
-
if (isApproved) {
|
|
396
|
-
approved = true;
|
|
397
|
-
persistPhase(reviewLabel, "APPROVED");
|
|
398
|
-
await notifyMessage(loop.messages?.on_approved, { cycle: fixCycles + 1 });
|
|
399
|
-
}
|
|
400
|
-
else if (fixCycles < MAX_CYCLES) {
|
|
401
|
-
fixCycles++;
|
|
402
|
-
persistPhase(reviewLabel, "REQUEST_CHANGES");
|
|
403
|
-
// Approval gate before fix loop
|
|
404
|
-
const gateKey = loop.approval_gate;
|
|
405
|
-
if (gateEnabled(gateKey) && db && workflowId) {
|
|
406
|
-
const approvalId = randomUUID();
|
|
407
|
-
db.createApproval({
|
|
408
|
-
id: approvalId,
|
|
409
|
-
workflowRunId: workflowId,
|
|
410
|
-
gate: gateKey,
|
|
411
|
-
summary: `Reviewer requested changes (cycle ${fixCycles}/${MAX_CYCLES}) on phase ${phaseName}.`,
|
|
412
|
-
requestedBy: ctx.sender,
|
|
413
|
-
createdAt: new Date().toISOString(),
|
|
414
|
-
});
|
|
415
|
-
db.updateWorkflowPhase(workflowId, "waiting_approval", {
|
|
416
|
-
phase: "waiting_approval",
|
|
417
|
-
timestamp: new Date().toISOString(),
|
|
418
|
-
success: true,
|
|
419
|
-
summary: `Waiting for approval: ${gateKey} (${approvalId})`,
|
|
420
|
-
});
|
|
421
|
-
db.pauseWorkflowRun(workflowId);
|
|
422
|
-
await notifyMessage(loop.messages?.on_pause_for_approval, {
|
|
423
|
-
cycle: fixCycles,
|
|
424
|
-
maxCycles: MAX_CYCLES,
|
|
425
|
-
gateKey,
|
|
426
|
-
});
|
|
427
|
-
return { success: true, phases, paused: true };
|
|
428
|
-
}
|
|
429
|
-
await notifyMessage(loop.messages?.on_request_changes, { cycle: fixCycles, maxCycles: MAX_CYCLES });
|
|
430
|
-
// Run fix phase
|
|
431
|
-
const fixLabel = `${phaseName}_fix_${fixCycles}`;
|
|
432
|
-
await onStart(fixLabel);
|
|
433
|
-
await notifyMessage(loop.messages?.on_fix_start, { cycle: fixCycles, maxCycles: MAX_CYCLES });
|
|
434
|
-
const fixModelRaw = loop.on_request_changes.fix_model
|
|
435
|
-
? renderTemplate(loop.on_request_changes.fix_model, ctx)
|
|
436
|
-
: undefined;
|
|
437
|
-
const fixModel = fixModelRaw || modelFor(`${phaseName}_fix`) || modelFor(phaseName);
|
|
438
|
-
const fixPromptRendered = renderPrompt(loop.on_request_changes.fix_prompt, {
|
|
439
|
-
fixCycle: fixCycles,
|
|
440
|
-
});
|
|
441
|
-
const fr = await runPhase(definition.name, fixLabel, taskId, triggerId, fixPromptRendered, config, db, fixModel, workflowId, githubAccess);
|
|
442
|
-
if (fr.skipped) {
|
|
443
|
-
if (fr.reason === "running") {
|
|
444
|
-
await notifyMessage(phase.messages?.on_skipped_done);
|
|
445
|
-
return { success: false, phases };
|
|
446
|
-
}
|
|
447
|
-
phases.push({ phase: fixLabel, success: true, output: "Already completed" });
|
|
448
|
-
}
|
|
449
|
-
else {
|
|
450
|
-
phases.push({ phase: fixLabel, ...pickResult(fr.result) });
|
|
451
|
-
await onEnd(fixLabel, phases[phases.length - 1]);
|
|
452
|
-
if (!fr.result.success) {
|
|
453
|
-
if (!isTerminated(fr.result.error)) {
|
|
454
|
-
await notifyMessage(loop.messages?.on_fix_failed, { cycle: fixCycles, maxCycles: MAX_CYCLES });
|
|
455
|
-
}
|
|
456
|
-
break;
|
|
457
|
-
}
|
|
458
|
-
persistPhase(fixLabel);
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
else {
|
|
462
|
-
persistPhase(reviewLabel, "REQUEST_CHANGES — max cycles reached");
|
|
463
|
-
await notifyMessage(loop.messages?.on_max_cycles, { cycle: fixCycles, maxCycles: MAX_CYCLES });
|
|
464
|
-
break;
|
|
465
|
-
}
|
|
223
|
+
const ready = getReadyNodes(dag);
|
|
224
|
+
// Capability gate: skip any ready node whose declared capability isn't
|
|
225
|
+
// available on this host. Safe-by-default graceful degradation — a gated
|
|
226
|
+
// phase (e.g. the browser-QA step that needs the docker image) silently
|
|
227
|
+
// no-ops instead of failing the workflow. Two reasons trigger a skip:
|
|
228
|
+
// 1. `requires_sandbox` names a backend other than the one running.
|
|
229
|
+
// 2. `sandbox_image: qa` but the browser-QA image isn't built here (only
|
|
230
|
+
// checked on docker — on other backends the field is inert).
|
|
231
|
+
// Uses the same non-failing skip mechanics as the trigger-rule skip above,
|
|
232
|
+
// plus the phase's `on_skipped_done` message so the user sees why.
|
|
233
|
+
const gatedSkip = [];
|
|
234
|
+
for (const node of ready) {
|
|
235
|
+
const phaseDef = phaseByName.get(node.name);
|
|
236
|
+
const req = phaseDef?.requires_sandbox;
|
|
237
|
+
if (req !== undefined && req !== activeBackend) {
|
|
238
|
+
gatedSkip.push({ node, reason: `requires ${req} sandbox; running ${activeBackend}` });
|
|
466
239
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
240
|
+
else if (phaseDef?.sandbox_image === "qa" &&
|
|
241
|
+
activeBackend === "docker" &&
|
|
242
|
+
!qaImageAvailable()) {
|
|
243
|
+
gatedSkip.push({ node, reason: `requires the ${SANDBOX_IMAGE_QA} image, not built on this host` });
|
|
471
244
|
}
|
|
472
|
-
continue;
|
|
473
245
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
}
|
|
483
|
-
// Reply-gate loops resume mid-flight: read the saved iteration out
|
|
484
|
-
// of scratch[scratch_key] so we pick up at N+1 instead of 1 and
|
|
485
|
-
// don't re-run iterations whose dedup rows are already "done".
|
|
486
|
-
const scratchKey = loop.scratch_key;
|
|
487
|
-
const scratchSlot = (scratchKey
|
|
488
|
-
? scratch[scratchKey] ?? {}
|
|
489
|
-
: {});
|
|
490
|
-
const resumeFromIter = loop.gate_kind === "reply" && typeof scratchSlot.iteration === "number"
|
|
491
|
-
? Math.min(scratchSlot.iteration, MAX_ITER)
|
|
492
|
-
: 0;
|
|
493
|
-
let iteration = resumeFromIter;
|
|
494
|
-
let complete = false;
|
|
495
|
-
let previousOutput = scratchSlot.lastOutput ?? "";
|
|
496
|
-
while (!complete && iteration < MAX_ITER) {
|
|
497
|
-
iteration++;
|
|
498
|
-
const iterLabel = `${phaseName}_iter_${iteration}`;
|
|
499
|
-
await onStart(iterLabel);
|
|
500
|
-
// Build prompt with loop context vars
|
|
501
|
-
const iterCtx = {
|
|
502
|
-
iteration,
|
|
503
|
-
maxIterations: MAX_ITER,
|
|
504
|
-
previousOutput: loop.fresh_context ? "" : previousOutput,
|
|
505
|
-
phaseOutputs,
|
|
506
|
-
scratch,
|
|
507
|
-
};
|
|
508
|
-
const prompt = buildPhasePrompt(phase, ctx, iterCtx);
|
|
509
|
-
const modelRaw = phase.model ? renderTemplate(phase.model, ctx) : undefined;
|
|
510
|
-
const model = modelRaw || modelFor(phaseName);
|
|
511
|
-
const ir = await runPhase(definition.name, iterLabel, taskId, triggerId, prompt, config, db, model, workflowId, githubAccess);
|
|
512
|
-
if (ir.skipped) {
|
|
513
|
-
if (ir.reason === "running") {
|
|
514
|
-
await notifyMessage(phase.messages?.on_skipped_done, { iteration });
|
|
515
|
-
return { success: false, phases };
|
|
516
|
-
}
|
|
517
|
-
// Already done — treat as complete
|
|
518
|
-
phases.push({ phase: iterLabel, success: true, output: "Already completed" });
|
|
519
|
-
complete = true;
|
|
520
|
-
break;
|
|
521
|
-
}
|
|
522
|
-
phases.push({ phase: iterLabel, ...pickResult(ir.result) });
|
|
523
|
-
await onEnd(iterLabel, phases[phases.length - 1]);
|
|
524
|
-
if (!ir.result.success) {
|
|
525
|
-
if (!isTerminated(ir.result.error)) {
|
|
526
|
-
await notifyMessage(phase.messages?.on_failure, { iteration });
|
|
527
|
-
}
|
|
528
|
-
failWorkflow(ir.result.error);
|
|
529
|
-
return { success: false, phases };
|
|
530
|
-
}
|
|
531
|
-
const iterOutput = ir.result.output || "";
|
|
532
|
-
// Accumulate previousOutput (cap at MAX_PREV_OUTPUT_BYTES)
|
|
533
|
-
if (!loop.fresh_context) {
|
|
534
|
-
const combined = previousOutput ? `${previousOutput}\n${iterOutput}` : iterOutput;
|
|
535
|
-
previousOutput = combined.length > MAX_PREV_OUTPUT_BYTES
|
|
536
|
-
? combined.slice(-MAX_PREV_OUTPUT_BYTES)
|
|
537
|
-
: combined;
|
|
538
|
-
}
|
|
539
|
-
// Evaluate until expression
|
|
540
|
-
let conditionMet = false;
|
|
541
|
-
if (loop.until) {
|
|
542
|
-
conditionMet = evalUntilExpression(loop.until, {
|
|
543
|
-
output: iterOutput,
|
|
544
|
-
scratch,
|
|
545
|
-
...Object.fromEntries(Object.entries(ctx)
|
|
546
|
-
.filter(([, v]) => typeof v === "string")
|
|
547
|
-
.map(([k, v]) => [k, v])),
|
|
548
|
-
});
|
|
549
|
-
}
|
|
550
|
-
// Evaluate until_bash
|
|
551
|
-
if (!conditionMet && loop.until_bash) {
|
|
552
|
-
try {
|
|
553
|
-
execSync(loop.until_bash, { timeout: 30_000, stdio: "pipe", cwd: config.sandboxDir ?? config.cwd });
|
|
554
|
-
conditionMet = true; // exit 0
|
|
555
|
-
}
|
|
556
|
-
catch {
|
|
557
|
-
conditionMet = false; // non-zero exit
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
if (conditionMet) {
|
|
561
|
-
complete = true;
|
|
562
|
-
if (scratchKey && db && workflowId) {
|
|
563
|
-
db.updateWorkflowRunScratch(workflowId, {
|
|
564
|
-
[scratchKey]: { ...scratchSlot, iteration, ready: true, lastOutput: iterOutput },
|
|
565
|
-
});
|
|
566
|
-
scratch[scratchKey] = {
|
|
567
|
-
...scratch[scratchKey],
|
|
568
|
-
iteration,
|
|
569
|
-
ready: true,
|
|
570
|
-
lastOutput: iterOutput,
|
|
571
|
-
};
|
|
572
|
-
}
|
|
573
|
-
persistPhase(iterLabel, `iteration ${iteration} — condition met`);
|
|
574
|
-
break;
|
|
575
|
-
}
|
|
576
|
-
// Interactive gate between iterations
|
|
577
|
-
if (loop.interactive && !complete && db && workflowId) {
|
|
578
|
-
const isReply = loop.gate_kind === "reply";
|
|
579
|
-
const gateMsg = loop.gate_message
|
|
580
|
-
? renderTemplate(loop.gate_message, { ...ctx, phaseOutputs, iteration, maxIterations: MAX_ITER, scratch })
|
|
581
|
-
: `Loop iteration ${iteration}/${MAX_ITER} complete.`;
|
|
582
|
-
// Persist iteration + last output into scratch BEFORE pausing so
|
|
583
|
-
// the resume path can pick up at N+1 instead of re-running from 1.
|
|
584
|
-
if (scratchKey) {
|
|
585
|
-
const slot = {
|
|
586
|
-
...scratch[scratchKey],
|
|
587
|
-
iteration,
|
|
588
|
-
lastOutput: iterOutput,
|
|
589
|
-
};
|
|
590
|
-
scratch[scratchKey] = slot;
|
|
591
|
-
db.updateWorkflowRunScratch(workflowId, { [scratchKey]: slot });
|
|
592
|
-
}
|
|
593
|
-
const approvalId = randomUUID();
|
|
594
|
-
db.createApproval({
|
|
595
|
-
id: approvalId,
|
|
596
|
-
workflowRunId: workflowId,
|
|
597
|
-
gate: `${phaseName}_iter_${iteration}`,
|
|
598
|
-
summary: gateMsg,
|
|
599
|
-
kind: isReply ? "reply" : "approve",
|
|
600
|
-
requestedBy: ctx.sender,
|
|
601
|
-
createdAt: new Date().toISOString(),
|
|
602
|
-
});
|
|
603
|
-
db.updateWorkflowPhase(workflowId, "waiting_approval", {
|
|
604
|
-
phase: "waiting_approval",
|
|
605
|
-
timestamp: new Date().toISOString(),
|
|
606
|
-
success: true,
|
|
607
|
-
summary: `Waiting for ${isReply ? "reply" : "approval"}: ${phaseName}_iter_${iteration} (${approvalId})`,
|
|
608
|
-
});
|
|
609
|
-
db.pauseWorkflowRun(workflowId);
|
|
610
|
-
if (isReply) {
|
|
611
|
-
// Combine the agent's questions + gate hint into one message
|
|
612
|
-
// so it reads as a single comment on GitHub / Slack.
|
|
613
|
-
const parts = [iterOutput.trim(), gateMsg.trim()].filter(Boolean);
|
|
614
|
-
if (parts.length > 0)
|
|
615
|
-
await notify(parts.join("\n\n---\n\n"));
|
|
616
|
-
}
|
|
617
|
-
else {
|
|
618
|
-
await notify(`**${phaseName} iteration ${iteration}/${MAX_ITER} complete** — approval required to continue.\n\n` +
|
|
619
|
-
`${gateMsg}\n\n` +
|
|
620
|
-
`**To continue:** comment \`@last-light approve\`\n` +
|
|
621
|
-
`**To abort:** comment \`@last-light reject [reason]\``);
|
|
622
|
-
}
|
|
623
|
-
return { success: true, phases, paused: true };
|
|
624
|
-
}
|
|
625
|
-
persistPhase(iterLabel);
|
|
626
|
-
}
|
|
627
|
-
if (!complete) {
|
|
628
|
-
await notifyMessage(phase.messages?.on_failure, {
|
|
629
|
-
iteration,
|
|
630
|
-
maxIterations: MAX_ITER,
|
|
246
|
+
if (gatedSkip.length > 0) {
|
|
247
|
+
for (const { node, reason } of gatedSkip) {
|
|
248
|
+
node.status = "skipped";
|
|
249
|
+
const phaseDef = phaseByName.get(node.name);
|
|
250
|
+
phases.push({
|
|
251
|
+
phase: node.name,
|
|
252
|
+
success: true,
|
|
253
|
+
output: `Skipped (${reason})`,
|
|
631
254
|
});
|
|
255
|
+
db?.executions.recordSkippedPhase(`${definition.name}:${node.name}`, triggerId, workflowId, githubAccess.repo);
|
|
256
|
+
await reportStep(node.name, "skipped", phaseDef?.messages?.on_skipped_done);
|
|
632
257
|
}
|
|
633
|
-
|
|
634
|
-
phaseOutputs[phase.output_var] = { completed: complete, iterations: iteration };
|
|
635
|
-
}
|
|
636
|
-
continue;
|
|
258
|
+
continue; // re-evaluate the DAG with these nodes now terminal
|
|
637
259
|
}
|
|
638
|
-
|
|
639
|
-
|
|
260
|
+
if (ready.length === 0) {
|
|
261
|
+
if (toSkip.length === 0)
|
|
262
|
+
break; // stuck (shouldn't happen in a valid DAG)
|
|
263
|
+
continue; // only had skips — loop to process downstream
|
|
264
|
+
}
|
|
265
|
+
// Sequential: run the earliest-declared ready node, one at a time.
|
|
266
|
+
// Resume is ledger-driven: a completed phase's `runPhase` call returns
|
|
267
|
+
// skipped:done via `shouldRunPhase`, so re-running from the top is safe.
|
|
268
|
+
const node = ready[0];
|
|
269
|
+
node.status = "running";
|
|
270
|
+
let outcome;
|
|
271
|
+
try {
|
|
272
|
+
outcome = await executor.execute(node, outputs);
|
|
273
|
+
}
|
|
274
|
+
catch (err) {
|
|
275
|
+
// An agent call threw (OOM / unexpected). Mark the node failed so the
|
|
276
|
+
// failure cascades to downstream skips, mirroring a normal failure.
|
|
277
|
+
console.error(`[runner] Phase "${node.name}" threw unexpectedly:`, err);
|
|
278
|
+
phases.push({ phase: node.name, success: false, error: String(err), output: "" });
|
|
279
|
+
node.status = "failed";
|
|
640
280
|
continue;
|
|
641
|
-
await onStart(phaseName);
|
|
642
|
-
await notifyMessage(phase.messages?.on_start);
|
|
643
|
-
// Resolve model
|
|
644
|
-
const modelRaw = phase.model ? renderTemplate(phase.model, ctx) : undefined;
|
|
645
|
-
const model = modelRaw || modelFor(phaseName);
|
|
646
|
-
const prompt = buildPhasePrompt(phase, ctx, { phaseOutputs });
|
|
647
|
-
const pr = await runPhase(definition.name, phaseName, taskId, triggerId, prompt, config, db, model, workflowId, githubAccess);
|
|
648
|
-
if (pr.skipped) {
|
|
649
|
-
if (pr.reason === "running") {
|
|
650
|
-
await notifyMessage(phase.messages?.on_skipped_done);
|
|
651
|
-
return { success: false, phases };
|
|
652
|
-
}
|
|
653
|
-
phases.push({ phase: phaseName, success: true, output: "Already completed" });
|
|
654
|
-
// Persist a phase_history entry even when the phase was deduped, so
|
|
655
|
-
// the dashboard's pipeline view shows the phase as 'done' instead of
|
|
656
|
-
// stuck on 'active'/pending. Without this the run looks half-finished
|
|
657
|
-
// even though it succeeded.
|
|
658
|
-
persistPhase(phaseName, "Already completed (deduplicated)");
|
|
659
|
-
await onEnd(phaseName, { phase: phaseName, success: true, output: "Already completed" });
|
|
660
|
-
await notifyMessage(phase.messages?.on_skipped_done);
|
|
661
281
|
}
|
|
662
|
-
|
|
663
|
-
phases.push(
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
//
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
if (phase.output_var) {
|
|
671
|
-
phaseOutputs[phase.output_var] = rawOutput;
|
|
672
|
-
}
|
|
673
|
-
if (!pr.result.success) {
|
|
674
|
-
if (!isTerminated(pr.result.error)) {
|
|
675
|
-
await notifyMessage(phase.messages?.on_failure);
|
|
676
|
-
}
|
|
677
|
-
failWorkflow(pr.result.error);
|
|
678
|
-
return { success: false, phases };
|
|
679
|
-
}
|
|
680
|
-
// Check on_output rules
|
|
681
|
-
if (phase.on_output) {
|
|
682
|
-
const outputUpper = (pr.result.output?.toUpperCase() || "");
|
|
683
|
-
if (phase.on_output.contains_BLOCKED && outputUpper.includes("BLOCKED")) {
|
|
684
|
-
const rule = phase.on_output.contains_BLOCKED;
|
|
685
|
-
const hasUnlessLabel = rule.unless_label && ctx.issueLabels.includes(rule.unless_label);
|
|
686
|
-
const titleMatches = !!rule.unless_title_matches &&
|
|
687
|
-
new RegExp(rule.unless_title_matches, "i").test(ctx.issueTitle || "");
|
|
688
|
-
if (hasUnlessLabel || titleMatches) {
|
|
689
|
-
// Rule bypassed — fall through to phase success path.
|
|
690
|
-
await notifyMessage(rule.bypass_message || phase.messages?.on_blocked_bypassed);
|
|
691
|
-
}
|
|
692
|
-
else if (rule.action === "fail") {
|
|
693
|
-
db?.markLatestAsFailed(`${definition.name}:${phaseName}`, triggerId, rule.message || "BLOCKED", workflowId);
|
|
694
|
-
failWorkflow(rule.message || "BLOCKED");
|
|
695
|
-
const blockedTemplate = rule.message || phase.messages?.on_blocked;
|
|
696
|
-
if (blockedTemplate) {
|
|
697
|
-
await notifyMessage(blockedTemplate);
|
|
698
|
-
}
|
|
699
|
-
return { success: false, phases };
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
// Approval gate
|
|
704
|
-
if (phase.approval_gate && gateEnabled(phase.approval_gate) && db && workflowId) {
|
|
705
|
-
const gateKey = phase.approval_gate;
|
|
706
|
-
const approvalId = randomUUID();
|
|
707
|
-
db.createApproval({
|
|
708
|
-
id: approvalId,
|
|
709
|
-
workflowRunId: workflowId,
|
|
710
|
-
gate: gateKey,
|
|
711
|
-
summary: `${phaseName} complete — awaiting ${gateKey} approval.`,
|
|
712
|
-
requestedBy: ctx.sender,
|
|
713
|
-
createdAt: new Date().toISOString(),
|
|
714
|
-
});
|
|
715
|
-
db.updateWorkflowPhase(workflowId, "waiting_approval", {
|
|
716
|
-
phase: "waiting_approval",
|
|
717
|
-
timestamp: new Date().toISOString(),
|
|
718
|
-
success: true,
|
|
719
|
-
summary: `Waiting for approval: ${gateKey} (${approvalId})`,
|
|
720
|
-
});
|
|
721
|
-
db.pauseWorkflowRun(workflowId);
|
|
722
|
-
await notifyMessage(phase.approval_gate_message, { gateKey });
|
|
723
|
-
return { success: true, phases, paused: true };
|
|
724
|
-
}
|
|
725
|
-
persistPhase(phaseName);
|
|
726
|
-
await notifyMessage(phase.messages?.on_success);
|
|
282
|
+
for (const r of outcome.results)
|
|
283
|
+
phases.push(r);
|
|
284
|
+
if (outcome.outputVars)
|
|
285
|
+
Object.assign(outputs, outcome.outputVars);
|
|
286
|
+
if (outcome.aborted) {
|
|
287
|
+
// Dedup running-skip — another instance owns this phase. Stop without
|
|
288
|
+
// cascading skips; this isn't a phase failure.
|
|
289
|
+
return { success: false, phases };
|
|
727
290
|
}
|
|
291
|
+
if (outcome.paused) {
|
|
292
|
+
return { success: true, phases, paused: true };
|
|
293
|
+
}
|
|
294
|
+
node.status = outcome.status;
|
|
728
295
|
}
|
|
729
296
|
// ── Workflow wrap-up ──────────────────────────────────────────────────────
|
|
730
297
|
//
|
|
731
298
|
// If the definition declares an `on_success.set_phase` terminal marker on any
|
|
732
|
-
// phase, record it so the DB row shows the workflow as fully complete.
|
|
733
|
-
//
|
|
734
|
-
// callers can surface it — purely opportunistic, no hardcoded phase names.
|
|
299
|
+
// phase, record it so the DB row shows the workflow as fully complete. Also
|
|
300
|
+
// opportunistically extract a PR number from the terminal phase's output.
|
|
735
301
|
const anyFailed = phases.some((p) => !p.success);
|
|
736
302
|
const success = !anyFailed;
|
|
737
303
|
let prNumber;
|
|
304
|
+
let prUrl;
|
|
738
305
|
const terminalPhase = [...definition.phases].reverse().find((p) => p.on_success?.set_phase);
|
|
739
306
|
if (terminalPhase) {
|
|
740
307
|
const terminalResult = phases.find((p) => p.phase === terminalPhase.name);
|
|
741
308
|
const prMatch = terminalResult?.output?.match(/#(\d+)/);
|
|
742
309
|
if (prMatch)
|
|
743
310
|
prNumber = parseInt(prMatch[1], 10);
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
311
|
+
const urlMatch = terminalResult?.output?.match(/https?:\/\/[^\s)]+\/pull\/\d+/);
|
|
312
|
+
if (urlMatch)
|
|
313
|
+
prUrl = urlMatch[0];
|
|
747
314
|
}
|
|
748
315
|
if (success) {
|
|
749
316
|
if (db && workflowId) {
|
|
750
|
-
|
|
317
|
+
// Fold the `on_success.set_phase` terminal marker into the same
|
|
318
|
+
// transaction as the status flip so the dashboard never sees one
|
|
319
|
+
// without the other.
|
|
320
|
+
const terminalMarker = terminalPhase?.on_success?.set_phase
|
|
321
|
+
? { phase: terminalPhase.on_success.set_phase, summary: prNumber ? `PR #${prNumber}` : undefined }
|
|
322
|
+
: undefined;
|
|
323
|
+
db.runs.finishRun(workflowId, "succeeded", terminalMarker ? { terminalMarker } : {});
|
|
751
324
|
}
|
|
752
325
|
}
|
|
753
326
|
else {
|
|
754
327
|
const firstFailure = phases.find((p) => !p.success);
|
|
755
328
|
failWorkflow(firstFailure?.error || "workflow failed");
|
|
756
329
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
await notifyMessage(phase.messages?.on_start);
|
|
774
|
-
const phaseCtx = { phaseOutputs: { ...outputs } };
|
|
775
|
-
const prompt = buildPhasePrompt(phase, ctx, phaseCtx);
|
|
776
|
-
const modelRaw = phase.model ? renderTemplate(phase.model, ctx) : undefined;
|
|
777
|
-
const model = modelRaw || modelFor(phaseName);
|
|
778
|
-
const pr = await runPhase(definition.name, phaseName, `${taskId}-${phaseName}`, triggerId, prompt, config, db, model, workflowId, githubAccess);
|
|
779
|
-
if (pr.skipped) {
|
|
780
|
-
return { result: { phase: phaseName, success: true, output: "Already completed" } };
|
|
781
|
-
}
|
|
782
|
-
const result = { phase: phaseName, ...pickResult(pr.result) };
|
|
783
|
-
if (!pr.result.success) {
|
|
784
|
-
await notifyMessage(phase.messages?.on_failure);
|
|
785
|
-
return { result };
|
|
786
|
-
}
|
|
787
|
-
// Check on_output rules
|
|
788
|
-
if (phase.on_output?.contains_BLOCKED && (pr.result.output?.toUpperCase() || "").includes("BLOCKED")) {
|
|
789
|
-
const rule = phase.on_output.contains_BLOCKED;
|
|
790
|
-
const hasUnlessLabel = rule.unless_label && ctx.issueLabels.includes(rule.unless_label);
|
|
791
|
-
const titleMatches = !!rule.unless_title_matches &&
|
|
792
|
-
new RegExp(rule.unless_title_matches, "i").test(ctx.issueTitle || "");
|
|
793
|
-
if (hasUnlessLabel || titleMatches) {
|
|
794
|
-
await notifyMessage(rule.bypass_message || phase.messages?.on_blocked_bypassed);
|
|
795
|
-
}
|
|
796
|
-
else if (rule.action === "fail") {
|
|
797
|
-
db?.markLatestAsFailed(`${definition.name}:${phaseName}`, triggerId, rule.message || "BLOCKED", workflowId);
|
|
798
|
-
failWorkflow(rule.message || "BLOCKED");
|
|
799
|
-
await notifyMessage(rule.message || phase.messages?.on_blocked);
|
|
800
|
-
return { result: { phase: phaseName, success: false, output: pr.result.output ?? "", error: "BLOCKED" } };
|
|
801
|
-
}
|
|
330
|
+
// Single final update: render the workflow's `final_message` against the
|
|
331
|
+
// accumulated outputs and deliver it once — folded into the checklist comment
|
|
332
|
+
// as its footer when the in-place reporter is active, else posted as one
|
|
333
|
+
// standalone comment. Empty render ⇒ no-op (e.g. the synthesizing phase was
|
|
334
|
+
// skipped). This is what lets verify/qa-test end with a single combined
|
|
335
|
+
// verdict instead of a comment per phase.
|
|
336
|
+
if (success && definition.final_message) {
|
|
337
|
+
const finalRendered = renderTemplate(definition.final_message, {
|
|
338
|
+
...ctx,
|
|
339
|
+
phaseOutputs: outputs,
|
|
340
|
+
}).trim();
|
|
341
|
+
if (finalRendered) {
|
|
342
|
+
if (reporter)
|
|
343
|
+
await reporter.footer(finalRendered);
|
|
344
|
+
else
|
|
345
|
+
await notify(finalRendered);
|
|
802
346
|
}
|
|
803
|
-
// Approval gate
|
|
804
|
-
if (phase.approval_gate && gateEnabled(phase.approval_gate) && db && workflowId) {
|
|
805
|
-
const gateKey = phase.approval_gate;
|
|
806
|
-
const approvalId = randomUUID();
|
|
807
|
-
db.createApproval({
|
|
808
|
-
id: approvalId,
|
|
809
|
-
workflowRunId: workflowId,
|
|
810
|
-
gate: gateKey,
|
|
811
|
-
summary: `${phaseName} complete — awaiting ${gateKey} approval.`,
|
|
812
|
-
requestedBy: ctx.sender,
|
|
813
|
-
createdAt: new Date().toISOString(),
|
|
814
|
-
});
|
|
815
|
-
db.updateWorkflowPhase(workflowId, "waiting_approval", {
|
|
816
|
-
phase: "waiting_approval",
|
|
817
|
-
timestamp: new Date().toISOString(),
|
|
818
|
-
success: true,
|
|
819
|
-
summary: `Waiting for approval: ${gateKey} (${approvalId})`,
|
|
820
|
-
});
|
|
821
|
-
db.pauseWorkflowRun(workflowId);
|
|
822
|
-
await notifyMessage(phase.approval_gate_message, { gateKey });
|
|
823
|
-
return { result, paused: true };
|
|
824
|
-
}
|
|
825
|
-
persistPhase(phaseName);
|
|
826
|
-
await notifyMessage(phase.messages?.on_success);
|
|
827
|
-
return { result };
|
|
828
347
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
for (const node of toSkip) {
|
|
834
|
-
node.status = "skipped";
|
|
835
|
-
phases.push({ phase: node.name, success: true, output: "Skipped (trigger rule not satisfied)" });
|
|
836
|
-
if (db && workflowId) {
|
|
837
|
-
db.updateNodeStatus(workflowId, node.name, "skipped");
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
const ready = getReadyNodes(dag);
|
|
841
|
-
if (ready.length === 0) {
|
|
842
|
-
if (toSkip.length === 0)
|
|
843
|
-
break; // stuck (shouldn't happen in a valid DAG)
|
|
844
|
-
continue; // only had skips — loop to process downstream
|
|
845
|
-
}
|
|
846
|
-
// Mark all ready nodes as running before dispatching
|
|
847
|
-
for (const node of ready) {
|
|
848
|
-
node.status = "running";
|
|
849
|
-
if (db && workflowId) {
|
|
850
|
-
db.updateNodeStatus(workflowId, node.name, "running");
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
// Dispatch all ready nodes concurrently
|
|
854
|
-
const nodePromises = ready.map(async (node) => {
|
|
855
|
-
try {
|
|
856
|
-
const phase = phaseMap.get(node.name);
|
|
857
|
-
await onStart(node.name);
|
|
858
|
-
// Context phase — immediate
|
|
859
|
-
if (phase.type === "context" || !phase.type) {
|
|
860
|
-
const r = { phase: node.name, success: true, output: "Context assembled" };
|
|
861
|
-
// Same persist-history fix as the linear runner so the dashboard
|
|
862
|
-
// marks context nodes done.
|
|
863
|
-
persistPhase(node.name, "Context assembled");
|
|
864
|
-
await onEnd(node.name, r);
|
|
865
|
-
return { node, result: r, paused: false, alreadyPushed: false };
|
|
866
|
-
}
|
|
867
|
-
// Phase with neither prompt nor skill — skip
|
|
868
|
-
if (!phase.prompt && !phase.skill) {
|
|
869
|
-
console.warn(`[dag] Phase "${node.name}" has type=agent but neither prompt: nor skill: — skipping`);
|
|
870
|
-
const r = { phase: node.name, success: true, output: "Skipped (no prompt or skill)" };
|
|
871
|
-
return { node, result: r, paused: false, alreadyPushed: false };
|
|
872
|
-
}
|
|
873
|
-
// Loop phase (reviewer-style) — run sequentially as a single DAG node
|
|
874
|
-
if (phase.loop) {
|
|
875
|
-
const loop = phase.loop;
|
|
876
|
-
const MAX_CYCLES = loop.max_cycles;
|
|
877
|
-
let approved = false;
|
|
878
|
-
let fixCycles = 0;
|
|
879
|
-
while (!approved && fixCycles <= MAX_CYCLES) {
|
|
880
|
-
const reviewLabel = fixCycles === 0 ? node.name : `${node.name}_${fixCycles + 1}`;
|
|
881
|
-
const reviewPrompt = fixCycles === 0
|
|
882
|
-
? buildPhasePrompt(phase, ctx, { phaseOutputs: { ...outputs }, fixCycle: fixCycles })
|
|
883
|
-
: renderPrompt(loop.on_request_changes.re_review_prompt, { phaseOutputs: { ...outputs }, fixCycle: fixCycles });
|
|
884
|
-
const reviewModelRaw = phase.model ? renderTemplate(phase.model, ctx) : undefined;
|
|
885
|
-
const reviewModel = reviewModelRaw || modelFor(node.name);
|
|
886
|
-
const rr = await runPhase(definition.name, reviewLabel, `${taskId}-${reviewLabel}`, triggerId, reviewPrompt, config, db, reviewModel, workflowId, githubAccess);
|
|
887
|
-
if (rr.skipped) {
|
|
888
|
-
approved = true;
|
|
889
|
-
phases.push({ phase: reviewLabel, success: true, output: "Already completed" });
|
|
890
|
-
break;
|
|
891
|
-
}
|
|
892
|
-
phases.push({ phase: reviewLabel, ...pickResult(rr.result) });
|
|
893
|
-
await onEnd(reviewLabel, phases[phases.length - 1]);
|
|
894
|
-
const reviewerOutput = (rr.result.output || "").trim();
|
|
895
|
-
const verdictMarker = reviewerOutput.match(/^\s*VERDICT:\s*(APPROVED|REQUEST_CHANGES)\s*$/im);
|
|
896
|
-
const isApproved = verdictMarker
|
|
897
|
-
? verdictMarker[1].toUpperCase() === "APPROVED"
|
|
898
|
-
: !(/\bREQUEST_CHANGES\b/.test(reviewerOutput.toUpperCase())) && /^APPROVED\b/.test(reviewerOutput.toUpperCase());
|
|
899
|
-
if (isApproved) {
|
|
900
|
-
approved = true;
|
|
901
|
-
}
|
|
902
|
-
else if (fixCycles < MAX_CYCLES) {
|
|
903
|
-
fixCycles++;
|
|
904
|
-
const fixLabel = `${node.name}_fix_${fixCycles}`;
|
|
905
|
-
const fixPromptRendered = renderPrompt(loop.on_request_changes.fix_prompt, { phaseOutputs: { ...outputs }, fixCycle: fixCycles });
|
|
906
|
-
const fixModelRaw = loop.on_request_changes.fix_model ? renderTemplate(loop.on_request_changes.fix_model, ctx) : undefined;
|
|
907
|
-
const fixModel = fixModelRaw || modelFor(`${node.name}_fix`) || modelFor(node.name);
|
|
908
|
-
const fr = await runPhase(definition.name, fixLabel, `${taskId}-fix${fixCycles}`, triggerId, fixPromptRendered, config, db, fixModel, workflowId, githubAccess);
|
|
909
|
-
if (!fr.skipped) {
|
|
910
|
-
phases.push({ phase: fixLabel, ...pickResult(fr.result) });
|
|
911
|
-
await onEnd(fixLabel, phases[phases.length - 1]);
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
else {
|
|
915
|
-
break;
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
const loopResult = { phase: node.name, success: approved, output: approved ? "Approved" : "Request changes" };
|
|
919
|
-
if (phase.output_var) {
|
|
920
|
-
outputs[phase.output_var] = { approved, cycles: fixCycles };
|
|
921
|
-
}
|
|
922
|
-
return { node, result: loopResult, paused: false, alreadyPushed: true };
|
|
923
|
-
}
|
|
924
|
-
// Generic loop phase
|
|
925
|
-
if (phase.generic_loop) {
|
|
926
|
-
const loop = phase.generic_loop;
|
|
927
|
-
const MAX_ITER = loop.max_iterations;
|
|
928
|
-
const MAX_PREV_OUTPUT_BYTES = 10 * 1024;
|
|
929
|
-
let iteration = 0;
|
|
930
|
-
let complete = false;
|
|
931
|
-
let previousOutput = "";
|
|
932
|
-
while (!complete && iteration < MAX_ITER) {
|
|
933
|
-
iteration++;
|
|
934
|
-
const iterLabel = `${node.name}_iter_${iteration}`;
|
|
935
|
-
const iterCtx = {
|
|
936
|
-
iteration,
|
|
937
|
-
maxIterations: MAX_ITER,
|
|
938
|
-
previousOutput: loop.fresh_context ? "" : previousOutput,
|
|
939
|
-
phaseOutputs: { ...outputs },
|
|
940
|
-
};
|
|
941
|
-
const iterPrompt = buildPhasePrompt(phase, ctx, iterCtx);
|
|
942
|
-
const modelRaw = phase.model ? renderTemplate(phase.model, ctx) : undefined;
|
|
943
|
-
const model = modelRaw || modelFor(node.name);
|
|
944
|
-
const ir = await runPhase(definition.name, iterLabel, `${taskId}-${iterLabel}`, triggerId, iterPrompt, config, db, model, workflowId, githubAccess);
|
|
945
|
-
if (ir.skipped) {
|
|
946
|
-
complete = true;
|
|
947
|
-
break;
|
|
948
|
-
}
|
|
949
|
-
phases.push({ phase: iterLabel, ...pickResult(ir.result) });
|
|
950
|
-
await onEnd(iterLabel, phases[phases.length - 1]);
|
|
951
|
-
if (!ir.result.success) {
|
|
952
|
-
failWorkflow(ir.result.error);
|
|
953
|
-
return { node, result: { phase: node.name, success: false, output: "", error: ir.result.error }, paused: false, alreadyPushed: true };
|
|
954
|
-
}
|
|
955
|
-
const iterOutput = ir.result.output || "";
|
|
956
|
-
if (!loop.fresh_context) {
|
|
957
|
-
const combined = previousOutput ? `${previousOutput}\n${iterOutput}` : iterOutput;
|
|
958
|
-
previousOutput = combined.length > MAX_PREV_OUTPUT_BYTES ? combined.slice(-MAX_PREV_OUTPUT_BYTES) : combined;
|
|
959
|
-
}
|
|
960
|
-
let conditionMet = false;
|
|
961
|
-
if (loop.until) {
|
|
962
|
-
conditionMet = evalUntilExpression(loop.until, { output: iterOutput, ...Object.fromEntries(Object.entries(ctx).filter(([, v]) => typeof v === "string").map(([k, v]) => [k, v])) });
|
|
963
|
-
}
|
|
964
|
-
if (!conditionMet && loop.until_bash) {
|
|
965
|
-
try {
|
|
966
|
-
execSync(loop.until_bash, { timeout: 30_000, stdio: "pipe", cwd: config.sandboxDir ?? config.cwd });
|
|
967
|
-
conditionMet = true;
|
|
968
|
-
}
|
|
969
|
-
catch {
|
|
970
|
-
conditionMet = false;
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
if (conditionMet) {
|
|
974
|
-
complete = true;
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
if (!complete) {
|
|
978
|
-
await notifyMessage(phase.messages?.on_failure, { iteration, maxIterations: MAX_ITER });
|
|
979
|
-
}
|
|
980
|
-
if (phase.output_var) {
|
|
981
|
-
outputs[phase.output_var] = { completed: complete, iterations: iteration };
|
|
982
|
-
}
|
|
983
|
-
return { node, result: { phase: node.name, success: true, output: previousOutput || "" }, paused: false, alreadyPushed: true };
|
|
984
|
-
}
|
|
985
|
-
// Standard agent phase
|
|
986
|
-
const { result, paused } = await executeSinglePhase(phase, node.name);
|
|
987
|
-
await onEnd(node.name, result);
|
|
988
|
-
return { node, result, paused: paused ?? false, alreadyPushed: false };
|
|
989
|
-
}
|
|
990
|
-
catch (err) {
|
|
991
|
-
console.error(`[dag] Phase "${node.name}" threw unexpectedly:`, err);
|
|
992
|
-
const result = { phase: node.name, success: false, error: String(err), output: "" };
|
|
993
|
-
return { node, result, paused: false, alreadyPushed: false };
|
|
994
|
-
}
|
|
995
|
-
});
|
|
996
|
-
const settled = await Promise.allSettled(nodePromises);
|
|
997
|
-
let anyPaused = false;
|
|
998
|
-
for (const settledItem of settled) {
|
|
999
|
-
if (settledItem.status === "rejected") {
|
|
1000
|
-
console.error("[dag] Phase promise rejected:", settledItem.reason);
|
|
1001
|
-
continue;
|
|
1002
|
-
}
|
|
1003
|
-
const { node, result, paused, alreadyPushed } = settledItem.value;
|
|
1004
|
-
if (!alreadyPushed) {
|
|
1005
|
-
phases.push(result);
|
|
1006
|
-
}
|
|
1007
|
-
if (paused) {
|
|
1008
|
-
anyPaused = true;
|
|
1009
|
-
node.status = "succeeded"; // treat paused as succeeded so downstream trigger rules fire correctly — the workflow will resume from this node's successors after approval
|
|
1010
|
-
continue;
|
|
1011
|
-
}
|
|
1012
|
-
node.status = result.success ? "succeeded" : "failed";
|
|
1013
|
-
if (db && workflowId) {
|
|
1014
|
-
db.updateNodeStatus(workflowId, node.name, node.status);
|
|
1015
|
-
}
|
|
1016
|
-
// Always record the raw phase output (both by phase name and any
|
|
1017
|
-
// configured output_var alias) so downstream prompts can interpolate via
|
|
1018
|
-
// ${phaseName.output} or {{alias}}. Loop phases that emit structured
|
|
1019
|
-
// objects set their output_var themselves above — don't clobber those.
|
|
1020
|
-
const phaseDef = phaseMap.get(node.name);
|
|
1021
|
-
if (result.output != null) {
|
|
1022
|
-
if (outputs[node.name] === undefined || typeof outputs[node.name] === "string") {
|
|
1023
|
-
outputs[node.name] = result.output;
|
|
1024
|
-
}
|
|
1025
|
-
if (phaseDef.output_var && outputs[phaseDef.output_var] === undefined) {
|
|
1026
|
-
outputs[phaseDef.output_var] = result.output;
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
if (anyPaused) {
|
|
1031
|
-
return { success: true, phases, paused: true };
|
|
348
|
+
if (reporter) {
|
|
349
|
+
if (success && prNumber && terminalPhase) {
|
|
350
|
+
const link = prUrl ? `[PR #${prNumber}](${prUrl})` : `PR #${prNumber}`;
|
|
351
|
+
await reporter.step(terminalPhase.name, "done", link);
|
|
1032
352
|
}
|
|
353
|
+
const prSuffix = prNumber ? ` — PR #${prNumber}` : "";
|
|
354
|
+
await reporter.noteTerminal(success
|
|
355
|
+
? `✅ **${definition.name} complete**${prSuffix}.`
|
|
356
|
+
: `❌ **${definition.name} failed** — see the checklist above for the failing step.`);
|
|
1033
357
|
}
|
|
1034
|
-
|
|
1035
|
-
const anyFailed = phases.some((p) => !p.success);
|
|
1036
|
-
return { success: !anyFailed, phases };
|
|
358
|
+
return { success, phases, prNumber };
|
|
1037
359
|
}
|
|
1038
360
|
//# sourceMappingURL=runner.js.map
|