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/index.js
CHANGED
|
@@ -1,20 +1,28 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync
|
|
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";
|
|
18
26
|
/**
|
|
19
27
|
* Pre-flight validation — checks that config is sane before starting any
|
|
20
28
|
* services. Exits with code 78 (EX_CONFIG) on configuration errors so
|
|
@@ -51,9 +59,32 @@ function validateConfig(config) {
|
|
|
51
59
|
async function main() {
|
|
52
60
|
console.log("Last Light v2.0 — Agent SDK Harness");
|
|
53
61
|
console.log("====================================");
|
|
54
|
-
// Load and validate config before starting anything
|
|
55
|
-
|
|
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
|
+
}
|
|
56
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");
|
|
57
88
|
console.log(`[config] Port: ${config.port}, Model: ${config.model}`);
|
|
58
89
|
const modelOverrides = Object.entries(config.models).filter(([k]) => k !== "default");
|
|
59
90
|
if (modelOverrides.length > 0) {
|
|
@@ -66,11 +97,56 @@ async function main() {
|
|
|
66
97
|
mkdirSync(resolve(config.stateDir, sub), { recursive: true });
|
|
67
98
|
}
|
|
68
99
|
console.log(`[state] State dir: ${config.stateDir}`);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
+
}
|
|
74
150
|
// Configure git with GitHub App credentials — agents can git clone/push natively.
|
|
75
151
|
// Non-fatal: the token is refreshed before each agent execution anyway, so a
|
|
76
152
|
// transient failure here (DNS, rate limit) doesn't block startup.
|
|
@@ -86,9 +162,6 @@ async function main() {
|
|
|
86
162
|
console.warn(`[git-auth] Initial token mint failed (will retry per-execution): ${err.message}`);
|
|
87
163
|
}
|
|
88
164
|
}
|
|
89
|
-
// Initialize state database
|
|
90
|
-
const db = new StateDb(config.dbPath);
|
|
91
|
-
console.log(`[state] Database: ${config.dbPath}`);
|
|
92
165
|
// GitHub API client for harness-level operations (posting comments, fetching issues)
|
|
93
166
|
const github = config.githubApp ? new GitHubClient(config.githubApp) : null;
|
|
94
167
|
/**
|
|
@@ -100,7 +173,7 @@ async function main() {
|
|
|
100
173
|
* The router still uses skill names for backwards compat — for the four
|
|
101
174
|
* agent skills they're 1:1 with workflow names.
|
|
102
175
|
*/
|
|
103
|
-
const dispatchWorkflow = async (workflowName, context) => {
|
|
176
|
+
const dispatchWorkflow = async (workflowName, context, onRunStart) => {
|
|
104
177
|
// Slack-initiated workflows (explore, /explore) carry a
|
|
105
178
|
// `slack:{team}:{channel}:{thread}` triggerId and don't require a
|
|
106
179
|
// managed `repo` — their postComment goes back to the Slack thread.
|
|
@@ -125,7 +198,7 @@ async function main() {
|
|
|
125
198
|
}
|
|
126
199
|
// Pluck the standard fields, leave the rest in `extra` for the workflow
|
|
127
200
|
// template to consume.
|
|
128
|
-
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;
|
|
129
202
|
// Preserve channelId/threadId in extra so they're stored on the
|
|
130
203
|
// workflow run context — needed by boot-time resume to rebuild the
|
|
131
204
|
// Slack postComment callback after a harness restart.
|
|
@@ -134,6 +207,35 @@ async function main() {
|
|
|
134
207
|
extra.channelId = channelId;
|
|
135
208
|
if (typeof threadId === "string")
|
|
136
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
|
+
}
|
|
137
239
|
const request = {
|
|
138
240
|
owner,
|
|
139
241
|
repo,
|
|
@@ -146,7 +248,64 @@ async function main() {
|
|
|
146
248
|
sender: typeof sender === "string" ? sender : "unknown",
|
|
147
249
|
triggerId: slackTriggerId,
|
|
148
250
|
extra,
|
|
251
|
+
prePopulateBranch,
|
|
149
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
|
+
}
|
|
150
309
|
const slackPost = slackTriggerId && slackConnector && typeof channelId === "string" && typeof threadId === "string"
|
|
151
310
|
? async (msg) => {
|
|
152
311
|
try {
|
|
@@ -158,7 +317,84 @@ async function main() {
|
|
|
158
317
|
}
|
|
159
318
|
}
|
|
160
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;
|
|
161
395
|
const callbacks = {
|
|
396
|
+
reporter: reporterProxy,
|
|
397
|
+
publicUrl: config.publicUrl,
|
|
162
398
|
postComment: slackPost
|
|
163
399
|
?? (github && issueNumber
|
|
164
400
|
? async (msg) => {
|
|
@@ -181,15 +417,29 @@ async function main() {
|
|
|
181
417
|
}
|
|
182
418
|
},
|
|
183
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,
|
|
184
430
|
};
|
|
185
431
|
try {
|
|
186
432
|
const result = await runSimpleWorkflow(workflowName, request, {
|
|
187
|
-
mcpConfigPath,
|
|
188
433
|
model: config.model,
|
|
189
434
|
maxTurns: config.maxTurns,
|
|
190
435
|
stateDir: config.stateDir,
|
|
191
436
|
sandboxDir: config.sandboxDir,
|
|
192
|
-
|
|
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);
|
|
193
443
|
const summary = result.phases.map((p) => `${p.phase}=${p.success ? "ok" : "fail"}`).join(", ");
|
|
194
444
|
if (result.paused) {
|
|
195
445
|
console.log(`[dispatch] ${workflowName} paused (${summary})`);
|
|
@@ -210,8 +460,6 @@ async function main() {
|
|
|
210
460
|
};
|
|
211
461
|
// Set up connector registry
|
|
212
462
|
const registry = new ConnectorRegistry();
|
|
213
|
-
// Session manager for messaging connectors (shared across Slack, Discord, etc.)
|
|
214
|
-
const sessionManager = new SessionManager(db.database);
|
|
215
463
|
// Message delivery service for cron output
|
|
216
464
|
const delivery = new MessageDeliveryService();
|
|
217
465
|
// GitHub webhook connector (optional — requires both webhook secret and GitHub App)
|
|
@@ -240,34 +488,59 @@ async function main() {
|
|
|
240
488
|
delivery.register("slack", (msg) => slackConnector.sendToDeliveryChannel(msg));
|
|
241
489
|
}
|
|
242
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
|
+
});
|
|
243
501
|
// Mount admin dashboard (needs an HTTP server — use GitHub connector or create standalone)
|
|
244
502
|
if (githubConnector) {
|
|
245
503
|
mountAdmin(githubConnector.honoApp, db, {
|
|
504
|
+
cronScheduler: cron,
|
|
246
505
|
stateDir: config.stateDir,
|
|
247
|
-
sessionsDir:
|
|
506
|
+
sessionsDir: config.sessionsDir,
|
|
507
|
+
buildAssetsDir: config.buildAssetsDir,
|
|
508
|
+
buildAssets: config.buildAssets,
|
|
248
509
|
adminPassword: process.env.ADMIN_PASSWORD ?? "",
|
|
249
510
|
adminSecret: process.env.ADMIN_SECRET ?? "lastlight-dev-secret",
|
|
511
|
+
publicConfig: config.publicConfig,
|
|
250
512
|
slackOAuthClientId: process.env.SLACK_OAUTH_CLIENT_ID,
|
|
251
513
|
slackOAuthClientSecret: process.env.SLACK_OAUTH_CLIENT_SECRET,
|
|
252
514
|
slackOAuthRedirectUri: process.env.SLACK_OAUTH_REDIRECT_URI,
|
|
253
515
|
slackAllowedWorkspace: process.env.SLACK_ALLOWED_WORKSPACE,
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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,
|
|
257
520
|
resumeWorkflow: async (workflowRun, sender) => {
|
|
258
521
|
if (!github) {
|
|
259
522
|
console.warn(`[admin] Cannot resume workflow ${workflowRun.id}: GitHub App not configured`);
|
|
260
523
|
return;
|
|
261
524
|
}
|
|
262
|
-
|
|
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("/")
|
|
263
530
|
? workflowRun.triggerId.replace(/#\d+$/, "").split("/")
|
|
264
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
|
+
}
|
|
265
538
|
const issueNumber = workflowRun.issueNumber;
|
|
266
539
|
if (!owner || !repo || !issueNumber) {
|
|
267
540
|
console.warn(`[admin] Cannot resume workflow ${workflowRun.id}: missing owner/repo/issueNumber`);
|
|
268
541
|
return;
|
|
269
542
|
}
|
|
270
|
-
db.
|
|
543
|
+
db.runs.setRunning(workflowRun.id);
|
|
271
544
|
console.log(`[admin] Resuming ${workflowRun.workflowName} for ${owner}/${repo}#${issueNumber} after dashboard approval by ${sender}`);
|
|
272
545
|
dispatchWorkflow(workflowRun.workflowName, {
|
|
273
546
|
repo: `${owner}/${repo}`,
|
|
@@ -346,429 +619,91 @@ async function main() {
|
|
|
346
619
|
return c.json({ accepted: true, owner, repo, issueNumber }, 202);
|
|
347
620
|
});
|
|
348
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
|
+
});
|
|
349
675
|
registry.onEvent(async (envelope) => {
|
|
350
676
|
console.log(`[event] ${envelope.source}:${envelope.type} from ${envelope.sender}${envelope.repo ? ` on ${envelope.repo}` : ""}`);
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
}
|
|
356
|
-
if (route.action === "reply") {
|
|
357
|
-
await envelope.reply(route.message);
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
const { skill, context } = route;
|
|
361
|
-
// Chat messages: handle directly (no sandbox, low latency)
|
|
362
|
-
if (skill === "chat") {
|
|
363
|
-
const messagingSessionId = context.sessionId;
|
|
364
|
-
const message = context.message;
|
|
365
|
-
const sender = context.sender;
|
|
366
|
-
// Look up the existing Agent SDK session id for this Slack thread.
|
|
367
|
-
// First message has none → fresh session; subsequent messages resume.
|
|
368
|
-
const messagingSession = sessionManager.getSession(messagingSessionId);
|
|
369
|
-
const resumeAgentSessionId = messagingSession?.agentSessionId ?? undefined;
|
|
370
|
-
// Record an executions row so chat usage shows up in dashboard stats
|
|
371
|
-
// alongside sandbox runs. triggerId is the messaging-session id, so a
|
|
372
|
-
// whole Slack thread groups together with `GROUP BY trigger_id`.
|
|
373
|
-
const executionId = randomUUID();
|
|
374
|
-
db.recordStart({
|
|
375
|
-
id: executionId,
|
|
376
|
-
triggerType: "chat",
|
|
377
|
-
triggerId: messagingSessionId,
|
|
378
|
-
skill: "chat",
|
|
379
|
-
startedAt: new Date().toISOString(),
|
|
380
|
-
});
|
|
381
|
-
try {
|
|
382
|
-
const result = await handleChatMessage(message, messagingSessionId, sender, sessionManager, {
|
|
383
|
-
mcpConfigPath,
|
|
384
|
-
model: resolveModel(config.models, "chat"),
|
|
385
|
-
maxTurns: 10,
|
|
386
|
-
}, resumeAgentSessionId);
|
|
387
|
-
// Persist the Agent SDK session id on the first turn so the next
|
|
388
|
-
// turn in this thread can resume into the same jsonl. We always
|
|
389
|
-
// overwrite — if the SDK rotated the id (e.g. resume failed and it
|
|
390
|
-
// started a fresh session) we want the latest one.
|
|
391
|
-
if (result.agentSessionId && result.agentSessionId !== resumeAgentSessionId) {
|
|
392
|
-
sessionManager.setAgentSessionId(messagingSessionId, result.agentSessionId);
|
|
393
|
-
}
|
|
394
|
-
db.recordFinish(executionId, {
|
|
395
|
-
success: result.success,
|
|
396
|
-
error: result.error,
|
|
397
|
-
turns: result.turns,
|
|
398
|
-
durationMs: result.durationMs,
|
|
399
|
-
sessionId: result.agentSessionId,
|
|
400
|
-
costUsd: result.costUsd,
|
|
401
|
-
inputTokens: result.inputTokens,
|
|
402
|
-
cacheCreationInputTokens: result.cacheCreationInputTokens,
|
|
403
|
-
cacheReadInputTokens: result.cacheReadInputTokens,
|
|
404
|
-
outputTokens: result.outputTokens,
|
|
405
|
-
apiDurationMs: result.apiDurationMs,
|
|
406
|
-
stopReason: result.stopReason,
|
|
407
|
-
});
|
|
408
|
-
await envelope.reply(result.text);
|
|
409
|
-
}
|
|
410
|
-
catch (err) {
|
|
411
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
412
|
-
console.error(`[event] Chat error:`, msg);
|
|
413
|
-
db.recordFinish(executionId, {
|
|
414
|
-
success: false,
|
|
415
|
-
error: msg,
|
|
416
|
-
durationMs: 0,
|
|
417
|
-
});
|
|
418
|
-
await envelope.reply("Sorry, I encountered an error. Please try again.");
|
|
419
|
-
}
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
|
-
// Chat reset: deactivate the session and confirm
|
|
423
|
-
if (skill === "chat-reset") {
|
|
424
|
-
const sessionId = context.sessionId;
|
|
425
|
-
if (sessionId) {
|
|
426
|
-
sessionManager.deactivateSession(sessionId);
|
|
427
|
-
}
|
|
428
|
-
await envelope.reply("Session reset. Starting fresh.");
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
|
-
// Status report: return running executions
|
|
432
|
-
if (skill === "status-report") {
|
|
433
|
-
const running = db.runningExecutions();
|
|
434
|
-
if (running.length === 0) {
|
|
435
|
-
await envelope.reply("No tasks currently running.");
|
|
436
|
-
}
|
|
437
|
-
else {
|
|
438
|
-
const lines = running.map((r) => `• *${r.skill}*${r.repo ? ` on ${r.repo}` : ""}${r.issueNumber ? ` #${r.issueNumber}` : ""} (started ${r.startedAt})`);
|
|
439
|
-
await envelope.reply(`Running tasks:\n${lines.join("\n")}`);
|
|
440
|
-
}
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
// Check if already running for this trigger
|
|
444
|
-
const triggerId = String(envelope.issueNumber || envelope.id);
|
|
445
|
-
if (db.isRunning(skill, triggerId)) {
|
|
446
|
-
console.log(`[event] Skipping: ${skill} already running for ${triggerId}`);
|
|
447
|
-
// Notify messaging users that the task is already in progress
|
|
448
|
-
if (envelope.type === "message") {
|
|
449
|
-
await envelope.reply(`That task is already running. Use /status to check progress.`);
|
|
450
|
-
}
|
|
451
|
-
return;
|
|
452
|
-
}
|
|
453
|
-
// PR fix: lightweight fix-and-push, no full build cycle
|
|
454
|
-
if (skill === "pr-fix" && context.prNumber && context.repo) {
|
|
455
|
-
const repoStr = context.repo;
|
|
456
|
-
const [owner, repo] = repoStr.includes("/") ? repoStr.split("/") : ["", repoStr];
|
|
457
|
-
const prNumber = context.prNumber;
|
|
458
|
-
if (!owner || !repo) {
|
|
459
|
-
console.error(`[event] Invalid repo format: ${repoStr}`);
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
// Fetch PR details and CI failures
|
|
463
|
-
let prTitle = context.title || "";
|
|
464
|
-
let prBody = context.body || "";
|
|
465
|
-
let branch = "";
|
|
466
|
-
let failedChecks = "";
|
|
467
|
-
if (github) {
|
|
468
|
-
try {
|
|
469
|
-
const pr = await github.getPullRequest(owner, repo, prNumber);
|
|
470
|
-
prTitle = prTitle || pr.title;
|
|
471
|
-
prBody = prBody || pr.body || "";
|
|
472
|
-
branch = pr.head.ref;
|
|
473
|
-
// Fetch CI failures for the PR's head commit
|
|
474
|
-
failedChecks = await github.getFailedChecks(owner, repo, pr.head.sha);
|
|
475
|
-
}
|
|
476
|
-
catch (err) {
|
|
477
|
-
console.warn(`[event] Could not fetch PR: ${err.message}`);
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
if (!branch) {
|
|
481
|
-
console.error(`[event] Could not determine branch for PR #${prNumber}`);
|
|
482
|
-
return;
|
|
483
|
-
}
|
|
484
|
-
console.log(`[event] PR fix for ${repoStr}#${prNumber} on branch ${branch}`);
|
|
485
|
-
const ciSection = failedChecks && !failedChecks.includes("No failed checks")
|
|
486
|
-
? `CI FAILURES (from GitHub Actions — fix these first):\n${failedChecks}`
|
|
487
|
-
: "";
|
|
488
|
-
dispatchWorkflow("pr-fix", {
|
|
489
|
-
repo: repoStr,
|
|
490
|
-
prNumber,
|
|
491
|
-
title: prTitle,
|
|
492
|
-
body: prBody,
|
|
493
|
-
commentBody: context.commentBody || "",
|
|
494
|
-
sender: context.sender || "unknown",
|
|
495
|
-
branch,
|
|
496
|
-
failedChecks,
|
|
497
|
-
ciSection,
|
|
498
|
-
_triggerType: "webhook",
|
|
499
|
-
}).catch((err) => {
|
|
500
|
-
console.error(`[event] PR fix failed:`, err);
|
|
501
|
-
});
|
|
502
|
-
return;
|
|
503
|
-
}
|
|
504
|
-
// Explore reply: free-form user reply on a paused socratic explore
|
|
505
|
-
// run. Resolve the reply gate with the message body, merge it into
|
|
506
|
-
// scratch.socratic.qa so the next iteration sees the answer, and
|
|
507
|
-
// re-dispatch the workflow to continue the loop.
|
|
508
|
-
if (skill === "explore-reply") {
|
|
509
|
-
const workflowRunId = context.workflowRunId;
|
|
510
|
-
const replyText = context.reply || "";
|
|
511
|
-
const sender = context.sender || "unknown";
|
|
512
|
-
const run = db.getWorkflowRun(workflowRunId);
|
|
513
|
-
if (!run) {
|
|
514
|
-
console.warn(`[event] explore-reply: run ${workflowRunId} not found`);
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
const pending = db.getPendingApprovalForWorkflow(workflowRunId);
|
|
518
|
-
if (!pending || pending.kind !== "reply") {
|
|
519
|
-
console.warn(`[event] explore-reply: no pending reply gate on ${workflowRunId}`);
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
522
|
-
db.resolveReplyGate(pending.id, replyText, sender);
|
|
523
|
-
// Append the QA entry to scratch.socratic.qa. The runner reads this
|
|
524
|
-
// via {{scratch.socratic.qa}} on the next iteration.
|
|
525
|
-
const prevScratch = (run.scratch || {});
|
|
526
|
-
const prevSocratic = (prevScratch.socratic || {});
|
|
527
|
-
const qaList = Array.isArray(prevSocratic.qa) ? [...prevSocratic.qa] : [];
|
|
528
|
-
qaList.push({
|
|
529
|
-
question: prevSocratic.lastOutput ?? "",
|
|
530
|
-
answer: replyText,
|
|
531
|
-
sender,
|
|
532
|
-
at: new Date().toISOString(),
|
|
533
|
-
});
|
|
534
|
-
db.updateWorkflowRunScratch(workflowRunId, {
|
|
535
|
-
socratic: { ...prevSocratic, qa: qaList },
|
|
536
|
-
});
|
|
537
|
-
// Set currentPhase to the phase BEFORE the loop owner so the
|
|
538
|
-
// runner's nextPhaseAfter lands back on the loop phase for the
|
|
539
|
-
// next iteration. Walk the workflow definition to find it.
|
|
540
|
-
try {
|
|
541
|
-
const { getWorkflow } = await import("./workflows/loader.js");
|
|
542
|
-
const def = getWorkflow(run.workflowName);
|
|
543
|
-
// Find the phase that owns this gate (pattern: socratic_iter_N)
|
|
544
|
-
const gateParts = pending.gate.match(/^(.+)_iter_\d+$/);
|
|
545
|
-
const owningPhaseName = gateParts ? gateParts[1] : null;
|
|
546
|
-
if (owningPhaseName) {
|
|
547
|
-
const ownIdx = def.phases.findIndex((p) => p.name === owningPhaseName);
|
|
548
|
-
const priorPhase = ownIdx > 0 ? def.phases[ownIdx - 1].name : owningPhaseName;
|
|
549
|
-
db.updateWorkflowPhase(workflowRunId, priorPhase, {
|
|
550
|
-
phase: priorPhase,
|
|
551
|
-
timestamp: new Date().toISOString(),
|
|
552
|
-
success: true,
|
|
553
|
-
summary: `Resumed after reply on gate: ${pending.gate}`,
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
catch (err) {
|
|
558
|
-
console.warn(`[event] explore-reply: could not resolve owning phase:`, err);
|
|
559
|
-
}
|
|
560
|
-
db.resumeWorkflowRun(workflowRunId);
|
|
561
|
-
// Re-dispatch. Use channelId/threadId from the current event context
|
|
562
|
-
// (the router captured them from the reply envelope), not from stored
|
|
563
|
-
// workflow context — they were never persisted there.
|
|
564
|
-
const isSlack = run.triggerId.startsWith("slack:");
|
|
565
|
-
const replyChannelId = context.channelId;
|
|
566
|
-
const replyThreadId = context.threadId;
|
|
567
|
-
// Reconstruct owner/repo from the stored workflow context.
|
|
568
|
-
const storedCtx = (run.context || {});
|
|
569
|
-
const storedOwner = storedCtx.owner;
|
|
570
|
-
const resumeRepo = storedOwner && run.repo
|
|
571
|
-
? `${storedOwner}/${run.repo}`
|
|
572
|
-
: run.repo || undefined;
|
|
573
|
-
console.log(`[event] explore-reply: resuming ${workflowRunId} after reply from ${sender}`);
|
|
574
|
-
dispatchWorkflow("explore", {
|
|
575
|
-
repo: resumeRepo || (isSlack ? undefined : run.triggerId.split("#")[0]),
|
|
576
|
-
issueNumber: run.issueNumber,
|
|
577
|
-
sender,
|
|
578
|
-
_triggerType: envelope.type === "message" ? "chat" : "webhook",
|
|
579
|
-
triggerId: isSlack ? run.triggerId : undefined,
|
|
580
|
-
channelId: replyChannelId,
|
|
581
|
-
threadId: replyThreadId,
|
|
582
|
-
}).catch((err) => console.error(`[event] explore-reply resume failed:`, err));
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
// Approval responses
|
|
586
|
-
if (skill === "approval-response") {
|
|
587
|
-
const decision = context.decision;
|
|
588
|
-
const sender = context.sender || "unknown";
|
|
589
|
-
const reason = context.reason;
|
|
590
|
-
const triggerId = context.repo && context.issueNumber
|
|
591
|
-
? `${context.repo}#${context.issueNumber}`
|
|
592
|
-
: undefined;
|
|
593
|
-
const approval = context.workflowRunId
|
|
594
|
-
? db.getPendingApprovalForWorkflow(context.workflowRunId)
|
|
595
|
-
: triggerId
|
|
596
|
-
? db.getPendingApprovalByTrigger(triggerId)
|
|
597
|
-
: null;
|
|
598
|
-
if (!approval) {
|
|
599
|
-
await envelope.reply("No pending approval found.");
|
|
600
|
-
return;
|
|
601
|
-
}
|
|
602
|
-
db.respondToApproval(approval.id, decision, sender, reason);
|
|
603
|
-
if (decision === "approved") {
|
|
604
|
-
// Re-trigger the build cycle — resume logic in orchestrator will pick up from DB state
|
|
605
|
-
const workflowRun = db.getWorkflowRun(approval.workflowRunId);
|
|
606
|
-
if (workflowRun && !github) {
|
|
607
|
-
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.");
|
|
608
|
-
return;
|
|
609
|
-
}
|
|
610
|
-
if (workflowRun && github) {
|
|
611
|
-
await envelope.reply(`Approved by ${sender}. Resuming \`${workflowRun.workflowName}\`...`);
|
|
612
|
-
const [owner, repo] = workflowRun.triggerId.includes("/")
|
|
613
|
-
? workflowRun.triggerId.replace(/#\d+$/, "").split("/")
|
|
614
|
-
: ["", ""];
|
|
615
|
-
const issueNumber = workflowRun.issueNumber;
|
|
616
|
-
if (owner && repo && issueNumber) {
|
|
617
|
-
db.resumeWorkflowRun(workflowRun.id);
|
|
618
|
-
dispatchWorkflow(workflowRun.workflowName, {
|
|
619
|
-
repo: `${owner}/${repo}`,
|
|
620
|
-
issueNumber,
|
|
621
|
-
title: `Issue #${issueNumber}`,
|
|
622
|
-
body: "",
|
|
623
|
-
sender,
|
|
624
|
-
_triggerType: "approval",
|
|
625
|
-
}).catch((err) => console.error(`[approval] Resume failed:`, err));
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
else {
|
|
630
|
-
const workflowRun = db.getWorkflowRun(approval.workflowRunId);
|
|
631
|
-
if (workflowRun) {
|
|
632
|
-
db.finishWorkflowRun(approval.workflowRunId, "failed", `Rejected by ${sender}: ${reason || "no reason given"}`);
|
|
633
|
-
}
|
|
634
|
-
await envelope.reply(`Rejected by ${sender}. Build cycle aborted.${reason ? ` Reason: ${reason}` : ""}`);
|
|
635
|
-
}
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
638
|
-
// Build requests: route to the programmatic orchestrator instead of the SKILL.md
|
|
639
|
-
if (skill === "github-orchestrator" && context.issueNumber && context.repo) {
|
|
640
|
-
const repoStr = context.repo;
|
|
641
|
-
const [owner, repo] = repoStr.includes("/") ? repoStr.split("/") : ["", repoStr];
|
|
642
|
-
const issueNumber = context.issueNumber;
|
|
643
|
-
if (!owner || !repo) {
|
|
644
|
-
console.error(`[event] Invalid repo format: ${repoStr}`);
|
|
645
|
-
return;
|
|
646
|
-
}
|
|
647
|
-
// Fetch full issue details if we don't have them
|
|
648
|
-
let issueTitle = context.title || "";
|
|
649
|
-
let issueBody = context.body || "";
|
|
650
|
-
let issueLabels = context.labels || [];
|
|
651
|
-
if (github && (!issueTitle || !issueBody || issueLabels.length === 0)) {
|
|
652
|
-
try {
|
|
653
|
-
const issue = await github.getIssue(owner, repo, issueNumber);
|
|
654
|
-
issueTitle = issueTitle || issue.title;
|
|
655
|
-
issueBody = issueBody || issue.body || "";
|
|
656
|
-
if (issueLabels.length === 0) {
|
|
657
|
-
issueLabels = (issue.labels || []).map((l) => typeof l === "string" ? l : l.name).filter(Boolean);
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
catch (err) {
|
|
661
|
-
console.warn(`[event] Could not fetch issue: ${err.message}`);
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
const executionId = randomUUID();
|
|
665
|
-
db.recordStart({
|
|
666
|
-
id: executionId,
|
|
667
|
-
triggerType: envelope.type === "message" ? "chat" : "webhook",
|
|
668
|
-
triggerId: String(issueNumber),
|
|
669
|
-
skill: "build-cycle",
|
|
670
|
-
repo: repoStr,
|
|
671
|
-
issueNumber,
|
|
672
|
-
startedAt: new Date().toISOString(),
|
|
673
|
-
});
|
|
674
|
-
if (envelope.type === "message") {
|
|
675
|
-
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}`);
|
|
676
681
|
}
|
|
677
|
-
else if (
|
|
678
|
-
|
|
679
|
-
// the user sees an instant ack before guardrails / architect / etc.
|
|
680
|
-
// start running. Non-fatal if it fails.
|
|
681
|
-
const commentId = envelope.raw?.comment?.id;
|
|
682
|
-
if (commentId) {
|
|
683
|
-
github
|
|
684
|
-
.reactToComment(owner, repo, commentId, "rocket")
|
|
685
|
-
.catch((err) => {
|
|
686
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
687
|
-
console.warn(`[event] Could not react to trigger comment: ${msg}`);
|
|
688
|
-
});
|
|
689
|
-
}
|
|
682
|
+
else if (outcome.kind === "skipped") {
|
|
683
|
+
console.log(`[event] Skipped: ${outcome.reason}`);
|
|
690
684
|
}
|
|
691
|
-
dispatchWorkflow("build", {
|
|
692
|
-
repo: repoStr,
|
|
693
|
-
issueNumber,
|
|
694
|
-
title: issueTitle || `Issue #${issueNumber}`,
|
|
695
|
-
body: issueBody,
|
|
696
|
-
labels: issueLabels,
|
|
697
|
-
commentBody: context.commentBody,
|
|
698
|
-
sender: context.sender || "unknown",
|
|
699
|
-
_triggerType: envelope.type === "message" ? "chat" : "webhook",
|
|
700
|
-
}).then((result) => {
|
|
701
|
-
db.recordFinish(executionId, {
|
|
702
|
-
success: result.success,
|
|
703
|
-
error: result.success ? undefined : "Build cycle failed",
|
|
704
|
-
durationMs: 0,
|
|
705
|
-
});
|
|
706
|
-
if (envelope.type === "message") {
|
|
707
|
-
envelope.reply(result.success ? `Build cycle complete.` : `Build cycle failed.`);
|
|
708
|
-
}
|
|
709
|
-
}).catch((err) => {
|
|
710
|
-
console.error(`[event] Build cycle failed:`, err);
|
|
711
|
-
db.recordFinish(executionId, { success: false, error: err.message, durationMs: 0 });
|
|
712
|
-
});
|
|
713
|
-
return;
|
|
714
|
-
}
|
|
715
|
-
// For messaging-triggered skills, acknowledge and reply when done.
|
|
716
|
-
// The router still uses skill names — they map 1:1 to workflow YAML names
|
|
717
|
-
// for the four agent skills (issue-triage, pr-review, repo-health, issue-comment).
|
|
718
|
-
if (envelope.type === "message") {
|
|
719
|
-
await envelope.reply(`Starting *${skill}*... I'll report back when it's done.`);
|
|
720
|
-
dispatchWorkflow(skill, { ...context, _triggerType: "chat" }).then(async (result) => {
|
|
721
|
-
if (result.paused) {
|
|
722
|
-
// Workflow paused at a gate (approval or reply) — don't say
|
|
723
|
-
// "completed", the workflow itself already posted instructions.
|
|
724
|
-
}
|
|
725
|
-
else if (result.success) {
|
|
726
|
-
await envelope.reply(`*${skill}* completed.`);
|
|
727
|
-
}
|
|
728
|
-
else {
|
|
729
|
-
await envelope.reply(`*${skill}* failed${result.error ? `: ${result.error}` : ""}.`);
|
|
730
|
-
}
|
|
731
|
-
}).catch(async (err) => {
|
|
732
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
733
|
-
console.error(`[event] workflow ${skill} threw: ${msg}`);
|
|
734
|
-
await envelope.reply(`*${skill}* failed: ${msg}`);
|
|
735
|
-
});
|
|
736
|
-
return;
|
|
737
685
|
}
|
|
738
|
-
|
|
739
|
-
dispatchWorkflow(skill, { ...context, _triggerType: "webhook" }).catch((err) => {
|
|
686
|
+
catch (err) {
|
|
740
687
|
const msg = err instanceof Error ? err.message : String(err);
|
|
741
|
-
console.error(`[event]
|
|
742
|
-
}
|
|
743
|
-
});
|
|
744
|
-
// Set up cron scheduler — each cron tick dispatches an agent workflow by name
|
|
745
|
-
const cron = new CronScheduler(db, async (workflowName, context) => {
|
|
746
|
-
await dispatchWorkflow(workflowName, { ...context, _triggerType: "cron" });
|
|
688
|
+
console.error(`[event] dispatch threw: ${msg}`);
|
|
689
|
+
}
|
|
747
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.
|
|
748
694
|
const webhooksEnabled = !!(config.webhookSecret && config.githubApp);
|
|
749
|
-
const jobs = getJobs({ webhooksEnabled });
|
|
695
|
+
const jobs = getJobs({ webhooksEnabled, db });
|
|
750
696
|
for (const job of jobs) {
|
|
751
697
|
cron.register(job);
|
|
752
698
|
}
|
|
753
699
|
if (webhooksEnabled) {
|
|
754
700
|
console.log("[cron] Webhooks enabled — skipping issue/PR polling crons");
|
|
755
701
|
}
|
|
756
|
-
// API usage/capacity checker — runs every 30 minutes, no sandbox needed.
|
|
757
|
-
// Passes a Slack notifier so the cron can alert the admin if the host
|
|
758
|
-
// claude CLI auth degrades (it then halts itself until cleared).
|
|
759
|
-
const { checkApiUsage } = await import("./cron/rate-limits.js");
|
|
760
|
-
const adminNotifier = slackConnector
|
|
761
|
-
? (msg) => slackConnector.sendToDeliveryChannel(msg)
|
|
762
|
-
: undefined;
|
|
763
|
-
cron.registerDirect({
|
|
764
|
-
name: "check-api-usage",
|
|
765
|
-
schedule: "*/30 * * * *",
|
|
766
|
-
handler: () => checkApiUsage(db, adminNotifier),
|
|
767
|
-
});
|
|
768
702
|
// Start everything
|
|
769
703
|
await registry.startAll();
|
|
770
704
|
console.log("[main] All connectors started");
|
|
771
705
|
console.log("[main] Cron jobs registered");
|
|
706
|
+
// Chat runs in-process via pi-ai — no long-lived server to boot.
|
|
772
707
|
// Boot-time recovery: any workflow_runs left in 'running' state from a
|
|
773
708
|
// previous harness lifetime have already had their sandbox containers
|
|
774
709
|
// killed by cleanupOrphanedSandboxes(). Mark their stale execution rows as
|
|
@@ -779,15 +714,21 @@ async function main() {
|
|
|
779
714
|
db,
|
|
780
715
|
github,
|
|
781
716
|
config: {
|
|
782
|
-
mcpConfigPath,
|
|
783
717
|
model: config.model,
|
|
784
718
|
maxTurns: config.maxTurns,
|
|
785
719
|
stateDir: config.stateDir,
|
|
786
720
|
sandboxDir: config.sandboxDir,
|
|
721
|
+
sessionsDir: config.sessionsDir,
|
|
722
|
+
sandbox: config.sandbox,
|
|
723
|
+
buildAssets: config.buildAssets,
|
|
724
|
+
buildAssetsDir: config.buildAssetsDir,
|
|
725
|
+
otel: config.otel,
|
|
787
726
|
},
|
|
788
727
|
models: config.models,
|
|
728
|
+
variants: config.variants,
|
|
789
729
|
approvalConfig: config.approval,
|
|
790
730
|
bootstrapLabel: config.bootstrapLabel,
|
|
731
|
+
publicUrl: config.publicUrl,
|
|
791
732
|
slackPoster: slackConnector
|
|
792
733
|
? (channelId, threadId, msg) => slackConnector.sendMessage(channelId, threadId, msg).then(() => { })
|
|
793
734
|
: undefined,
|
|
@@ -798,6 +739,10 @@ async function main() {
|
|
|
798
739
|
console.log("\n[main] Shutting down...");
|
|
799
740
|
cron.stopAll();
|
|
800
741
|
await registry.stopAll();
|
|
742
|
+
if (!telemetryShutdownStarted) {
|
|
743
|
+
telemetryShutdownStarted = true;
|
|
744
|
+
await shutdownTelemetry();
|
|
745
|
+
}
|
|
801
746
|
db.close();
|
|
802
747
|
process.exit(0);
|
|
803
748
|
};
|