lastlight 0.1.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +423 -437
- 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 +31 -12
- package/dist/setup.js +516 -174
- 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 -7
- 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/index.js
CHANGED
|
@@ -1,25 +1,90 @@
|
|
|
1
|
-
import { mkdirSync,
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync } from "fs";
|
|
2
2
|
import { resolve } from "path";
|
|
3
3
|
import { randomUUID } from "crypto";
|
|
4
|
-
import { loadConfig,
|
|
4
|
+
import { loadConfig, resolveModel, resolveVariant } from "./config.js";
|
|
5
5
|
import { ConnectorRegistry, GitHubWebhookConnector, SlackConnector, SessionManager, MessageDeliveryService } from "./connectors/index.js";
|
|
6
|
-
import {
|
|
7
|
-
import { handleChatMessage } from "./engine/chat.js";
|
|
6
|
+
import { dispatch } from "./engine/dispatcher.js";
|
|
7
|
+
import { CHAT_SYSTEM_SUFFIX, handleChatMessage, loadAgentContext } from "./engine/chat.js";
|
|
8
|
+
import { configureWorkflowAssets, validateAssets, getWorkflow } from "./workflows/loader.js";
|
|
9
|
+
import { ChatRunner } from "./engine/chat-runner.js";
|
|
10
|
+
import { buildReadSkillTool, loadChatSkillCatalogue } from "./engine/chat-skills.js";
|
|
8
11
|
import { configureGitAuth } from "./engine/git-auth.js";
|
|
9
12
|
import { StateDb } from "./state/db.js";
|
|
10
13
|
import { CronScheduler } from "./cron/scheduler.js";
|
|
11
14
|
import { getJobs } from "./cron/jobs.js";
|
|
15
|
+
import { dispatchCronWorkflow } from "./cron/fanout.js";
|
|
12
16
|
import { mountAdmin } from "./admin/index.js";
|
|
13
17
|
import { cleanupOrphanedSandboxes } from "./sandbox/index.js";
|
|
18
|
+
import { writeEgressFirewallConfigs, writeOtelCollectorConfig } from "./sandbox/egress-firewall-config.js";
|
|
19
|
+
import { initTelemetry, shutdownTelemetry } from "./telemetry/index.js";
|
|
14
20
|
import { authMiddleware } from "./admin/auth.js";
|
|
15
21
|
import { GitHubClient } from "./engine/github.js";
|
|
22
|
+
import { screenForInjection, flagPrefix } from "./engine/screen.js";
|
|
16
23
|
import { runSimpleWorkflow } from "./workflows/simple.js";
|
|
17
24
|
import { resumeOrphanedWorkflows } from "./workflows/resume.js";
|
|
25
|
+
import { ProgressNotifier, GitHubTransport, SlackTransport, } from "./notify/index.js";
|
|
26
|
+
/**
|
|
27
|
+
* Pre-flight validation — checks that config is sane before starting any
|
|
28
|
+
* services. Exits with code 78 (EX_CONFIG) on configuration errors so
|
|
29
|
+
* Docker's restart policy doesn't loop forever on a misconfigured container.
|
|
30
|
+
*/
|
|
31
|
+
function validateConfig(config) {
|
|
32
|
+
const fatal = (msg) => {
|
|
33
|
+
console.error(`\n[startup] FATAL: ${msg}`);
|
|
34
|
+
console.error("[startup] Fix your .env and restart.\n");
|
|
35
|
+
process.exit(78); // EX_CONFIG — sysexits.h convention
|
|
36
|
+
};
|
|
37
|
+
if (config.githubApp) {
|
|
38
|
+
const { appId, privateKeyPath, installationId } = config.githubApp;
|
|
39
|
+
if (!appId || !installationId) {
|
|
40
|
+
fatal("GITHUB_APP_ID and GITHUB_APP_INSTALLATION_ID are required when the GitHub App is configured.");
|
|
41
|
+
}
|
|
42
|
+
if (!existsSync(resolve(privateKeyPath))) {
|
|
43
|
+
fatal(`GITHUB_APP_PRIVATE_KEY_PATH points to "${privateKeyPath}" which does not exist.`);
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const content = readFileSync(resolve(privateKeyPath), "utf8");
|
|
47
|
+
if (!content.startsWith("-----BEGIN")) {
|
|
48
|
+
fatal(`GITHUB_APP_PRIVATE_KEY_PATH ("${privateKeyPath}") does not look like a PEM file.`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
fatal(`Cannot read GITHUB_APP_PRIVATE_KEY_PATH ("${privateKeyPath}"): ${err.message}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (!config.webhookSecret && config.githubApp) {
|
|
56
|
+
console.warn("[startup] WEBHOOK_SECRET is not set — webhook signature verification is disabled.");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
18
59
|
async function main() {
|
|
19
60
|
console.log("Last Light v2.0 — Agent SDK Harness");
|
|
20
61
|
console.log("====================================");
|
|
21
|
-
// Load config
|
|
22
|
-
|
|
62
|
+
// Load and validate config + overlay assets before starting anything. These
|
|
63
|
+
// throw on a broken/empty overlay, a cron targeting a missing workflow, or a
|
|
64
|
+
// phase whose prompt/skill can't resolve — all unfixable by a restart, so we
|
|
65
|
+
// exit 78 (EX_CONFIG) to stop Docker's restart policy from looping.
|
|
66
|
+
let config;
|
|
67
|
+
try {
|
|
68
|
+
config = loadConfig();
|
|
69
|
+
configureWorkflowAssets({
|
|
70
|
+
builtInRoot: config.builtInRoot,
|
|
71
|
+
overlayRoot: config.overlayDir,
|
|
72
|
+
disabled: config.disabled,
|
|
73
|
+
});
|
|
74
|
+
validateAssets(config.routes);
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
console.error(`\n[startup] FATAL: ${err.message}`);
|
|
78
|
+
console.error("[startup] Fix your config/overlay and restart.\n");
|
|
79
|
+
process.exit(78); // EX_CONFIG — sysexits.h convention
|
|
80
|
+
}
|
|
81
|
+
validateConfig(config);
|
|
82
|
+
const packageJson = JSON.parse(readFileSync(resolve("package.json"), "utf8"));
|
|
83
|
+
await initTelemetry(config.otel, { packageVersion: packageJson.version });
|
|
84
|
+
let telemetryShutdownStarted = false;
|
|
85
|
+
console.log(config.otel.enabled
|
|
86
|
+
? `[otel] enabled service=${config.otel.serviceName} forwardToSandbox=${config.otel.forwardToSandbox} includeContent=${config.otel.includeContent}`
|
|
87
|
+
: "[otel] disabled");
|
|
23
88
|
console.log(`[config] Port: ${config.port}, Model: ${config.model}`);
|
|
24
89
|
const modelOverrides = Object.entries(config.models).filter(([k]) => k !== "default");
|
|
25
90
|
if (modelOverrides.length > 0) {
|
|
@@ -32,14 +97,59 @@ async function main() {
|
|
|
32
97
|
mkdirSync(resolve(config.stateDir, sub), { recursive: true });
|
|
33
98
|
}
|
|
34
99
|
console.log(`[state] State dir: ${config.stateDir}`);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
100
|
+
console.log(`[state] Sessions dir: ${config.sessionsDir}`);
|
|
101
|
+
console.log(`[config] Sandbox backend: ${config.sandbox}`);
|
|
102
|
+
// Regenerate egress firewall configs (nginx ssl_preread + coredns) from
|
|
103
|
+
// the allowlist source of truth. Only meaningful for the docker backend;
|
|
104
|
+
// cheap enough to do unconditionally so a backend switch doesn't leave
|
|
105
|
+
// stale configs on disk. The docker backend forwards sandbox telemetry
|
|
106
|
+
// through the in-network OTEL collector (reached by IP), so the strict
|
|
107
|
+
// SNI allowlist no longer needs collector hosts — that hop happens on
|
|
108
|
+
// the collector's trusted outbound leg, not through the firewall.
|
|
109
|
+
const proxyDir = writeEgressFirewallConfigs(config.stateDir);
|
|
110
|
+
console.log(`[state] Egress firewall configs: ${proxyDir}`);
|
|
111
|
+
// Generate the in-network OTEL collector config (docker backend). Derived
|
|
112
|
+
// from the harness's OTEL_* backend env so the collector re-exports to the
|
|
113
|
+
// same backend the harness uses — with auth headers that stay host-side.
|
|
114
|
+
// Forwarding is gated on telemetry being active: when disabled (or sandbox
|
|
115
|
+
// forwarding off) the collector renders an inert debug-only config so the
|
|
116
|
+
// static collector IP can't be used as a sandbox exfil path.
|
|
117
|
+
const collectorConfigPath = writeOtelCollectorConfig(config.stateDir, {
|
|
118
|
+
active: config.otel.enabled && config.otel.forwardToSandbox,
|
|
119
|
+
});
|
|
120
|
+
console.log(`[state] OTEL collector config: ${collectorConfigPath} (forwarding ${config.otel.enabled && config.otel.forwardToSandbox ? "active" : "disabled"})`);
|
|
121
|
+
// Initialize state database first — ChatRunner needs SessionManager
|
|
122
|
+
// (DB-backed) at construction time.
|
|
123
|
+
const db = new StateDb(config.dbPath);
|
|
124
|
+
console.log(`[state] Database: ${config.dbPath}`);
|
|
125
|
+
// Session manager for messaging connectors (shared across Slack, Discord, etc.)
|
|
126
|
+
const sessionManager = new SessionManager(db.database);
|
|
127
|
+
// In-process chat runner backs the messaging chat skill. One pi-ai
|
|
128
|
+
// conversation per Slack/Discord thread; locked down to read-only
|
|
129
|
+
// GitHub tools plus a `read_skill` tool that exposes the curated
|
|
130
|
+
// chat skill catalogue. Skills are listed in the system prompt via
|
|
131
|
+
// an XML <available_skills> block; the agent pulls full SKILL.md on
|
|
132
|
+
// demand — same progressive-disclosure model the sandbox phases use.
|
|
133
|
+
const chatSkills = loadChatSkillCatalogue();
|
|
134
|
+
const readSkill = buildReadSkillTool(chatSkills.skills);
|
|
135
|
+
const chatRunner = new ChatRunner({
|
|
136
|
+
model: resolveModel(config.models, "chat"),
|
|
137
|
+
thinking: resolveVariant(config.variants, "chat"),
|
|
138
|
+
systemPrompt: loadAgentContext() + CHAT_SYSTEM_SUFFIX + chatSkills.catalogueXml,
|
|
139
|
+
github: config.githubApp,
|
|
140
|
+
extraTools: chatSkills.skills.length > 0
|
|
141
|
+
? { tools: [readSkill.tool], execute: readSkill.execute }
|
|
142
|
+
: undefined,
|
|
143
|
+
}, sessionManager);
|
|
144
|
+
if (chatSkills.skills.length > 0) {
|
|
145
|
+
console.log(`[chat] Loaded ${chatSkills.skills.length} skill(s): ${chatSkills.skills.map((s) => s.name).join(", ")}`);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
console.warn("[chat] No skills loaded — frontmatter missing or no matching SKILL.md found");
|
|
149
|
+
}
|
|
150
|
+
// Configure git with GitHub App credentials — agents can git clone/push natively.
|
|
151
|
+
// Non-fatal: the token is refreshed before each agent execution anyway, so a
|
|
152
|
+
// transient failure here (DNS, rate limit) doesn't block startup.
|
|
43
153
|
if (config.githubApp) {
|
|
44
154
|
try {
|
|
45
155
|
await configureGitAuth({
|
|
@@ -49,12 +159,9 @@ async function main() {
|
|
|
49
159
|
});
|
|
50
160
|
}
|
|
51
161
|
catch (err) {
|
|
52
|
-
console.warn(`[git-auth] Initial
|
|
162
|
+
console.warn(`[git-auth] Initial token mint failed (will retry per-execution): ${err.message}`);
|
|
53
163
|
}
|
|
54
164
|
}
|
|
55
|
-
// Initialize state database
|
|
56
|
-
const db = new StateDb(config.dbPath);
|
|
57
|
-
console.log(`[state] Database: ${config.dbPath}`);
|
|
58
165
|
// GitHub API client for harness-level operations (posting comments, fetching issues)
|
|
59
166
|
const github = config.githubApp ? new GitHubClient(config.githubApp) : null;
|
|
60
167
|
/**
|
|
@@ -66,7 +173,7 @@ async function main() {
|
|
|
66
173
|
* The router still uses skill names for backwards compat — for the four
|
|
67
174
|
* agent skills they're 1:1 with workflow names.
|
|
68
175
|
*/
|
|
69
|
-
const dispatchWorkflow = async (workflowName, context) => {
|
|
176
|
+
const dispatchWorkflow = async (workflowName, context, onRunStart) => {
|
|
70
177
|
// Slack-initiated workflows (explore, /explore) carry a
|
|
71
178
|
// `slack:{team}:{channel}:{thread}` triggerId and don't require a
|
|
72
179
|
// managed `repo` — their postComment goes back to the Slack thread.
|
|
@@ -91,7 +198,7 @@ async function main() {
|
|
|
91
198
|
}
|
|
92
199
|
// Pluck the standard fields, leave the rest in `extra` for the workflow
|
|
93
200
|
// template to consume.
|
|
94
|
-
const { _triggerType, repo: _r, issueNumber, prNumber, title, body, labels, sender, commentBody, triggerId: _triggerId, channelId, threadId, ...rest } = context;
|
|
201
|
+
const { _triggerType, repo: _r, issueNumber, prNumber, title, body, labels, sender, commentBody, triggerId: _triggerId, channelId, threadId, prePopulateBranch: ctxPrePopulateBranch, branch: ctxBranch, ...rest } = context;
|
|
95
202
|
// Preserve channelId/threadId in extra so they're stored on the
|
|
96
203
|
// workflow run context — needed by boot-time resume to rebuild the
|
|
97
204
|
// Slack postComment callback after a harness restart.
|
|
@@ -100,6 +207,35 @@ async function main() {
|
|
|
100
207
|
extra.channelId = channelId;
|
|
101
208
|
if (typeof threadId === "string")
|
|
102
209
|
extra.threadId = threadId;
|
|
210
|
+
// For PR-scoped read workflows, resolve the PR head ref and ask the
|
|
211
|
+
// sandbox to pre-clone the repo at that branch. The agent then enters
|
|
212
|
+
// a workspace that's already a checkout of the PR's actual code —
|
|
213
|
+
// saves a redundant clone_repo MCP call inside the session.
|
|
214
|
+
//
|
|
215
|
+
// pr-fix already plumbs `branch` through context (line ~709 below)
|
|
216
|
+
// because the architect/executor need the branch name to push to;
|
|
217
|
+
// we honor that here as the pre-populate branch too.
|
|
218
|
+
let prePopulateBranch = typeof ctxPrePopulateBranch === "string" ? ctxPrePopulateBranch : undefined;
|
|
219
|
+
if (!prePopulateBranch && typeof ctxBranch === "string" && ctxBranch && workflowName === "pr-fix") {
|
|
220
|
+
prePopulateBranch = ctxBranch;
|
|
221
|
+
}
|
|
222
|
+
if (!prePopulateBranch &&
|
|
223
|
+
workflowName === "pr-review" &&
|
|
224
|
+
typeof prNumber === "number" &&
|
|
225
|
+
github &&
|
|
226
|
+
owner &&
|
|
227
|
+
repo) {
|
|
228
|
+
try {
|
|
229
|
+
const pr = await github.getPullRequest(owner, repo, prNumber);
|
|
230
|
+
prePopulateBranch = pr.head.ref;
|
|
231
|
+
console.log(`[dispatch] pr-review: pre-populating workspace at ${owner}/${repo}@${prePopulateBranch}`);
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
235
|
+
console.warn(`[dispatch] pr-review: could not resolve PR head ref (${msg}); ` +
|
|
236
|
+
`agent will need to clone via MCP`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
103
239
|
const request = {
|
|
104
240
|
owner,
|
|
105
241
|
repo,
|
|
@@ -112,7 +248,64 @@ async function main() {
|
|
|
112
248
|
sender: typeof sender === "string" ? sender : "unknown",
|
|
113
249
|
triggerId: slackTriggerId,
|
|
114
250
|
extra,
|
|
251
|
+
prePopulateBranch,
|
|
115
252
|
};
|
|
253
|
+
// For workflows where the architect/agent needs to see the full issue
|
|
254
|
+
// history (e.g. a build greenlit by "@last-light lets build this!" needs
|
|
255
|
+
// the spec the explore phase wrote in earlier comments), fetch the real
|
|
256
|
+
// issue body and the comment thread, combine them into a single context
|
|
257
|
+
// blob, and screen the combined text in ONE SDK call.
|
|
258
|
+
//
|
|
259
|
+
// Why single-shot: screening per-comment fans out N concurrent SDK calls,
|
|
260
|
+
// and on a busy issue (16+ comments) that exhausts memory and trips the
|
|
261
|
+
// EventEmitter listener cap. The combined-context approach keeps screen
|
|
262
|
+
// cost at exactly one haiku call regardless of thread length.
|
|
263
|
+
//
|
|
264
|
+
// For comment-triggered builds the envelope's `body` field is the
|
|
265
|
+
// triggering comment, not the issue body — we explicitly fetch the
|
|
266
|
+
// real issue body here so the architect sees both the spec (issue body
|
|
267
|
+
// + thread) and the trigger (commentBody) cleanly separated.
|
|
268
|
+
const ENRICH_WORKFLOWS = new Set(["build", "pr-fix", "explore"]);
|
|
269
|
+
if (github &&
|
|
270
|
+
ENRICH_WORKFLOWS.has(workflowName) &&
|
|
271
|
+
request.issueNumber &&
|
|
272
|
+
owner && repo) {
|
|
273
|
+
try {
|
|
274
|
+
const [trueIssueBody, comments] = await Promise.all([
|
|
275
|
+
github.getIssueBody(owner, repo, request.issueNumber),
|
|
276
|
+
github.listIssueComments(owner, repo, request.issueNumber),
|
|
277
|
+
]);
|
|
278
|
+
const formattedComments = comments
|
|
279
|
+
.filter((c) => c.body.trim())
|
|
280
|
+
.map((c) => `--- @${c.user} (${c.createdAt}) ---\n${c.body}`)
|
|
281
|
+
.join("\n\n");
|
|
282
|
+
const combinedContext = [
|
|
283
|
+
trueIssueBody ? `# Issue body\n\n${trueIssueBody}` : "",
|
|
284
|
+
formattedComments ? `# Issue thread (oldest → newest)\n\n${formattedComments}` : "",
|
|
285
|
+
]
|
|
286
|
+
.filter(Boolean)
|
|
287
|
+
.join("\n\n");
|
|
288
|
+
if (combinedContext) {
|
|
289
|
+
// Single screening call over the entire combined context.
|
|
290
|
+
const screen = await screenForInjection(combinedContext);
|
|
291
|
+
const annotated = screen.flagged
|
|
292
|
+
? `${flagPrefix(screen.reason)}${combinedContext}`
|
|
293
|
+
: combinedContext;
|
|
294
|
+
(request.extra ||= {}).combinedContext = annotated;
|
|
295
|
+
// Clear individual issueBody so simple.ts uses combinedContext
|
|
296
|
+
// exclusively (avoids double-rendering the body).
|
|
297
|
+
request.issueBody = "";
|
|
298
|
+
if (screen.flagged) {
|
|
299
|
+
console.warn(`[dispatch] Screener flagged combined issue context for ${owner}/${repo}#${request.issueNumber}: ${screen.reason || "no reason"}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
catch (err) {
|
|
304
|
+
const m = err instanceof Error ? err.message : String(err);
|
|
305
|
+
console.warn(`[dispatch] Failed to fetch/screen issue context: ${m}`);
|
|
306
|
+
// Non-fatal — workflow proceeds with whatever context the envelope had.
|
|
307
|
+
}
|
|
308
|
+
}
|
|
116
309
|
const slackPost = slackTriggerId && slackConnector && typeof channelId === "string" && typeof threadId === "string"
|
|
117
310
|
? async (msg) => {
|
|
118
311
|
try {
|
|
@@ -124,7 +317,84 @@ async function main() {
|
|
|
124
317
|
}
|
|
125
318
|
}
|
|
126
319
|
: undefined;
|
|
320
|
+
// In-place "task list" progress checklist — opt-in per workflow via
|
|
321
|
+
// `status_checklist: true` in the YAML. Build a transport for whichever
|
|
322
|
+
// surface triggered the run (GitHub comment and/or Slack thread) and hand
|
|
323
|
+
// the runner a ProgressNotifier instead of letting it post a comment per
|
|
324
|
+
// phase. The notifier is created inside onRunStart because it needs the
|
|
325
|
+
// workflow-run id (only known once simple.ts creates the row) to persist
|
|
326
|
+
// its in-place update handles to scratch.notifier. better-sqlite3 is
|
|
327
|
+
// synchronous and simple.ts invokes onRunStart synchronously before the
|
|
328
|
+
// first reporter call, so the notifier is ready in time; the proxy guards
|
|
329
|
+
// the brief window before assignment.
|
|
330
|
+
// Which in-place surface(s) can the checklist edit? Knowable synchronously
|
|
331
|
+
// (transport existence needs only github/issue or slack/channel/thread —
|
|
332
|
+
// the run id is needed solely for persistence + resume handles).
|
|
333
|
+
const ghChecklist = !!(github && typeof issueNumber === "number");
|
|
334
|
+
const slackChecklist = !!(slackConnector && typeof channelId === "string" && typeof threadId === "string");
|
|
335
|
+
let statusChecklist = false;
|
|
336
|
+
try {
|
|
337
|
+
// Only activate the checklist when the workflow opts in AND there's a
|
|
338
|
+
// surface to render it on — otherwise leave `reporter` undefined so the
|
|
339
|
+
// runner keeps its legacy per-phase comment behavior instead of going
|
|
340
|
+
// silent.
|
|
341
|
+
statusChecklist =
|
|
342
|
+
getWorkflow(workflowName).status_checklist === true && (ghChecklist || slackChecklist);
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
/* unknown workflow — surfaced downstream by runSimpleWorkflow */
|
|
346
|
+
}
|
|
347
|
+
let notifier;
|
|
348
|
+
const reporterProxy = statusChecklist
|
|
349
|
+
? {
|
|
350
|
+
start: (m) => notifier?.start(m) ?? Promise.resolve(),
|
|
351
|
+
step: (k, s, d) => notifier?.step(k, s, d) ?? Promise.resolve(),
|
|
352
|
+
insertStep: (st, b) => notifier?.insertStep(st, b) ?? Promise.resolve(),
|
|
353
|
+
note: (m) => notifier?.note(m) ?? Promise.resolve(),
|
|
354
|
+
footer: (m) => notifier?.footer(m) ?? Promise.resolve(),
|
|
355
|
+
noteTerminal: (m) => notifier?.noteTerminal(m) ?? Promise.resolve(),
|
|
356
|
+
}
|
|
357
|
+
: undefined;
|
|
358
|
+
const notifierOnRunStart = statusChecklist
|
|
359
|
+
? (runId) => {
|
|
360
|
+
try {
|
|
361
|
+
const saved = ((db.runs.getRun(runId)?.scratch?.notifier) ?? {});
|
|
362
|
+
const persist = (patch) => {
|
|
363
|
+
const cur = ((db.runs.getRun(runId)?.scratch?.notifier) ?? {});
|
|
364
|
+
db.runs.mergeScratch(runId, { notifier: { ...cur, ...patch } });
|
|
365
|
+
};
|
|
366
|
+
const transports = [];
|
|
367
|
+
if (ghChecklist && github && typeof issueNumber === "number") {
|
|
368
|
+
transports.push(new GitHubTransport({
|
|
369
|
+
github,
|
|
370
|
+
owner,
|
|
371
|
+
repo,
|
|
372
|
+
issueNumber,
|
|
373
|
+
commentId: saved.githubCommentId,
|
|
374
|
+
save: (id) => persist({ githubCommentId: id }),
|
|
375
|
+
}));
|
|
376
|
+
}
|
|
377
|
+
if (slackChecklist && slackConnector && typeof channelId === "string" && typeof threadId === "string") {
|
|
378
|
+
transports.push(new SlackTransport({
|
|
379
|
+
slack: slackConnector,
|
|
380
|
+
channel: channelId,
|
|
381
|
+
thread: threadId,
|
|
382
|
+
ts: saved.slackTs,
|
|
383
|
+
save: (ts) => persist({ slackTs: ts, slackChannel: channelId, slackThread: threadId }),
|
|
384
|
+
}));
|
|
385
|
+
}
|
|
386
|
+
if (transports.length > 0)
|
|
387
|
+
notifier = new ProgressNotifier(transports);
|
|
388
|
+
}
|
|
389
|
+
catch (err) {
|
|
390
|
+
const m = err instanceof Error ? err.message : String(err);
|
|
391
|
+
console.warn(`[dispatch] notifier setup failed: ${m}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
: undefined;
|
|
127
395
|
const callbacks = {
|
|
396
|
+
reporter: reporterProxy,
|
|
397
|
+
publicUrl: config.publicUrl,
|
|
128
398
|
postComment: slackPost
|
|
129
399
|
?? (github && issueNumber
|
|
130
400
|
? async (msg) => {
|
|
@@ -147,15 +417,29 @@ async function main() {
|
|
|
147
417
|
}
|
|
148
418
|
},
|
|
149
419
|
onPhaseEnd: async (phase, result) => console.log(`[dispatch] ◀ ${workflowName}/${phase}: ${result.success ? "OK" : "FAILED"}`),
|
|
420
|
+
onRunStart: notifierOnRunStart
|
|
421
|
+
? async (runId) => {
|
|
422
|
+
// Synchronous notifier setup must finish before simple.ts calls
|
|
423
|
+
// reporter.start() (the next statement after it invokes this), so
|
|
424
|
+
// run it first, then chain any caller-provided onRunStart.
|
|
425
|
+
notifierOnRunStart(runId);
|
|
426
|
+
if (onRunStart)
|
|
427
|
+
await onRunStart(runId);
|
|
428
|
+
}
|
|
429
|
+
: onRunStart,
|
|
150
430
|
};
|
|
151
431
|
try {
|
|
152
432
|
const result = await runSimpleWorkflow(workflowName, request, {
|
|
153
|
-
mcpConfigPath,
|
|
154
433
|
model: config.model,
|
|
155
434
|
maxTurns: config.maxTurns,
|
|
156
435
|
stateDir: config.stateDir,
|
|
157
436
|
sandboxDir: config.sandboxDir,
|
|
158
|
-
|
|
437
|
+
sessionsDir: config.sessionsDir,
|
|
438
|
+
sandbox: config.sandbox,
|
|
439
|
+
buildAssets: config.buildAssets,
|
|
440
|
+
buildAssetsDir: config.buildAssetsDir,
|
|
441
|
+
otel: config.otel,
|
|
442
|
+
}, callbacks, db, config.models, config.approval, config.bootstrapLabel, config.variants);
|
|
159
443
|
const summary = result.phases.map((p) => `${p.phase}=${p.success ? "ok" : "fail"}`).join(", ");
|
|
160
444
|
if (result.paused) {
|
|
161
445
|
console.log(`[dispatch] ${workflowName} paused (${summary})`);
|
|
@@ -176,8 +460,6 @@ async function main() {
|
|
|
176
460
|
};
|
|
177
461
|
// Set up connector registry
|
|
178
462
|
const registry = new ConnectorRegistry();
|
|
179
|
-
// Session manager for messaging connectors (shared across Slack, Discord, etc.)
|
|
180
|
-
const sessionManager = new SessionManager(db.database);
|
|
181
463
|
// Message delivery service for cron output
|
|
182
464
|
const delivery = new MessageDeliveryService();
|
|
183
465
|
// GitHub webhook connector (optional — requires both webhook secret and GitHub App)
|
|
@@ -206,34 +488,59 @@ async function main() {
|
|
|
206
488
|
delivery.register("slack", (msg) => slackConnector.sendToDeliveryChannel(msg));
|
|
207
489
|
}
|
|
208
490
|
}
|
|
491
|
+
// Construct the cron scheduler before mounting admin so the dashboard can
|
|
492
|
+
// list/toggle/edit registered cron jobs. Jobs are registered further down
|
|
493
|
+
// (after we know whether webhooks are enabled). The runner closes over
|
|
494
|
+
// `dispatchWorkflow`, which is defined earlier in this file.
|
|
495
|
+
const cron = new CronScheduler(db, async (workflowName, context) => {
|
|
496
|
+
const { dispatched, failures } = await dispatchCronWorkflow(workflowName, context, dispatchWorkflow);
|
|
497
|
+
if (failures > 0) {
|
|
498
|
+
console.warn(`[cron] ${workflowName}: ${failures}/${dispatched} dispatches failed`);
|
|
499
|
+
}
|
|
500
|
+
});
|
|
209
501
|
// Mount admin dashboard (needs an HTTP server — use GitHub connector or create standalone)
|
|
210
502
|
if (githubConnector) {
|
|
211
503
|
mountAdmin(githubConnector.honoApp, db, {
|
|
504
|
+
cronScheduler: cron,
|
|
212
505
|
stateDir: config.stateDir,
|
|
213
|
-
sessionsDir:
|
|
506
|
+
sessionsDir: config.sessionsDir,
|
|
507
|
+
buildAssetsDir: config.buildAssetsDir,
|
|
508
|
+
buildAssets: config.buildAssets,
|
|
214
509
|
adminPassword: process.env.ADMIN_PASSWORD ?? "",
|
|
215
510
|
adminSecret: process.env.ADMIN_SECRET ?? "lastlight-dev-secret",
|
|
511
|
+
publicConfig: config.publicConfig,
|
|
216
512
|
slackOAuthClientId: process.env.SLACK_OAUTH_CLIENT_ID,
|
|
217
513
|
slackOAuthClientSecret: process.env.SLACK_OAUTH_CLIENT_SECRET,
|
|
218
514
|
slackOAuthRedirectUri: process.env.SLACK_OAUTH_REDIRECT_URI,
|
|
219
515
|
slackAllowedWorkspace: process.env.SLACK_ALLOWED_WORKSPACE,
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
516
|
+
githubOAuthClientId: process.env.GITHUB_OAUTH_CLIENT_ID,
|
|
517
|
+
githubOAuthClientSecret: process.env.GITHUB_OAUTH_CLIENT_SECRET,
|
|
518
|
+
githubOAuthRedirectUri: process.env.GITHUB_OAUTH_REDIRECT_URI,
|
|
519
|
+
githubAllowedOrg: process.env.GITHUB_ALLOWED_ORG,
|
|
223
520
|
resumeWorkflow: async (workflowRun, sender) => {
|
|
224
521
|
if (!github) {
|
|
225
522
|
console.warn(`[admin] Cannot resume workflow ${workflowRun.id}: GitHub App not configured`);
|
|
226
523
|
return;
|
|
227
524
|
}
|
|
228
|
-
|
|
525
|
+
// Derive owner/repo for the resume dispatch. Prefer the triggerId
|
|
526
|
+
// (owner/repo#N) but fall back to the stored repo + context.owner so a
|
|
527
|
+
// run keyed on a non-GitHub triggerId (e.g. a Slack-thread override)
|
|
528
|
+
// still resumes from the dashboard/focused-approval flow.
|
|
529
|
+
let [owner, repo] = workflowRun.triggerId.includes("/")
|
|
229
530
|
? workflowRun.triggerId.replace(/#\d+$/, "").split("/")
|
|
230
531
|
: ["", ""];
|
|
532
|
+
if (!owner || !repo) {
|
|
533
|
+
const ctxOwner = workflowRun.context?.owner || "";
|
|
534
|
+
const storedRepo = workflowRun.repo || "";
|
|
535
|
+
owner = ctxOwner || (storedRepo.includes("/") ? storedRepo.split("/")[0] : "");
|
|
536
|
+
repo = storedRepo.includes("/") ? storedRepo.split("/")[1] : storedRepo;
|
|
537
|
+
}
|
|
231
538
|
const issueNumber = workflowRun.issueNumber;
|
|
232
539
|
if (!owner || !repo || !issueNumber) {
|
|
233
540
|
console.warn(`[admin] Cannot resume workflow ${workflowRun.id}: missing owner/repo/issueNumber`);
|
|
234
541
|
return;
|
|
235
542
|
}
|
|
236
|
-
db.
|
|
543
|
+
db.runs.setRunning(workflowRun.id);
|
|
237
544
|
console.log(`[admin] Resuming ${workflowRun.workflowName} for ${owner}/${repo}#${issueNumber} after dashboard approval by ${sender}`);
|
|
238
545
|
dispatchWorkflow(workflowRun.workflowName, {
|
|
239
546
|
repo: `${owner}/${repo}`,
|
|
@@ -312,429 +619,91 @@ async function main() {
|
|
|
312
619
|
return c.json({ accepted: true, owner, repo, issueNumber }, 202);
|
|
313
620
|
});
|
|
314
621
|
// Handle events from any connector
|
|
622
|
+
// Handle events from any connector. The dispatcher turns each EventEnvelope
|
|
623
|
+
// into a workflow dispatch (or in-process handler run) through one testable
|
|
624
|
+
// seam; every per-event branch lives in src/engine/dispatcher.ts. main()
|
|
625
|
+
// only constructs the deps and relays the typed outcome.
|
|
626
|
+
const dispatchDeps = {
|
|
627
|
+
db,
|
|
628
|
+
github,
|
|
629
|
+
dispatchWorkflow,
|
|
630
|
+
sessionManager,
|
|
631
|
+
// One in-process chat turn. handleChatMessage manages session resume via
|
|
632
|
+
// sessionManager internally; the dispatcher uses resumeAgentSessionId only
|
|
633
|
+
// to decide whether to persist a newly-minted agent session id.
|
|
634
|
+
runChat: (message, messagingSessionId, sender) => handleChatMessage(message, messagingSessionId, sender, sessionManager, { chatRunner, sessionsHomeDir: config.sessionsDir }, { model: resolveModel(config.models, "chat"), maxTurns: 10 }),
|
|
635
|
+
reviewPostsCheck: config.reviewPostsCheck,
|
|
636
|
+
publicUrl: config.publicUrl,
|
|
637
|
+
};
|
|
638
|
+
// One chat turn over HTTP — `lastlight chat` without a messaging platform.
|
|
639
|
+
// Routes through the SAME dispatcher seam Slack uses (forcing the chat
|
|
640
|
+
// handler), so the executions row, agent-session resume, and telemetry are
|
|
641
|
+
// recorded identically and the turn shows up in the dashboard Chat tab. The
|
|
642
|
+
// synthetic envelope's reply() just captures the assistant text to return.
|
|
643
|
+
githubConnector?.honoApp.post("/api/chat", async (c) => {
|
|
644
|
+
const body = await c.req.json().catch(() => ({}));
|
|
645
|
+
const message = typeof body.message === "string" ? body.message : "";
|
|
646
|
+
if (!message.trim())
|
|
647
|
+
return c.json({ error: "Missing 'message'" }, 400);
|
|
648
|
+
const user = typeof body.user === "string" && body.user ? body.user : "cli";
|
|
649
|
+
const threadId = typeof body.thread === "string" && body.thread ? body.thread : null;
|
|
650
|
+
const session = sessionManager.getOrCreateSession({
|
|
651
|
+
platform: "cli", channelId: user, threadId, userId: user,
|
|
652
|
+
});
|
|
653
|
+
let reply = "";
|
|
654
|
+
const envelope = {
|
|
655
|
+
id: session.id,
|
|
656
|
+
source: "cli",
|
|
657
|
+
type: "message",
|
|
658
|
+
sender: user,
|
|
659
|
+
senderIsBot: false,
|
|
660
|
+
body: message,
|
|
661
|
+
raw: { cli: true },
|
|
662
|
+
reply: async (msg) => { reply = msg; },
|
|
663
|
+
timestamp: new Date(),
|
|
664
|
+
};
|
|
665
|
+
const outcome = await dispatch(envelope, {
|
|
666
|
+
...dispatchDeps,
|
|
667
|
+
route: async () => ({
|
|
668
|
+
action: "handler",
|
|
669
|
+
handler: "chat",
|
|
670
|
+
context: { sessionId: session.id, message, sender: user },
|
|
671
|
+
}),
|
|
672
|
+
});
|
|
673
|
+
return c.json({ text: reply, thread: threadId ?? session.id, sessionId: session.id, outcome: outcome.kind });
|
|
674
|
+
});
|
|
315
675
|
registry.onEvent(async (envelope) => {
|
|
316
676
|
console.log(`[event] ${envelope.source}:${envelope.type} from ${envelope.sender}${envelope.repo ? ` on ${envelope.repo}` : ""}`);
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}
|
|
322
|
-
if (route.action === "reply") {
|
|
323
|
-
await envelope.reply(route.message);
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
const { skill, context } = route;
|
|
327
|
-
// Chat messages: handle directly (no sandbox, low latency)
|
|
328
|
-
if (skill === "chat") {
|
|
329
|
-
const messagingSessionId = context.sessionId;
|
|
330
|
-
const message = context.message;
|
|
331
|
-
const sender = context.sender;
|
|
332
|
-
// Look up the existing Agent SDK session id for this Slack thread.
|
|
333
|
-
// First message has none → fresh session; subsequent messages resume.
|
|
334
|
-
const messagingSession = sessionManager.getSession(messagingSessionId);
|
|
335
|
-
const resumeAgentSessionId = messagingSession?.agentSessionId ?? undefined;
|
|
336
|
-
// Record an executions row so chat usage shows up in dashboard stats
|
|
337
|
-
// alongside sandbox runs. triggerId is the messaging-session id, so a
|
|
338
|
-
// whole Slack thread groups together with `GROUP BY trigger_id`.
|
|
339
|
-
const executionId = randomUUID();
|
|
340
|
-
db.recordStart({
|
|
341
|
-
id: executionId,
|
|
342
|
-
triggerType: "chat",
|
|
343
|
-
triggerId: messagingSessionId,
|
|
344
|
-
skill: "chat",
|
|
345
|
-
startedAt: new Date().toISOString(),
|
|
346
|
-
});
|
|
347
|
-
try {
|
|
348
|
-
const result = await handleChatMessage(message, messagingSessionId, sender, sessionManager, {
|
|
349
|
-
mcpConfigPath,
|
|
350
|
-
model: resolveModel(config.models, "chat"),
|
|
351
|
-
maxTurns: 10,
|
|
352
|
-
}, resumeAgentSessionId);
|
|
353
|
-
// Persist the Agent SDK session id on the first turn so the next
|
|
354
|
-
// turn in this thread can resume into the same jsonl. We always
|
|
355
|
-
// overwrite — if the SDK rotated the id (e.g. resume failed and it
|
|
356
|
-
// started a fresh session) we want the latest one.
|
|
357
|
-
if (result.agentSessionId && result.agentSessionId !== resumeAgentSessionId) {
|
|
358
|
-
sessionManager.setAgentSessionId(messagingSessionId, result.agentSessionId);
|
|
359
|
-
}
|
|
360
|
-
db.recordFinish(executionId, {
|
|
361
|
-
success: result.success,
|
|
362
|
-
error: result.error,
|
|
363
|
-
turns: result.turns,
|
|
364
|
-
durationMs: result.durationMs,
|
|
365
|
-
sessionId: result.agentSessionId,
|
|
366
|
-
costUsd: result.costUsd,
|
|
367
|
-
inputTokens: result.inputTokens,
|
|
368
|
-
cacheCreationInputTokens: result.cacheCreationInputTokens,
|
|
369
|
-
cacheReadInputTokens: result.cacheReadInputTokens,
|
|
370
|
-
outputTokens: result.outputTokens,
|
|
371
|
-
apiDurationMs: result.apiDurationMs,
|
|
372
|
-
stopReason: result.stopReason,
|
|
373
|
-
});
|
|
374
|
-
await envelope.reply(result.text);
|
|
375
|
-
}
|
|
376
|
-
catch (err) {
|
|
377
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
378
|
-
console.error(`[event] Chat error:`, msg);
|
|
379
|
-
db.recordFinish(executionId, {
|
|
380
|
-
success: false,
|
|
381
|
-
error: msg,
|
|
382
|
-
durationMs: 0,
|
|
383
|
-
});
|
|
384
|
-
await envelope.reply("Sorry, I encountered an error. Please try again.");
|
|
385
|
-
}
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
// Chat reset: deactivate the session and confirm
|
|
389
|
-
if (skill === "chat-reset") {
|
|
390
|
-
const sessionId = context.sessionId;
|
|
391
|
-
if (sessionId) {
|
|
392
|
-
sessionManager.deactivateSession(sessionId);
|
|
393
|
-
}
|
|
394
|
-
await envelope.reply("Session reset. Starting fresh.");
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
// Status report: return running executions
|
|
398
|
-
if (skill === "status-report") {
|
|
399
|
-
const running = db.runningExecutions();
|
|
400
|
-
if (running.length === 0) {
|
|
401
|
-
await envelope.reply("No tasks currently running.");
|
|
402
|
-
}
|
|
403
|
-
else {
|
|
404
|
-
const lines = running.map((r) => `• *${r.skill}*${r.repo ? ` on ${r.repo}` : ""}${r.issueNumber ? ` #${r.issueNumber}` : ""} (started ${r.startedAt})`);
|
|
405
|
-
await envelope.reply(`Running tasks:\n${lines.join("\n")}`);
|
|
406
|
-
}
|
|
407
|
-
return;
|
|
408
|
-
}
|
|
409
|
-
// Check if already running for this trigger
|
|
410
|
-
const triggerId = String(envelope.issueNumber || envelope.id);
|
|
411
|
-
if (db.isRunning(skill, triggerId)) {
|
|
412
|
-
console.log(`[event] Skipping: ${skill} already running for ${triggerId}`);
|
|
413
|
-
// Notify messaging users that the task is already in progress
|
|
414
|
-
if (envelope.type === "message") {
|
|
415
|
-
await envelope.reply(`That task is already running. Use /status to check progress.`);
|
|
416
|
-
}
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
// PR fix: lightweight fix-and-push, no full build cycle
|
|
420
|
-
if (skill === "pr-fix" && context.prNumber && context.repo) {
|
|
421
|
-
const repoStr = context.repo;
|
|
422
|
-
const [owner, repo] = repoStr.includes("/") ? repoStr.split("/") : ["", repoStr];
|
|
423
|
-
const prNumber = context.prNumber;
|
|
424
|
-
if (!owner || !repo) {
|
|
425
|
-
console.error(`[event] Invalid repo format: ${repoStr}`);
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
// Fetch PR details and CI failures
|
|
429
|
-
let prTitle = context.title || "";
|
|
430
|
-
let prBody = context.body || "";
|
|
431
|
-
let branch = "";
|
|
432
|
-
let failedChecks = "";
|
|
433
|
-
if (github) {
|
|
434
|
-
try {
|
|
435
|
-
const pr = await github.getPullRequest(owner, repo, prNumber);
|
|
436
|
-
prTitle = prTitle || pr.title;
|
|
437
|
-
prBody = prBody || pr.body || "";
|
|
438
|
-
branch = pr.head.ref;
|
|
439
|
-
// Fetch CI failures for the PR's head commit
|
|
440
|
-
failedChecks = await github.getFailedChecks(owner, repo, pr.head.sha);
|
|
441
|
-
}
|
|
442
|
-
catch (err) {
|
|
443
|
-
console.warn(`[event] Could not fetch PR: ${err.message}`);
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
if (!branch) {
|
|
447
|
-
console.error(`[event] Could not determine branch for PR #${prNumber}`);
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
450
|
-
console.log(`[event] PR fix for ${repoStr}#${prNumber} on branch ${branch}`);
|
|
451
|
-
const ciSection = failedChecks && !failedChecks.includes("No failed checks")
|
|
452
|
-
? `CI FAILURES (from GitHub Actions — fix these first):\n${failedChecks}`
|
|
453
|
-
: "";
|
|
454
|
-
dispatchWorkflow("pr-fix", {
|
|
455
|
-
repo: repoStr,
|
|
456
|
-
prNumber,
|
|
457
|
-
title: prTitle,
|
|
458
|
-
body: prBody,
|
|
459
|
-
commentBody: context.commentBody || "",
|
|
460
|
-
sender: context.sender || "unknown",
|
|
461
|
-
branch,
|
|
462
|
-
failedChecks,
|
|
463
|
-
ciSection,
|
|
464
|
-
_triggerType: "webhook",
|
|
465
|
-
}).catch((err) => {
|
|
466
|
-
console.error(`[event] PR fix failed:`, err);
|
|
467
|
-
});
|
|
468
|
-
return;
|
|
469
|
-
}
|
|
470
|
-
// Explore reply: free-form user reply on a paused socratic explore
|
|
471
|
-
// run. Resolve the reply gate with the message body, merge it into
|
|
472
|
-
// scratch.socratic.qa so the next iteration sees the answer, and
|
|
473
|
-
// re-dispatch the workflow to continue the loop.
|
|
474
|
-
if (skill === "explore-reply") {
|
|
475
|
-
const workflowRunId = context.workflowRunId;
|
|
476
|
-
const replyText = context.reply || "";
|
|
477
|
-
const sender = context.sender || "unknown";
|
|
478
|
-
const run = db.getWorkflowRun(workflowRunId);
|
|
479
|
-
if (!run) {
|
|
480
|
-
console.warn(`[event] explore-reply: run ${workflowRunId} not found`);
|
|
481
|
-
return;
|
|
482
|
-
}
|
|
483
|
-
const pending = db.getPendingApprovalForWorkflow(workflowRunId);
|
|
484
|
-
if (!pending || pending.kind !== "reply") {
|
|
485
|
-
console.warn(`[event] explore-reply: no pending reply gate on ${workflowRunId}`);
|
|
486
|
-
return;
|
|
487
|
-
}
|
|
488
|
-
db.resolveReplyGate(pending.id, replyText, sender);
|
|
489
|
-
// Append the QA entry to scratch.socratic.qa. The runner reads this
|
|
490
|
-
// via {{scratch.socratic.qa}} on the next iteration.
|
|
491
|
-
const prevScratch = (run.scratch || {});
|
|
492
|
-
const prevSocratic = (prevScratch.socratic || {});
|
|
493
|
-
const qaList = Array.isArray(prevSocratic.qa) ? [...prevSocratic.qa] : [];
|
|
494
|
-
qaList.push({
|
|
495
|
-
question: prevSocratic.lastOutput ?? "",
|
|
496
|
-
answer: replyText,
|
|
497
|
-
sender,
|
|
498
|
-
at: new Date().toISOString(),
|
|
499
|
-
});
|
|
500
|
-
db.updateWorkflowRunScratch(workflowRunId, {
|
|
501
|
-
socratic: { ...prevSocratic, qa: qaList },
|
|
502
|
-
});
|
|
503
|
-
// Set currentPhase to the phase BEFORE the loop owner so the
|
|
504
|
-
// runner's nextPhaseAfter lands back on the loop phase for the
|
|
505
|
-
// next iteration. Walk the workflow definition to find it.
|
|
506
|
-
try {
|
|
507
|
-
const { getWorkflow } = await import("./workflows/loader.js");
|
|
508
|
-
const def = getWorkflow(run.workflowName);
|
|
509
|
-
// Find the phase that owns this gate (pattern: socratic_iter_N)
|
|
510
|
-
const gateParts = pending.gate.match(/^(.+)_iter_\d+$/);
|
|
511
|
-
const owningPhaseName = gateParts ? gateParts[1] : null;
|
|
512
|
-
if (owningPhaseName) {
|
|
513
|
-
const ownIdx = def.phases.findIndex((p) => p.name === owningPhaseName);
|
|
514
|
-
const priorPhase = ownIdx > 0 ? def.phases[ownIdx - 1].name : owningPhaseName;
|
|
515
|
-
db.updateWorkflowPhase(workflowRunId, priorPhase, {
|
|
516
|
-
phase: priorPhase,
|
|
517
|
-
timestamp: new Date().toISOString(),
|
|
518
|
-
success: true,
|
|
519
|
-
summary: `Resumed after reply on gate: ${pending.gate}`,
|
|
520
|
-
});
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
catch (err) {
|
|
524
|
-
console.warn(`[event] explore-reply: could not resolve owning phase:`, err);
|
|
525
|
-
}
|
|
526
|
-
db.resumeWorkflowRun(workflowRunId);
|
|
527
|
-
// Re-dispatch. Use channelId/threadId from the current event context
|
|
528
|
-
// (the router captured them from the reply envelope), not from stored
|
|
529
|
-
// workflow context — they were never persisted there.
|
|
530
|
-
const isSlack = run.triggerId.startsWith("slack:");
|
|
531
|
-
const replyChannelId = context.channelId;
|
|
532
|
-
const replyThreadId = context.threadId;
|
|
533
|
-
// Reconstruct owner/repo from the stored workflow context.
|
|
534
|
-
const storedCtx = (run.context || {});
|
|
535
|
-
const storedOwner = storedCtx.owner;
|
|
536
|
-
const resumeRepo = storedOwner && run.repo
|
|
537
|
-
? `${storedOwner}/${run.repo}`
|
|
538
|
-
: run.repo || undefined;
|
|
539
|
-
console.log(`[event] explore-reply: resuming ${workflowRunId} after reply from ${sender}`);
|
|
540
|
-
dispatchWorkflow("explore", {
|
|
541
|
-
repo: resumeRepo || (isSlack ? undefined : run.triggerId.split("#")[0]),
|
|
542
|
-
issueNumber: run.issueNumber,
|
|
543
|
-
sender,
|
|
544
|
-
_triggerType: envelope.type === "message" ? "chat" : "webhook",
|
|
545
|
-
triggerId: isSlack ? run.triggerId : undefined,
|
|
546
|
-
channelId: replyChannelId,
|
|
547
|
-
threadId: replyThreadId,
|
|
548
|
-
}).catch((err) => console.error(`[event] explore-reply resume failed:`, err));
|
|
549
|
-
return;
|
|
550
|
-
}
|
|
551
|
-
// Approval responses
|
|
552
|
-
if (skill === "approval-response") {
|
|
553
|
-
const decision = context.decision;
|
|
554
|
-
const sender = context.sender || "unknown";
|
|
555
|
-
const reason = context.reason;
|
|
556
|
-
const triggerId = context.repo && context.issueNumber
|
|
557
|
-
? `${context.repo}#${context.issueNumber}`
|
|
558
|
-
: undefined;
|
|
559
|
-
const approval = context.workflowRunId
|
|
560
|
-
? db.getPendingApprovalForWorkflow(context.workflowRunId)
|
|
561
|
-
: triggerId
|
|
562
|
-
? db.getPendingApprovalByTrigger(triggerId)
|
|
563
|
-
: null;
|
|
564
|
-
if (!approval) {
|
|
565
|
-
await envelope.reply("No pending approval found.");
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
db.respondToApproval(approval.id, decision, sender, reason);
|
|
569
|
-
if (decision === "approved") {
|
|
570
|
-
// Re-trigger the build cycle — resume logic in orchestrator will pick up from DB state
|
|
571
|
-
const workflowRun = db.getWorkflowRun(approval.workflowRunId);
|
|
572
|
-
if (workflowRun && !github) {
|
|
573
|
-
await envelope.reply("Approval recorded, but cannot resume: GitHub App is not configured. Configure GITHUB_APP_ID and related env vars to enable build resumption.");
|
|
574
|
-
return;
|
|
575
|
-
}
|
|
576
|
-
if (workflowRun && github) {
|
|
577
|
-
await envelope.reply(`Approved by ${sender}. Resuming \`${workflowRun.workflowName}\`...`);
|
|
578
|
-
const [owner, repo] = workflowRun.triggerId.includes("/")
|
|
579
|
-
? workflowRun.triggerId.replace(/#\d+$/, "").split("/")
|
|
580
|
-
: ["", ""];
|
|
581
|
-
const issueNumber = workflowRun.issueNumber;
|
|
582
|
-
if (owner && repo && issueNumber) {
|
|
583
|
-
db.resumeWorkflowRun(workflowRun.id);
|
|
584
|
-
dispatchWorkflow(workflowRun.workflowName, {
|
|
585
|
-
repo: `${owner}/${repo}`,
|
|
586
|
-
issueNumber,
|
|
587
|
-
title: `Issue #${issueNumber}`,
|
|
588
|
-
body: "",
|
|
589
|
-
sender,
|
|
590
|
-
_triggerType: "approval",
|
|
591
|
-
}).catch((err) => console.error(`[approval] Resume failed:`, err));
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
else {
|
|
596
|
-
const workflowRun = db.getWorkflowRun(approval.workflowRunId);
|
|
597
|
-
if (workflowRun) {
|
|
598
|
-
db.finishWorkflowRun(approval.workflowRunId, "failed", `Rejected by ${sender}: ${reason || "no reason given"}`);
|
|
599
|
-
}
|
|
600
|
-
await envelope.reply(`Rejected by ${sender}. Build cycle aborted.${reason ? ` Reason: ${reason}` : ""}`);
|
|
601
|
-
}
|
|
602
|
-
return;
|
|
603
|
-
}
|
|
604
|
-
// Build requests: route to the programmatic orchestrator instead of the SKILL.md
|
|
605
|
-
if (skill === "github-orchestrator" && context.issueNumber && context.repo) {
|
|
606
|
-
const repoStr = context.repo;
|
|
607
|
-
const [owner, repo] = repoStr.includes("/") ? repoStr.split("/") : ["", repoStr];
|
|
608
|
-
const issueNumber = context.issueNumber;
|
|
609
|
-
if (!owner || !repo) {
|
|
610
|
-
console.error(`[event] Invalid repo format: ${repoStr}`);
|
|
611
|
-
return;
|
|
612
|
-
}
|
|
613
|
-
// Fetch full issue details if we don't have them
|
|
614
|
-
let issueTitle = context.title || "";
|
|
615
|
-
let issueBody = context.body || "";
|
|
616
|
-
let issueLabels = context.labels || [];
|
|
617
|
-
if (github && (!issueTitle || !issueBody || issueLabels.length === 0)) {
|
|
618
|
-
try {
|
|
619
|
-
const issue = await github.getIssue(owner, repo, issueNumber);
|
|
620
|
-
issueTitle = issueTitle || issue.title;
|
|
621
|
-
issueBody = issueBody || issue.body || "";
|
|
622
|
-
if (issueLabels.length === 0) {
|
|
623
|
-
issueLabels = (issue.labels || []).map((l) => typeof l === "string" ? l : l.name).filter(Boolean);
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
catch (err) {
|
|
627
|
-
console.warn(`[event] Could not fetch issue: ${err.message}`);
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
const executionId = randomUUID();
|
|
631
|
-
db.recordStart({
|
|
632
|
-
id: executionId,
|
|
633
|
-
triggerType: envelope.type === "message" ? "chat" : "webhook",
|
|
634
|
-
triggerId: String(issueNumber),
|
|
635
|
-
skill: "build-cycle",
|
|
636
|
-
repo: repoStr,
|
|
637
|
-
issueNumber,
|
|
638
|
-
startedAt: new Date().toISOString(),
|
|
639
|
-
});
|
|
640
|
-
if (envelope.type === "message") {
|
|
641
|
-
await envelope.reply(`Starting build cycle for ${repoStr}#${issueNumber}...`);
|
|
677
|
+
try {
|
|
678
|
+
const outcome = await dispatch(envelope, dispatchDeps);
|
|
679
|
+
if (outcome.kind === "ignored") {
|
|
680
|
+
console.log(`[event] Ignored: ${outcome.reason}`);
|
|
642
681
|
}
|
|
643
|
-
else if (
|
|
644
|
-
|
|
645
|
-
// the user sees an instant ack before guardrails / architect / etc.
|
|
646
|
-
// start running. Non-fatal if it fails.
|
|
647
|
-
const commentId = envelope.raw?.comment?.id;
|
|
648
|
-
if (commentId) {
|
|
649
|
-
github
|
|
650
|
-
.reactToComment(owner, repo, commentId, "rocket")
|
|
651
|
-
.catch((err) => {
|
|
652
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
653
|
-
console.warn(`[event] Could not react to trigger comment: ${msg}`);
|
|
654
|
-
});
|
|
655
|
-
}
|
|
682
|
+
else if (outcome.kind === "skipped") {
|
|
683
|
+
console.log(`[event] Skipped: ${outcome.reason}`);
|
|
656
684
|
}
|
|
657
|
-
dispatchWorkflow("build", {
|
|
658
|
-
repo: repoStr,
|
|
659
|
-
issueNumber,
|
|
660
|
-
title: issueTitle || `Issue #${issueNumber}`,
|
|
661
|
-
body: issueBody,
|
|
662
|
-
labels: issueLabels,
|
|
663
|
-
commentBody: context.commentBody,
|
|
664
|
-
sender: context.sender || "unknown",
|
|
665
|
-
_triggerType: envelope.type === "message" ? "chat" : "webhook",
|
|
666
|
-
}).then((result) => {
|
|
667
|
-
db.recordFinish(executionId, {
|
|
668
|
-
success: result.success,
|
|
669
|
-
error: result.success ? undefined : "Build cycle failed",
|
|
670
|
-
durationMs: 0,
|
|
671
|
-
});
|
|
672
|
-
if (envelope.type === "message") {
|
|
673
|
-
envelope.reply(result.success ? `Build cycle complete.` : `Build cycle failed.`);
|
|
674
|
-
}
|
|
675
|
-
}).catch((err) => {
|
|
676
|
-
console.error(`[event] Build cycle failed:`, err);
|
|
677
|
-
db.recordFinish(executionId, { success: false, error: err.message, durationMs: 0 });
|
|
678
|
-
});
|
|
679
|
-
return;
|
|
680
685
|
}
|
|
681
|
-
|
|
682
|
-
// The router still uses skill names — they map 1:1 to workflow YAML names
|
|
683
|
-
// for the four agent skills (issue-triage, pr-review, repo-health, issue-comment).
|
|
684
|
-
if (envelope.type === "message") {
|
|
685
|
-
await envelope.reply(`Starting *${skill}*... I'll report back when it's done.`);
|
|
686
|
-
dispatchWorkflow(skill, { ...context, _triggerType: "chat" }).then(async (result) => {
|
|
687
|
-
if (result.paused) {
|
|
688
|
-
// Workflow paused at a gate (approval or reply) — don't say
|
|
689
|
-
// "completed", the workflow itself already posted instructions.
|
|
690
|
-
}
|
|
691
|
-
else if (result.success) {
|
|
692
|
-
await envelope.reply(`*${skill}* completed.`);
|
|
693
|
-
}
|
|
694
|
-
else {
|
|
695
|
-
await envelope.reply(`*${skill}* failed${result.error ? `: ${result.error}` : ""}.`);
|
|
696
|
-
}
|
|
697
|
-
}).catch(async (err) => {
|
|
698
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
699
|
-
console.error(`[event] workflow ${skill} threw: ${msg}`);
|
|
700
|
-
await envelope.reply(`*${skill}* failed: ${msg}`);
|
|
701
|
-
});
|
|
702
|
-
return;
|
|
703
|
-
}
|
|
704
|
-
// Run workflow asynchronously (webhook triggers)
|
|
705
|
-
dispatchWorkflow(skill, { ...context, _triggerType: "webhook" }).catch((err) => {
|
|
686
|
+
catch (err) {
|
|
706
687
|
const msg = err instanceof Error ? err.message : String(err);
|
|
707
|
-
console.error(`[event]
|
|
708
|
-
}
|
|
709
|
-
});
|
|
710
|
-
// Set up cron scheduler — each cron tick dispatches an agent workflow by name
|
|
711
|
-
const cron = new CronScheduler(db, async (workflowName, context) => {
|
|
712
|
-
await dispatchWorkflow(workflowName, { ...context, _triggerType: "cron" });
|
|
688
|
+
console.error(`[event] dispatch threw: ${msg}`);
|
|
689
|
+
}
|
|
713
690
|
});
|
|
691
|
+
// Cron jobs — fan out from each tick into one workflow run per managed repo
|
|
692
|
+
// (see dispatchCronWorkflow). The scheduler itself was constructed earlier
|
|
693
|
+
// so the admin dashboard could be wired with it.
|
|
714
694
|
const webhooksEnabled = !!(config.webhookSecret && config.githubApp);
|
|
715
|
-
const jobs = getJobs({ webhooksEnabled });
|
|
695
|
+
const jobs = getJobs({ webhooksEnabled, db });
|
|
716
696
|
for (const job of jobs) {
|
|
717
697
|
cron.register(job);
|
|
718
698
|
}
|
|
719
699
|
if (webhooksEnabled) {
|
|
720
700
|
console.log("[cron] Webhooks enabled — skipping issue/PR polling crons");
|
|
721
701
|
}
|
|
722
|
-
// API usage/capacity checker — runs every 30 minutes, no sandbox needed.
|
|
723
|
-
// Passes a Slack notifier so the cron can alert the admin if the host
|
|
724
|
-
// claude CLI auth degrades (it then halts itself until cleared).
|
|
725
|
-
const { checkApiUsage } = await import("./cron/rate-limits.js");
|
|
726
|
-
const adminNotifier = slackConnector
|
|
727
|
-
? (msg) => slackConnector.sendToDeliveryChannel(msg)
|
|
728
|
-
: undefined;
|
|
729
|
-
cron.registerDirect({
|
|
730
|
-
name: "check-api-usage",
|
|
731
|
-
schedule: "*/30 * * * *",
|
|
732
|
-
handler: () => checkApiUsage(db, adminNotifier),
|
|
733
|
-
});
|
|
734
702
|
// Start everything
|
|
735
703
|
await registry.startAll();
|
|
736
704
|
console.log("[main] All connectors started");
|
|
737
705
|
console.log("[main] Cron jobs registered");
|
|
706
|
+
// Chat runs in-process via pi-ai — no long-lived server to boot.
|
|
738
707
|
// Boot-time recovery: any workflow_runs left in 'running' state from a
|
|
739
708
|
// previous harness lifetime have already had their sandbox containers
|
|
740
709
|
// killed by cleanupOrphanedSandboxes(). Mark their stale execution rows as
|
|
@@ -745,15 +714,21 @@ async function main() {
|
|
|
745
714
|
db,
|
|
746
715
|
github,
|
|
747
716
|
config: {
|
|
748
|
-
mcpConfigPath,
|
|
749
717
|
model: config.model,
|
|
750
718
|
maxTurns: config.maxTurns,
|
|
751
719
|
stateDir: config.stateDir,
|
|
752
720
|
sandboxDir: config.sandboxDir,
|
|
721
|
+
sessionsDir: config.sessionsDir,
|
|
722
|
+
sandbox: config.sandbox,
|
|
723
|
+
buildAssets: config.buildAssets,
|
|
724
|
+
buildAssetsDir: config.buildAssetsDir,
|
|
725
|
+
otel: config.otel,
|
|
753
726
|
},
|
|
754
727
|
models: config.models,
|
|
728
|
+
variants: config.variants,
|
|
755
729
|
approvalConfig: config.approval,
|
|
756
730
|
bootstrapLabel: config.bootstrapLabel,
|
|
731
|
+
publicUrl: config.publicUrl,
|
|
757
732
|
slackPoster: slackConnector
|
|
758
733
|
? (channelId, threadId, msg) => slackConnector.sendMessage(channelId, threadId, msg).then(() => { })
|
|
759
734
|
: undefined,
|
|
@@ -764,6 +739,10 @@ async function main() {
|
|
|
764
739
|
console.log("\n[main] Shutting down...");
|
|
765
740
|
cron.stopAll();
|
|
766
741
|
await registry.stopAll();
|
|
742
|
+
if (!telemetryShutdownStarted) {
|
|
743
|
+
telemetryShutdownStarted = true;
|
|
744
|
+
await shutdownTelemetry();
|
|
745
|
+
}
|
|
767
746
|
db.close();
|
|
768
747
|
process.exit(0);
|
|
769
748
|
};
|
|
@@ -772,6 +751,13 @@ async function main() {
|
|
|
772
751
|
}
|
|
773
752
|
main().catch((err) => {
|
|
774
753
|
console.error("[main] Fatal error:", err);
|
|
775
|
-
|
|
754
|
+
// Exit 78 (EX_CONFIG) to signal Docker restart policy that looping won't help.
|
|
755
|
+
// Common causes: bad PEM, wrong App ID, missing env vars.
|
|
756
|
+
const msg = err?.message || "";
|
|
757
|
+
const isConfig = msg.includes("could not be decoded") ||
|
|
758
|
+
msg.includes("not found") ||
|
|
759
|
+
msg.includes("ENOENT") ||
|
|
760
|
+
msg.includes("required");
|
|
761
|
+
process.exit(isConfig ? 78 : 1);
|
|
776
762
|
});
|
|
777
763
|
//# sourceMappingURL=index.js.map
|