elanous 0.2.8 → 0.2.10
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/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/2DsiDfDCks44g1cPUMhiW/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/24-e6d28380efab7faf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3615-740dd270764ffa24.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3734-1015fe991eb9b7e7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5012-a7fe6f3c8b6fe45a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5783-ef40ebfc91d55230.js +1 -0
- package/apps/pwa/out/_next/static/chunks/62-ff227c3edb8b248b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7633-1b3709b0854c9412.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7951-8b03dbb10b24cd06.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8067-87ce253ec2f23697.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{5080-1613752891fcadb4.js → 8195-6fde3d7a39859206.js} +5 -5
- package/apps/pwa/out/_next/static/chunks/8894-b5fb36bab837cd29.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9454-84d0d5d77446bd92.js +1 -0
- package/apps/pwa/out/_next/static/chunks/988-5f92e7ab7ccea3dd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9989-5582582f06429840.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-d2253d7e58ef1890.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-5441d4a9ab35e518.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-d9b12d680922d980.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-d4b6df905f185055.js → page-54b32bd27726588d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-e268d6d145827a22.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-186fc6952f027acc.js → page-f8116559c7424da7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-1085399fd0f32112.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-1df686a14f65895a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-cbbe03c0da7b081a.js → page-54e1f966ab60a4dc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-7ed95f296f5ee8e9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/page-11edac668d77e5c3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-59e2b9a22cd5a6d5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-2976df21ad74ab48.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-34d211a67f16d510.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-8ff652d5d950639b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-c0c0d19d51342d3e.js → page-b31610deda76337b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-2e002ad8faa43291.js → page-6cb0e1bbefe5fb3d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-a286a152cf584576.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/not-found-d9237581e7003bcb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-83274e761643fdd2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-ceee6cfb1d4e6d68.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-9c9f8bb3e4a64091.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c17217603ea3426a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-efb6177cbf1e8a51.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-d307523703bc4df3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-462f4e4bab45ce7d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-8990e96f33af3837.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-7464c1987dd76e6d.js → page-e168fa1030af0548.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-1233e05a4068bc9c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-13a1840b1fac337a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-64a29fc80b11ab55.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-68e787339a16b3dc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/{page-d8ab4d1489223459.js → page-4fddc90b23feb215.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-886ba8260dc79c79.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-0f26507a1722e40b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-109d4ccd42259c9d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-f5c2a4e510726774.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-b2e0039416f42522.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-26102fd7350f171e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-708948bc8a802794.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-5e5611620f78fb0b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-139ba5c367bbf60a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-202b4e81f33895ea.js +1 -0
- package/apps/pwa/out/_next/static/css/43feca5fd387dcbd.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +30 -29
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +30 -29
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +30 -29
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +27 -26
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +27 -26
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +27 -26
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +30 -29
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +30 -29
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +30 -29
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +30 -29
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +30 -29
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +30 -29
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +27 -26
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +30 -29
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +30 -29
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +27 -26
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +27 -26
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +30 -29
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +30 -29
- package/apps/pwa/out/ops/checklist/index.html +1 -0
- package/apps/pwa/out/ops/checklist/index.txt +30 -0
- package/apps/pwa/out/ops/release/index.html +1 -0
- package/apps/pwa/out/ops/release/index.txt +30 -0
- package/apps/pwa/out/ops/seats/index.html +1 -0
- package/apps/pwa/out/ops/seats/index.txt +30 -0
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +28 -27
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +30 -29
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +27 -26
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +30 -29
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +30 -29
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +30 -29
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +30 -29
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +30 -29
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +27 -26
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +30 -29
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +30 -29
- package/apps/pwa/out/today/index.html +1 -0
- package/apps/pwa/out/today/index.txt +33 -0
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +30 -29
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +30 -29
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +30 -29
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +30 -29
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +30 -29
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +30 -29
- package/graphs/field/field-feed.yaml +2 -0
- package/graphs/implement-loop.yaml +1 -1
- package/graphs/release/recipes.yaml +3 -0
- package/graphs/release/release-loop.yaml +3 -1
- package/graphs/test-diet/nightly-audit.yaml +18 -0
- package/graphs/test-diet/recipes.yaml +3 -0
- package/package.json +1 -1
- package/plugins/card-followup/graphs/run-step.ts +48 -13
- package/plugins/doc-draft/graphs/doc-draft.yaml +3 -3
- package/plugins/doc-draft/graphs/run-step.ts +14 -7
- package/scripts/ci-pwa-build-gate.ts +160 -0
- package/scripts/coord-channel-watch.sh +42 -8
- package/scripts/coord-digest.ts +199 -0
- package/scripts/coord-post.sh +60 -3
- package/scripts/install.ps1 +68 -9
- package/scripts/install.sh +39 -12
- package/scripts/lib/pwa-scenarios/scenarios.ts +20 -0
- package/scripts/lib/tui-regress-checks.ts +146 -0
- package/scripts/memory-lifecycle-cycle.ts +2 -2
- package/scripts/ops-now-probe.ts +327 -0
- package/scripts/publish-internal-dist.ts +11 -1
- package/scripts/release-loop/cutoff.ts +31 -2
- package/scripts/release-loop/export-check-node.ts +85 -0
- package/scripts/release-loop/gate-node.ts +18 -4
- package/scripts/release-loop/node-verdict.ts +8 -1
- package/scripts/release-loop/ops-upgrade-node.ts +72 -16
- package/scripts/release-loop/release-readiness.ts +9 -0
- package/scripts/release-loop/version-node.ts +58 -10
- package/scripts/repo-watch-cycle.ts +1 -1
- package/scripts/retro-cycle.ts +2 -2
- package/scripts/schedule-health-report.ts +1 -1
- package/scripts/storyboard/storyboard.ts +5 -161
- package/scripts/test-diet/lib.ts +31 -1
- package/scripts/test-diet/node.ts +134 -17
- package/scripts/test-timeout-audit.ts +114 -0
- package/scripts/tree-sync-alert.ts +1 -1
- package/scripts/tui-regress.ts +71 -0
- package/scripts/waitlist-daily-digest.ts +1 -1
- package/skills/explainer-video/SKILL.md +4 -0
- package/skills/explainer-video/engine/reel.mjs +18 -13
- package/skills/explainer-video/engine/reel.sh +24 -5
- package/skills/explainer-video/engine/stitch.mjs +145 -0
- package/skills/pitch-deck/SKILL.md +36 -0
- package/skills/pitch-deck/template.html +80 -0
- package/src/acp/codex-approval-adapter.ts +9 -3
- package/src/acp/core-turn-bridge.ts +117 -18
- package/src/acp/fact-question.ts +11 -0
- package/src/acp/server.ts +48 -7
- package/src/acp/tool-approval.ts +126 -0
- package/src/acp/turn-runner.ts +36 -0
- package/src/agent/monad-agent-turn.ts +8 -0
- package/src/ask-user-question/pending-questions.ts +25 -10
- package/src/ask-user-question/types.ts +9 -0
- package/src/auth/setup-link-tokens.fixture.ts +19 -0
- package/src/auth/setup-link-tokens.ts +164 -0
- package/src/autopilot/build/integrity-gate.ts +12 -0
- package/src/boot/daemon-prompt-turn.ts +3 -0
- package/src/boot/daemon-tools/index.ts +8 -0
- package/src/boot/daemon-tools/types.ts +2 -0
- package/src/boot/daemon-tools/web-search.ts +1 -0
- package/src/budget/unified-usage.ts +75 -0
- package/src/channel-bot-token.ts +37 -10
- package/src/chat/ctrl-c-in-input.ts +14 -0
- package/src/chat/index.ts +61 -9
- package/src/chat/pickers/state.ts +6 -2
- package/src/cli/agent-cli.ts +7 -0
- package/src/cli/decisions-cli.ts +25 -3
- package/src/cli/doctor-bundle-upload.ts +68 -0
- package/src/cli/doctor-cli.ts +38 -4
- package/src/cli/op-proxy-sync.ts +83 -0
- package/src/cli/ops-cli.ts +20 -0
- package/src/cli/pod-cli.ts +49 -0
- package/src/cli/pr-cli.ts +37 -4
- package/src/cli/pwa-stop.ts +163 -13
- package/src/cli/pwa-test.ts +3 -0
- package/src/cli/release-cli.ts +98 -15
- package/src/cli/self-update.ts +14 -0
- package/src/cli/storyboard-cli.ts +37 -0
- package/src/cli/test-state-dir-flag.ts +11 -4
- package/src/connectors/linear.ts +146 -0
- package/src/context-bus/coord-events.ts +115 -0
- package/src/dashboard/dashboard-boot-fixture.ts +23 -0
- package/src/dashboard/dashboard-status-lines.ts +45 -0
- package/src/dashboard/first-screen-band.ts +35 -0
- package/src/dashboard/index.ts +128 -34
- package/src/dashboard/provider-slash-runtime.ts +28 -11
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +17 -0
- package/src/dashboard/slash-runtime/help-from-registry.ts +43 -28
- package/src/dashboard/turn-stream-formatter.ts +1 -1
- package/src/decisions/decision-cards.ts +50 -2
- package/src/decisions/decision-ledger.ts +75 -4
- package/src/decisions/decision-linear-projection.ts +157 -0
- package/src/decisions/web-push-decision-cards.ts +50 -0
- package/src/directive/interpretation-card.ts +36 -0
- package/src/discord.ts +2 -1
- package/src/domains/coo-admin-tool.ts +62 -0
- package/src/domains/core-tools.ts +11 -3
- package/src/domains/elanous-card.ts +29 -0
- package/src/domains/ops-facts-tool.ts +76 -0
- package/src/domains/outbound-alert.ts +41 -13
- package/src/domains/release-tool.ts +168 -0
- package/src/domains/telegram-kind-route.ts +19 -1
- package/src/exec-requests/planner.ts +98 -28
- package/src/exec-requests/runner.ts +107 -10
- package/src/exec-requests/store.ts +8 -2
- package/src/field/field-media.ts +10 -7
- package/src/field/field-reel.ts +14 -4
- package/src/git-fs/worktree.ts +45 -6
- package/src/graph-runner/graph-approval-card.ts +113 -0
- package/src/graph-runner/graph-cli.ts +129 -4
- package/src/graph-runner/graph-notify.ts +25 -3
- package/src/graph-runner/runner.ts +142 -16
- package/src/harness/browser-verify.ts +25 -2
- package/src/harness/harness-cli-command.ts +231 -35
- package/src/harness/harness-incidents-writer.fixture.ts +24 -0
- package/src/harness/harness-incidents.ts +129 -0
- package/src/harness/harness-stop.ts +53 -3
- package/src/hitl/clarification-policy.ts +3 -1
- package/src/index.ts +172 -41
- package/src/intake-plane/author-goals.ts +37 -4
- package/src/intake-plane/check.ts +3 -0
- package/src/intake-plane/discord-seat-work.ts +47 -2
- package/src/intake-plane/seat-doc-route.ts +170 -6
- package/src/intake-plane/telegram-seat-work.ts +44 -2
- package/src/intake-plane/telegram-work.ts +1 -0
- package/src/maturity/cli-maturity.ts +34 -0
- package/src/maturity/feature-maturity.ts +100 -1
- package/src/maturity/tui-slash-maturity.ts +31 -0
- package/src/memory.ts +32 -4
- package/src/nexus/api/exec-requests.ts +33 -3
- package/src/nexus/api/http-server.ts +73 -1
- package/src/nexus/api/meta-api.ts +38 -0
- package/src/nexus/api/operator.ts +61 -0
- package/src/nexus/api/ops-api.ts +148 -0
- package/src/nexus/api/ops-seats-sources.ts +95 -0
- package/src/nexus/api/ops-seats.ts +124 -0
- package/src/nexus/api/public-routes.ts +14 -1
- package/src/nexus/api/seat-requests.ts +88 -8
- package/src/nexus/api/sessions-store.ts +63 -0
- package/src/nexus/api/setup-claim.ts +21 -0
- package/src/nexus/api/setup-codex-login.ts +160 -0
- package/src/nexus/api/usage-api.ts +102 -0
- package/src/nexus/config/secrets/file-backend.ts +56 -18
- package/src/nexus/index.ts +45 -4
- package/src/onboarding/first-run-doctor.ts +54 -0
- package/src/onboarding/first-run.ts +89 -0
- package/src/onboarding/lan-address.ts +45 -0
- package/src/onboarding/web-first.ts +119 -24
- package/src/onboarding.ts +4 -0
- package/src/ops-now/ops-now-refresh.ts +100 -0
- package/src/project/project-store.ts +115 -0
- package/src/release-loop/checklist.ts +76 -61
- package/src/release-loop/feature-store.ts +460 -0
- package/src/release-loop/manifest.ts +59 -5
- package/src/release-loop/release-schedule.ts +53 -0
- package/src/repl/index.ts +7 -0
- package/src/seat-dispatch/ceo-commands.ts +98 -0
- package/src/seat-dispatch/ceo-intent.ts +38 -0
- package/src/seat-loop/seat-loop.ts +231 -0
- package/src/seat-loop/seat-report.ts +64 -0
- package/src/self-dev/decompose.ts +66 -11
- package/src/self-dev/run-store.ts +10 -1
- package/src/self-implement/execution-clarification.ts +115 -0
- package/src/self-implement/gate-cli.ts +6 -3
- package/src/self-implement/orchestrator.ts +37 -3
- package/src/self-implement/pipeline-shape.ts +1 -1
- package/src/self-implement/run-status-mapping.ts +5 -3
- package/src/session/chat.ts +2 -0
- package/src/session/index.ts +9 -0
- package/src/shell-env-bootstrap.ts +17 -0
- package/src/skills/tools/usage-slash.ts +8 -7
- package/src/steward/directive.ts +51 -2
- package/src/steward/launch.ts +250 -0
- package/src/steward/outcome-notify.ts +115 -0
- package/src/steward/steward-cards.ts +8 -2
- package/src/steward/triage-plan.ts +32 -4
- package/src/steward/triage.ts +93 -9
- package/src/steward/working-backwards.ts +64 -0
- package/src/storyboard/storyboard.ts +141 -0
- package/src/task-cards/card-store.ts +2 -2
- package/src/task-orchestrator/surfaces/pod-lease.ts +260 -0
- package/src/telegram-card-followup.ts +233 -0
- package/src/telegram-commands.ts +16 -0
- package/src/telegram-report.ts +15 -9
- package/src/telegram-run.ts +90 -13
- package/src/telegram.ts +29 -8
- package/src/thinking-line.ts +28 -20
- package/src/tool-runtime/types.ts +4 -0
- package/src/tui/chat/execution-badge.ts +1 -1
- package/src/tui-client/dashboard-session.ts +2 -0
- package/src/user-config.ts +118 -11
- package/src/ux-sim/turn-states.ts +1 -1
- package/src/version/packed-revision.json +1 -1
- package/src/web-push/notify-exec-request.ts +32 -0
- package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4836-3084c58c1725a40b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6090-85a3f05f954e5697.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6284-69db415193d5f8bd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7032-f4ef698518fba61c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8067-d976b441a104ab74.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8519-5632377cce994f86.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8894-7317998f6cb97d8c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9454-a3e0953307216c58.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9740-8f5ff96c522b5f55.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-9aa47b0bc6d570ff.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f4d2ecbbae6088d6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-2053e49930c82748.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-a70ba27c33003dd0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-18166f885d831d3e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-1c9d6cfe68b121c2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-c108ff1fdb665c97.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-147a328753d983e2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-799395a9df7d95aa.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-6a8158b71d1fe9d7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-9567560270857531.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-e7ed3d010153d818.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-b672e02e4bec8deb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-1ba4a5d857276334.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-9db2226f38c542fb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-70ee65f5f7a151dc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-0bb8e43203efb23b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-092e1bcf27d751cc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-eb52580c4b75b905.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-e573437b0a1c728f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-294300625e49e4d3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-085847b088675978.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-9fe9d38c36999eb4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-6141f67f531d8b6a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-295a02b1026b8255.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-1eb9a0e1e054209d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-82efa177a30f3741.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d0be51cec4f62606.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-2e367075ffdbc062.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-647ed2b66c1d49f1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ba058c56e38f2792.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-6870f30ea99f8346.js +0 -1
- package/apps/pwa/out/_next/static/css/5ef72a618a8994f3.css +0 -1
- package/apps/pwa/out/_next/static/lywc8m_HDzH9MKSDQhyEj/_buildManifest.js +0 -1
- package/src/dashboard/turn-typeahead-echo.ts +0 -23
- /package/apps/pwa/out/_next/static/{lywc8m_HDzH9MKSDQhyEj → 2DsiDfDCks44g1cPUMhiW}/_ssgManifest.js +0 -0
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { realpathSync, statSync } from 'node:fs';
|
|
2
|
+
import { extname, isAbsolute, join, sep } from 'node:path';
|
|
2
3
|
import { ExecRequestRunner } from '../../exec-requests/runner.js';
|
|
4
|
+
import type { ExecAttachment } from '../../exec-requests/store.js';
|
|
5
|
+
import { getElanousConfigDir } from '../../elanous-config-dir.js';
|
|
6
|
+
import { defaultAttachmentBaseDir } from '../../boot/attachment-store.js';
|
|
3
7
|
import { jsonResponse } from './json-response.js';
|
|
4
8
|
|
|
5
9
|
const mime: Record<string, string> = {
|
|
@@ -8,7 +12,21 @@ const mime: Record<string, string> = {
|
|
|
8
12
|
'.md': 'text/markdown; charset=utf-8', '.txt': 'text/plain; charset=utf-8',
|
|
9
13
|
};
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
function resolveAttachment(path: string, roots: string[]): string | null {
|
|
16
|
+
if (!isAbsolute(path)) return null;
|
|
17
|
+
try {
|
|
18
|
+
const real = realpathSync(path);
|
|
19
|
+
if (!statSync(real).isFile()) return null;
|
|
20
|
+
return roots.some(root => {
|
|
21
|
+
try {
|
|
22
|
+
const base = realpathSync(root);
|
|
23
|
+
return real.startsWith(`${base}${sep}`);
|
|
24
|
+
} catch { return false; }
|
|
25
|
+
}) ? real : null;
|
|
26
|
+
} catch { return null; }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function handleExecRequests(req: Request, runner: ExecRequestRunner, uploadDir = defaultAttachmentBaseDir(), fieldDir = join(getElanousConfigDir(), 'field')): Promise<Response> {
|
|
12
30
|
const path = new URL(req.url).pathname;
|
|
13
31
|
if (path === '/v1/exec-requests') {
|
|
14
32
|
if (req.method === 'POST') {
|
|
@@ -16,7 +34,19 @@ export async function handleExecRequests(req: Request, runner: ExecRequestRunner
|
|
|
16
34
|
try { body = await req.json(); } catch { return jsonResponse({ error: 'bad_request' }, 400); }
|
|
17
35
|
const text = body && typeof body === 'object' && !Array.isArray(body) ? (body as { text?: unknown }).text : undefined;
|
|
18
36
|
if (typeof text !== 'string' || !text.trim()) return jsonResponse({ error: 'bad_request', reason: 'text required' }, 400);
|
|
19
|
-
const
|
|
37
|
+
const supplied = (body as { attachments?: unknown }).attachments;
|
|
38
|
+
if (supplied !== undefined && (!Array.isArray(supplied) || supplied.some(entry =>
|
|
39
|
+
!entry || typeof entry !== 'object' || Array.isArray(entry)
|
|
40
|
+
|| typeof entry.name !== 'string' || !entry.name.trim() || typeof entry.path !== 'string'))) {
|
|
41
|
+
return jsonResponse({ error: 'bad_request', reason: 'attachments must be [{name,path}]' }, 400);
|
|
42
|
+
}
|
|
43
|
+
const roots = [uploadDir, fieldDir];
|
|
44
|
+
const resolved: Array<{ name: string; path: string | null }> | undefined = supplied?.map((entry: ExecAttachment) => ({
|
|
45
|
+
name: entry.name, path: resolveAttachment(entry.path, roots),
|
|
46
|
+
}));
|
|
47
|
+
if (resolved?.some(entry => !entry.path)) return jsonResponse({ error: 'attachment-outside' }, 400);
|
|
48
|
+
const attachments: ExecAttachment[] | undefined = resolved?.map(entry => ({ name: entry.name, path: entry.path! }));
|
|
49
|
+
const item = runner.submit(text.trim(), attachments);
|
|
20
50
|
return jsonResponse({ id: item.id, status: 'planning' }, 202);
|
|
21
51
|
}
|
|
22
52
|
if (req.method === 'GET') return jsonResponse({ items: runner.store.list().map(item => {
|
|
@@ -19,6 +19,8 @@ import { setInProcessOutbound } from '../../domains/outbound-alert.js';
|
|
|
19
19
|
import { getUserConfig } from '../../user-config.js';
|
|
20
20
|
import type { TelegramEvent } from '../../workflow-runtime/triggers/telegram-source.js';
|
|
21
21
|
import { spawnSync } from 'node:child_process';
|
|
22
|
+
import { isAbsolute } from 'node:path';
|
|
23
|
+
import { createProject, listProjects } from '../../project/project-store.js';
|
|
22
24
|
import { compareTokenConstTime } from '../../acp/transport/auth.js';
|
|
23
25
|
import { ensureAuthToken } from '../../auth/acp-token.js';
|
|
24
26
|
import type { NexusState } from '../state/state.js';
|
|
@@ -134,6 +136,7 @@ import {
|
|
|
134
136
|
type ReflectionRouteOpts,
|
|
135
137
|
} from './reflection.js';
|
|
136
138
|
import { handleSignalBoard } from './signal-board-api.js';
|
|
139
|
+
import { handleUsageGet } from './usage-api.js';
|
|
137
140
|
import {
|
|
138
141
|
handleDashboard,
|
|
139
142
|
handleDashboardRefreshLive,
|
|
@@ -155,11 +158,15 @@ import {
|
|
|
155
158
|
import { handleBuildsGet, parseBuildsPath } from './builds-api.js';
|
|
156
159
|
import { handleHarnessAskPost, handleHarnessAskStatusGet, handleHarnessRunEventsGet, handleHarnessRunScreenGet, handleHarnessRunsGet, handleHarnessStopPost } from './harness-api.js';
|
|
157
160
|
import { dispatchPersonaRoute } from './personas.js';
|
|
161
|
+
import { handleMe } from './operator.js';
|
|
162
|
+
import { handleOpsApi } from './ops-api.js';
|
|
158
163
|
import { handleRoleJudge } from './role-judge.js';
|
|
159
164
|
import { handleAudioStt } from './audio-stt.js';
|
|
160
165
|
import { handleLlmModels } from './llm-models.js';
|
|
161
166
|
import { handleLlmHostsConfig } from './llm-hosts-config.js';
|
|
162
167
|
import { handleLlmRotationGet, handleLlmRotationNext } from './llm-rotation.js';
|
|
168
|
+
import { handleSetupClaim } from './setup-claim.js';
|
|
169
|
+
import { handleSetupCodexLoginGet, handleSetupCodexLoginPost } from './setup-codex-login.js';
|
|
163
170
|
import {
|
|
164
171
|
handleLlmProvidersList,
|
|
165
172
|
handleLlmProviderSet,
|
|
@@ -275,7 +282,7 @@ import { handleSchedulesActionPost } from './schedules-action.js';
|
|
|
275
282
|
import { handleSchedulesList, handleScheduleDetail, handleScheduleRuns } from './schedules-read.js';
|
|
276
283
|
import { handleIntakeRunsList } from './intake-runs.js';
|
|
277
284
|
import { handleMissionsList, handleMissionDetail } from './missions.js';
|
|
278
|
-
import { handleSessionsStoreList, handleSessionsStoreGet, handleSessionsStoreFork, handleSessionsStoreDelete } from './sessions-store.js';
|
|
285
|
+
import { handleSessionsStoreList, handleSessionsStoreGet, handleSessionsStoreFork, handleSessionsStoreDelete, handleSessionsStorePatch } from './sessions-store.js';
|
|
279
286
|
import { handleLogsQuery, handleLogsStream, handleLogsLevelGet, handleLogsLevelPost, handleLogsFacets, handleLogsHistogram, handleLogsInstances } from './log-fabric.js';
|
|
280
287
|
import { handleTrace, handleTraceEvidence } from './trace.js';
|
|
281
288
|
import { handleDispatchRunsList } from './dispatch-runs.js';
|
|
@@ -312,6 +319,7 @@ import {
|
|
|
312
319
|
handleVoiceTranscribe,
|
|
313
320
|
checkAuth,
|
|
314
321
|
bearerCredential,
|
|
322
|
+
setupBearerScopeDenied,
|
|
315
323
|
registerAuthPeerAddress,
|
|
316
324
|
type MetaApiOpts,
|
|
317
325
|
} from './meta-api.js';
|
|
@@ -461,6 +469,8 @@ export interface NexusHttpServerOpts {
|
|
|
461
469
|
state: NexusState;
|
|
462
470
|
registry: TabRegistry;
|
|
463
471
|
eventBus: NexusEventBus;
|
|
472
|
+
/** Test seam for the read-only usage collector; production uses the cached handler. */
|
|
473
|
+
usageHandler?: () => Promise<Response>;
|
|
464
474
|
/** Supervisor handle — required for the write API (PR ι). When omitted
|
|
465
475
|
* (skipSupervisor mode), mutation routes return 503. */
|
|
466
476
|
supervisor?: Supervisor;
|
|
@@ -791,6 +801,27 @@ export function startNexusHttpServer(opts: NexusHttpServerOpts): NexusHttpServer
|
|
|
791
801
|
};
|
|
792
802
|
}
|
|
793
803
|
|
|
804
|
+
export function handleProjectsGet(): Response {
|
|
805
|
+
return jsonResponse({ projects: listProjects() });
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
export async function handleProjectsPost(req: Request): Promise<Response> {
|
|
809
|
+
let body: unknown;
|
|
810
|
+
try { body = await req.json(); }
|
|
811
|
+
catch { return jsonResponse({ error: 'invalid_json' }, 400); }
|
|
812
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
|
813
|
+
return jsonResponse({ error: 'invalid_name' }, 400);
|
|
814
|
+
}
|
|
815
|
+
const { name, primaryFolder } = body as Record<string, unknown>;
|
|
816
|
+
if (typeof name !== 'string' || !name.trim() || name.length > 80) {
|
|
817
|
+
return jsonResponse({ error: 'invalid_name' }, 400);
|
|
818
|
+
}
|
|
819
|
+
if (primaryFolder !== undefined && (typeof primaryFolder !== 'string' || !isAbsolute(primaryFolder))) {
|
|
820
|
+
return jsonResponse({ error: 'invalid_primary_folder' }, 400);
|
|
821
|
+
}
|
|
822
|
+
return jsonResponse({ project: createProject({ name, ...(primaryFolder !== undefined ? { primaryFolder } : {}) }) }, 201);
|
|
823
|
+
}
|
|
824
|
+
|
|
794
825
|
function isPortBusy(err: unknown): boolean {
|
|
795
826
|
if (!err) return false;
|
|
796
827
|
// Bun.serve errors expose `.code === 'EADDRINUSE'` (Node-compatible)
|
|
@@ -826,6 +857,9 @@ export async function routeRequest(
|
|
|
826
857
|
registerAuthPeerAddress(req, (server as BunServerLike & {
|
|
827
858
|
requestIP?: (request: Request) => { address: string } | null;
|
|
828
859
|
}).requestIP?.(req)?.address);
|
|
860
|
+
if (opts.metaApi && setupBearerScopeDenied(req, opts.metaApi)) {
|
|
861
|
+
return jsonResponse({ error: 'forbidden' }, 403);
|
|
862
|
+
}
|
|
829
863
|
|
|
830
864
|
// PR c — try WS upgrade before HTTP routing. On undefined return the
|
|
831
865
|
// bridge has accepted the upgrade (Bun completes the handshake) and
|
|
@@ -852,6 +886,12 @@ export async function routeRequest(
|
|
|
852
886
|
const { pathname } = url;
|
|
853
887
|
const method = req.method;
|
|
854
888
|
|
|
889
|
+
// Operator proxy credentials are distinct from the owner bearer. The ops handler
|
|
890
|
+
// performs its own operatorSignal gate before reading any run or checklist data.
|
|
891
|
+
if (pathname === '/v1/ops' || pathname.startsWith('/v1/ops/')) {
|
|
892
|
+
return handleOpsApi(req, opts.metaApi);
|
|
893
|
+
}
|
|
894
|
+
|
|
855
895
|
// Default-deny for every `/v1/` path that is not on PUBLIC_ROUTES.
|
|
856
896
|
// Reuses checkAuth (same-origin exemption + constant-time bearer).
|
|
857
897
|
// setupMode is not implemented on this server (P24b) — pass false.
|
|
@@ -1272,11 +1312,22 @@ export async function routeRequest(
|
|
|
1272
1312
|
return handleGraphsValidatePost(req, opts.metaApi);
|
|
1273
1313
|
}
|
|
1274
1314
|
|
|
1315
|
+
if (pathname === '/v1/projects' && (method === 'GET' || method === 'POST')) {
|
|
1316
|
+
if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
1317
|
+
return method === 'GET' ? handleProjectsGet() : handleProjectsPost(req);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1275
1320
|
if (pathname === '/v1/config/chat-fast-path' && (method === 'GET' || method === 'PUT')) {
|
|
1276
1321
|
if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
1277
1322
|
return handleChatFastPathConfig(req);
|
|
1278
1323
|
}
|
|
1279
1324
|
|
|
1325
|
+
if (pathname === '/v1/usage') {
|
|
1326
|
+
if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
1327
|
+
if (method !== 'GET') return jsonResponse({ error: 'method-not-allowed' }, 405);
|
|
1328
|
+
return (opts.usageHandler ?? handleUsageGet)();
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1280
1331
|
// Mutation routes (PR ι) — POST/PATCH/DELETE on /v1/nexus/tabs[/:id[/action]].
|
|
1281
1332
|
// Templates POST handled here too (PR κ).
|
|
1282
1333
|
// Config / secrets PUT/POST/DELETE here too (PR μ).
|
|
@@ -1520,9 +1571,18 @@ export async function routeRequest(
|
|
|
1520
1571
|
if (pathname === '/v1/llm/rotation' && method === 'OPTIONS') {
|
|
1521
1572
|
return handleLlmRotationGet(req);
|
|
1522
1573
|
}
|
|
1574
|
+
// The one-use link is verified by the claim handler, not by the owner bearer.
|
|
1575
|
+
if (pathname === '/v1/setup/claim' && (method === 'POST' || method === 'OPTIONS')) {
|
|
1576
|
+
return method === 'OPTIONS' ? corsPreflight('POST, OPTIONS') : handleSetupClaim(req);
|
|
1577
|
+
}
|
|
1523
1578
|
// PWA `/setup` wizard Phase 1 (2026-05-19) — LLM provider 첫 셋업.
|
|
1524
1579
|
// POST 는 mutation 블록 안에서 등록 (memory `feedback_post_route_must_be_in_method_block`).
|
|
1525
1580
|
// GET sibling 은 아래 GET-only 블록에 등록.
|
|
1581
|
+
if (pathname === '/v1/setup/codex-login' && (method === 'POST' || method === 'OPTIONS')) {
|
|
1582
|
+
if (method === 'OPTIONS') return corsPreflight('GET, POST, OPTIONS');
|
|
1583
|
+
if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
1584
|
+
return handleSetupCodexLoginPost(req);
|
|
1585
|
+
}
|
|
1526
1586
|
if (pathname === '/v1/setup/llm-provider' && (method === 'POST' || method === 'OPTIONS')) {
|
|
1527
1587
|
if (method === 'POST') { if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401); }
|
|
1528
1588
|
return handleLlmProviderSet(req);
|
|
@@ -1887,6 +1947,11 @@ export async function routeRequest(
|
|
|
1887
1947
|
const id = decodeURIComponent(pathname.slice('/v1/sessions/store/'.length, -'/fork'.length));
|
|
1888
1948
|
return handleSessionsStoreFork(req, id, opts.metaApi);
|
|
1889
1949
|
}
|
|
1950
|
+
if (pathname.startsWith('/v1/sessions/store/') && !pathname.endsWith('/fork') && method === 'PATCH') {
|
|
1951
|
+
if (!opts.metaApi) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
1952
|
+
const id = decodeURIComponent(pathname.slice('/v1/sessions/store/'.length));
|
|
1953
|
+
return handleSessionsStorePatch(req, id, opts.metaApi);
|
|
1954
|
+
}
|
|
1890
1955
|
// on-disk 세션 삭제(파괴적) — 라이브 세션 관리. /store/:id (fork/transcript 와 구분).
|
|
1891
1956
|
if (pathname.startsWith('/v1/sessions/store/') && !pathname.endsWith('/fork') && method === 'DELETE') {
|
|
1892
1957
|
if (!opts.metaApi) return jsonResponse({ error: 'meta-api-runtime-not-wired' }, 503);
|
|
@@ -2153,6 +2218,9 @@ export async function routeRequest(
|
|
|
2153
2218
|
}
|
|
2154
2219
|
return jsonResponse({ error: 'static-not-wired' }, 404);
|
|
2155
2220
|
}
|
|
2221
|
+
// OPS1·OPS2 — whether this caller is an operator (draws the ops area). Answers everyone before any auth gate;
|
|
2222
|
+
// false unless this daemon's config has operator.enabled.
|
|
2223
|
+
if (pathname === '/v1/me' && req.method === 'GET') return handleMe(req, opts.metaApi ?? ({} as MetaApiOpts));
|
|
2156
2224
|
if (pathname === '/v1/health') {
|
|
2157
2225
|
return handleHealth(opts.state, opts.registry, {
|
|
2158
2226
|
bindHost: bind.hostname,
|
|
@@ -2405,6 +2473,10 @@ export async function routeRequest(
|
|
|
2405
2473
|
}
|
|
2406
2474
|
// PWA `/setup` wizard Phase 1 (2026-05-19) — provider catalog 표시.
|
|
2407
2475
|
// POST sibling (`/v1/setup/llm-provider`) 는 위 mutation 블록.
|
|
2476
|
+
if (pathname === '/v1/setup/codex-login') {
|
|
2477
|
+
if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
2478
|
+
return handleSetupCodexLoginGet();
|
|
2479
|
+
}
|
|
2408
2480
|
if (pathname === '/v1/setup/llm-providers') {
|
|
2409
2481
|
return handleLlmProvidersList(req);
|
|
2410
2482
|
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { randomUUID } from 'node:crypto';
|
|
18
18
|
import { matchTempToken } from '../../auth/temp-tokens.js';
|
|
19
|
+
import { matchSetupBearer } from '../../auth/setup-link-tokens.js';
|
|
19
20
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
20
21
|
import { extname } from 'node:path';
|
|
21
22
|
|
|
@@ -146,6 +147,10 @@ export interface MetaApiOpts {
|
|
|
146
147
|
* compared constant-time against the `Authorization: Bearer …`
|
|
147
148
|
* header. */
|
|
148
149
|
bearerToken?: string;
|
|
150
|
+
/** Server-side human identity verification, independent of the shared bearer credential.
|
|
151
|
+
* Never derive this result from request body, claimed source, or bearer possession alone.
|
|
152
|
+
* Without a verifier, prompt tool mutations remain unavailable. */
|
|
153
|
+
resolveVerifiedOwner?: (req: Request) => { id: string } | undefined;
|
|
149
154
|
/** Convenience for Tailscale-only / dogfood-loopback mode. */
|
|
150
155
|
noAuth?: boolean;
|
|
151
156
|
// ── PR k · runtime DI for the lifted endpoints ─────────────────
|
|
@@ -215,6 +220,8 @@ type AuthReason =
|
|
|
215
220
|
| 'untrusted-same-origin-peer'
|
|
216
221
|
| 'bearer-match'
|
|
217
222
|
| 'temp-token'
|
|
223
|
+
| 'setup-bearer'
|
|
224
|
+
| 'setup-scope-denied'
|
|
218
225
|
| 'no-bearer-configured'
|
|
219
226
|
| 'missing-auth-header'
|
|
220
227
|
| 'bearer-length-mismatch'
|
|
@@ -248,8 +255,27 @@ export function registerAuthPeerAddress(req: Request, peerAddress: string | unde
|
|
|
248
255
|
authPeerAddresses.set(req, peerAddress);
|
|
249
256
|
}
|
|
250
257
|
|
|
258
|
+
function setupRouteAllowed(req: Request): boolean {
|
|
259
|
+
const path = new URL(req.url).pathname;
|
|
260
|
+
if (path.startsWith('/v1/setup/')) return true;
|
|
261
|
+
return (req.method === 'POST' && path === '/v1/prompt/stream')
|
|
262
|
+
|| (req.method === 'GET' && (
|
|
263
|
+
/^\/v1\/sessions\/store\/[^/]+$/.test(path) || path === '/v1/chat/events'
|
|
264
|
+
));
|
|
265
|
+
}
|
|
266
|
+
|
|
251
267
|
function decideAuth(req: Request, opts: MetaApiOpts): { ok: boolean; reason: AuthReason } {
|
|
252
268
|
if (opts.noAuth) return { ok: true, reason: 'noauth' };
|
|
269
|
+
const offered = req.headers.get('authorization');
|
|
270
|
+
if (offered?.startsWith('Bearer elsb_')) {
|
|
271
|
+
const owner = decideBearer(req, opts);
|
|
272
|
+
if (owner.ok) return owner;
|
|
273
|
+
const bearer = offered.slice('Bearer '.length).trim();
|
|
274
|
+
if (!matchSetupBearer(bearer)) return { ok: false, reason: 'bearer-mismatch' };
|
|
275
|
+
return setupRouteAllowed(req)
|
|
276
|
+
? { ok: true, reason: 'setup-bearer' }
|
|
277
|
+
: { ok: false, reason: 'setup-scope-denied' };
|
|
278
|
+
}
|
|
253
279
|
const peerAddress = authPeerAddresses.get(req);
|
|
254
280
|
if (isSameOriginRequest(req, peerAddress)) return { ok: true, reason: 'same-origin' };
|
|
255
281
|
const peerRejected = req.headers.get('sec-fetch-site') === 'same-origin'
|
|
@@ -292,6 +318,10 @@ export function bearerCredential(req: Request, opts: MetaApiOpts): 'noauth' | 'b
|
|
|
292
318
|
// check — without duplicating security-sensitive code.
|
|
293
319
|
const authDecisions = new WeakMap<Request, { ok: boolean; reason: AuthReason }>();
|
|
294
320
|
|
|
321
|
+
export function setupBearerScopeDenied(req: Request, opts: MetaApiOpts): boolean {
|
|
322
|
+
return decideAuth(req, opts).reason === 'setup-scope-denied';
|
|
323
|
+
}
|
|
324
|
+
|
|
295
325
|
export function checkAuth(req: Request, opts: MetaApiOpts): boolean {
|
|
296
326
|
const decision = decideAuth(req, opts);
|
|
297
327
|
authDecisions.set(req, decision);
|
|
@@ -322,6 +352,7 @@ export function checkAuth(req: Request, opts: MetaApiOpts): boolean {
|
|
|
322
352
|
* intentionally collapse here to avoid exposing token-length information. */
|
|
323
353
|
function authFailureResponse(req: Request, opts: MetaApiOpts): Response {
|
|
324
354
|
const decision = authDecisions.get(req) ?? decideAuth(req, opts);
|
|
355
|
+
if (decision.reason === 'setup-scope-denied') return jsonResponse({ error: 'forbidden' }, 403);
|
|
325
356
|
return jsonResponse({ error: 'unauthorized', reason: publicAuthFailureReason(decision.reason) }, 401);
|
|
326
357
|
}
|
|
327
358
|
|
|
@@ -761,6 +792,11 @@ function notFound(payload: Record<string, unknown> = {}): Response {
|
|
|
761
792
|
|
|
762
793
|
// ── /v1/prompt ────────────────────────────────────────────────────
|
|
763
794
|
|
|
795
|
+
export function promptVerifiedOwner(req: Request, opts: MetaApiOpts): { id: string } | undefined {
|
|
796
|
+
const owner = opts.resolveVerifiedOwner?.(req);
|
|
797
|
+
return typeof owner?.id === 'string' && owner.id.trim() ? { id: owner.id.trim() } : undefined;
|
|
798
|
+
}
|
|
799
|
+
|
|
764
800
|
export async function handlePromptPost(
|
|
765
801
|
req: Request,
|
|
766
802
|
opts: MetaApiOpts,
|
|
@@ -804,6 +840,7 @@ export async function handlePromptPost(
|
|
|
804
840
|
surface: surfaceResolution.surface,
|
|
805
841
|
surfaceResolutionReason: surfaceResolution.reason,
|
|
806
842
|
...(parsed.value.userContent ? { promptBlocks: parsed.value.userContent } : {}),
|
|
843
|
+
...((owner) => owner ? { verifiedOwner: owner } : {})(promptVerifiedOwner(req, opts)),
|
|
807
844
|
...(resolvePromptHitlChannels(typeof body.sessionId === 'string' ? body.sessionId : '', opts) ?? {}),
|
|
808
845
|
...(effectiveSurface ? { toolSurface: effectiveSurface } : {}),
|
|
809
846
|
...(opts.toolCwd ? { toolCwd: opts.toolCwd } : {}),
|
|
@@ -1161,6 +1198,7 @@ export async function handlePromptStreamPost(
|
|
|
1161
1198
|
surface: surfaceResolution.surface,
|
|
1162
1199
|
surfaceResolutionReason: surfaceResolution.reason,
|
|
1163
1200
|
...(parsed.value.userContent ? { promptBlocks: parsed.value.userContent } : {}),
|
|
1201
|
+
...((owner) => owner ? { verifiedOwner: owner } : {})(promptVerifiedOwner(req, opts)),
|
|
1164
1202
|
...(resolvePromptHitlChannels(typeof body.sessionId === 'string' ? body.sessionId : '', opts) ?? {}),
|
|
1165
1203
|
...(effectiveSurface ? { toolSurface: effectiveSurface } : {}),
|
|
1166
1204
|
...(opts.toolCwd ? { toolCwd: opts.toolCwd } : {}),
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// OPS1·OPS2 — who is an «operator» (sees the ops area). Off unless this daemon's config says `operator.enabled: true`
|
|
2
|
+
// (only our own machines); an external install never reports an operator, whatever header or token arrives.
|
|
3
|
+
// Design: 내부 문서 `DESIGN-pwa-menu-reorg-and-ops-area-2026-10-02` §2.
|
|
4
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
5
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
6
|
+
import { debug } from '../../debug/log.js';
|
|
7
|
+
import { getUserConfig } from '../../user-config.js';
|
|
8
|
+
import { checkAuth, type MetaApiOpts } from './meta-api.js';
|
|
9
|
+
|
|
10
|
+
/** Set by the op.elanous.ai proxy (Caddy) after Google sign-in, server side; the proxy strips any incoming copy. */
|
|
11
|
+
export const OPERATOR_HEADER = 'x-elanous-operator';
|
|
12
|
+
|
|
13
|
+
export type OperatorSource = 'op-proxy' | 'owner' | null;
|
|
14
|
+
export interface OperatorSignal { operator: boolean; operatorSource: OperatorSource }
|
|
15
|
+
export interface OperatorConfig { enabled: boolean; proxySecretFile?: string }
|
|
16
|
+
|
|
17
|
+
export function readOperatorConfig(raw: unknown = getUserConfig().raw?.operator): OperatorConfig {
|
|
18
|
+
const o = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw as Record<string, unknown> : {};
|
|
19
|
+
return {
|
|
20
|
+
enabled: o.enabled === true,
|
|
21
|
+
...(typeof o.proxySecretFile === 'string' && o.proxySecretFile.trim() ? { proxySecretFile: o.proxySecretFile.trim() } : {}),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The proxy secret, only from a regular file nobody but the owner can read (mode 600); anything else turns the header path off. */
|
|
26
|
+
export function readProxySecret(path: string | undefined): string | null {
|
|
27
|
+
if (!path) return null;
|
|
28
|
+
try {
|
|
29
|
+
const st = statSync(path);
|
|
30
|
+
if (!st.isFile() || (st.mode & 0o077) !== 0) return null;
|
|
31
|
+
const value = readFileSync(path, 'utf8').trim();
|
|
32
|
+
return value.length >= 16 ? value : null;
|
|
33
|
+
} catch { return null; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function sameSecret(offered: string, secret: string): boolean {
|
|
37
|
+
const a = Buffer.from(offered);
|
|
38
|
+
const b = Buffer.from(secret);
|
|
39
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface OperatorDeps { config?: OperatorConfig; readSecret?: (path: string | undefined) => string | null; isOwner?: (req: Request) => boolean }
|
|
43
|
+
|
|
44
|
+
export function operatorSignal(req: Request, opts: MetaApiOpts, deps: OperatorDeps = {}): OperatorSignal {
|
|
45
|
+
const config = deps.config ?? readOperatorConfig();
|
|
46
|
+
if (!config.enabled) return { operator: false, operatorSource: null };
|
|
47
|
+
const offered = req.headers.get(OPERATOR_HEADER);
|
|
48
|
+
if (offered) {
|
|
49
|
+
const secret = (deps.readSecret ?? readProxySecret)(config.proxySecretFile);
|
|
50
|
+
if (secret && sameSecret(offered, secret)) return { operator: true, operatorSource: 'op-proxy' };
|
|
51
|
+
}
|
|
52
|
+
if ((deps.isOwner ?? ((r: Request) => checkAuth(r, opts)))(req)) return { operator: true, operatorSource: 'owner' };
|
|
53
|
+
return { operator: false, operatorSource: null };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** `GET /v1/me` — answers every caller (no auth needed: it only says whether to draw the ops area). */
|
|
57
|
+
export function handleMe(req: Request, opts: MetaApiOpts, deps: OperatorDeps = {}): Response {
|
|
58
|
+
const signal = operatorSignal(req, opts, deps);
|
|
59
|
+
debug.log('ops.api', 'me', { operator: signal.operator, source: signal.operatorSource });
|
|
60
|
+
return Response.json(signal, { headers: { 'cache-control': 'no-store' } });
|
|
61
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { lstatSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { debug, redactSecretText } from '../../debug/log.js';
|
|
4
|
+
import { effectiveInstanceRoot } from '../../instance/resolve.js';
|
|
5
|
+
import { listChecklist } from '../../release-loop/checklist.js';
|
|
6
|
+
import { getSchedule } from '../../release-loop/release-schedule.js';
|
|
7
|
+
import { jsonResponse } from './json-response.js';
|
|
8
|
+
import { operatorSignal } from './operator.js';
|
|
9
|
+
import { createSeatsCache, todayKst } from './ops-seats.js';
|
|
10
|
+
import { liveSeatsSources } from './ops-seats-sources.js';
|
|
11
|
+
import type { MetaApiOpts } from './meta-api.js';
|
|
12
|
+
|
|
13
|
+
const VERSION = /^\d+\.\d+\.\d+(?:-(?:rc|alpha|beta)\.\d+)?$/;
|
|
14
|
+
const IDENTIFIER = /^[A-Za-z0-9-]+$/;
|
|
15
|
+
const DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
16
|
+
let seatsCache: ReturnType<typeof createSeatsCache> | null = null;
|
|
17
|
+
/** Tests swap the sources; production builds them lazily on first use. */
|
|
18
|
+
export function setSeatsCacheForTest(cache: ReturnType<typeof createSeatsCache> | null): void { seatsCache = cache; }
|
|
19
|
+
const LOG_PATH = /^\/v1\/ops\/release\/runs\/([^/]+)\/nodes\/([^/]+)\/log$/;
|
|
20
|
+
|
|
21
|
+
interface RunNode { nodeId: string; ok?: unknown; output?: unknown }
|
|
22
|
+
interface RunRecord {
|
|
23
|
+
status: string;
|
|
24
|
+
path: string[];
|
|
25
|
+
startedAt: string;
|
|
26
|
+
input?: unknown;
|
|
27
|
+
nodes: RunNode[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function recordAt(path: string): RunRecord | null {
|
|
31
|
+
try {
|
|
32
|
+
if (!lstatSync(path).isFile()) return null;
|
|
33
|
+
const value: unknown = JSON.parse(readFileSync(path, 'utf8'));
|
|
34
|
+
if (!value || typeof value !== 'object') return null;
|
|
35
|
+
const run = value as Partial<RunRecord>;
|
|
36
|
+
if (typeof run.status !== 'string' || !Array.isArray(run.path)
|
|
37
|
+
|| typeof run.startedAt !== 'string' || !Array.isArray(run.nodes)) return null;
|
|
38
|
+
return run as RunRecord;
|
|
39
|
+
} catch { return null; }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function outputText(output: unknown): string {
|
|
43
|
+
if (typeof output === 'string') return output;
|
|
44
|
+
if (output === undefined || output === null) return '';
|
|
45
|
+
return JSON.stringify(output) ?? '';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function nodeSummary(output: unknown): string {
|
|
49
|
+
let value = output;
|
|
50
|
+
if (typeof value === 'string') {
|
|
51
|
+
const text = value;
|
|
52
|
+
try { value = JSON.parse(text) as unknown; }
|
|
53
|
+
catch {
|
|
54
|
+
const last = text.split('\n').map((line) => line.trim()).filter(Boolean).at(-1);
|
|
55
|
+
try { value = JSON.parse(last ?? '') as unknown; } catch { return ''; }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (!value || typeof value !== 'object') return '';
|
|
59
|
+
const summary = (value as { summary?: unknown }).summary;
|
|
60
|
+
return typeof summary === 'string' ? redactSecretText(summary).slice(0, 300) : '';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function runVersion(input: unknown): string | null {
|
|
64
|
+
if (!input || typeof input !== 'object') return null;
|
|
65
|
+
const version = (input as { version?: unknown }).version;
|
|
66
|
+
return typeof version === 'string' && VERSION.test(version) ? version : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The sole data gate for every read-only /v1/ops endpoint. */
|
|
70
|
+
export function handleOpsApi(req: Request, metaApi: MetaApiOpts | undefined): Response | Promise<Response> {
|
|
71
|
+
const url = new URL(req.url);
|
|
72
|
+
const { pathname } = url;
|
|
73
|
+
if (req.method !== 'GET') return jsonResponse({ error: 'method-not-allowed' }, 405);
|
|
74
|
+
|
|
75
|
+
const signal = metaApi ? operatorSignal(req, metaApi) : { operator: false, operatorSource: null };
|
|
76
|
+
if (!signal.operator) {
|
|
77
|
+
debug.log('ops.api', 'refused', { path: pathname, source: signal.operatorSource });
|
|
78
|
+
return jsonResponse({ error: 'forbidden' }, 403);
|
|
79
|
+
}
|
|
80
|
+
const served = (response: Response): Response => {
|
|
81
|
+
debug.log('ops.api', 'served', { path: pathname, source: signal.operatorSource });
|
|
82
|
+
response.headers.set('cache-control', 'no-store');
|
|
83
|
+
return response;
|
|
84
|
+
};
|
|
85
|
+
if (pathname === '/v1/ops/seats') {
|
|
86
|
+
const date = url.searchParams.get('date') ?? todayKst();
|
|
87
|
+
if (!DATE.test(date) || Number.isNaN(Date.parse(`${date}T00:00:00+09:00`))) return jsonResponse({ error: 'invalid-date' }, 400);
|
|
88
|
+
seatsCache ??= createSeatsCache(liveSeatsSources());
|
|
89
|
+
return seatsCache(date).then((board) => served(jsonResponse(board)), () => jsonResponse({ error: 'seats-unavailable' }, 500));
|
|
90
|
+
}
|
|
91
|
+
const version = url.searchParams.get('version');
|
|
92
|
+
if (pathname === '/v1/ops/checklist') {
|
|
93
|
+
if (!version || !VERSION.test(version)) return jsonResponse({ error: 'invalid-version' }, 400);
|
|
94
|
+
// OPS2 header «컷 · 착지 마감»: the version's row in the release schedule (REL9a), or null when none is set.
|
|
95
|
+
let schedule: { cutAt: string; landBy: string | null } | null = null;
|
|
96
|
+
try {
|
|
97
|
+
const row = getSchedule(version);
|
|
98
|
+
schedule = row ? { cutAt: row.cutAt, landBy: row.landBy ?? null } : null;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
debug.log('ops.api', 'schedule-unreadable', { version, reason: error instanceof Error ? error.message.slice(0, 80) : 'unknown' }, { level: 'warn' });
|
|
101
|
+
}
|
|
102
|
+
return served(jsonResponse({ ...listChecklist(version), schedule }));
|
|
103
|
+
}
|
|
104
|
+
if (pathname === '/v1/ops/release/runs') {
|
|
105
|
+
if (version !== null && !VERSION.test(version)) return jsonResponse({ error: 'invalid-version' }, 400);
|
|
106
|
+
const dir = join(effectiveInstanceRoot(), 'graph-runs', 'release-loop');
|
|
107
|
+
let filenames: string[];
|
|
108
|
+
try {
|
|
109
|
+
filenames = readdirSync(dir, { withFileTypes: true })
|
|
110
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.json')
|
|
111
|
+
&& IDENTIFIER.test(entry.name.slice(0, -5)))
|
|
112
|
+
.map((entry) => entry.name);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return served(jsonResponse([]));
|
|
116
|
+
return jsonResponse({ error: 'runs-unavailable' }, 500);
|
|
117
|
+
}
|
|
118
|
+
const runs = filenames.flatMap((filename) => {
|
|
119
|
+
const run = recordAt(join(dir, filename));
|
|
120
|
+
if (!run) return [];
|
|
121
|
+
const v = runVersion(run.input);
|
|
122
|
+
if (version !== null && v !== version) return [];
|
|
123
|
+
return [{
|
|
124
|
+
runId: redactSecretText(filename.slice(0, -5)), status: redactSecretText(run.status),
|
|
125
|
+
startedAt: redactSecretText(run.startedAt), version: v,
|
|
126
|
+
path: run.path.filter((step): step is string => typeof step === 'string').map(redactSecretText),
|
|
127
|
+
nodes: run.nodes.filter((node) => node && typeof node.nodeId === 'string').map((node) => ({
|
|
128
|
+
nodeId: redactSecretText(node.nodeId), ok: typeof node.ok === 'boolean' ? node.ok : null,
|
|
129
|
+
summary: nodeSummary(node.output),
|
|
130
|
+
})),
|
|
131
|
+
}];
|
|
132
|
+
}).sort((a, b) => b.startedAt.localeCompare(a.startedAt)).slice(0, 20);
|
|
133
|
+
return served(jsonResponse(runs));
|
|
134
|
+
}
|
|
135
|
+
// WHATWG URL normalizes a literal runs/../nodes/... before routing.
|
|
136
|
+
if (pathname.startsWith('/v1/ops/release/nodes/')) return jsonResponse({ error: 'invalid-id' }, 400);
|
|
137
|
+
if (pathname.startsWith('/v1/ops/release/runs/')) {
|
|
138
|
+
const match = LOG_PATH.exec(pathname);
|
|
139
|
+
if (!match) return jsonResponse({ error: 'invalid-id' }, 400);
|
|
140
|
+
const [, id, nodeId] = match;
|
|
141
|
+
if (!IDENTIFIER.test(id!) || !IDENTIFIER.test(nodeId!)) return jsonResponse({ error: 'invalid-id' }, 400);
|
|
142
|
+
const run = recordAt(join(effectiveInstanceRoot(), 'graph-runs', 'release-loop', `${id}.json`));
|
|
143
|
+
const node = run?.nodes.find((candidate) => candidate?.nodeId === nodeId);
|
|
144
|
+
if (!node || node.output === undefined || node.output === null) return jsonResponse({ error: 'not-found' }, 404);
|
|
145
|
+
return served(jsonResponse({ log: redactSecretText(outputText(node.output)).slice(-4_000) }));
|
|
146
|
+
}
|
|
147
|
+
return jsonResponse({ error: 'not-found' }, 404);
|
|
148
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Live sources for /v1/ops/seats: the coordination PR and merged PRs through gh (automation App identity, proxy variables
|
|
2
|
+
// removed — same rule as decision replies), the release checklist, and the decision ledger. Each failure becomes null upstream.
|
|
3
|
+
import { DecisionLedger } from '../../decisions/decision-ledger.js';
|
|
4
|
+
import { listCoordEvents, type CoordEvent } from '../../context-bus/coord-events.js';
|
|
5
|
+
import { devVersion, listChecklist, type ChecklistItem } from '../../release-loop/checklist.js';
|
|
6
|
+
import { getUserConfig } from '../../user-config.js';
|
|
7
|
+
import { SEATS, kstDayRange, type ChannelComment, type ChannelSource, type MergedPr, type SeatsSources } from './ops-seats.js';
|
|
8
|
+
|
|
9
|
+
const GH_TIMEOUT_MS = 20_000;
|
|
10
|
+
|
|
11
|
+
async function ghLines(args: string[]): Promise<string[] | null> {
|
|
12
|
+
const env: Record<string, string> = {};
|
|
13
|
+
for (const [k, v] of Object.entries(process.env)) if (v !== undefined && !/^(https?|all|no)_proxy$/i.test(k)) env[k] = v;
|
|
14
|
+
try {
|
|
15
|
+
const { githubAutomationToken } = await import('../../auth/github-app-token.js');
|
|
16
|
+
const token = githubAutomationToken();
|
|
17
|
+
if (token) env.GH_TOKEN = token;
|
|
18
|
+
} catch { /* machine login */ }
|
|
19
|
+
const proc = Bun.spawn(['gh', ...args], { stdin: 'ignore', stdout: 'pipe', stderr: 'ignore', env });
|
|
20
|
+
const timer = setTimeout(() => proc.kill(), GH_TIMEOUT_MS);
|
|
21
|
+
try {
|
|
22
|
+
const [out, code] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
|
23
|
+
return code === 0 ? out.split('\n').filter(Boolean) : null;
|
|
24
|
+
} finally { clearTimeout(timer); }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function channelTarget(): { repo: string; pr: string } | null {
|
|
28
|
+
const raw = (getUserConfig().raw?.decisions as { replyGhPr?: unknown } | undefined)?.replyGhPr;
|
|
29
|
+
const m = typeof raw === 'string' ? /^([\w.-]+\/[\w.-]+)#(\d+)$/.exec(raw.trim()) : null;
|
|
30
|
+
return m ? { repo: m[1]!, pr: m[2]! } : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function channelCommentsSince(sinceIso: string): Promise<Array<{ body: string; createdAt: string; url: string }> | null> {
|
|
34
|
+
const target = channelTarget();
|
|
35
|
+
if (!target) return null;
|
|
36
|
+
const lines = await ghLines(['api', '--paginate', `repos/${target.repo}/issues/${target.pr}/comments?since=${sinceIso}&per_page=100`,
|
|
37
|
+
'--jq', '.[] | {body: .body, createdAt: .created_at, url: .html_url} | tojson']);
|
|
38
|
+
return lines?.map((line) => JSON.parse(line) as { body: string; createdAt: string; url: string }) ?? null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function nextPatch(version: string): string {
|
|
42
|
+
const [major, minor, patch] = version.split('.').map(Number);
|
|
43
|
+
return `${major}.${minor}.${patch! + 1}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function liveSeatsSources(deps: { listEvents?: typeof listCoordEvents; githubChannel?: typeof channelCommentsSince } = {}): SeatsSources {
|
|
47
|
+
return {
|
|
48
|
+
async channel(date) {
|
|
49
|
+
const since = kstDayRange(date).start;
|
|
50
|
+
let events: CoordEvent[] = [];
|
|
51
|
+
try { events = (deps.listEvents ?? listCoordEvents)({ since }); } catch { /* GitHub fallback */ }
|
|
52
|
+
const latest = new Map<string, CoordEvent>();
|
|
53
|
+
for (const event of events) {
|
|
54
|
+
if (!SEATS.some(({ seat }) => seat === event.refs.seat)) continue;
|
|
55
|
+
const prior = latest.get(event.refs.seat);
|
|
56
|
+
if (!prior || event.at > prior.at) latest.set(event.refs.seat, event);
|
|
57
|
+
}
|
|
58
|
+
const seatSources: Partial<Record<(typeof SEATS)[number]['seat'], ChannelSource>> = {};
|
|
59
|
+
if (latest.size) {
|
|
60
|
+
const comments: ChannelComment[] = [];
|
|
61
|
+
for (const { seat } of SEATS) {
|
|
62
|
+
const event = latest.get(seat);
|
|
63
|
+
if (!event) { seatSources[seat] = 'unknown'; continue; }
|
|
64
|
+
comments.push({ body: `**[${seat}]** ${event.text}`, createdAt: event.at, url: event.refs.url ?? '' });
|
|
65
|
+
seatSources[seat] = 'ledger';
|
|
66
|
+
}
|
|
67
|
+
return { source: 'ledger', comments, seatSources };
|
|
68
|
+
}
|
|
69
|
+
let github: ChannelComment[] | null;
|
|
70
|
+
try { github = await (deps.githubChannel ?? channelCommentsSince)(since); }
|
|
71
|
+
catch { github = null; }
|
|
72
|
+
if (github === null) return null;
|
|
73
|
+
for (const { seat } of SEATS) seatSources[seat] = 'github';
|
|
74
|
+
return { source: 'github', comments: github, seatSources };
|
|
75
|
+
},
|
|
76
|
+
async merged(date) {
|
|
77
|
+
const target = channelTarget();
|
|
78
|
+
if (!target) return null;
|
|
79
|
+
const { start, end } = kstDayRange(date);
|
|
80
|
+
const lines = await ghLines(['pr', 'list', '--repo', target.repo, '--state', 'merged', '--limit', '300',
|
|
81
|
+
'--search', `merged:${start.slice(0, 19)}Z..${end.slice(0, 19)}Z`, '--json', 'number,title,body,mergedAt', '--jq', '.[] | tojson']);
|
|
82
|
+
return lines?.map((line) => JSON.parse(line) as MergedPr) ?? null;
|
|
83
|
+
},
|
|
84
|
+
checklist() {
|
|
85
|
+
const dev = devVersion();
|
|
86
|
+
const next = listChecklist(nextPatch(dev)).items;
|
|
87
|
+
const current = listChecklist(dev).items;
|
|
88
|
+
const all: ChecklistItem[] = [...current, ...next];
|
|
89
|
+
return { current: next.length ? next : current, all };
|
|
90
|
+
},
|
|
91
|
+
openDecisionRaisers() {
|
|
92
|
+
return new DecisionLedger().list({ status: 'open' }).map((entry) => entry.raisedBy.agent);
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|