lastlight 0.16.0 → 0.16.2
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 +71 -692
- package/dist/cli-config.js.map +1 -0
- package/dist/cli-format.js.map +1 -0
- package/dist/{cli/cli-server.d.ts → cli-server.d.ts} +39 -1
- package/dist/{cli/cli-server.js → cli-server.js} +105 -5
- package/dist/cli-server.js.map +1 -0
- package/dist/cli-timeline.js.map +1 -0
- package/dist/{cli/cli.js → cli.js} +9 -7
- package/dist/cli.js.map +1 -0
- package/dist/{cli/fork-cli.d.ts → fork-cli.d.ts} +3 -3
- package/dist/{cli/fork-cli.js → fork-cli.js} +28 -27
- package/dist/fork-cli.js.map +1 -0
- package/dist/{cli/oauth-cli.js → oauth-cli.js} +1 -1
- package/dist/oauth-cli.js.map +1 -0
- package/dist/{cli/setup.js → setup.js} +10 -4
- package/dist/setup.js.map +1 -0
- package/dist/{cli/skills-install.d.ts → skills-install.d.ts} +6 -0
- package/dist/{cli/skills-install.js → skills-install.js} +6 -2
- package/dist/skills-install.js.map +1 -0
- package/package.json +20 -65
- package/plugins/lastlight/.claude-plugin/plugin.json +1 -1
- package/agent-context/rules.md +0 -80
- package/agent-context/security.md +0 -69
- package/agent-context/soul.md +0 -21
- package/config/default.yaml +0 -119
- package/deploy/.env.production.example +0 -100
- package/deploy/entrypoint.sh +0 -82
- package/deploy/native/README.md +0 -160
- package/deploy/native/install.sh +0 -110
- package/deploy/native/lastlight.env.example +0 -95
- package/deploy/native/lastlight.service +0 -50
- package/deploy/sandbox-entrypoint.sh +0 -93
- package/dist/admin/auth.d.ts +0 -51
- package/dist/admin/auth.js +0 -121
- package/dist/admin/auth.js.map +0 -1
- package/dist/admin/chat-session-reader.d.ts +0 -39
- package/dist/admin/chat-session-reader.js +0 -84
- package/dist/admin/chat-session-reader.js.map +0 -1
- package/dist/admin/docker.d.ts +0 -61
- package/dist/admin/docker.js +0 -187
- package/dist/admin/docker.js.map +0 -1
- package/dist/admin/index.d.ts +0 -9
- package/dist/admin/index.js +0 -59
- package/dist/admin/index.js.map +0 -1
- package/dist/admin/routes.d.ts +0 -81
- package/dist/admin/routes.js +0 -1381
- package/dist/admin/routes.js.map +0 -1
- package/dist/admin/sessions.d.ts +0 -89
- package/dist/admin/sessions.js +0 -204
- package/dist/admin/sessions.js.map +0 -1
- package/dist/admin/tail.d.ts +0 -12
- package/dist/admin/tail.js +0 -145
- package/dist/admin/tail.js.map +0 -1
- package/dist/admin/version.d.ts +0 -22
- package/dist/admin/version.js +0 -92
- package/dist/admin/version.js.map +0 -1
- package/dist/cli/cli-config.js.map +0 -1
- package/dist/cli/cli-format.js.map +0 -1
- package/dist/cli/cli-server.js.map +0 -1
- package/dist/cli/cli-timeline.js.map +0 -1
- package/dist/cli/cli.js.map +0 -1
- package/dist/cli/fork-cli.js.map +0 -1
- package/dist/cli/oauth-cli.js.map +0 -1
- package/dist/cli/setup.js.map +0 -1
- package/dist/cli/skills-install.js.map +0 -1
- package/dist/config/config-resolve.d.ts +0 -28
- package/dist/config/config-resolve.js +0 -47
- package/dist/config/config-resolve.js.map +0 -1
- package/dist/config/config.d.ts +0 -157
- package/dist/config/config.js +0 -581
- package/dist/config/config.js.map +0 -1
- package/dist/config/core-pin.d.ts +0 -16
- package/dist/config/core-pin.js +0 -85
- package/dist/config/core-pin.js.map +0 -1
- package/dist/config/overlay-assets.d.ts +0 -20
- package/dist/config/overlay-assets.js +0 -114
- package/dist/config/overlay-assets.js.map +0 -1
- package/dist/config/overlay-bootstrap.d.ts +0 -44
- package/dist/config/overlay-bootstrap.js +0 -304
- package/dist/config/overlay-bootstrap.js.map +0 -1
- package/dist/connectors/github-webhook.d.ts +0 -45
- package/dist/connectors/github-webhook.js +0 -321
- package/dist/connectors/github-webhook.js.map +0 -1
- package/dist/connectors/index.d.ts +0 -18
- package/dist/connectors/index.js +0 -41
- package/dist/connectors/index.js.map +0 -1
- package/dist/connectors/messaging/base.d.ts +0 -42
- package/dist/connectors/messaging/base.js +0 -139
- package/dist/connectors/messaging/base.js.map +0 -1
- package/dist/connectors/messaging/delivery.d.ts +0 -14
- package/dist/connectors/messaging/delivery.js +0 -38
- package/dist/connectors/messaging/delivery.js.map +0 -1
- package/dist/connectors/messaging/index.d.ts +0 -4
- package/dist/connectors/messaging/index.js +0 -4
- package/dist/connectors/messaging/index.js.map +0 -1
- package/dist/connectors/messaging/session-manager.d.ts +0 -53
- package/dist/connectors/messaging/session-manager.js +0 -255
- package/dist/connectors/messaging/session-manager.js.map +0 -1
- package/dist/connectors/messaging/types.d.ts +0 -58
- package/dist/connectors/messaging/types.js +0 -7
- package/dist/connectors/messaging/types.js.map +0 -1
- package/dist/connectors/slack/connector.d.ts +0 -142
- package/dist/connectors/slack/connector.js +0 -494
- package/dist/connectors/slack/connector.js.map +0 -1
- package/dist/connectors/slack/index.d.ts +0 -2
- package/dist/connectors/slack/index.js +0 -3
- package/dist/connectors/slack/index.js.map +0 -1
- package/dist/connectors/slack/mrkdwn.d.ts +0 -19
- package/dist/connectors/slack/mrkdwn.js +0 -240
- package/dist/connectors/slack/mrkdwn.js.map +0 -1
- package/dist/connectors/types.d.ts +0 -61
- package/dist/connectors/types.js +0 -2
- package/dist/connectors/types.js.map +0 -1
- package/dist/cron/fanout.d.ts +0 -33
- package/dist/cron/fanout.js +0 -55
- package/dist/cron/fanout.js.map +0 -1
- package/dist/cron/jobs.d.ts +0 -18
- package/dist/cron/jobs.js +0 -37
- package/dist/cron/jobs.js.map +0 -1
- package/dist/cron/scheduler.d.ts +0 -45
- package/dist/cron/scheduler.js +0 -105
- package/dist/cron/scheduler.js.map +0 -1
- package/dist/engine/agent-executor.d.ts +0 -34
- package/dist/engine/agent-executor.js +0 -246
- package/dist/engine/agent-executor.js.map +0 -1
- package/dist/engine/chat/chat-runner.d.ts +0 -132
- package/dist/engine/chat/chat-runner.js +0 -424
- package/dist/engine/chat/chat-runner.js.map +0 -1
- package/dist/engine/chat/chat-skills.d.ts +0 -45
- package/dist/engine/chat/chat-skills.js +0 -184
- package/dist/engine/chat/chat-skills.js.map +0 -1
- package/dist/engine/chat/chat.d.ts +0 -54
- package/dist/engine/chat/chat.js +0 -316
- package/dist/engine/chat/chat.js.map +0 -1
- package/dist/engine/chat/message-batcher.d.ts +0 -32
- package/dist/engine/chat/message-batcher.js +0 -109
- package/dist/engine/chat/message-batcher.js.map +0 -1
- package/dist/engine/dispatcher.d.ts +0 -73
- package/dist/engine/dispatcher.js +0 -663
- package/dist/engine/dispatcher.js.map +0 -1
- package/dist/engine/event-shim.d.ts +0 -117
- package/dist/engine/event-shim.js +0 -435
- package/dist/engine/event-shim.js.map +0 -1
- package/dist/engine/executors/orchestrator.d.ts +0 -81
- package/dist/engine/executors/orchestrator.js +0 -365
- package/dist/engine/executors/orchestrator.js.map +0 -1
- package/dist/engine/executors/shared.d.ts +0 -189
- package/dist/engine/executors/shared.js +0 -616
- package/dist/engine/executors/shared.js.map +0 -1
- package/dist/engine/github/git-auth.d.ts +0 -62
- package/dist/engine/github/git-auth.js +0 -161
- package/dist/engine/github/git-auth.js.map +0 -1
- package/dist/engine/github/github-app-client.d.ts +0 -23
- package/dist/engine/github/github-app-client.js +0 -33
- package/dist/engine/github/github-app-client.js.map +0 -1
- package/dist/engine/github/github-tools.d.ts +0 -18
- package/dist/engine/github/github-tools.js +0 -263
- package/dist/engine/github/github-tools.js.map +0 -1
- package/dist/engine/github/github.d.ts +0 -1193
- package/dist/engine/github/github.js +0 -357
- package/dist/engine/github/github.js.map +0 -1
- package/dist/engine/github/profiles.d.ts +0 -279
- package/dist/engine/github/profiles.js +0 -42
- package/dist/engine/github/profiles.js.map +0 -1
- package/dist/engine/github/review-poster.d.ts +0 -98
- package/dist/engine/github/review-poster.js +0 -177
- package/dist/engine/github/review-poster.js.map +0 -1
- package/dist/engine/llm.d.ts +0 -79
- package/dist/engine/llm.js +0 -269
- package/dist/engine/llm.js.map +0 -1
- package/dist/engine/oauth.d.ts +0 -43
- package/dist/engine/oauth.js +0 -103
- package/dist/engine/oauth.js.map +0 -1
- package/dist/engine/router.d.ts +0 -29
- package/dist/engine/router.js +0 -680
- package/dist/engine/router.js.map +0 -1
- package/dist/engine/screen/classifier.d.ts +0 -93
- package/dist/engine/screen/classifier.js +0 -215
- package/dist/engine/screen/classifier.js.map +0 -1
- package/dist/engine/screen/screen.d.ts +0 -41
- package/dist/engine/screen/screen.js +0 -93
- package/dist/engine/screen/screen.js.map +0 -1
- package/dist/evals-api.d.ts +0 -24
- package/dist/evals-api.js +0 -22
- package/dist/evals-api.js.map +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -940
- package/dist/index.js.map +0 -1
- package/dist/managed-repos.d.ts +0 -30
- package/dist/managed-repos.js +0 -72
- package/dist/managed-repos.js.map +0 -1
- package/dist/notify/blocks.d.ts +0 -23
- package/dist/notify/blocks.js +0 -118
- package/dist/notify/blocks.js.map +0 -1
- package/dist/notify/index.d.ts +0 -17
- package/dist/notify/index.js +0 -7
- package/dist/notify/index.js.map +0 -1
- package/dist/notify/model.d.ts +0 -57
- package/dist/notify/model.js +0 -89
- package/dist/notify/model.js.map +0 -1
- package/dist/notify/notifier.d.ts +0 -18
- package/dist/notify/notifier.js +0 -100
- package/dist/notify/notifier.js.map +0 -1
- package/dist/notify/render.d.ts +0 -21
- package/dist/notify/render.js +0 -59
- package/dist/notify/render.js.map +0 -1
- package/dist/notify/transports/github.d.ts +0 -26
- package/dist/notify/transports/github.js +0 -27
- package/dist/notify/transports/github.js.map +0 -1
- package/dist/notify/transports/slack.d.ts +0 -27
- package/dist/notify/transports/slack.js +0 -37
- package/dist/notify/transports/slack.js.map +0 -1
- package/dist/notify/types.d.ts +0 -119
- package/dist/notify/types.js +0 -16
- package/dist/notify/types.js.map +0 -1
- package/dist/providers.d.ts +0 -125
- package/dist/providers.js +0 -279
- package/dist/providers.js.map +0 -1
- package/dist/sandbox/docker.d.ts +0 -180
- package/dist/sandbox/docker.js +0 -517
- package/dist/sandbox/docker.js.map +0 -1
- package/dist/sandbox/egress-allowlist.d.ts +0 -74
- package/dist/sandbox/egress-allowlist.js +0 -164
- package/dist/sandbox/egress-allowlist.js.map +0 -1
- package/dist/sandbox/egress-firewall-config.d.ts +0 -127
- package/dist/sandbox/egress-firewall-config.js +0 -434
- package/dist/sandbox/egress-firewall-config.js.map +0 -1
- package/dist/sandbox/images.d.ts +0 -23
- package/dist/sandbox/images.js +0 -55
- package/dist/sandbox/images.js.map +0 -1
- package/dist/sandbox/index.d.ts +0 -112
- package/dist/sandbox/index.js +0 -332
- package/dist/sandbox/index.js.map +0 -1
- package/dist/sandbox/sandbox.d.ts +0 -214
- package/dist/sandbox/sandbox.js +0 -453
- package/dist/sandbox/sandbox.js.map +0 -1
- package/dist/sandbox/smol.d.ts +0 -162
- package/dist/sandbox/smol.js +0 -485
- package/dist/sandbox/smol.js.map +0 -1
- package/dist/session-log.d.ts +0 -63
- package/dist/session-log.js +0 -290
- package/dist/session-log.js.map +0 -1
- package/dist/state/approval-store.d.ts +0 -88
- package/dist/state/approval-store.js +0 -149
- package/dist/state/approval-store.js.map +0 -1
- package/dist/state/build-assets.d.ts +0 -50
- package/dist/state/build-assets.js +0 -154
- package/dist/state/build-assets.js.map +0 -1
- package/dist/state/db.d.ts +0 -81
- package/dist/state/db.js +0 -155
- package/dist/state/db.js.map +0 -1
- package/dist/state/execution-store.d.ts +0 -257
- package/dist/state/execution-store.js +0 -527
- package/dist/state/execution-store.js.map +0 -1
- package/dist/state/migrate.d.ts +0 -15
- package/dist/state/migrate.js +0 -175
- package/dist/state/migrate.js.map +0 -1
- package/dist/state/workflow-run-store.d.ts +0 -216
- package/dist/state/workflow-run-store.js +0 -403
- package/dist/state/workflow-run-store.js.map +0 -1
- package/dist/telemetry/index.d.ts +0 -38
- package/dist/telemetry/index.js +0 -253
- package/dist/telemetry/index.js.map +0 -1
- package/dist/telemetry/pi-events.d.ts +0 -12
- package/dist/telemetry/pi-events.js +0 -134
- package/dist/telemetry/pi-events.js.map +0 -1
- package/dist/workflows/dag.d.ts +0 -37
- package/dist/workflows/dag.js +0 -118
- package/dist/workflows/dag.js.map +0 -1
- package/dist/workflows/loader.d.ts +0 -53
- package/dist/workflows/loader.js +0 -421
- package/dist/workflows/loader.js.map +0 -1
- package/dist/workflows/loop-eval.d.ts +0 -28
- package/dist/workflows/loop-eval.js +0 -74
- package/dist/workflows/loop-eval.js.map +0 -1
- package/dist/workflows/phase-executor.d.ts +0 -222
- package/dist/workflows/phase-executor.js +0 -1153
- package/dist/workflows/phase-executor.js.map +0 -1
- package/dist/workflows/phase-ref.d.ts +0 -50
- package/dist/workflows/phase-ref.js +0 -91
- package/dist/workflows/phase-ref.js.map +0 -1
- package/dist/workflows/resume.d.ts +0 -58
- package/dist/workflows/resume.js +0 -313
- package/dist/workflows/resume.js.map +0 -1
- package/dist/workflows/runner.d.ts +0 -63
- package/dist/workflows/runner.js +0 -381
- package/dist/workflows/runner.js.map +0 -1
- package/dist/workflows/schema.d.ts +0 -344
- package/dist/workflows/schema.js +0 -460
- package/dist/workflows/schema.js.map +0 -1
- package/dist/workflows/simple.d.ts +0 -90
- package/dist/workflows/simple.js +0 -393
- package/dist/workflows/simple.js.map +0 -1
- package/dist/workflows/target-policy.d.ts +0 -74
- package/dist/workflows/target-policy.js +0 -80
- package/dist/workflows/target-policy.js.map +0 -1
- package/dist/workflows/templates.d.ts +0 -88
- package/dist/workflows/templates.js +0 -127
- package/dist/workflows/templates.js.map +0 -1
- package/dist/workflows/triggers.d.ts +0 -21
- package/dist/workflows/triggers.js +0 -45
- package/dist/workflows/triggers.js.map +0 -1
- package/dist/workflows/verdict.d.ts +0 -19
- package/dist/workflows/verdict.js +0 -30
- package/dist/workflows/verdict.js.map +0 -1
- package/dist/worktree/manager.d.ts +0 -43
- package/dist/worktree/manager.js +0 -127
- package/dist/worktree/manager.js.map +0 -1
- package/docker-compose.yml +0 -274
- package/sandbox.Dockerfile +0 -68
- package/skills/README.md +0 -42
- package/skills/browser-qa/SKILL.md +0 -208
- package/skills/browser-qa/scripts/agent-browser.mjs +0 -531
- package/skills/building/SKILL.md +0 -95
- package/skills/chat/SKILL.md +0 -36
- package/skills/code-review/SKILL.md +0 -82
- package/skills/debug-production/SKILL.md +0 -108
- package/skills/demo/SKILL.md +0 -228
- package/skills/demo/scripts/compose-demo.sh +0 -243
- package/skills/issue-answer/SKILL.md +0 -91
- package/skills/issue-comment/SKILL.md +0 -49
- package/skills/issue-triage/SKILL.md +0 -149
- package/skills/issue-triage/references/AGENT-BRIEF.md +0 -65
- package/skills/pr-comment/SKILL.md +0 -64
- package/skills/pr-review/SKILL.md +0 -148
- package/skills/pr-review/references/findings-schema.md +0 -89
- package/skills/qa-test/SKILL.md +0 -97
- package/skills/repo-health/SKILL.md +0 -67
- package/skills/security-feedback/SKILL.md +0 -173
- package/skills/security-feedback/references/templates.md +0 -77
- package/skills/security-review/SKILL.md +0 -215
- package/skills/security-review/references/issue-format.md +0 -302
- package/skills/verify/SKILL.md +0 -118
- package/workflows/answer.yaml +0 -44
- package/workflows/build.yaml +0 -129
- package/workflows/cron-health.yaml +0 -6
- package/workflows/cron-review.yaml +0 -8
- package/workflows/cron-security.yaml +0 -6
- package/workflows/cron-triage.yaml +0 -8
- package/workflows/demo.yaml +0 -62
- package/workflows/examples/bash-smoke.yaml +0 -41
- package/workflows/examples/parallel-review.yaml +0 -36
- package/workflows/examples/tdd-loop.yaml +0 -17
- package/workflows/explore.yaml +0 -80
- package/workflows/issue-comment.yaml +0 -15
- package/workflows/issue-triage.yaml +0 -22
- package/workflows/pr-comment.yaml +0 -16
- package/workflows/pr-fix.yaml +0 -17
- package/workflows/pr-review.yaml +0 -50
- package/workflows/prompts/answer.md +0 -68
- package/workflows/prompts/architect.md +0 -44
- package/workflows/prompts/classifier.md +0 -60
- package/workflows/prompts/classify-adds-info.md +0 -17
- package/workflows/prompts/demo.md +0 -125
- package/workflows/prompts/executor.md +0 -42
- package/workflows/prompts/explore-ask.md +0 -99
- package/workflows/prompts/explore-publish.md +0 -31
- package/workflows/prompts/explore-read.md +0 -120
- package/workflows/prompts/explore-synthesize.md +0 -88
- package/workflows/prompts/fix.md +0 -18
- package/workflows/prompts/guardrails.md +0 -96
- package/workflows/prompts/pr-fix.md +0 -26
- package/workflows/prompts/pr.md +0 -39
- package/workflows/prompts/qa-browser.md +0 -95
- package/workflows/prompts/qa-synth.md +0 -44
- package/workflows/prompts/qa-test.md +0 -60
- package/workflows/prompts/re-reviewer.md +0 -33
- package/workflows/prompts/reviewer.md +0 -60
- package/workflows/prompts/verify-browser.md +0 -93
- package/workflows/prompts/verify-synth.md +0 -44
- package/workflows/prompts/verify.md +0 -63
- package/workflows/qa-test.yaml +0 -94
- package/workflows/repo-health.yaml +0 -15
- package/workflows/security-feedback.yaml +0 -25
- package/workflows/security-review.yaml +0 -37
- package/workflows/verify.yaml +0 -93
- /package/dist/{cli/cli-config.d.ts → cli-config.d.ts} +0 -0
- /package/dist/{cli/cli-config.js → cli-config.js} +0 -0
- /package/dist/{cli/cli-format.d.ts → cli-format.d.ts} +0 -0
- /package/dist/{cli/cli-format.js → cli-format.js} +0 -0
- /package/dist/{cli/cli-timeline.d.ts → cli-timeline.d.ts} +0 -0
- /package/dist/{cli/cli-timeline.js → cli-timeline.js} +0 -0
- /package/dist/{cli/cli.d.ts → cli.d.ts} +0 -0
- /package/dist/{cli/oauth-cli.d.ts → oauth-cli.d.ts} +0 -0
- /package/dist/{cli/setup.d.ts → setup.d.ts} +0 -0
package/dist/index.js
DELETED
|
@@ -1,940 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync } from "fs";
|
|
2
|
-
import { resolve } from "path";
|
|
3
|
-
import { randomUUID } from "crypto";
|
|
4
|
-
import { Hono } from "hono";
|
|
5
|
-
import { serve } from "@hono/node-server";
|
|
6
|
-
import { loadConfig, resolveModel, resolveVariant, resolveGithubAuth } from "./config/config.js";
|
|
7
|
-
import { ConnectorRegistry, GitHubWebhookConnector, SlackConnector, SessionManager, MessageDeliveryService } from "./connectors/index.js";
|
|
8
|
-
import { dispatch } from "./engine/dispatcher.js";
|
|
9
|
-
import { MessageBatcher } from "./engine/chat/message-batcher.js";
|
|
10
|
-
import { chatSystemSuffix, handleChatMessage, loadAgentContext } from "./engine/chat/chat.js";
|
|
11
|
-
import { configureWorkflowAssets, validateAssets, getWorkflow } from "./workflows/loader.js";
|
|
12
|
-
import { ChatRunner } from "./engine/chat/chat-runner.js";
|
|
13
|
-
import { buildReadSkillTool, loadChatSkillCatalogue } from "./engine/chat/chat-skills.js";
|
|
14
|
-
import { configureGitAuth } from "./engine/github/git-auth.js";
|
|
15
|
-
import { StateDb } from "./state/db.js";
|
|
16
|
-
import { CronScheduler } from "./cron/scheduler.js";
|
|
17
|
-
import { getJobs } from "./cron/jobs.js";
|
|
18
|
-
import { dispatchCronWorkflow } from "./cron/fanout.js";
|
|
19
|
-
import { mountAdmin } from "./admin/index.js";
|
|
20
|
-
import { cleanupOrphanedSandboxes } from "./sandbox/index.js";
|
|
21
|
-
import { writeEgressFirewallConfigs, writeOtelCollectorConfig } from "./sandbox/egress-firewall-config.js";
|
|
22
|
-
import { initTelemetry, shutdownTelemetry } from "./telemetry/index.js";
|
|
23
|
-
import { authMiddleware, authIsEnabled } from "./admin/auth.js";
|
|
24
|
-
import { GitHubClient } from "./engine/github/github.js";
|
|
25
|
-
import { setInstallationRepos } from "./managed-repos.js";
|
|
26
|
-
import { screenForInjection, flagPrefix } from "./engine/screen/screen.js";
|
|
27
|
-
import { runSimpleWorkflow, PR_HEADREF_PREPOPULATE_WORKFLOWS } from "./workflows/simple.js";
|
|
28
|
-
import { resumeOrphanedWorkflows, resumeSimpleRun } from "./workflows/resume.js";
|
|
29
|
-
import { ProgressNotifier, GitHubTransport, SlackTransport, } from "./notify/index.js";
|
|
30
|
-
/**
|
|
31
|
-
* Pre-flight validation — checks that config is sane before starting any
|
|
32
|
-
* services. Exits with code 78 (EX_CONFIG) on configuration errors so
|
|
33
|
-
* Docker's restart policy doesn't loop forever on a misconfigured container.
|
|
34
|
-
*/
|
|
35
|
-
function validateConfig(config) {
|
|
36
|
-
const fatal = (msg) => {
|
|
37
|
-
console.error(`\n[startup] FATAL: ${msg}`);
|
|
38
|
-
console.error("[startup] Fix your .env and restart.\n");
|
|
39
|
-
process.exit(78); // EX_CONFIG — sysexits.h convention
|
|
40
|
-
};
|
|
41
|
-
if (config.githubApp) {
|
|
42
|
-
const { appId, privateKeyPath, installationId } = config.githubApp;
|
|
43
|
-
if (!appId || !installationId) {
|
|
44
|
-
fatal("GITHUB_APP_ID and GITHUB_APP_INSTALLATION_ID are required when the GitHub App is configured.");
|
|
45
|
-
}
|
|
46
|
-
if (!existsSync(resolve(privateKeyPath))) {
|
|
47
|
-
fatal(`GITHUB_APP_PRIVATE_KEY_PATH points to "${privateKeyPath}" which does not exist.`);
|
|
48
|
-
}
|
|
49
|
-
try {
|
|
50
|
-
const content = readFileSync(resolve(privateKeyPath), "utf8");
|
|
51
|
-
if (!content.startsWith("-----BEGIN")) {
|
|
52
|
-
fatal(`GITHUB_APP_PRIVATE_KEY_PATH ("${privateKeyPath}") does not look like a PEM file.`);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
catch (err) {
|
|
56
|
-
fatal(`Cannot read GITHUB_APP_PRIVATE_KEY_PATH ("${privateKeyPath}"): ${err.message}`);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
if (!config.webhookSecret && config.githubApp) {
|
|
60
|
-
console.warn("[startup] WEBHOOK_SECRET is not set — webhook signature verification is disabled.");
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
async function main() {
|
|
64
|
-
console.log("Last Light v2.0 — Agent SDK Harness");
|
|
65
|
-
console.log("====================================");
|
|
66
|
-
// Load and validate config + overlay assets before starting anything. These
|
|
67
|
-
// throw on a broken/empty overlay, a cron targeting a missing workflow, or a
|
|
68
|
-
// phase whose prompt/skill can't resolve — all unfixable by a restart, so we
|
|
69
|
-
// exit 78 (EX_CONFIG) to stop Docker's restart policy from looping.
|
|
70
|
-
let config;
|
|
71
|
-
try {
|
|
72
|
-
config = loadConfig();
|
|
73
|
-
configureWorkflowAssets({
|
|
74
|
-
builtInRoot: config.builtInRoot,
|
|
75
|
-
overlayRoot: config.overlayDir,
|
|
76
|
-
disabled: config.disabled,
|
|
77
|
-
});
|
|
78
|
-
validateAssets(config.routes);
|
|
79
|
-
}
|
|
80
|
-
catch (err) {
|
|
81
|
-
console.error(`\n[startup] FATAL: ${err.message}`);
|
|
82
|
-
console.error("[startup] Fix your config/overlay and restart.\n");
|
|
83
|
-
process.exit(78); // EX_CONFIG — sysexits.h convention
|
|
84
|
-
}
|
|
85
|
-
validateConfig(config);
|
|
86
|
-
const packageJson = JSON.parse(readFileSync(resolve("package.json"), "utf8"));
|
|
87
|
-
await initTelemetry(config.otel, { packageVersion: packageJson.version });
|
|
88
|
-
let telemetryShutdownStarted = false;
|
|
89
|
-
console.log(config.otel.enabled
|
|
90
|
-
? `[otel] enabled service=${config.otel.serviceName} forwardToSandbox=${config.otel.forwardToSandbox} includeContent=${config.otel.includeContent}`
|
|
91
|
-
: "[otel] disabled");
|
|
92
|
-
console.log(`[config] Port: ${config.port}, Model: ${config.model}`);
|
|
93
|
-
const modelOverrides = Object.entries(config.models).filter(([k]) => k !== "default");
|
|
94
|
-
if (modelOverrides.length > 0) {
|
|
95
|
-
console.log(`[config] Model overrides: ${modelOverrides.map(([k, v]) => `${k}=${v}`).join(", ")}`);
|
|
96
|
-
}
|
|
97
|
-
// Clean up any sandbox containers left over from a previous run
|
|
98
|
-
cleanupOrphanedSandboxes();
|
|
99
|
-
// Ensure state directory structure exists (mountable as Docker volume)
|
|
100
|
-
for (const sub of ["sessions", "logs", "sandboxes"]) {
|
|
101
|
-
mkdirSync(resolve(config.stateDir, sub), { recursive: true });
|
|
102
|
-
}
|
|
103
|
-
console.log(`[state] State dir: ${config.stateDir}`);
|
|
104
|
-
console.log(`[state] Sessions dir: ${config.sessionsDir}`);
|
|
105
|
-
console.log(`[config] Sandbox backend: ${config.sandbox}`);
|
|
106
|
-
// Regenerate egress firewall configs (nginx ssl_preread + coredns) from
|
|
107
|
-
// the allowlist source of truth. Only meaningful for the docker backend;
|
|
108
|
-
// cheap enough to do unconditionally so a backend switch doesn't leave
|
|
109
|
-
// stale configs on disk. The docker backend forwards sandbox telemetry
|
|
110
|
-
// through the in-network OTEL collector (reached by IP), so the strict
|
|
111
|
-
// SNI allowlist no longer needs collector hosts — that hop happens on
|
|
112
|
-
// the collector's trusted outbound leg, not through the firewall.
|
|
113
|
-
const proxyDir = writeEgressFirewallConfigs(config.stateDir);
|
|
114
|
-
console.log(`[state] Egress firewall configs: ${proxyDir}`);
|
|
115
|
-
// Generate the in-network OTEL collector config (docker backend). Derived
|
|
116
|
-
// from the harness's OTEL_* backend env so the collector re-exports to the
|
|
117
|
-
// same backend the harness uses — with auth headers that stay host-side.
|
|
118
|
-
// Forwarding is gated on telemetry being active: when disabled (or sandbox
|
|
119
|
-
// forwarding off) the collector renders an inert debug-only config so the
|
|
120
|
-
// static collector IP can't be used as a sandbox exfil path.
|
|
121
|
-
const collectorConfigPath = writeOtelCollectorConfig(config.stateDir, {
|
|
122
|
-
active: config.otel.enabled && config.otel.forwardToSandbox,
|
|
123
|
-
});
|
|
124
|
-
console.log(`[state] OTEL collector config: ${collectorConfigPath} (forwarding ${config.otel.enabled && config.otel.forwardToSandbox ? "active" : "disabled"})`);
|
|
125
|
-
// Initialize state database first — ChatRunner needs SessionManager
|
|
126
|
-
// (DB-backed) at construction time.
|
|
127
|
-
const db = new StateDb(config.dbPath);
|
|
128
|
-
console.log(`[state] Database: ${config.dbPath}`);
|
|
129
|
-
// Session manager for messaging connectors (shared across Slack, Discord, etc.)
|
|
130
|
-
const sessionManager = new SessionManager(db.database);
|
|
131
|
-
// In-process chat runner backs the messaging chat skill. One pi-ai
|
|
132
|
-
// conversation per Slack/Discord thread; locked down to read-only
|
|
133
|
-
// GitHub tools plus a `read_skill` tool that exposes the curated
|
|
134
|
-
// chat skill catalogue. Skills are listed in the system prompt via
|
|
135
|
-
// an XML <available_skills> block; the agent pulls full SKILL.md on
|
|
136
|
-
// demand — same progressive-disclosure model the sandbox phases use.
|
|
137
|
-
const chatSkills = loadChatSkillCatalogue();
|
|
138
|
-
const readSkill = buildReadSkillTool(chatSkills.skills);
|
|
139
|
-
// Resolve GitHub auth once (App wins; PAT fallback; else none). Drives the
|
|
140
|
-
// chat GitHub tools, the harness client, and the chat prompt's tool section.
|
|
141
|
-
const githubAuth = resolveGithubAuth(config);
|
|
142
|
-
const chatGithubAuth = githubAuth?.kind === "app"
|
|
143
|
-
? {
|
|
144
|
-
appId: githubAuth.appId,
|
|
145
|
-
privateKeyPath: githubAuth.privateKeyPath,
|
|
146
|
-
installationId: githubAuth.installationId,
|
|
147
|
-
}
|
|
148
|
-
: githubAuth?.kind === "token"
|
|
149
|
-
? { token: githubAuth.token }
|
|
150
|
-
: undefined;
|
|
151
|
-
const chatRunner = new ChatRunner({
|
|
152
|
-
model: resolveModel(config.models, "chat"),
|
|
153
|
-
thinking: resolveVariant(config.variants, "chat"),
|
|
154
|
-
systemPrompt: loadAgentContext() +
|
|
155
|
-
chatSystemSuffix(githubAuth !== undefined) +
|
|
156
|
-
chatSkills.catalogueXml,
|
|
157
|
-
github: chatGithubAuth,
|
|
158
|
-
extraTools: chatSkills.skills.length > 0
|
|
159
|
-
? { tools: [readSkill.tool], execute: readSkill.execute }
|
|
160
|
-
: undefined,
|
|
161
|
-
}, sessionManager);
|
|
162
|
-
if (chatSkills.skills.length > 0) {
|
|
163
|
-
console.log(`[chat] Loaded ${chatSkills.skills.length} skill(s): ${chatSkills.skills.map((s) => s.name).join(", ")}`);
|
|
164
|
-
}
|
|
165
|
-
else {
|
|
166
|
-
console.warn("[chat] No skills loaded — frontmatter missing or no matching SKILL.md found");
|
|
167
|
-
}
|
|
168
|
-
// Configure git with GitHub App credentials — agents can git clone/push natively.
|
|
169
|
-
// Non-fatal: the token is refreshed before each agent execution anyway, so a
|
|
170
|
-
// transient failure here (DNS, rate limit) doesn't block startup.
|
|
171
|
-
if (config.githubApp) {
|
|
172
|
-
try {
|
|
173
|
-
await configureGitAuth({
|
|
174
|
-
appId: config.githubApp.appId,
|
|
175
|
-
privateKeyPath: config.githubApp.privateKeyPath,
|
|
176
|
-
installationId: config.githubApp.installationId,
|
|
177
|
-
botLogin: config.botLogin,
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
catch (err) {
|
|
181
|
-
console.warn(`[git-auth] Initial token mint failed (will retry per-execution): ${err.message}`);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
// GitHub API client for harness-level operations (posting comments, fetching
|
|
185
|
-
// issues). App auth when configured; otherwise a PAT (read-only unless the
|
|
186
|
-
// token carries write scope); null in chat-only mode.
|
|
187
|
-
const github = config.githubApp
|
|
188
|
-
? new GitHubClient(config.githubApp)
|
|
189
|
-
: config.githubToken
|
|
190
|
-
? GitHubClient.withToken(config.githubToken)
|
|
191
|
-
: null;
|
|
192
|
-
// Discover the repos the App installation can access and seed the managed-repo
|
|
193
|
-
// list. When the overlay's `managedRepos` is empty this becomes the effective
|
|
194
|
-
// allowlist (getManagedRepos falls back to it); a configured list still wins.
|
|
195
|
-
// Kept live afterwards by installation webhooks (github-webhook.ts). Non-fatal:
|
|
196
|
-
// on failure we fall back to whatever `managedRepos` config provides. Runs
|
|
197
|
-
// before the HTTP listener opens, so the list is warm before the first event.
|
|
198
|
-
if (github && config.githubApp) {
|
|
199
|
-
try {
|
|
200
|
-
const repos = await github.listInstallationRepos();
|
|
201
|
-
setInstallationRepos(repos);
|
|
202
|
-
console.log(`[github] Discovered ${repos.length} installation repos`);
|
|
203
|
-
}
|
|
204
|
-
catch (err) {
|
|
205
|
-
console.warn(`[github] Installation repo discovery failed: ${err.message}`);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* Dispatch a workflow by name. Used by webhook events, cron jobs, and the
|
|
210
|
-
* /api/run endpoint. Every dispatch creates a workflow_run row visible in
|
|
211
|
-
* the dashboard, regardless of whether it's a single-phase workflow (like
|
|
212
|
-
* issue-triage) or a multi-phase one.
|
|
213
|
-
*
|
|
214
|
-
* The router still uses skill names for backwards compat — for the four
|
|
215
|
-
* agent skills they're 1:1 with workflow names.
|
|
216
|
-
*/
|
|
217
|
-
const dispatchWorkflow = async (workflowName, context, onRunStart) => {
|
|
218
|
-
// Slack-initiated workflows (explore, /explore) carry a
|
|
219
|
-
// `slack:{team}:{channel}:{thread}` triggerId and don't require a
|
|
220
|
-
// managed `repo` — their postComment goes back to the Slack thread.
|
|
221
|
-
const slackTriggerId = typeof context.triggerId === "string" && context.triggerId.startsWith("slack:")
|
|
222
|
-
? context.triggerId
|
|
223
|
-
: undefined;
|
|
224
|
-
const repoStr = context.repo;
|
|
225
|
-
if (!repoStr && !slackTriggerId) {
|
|
226
|
-
const msg = `dispatchWorkflow(${workflowName}): missing 'repo' in context`;
|
|
227
|
-
console.error(`[dispatch] ${msg}`);
|
|
228
|
-
return { success: false, error: msg };
|
|
229
|
-
}
|
|
230
|
-
const [owner, repo] = repoStr && repoStr.includes("/")
|
|
231
|
-
? repoStr.split("/")
|
|
232
|
-
: repoStr
|
|
233
|
-
? ["", repoStr]
|
|
234
|
-
: ["", ""];
|
|
235
|
-
if (repoStr && (!owner || !repo)) {
|
|
236
|
-
const msg = `dispatchWorkflow(${workflowName}): invalid repo format '${repoStr}'`;
|
|
237
|
-
console.error(`[dispatch] ${msg}`);
|
|
238
|
-
return { success: false, error: msg };
|
|
239
|
-
}
|
|
240
|
-
// Pluck the standard fields, leave the rest in `extra` for the workflow
|
|
241
|
-
// template to consume.
|
|
242
|
-
const { _triggerType, repo: _r, issueNumber, prNumber, title, body, labels, sender, commentBody, triggerId: _triggerId, channelId, threadId, prePopulateBranch: ctxPrePopulateBranch, branch: ctxBranch, ...rest } = context;
|
|
243
|
-
// Preserve channelId/threadId in extra so they're stored on the
|
|
244
|
-
// workflow run context — needed by boot-time resume to rebuild the
|
|
245
|
-
// Slack postComment callback after a harness restart.
|
|
246
|
-
const extra = { ...rest };
|
|
247
|
-
if (typeof channelId === "string")
|
|
248
|
-
extra.channelId = channelId;
|
|
249
|
-
if (typeof threadId === "string")
|
|
250
|
-
extra.threadId = threadId;
|
|
251
|
-
// For PR-scoped read workflows, resolve the PR head ref and ask the
|
|
252
|
-
// sandbox to pre-clone the repo at that branch. The agent then enters
|
|
253
|
-
// a workspace that's already a checkout of the PR's actual code —
|
|
254
|
-
// saves a redundant clone_repo MCP call inside the session.
|
|
255
|
-
//
|
|
256
|
-
// pr-fix already plumbs `branch` through context (line ~709 below)
|
|
257
|
-
// because the architect/executor need the branch name to push to;
|
|
258
|
-
// we honor that here as the pre-populate branch too.
|
|
259
|
-
let prePopulateBranch = typeof ctxPrePopulateBranch === "string" ? ctxPrePopulateBranch : undefined;
|
|
260
|
-
if (!prePopulateBranch && typeof ctxBranch === "string" && ctxBranch && workflowName === "pr-fix") {
|
|
261
|
-
prePopulateBranch = ctxBranch;
|
|
262
|
-
}
|
|
263
|
-
// PR-scoped read workflows that benefit from a workspace pre-checked-out at
|
|
264
|
-
// the PR's *real* head ref. Each one synthesizes a `lastlight/N-<title-slug>`
|
|
265
|
-
// branch (see resolveRunBranch) that does NOT exist on the remote, so
|
|
266
|
-
// prePopulateWorkspace's missing-branch fallback silently clones the
|
|
267
|
-
// *default* branch — testing/demoing code that lacks the PR's changes
|
|
268
|
-
// (a false-negative QA, or a before/after demo whose "after" matches
|
|
269
|
-
// "before"). Resolving the head ref here pins the workspace to the actual
|
|
270
|
-
// PR code. See `PR_HEADREF_PREPOPULATE_WORKFLOWS` for the per-workflow why.
|
|
271
|
-
if (!prePopulateBranch &&
|
|
272
|
-
PR_HEADREF_PREPOPULATE_WORKFLOWS.has(workflowName) &&
|
|
273
|
-
typeof prNumber === "number" &&
|
|
274
|
-
github &&
|
|
275
|
-
owner &&
|
|
276
|
-
repo) {
|
|
277
|
-
try {
|
|
278
|
-
const pr = await github.getPullRequest(owner, repo, prNumber);
|
|
279
|
-
prePopulateBranch = pr.head.ref;
|
|
280
|
-
// Surface the base ref so a before/after demo can fetch + check out the
|
|
281
|
-
// baseline (the read-only pre-clone is shallow + single-branch at the
|
|
282
|
-
// head ref, so `origin/<base>` isn't present until the agent fetches it).
|
|
283
|
-
if (pr.base?.ref)
|
|
284
|
-
extra.baseBranch = pr.base.ref;
|
|
285
|
-
console.log(`[dispatch] ${workflowName}: pre-populating workspace at ${owner}/${repo}@${prePopulateBranch} ` +
|
|
286
|
-
`(base ${pr.base?.ref ?? "?"})`);
|
|
287
|
-
}
|
|
288
|
-
catch (err) {
|
|
289
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
290
|
-
console.warn(`[dispatch] ${workflowName}: could not resolve PR head ref (${msg}); ` +
|
|
291
|
-
`agent will need to clone via MCP`);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
// Base branch for scoping. PR-triggered runs set it from the PR's base ref
|
|
295
|
-
// above; build/issue-triggered runs have no PR, so resolve the repo's real
|
|
296
|
-
// default branch here. Without this, everything that diffs against the base
|
|
297
|
-
// — notably the reviewer prompt's `git ... {{baseBranch}}..HEAD` — assumes
|
|
298
|
-
// `main` and breaks on a `master`-default (or otherwise non-`main`) repo.
|
|
299
|
-
// Best-effort: on failure fall back to `main` so the template still renders
|
|
300
|
-
// a valid ref rather than an empty `..HEAD`.
|
|
301
|
-
if (!extra.baseBranch && github && owner && repo) {
|
|
302
|
-
try {
|
|
303
|
-
extra.baseBranch = await github.getDefaultBranch(owner, repo);
|
|
304
|
-
}
|
|
305
|
-
catch (err) {
|
|
306
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
307
|
-
console.warn(`[dispatch] ${workflowName}: could not resolve default branch for ${owner}/${repo} ` +
|
|
308
|
-
`(${msg}); assuming main`);
|
|
309
|
-
extra.baseBranch = "main";
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
const request = {
|
|
313
|
-
owner,
|
|
314
|
-
repo,
|
|
315
|
-
issueNumber: typeof issueNumber === "number" ? issueNumber : undefined,
|
|
316
|
-
prNumber: typeof prNumber === "number" ? prNumber : undefined,
|
|
317
|
-
issueTitle: typeof title === "string" ? title : "",
|
|
318
|
-
issueBody: typeof body === "string" ? body : "",
|
|
319
|
-
issueLabels: Array.isArray(labels) ? labels : undefined,
|
|
320
|
-
commentBody: typeof commentBody === "string" ? commentBody : undefined,
|
|
321
|
-
sender: typeof sender === "string" ? sender : "unknown",
|
|
322
|
-
triggerId: slackTriggerId,
|
|
323
|
-
extra,
|
|
324
|
-
prePopulateBranch,
|
|
325
|
-
};
|
|
326
|
-
// For workflows where the architect/agent needs to see the full issue
|
|
327
|
-
// history (e.g. a build greenlit by "@last-light lets build this!" needs
|
|
328
|
-
// the spec the explore phase wrote in earlier comments), fetch the real
|
|
329
|
-
// issue body and the comment thread, combine them into a single context
|
|
330
|
-
// blob, and screen the combined text in ONE SDK call.
|
|
331
|
-
//
|
|
332
|
-
// Why single-shot: screening per-comment fans out N concurrent SDK calls,
|
|
333
|
-
// and on a busy issue (16+ comments) that exhausts memory and trips the
|
|
334
|
-
// EventEmitter listener cap. The combined-context approach keeps screen
|
|
335
|
-
// cost at exactly one haiku call regardless of thread length.
|
|
336
|
-
//
|
|
337
|
-
// For comment-triggered builds the envelope's `body` field is the
|
|
338
|
-
// triggering comment, not the issue body — we explicitly fetch the
|
|
339
|
-
// real issue body here so the architect sees both the spec (issue body
|
|
340
|
-
// + thread) and the trigger (commentBody) cleanly separated.
|
|
341
|
-
const ENRICH_WORKFLOWS = new Set(["build", "pr-fix", "explore"]);
|
|
342
|
-
if (github &&
|
|
343
|
-
ENRICH_WORKFLOWS.has(workflowName) &&
|
|
344
|
-
request.issueNumber &&
|
|
345
|
-
owner && repo) {
|
|
346
|
-
try {
|
|
347
|
-
const [trueIssueBody, comments] = await Promise.all([
|
|
348
|
-
github.getIssueBody(owner, repo, request.issueNumber),
|
|
349
|
-
github.listIssueComments(owner, repo, request.issueNumber),
|
|
350
|
-
]);
|
|
351
|
-
const formattedComments = comments
|
|
352
|
-
.filter((c) => c.body.trim())
|
|
353
|
-
.map((c) => `--- @${c.user} (${c.createdAt}) ---\n${c.body}`)
|
|
354
|
-
.join("\n\n");
|
|
355
|
-
const combinedContext = [
|
|
356
|
-
trueIssueBody ? `# Issue body\n\n${trueIssueBody}` : "",
|
|
357
|
-
formattedComments ? `# Issue thread (oldest → newest)\n\n${formattedComments}` : "",
|
|
358
|
-
]
|
|
359
|
-
.filter(Boolean)
|
|
360
|
-
.join("\n\n");
|
|
361
|
-
if (combinedContext) {
|
|
362
|
-
// Single screening call over the entire combined context.
|
|
363
|
-
const screen = await screenForInjection(combinedContext);
|
|
364
|
-
const annotated = screen.flagged
|
|
365
|
-
? `${flagPrefix(screen.reason)}${combinedContext}`
|
|
366
|
-
: combinedContext;
|
|
367
|
-
(request.extra ||= {}).combinedContext = annotated;
|
|
368
|
-
// Clear individual issueBody so simple.ts uses combinedContext
|
|
369
|
-
// exclusively (avoids double-rendering the body).
|
|
370
|
-
request.issueBody = "";
|
|
371
|
-
if (screen.flagged) {
|
|
372
|
-
console.warn(`[dispatch] Screener flagged combined issue context for ${owner}/${repo}#${request.issueNumber}: ${screen.reason || "no reason"}`);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
catch (err) {
|
|
377
|
-
const m = err instanceof Error ? err.message : String(err);
|
|
378
|
-
console.warn(`[dispatch] Failed to fetch/screen issue context: ${m}`);
|
|
379
|
-
// Non-fatal — workflow proceeds with whatever context the envelope had.
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
const slackPost = slackTriggerId && slackConnector && typeof channelId === "string" && typeof threadId === "string"
|
|
383
|
-
? async (msg) => {
|
|
384
|
-
try {
|
|
385
|
-
await slackConnector.sendMessage(channelId, threadId, msg);
|
|
386
|
-
}
|
|
387
|
-
catch (err) {
|
|
388
|
-
const m = err instanceof Error ? err.message : String(err);
|
|
389
|
-
console.warn(`[dispatch] Failed to post to Slack thread: ${m}`);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
: undefined;
|
|
393
|
-
// In-place "task list" progress checklist — opt-in per workflow via
|
|
394
|
-
// `status_checklist: true` in the YAML. Build a transport for whichever
|
|
395
|
-
// surface triggered the run (GitHub comment and/or Slack thread) and hand
|
|
396
|
-
// the runner a ProgressNotifier instead of letting it post a comment per
|
|
397
|
-
// phase. The notifier is created inside onRunStart because it needs the
|
|
398
|
-
// workflow-run id (only known once simple.ts creates the row) to persist
|
|
399
|
-
// its in-place update handles to scratch.notifier. better-sqlite3 is
|
|
400
|
-
// synchronous and simple.ts invokes onRunStart synchronously before the
|
|
401
|
-
// first reporter call, so the notifier is ready in time; the proxy guards
|
|
402
|
-
// the brief window before assignment.
|
|
403
|
-
// Which in-place surface(s) can the checklist edit? Knowable synchronously
|
|
404
|
-
// (transport existence needs only github/issue or slack/channel/thread —
|
|
405
|
-
// the run id is needed solely for persistence + resume handles).
|
|
406
|
-
const ghChecklist = !!(github && typeof issueNumber === "number");
|
|
407
|
-
const slackChecklist = !!(slackConnector && typeof channelId === "string" && typeof threadId === "string");
|
|
408
|
-
let statusChecklist = false;
|
|
409
|
-
try {
|
|
410
|
-
// Only activate the checklist when the workflow opts in AND there's a
|
|
411
|
-
// surface to render it on — otherwise leave `reporter` undefined so the
|
|
412
|
-
// runner keeps its legacy per-phase comment behavior instead of going
|
|
413
|
-
// silent.
|
|
414
|
-
statusChecklist =
|
|
415
|
-
getWorkflow(workflowName).status_checklist === true && (ghChecklist || slackChecklist);
|
|
416
|
-
}
|
|
417
|
-
catch {
|
|
418
|
-
/* unknown workflow — surfaced downstream by runSimpleWorkflow */
|
|
419
|
-
}
|
|
420
|
-
let notifier;
|
|
421
|
-
const reporterProxy = statusChecklist
|
|
422
|
-
? {
|
|
423
|
-
start: (m) => notifier?.start(m) ?? Promise.resolve(),
|
|
424
|
-
step: (k, s, d) => notifier?.step(k, s, d) ?? Promise.resolve(),
|
|
425
|
-
insertStep: (st, b) => notifier?.insertStep(st, b) ?? Promise.resolve(),
|
|
426
|
-
note: (m) => notifier?.note(m) ?? Promise.resolve(),
|
|
427
|
-
noteApproval: (m, meta) => notifier?.noteApproval(m, meta) ?? Promise.resolve(),
|
|
428
|
-
footer: (m) => notifier?.footer(m) ?? Promise.resolve(),
|
|
429
|
-
noteTerminal: (m) => notifier?.noteTerminal(m) ?? Promise.resolve(),
|
|
430
|
-
}
|
|
431
|
-
: undefined;
|
|
432
|
-
const notifierOnRunStart = statusChecklist
|
|
433
|
-
? (runId) => {
|
|
434
|
-
try {
|
|
435
|
-
const saved = ((db.runs.getRun(runId)?.scratch?.notifier) ?? {});
|
|
436
|
-
const persist = (patch) => {
|
|
437
|
-
const cur = ((db.runs.getRun(runId)?.scratch?.notifier) ?? {});
|
|
438
|
-
db.runs.mergeScratch(runId, { notifier: { ...cur, ...patch } });
|
|
439
|
-
};
|
|
440
|
-
const transports = [];
|
|
441
|
-
if (ghChecklist && github && typeof issueNumber === "number") {
|
|
442
|
-
transports.push(new GitHubTransport({
|
|
443
|
-
github,
|
|
444
|
-
owner,
|
|
445
|
-
repo,
|
|
446
|
-
issueNumber,
|
|
447
|
-
commentId: saved.githubCommentId,
|
|
448
|
-
save: (id) => persist({ githubCommentId: id }),
|
|
449
|
-
}));
|
|
450
|
-
}
|
|
451
|
-
if (slackChecklist && slackConnector && typeof channelId === "string" && typeof threadId === "string") {
|
|
452
|
-
transports.push(new SlackTransport({
|
|
453
|
-
slack: slackConnector,
|
|
454
|
-
channel: channelId,
|
|
455
|
-
thread: threadId,
|
|
456
|
-
ts: saved.slackTs,
|
|
457
|
-
save: (ts) => persist({ slackTs: ts, slackChannel: channelId, slackThread: threadId }),
|
|
458
|
-
}));
|
|
459
|
-
}
|
|
460
|
-
if (transports.length > 0)
|
|
461
|
-
notifier = new ProgressNotifier(transports);
|
|
462
|
-
}
|
|
463
|
-
catch (err) {
|
|
464
|
-
const m = err instanceof Error ? err.message : String(err);
|
|
465
|
-
console.warn(`[dispatch] notifier setup failed: ${m}`);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
: undefined;
|
|
469
|
-
const callbacks = {
|
|
470
|
-
reporter: reporterProxy,
|
|
471
|
-
publicUrl: config.publicUrl,
|
|
472
|
-
postComment: slackPost
|
|
473
|
-
?? (github && issueNumber
|
|
474
|
-
? async (msg) => {
|
|
475
|
-
try {
|
|
476
|
-
await github.postComment(owner, repo, issueNumber, msg);
|
|
477
|
-
}
|
|
478
|
-
catch (err) {
|
|
479
|
-
const m = err instanceof Error ? err.message : String(err);
|
|
480
|
-
console.warn(`[dispatch] Failed to post comment: ${m}`);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
: undefined),
|
|
484
|
-
onPhaseStart: async (phase) => {
|
|
485
|
-
console.log(`[dispatch] ▶ ${workflowName}/${phase}`);
|
|
486
|
-
// Refresh the Slack thinking indicator so long-running phases
|
|
487
|
-
// don't leave the thread looking dead. threadId doubles as both
|
|
488
|
-
// the message anchor and the thread root for DM threads.
|
|
489
|
-
if (slackPost && slackConnector && typeof channelId === "string" && typeof threadId === "string") {
|
|
490
|
-
slackConnector.showTyping(channelId, threadId, threadId).catch(() => { });
|
|
491
|
-
}
|
|
492
|
-
},
|
|
493
|
-
onPhaseEnd: async (phase, result) => console.log(`[dispatch] ◀ ${workflowName}/${phase}: ${result.success ? "OK" : "FAILED"}`),
|
|
494
|
-
onRunStart: notifierOnRunStart
|
|
495
|
-
? async (runId) => {
|
|
496
|
-
// Synchronous notifier setup must finish before simple.ts calls
|
|
497
|
-
// reporter.start() (the next statement after it invokes this), so
|
|
498
|
-
// run it first, then chain any caller-provided onRunStart.
|
|
499
|
-
notifierOnRunStart(runId);
|
|
500
|
-
if (onRunStart)
|
|
501
|
-
await onRunStart(runId);
|
|
502
|
-
}
|
|
503
|
-
: onRunStart,
|
|
504
|
-
};
|
|
505
|
-
try {
|
|
506
|
-
const result = await runSimpleWorkflow(workflowName, request, {
|
|
507
|
-
model: config.model,
|
|
508
|
-
maxTurns: config.maxTurns,
|
|
509
|
-
stateDir: config.stateDir,
|
|
510
|
-
sandboxDir: config.sandboxDir,
|
|
511
|
-
sessionsDir: config.sessionsDir,
|
|
512
|
-
sandbox: config.sandbox,
|
|
513
|
-
buildAssets: config.buildAssets,
|
|
514
|
-
buildAssetsDir: config.buildAssetsDir,
|
|
515
|
-
otel: config.otel,
|
|
516
|
-
}, callbacks, db, config.models, config.approval, config.bootstrapLabel, config.variants);
|
|
517
|
-
const summary = result.phases.map((p) => `${p.phase}=${p.success ? "ok" : "fail"}`).join(", ");
|
|
518
|
-
if (result.paused) {
|
|
519
|
-
console.log(`[dispatch] ${workflowName} paused (${summary})`);
|
|
520
|
-
}
|
|
521
|
-
else if (result.success) {
|
|
522
|
-
console.log(`[dispatch] ${workflowName} completed (${summary})`);
|
|
523
|
-
}
|
|
524
|
-
else {
|
|
525
|
-
console.warn(`[dispatch] ${workflowName} failed (${summary})`);
|
|
526
|
-
}
|
|
527
|
-
return { success: result.success, paused: result.paused };
|
|
528
|
-
}
|
|
529
|
-
catch (err) {
|
|
530
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
531
|
-
console.error(`[dispatch] ${workflowName} threw: ${msg}`);
|
|
532
|
-
return { success: false, error: msg };
|
|
533
|
-
}
|
|
534
|
-
};
|
|
535
|
-
// Set up connector registry
|
|
536
|
-
const registry = new ConnectorRegistry();
|
|
537
|
-
// Message delivery service for cron output
|
|
538
|
-
const delivery = new MessageDeliveryService();
|
|
539
|
-
// Shared HTTP server — always boots, independent of GitHub. `main()` owns the
|
|
540
|
-
// Hono app + serve() lifecycle that the webhook connector used to own, so the
|
|
541
|
-
// `lastlight` CLI + admin dashboard + /api/* work even with no GitHub App
|
|
542
|
-
// (chat-only / PAT modes). The root /health is what the CLI hits
|
|
543
|
-
// (src/cli/cli.ts, cli-server.ts).
|
|
544
|
-
const app = new Hono();
|
|
545
|
-
app.get("/health", (c) => c.json({ status: "ok" }));
|
|
546
|
-
// GitHub webhook connector (optional — requires both webhook secret and GitHub
|
|
547
|
-
// App). It registers /webhooks/github onto the shared app; it no longer owns
|
|
548
|
-
// the HTTP listener.
|
|
549
|
-
let githubConnector = null;
|
|
550
|
-
if (config.webhookSecret && config.githubApp) {
|
|
551
|
-
githubConnector = new GitHubWebhookConnector({
|
|
552
|
-
port: config.port,
|
|
553
|
-
webhookSecret: config.webhookSecret,
|
|
554
|
-
botLogin: config.botLogin,
|
|
555
|
-
app,
|
|
556
|
-
});
|
|
557
|
-
registry.register(githubConnector);
|
|
558
|
-
}
|
|
559
|
-
// Slack connector (optional — only if SLACK_BOT_TOKEN is set)
|
|
560
|
-
let slackConnector = null;
|
|
561
|
-
if (config.slack) {
|
|
562
|
-
slackConnector = new SlackConnector({
|
|
563
|
-
botToken: config.slack.botToken,
|
|
564
|
-
mode: config.slack.mode,
|
|
565
|
-
appToken: config.slack.appToken,
|
|
566
|
-
signingSecret: config.slack.signingSecret,
|
|
567
|
-
// Webhook mode mounts /webhooks/slack on the shared HTTP server.
|
|
568
|
-
honoApp: app,
|
|
569
|
-
allowedUsers: config.slack.allowedUsers,
|
|
570
|
-
deliveryChannel: config.slack.deliveryChannel,
|
|
571
|
-
botIdentifier: "", // Will be resolved from Slack API on connect
|
|
572
|
-
}, sessionManager);
|
|
573
|
-
registry.register(slackConnector);
|
|
574
|
-
// Register Slack as a delivery target for cron reports
|
|
575
|
-
if (config.slack.deliveryChannel) {
|
|
576
|
-
delivery.register("slack", (msg) => slackConnector.sendToDeliveryChannel(msg));
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
// Construct the cron scheduler before mounting admin so the dashboard can
|
|
580
|
-
// list/toggle/edit registered cron jobs. Jobs are registered further down
|
|
581
|
-
// (after we know whether webhooks are enabled). The runner closes over
|
|
582
|
-
// `dispatchWorkflow`, which is defined earlier in this file.
|
|
583
|
-
const cron = new CronScheduler(db, async (workflowName, context) => {
|
|
584
|
-
const { dispatched, failures } = await dispatchCronWorkflow(workflowName, context, dispatchWorkflow);
|
|
585
|
-
if (failures > 0) {
|
|
586
|
-
console.warn(`[cron] ${workflowName}: ${failures}/${dispatched} dispatches failed`);
|
|
587
|
-
}
|
|
588
|
-
});
|
|
589
|
-
// Options for the ledger-driven resume machinery (`resumeSimpleRun`). Shared
|
|
590
|
-
// by the boot-time orphan sweep (`resumeOrphanedWorkflows`, below) AND the
|
|
591
|
-
// dashboard/CLI "retry a failed run" callback (`retryWorkflow`, in mountAdmin)
|
|
592
|
-
// so both reconstruct context from the stored `workflow_runs` row identically.
|
|
593
|
-
const resumeOpts = {
|
|
594
|
-
db,
|
|
595
|
-
github,
|
|
596
|
-
config: {
|
|
597
|
-
model: config.model,
|
|
598
|
-
maxTurns: config.maxTurns,
|
|
599
|
-
stateDir: config.stateDir,
|
|
600
|
-
sandboxDir: config.sandboxDir,
|
|
601
|
-
sessionsDir: config.sessionsDir,
|
|
602
|
-
sandbox: config.sandbox,
|
|
603
|
-
buildAssets: config.buildAssets,
|
|
604
|
-
buildAssetsDir: config.buildAssetsDir,
|
|
605
|
-
otel: config.otel,
|
|
606
|
-
},
|
|
607
|
-
models: config.models,
|
|
608
|
-
variants: config.variants,
|
|
609
|
-
approvalConfig: config.approval,
|
|
610
|
-
bootstrapLabel: config.bootstrapLabel,
|
|
611
|
-
publicUrl: config.publicUrl,
|
|
612
|
-
slackPoster: slackConnector
|
|
613
|
-
? (channelId, threadId, msg) => slackConnector.sendMessage(channelId, threadId, msg).then(() => { })
|
|
614
|
-
: undefined,
|
|
615
|
-
};
|
|
616
|
-
// Mount admin dashboard on the shared HTTP server (always available).
|
|
617
|
-
{
|
|
618
|
-
mountAdmin(app, db, {
|
|
619
|
-
cronScheduler: cron,
|
|
620
|
-
stateDir: config.stateDir,
|
|
621
|
-
sessionsDir: config.sessionsDir,
|
|
622
|
-
buildAssetsDir: config.buildAssetsDir,
|
|
623
|
-
buildAssets: config.buildAssets,
|
|
624
|
-
adminPassword: process.env.ADMIN_PASSWORD ?? "",
|
|
625
|
-
adminSecret: process.env.ADMIN_SECRET ?? "lastlight-dev-secret",
|
|
626
|
-
publicConfig: config.publicConfig,
|
|
627
|
-
builtInRoot: config.builtInRoot,
|
|
628
|
-
overlayDir: config.overlayDir,
|
|
629
|
-
slackOAuthClientId: process.env.SLACK_OAUTH_CLIENT_ID,
|
|
630
|
-
slackOAuthClientSecret: process.env.SLACK_OAUTH_CLIENT_SECRET,
|
|
631
|
-
slackOAuthRedirectUri: process.env.SLACK_OAUTH_REDIRECT_URI,
|
|
632
|
-
slackAllowedWorkspace: process.env.SLACK_ALLOWED_WORKSPACE,
|
|
633
|
-
githubOAuthClientId: process.env.GITHUB_OAUTH_CLIENT_ID,
|
|
634
|
-
githubOAuthClientSecret: process.env.GITHUB_OAUTH_CLIENT_SECRET,
|
|
635
|
-
githubOAuthRedirectUri: process.env.GITHUB_OAUTH_REDIRECT_URI,
|
|
636
|
-
githubAllowedOrg: process.env.GITHUB_ALLOWED_ORG,
|
|
637
|
-
resumeWorkflow: async (workflowRun, sender) => {
|
|
638
|
-
if (!github) {
|
|
639
|
-
console.warn(`[admin] Cannot resume workflow ${workflowRun.id}: GitHub App not configured`);
|
|
640
|
-
return;
|
|
641
|
-
}
|
|
642
|
-
// Derive owner/repo for the resume dispatch. Prefer the triggerId
|
|
643
|
-
// (owner/repo#N) but fall back to the stored repo + context.owner so a
|
|
644
|
-
// run keyed on a non-GitHub triggerId (e.g. a Slack-thread override)
|
|
645
|
-
// still resumes from the dashboard/focused-approval flow.
|
|
646
|
-
let [owner, repo] = workflowRun.triggerId.includes("/")
|
|
647
|
-
? workflowRun.triggerId.replace(/#\d+$/, "").split("/")
|
|
648
|
-
: ["", ""];
|
|
649
|
-
if (!owner || !repo) {
|
|
650
|
-
const ctxOwner = workflowRun.context?.owner || "";
|
|
651
|
-
const storedRepo = workflowRun.repo || "";
|
|
652
|
-
owner = ctxOwner || (storedRepo.includes("/") ? storedRepo.split("/")[0] : "");
|
|
653
|
-
repo = storedRepo.includes("/") ? storedRepo.split("/")[1] : storedRepo;
|
|
654
|
-
}
|
|
655
|
-
const issueNumber = workflowRun.issueNumber;
|
|
656
|
-
if (!owner || !repo || !issueNumber) {
|
|
657
|
-
console.warn(`[admin] Cannot resume workflow ${workflowRun.id}: missing owner/repo/issueNumber`);
|
|
658
|
-
return;
|
|
659
|
-
}
|
|
660
|
-
db.runs.setRunning(workflowRun.id);
|
|
661
|
-
console.log(`[admin] Resuming ${workflowRun.workflowName} for ${owner}/${repo}#${issueNumber} after dashboard approval by ${sender}`);
|
|
662
|
-
dispatchWorkflow(workflowRun.workflowName, {
|
|
663
|
-
repo: `${owner}/${repo}`,
|
|
664
|
-
issueNumber,
|
|
665
|
-
title: `Issue #${issueNumber}`,
|
|
666
|
-
body: "",
|
|
667
|
-
sender,
|
|
668
|
-
_triggerType: "admin",
|
|
669
|
-
}).catch((err) => console.error(`[admin] Resume failed:`, err));
|
|
670
|
-
},
|
|
671
|
-
// Retry a FAILED run, resuming from the phase that failed with the same
|
|
672
|
-
// context. Unlike `resumeWorkflow` (approval-gate resume, which rebuilds a
|
|
673
|
-
// lossy owner/repo/issueNumber context and bails on non-issue runs), this
|
|
674
|
-
// re-enters via `resumeSimpleRun`, reconstructing the full context from the
|
|
675
|
-
// stored `workflow_runs.context` + `scratch` — so it also retries
|
|
676
|
-
// Slack-thread-scoped runs (e.g. an `explore` started from Slack). The
|
|
677
|
-
// failed phase's ledger row is `success=0`, so it re-runs while
|
|
678
|
-
// already-succeeded phases skip.
|
|
679
|
-
retryWorkflow: async (workflowRun, sender) => {
|
|
680
|
-
if (workflowRun.status !== "failed") {
|
|
681
|
-
console.warn(`[admin] Cannot retry ${workflowRun.id}: status is '${workflowRun.status}', not 'failed'`);
|
|
682
|
-
return;
|
|
683
|
-
}
|
|
684
|
-
// Compare-and-set: flip failed→running and clear the terminal markers.
|
|
685
|
-
// If a racing retry already flipped it, changes===0 and we don't dispatch.
|
|
686
|
-
const changed = db.runs.restartRun(workflowRun.id);
|
|
687
|
-
if (changed !== 1) {
|
|
688
|
-
console.warn(`[admin] Retry ${workflowRun.id}: run is no longer 'failed' (raced) — skipping dispatch`);
|
|
689
|
-
return;
|
|
690
|
-
}
|
|
691
|
-
const fresh = db.runs.getRun(workflowRun.id);
|
|
692
|
-
if (!fresh)
|
|
693
|
-
return;
|
|
694
|
-
console.log(`[admin] Retrying ${fresh.workflowName} run ${fresh.id} (was on phase=${workflowRun.currentPhase}) by ${sender}`);
|
|
695
|
-
resumeSimpleRun(fresh, resumeOpts).catch((err) => console.error(`[admin] Retry ${fresh.id} failed:`, err));
|
|
696
|
-
},
|
|
697
|
-
});
|
|
698
|
-
console.log(`[admin] Dashboard mounted at /admin`);
|
|
699
|
-
}
|
|
700
|
-
// Protect API endpoints with auth when any login method is configured
|
|
701
|
-
// (password OR OAuth) — same gate as the dashboard, via the shared helper.
|
|
702
|
-
const adminPassword = process.env.ADMIN_PASSWORD ?? "";
|
|
703
|
-
const adminSecret = process.env.ADMIN_SECRET ?? "lastlight-dev-secret";
|
|
704
|
-
const apiAuthEnabled = authIsEnabled({
|
|
705
|
-
adminPassword,
|
|
706
|
-
slackOAuthClientId: process.env.SLACK_OAUTH_CLIENT_ID,
|
|
707
|
-
slackOAuthClientSecret: process.env.SLACK_OAUTH_CLIENT_SECRET,
|
|
708
|
-
githubOAuthClientId: process.env.GITHUB_OAUTH_CLIENT_ID,
|
|
709
|
-
githubOAuthClientSecret: process.env.GITHUB_OAUTH_CLIENT_SECRET,
|
|
710
|
-
githubAllowedOrg: process.env.GITHUB_ALLOWED_ORG,
|
|
711
|
-
});
|
|
712
|
-
if (apiAuthEnabled) {
|
|
713
|
-
app.use("/api/*", authMiddleware(apiAuthEnabled, adminSecret));
|
|
714
|
-
console.log(`[api] API endpoints protected with auth`);
|
|
715
|
-
}
|
|
716
|
-
// API endpoint for CLI triggers
|
|
717
|
-
app.post("/api/run", async (c) => {
|
|
718
|
-
const body = await c.req.json();
|
|
719
|
-
// Accept either `skill` (legacy) or `workflow` (preferred). They map 1:1
|
|
720
|
-
// for the four agent skills (issue-triage, pr-review, repo-health,
|
|
721
|
-
// issue-comment) which are now backed by single-phase YAML workflows.
|
|
722
|
-
const workflowName = (body.workflow ?? body.skill);
|
|
723
|
-
const context = (body.context ?? {});
|
|
724
|
-
if (!workflowName) {
|
|
725
|
-
return c.json({ error: "Missing 'workflow' (or 'skill') field" }, 400);
|
|
726
|
-
}
|
|
727
|
-
console.log(`[api] CLI triggered: workflow=${workflowName}`);
|
|
728
|
-
// Run asynchronously — return immediately with a stable id the caller
|
|
729
|
-
// can correlate with workflow_runs in the dashboard.
|
|
730
|
-
const executionId = randomUUID();
|
|
731
|
-
dispatchWorkflow(workflowName, { ...context, _triggerType: "api" }).catch((err) => {
|
|
732
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
733
|
-
console.error(`[api] workflow ${workflowName} failed: ${msg}`);
|
|
734
|
-
});
|
|
735
|
-
return c.json({ accepted: true, executionId, workflow: workflowName }, 202);
|
|
736
|
-
});
|
|
737
|
-
// API endpoint for build cycle triggers (issue URL)
|
|
738
|
-
app.post("/api/build", async (c) => {
|
|
739
|
-
const body = await c.req.json();
|
|
740
|
-
const { owner, repo, issueNumber, issueTitle, issueBody, issueLabels, sender } = body;
|
|
741
|
-
if (!owner || !repo || !issueNumber) {
|
|
742
|
-
return c.json({ error: "Missing owner, repo, or issueNumber" }, 400);
|
|
743
|
-
}
|
|
744
|
-
console.log(`[api] CLI build triggered: ${owner}/${repo}#${issueNumber}`);
|
|
745
|
-
// If labels weren't supplied, fetch them so the orchestrator can detect
|
|
746
|
-
// bootstrap tasks (lastlight:bootstrap label) and skip the BLOCKED gate.
|
|
747
|
-
let resolvedLabels = issueLabels;
|
|
748
|
-
if (!resolvedLabels && github) {
|
|
749
|
-
try {
|
|
750
|
-
const issue = await github.getIssue(owner, repo, issueNumber);
|
|
751
|
-
resolvedLabels = (issue.labels || []).map((l) => typeof l === "string" ? l : l.name).filter(Boolean);
|
|
752
|
-
}
|
|
753
|
-
catch { /* non-fatal */ }
|
|
754
|
-
}
|
|
755
|
-
// Run build cycle asynchronously via the generic dispatcher
|
|
756
|
-
dispatchWorkflow("build", {
|
|
757
|
-
repo: `${owner}/${repo}`,
|
|
758
|
-
issueNumber,
|
|
759
|
-
title: issueTitle || `Issue #${issueNumber}`,
|
|
760
|
-
body: issueBody || "",
|
|
761
|
-
labels: resolvedLabels,
|
|
762
|
-
sender: sender || "cli",
|
|
763
|
-
_triggerType: "api",
|
|
764
|
-
}).catch((err) => {
|
|
765
|
-
console.error(`[api] Build failed:`, err);
|
|
766
|
-
});
|
|
767
|
-
return c.json({ accepted: true, owner, repo, issueNumber }, 202);
|
|
768
|
-
});
|
|
769
|
-
// Handle events from any connector
|
|
770
|
-
// Handle events from any connector. The dispatcher turns each EventEnvelope
|
|
771
|
-
// into a workflow dispatch (or in-process handler run) through one testable
|
|
772
|
-
// seam; every per-event branch lives in src/engine/dispatcher.ts. main()
|
|
773
|
-
// only constructs the deps and relays the typed outcome.
|
|
774
|
-
const dispatchDeps = {
|
|
775
|
-
db,
|
|
776
|
-
github,
|
|
777
|
-
dispatchWorkflow,
|
|
778
|
-
sessionManager,
|
|
779
|
-
// One in-process chat turn. handleChatMessage manages session resume via
|
|
780
|
-
// sessionManager internally; the dispatcher uses resumeAgentSessionId only
|
|
781
|
-
// to decide whether to persist a newly-minted agent session id.
|
|
782
|
-
runChat: (message, messagingSessionId, sender) => handleChatMessage(message, messagingSessionId, sender, sessionManager, { chatRunner, sessionsHomeDir: config.sessionsDir }, { model: resolveModel(config.models, "chat"), maxTurns: 10 }),
|
|
783
|
-
reviewPostsCheck: config.reviewPostsCheck,
|
|
784
|
-
publicUrl: config.publicUrl,
|
|
785
|
-
};
|
|
786
|
-
// Wire Slack approval buttons into the SAME approval-resolution path as the
|
|
787
|
-
// `/approve` slash command. The connector verifies + parses the button click
|
|
788
|
-
// (on /webhooks/slack/interactions) and hands us the workflow run id; we force
|
|
789
|
-
// the route to `approval-response`, reusing the whole dispatcher/resume seam.
|
|
790
|
-
if (slackConnector) {
|
|
791
|
-
slackConnector.onApprovalAction(async ({ decision, workflowRunId, sender, envelope }) => {
|
|
792
|
-
await dispatch(envelope, {
|
|
793
|
-
...dispatchDeps,
|
|
794
|
-
route: async () => ({
|
|
795
|
-
action: "handler",
|
|
796
|
-
handler: "approval-response",
|
|
797
|
-
context: { decision, workflowRunId, sender, source: "slack" },
|
|
798
|
-
}),
|
|
799
|
-
});
|
|
800
|
-
});
|
|
801
|
-
}
|
|
802
|
-
// One chat turn over HTTP — `lastlight chat` without a messaging platform.
|
|
803
|
-
// Routes through the SAME dispatcher seam Slack uses (forcing the chat
|
|
804
|
-
// handler), so the executions row, agent-session resume, and telemetry are
|
|
805
|
-
// recorded identically and the turn shows up in the dashboard Chat tab. The
|
|
806
|
-
// synthetic envelope's reply() just captures the assistant text to return.
|
|
807
|
-
app.post("/api/chat", async (c) => {
|
|
808
|
-
const body = await c.req.json().catch(() => ({}));
|
|
809
|
-
const message = typeof body.message === "string" ? body.message : "";
|
|
810
|
-
if (!message.trim())
|
|
811
|
-
return c.json({ error: "Missing 'message'" }, 400);
|
|
812
|
-
const user = typeof body.user === "string" && body.user ? body.user : "cli";
|
|
813
|
-
const threadId = typeof body.thread === "string" && body.thread ? body.thread : null;
|
|
814
|
-
const session = sessionManager.getOrCreateSession({
|
|
815
|
-
platform: "cli", channelId: user, threadId, userId: user,
|
|
816
|
-
});
|
|
817
|
-
let reply = "";
|
|
818
|
-
const envelope = {
|
|
819
|
-
id: session.id,
|
|
820
|
-
source: "cli",
|
|
821
|
-
type: "message",
|
|
822
|
-
sender: user,
|
|
823
|
-
senderIsBot: false,
|
|
824
|
-
body: message,
|
|
825
|
-
raw: { cli: true },
|
|
826
|
-
reply: async (msg) => { reply = msg; },
|
|
827
|
-
timestamp: new Date(),
|
|
828
|
-
};
|
|
829
|
-
const outcome = await dispatch(envelope, {
|
|
830
|
-
...dispatchDeps,
|
|
831
|
-
route: async () => ({
|
|
832
|
-
action: "handler",
|
|
833
|
-
handler: "chat",
|
|
834
|
-
context: { sessionId: session.id, message, sender: user },
|
|
835
|
-
}),
|
|
836
|
-
});
|
|
837
|
-
return c.json({ text: reply, thread: threadId ?? session.id, sessionId: session.id, outcome: outcome.kind });
|
|
838
|
-
});
|
|
839
|
-
const handleEnvelope = async (envelope) => {
|
|
840
|
-
console.log(`[event] ${envelope.source}:${envelope.type} from ${envelope.sender}${envelope.repo ? ` on ${envelope.repo}` : ""}`);
|
|
841
|
-
try {
|
|
842
|
-
const outcome = await dispatch(envelope, dispatchDeps);
|
|
843
|
-
if (outcome.kind === "ignored") {
|
|
844
|
-
console.log(`[event] Ignored: ${outcome.reason}`);
|
|
845
|
-
}
|
|
846
|
-
else if (outcome.kind === "skipped") {
|
|
847
|
-
console.log(`[event] Skipped: ${outcome.reason}`);
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
catch (err) {
|
|
851
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
852
|
-
console.error(`[event] dispatch threw: ${msg}`);
|
|
853
|
-
}
|
|
854
|
-
};
|
|
855
|
-
// Batch bursty messaging input per session BEFORE routing: a rapid Slack
|
|
856
|
-
// burst is collected, sorted into send order, and collapsed into ONE
|
|
857
|
-
// envelope so it's classified once and answered as a single ordered turn.
|
|
858
|
-
// Gated on `type === "message"` — only messaging connectors emit that here
|
|
859
|
-
// (GitHub events have richer types; the CLI dispatches directly, not via the
|
|
860
|
-
// registry). Tunable settle window via CHAT_BATCH_DEBOUNCE_MS (default 700ms;
|
|
861
|
-
// 0 disables).
|
|
862
|
-
const messageBatcher = new MessageBatcher({
|
|
863
|
-
dispatch: handleEnvelope,
|
|
864
|
-
debounceMs: Number.parseInt(process.env.CHAT_BATCH_DEBOUNCE_MS || "700", 10),
|
|
865
|
-
});
|
|
866
|
-
registry.onEvent(async (envelope) => {
|
|
867
|
-
if (envelope.type === "message") {
|
|
868
|
-
messageBatcher.submit(envelope);
|
|
869
|
-
return;
|
|
870
|
-
}
|
|
871
|
-
await handleEnvelope(envelope);
|
|
872
|
-
});
|
|
873
|
-
// Cron jobs — fan out from each tick into one workflow run per managed repo
|
|
874
|
-
// (see dispatchCronWorkflow). The scheduler itself was constructed earlier
|
|
875
|
-
// so the admin dashboard could be wired with it. Every job (health/security
|
|
876
|
-
// reports + issue/PR polling) drives a GitHub-scoped workflow, so skip
|
|
877
|
-
// registration entirely without a GitHub client — a chat-only instance would
|
|
878
|
-
// otherwise fire periodic no-op dispatch failures.
|
|
879
|
-
const webhooksEnabled = !!(config.webhookSecret && config.githubApp);
|
|
880
|
-
if (github) {
|
|
881
|
-
const jobs = getJobs({ webhooksEnabled, db });
|
|
882
|
-
for (const job of jobs) {
|
|
883
|
-
cron.register(job);
|
|
884
|
-
}
|
|
885
|
-
if (webhooksEnabled) {
|
|
886
|
-
console.log("[cron] Webhooks enabled — skipping issue/PR polling crons");
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
else {
|
|
890
|
-
console.log("[cron] No GitHub client — skipping all cron jobs (chat-only mode)");
|
|
891
|
-
}
|
|
892
|
-
// Start everything
|
|
893
|
-
await registry.startAll();
|
|
894
|
-
console.log("[main] All connectors started");
|
|
895
|
-
console.log("[main] Cron jobs registered");
|
|
896
|
-
// Open the shared HTTP listener. All routes (admin, /api/*, /health,
|
|
897
|
-
// /webhooks/github, /webhooks/slack) are registered synchronously above, so
|
|
898
|
-
// the port is ready the moment it opens. Always boots — chat-only, PAT, and
|
|
899
|
-
// full GitHub App modes alike.
|
|
900
|
-
const server = serve({ fetch: app.fetch, port: config.port, hostname: "0.0.0.0" });
|
|
901
|
-
console.log(`[http] Listening on port ${config.port}`);
|
|
902
|
-
// Chat runs in-process via pi-ai — no long-lived server to boot.
|
|
903
|
-
// Boot-time recovery: any workflow_runs left in 'running' state from a
|
|
904
|
-
// previous harness lifetime have already had their sandbox containers
|
|
905
|
-
// killed by cleanupOrphanedSandboxes(). Mark their stale execution rows as
|
|
906
|
-
// failed and re-dispatch each run so the runner can pick up after the last
|
|
907
|
-
// completed phase. Skips 'paused' runs — those intentionally wait for a
|
|
908
|
-
// human approval and are resumed via the dashboard / GitHub comment flow.
|
|
909
|
-
resumeOrphanedWorkflows(resumeOpts).catch((err) => console.error("[main] Resume sweep failed:", err));
|
|
910
|
-
console.log("[main] Ready to receive events");
|
|
911
|
-
// Graceful shutdown
|
|
912
|
-
const shutdown = async () => {
|
|
913
|
-
console.log("\n[main] Shutting down...");
|
|
914
|
-
cron.stopAll();
|
|
915
|
-
await registry.stopAll();
|
|
916
|
-
// The shared HTTP server is owned here now (no longer by the webhook
|
|
917
|
-
// connector's stop()), so close it explicitly.
|
|
918
|
-
server.close();
|
|
919
|
-
if (!telemetryShutdownStarted) {
|
|
920
|
-
telemetryShutdownStarted = true;
|
|
921
|
-
await shutdownTelemetry();
|
|
922
|
-
}
|
|
923
|
-
db.close();
|
|
924
|
-
process.exit(0);
|
|
925
|
-
};
|
|
926
|
-
process.on("SIGINT", shutdown);
|
|
927
|
-
process.on("SIGTERM", shutdown);
|
|
928
|
-
}
|
|
929
|
-
main().catch((err) => {
|
|
930
|
-
console.error("[main] Fatal error:", err);
|
|
931
|
-
// Exit 78 (EX_CONFIG) to signal Docker restart policy that looping won't help.
|
|
932
|
-
// Common causes: bad PEM, wrong App ID, missing env vars.
|
|
933
|
-
const msg = err?.message || "";
|
|
934
|
-
const isConfig = msg.includes("could not be decoded") ||
|
|
935
|
-
msg.includes("not found") ||
|
|
936
|
-
msg.includes("ENOENT") ||
|
|
937
|
-
msg.includes("required");
|
|
938
|
-
process.exit(isConfig ? 78 : 1);
|
|
939
|
-
});
|
|
940
|
-
//# sourceMappingURL=index.js.map
|