lastlight 0.1.15 → 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 +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 +17 -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/admin/routes.js
CHANGED
|
@@ -2,24 +2,137 @@ import { Hono } from "hono";
|
|
|
2
2
|
import { timingSafeEqual, randomBytes } from "node:crypto";
|
|
3
3
|
import { streamSSE } from "hono/streaming";
|
|
4
4
|
import { getCookie, setCookie, deleteCookie } from "hono/cookie";
|
|
5
|
-
import { Slack } from "arctic";
|
|
6
|
-
import { unwrapLine } from "./sessions.js";
|
|
5
|
+
import { Slack, GitHub } from "arctic";
|
|
7
6
|
import { tailJsonl } from "./tail.js";
|
|
8
|
-
import { listRunningContainers, killContainer, getContainerStats } from "./docker.js";
|
|
7
|
+
import { listRunningContainers, killContainer, getContainerStats, listServerContainers, resolveServerContainer, getContainerLogs, streamContainerLogs, } from "./docker.js";
|
|
9
8
|
import { authMiddleware, createToken } from "./auth.js";
|
|
9
|
+
import { Cron } from "croner";
|
|
10
|
+
import { getCronWorkflows, getWorkflow, listAgentWorkflows, loadWorkflowYamlRaw, loadPromptTemplate, loadSkillRaw, } from "../workflows/loader.js";
|
|
11
|
+
import { getWorkflowTriggers, getWorkflowTriggerKinds, } from "../workflows/triggers.js";
|
|
12
|
+
import { getManagedRepos } from "../managed-repos.js";
|
|
13
|
+
import { getServerVersion } from "./version.js";
|
|
14
|
+
import { BuildAssetStore, buildAssetIssueKey } from "../state/build-assets.js";
|
|
15
|
+
/**
|
|
16
|
+
* Map a build-asset filename extension to a binary MIME type, or null when the
|
|
17
|
+
* file should be served as text/plain (markdown handoff docs). Binary artifacts
|
|
18
|
+
* — PNG screenshot evidence from browser QA, and `/demo`'s mp4/webm video — must
|
|
19
|
+
* be served as raw bytes, not utf-8 text, so the dashboard can render them in an
|
|
20
|
+
* <img>/<video> and GitHub can embed them.
|
|
21
|
+
*/
|
|
22
|
+
export function binaryMimeForArtifact(name) {
|
|
23
|
+
const ext = name.slice(name.lastIndexOf(".")).toLowerCase();
|
|
24
|
+
switch (ext) {
|
|
25
|
+
case ".png":
|
|
26
|
+
return "image/png";
|
|
27
|
+
case ".jpg":
|
|
28
|
+
case ".jpeg":
|
|
29
|
+
return "image/jpeg";
|
|
30
|
+
case ".gif":
|
|
31
|
+
return "image/gif";
|
|
32
|
+
case ".webp":
|
|
33
|
+
return "image/webp";
|
|
34
|
+
case ".svg":
|
|
35
|
+
return "image/svg+xml";
|
|
36
|
+
case ".mp4":
|
|
37
|
+
return "video/mp4";
|
|
38
|
+
case ".webm":
|
|
39
|
+
return "video/webm";
|
|
40
|
+
default:
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Build an HTTP response for a binary artifact buffer, honoring a `Range`
|
|
46
|
+
* request so <video> elements can seek/stream (GitHub's inline player and
|
|
47
|
+
* browsers send `Range: bytes=…`; without 206 support, seeking — and some
|
|
48
|
+
* players — break). Artifacts are small (≤ a few MB), so we slice the in-memory
|
|
49
|
+
* buffer rather than streaming from disk. Returns 206 for a satisfiable range,
|
|
50
|
+
* 416 for an unsatisfiable one, else 200 with the full body. `Accept-Ranges:
|
|
51
|
+
* bytes` is always advertised.
|
|
52
|
+
*/
|
|
53
|
+
export function rangeResponse(rangeHeader, buf, mime, cacheControl) {
|
|
54
|
+
const total = buf.length;
|
|
55
|
+
const baseHeaders = {
|
|
56
|
+
"Content-Type": mime,
|
|
57
|
+
"Accept-Ranges": "bytes",
|
|
58
|
+
"Cache-Control": cacheControl,
|
|
59
|
+
};
|
|
60
|
+
const m = rangeHeader ? /^bytes=(\d*)-(\d*)$/.exec(rangeHeader.trim()) : null;
|
|
61
|
+
if (m) {
|
|
62
|
+
let start = m[1] ? parseInt(m[1], 10) : 0;
|
|
63
|
+
let end = m[2] ? parseInt(m[2], 10) : total - 1;
|
|
64
|
+
if (Number.isNaN(start))
|
|
65
|
+
start = 0;
|
|
66
|
+
if (Number.isNaN(end) || end >= total)
|
|
67
|
+
end = total - 1;
|
|
68
|
+
if (start > end || start >= total) {
|
|
69
|
+
return new Response(null, {
|
|
70
|
+
status: 416,
|
|
71
|
+
headers: { ...baseHeaders, "Content-Range": `bytes */${total}` },
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
const chunk = buf.subarray(start, end + 1);
|
|
75
|
+
return new Response(new Uint8Array(chunk), {
|
|
76
|
+
status: 206,
|
|
77
|
+
headers: {
|
|
78
|
+
...baseHeaders,
|
|
79
|
+
"Content-Range": `bytes ${start}-${end}/${total}`,
|
|
80
|
+
"Content-Length": String(chunk.length),
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return new Response(new Uint8Array(buf), {
|
|
85
|
+
status: 200,
|
|
86
|
+
headers: { ...baseHeaders, "Content-Length": String(total) },
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Parse a JSON status column (`extension_status` / `skills_status`) into the
|
|
91
|
+
* object the dashboard renders. Tolerates null / malformed JSON (returns
|
|
92
|
+
* undefined) so a bad row never breaks the executions endpoint.
|
|
93
|
+
*/
|
|
94
|
+
function parseJsonColumn(raw) {
|
|
95
|
+
if (!raw)
|
|
96
|
+
return undefined;
|
|
97
|
+
try {
|
|
98
|
+
return JSON.parse(raw);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
10
104
|
/**
|
|
11
105
|
* Check if a session is live by matching against running container taskIds.
|
|
12
106
|
* Sessions are live if they were recently active (within 5 min) and a container
|
|
13
107
|
* with a matching pattern is running.
|
|
14
108
|
*/
|
|
109
|
+
/**
|
|
110
|
+
* A session is worth listing only if it actually produced something. A
|
|
111
|
+
* zero-message session is a run that died before writing any conversation
|
|
112
|
+
* (e.g. an aborted/duplicate task that left an empty jsonl behind) — it has
|
|
113
|
+
* nothing to render and previously surfaced as a phantom "live" row at the
|
|
114
|
+
* top of the list. Error-only runs still write an assistant error line, so
|
|
115
|
+
* they keep a non-zero count and remain visible.
|
|
116
|
+
*/
|
|
117
|
+
function hasContent(meta) {
|
|
118
|
+
return meta.message_count > 0;
|
|
119
|
+
}
|
|
15
120
|
function isSessionLive(meta, liveTaskIds) {
|
|
16
|
-
//
|
|
121
|
+
// Stale sessions are never live, regardless of containers.
|
|
17
122
|
const lastActivity = meta.last_message_at ?? meta.started_at;
|
|
18
123
|
const fiveMinAgo = Date.now() / 1000 - 300;
|
|
19
124
|
if (lastActivity < fiveMinAgo)
|
|
20
125
|
return false;
|
|
21
|
-
//
|
|
22
|
-
//
|
|
126
|
+
// Fallback-named sessions encode their taskId as `exec-<taskId>`, and that
|
|
127
|
+
// taskId is exactly what listRunningContainers parses from the sandbox
|
|
128
|
+
// container name — so we can match them precisely instead of guessing.
|
|
129
|
+
if (meta.id.startsWith("exec-")) {
|
|
130
|
+
return liveTaskIds.has(meta.id.slice("exec-".length));
|
|
131
|
+
}
|
|
132
|
+
// UUID-named sessions don't carry their taskId in the meta, so we can't map
|
|
133
|
+
// them to a specific container. Recent activity + at least one sandbox
|
|
134
|
+
// running (the caller gates on liveTaskIds being non-empty) is the best
|
|
135
|
+
// signal available — keeps live agent logs flowing for in-flight phases.
|
|
23
136
|
return true;
|
|
24
137
|
}
|
|
25
138
|
/**
|
|
@@ -40,6 +153,7 @@ function mountSessionRoutes(app, sessions, prefix) {
|
|
|
40
153
|
const liveTaskIds = new Set(containers.map((c) => c.taskId).filter(Boolean));
|
|
41
154
|
const valid = metas
|
|
42
155
|
.filter((m) => m !== null)
|
|
156
|
+
.filter(hasContent)
|
|
43
157
|
.sort((a, b) => b.started_at - a.started_at)
|
|
44
158
|
.slice(0, limit)
|
|
45
159
|
.map((m) => ({ ...m, live: liveTaskIds.size > 0 && isSessionLive(m, liveTaskIds) }));
|
|
@@ -61,6 +175,7 @@ function mountSessionRoutes(app, sessions, prefix) {
|
|
|
61
175
|
const metas = await Promise.all(allIds.slice(0, limit * 2).map((id) => sessions.getSessionMeta(id)));
|
|
62
176
|
const valid = metas
|
|
63
177
|
.filter((m) => m !== null)
|
|
178
|
+
.filter(hasContent)
|
|
64
179
|
.sort((a, b) => b.started_at - a.started_at)
|
|
65
180
|
.slice(0, limit)
|
|
66
181
|
.map((m) => ({ ...m, live: liveTaskIds.size > 0 && isSessionLive(m, liveTaskIds) }));
|
|
@@ -125,7 +240,7 @@ function mountSessionRoutes(app, sessions, prefix) {
|
|
|
125
240
|
let msgIndex = sinceIndex;
|
|
126
241
|
const tailer = await tailJsonl(filePath, (lines) => {
|
|
127
242
|
for (const { msg } of lines) {
|
|
128
|
-
const unwrapped =
|
|
243
|
+
const unwrapped = sessions.normalizeRawLine(msg);
|
|
129
244
|
for (const m of unwrapped) {
|
|
130
245
|
msgIndex++;
|
|
131
246
|
stream.writeSSE({ event: "message", data: JSON.stringify({ id: msgIndex, ...m }) });
|
|
@@ -152,11 +267,20 @@ function mountSessionRoutes(app, sessions, prefix) {
|
|
|
152
267
|
export function createAdminRoutes(db, sessions, chatSessions, config) {
|
|
153
268
|
const app = new Hono();
|
|
154
269
|
const slackOAuthEnabled = Boolean(config.slackOAuthClientId && config.slackOAuthClientSecret);
|
|
270
|
+
const githubCredsSet = Boolean(config.githubOAuthClientId && config.githubOAuthClientSecret);
|
|
271
|
+
const githubOAuthEnabled = githubCredsSet && Boolean(config.githubAllowedOrg);
|
|
272
|
+
if (githubCredsSet && !config.githubAllowedOrg) {
|
|
273
|
+
console.error("[oauth] GitHub OAuth client id/secret are set but GITHUB_ALLOWED_ORG is empty. " +
|
|
274
|
+
"Set it to a GitHub org slug to restrict login to that org, or to \"*\" to " +
|
|
275
|
+
"explicitly allow any GitHub user. GitHub OAuth is disabled until this is set.");
|
|
276
|
+
}
|
|
277
|
+
const githubAllowAnyUser = config.githubAllowedOrg === "*";
|
|
155
278
|
// Auth middleware
|
|
156
279
|
app.use("/*", authMiddleware(config.adminPassword, config.adminSecret));
|
|
280
|
+
app.get("/config", (c) => c.json(config.publicConfig || { default: {}, overlay: null, merged: {}, sources: {} }));
|
|
157
281
|
// Auth endpoints
|
|
158
282
|
app.get("/auth-required", (c) => {
|
|
159
|
-
return c.json({ required: Boolean(config.adminPassword), slackOAuth: slackOAuthEnabled });
|
|
283
|
+
return c.json({ required: Boolean(config.adminPassword), slackOAuth: slackOAuthEnabled, githubOAuth: githubOAuthEnabled });
|
|
160
284
|
});
|
|
161
285
|
app.post("/login", async (c) => {
|
|
162
286
|
if (!config.adminPassword) {
|
|
@@ -205,16 +329,16 @@ export function createAdminRoutes(db, sessions, chatSessions, config) {
|
|
|
205
329
|
}
|
|
206
330
|
try {
|
|
207
331
|
const slack = new Slack(config.slackOAuthClientId, config.slackOAuthClientSecret, config.slackOAuthRedirectUri ?? "");
|
|
208
|
-
const tokens = await slack.validateAuthorizationCode(code);
|
|
209
|
-
const accessToken = tokens.accessToken();
|
|
210
332
|
// "Sign in with Slack" issues OIDC-scoped tokens (openid + profile),
|
|
211
333
|
// which Slack's classic auth.test endpoint rejects with invalid_auth.
|
|
212
334
|
// Use the OIDC userInfo endpoint instead — it returns a JWT-style
|
|
213
335
|
// payload with claims under namespaced URLs.
|
|
214
|
-
const
|
|
336
|
+
const tokens = await slack.validateAuthorizationCode(code);
|
|
337
|
+
const accessToken = tokens.accessToken();
|
|
338
|
+
const res = await fetch("https://slack.com/api/openid.connect.userInfo", {
|
|
215
339
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
216
340
|
});
|
|
217
|
-
const userInfo = (await
|
|
341
|
+
const userInfo = (await res.json());
|
|
218
342
|
if (userInfo.ok === false) {
|
|
219
343
|
console.error("Slack openid.connect.userInfo failed:", userInfo.error);
|
|
220
344
|
return c.json({ error: "Slack userInfo failed" }, 502);
|
|
@@ -242,6 +366,87 @@ export function createAdminRoutes(db, sessions, chatSessions, config) {
|
|
|
242
366
|
return c.json({ error: "OAuth exchange failed" }, 502);
|
|
243
367
|
}
|
|
244
368
|
});
|
|
369
|
+
// GitHub OAuth routes (only active when GitHub OAuth env vars are configured)
|
|
370
|
+
app.get("/oauth/github/authorize", (c) => {
|
|
371
|
+
if (!githubOAuthEnabled) {
|
|
372
|
+
return c.json({ error: "GitHub OAuth not configured" }, 404);
|
|
373
|
+
}
|
|
374
|
+
const github = new GitHub(config.githubOAuthClientId, config.githubOAuthClientSecret, config.githubOAuthRedirectUri ?? "");
|
|
375
|
+
const state = randomBytes(16).toString("hex");
|
|
376
|
+
setCookie(c, "github_oauth_state", state, {
|
|
377
|
+
httpOnly: true,
|
|
378
|
+
sameSite: "Lax",
|
|
379
|
+
path: "/",
|
|
380
|
+
maxAge: 600, // 10 minutes
|
|
381
|
+
});
|
|
382
|
+
// `login` on GET /user needs no scope; read:org is only needed for the
|
|
383
|
+
// org-membership check, so skip it when the allowlist is "*".
|
|
384
|
+
const scopes = githubAllowAnyUser ? [] : ["read:org"];
|
|
385
|
+
const url = github.createAuthorizationURL(state, scopes);
|
|
386
|
+
return c.redirect(url.toString());
|
|
387
|
+
});
|
|
388
|
+
app.get("/oauth/github/callback", async (c) => {
|
|
389
|
+
if (!githubOAuthEnabled) {
|
|
390
|
+
return c.json({ error: "GitHub OAuth not configured" }, 404);
|
|
391
|
+
}
|
|
392
|
+
// Redirect the user back to the dashboard login screen with a short,
|
|
393
|
+
// URL-safe error code. The SPA maps this code to a human-readable
|
|
394
|
+
// message so the user sees the login card with an inline error instead
|
|
395
|
+
// of a raw JSON body.
|
|
396
|
+
const fail = (code) => c.redirect(`/admin/?error=${encodeURIComponent(code)}`);
|
|
397
|
+
const storedState = getCookie(c, "github_oauth_state");
|
|
398
|
+
deleteCookie(c, "github_oauth_state", { path: "/" });
|
|
399
|
+
const { code, state } = c.req.query();
|
|
400
|
+
if (!storedState || !state || storedState !== state) {
|
|
401
|
+
return fail("oauth_state");
|
|
402
|
+
}
|
|
403
|
+
if (!code) {
|
|
404
|
+
return fail("oauth_code");
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
const github = new GitHub(config.githubOAuthClientId, config.githubOAuthClientSecret, config.githubOAuthRedirectUri ?? "");
|
|
408
|
+
const tokens = await github.validateAuthorizationCode(code);
|
|
409
|
+
const accessToken = tokens.accessToken();
|
|
410
|
+
const res = await fetch("https://api.github.com/user", {
|
|
411
|
+
headers: {
|
|
412
|
+
Authorization: `Bearer ${accessToken}`,
|
|
413
|
+
"User-Agent": "lastlight-admin",
|
|
414
|
+
Accept: "application/vnd.github+json",
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
const userInfo = (await res.json());
|
|
418
|
+
let memberStatus;
|
|
419
|
+
if (userInfo.login && !githubAllowAnyUser) {
|
|
420
|
+
const org = config.githubAllowedOrg;
|
|
421
|
+
const memberRes = await fetch(`https://api.github.com/orgs/${encodeURIComponent(org)}/members/${encodeURIComponent(userInfo.login)}`, {
|
|
422
|
+
headers: {
|
|
423
|
+
Authorization: `Bearer ${accessToken}`,
|
|
424
|
+
"User-Agent": "lastlight-admin",
|
|
425
|
+
Accept: "application/vnd.github+json",
|
|
426
|
+
},
|
|
427
|
+
redirect: "manual",
|
|
428
|
+
});
|
|
429
|
+
memberStatus = memberRes.status;
|
|
430
|
+
}
|
|
431
|
+
if (!userInfo.login) {
|
|
432
|
+
console.error("GitHub /user failed: missing login field");
|
|
433
|
+
return fail("github_userinfo");
|
|
434
|
+
}
|
|
435
|
+
const login = userInfo.login;
|
|
436
|
+
// Only 204 No Content means confirmed member. 302 means caller lacks
|
|
437
|
+
// read:org visibility; 404 means not a member. Both cases are rejected.
|
|
438
|
+
if (!githubAllowAnyUser && memberStatus !== 204) {
|
|
439
|
+
console.warn(`[oauth] GitHub login rejected: ${login} not a confirmed member of ${config.githubAllowedOrg} (status ${memberStatus})`);
|
|
440
|
+
return fail("github_org");
|
|
441
|
+
}
|
|
442
|
+
const token = createToken(config.adminSecret, "github");
|
|
443
|
+
return c.redirect(`/admin/?token=${encodeURIComponent(token)}`);
|
|
444
|
+
}
|
|
445
|
+
catch (err) {
|
|
446
|
+
console.error("GitHub OAuth exchange failed:", err);
|
|
447
|
+
return fail("oauth_exchange");
|
|
448
|
+
}
|
|
449
|
+
});
|
|
245
450
|
// Health
|
|
246
451
|
app.get("/health", (c) => {
|
|
247
452
|
return c.json({ status: "ok", stateDir: config.stateDir });
|
|
@@ -254,7 +459,7 @@ export function createAdminRoutes(db, sessions, chatSessions, config) {
|
|
|
254
459
|
// Stats — running count uses live Docker containers, not stale DB records
|
|
255
460
|
app.get("/stats", async (c) => {
|
|
256
461
|
const [stats, containers] = await Promise.all([
|
|
257
|
-
Promise.resolve(db.executionStats()),
|
|
462
|
+
Promise.resolve(db.executions.executionStats()),
|
|
258
463
|
listRunningContainers(),
|
|
259
464
|
]);
|
|
260
465
|
stats.running = containers.length;
|
|
@@ -264,36 +469,13 @@ export function createAdminRoutes(db, sessions, chatSessions, config) {
|
|
|
264
469
|
app.get("/stats/daily", (c) => {
|
|
265
470
|
const daysParam = c.req.query("days");
|
|
266
471
|
const days = Math.min(Math.max(1, parseInt(daysParam ?? "30", 10) || 30), 90);
|
|
267
|
-
return c.json({ daily: db.dailyStats(days) });
|
|
472
|
+
return c.json({ daily: db.executions.dailyStats(days) });
|
|
268
473
|
});
|
|
269
474
|
// Hourly aggregated stats (rolling last N hours, default 24)
|
|
270
475
|
app.get("/stats/hourly", (c) => {
|
|
271
476
|
const hoursParam = c.req.query("hours");
|
|
272
477
|
const hours = Math.min(Math.max(1, parseInt(hoursParam ?? "24", 10) || 24), 168);
|
|
273
|
-
return c.json({ hourly: db.hourlyStats(hours) });
|
|
274
|
-
});
|
|
275
|
-
// API rate limits
|
|
276
|
-
app.get("/rate-limits", (c) => {
|
|
277
|
-
return c.json({ limits: db.getRateLimits() });
|
|
278
|
-
});
|
|
279
|
-
// Component health states (e.g. host-claude-auth degraded after auth failure)
|
|
280
|
-
app.get("/system-status", (c) => {
|
|
281
|
-
return c.json({ statuses: db.listSystemStatus() });
|
|
282
|
-
});
|
|
283
|
-
// Manually trigger a recheck of the host claude CLI auth.
|
|
284
|
-
// Force-runs the check even if the state is currently degraded; the
|
|
285
|
-
// notifier only fires on actual state transitions, so calling this
|
|
286
|
-
// endpoint repeatedly while degraded does NOT spam the admin.
|
|
287
|
-
app.post("/system-status/host-claude-auth/recheck", async (c) => {
|
|
288
|
-
try {
|
|
289
|
-
const { checkApiUsage } = await import("../cron/rate-limits.js");
|
|
290
|
-
await checkApiUsage(db, config.adminNotifier, { force: true });
|
|
291
|
-
const status = db.getSystemStatus("host-claude-auth");
|
|
292
|
-
return c.json({ status });
|
|
293
|
-
}
|
|
294
|
-
catch (err) {
|
|
295
|
-
return c.json({ error: err.message }, 500);
|
|
296
|
-
}
|
|
478
|
+
return c.json({ hourly: db.executions.hourlyStats(hours) });
|
|
297
479
|
});
|
|
298
480
|
// Running Docker containers
|
|
299
481
|
app.get("/containers", async (c) => {
|
|
@@ -305,6 +487,61 @@ export function createAdminRoutes(db, sessions, chatSessions, config) {
|
|
|
305
487
|
const stats = await getContainerStats();
|
|
306
488
|
return c.json({ stats });
|
|
307
489
|
});
|
|
490
|
+
// ── Server logs ───────────────────────────────────────────────────────────
|
|
491
|
+
// Raw `docker logs` for the lastlight-* containers (the agent harness + the
|
|
492
|
+
// egress sidecars + otel-collector). Lets an operator read the actual
|
|
493
|
+
// server/process logs over the admin API instead of SSHing to the host. The
|
|
494
|
+
// requested container is resolved against the live container list, so an
|
|
495
|
+
// arbitrary name can never reach `docker logs`.
|
|
496
|
+
app.get("/server/containers", async (c) => {
|
|
497
|
+
return c.json({ containers: await listServerContainers() });
|
|
498
|
+
});
|
|
499
|
+
// Version + drift (core/overlay) for the dashboard "update available" banner.
|
|
500
|
+
// Best-effort: an unreachable remote yields latest=null (behind=false), never
|
|
501
|
+
// a false positive. The authoritative view is `lastlight server status`.
|
|
502
|
+
app.get("/server/info", async (c) => {
|
|
503
|
+
try {
|
|
504
|
+
return c.json(await getServerVersion());
|
|
505
|
+
}
|
|
506
|
+
catch (err) {
|
|
507
|
+
return c.json({ error: `version lookup failed: ${err.message}` }, 500);
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
app.get("/server/logs", async (c) => {
|
|
511
|
+
const name = await resolveServerContainer(c.req.query("container"));
|
|
512
|
+
if (!name)
|
|
513
|
+
return c.json({ error: "no matching lastlight container" }, 404);
|
|
514
|
+
const tail = Math.min(Math.max(parseInt(c.req.query("tail") ?? "200", 10) || 200, 1), 5000);
|
|
515
|
+
const since = c.req.query("since") || undefined;
|
|
516
|
+
try {
|
|
517
|
+
const lines = await getContainerLogs(name, { tail, since });
|
|
518
|
+
return c.json({ container: name, lines });
|
|
519
|
+
}
|
|
520
|
+
catch (err) {
|
|
521
|
+
return c.json({ error: `docker logs failed: ${err.message}` }, 500);
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
app.get("/server/logs/stream", (c) => {
|
|
525
|
+
return streamSSE(c, async (stream) => {
|
|
526
|
+
const name = await resolveServerContainer(c.req.query("container"));
|
|
527
|
+
if (!name) {
|
|
528
|
+
await stream.writeSSE({ data: JSON.stringify({ error: "no matching lastlight container" }) });
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
const tail = Math.min(Math.max(parseInt(c.req.query("tail") ?? "100", 10) || 100, 1), 5000);
|
|
532
|
+
let stopped = false;
|
|
533
|
+
const stop = streamContainerLogs(name, { tail }, (line) => {
|
|
534
|
+
if (!stopped)
|
|
535
|
+
void stream.writeSSE({ data: line });
|
|
536
|
+
});
|
|
537
|
+
stream.onAbort(() => { stopped = true; stop(); });
|
|
538
|
+
// Hold the SSE open until the client disconnects.
|
|
539
|
+
while (!stopped) {
|
|
540
|
+
await stream.sleep(15000);
|
|
541
|
+
}
|
|
542
|
+
stop();
|
|
543
|
+
});
|
|
544
|
+
});
|
|
308
545
|
// Kill a sandbox container and mark related DB executions as failed
|
|
309
546
|
app.delete("/containers/:name", async (c) => {
|
|
310
547
|
const name = c.req.param("name");
|
|
@@ -319,11 +556,11 @@ export function createAdminRoutes(db, sessions, chatSessions, config) {
|
|
|
319
556
|
const taskId = match[1];
|
|
320
557
|
// Mark any running executions with matching skill as failed. Phase
|
|
321
558
|
// skill keys are `<workflowName>:<phaseName>` — match on the colon.
|
|
322
|
-
const skills = db.runningExecutions()
|
|
559
|
+
const skills = db.executions.runningExecutions()
|
|
323
560
|
.filter((e) => e.skill.includes(":") || e.skill === "pr-fix")
|
|
324
561
|
.filter((e) => taskId.includes(e.triggerId?.replace(/[^a-z0-9]/gi, "") || "---"));
|
|
325
562
|
for (const e of skills) {
|
|
326
|
-
db.recordFinish(e.id, { success: false, error: "terminated via admin dashboard" });
|
|
563
|
+
db.executions.recordFinish(e.id, { success: false, error: "terminated via admin dashboard" });
|
|
327
564
|
}
|
|
328
565
|
}
|
|
329
566
|
return c.json({ killed: name });
|
|
@@ -336,9 +573,71 @@ export function createAdminRoutes(db, sessions, chatSessions, config) {
|
|
|
336
573
|
app.get("/executions", (c) => {
|
|
337
574
|
const limit = Number(c.req.query("limit") ?? 100);
|
|
338
575
|
const offset = Number(c.req.query("offset") ?? 0);
|
|
339
|
-
const executions = db.allExecutions(limit, offset);
|
|
576
|
+
const executions = db.executions.allExecutions(limit, offset);
|
|
340
577
|
return c.json({ executions });
|
|
341
578
|
});
|
|
579
|
+
// Free-text log search — the backbone of remote debugging via the CLI
|
|
580
|
+
// (`lastlight logs search`). Two scopes:
|
|
581
|
+
// - errors (default): substring match over the executions ledger
|
|
582
|
+
// (error / skill / repo) — fast, indexed-ish.
|
|
583
|
+
// - messages: grep the most-recent session transcripts for matching
|
|
584
|
+
// conversation content, returning a snippet. Bounded by
|
|
585
|
+
// `maxSessions` so a deep history can't make this unbounded.
|
|
586
|
+
// - all: both, errors first.
|
|
587
|
+
app.get("/log-search", async (c) => {
|
|
588
|
+
const q = (c.req.query("q") ?? "").trim();
|
|
589
|
+
if (!q)
|
|
590
|
+
return c.json({ error: "missing 'q' query parameter" }, 400);
|
|
591
|
+
const scope = (c.req.query("scope") ?? "errors");
|
|
592
|
+
const limit = Math.min(Math.max(parseInt(c.req.query("limit") ?? "50", 10) || 50, 1), 200);
|
|
593
|
+
const results = [];
|
|
594
|
+
if (scope === "errors" || scope === "all") {
|
|
595
|
+
for (const r of db.executions.searchErrors(q, limit)) {
|
|
596
|
+
results.push({
|
|
597
|
+
source: "error",
|
|
598
|
+
executionId: r.id,
|
|
599
|
+
sessionId: r.sessionId,
|
|
600
|
+
workflowRunId: r.workflowRunId,
|
|
601
|
+
skill: r.skill,
|
|
602
|
+
repo: r.repo,
|
|
603
|
+
startedAt: r.startedAt,
|
|
604
|
+
success: r.success,
|
|
605
|
+
snippet: r.error ?? r.skill,
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (scope === "messages" || scope === "all") {
|
|
610
|
+
const needle = q.toLowerCase();
|
|
611
|
+
const maxSessions = 200; // newest-first cap on transcripts scanned
|
|
612
|
+
const ids = sessions.listSessionIds().slice(0, maxSessions);
|
|
613
|
+
outer: for (const id of ids) {
|
|
614
|
+
let msgs;
|
|
615
|
+
try {
|
|
616
|
+
msgs = (await sessions.read(id));
|
|
617
|
+
}
|
|
618
|
+
catch {
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
for (const { index, msg } of msgs) {
|
|
622
|
+
const text = JSON.stringify(msg.content ?? "");
|
|
623
|
+
const at = text.toLowerCase().indexOf(needle);
|
|
624
|
+
if (at === -1)
|
|
625
|
+
continue;
|
|
626
|
+
const start = Math.max(0, at - 60);
|
|
627
|
+
results.push({
|
|
628
|
+
source: "message",
|
|
629
|
+
sessionId: id,
|
|
630
|
+
messageIndex: index,
|
|
631
|
+
role: msg.role,
|
|
632
|
+
snippet: text.slice(start, at + needle.length + 120),
|
|
633
|
+
});
|
|
634
|
+
if (results.length >= limit)
|
|
635
|
+
break outer;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return c.json({ results: results.slice(0, limit) });
|
|
640
|
+
});
|
|
342
641
|
// Workflow runs — paginated, optional filters by date, workflow name, and
|
|
343
642
|
// status. Returns `total` so the dashboard can drive a "load more" pager.
|
|
344
643
|
// `status=active` is shorthand for ('running','paused') — used by the
|
|
@@ -358,7 +657,7 @@ export function createAdminRoutes(db, sessions, chatSessions, config) {
|
|
|
358
657
|
else if (statusParam) {
|
|
359
658
|
statuses = statusParam.split(",").filter(Boolean);
|
|
360
659
|
}
|
|
361
|
-
const { runs, total } = db.
|
|
660
|
+
const { runs, total } = db.runs.list({
|
|
362
661
|
limit,
|
|
363
662
|
offset,
|
|
364
663
|
sinceIso: since,
|
|
@@ -369,11 +668,11 @@ export function createAdminRoutes(db, sessions, chatSessions, config) {
|
|
|
369
668
|
});
|
|
370
669
|
// Distinct workflow names — used to populate the dashboard's filter row.
|
|
371
670
|
app.get("/workflow-names", (c) => {
|
|
372
|
-
return c.json({ names: db.
|
|
671
|
+
return c.json({ names: db.runs.distinctNames() });
|
|
373
672
|
});
|
|
374
673
|
app.get("/workflow-runs/:id", (c) => {
|
|
375
674
|
const id = c.req.param("id");
|
|
376
|
-
const run = db.
|
|
675
|
+
const run = db.runs.getRun(id);
|
|
377
676
|
if (!run)
|
|
378
677
|
return c.json({ error: "workflow run not found" }, 404);
|
|
379
678
|
return c.json({ workflowRun: run });
|
|
@@ -383,10 +682,10 @@ export function createAdminRoutes(db, sessions, chatSessions, config) {
|
|
|
383
682
|
// (and usage metrics) for any phase the user clicks.
|
|
384
683
|
app.get("/workflow-runs/:id/executions", (c) => {
|
|
385
684
|
const id = c.req.param("id");
|
|
386
|
-
const run = db.
|
|
685
|
+
const run = db.runs.getRun(id);
|
|
387
686
|
if (!run)
|
|
388
687
|
return c.json({ error: "workflow run not found" }, 404);
|
|
389
|
-
const rows = db.getExecutionsForWorkflowRun(run.id, run.triggerId, run.workflowName);
|
|
688
|
+
const rows = db.executions.getExecutionsForWorkflowRun(run.id, run.triggerId, run.workflowName);
|
|
390
689
|
const prefix = `${run.workflowName}:`;
|
|
391
690
|
const executions = rows.map((r) => ({
|
|
392
691
|
id: r.id,
|
|
@@ -407,32 +706,99 @@ export function createAdminRoutes(db, sessions, chatSessions, config) {
|
|
|
407
706
|
outputTokens: r.outputTokens,
|
|
408
707
|
apiDurationMs: r.apiDurationMs,
|
|
409
708
|
stopReason: r.stopReason,
|
|
709
|
+
extensions: parseJsonColumn(r.extensionStatus),
|
|
710
|
+
skills: parseJsonColumn(r.skillsStatus),
|
|
410
711
|
}));
|
|
411
712
|
return c.json({ executions });
|
|
412
713
|
});
|
|
413
|
-
|
|
714
|
+
// All approvals (pending + resolved) for a workflow run, oldest first. Powers
|
|
715
|
+
// the pipeline's approval-gate nodes + the detail panel's read-only approval
|
|
716
|
+
// history (status, who responded, when, and any comment). The global
|
|
717
|
+
// /approvals endpoint only lists pending ones, so it can't show history.
|
|
718
|
+
app.get("/workflow-runs/:id/approvals", (c) => {
|
|
719
|
+
const id = c.req.param("id");
|
|
720
|
+
const run = db.runs.getRun(id);
|
|
721
|
+
if (!run)
|
|
722
|
+
return c.json({ error: "workflow run not found" }, 404);
|
|
723
|
+
const approvals = db.approvals.listForWorkflow(run.id);
|
|
724
|
+
return c.json({ approvals });
|
|
725
|
+
});
|
|
726
|
+
app.post("/workflow-runs/:id/cancel", async (c) => {
|
|
414
727
|
const id = c.req.param("id");
|
|
415
|
-
const run = db.
|
|
728
|
+
const run = db.runs.getRun(id);
|
|
416
729
|
if (!run)
|
|
417
730
|
return c.json({ error: "workflow run not found" }, 404);
|
|
418
731
|
if (run.status !== "running" && run.status !== "paused") {
|
|
419
732
|
return c.json({ error: `cannot cancel a run with status '${run.status}'` }, 400);
|
|
420
733
|
}
|
|
421
|
-
db.
|
|
422
|
-
|
|
734
|
+
db.runs.cancelRun(id);
|
|
735
|
+
// Flipping the DB row alone only stops the runner before the NEXT phase.
|
|
736
|
+
// Kill any sandbox container currently executing a phase of this run so
|
|
737
|
+
// the in-flight phase stops too. Container names are
|
|
738
|
+
// lastlight-sandbox-<taskId>-<uuid>
|
|
739
|
+
// where taskId is the linear run's taskId or the DAG's phase-scoped
|
|
740
|
+
// `<taskId>-<phaseName>`, both of which start with the stored taskId.
|
|
741
|
+
const storedTaskId = run.context?.taskId;
|
|
742
|
+
let killed = [];
|
|
743
|
+
if (typeof storedTaskId === "string" && storedTaskId) {
|
|
744
|
+
try {
|
|
745
|
+
const containers = await listRunningContainers();
|
|
746
|
+
const matches = containers.filter((ctr) => ctr.taskId && ctr.taskId.startsWith(storedTaskId));
|
|
747
|
+
await Promise.all(matches.map(async (ctr) => {
|
|
748
|
+
try {
|
|
749
|
+
await killContainer(ctr.name);
|
|
750
|
+
killed.push(ctr.name);
|
|
751
|
+
}
|
|
752
|
+
catch (err) {
|
|
753
|
+
console.warn(`[cancel] failed to kill ${ctr.name}:`, err);
|
|
754
|
+
}
|
|
755
|
+
}));
|
|
756
|
+
// Mark execution rows belonging to THIS cancelled run as failed.
|
|
757
|
+
// Matching by workflowRunId (the run's id) instead of triggerId
|
|
758
|
+
// avoids clobbering a sibling run that happens to share the same
|
|
759
|
+
// trigger — e.g. two webhook deliveries for the same PR that
|
|
760
|
+
// raced before dedup closed.
|
|
761
|
+
for (const e of db.executions.runningExecutions()) {
|
|
762
|
+
if (e.workflowRunId === id) {
|
|
763
|
+
db.executions.recordFinish(e.id, { success: false, error: "cancelled via admin dashboard" });
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
catch (err) {
|
|
768
|
+
console.warn(`[cancel] container enumeration failed:`, err);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return c.json({ cancelled: id, killedContainers: killed });
|
|
423
772
|
});
|
|
424
773
|
// ── Workflow definitions ─────────────────────────────────────────
|
|
425
774
|
//
|
|
426
775
|
// The dashboard's pipeline visualisation fetches definitions from here so
|
|
427
776
|
// it can render exactly the phases the YAML file declares — including
|
|
428
777
|
// user-defined custom workflows. No hardcoded phase list, no fallback.
|
|
429
|
-
|
|
778
|
+
// List all agent workflows for the dashboard's Workflows browser.
|
|
779
|
+
app.get("/workflows", (c) => {
|
|
780
|
+
const defs = listAgentWorkflows();
|
|
781
|
+
const overrides = db.getAllWorkflowOverrides();
|
|
782
|
+
const workflows = defs.map((def) => ({
|
|
783
|
+
name: def.name,
|
|
784
|
+
kind: def.kind,
|
|
785
|
+
description: def.description,
|
|
786
|
+
trigger: def.trigger,
|
|
787
|
+
phaseCount: def.phases.length,
|
|
788
|
+
hasDag: def.phases.some((p) => Array.isArray(p.depends_on) && p.depends_on.length > 0),
|
|
789
|
+
triggerKinds: getWorkflowTriggerKinds(def.name),
|
|
790
|
+
enabled: overrides.get(def.name)?.enabled ?? true,
|
|
791
|
+
}));
|
|
792
|
+
workflows.sort((a, b) => a.name.localeCompare(b.name));
|
|
793
|
+
return c.json({ workflows });
|
|
794
|
+
});
|
|
795
|
+
app.get("/workflows/:name", (c) => {
|
|
430
796
|
const name = c.req.param("name");
|
|
431
797
|
try {
|
|
432
|
-
const { getWorkflow } = await import("../workflows/loader.js");
|
|
433
798
|
const def = getWorkflow(name);
|
|
434
799
|
// Return only the dashboard-relevant subset (no prompt template paths,
|
|
435
|
-
// no model overrides) — keeps the surface small and stable
|
|
800
|
+
// no model overrides) — keeps the surface small and stable for the
|
|
801
|
+
// run-detail pipeline. Use /workflows/:name/full for the editor.
|
|
436
802
|
return c.json({
|
|
437
803
|
workflow: {
|
|
438
804
|
name: def.name,
|
|
@@ -453,32 +819,383 @@ export function createAdminRoutes(db, sessions, chatSessions, config) {
|
|
|
453
819
|
return c.json({ error: `workflow definition not found: ${name}`, detail: msg }, 404);
|
|
454
820
|
}
|
|
455
821
|
});
|
|
822
|
+
// Full structured definition: every phase field, used by the definition
|
|
823
|
+
// browser to render phase details and the diagram.
|
|
824
|
+
app.get("/workflows/:name/full", (c) => {
|
|
825
|
+
const name = c.req.param("name");
|
|
826
|
+
try {
|
|
827
|
+
const def = getWorkflow(name);
|
|
828
|
+
return c.json({
|
|
829
|
+
workflow: def,
|
|
830
|
+
triggers: getWorkflowTriggers(name),
|
|
831
|
+
enabled: db.isWorkflowEnabled(name),
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
catch (err) {
|
|
835
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
836
|
+
return c.json({ error: `workflow definition not found: ${name}`, detail: msg }, 404);
|
|
837
|
+
}
|
|
838
|
+
});
|
|
839
|
+
// Toggle the kill switch. Mirrors POST /crons/:name/toggle. Persisted to
|
|
840
|
+
// `workflow_overrides`; reads happen on every dispatch in
|
|
841
|
+
// `runSimpleWorkflow`, so the change applies to in-flight cron ticks and
|
|
842
|
+
// webhook dispatches without needing a restart.
|
|
843
|
+
app.post("/workflows/:name/toggle", async (c) => {
|
|
844
|
+
const name = c.req.param("name");
|
|
845
|
+
try {
|
|
846
|
+
// Validate the name actually exists before persisting an override.
|
|
847
|
+
getWorkflow(name);
|
|
848
|
+
}
|
|
849
|
+
catch {
|
|
850
|
+
return c.json({ error: `unknown workflow: ${name}` }, 404);
|
|
851
|
+
}
|
|
852
|
+
const current = db.isWorkflowEnabled(name);
|
|
853
|
+
const next = !current;
|
|
854
|
+
db.setWorkflowEnabled(name, next, "admin");
|
|
855
|
+
return c.json({ name, enabled: next });
|
|
856
|
+
});
|
|
857
|
+
// Raw YAML file content — preserves comments and formatting for the
|
|
858
|
+
// dashboard's syntax-highlighted YAML view.
|
|
859
|
+
app.get("/workflows/:name/yaml", (c) => {
|
|
860
|
+
const name = c.req.param("name");
|
|
861
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
862
|
+
return c.json({ error: "invalid workflow name" }, 400);
|
|
863
|
+
}
|
|
864
|
+
try {
|
|
865
|
+
const yaml = loadWorkflowYamlRaw(name);
|
|
866
|
+
return c.text(yaml, 200, { "Content-Type": "text/plain; charset=utf-8" });
|
|
867
|
+
}
|
|
868
|
+
catch (err) {
|
|
869
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
870
|
+
return c.json({ error: `workflow yaml not found: ${name}`, detail: msg }, 404);
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
// Read a prompt template referenced by a phase (e.g. ?path=prompts/architect.md).
|
|
874
|
+
// Path is validated by `loadPromptTemplate` to live within workflowDir.
|
|
875
|
+
app.get("/workflows/:name/prompt", (c) => {
|
|
876
|
+
const name = c.req.param("name");
|
|
877
|
+
const promptPath = c.req.query("path");
|
|
878
|
+
if (!promptPath)
|
|
879
|
+
return c.json({ error: "missing ?path query" }, 400);
|
|
880
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
881
|
+
return c.json({ error: "invalid workflow name" }, 400);
|
|
882
|
+
}
|
|
883
|
+
// Restrict to the prompts/ subdirectory — workflows reference templates
|
|
884
|
+
// via `prompts/foo.md` (loader.ts:resolvePromptPath also catches escapes).
|
|
885
|
+
if (!promptPath.startsWith("prompts/") || promptPath.includes("..")) {
|
|
886
|
+
return c.json({ error: "prompt path must be under prompts/" }, 400);
|
|
887
|
+
}
|
|
888
|
+
try {
|
|
889
|
+
const text = loadPromptTemplate(promptPath);
|
|
890
|
+
return c.text(text, 200, { "Content-Type": "text/plain; charset=utf-8" });
|
|
891
|
+
}
|
|
892
|
+
catch (err) {
|
|
893
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
894
|
+
return c.json({ error: `prompt not found`, detail: msg }, 404);
|
|
895
|
+
}
|
|
896
|
+
});
|
|
897
|
+
// Read a skill's SKILL.md file. Used by the phase detail drawer when a
|
|
898
|
+
// phase declares `skill: <name>`.
|
|
899
|
+
app.get("/skills/:name", (c) => {
|
|
900
|
+
const name = c.req.param("name");
|
|
901
|
+
try {
|
|
902
|
+
const text = loadSkillRaw(name);
|
|
903
|
+
return c.text(text, 200, { "Content-Type": "text/plain; charset=utf-8" });
|
|
904
|
+
}
|
|
905
|
+
catch (err) {
|
|
906
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
907
|
+
return c.json({ error: `skill not found: ${name}`, detail: msg }, 404);
|
|
908
|
+
}
|
|
909
|
+
});
|
|
910
|
+
// ── Build assets (server mode) ─────────────────────────────────
|
|
911
|
+
// Read-only views of the externalized handoff docs (architect-plan.md, …)
|
|
912
|
+
// that live in the server store when `buildAssets.location: server`. The
|
|
913
|
+
// store itself rejects path traversal on every segment. With no store dir
|
|
914
|
+
// configured (repo mode) the list endpoints report empty rather than 404 so
|
|
915
|
+
// the dashboard tab degrades gracefully.
|
|
916
|
+
const buildAssetStore = config.buildAssetsDir ? new BuildAssetStore(config.buildAssetsDir) : null;
|
|
917
|
+
// List the run keys (issue-N / <workflow>-<id>) stored for ?repo=owner/repo.
|
|
918
|
+
app.get("/artifacts", (c) => {
|
|
919
|
+
const repoParam = c.req.query("repo");
|
|
920
|
+
if (!repoParam || !repoParam.includes("/")) {
|
|
921
|
+
return c.json({ error: "missing or invalid ?repo=owner/repo" }, 400);
|
|
922
|
+
}
|
|
923
|
+
if (!buildAssetStore)
|
|
924
|
+
return c.json({ keys: [] });
|
|
925
|
+
const [owner, repo] = repoParam.split("/", 2);
|
|
926
|
+
try {
|
|
927
|
+
return c.json({ keys: buildAssetStore.listKeys(owner, repo) });
|
|
928
|
+
}
|
|
929
|
+
catch (err) {
|
|
930
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 400);
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
// List the doc filenames stored for one run.
|
|
934
|
+
app.get("/artifacts/:owner/:repo/:key", (c) => {
|
|
935
|
+
if (!buildAssetStore)
|
|
936
|
+
return c.json({ files: [] });
|
|
937
|
+
const { owner, repo, key } = c.req.param();
|
|
938
|
+
try {
|
|
939
|
+
return c.json({ files: buildAssetStore.listFiles({ owner, repo, issueKey: key }) });
|
|
940
|
+
}
|
|
941
|
+
catch (err) {
|
|
942
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 400);
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
// Read one doc. Binary artifacts (PNG screenshot evidence, /demo mp4/webm) are
|
|
946
|
+
// served as raw bytes with the right Content-Type and Range support (so the
|
|
947
|
+
// dashboard <video> can seek); everything else is text/plain (the dashboard
|
|
948
|
+
// renders it with marked/DOMPurify).
|
|
949
|
+
app.get("/artifacts/:owner/:repo/:key/:doc", (c) => {
|
|
950
|
+
if (!buildAssetStore)
|
|
951
|
+
return c.json({ error: "build-assets store not configured" }, 404);
|
|
952
|
+
const { owner, repo, key, doc } = c.req.param();
|
|
953
|
+
try {
|
|
954
|
+
const binMime = binaryMimeForArtifact(doc);
|
|
955
|
+
if (binMime) {
|
|
956
|
+
const buf = buildAssetStore.readBuffer({ owner, repo, issueKey: key }, doc);
|
|
957
|
+
if (buf === undefined)
|
|
958
|
+
return c.json({ error: `doc not found: ${doc}` }, 404);
|
|
959
|
+
return rangeResponse(c.req.header("range"), buf, binMime, "no-store");
|
|
960
|
+
}
|
|
961
|
+
const content = buildAssetStore.read({ owner, repo, issueKey: key }, doc);
|
|
962
|
+
if (content === undefined)
|
|
963
|
+
return c.json({ error: `doc not found: ${doc}` }, 404);
|
|
964
|
+
return c.text(content, 200, { "Content-Type": "text/plain; charset=utf-8" });
|
|
965
|
+
}
|
|
966
|
+
catch (err) {
|
|
967
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 400);
|
|
968
|
+
}
|
|
969
|
+
});
|
|
970
|
+
// Overwrite one doc with the raw markdown request body. The store creates
|
|
971
|
+
// the run dir on demand and rejects path traversal on every segment.
|
|
972
|
+
app.put("/artifacts/:owner/:repo/:key/:doc", async (c) => {
|
|
973
|
+
if (!buildAssetStore)
|
|
974
|
+
return c.json({ error: "build-assets store not configured" }, 404);
|
|
975
|
+
const { owner, repo, key, doc } = c.req.param();
|
|
976
|
+
try {
|
|
977
|
+
const body = await c.req.text();
|
|
978
|
+
buildAssetStore.write({ owner, repo, issueKey: key }, doc, body);
|
|
979
|
+
return c.json({ ok: true });
|
|
980
|
+
}
|
|
981
|
+
catch (err) {
|
|
982
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 400);
|
|
983
|
+
}
|
|
984
|
+
});
|
|
456
985
|
// ── Approval Gates ─────────────────────────────────────────────
|
|
457
986
|
app.get("/approvals", (c) => {
|
|
458
|
-
const approvals = db.
|
|
987
|
+
const approvals = db.approvals.listPending();
|
|
459
988
|
return c.json({ approvals });
|
|
460
989
|
});
|
|
990
|
+
// Single approval, enriched with an `artifactRef` the focused approval view
|
|
991
|
+
// uses to open the right doc. `artifactRef` is null when the gate carries no
|
|
992
|
+
// artifact. In server mode the ref points at the editable store doc; in repo
|
|
993
|
+
// mode it carries a GitHub blob URL (the doc is committed on the branch).
|
|
994
|
+
app.get("/approvals/:id", (c) => {
|
|
995
|
+
const approval = db.approvals.getById(c.req.param("id"));
|
|
996
|
+
if (!approval)
|
|
997
|
+
return c.json({ error: "approval not found" }, 404);
|
|
998
|
+
const run = db.runs.getRun(approval.workflowRunId);
|
|
999
|
+
let artifactRef = null;
|
|
1000
|
+
if (approval.artifact && run && run.repo) {
|
|
1001
|
+
// `workflow_runs.repo` is the BARE repo name and `owner` lives in
|
|
1002
|
+
// run.context (set by simple.ts) — except in tests / legacy rows that
|
|
1003
|
+
// may store "owner/repo". Handle both: prefer context.owner, else split.
|
|
1004
|
+
const ctx = run.context ?? {};
|
|
1005
|
+
const ctxOwner = typeof ctx.owner === "string" ? ctx.owner : undefined;
|
|
1006
|
+
const repo = run.repo.includes("/") ? run.repo.split("/")[1] : run.repo;
|
|
1007
|
+
const owner = ctxOwner ?? (run.repo.includes("/") ? run.repo.split("/")[0] : "");
|
|
1008
|
+
if (owner && repo) {
|
|
1009
|
+
const mode = config.buildAssets ?? "repo";
|
|
1010
|
+
const issueKey = buildAssetIssueKey(run.workflowName, run.issueNumber, run.id);
|
|
1011
|
+
const issueDir = typeof ctx.issueDir === "string" ? ctx.issueDir : `.lastlight/${issueKey}`;
|
|
1012
|
+
const branch = typeof ctx.branch === "string" ? ctx.branch : undefined;
|
|
1013
|
+
const githubUrl = mode === "repo" && branch
|
|
1014
|
+
? `https://github.com/${owner}/${repo}/blob/${encodeURIComponent(branch)}/${issueDir}/${approval.artifact}`
|
|
1015
|
+
: undefined;
|
|
1016
|
+
artifactRef = { mode, owner, repo, issueKey, doc: approval.artifact, githubUrl };
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
return c.json({ approval, artifactRef, run: run ?? null });
|
|
1020
|
+
});
|
|
461
1021
|
app.post("/approvals/:id/respond", async (c) => {
|
|
462
1022
|
const id = c.req.param("id");
|
|
463
1023
|
const body = await c.req.json();
|
|
464
|
-
const approval = db.
|
|
1024
|
+
const approval = db.approvals.getById(id);
|
|
465
1025
|
if (!approval)
|
|
466
1026
|
return c.json({ error: "approval not found" }, 404);
|
|
467
1027
|
if (approval.status !== "pending")
|
|
468
1028
|
return c.json({ error: `already ${approval.status}` }, 400);
|
|
469
|
-
db.respondToApproval(id, body.decision, "admin", body.reason);
|
|
470
|
-
const workflowRun = db.getWorkflowRun(approval.workflowRunId);
|
|
471
1029
|
if (body.decision === "rejected") {
|
|
472
|
-
|
|
473
|
-
|
|
1030
|
+
// One transaction: respond 'rejected' + fail the run.
|
|
1031
|
+
db.runs.resolveGateAndFail(id, "admin", body.reason);
|
|
1032
|
+
}
|
|
1033
|
+
else {
|
|
1034
|
+
// Record the approval, then let resumeWorkflow flip the run back to
|
|
1035
|
+
// `running` — but only as part of an actual dispatch. resumeWorkflow
|
|
1036
|
+
// validates the target (GitHub App present, triggerId is an owner/repo#N
|
|
1037
|
+
// issue) and calls setRunning right before dispatching, so a
|
|
1038
|
+
// non-resumable approval (no resumeWorkflow wired, App down, or a
|
|
1039
|
+
// non-issue trigger) leaves the run paused rather than flipping it to
|
|
1040
|
+
// `running` with no worker. We deliberately do NOT use the atomic
|
|
1041
|
+
// resolveGateAndResume here: unlike the GitHub/Slack path, the dashboard
|
|
1042
|
+
// can't prove a dispatch will follow before responding.
|
|
1043
|
+
//
|
|
1044
|
+
// respond() is a compare-and-set on the still-pending row, so a racing
|
|
1045
|
+
// responder (the status check above is a TOCTOU read) changes 0 rows.
|
|
1046
|
+
// Only the winner resumes — the loser must not dispatch a second time.
|
|
1047
|
+
const changed = db.approvals.respond(id, "approved", "admin", body.reason);
|
|
1048
|
+
if (changed !== 1) {
|
|
1049
|
+
return c.json({ error: "already resolved" }, 409);
|
|
1050
|
+
}
|
|
1051
|
+
const workflowRun = db.runs.getRun(approval.workflowRunId);
|
|
1052
|
+
if (workflowRun && config.resumeWorkflow) {
|
|
1053
|
+
config.resumeWorkflow(workflowRun, "admin").catch((err) => {
|
|
1054
|
+
console.error(`[admin] Failed to resume workflow ${workflowRun.id}:`, err);
|
|
1055
|
+
});
|
|
474
1056
|
}
|
|
475
1057
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
1058
|
+
return c.json({ status: body.decision });
|
|
1059
|
+
});
|
|
1060
|
+
// ── Crons ──────────────────────────────────────────────────────
|
|
1061
|
+
// List every cron defined in workflows/cron-*.yaml, merged with the
|
|
1062
|
+
// override row (if any) and the live scheduler state.
|
|
1063
|
+
app.get("/crons", (c) => {
|
|
1064
|
+
const overrides = db.getAllCronOverrides();
|
|
1065
|
+
const liveByName = new Map((config.cronScheduler?.list() ?? []).map((j) => [j.name, j]));
|
|
1066
|
+
const defs = getCronWorkflows();
|
|
1067
|
+
const crons = defs.map((def) => {
|
|
1068
|
+
const override = overrides.get(def.name) ?? null;
|
|
1069
|
+
const enabled = override ? override.enabled : true;
|
|
1070
|
+
const live = liveByName.get(def.name) ?? null;
|
|
1071
|
+
const recentFailures = db.executions.consecutiveFailures(def.workflow);
|
|
1072
|
+
// Find the most recent workflow_run for this cron's workflow
|
|
1073
|
+
const recent = db.runs.listRecent(50).find((r) => r.workflowName === def.workflow);
|
|
1074
|
+
return {
|
|
1075
|
+
name: def.name,
|
|
1076
|
+
workflow: def.workflow,
|
|
1077
|
+
schedule: override?.schedule ?? def.schedule,
|
|
1078
|
+
originalSchedule: def.schedule,
|
|
1079
|
+
enabled,
|
|
1080
|
+
registered: !!live,
|
|
1081
|
+
nextRun: live?.nextRun?.toISOString() ?? null,
|
|
1082
|
+
lastRun: recent?.startedAt ?? null,
|
|
1083
|
+
lastStatus: recent?.status ?? null,
|
|
1084
|
+
recentFailures,
|
|
1085
|
+
context: { repos: getManagedRepos(), ...def.context },
|
|
1086
|
+
override: override
|
|
1087
|
+
? {
|
|
1088
|
+
updatedAt: override.updatedAt,
|
|
1089
|
+
updatedBy: override.updatedBy,
|
|
1090
|
+
hasScheduleOverride: override.schedule != null,
|
|
1091
|
+
}
|
|
1092
|
+
: null,
|
|
1093
|
+
};
|
|
1094
|
+
});
|
|
1095
|
+
return c.json({ crons });
|
|
1096
|
+
});
|
|
1097
|
+
// Toggle the enabled bit for a cron. Updates the scheduler in lockstep so
|
|
1098
|
+
// the change takes effect immediately (no restart).
|
|
1099
|
+
app.post("/crons/:name/toggle", async (c) => {
|
|
1100
|
+
if (!config.cronScheduler) {
|
|
1101
|
+
return c.json({ error: "cron scheduler not configured" }, 503);
|
|
1102
|
+
}
|
|
1103
|
+
const name = c.req.param("name");
|
|
1104
|
+
const def = getCronWorkflows().find((d) => d.name === name);
|
|
1105
|
+
if (!def)
|
|
1106
|
+
return c.json({ error: `cron not found: ${name}` }, 404);
|
|
1107
|
+
const override = db.getCronOverride(name);
|
|
1108
|
+
const currentlyEnabled = override ? override.enabled : true;
|
|
1109
|
+
const nextEnabled = !currentlyEnabled;
|
|
1110
|
+
db.setCronOverride(name, { enabled: nextEnabled, updatedBy: "admin" });
|
|
1111
|
+
if (nextEnabled) {
|
|
1112
|
+
// Re-register with the (possibly overridden) schedule
|
|
1113
|
+
const schedule = override?.schedule || def.schedule;
|
|
1114
|
+
if (config.cronScheduler.has(name)) {
|
|
1115
|
+
config.cronScheduler.update({
|
|
1116
|
+
name,
|
|
1117
|
+
schedule,
|
|
1118
|
+
workflow: def.workflow,
|
|
1119
|
+
context: { repos: getManagedRepos(), ...def.context },
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
else {
|
|
1123
|
+
config.cronScheduler.register({
|
|
1124
|
+
name,
|
|
1125
|
+
schedule,
|
|
1126
|
+
workflow: def.workflow,
|
|
1127
|
+
context: { repos: getManagedRepos(), ...def.context },
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
else {
|
|
1132
|
+
config.cronScheduler.unregister(name);
|
|
1133
|
+
}
|
|
1134
|
+
return c.json({ name, enabled: nextEnabled });
|
|
1135
|
+
});
|
|
1136
|
+
// Persist a schedule override and apply it to the scheduler. Validates the
|
|
1137
|
+
// expression with croner before saving so a bad expression returns 400 and
|
|
1138
|
+
// the live cron isn't disturbed.
|
|
1139
|
+
app.post("/crons/:name/schedule", async (c) => {
|
|
1140
|
+
if (!config.cronScheduler) {
|
|
1141
|
+
return c.json({ error: "cron scheduler not configured" }, 503);
|
|
1142
|
+
}
|
|
1143
|
+
const name = c.req.param("name");
|
|
1144
|
+
const def = getCronWorkflows().find((d) => d.name === name);
|
|
1145
|
+
if (!def)
|
|
1146
|
+
return c.json({ error: `cron not found: ${name}` }, 404);
|
|
1147
|
+
const body = await c.req.json();
|
|
1148
|
+
const schedule = (body.schedule ?? "").trim();
|
|
1149
|
+
if (!schedule)
|
|
1150
|
+
return c.json({ error: "schedule is required" }, 400);
|
|
1151
|
+
try {
|
|
1152
|
+
// Construct a paused Cron purely to validate the pattern, then dispose.
|
|
1153
|
+
const probe = new Cron(schedule, { paused: true }, () => { });
|
|
1154
|
+
probe.stop();
|
|
1155
|
+
}
|
|
1156
|
+
catch (err) {
|
|
1157
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1158
|
+
return c.json({ error: `invalid schedule: ${msg}` }, 400);
|
|
1159
|
+
}
|
|
1160
|
+
db.setCronOverride(name, { schedule, updatedBy: "admin" });
|
|
1161
|
+
const override = db.getCronOverride(name);
|
|
1162
|
+
if (override?.enabled !== false) {
|
|
1163
|
+
config.cronScheduler.update({
|
|
1164
|
+
name,
|
|
1165
|
+
schedule,
|
|
1166
|
+
workflow: def.workflow,
|
|
1167
|
+
context: { repos: getManagedRepos(), ...def.context },
|
|
479
1168
|
});
|
|
480
1169
|
}
|
|
481
|
-
return c.json({
|
|
1170
|
+
return c.json({ name, schedule });
|
|
1171
|
+
});
|
|
1172
|
+
// Drop the override row and re-register the cron at its YAML default.
|
|
1173
|
+
app.delete("/crons/:name/override", (c) => {
|
|
1174
|
+
if (!config.cronScheduler) {
|
|
1175
|
+
return c.json({ error: "cron scheduler not configured" }, 503);
|
|
1176
|
+
}
|
|
1177
|
+
const name = c.req.param("name");
|
|
1178
|
+
const def = getCronWorkflows().find((d) => d.name === name);
|
|
1179
|
+
if (!def)
|
|
1180
|
+
return c.json({ error: `cron not found: ${name}` }, 404);
|
|
1181
|
+
db.clearCronOverride(name);
|
|
1182
|
+
if (config.cronScheduler.has(name)) {
|
|
1183
|
+
config.cronScheduler.update({
|
|
1184
|
+
name,
|
|
1185
|
+
schedule: def.schedule,
|
|
1186
|
+
workflow: def.workflow,
|
|
1187
|
+
context: { repos: getManagedRepos(), ...def.context },
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
else {
|
|
1191
|
+
config.cronScheduler.register({
|
|
1192
|
+
name,
|
|
1193
|
+
schedule: def.schedule,
|
|
1194
|
+
workflow: def.workflow,
|
|
1195
|
+
context: { repos: getManagedRepos(), ...def.context },
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
return c.json({ name, schedule: def.schedule, enabled: true });
|
|
482
1199
|
});
|
|
483
1200
|
return app;
|
|
484
1201
|
}
|