elanous 0.2.9 → 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-f972fc1c00a62e88.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-f8116559c7424da7.js +1 -0
- 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-54e1f966ab60a4dc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-973996d584f008c1.js → page-7ed95f296f5ee8e9.js} +1 -1
- 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-8b0402a46a749a3d.js → page-b31610deda76337b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-037f83e2a1edc8e2.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-0df30e0c7e50148a.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-4fddc90b23feb215.js +1 -0
- 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-90670589b87abea8.js → page-26102fd7350f171e.js} +1 -1
- 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-35bedde6d885f08f.js → page-139ba5c367bbf60a.js} +1 -1
- 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 +14 -14
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +14 -14
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +14 -14
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +14 -14
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +14 -14
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +14 -14
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +14 -14
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +14 -14
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +14 -14
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +14 -14
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +14 -14
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +14 -14
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +14 -14
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +14 -14
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +14 -14
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +14 -14
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +14 -14
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +14 -14
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +14 -14
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +14 -14
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +14 -14
- 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 +15 -15
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +14 -14
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +14 -14
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +14 -14
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +14 -14
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +14 -14
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +14 -14
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +14 -14
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +14 -14
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +14 -14
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +14 -14
- 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 +14 -14
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +14 -14
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +14 -14
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +14 -14
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +14 -14
- package/graphs/field/field-feed.yaml +2 -0
- package/graphs/implement-loop.yaml +1 -1
- 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 +13 -6
- 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 +19 -8
- 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 +30 -1
- package/scripts/test-diet/node.ts +82 -7
- 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.ts +16 -5
- 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/chat/ctrl-c-in-input.ts +14 -0
- package/src/chat/index.ts +52 -0
- 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/ops-cli.ts +20 -0
- package/src/cli/pod-cli.ts +49 -0
- package/src/cli/pr-cli.ts +31 -2
- package/src/cli/pwa-stop.ts +163 -13
- package/src/cli/pwa-test.ts +3 -0
- package/src/cli/release-cli.ts +63 -20
- 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/index.ts +58 -21
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +7 -0
- package/src/dashboard/slash-runtime/help-from-registry.ts +43 -28
- package/src/decisions/decision-cards.ts +49 -1
- 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 +156 -37
- 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 +98 -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 +53 -1
- package/src/nexus/api/meta-api.ts +11 -0
- package/src/nexus/api/ops-api.ts +10 -1
- package/src/nexus/api/ops-seats-sources.ts +37 -7
- package/src/nexus/api/ops-seats.ts +16 -6
- package/src/nexus/api/seat-requests.ts +88 -8
- package/src/nexus/api/sessions-store.ts +63 -0
- package/src/nexus/api/setup-codex-login.ts +160 -0
- package/src/nexus/api/usage-api.ts +102 -0
- package/src/nexus/index.ts +45 -4
- 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 +78 -4
- package/src/release-loop/feature-store.ts +154 -21
- package/src/release-loop/release-schedule.ts +53 -0
- package/src/repl/index.ts +7 -0
- package/src/seat-dispatch/ceo-commands.ts +41 -8
- 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/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-report.ts +15 -9
- package/src/telegram-run.ts +7 -0
- package/src/telegram.ts +29 -8
- package/src/tool-runtime/types.ts +4 -0
- package/src/tui-client/dashboard-session.ts +2 -0
- package/src/user-config.ts +118 -11
- package/src/version/packed-revision.json +1 -1
- package/src/web-push/notify-exec-request.ts +32 -0
- package/apps/pwa/out/_next/static/bFI5EEwg7T6aG88ibX4Zq/_buildManifest.js +0 -1
- 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/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/8630-cdfc61e82c653ffc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8894-e263b332208e3957.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9454-139040d37c79dbc9.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-fb7c2d458f32fcad.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-35e2713597dd24d1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-918b656f064577ba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-5f30e00693527ebd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-7fbae173b3c103a7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-3b4edcce364062ea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-c5794f484c251f44.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-1130d8eefcd87e76.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-016e2bda04eecb1f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-b4d6d9a3b56f2940.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-ec49a466121233ba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-f72db356744ceade.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-a6fd9b50d1b85378.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-adfbd2ba9598e59a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-46fedff57d4bec67.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-0cf318e9dcee2bbf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-899b327cb8b6845c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-6c1e6ba743a29dfe.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-1a8e4807292f6b17.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-28deba6bcb834916.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-de68e2205794037d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-c71c33a573230672.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-4fc56790fe0d2682.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-59df2aebb38a7c1e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-4647be5dbcbd0795.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-e025892bda74b743.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-a0955de73b89f078.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-369846d2921af584.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-72cbc1c8cdd0f65d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-77b28bfb0996d5ea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-3220c4934f49c6c2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-7df9243ed021a4cc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-4498c3853f4f688d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-6870f30ea99f8346.js +0 -1
- package/apps/pwa/out/_next/static/css/f92f63a5df0ba229.css +0 -1
- package/src/dashboard/turn-typeahead-echo.ts +0 -23
- /package/apps/pwa/out/_next/static/{bFI5EEwg7T6aG88ibX4Zq → 2DsiDfDCks44g1cPUMhiW}/_ssgManifest.js +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { closeSync, existsSync, fstatSync, ftruncateSync, mkdirSync, openSync, readFileSync, realpathSync, statSync, unlinkSync, writeSync } from 'node:fs';
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
3
|
import { elanousStateRoot } from '../autopilot/state-paths.js';
|
|
4
|
+
import { readPendingQuestionAnswer, readPendingQuestions, writePendingQuestionAnswer } from '../ask-user-question/pending-questions.js';
|
|
4
5
|
import { debug, redactSecretText } from '../debug/log.js';
|
|
5
6
|
import { createVersionResolver, type VersionOptions, type Versions } from '../directives/version-at.js';
|
|
6
7
|
import { getUserConfig } from '../user-config.js';
|
|
@@ -15,15 +16,27 @@ export interface DecisionEntry {
|
|
|
15
16
|
options: DecisionOption[]; recommendation: Recommendation; raisedAt?: string; importedAt?: string;
|
|
16
17
|
raisedBy: { agent: string; track?: DecisionTrack; session?: string }; version?: Versions;
|
|
17
18
|
status: 'open' | 'decided' | 'withdrawn'; refs?: string[];
|
|
19
|
+
resume?: { questionId: string; runId?: string };
|
|
18
20
|
/** DEC-TG — optional deadline (UTC). Cards remind the owner two hours before it. */
|
|
19
21
|
dueAt?: string;
|
|
20
22
|
decidedAt?: string; decidedBy?: DecisionActor; choice?: string; note?: string; versionAtDecision?: Versions;
|
|
21
23
|
withdrawnAt?: string; withdrawReason?: string;
|
|
22
24
|
history: Array<{ type: 'raised' | 'options-added' | 'decided' | 'withdrawn'; at?: string; by: string; version?: Versions; choice?: string; reason?: string }>;
|
|
23
25
|
}
|
|
24
|
-
export type RaiseInput = Pick<DecisionEntry, 'title' | 'category' | 'scqa' | 'options' | 'recommendation' | 'raisedBy' | 'refs' | 'dueAt'> & { raisedAt?: string };
|
|
26
|
+
export type RaiseInput = Pick<DecisionEntry, 'title' | 'category' | 'scqa' | 'options' | 'recommendation' | 'raisedBy' | 'refs' | 'dueAt' | 'resume'> & { raisedAt?: string };
|
|
25
27
|
type Event = { type: 'raised'; entry: DecisionEntry } | { type: 'options-added'; id: string; at: string; options: DecisionOption[]; by: string } | { type: 'decided'; id: string; at?: string; by: DecisionActor; choice?: string; version?: Versions; note?: string } | { type: 'withdrawn'; id: string; at: string; reason: string; version: Versions };
|
|
26
|
-
export interface DecisionLedgerOptions extends VersionOptions { stateDir?: string; now?: () => Date; resolveVersion?: (at: string) => Versions }
|
|
28
|
+
export interface DecisionLedgerOptions extends VersionOptions { stateDir?: string; now?: () => Date; resolveVersion?: (at: string) => Versions; writeAnswer?: typeof writePendingQuestionAnswer }
|
|
29
|
+
|
|
30
|
+
/** null = the decision resumes no run; otherwise whether the answer file was written (or already matched). */
|
|
31
|
+
export type AnswerDelivery = null | { ok: true; questionId: string } | { ok: false; questionId: string; reason: string };
|
|
32
|
+
|
|
33
|
+
/** The decision is durable even when delivery fails; retryAnswer(id) only retries delivery. */
|
|
34
|
+
export class DecisionAnswerDeliveryError extends Error {
|
|
35
|
+
constructor(readonly decisionId: string, readonly questionId: string) {
|
|
36
|
+
super(`decision ${decisionId} recorded but answer delivery failed for ${questionId}; retry with decisions retry-answer ${decisionId}`);
|
|
37
|
+
this.name = 'DecisionAnswerDeliveryError';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
27
40
|
const CATEGORIES: readonly string[] = ['secret', 'publish', 'money', 'security', 'scope', 'irreversible', 'other'];
|
|
28
41
|
const TRACKS: readonly string[] = ['S', 'T', 'F', 'O'];
|
|
29
42
|
|
|
@@ -73,7 +86,11 @@ function validate(input: RaiseInput, historical = false): RaiseInput {
|
|
|
73
86
|
? { skipped: true as const, reason: safe(single(input.recommendation.reason, 'skip reason')) }
|
|
74
87
|
: { option: input.recommendation.option, why: safe(single(input.recommendation.why, 'recommendation why')) };
|
|
75
88
|
if ('option' in recommendation && !options.some(o => o.key === recommendation.option)) throw new Error('recommended option not found');
|
|
89
|
+
if (input.resume && !/^auq:[0-9a-z]+:[0-9a-z]{5}(?![\s\S])/.test(input.resume.questionId)) throw new Error('invalid resume questionId');
|
|
90
|
+
const resume = input.resume ? { questionId: input.resume.questionId,
|
|
91
|
+
...(input.resume.runId === undefined ? {} : { runId: single(input.resume.runId, 'resume runId') }) } : undefined;
|
|
76
92
|
return { ...input, title, scqa: { s, c, ...(q ? { q } : {}), ...(a ? { a } : {}) }, options, recommendation,
|
|
93
|
+
...(resume ? { resume } : {}),
|
|
77
94
|
raisedBy: { agent: safe(single(input.raisedBy.agent, 'agent')), ...(input.raisedBy.track ? { track: input.raisedBy.track } : {}), ...(input.raisedBy.session ? { session: safe(single(input.raisedBy.session, 'session')) } : {}) },
|
|
78
95
|
...(input.refs ? { refs: input.refs.map(r => safe(single(r, 'ref'))) } : {}),
|
|
79
96
|
...(input.dueAt ? { dueAt: utc(input.dueAt) } : {}) };
|
|
@@ -83,12 +100,16 @@ export class DecisionLedger {
|
|
|
83
100
|
readonly path: string;
|
|
84
101
|
private readonly lockPath: string;
|
|
85
102
|
private readonly now: () => Date;
|
|
103
|
+
private readonly writeAnswer: typeof writePendingQuestionAnswer;
|
|
104
|
+
private readonly stateDir: string;
|
|
86
105
|
private versionResolver?: (at: string) => Versions;
|
|
87
106
|
private readonly versionOptions: DecisionLedgerOptions;
|
|
88
107
|
constructor(options: DecisionLedgerOptions = {}) {
|
|
89
|
-
this.
|
|
108
|
+
this.stateDir = options.stateDir ?? elanousStateRoot();
|
|
109
|
+
this.path = join(this.stateDir, 'decisions', 'decisions.jsonl');
|
|
90
110
|
this.lockPath = `${this.path}.lock`;
|
|
91
111
|
this.now = options.now ?? (() => new Date());
|
|
112
|
+
this.writeAnswer = options.writeAnswer ?? writePendingQuestionAnswer;
|
|
92
113
|
this.versionOptions = options;
|
|
93
114
|
this.versionResolver = options.resolveVersion;
|
|
94
115
|
}
|
|
@@ -196,7 +217,14 @@ export class DecisionLedger {
|
|
|
196
217
|
return entry;
|
|
197
218
|
});
|
|
198
219
|
}
|
|
220
|
+
/** Record the decision and say whether its answer reached the waiting run (null = the decision resumes nothing). */
|
|
221
|
+
decideWithDelivery(id: string, choice: string, by: DecisionActor, note?: string, decidedAt?: string): { entry: DecisionEntry; delivery: AnswerDelivery } {
|
|
222
|
+
return this.decideLocked(id, choice, by, note, decidedAt);
|
|
223
|
+
}
|
|
199
224
|
decide(id: string, choice: string, by: DecisionActor, note?: string, decidedAt?: string): DecisionEntry {
|
|
225
|
+
return this.decideLocked(id, choice, by, note, decidedAt).entry;
|
|
226
|
+
}
|
|
227
|
+
private decideLocked(id: string, choice: string, by: DecisionActor, note?: string, decidedAt?: string): { entry: DecisionEntry; delivery: AnswerDelivery } {
|
|
200
228
|
if (!by || (by.kind !== 'human' && by.kind !== 'auto')) throw new Error('decidedBy is required');
|
|
201
229
|
if (by.kind === 'auto') {
|
|
202
230
|
single(by.delegation, 'delegation'); single(by.agent, 'agent');
|
|
@@ -211,9 +239,52 @@ export class DecisionLedger {
|
|
|
211
239
|
if (old.raisedAt && at < old.raisedAt) throw new Error('decision before raise');
|
|
212
240
|
this.append({ type: 'decided', id, at, by, choice, version: this.version(at), ...(note ? { note: safe(single(note, 'note')) } : {}) });
|
|
213
241
|
debug.log('decisions', 'decided', { id, category: old.category, by: by.kind });
|
|
214
|
-
|
|
242
|
+
const decided = this.show(id);
|
|
243
|
+
return { entry: decided, delivery: this.deliverAnswer(decided) };
|
|
215
244
|
});
|
|
216
245
|
}
|
|
246
|
+
/** Retry delivery of the recorded choice without adding another decided event or changing its actor/note. */
|
|
247
|
+
retryAnswer(id: string): DecisionEntry {
|
|
248
|
+
return this.locked(() => {
|
|
249
|
+
const entry = this.show(id);
|
|
250
|
+
if (entry.status !== 'decided' || !entry.resume || !entry.choice || !entry.decidedBy) throw new Error(`decision has no recorded resume answer: ${id}`);
|
|
251
|
+
this.deliverAnswer(entry, true);
|
|
252
|
+
return entry;
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
private deliverAnswer(entry: DecisionEntry, throwOnFailure = false): AnswerDelivery {
|
|
256
|
+
if (!entry.resume) return null;
|
|
257
|
+
const { questionId, runId } = entry.resume;
|
|
258
|
+
try {
|
|
259
|
+
const label = entry.options.find(option => option.key === entry.choice)?.label;
|
|
260
|
+
if (!label || !entry.choice) throw new Error('recorded choice not found');
|
|
261
|
+
const deps = { root: () => this.stateDir };
|
|
262
|
+
const pending = readPendingQuestions(deps);
|
|
263
|
+
if (!pending.ok) throw new Error(pending.error);
|
|
264
|
+
const target = pending.questions.find(item => item.id === questionId);
|
|
265
|
+
if (!target || target.questions.length !== 1 || !target.questions[0]?.id) throw new Error('pending question target not found or ambiguous');
|
|
266
|
+
const question = target.questions[0];
|
|
267
|
+
if (question.options.length !== entry.options.length || question.options.some((option, index) =>
|
|
268
|
+
entry.options[index]?.key !== String.fromCharCode(97 + index) || entry.options[index]?.label !== option.label)) {
|
|
269
|
+
throw new Error('pending question options differ from decision');
|
|
270
|
+
}
|
|
271
|
+
const answerKey = question.id;
|
|
272
|
+
const answer = { id: questionId, result: { answers: { [answerKey]: `${entry.choice}) ${label}` },
|
|
273
|
+
...(entry.note ? { otherText: { [answerKey]: entry.note } } : {}) } };
|
|
274
|
+
const existing = readPendingQuestionAnswer(questionId, deps);
|
|
275
|
+
if (!existing.ok) throw new Error('existing answer unreadable');
|
|
276
|
+
if (existing.answer) {
|
|
277
|
+
if (JSON.stringify(existing.answer) !== JSON.stringify(answer)) throw new Error('pending question already answered differently');
|
|
278
|
+
} else this.writeAnswer(answer, deps);
|
|
279
|
+
debug.log('hitl.card-bridge', 'answered', { decisionId: entry.id, questionId, runId, via: entry.decidedBy?.kind });
|
|
280
|
+
return { ok: true, questionId };
|
|
281
|
+
} catch (error) {
|
|
282
|
+
const reason = error instanceof Error ? (error as NodeJS.ErrnoException).code ?? error.name : typeof error;
|
|
283
|
+
debug.log('hitl.card-bridge', 'answer-write-failed', { decisionId: entry.id, questionId, reason });
|
|
284
|
+
if (throwOnFailure) throw new DecisionAnswerDeliveryError(entry.id, questionId);
|
|
285
|
+
return { ok: false, questionId, reason: error instanceof Error ? error.message.slice(0, 120) : String(reason) };
|
|
286
|
+
}
|
|
287
|
+
}
|
|
217
288
|
/** Import an open historical request with known alternatives and an unknown original time. */
|
|
218
289
|
importOpen(input: RaiseInput): DecisionEntry {
|
|
219
290
|
const clean = validate(input);
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, writeSync } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { elanousStateRoot } from '../autopilot/state-paths.js';
|
|
5
|
+
import { createLinearIssue, commentLinearIssue, findLinearDecisionIssue, moveLinearIssueToStateType, resolveLinearProjectId } from '../connectors/linear.js';
|
|
6
|
+
import { debug } from '../debug/log.js';
|
|
7
|
+
import { getSecretAsync } from '../nexus/config/secrets/index.js';
|
|
8
|
+
import { getUserConfig, type UserConfig } from '../user-config.js';
|
|
9
|
+
import { DecisionLedger, type DecisionEntry } from './decision-ledger.js';
|
|
10
|
+
|
|
11
|
+
interface ProjectionRecord { issue: string; closedAt?: string; commentPosted?: boolean }
|
|
12
|
+
type ProjectionState = Record<string, ProjectionRecord>;
|
|
13
|
+
export interface DecisionLinearProjectionDeps {
|
|
14
|
+
stateDir?: string;
|
|
15
|
+
ledger?: Pick<DecisionLedger, 'list'>;
|
|
16
|
+
config?: Pick<UserConfig, 'decisions' | 'coo'>;
|
|
17
|
+
getSecret?: (id: string) => Promise<string | undefined>;
|
|
18
|
+
fetch?: typeof fetch;
|
|
19
|
+
dryRun?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface DecisionLinearProjectionResult {
|
|
22
|
+
created: number; closed: number; skipped: number; failed: number;
|
|
23
|
+
reason?: string;
|
|
24
|
+
plan?: Array<{ decisionId: string; action: 'create' | 'close'; issue?: string }>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readState(path: string): ProjectionState {
|
|
28
|
+
if (!existsSync(path)) return {};
|
|
29
|
+
const value: unknown = JSON.parse(readFileSync(path, 'utf8'));
|
|
30
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.entries(value).some(([id, row]) =>
|
|
31
|
+
!/^D-\d{8}-\d+$/.test(id) || !row || typeof row !== 'object' || Array.isArray(row) ||
|
|
32
|
+
typeof (row as ProjectionRecord).issue !== 'string' || !(row as ProjectionRecord).issue ||
|
|
33
|
+
((row as ProjectionRecord).closedAt !== undefined && typeof (row as ProjectionRecord).closedAt !== 'string') ||
|
|
34
|
+
((row as ProjectionRecord).commentPosted !== undefined && typeof (row as ProjectionRecord).commentPosted !== 'boolean'))) {
|
|
35
|
+
throw new Error('invalid decision Linear projection state');
|
|
36
|
+
}
|
|
37
|
+
return value as ProjectionState;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function saveState(path: string, state: ProjectionState): void {
|
|
41
|
+
mkdirSync(join(path, '..'), { recursive: true });
|
|
42
|
+
const temp = `${path}.${randomUUID()}.tmp`;
|
|
43
|
+
try {
|
|
44
|
+
writeFileSync(temp, JSON.stringify(state), { mode: 0o600 });
|
|
45
|
+
renameSync(temp, path);
|
|
46
|
+
} finally { if (existsSync(temp)) unlinkSync(temp); }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function description(entry: DecisionEntry): string {
|
|
50
|
+
return [`카테고리: ${entry.category}`, ...(entry.dueAt ? [`기한: ${entry.dueAt}`] : []),
|
|
51
|
+
`결정 id: ${entry.id}`, '텔레그램/디스코드 카드에서 정한다',
|
|
52
|
+
...(entry.refs ?? []).filter(ref => ref.startsWith('http'))].join('\n');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A single writer holds the file lock across remote creation and local persistence. */
|
|
56
|
+
export async function projectDecisionsToLinear(deps: DecisionLinearProjectionDeps = {}): Promise<DecisionLinearProjectionResult> {
|
|
57
|
+
const result: DecisionLinearProjectionResult = { created: 0, closed: 0, skipped: 0, failed: 0 };
|
|
58
|
+
const config = deps.config ?? getUserConfig();
|
|
59
|
+
if (config.decisions?.linearProjection?.enabled !== true) {
|
|
60
|
+
result.reason = 'decisions.linearProjection.enabled 꺼짐';
|
|
61
|
+
debug.log('decisions.linear', 'skipped', { decisionId: null, issue: null, reason: 'disabled' });
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
// A dry run only reads the local ledger and state — it must work before a key is set.
|
|
65
|
+
const apiKey = (deps.dryRun ? '' : await (deps.getSecret ?? getSecretAsync)('connector.linear.apiKey')) ?? '';
|
|
66
|
+
if (!deps.dryRun && !apiKey) {
|
|
67
|
+
result.reason = 'Linear 키가 없습니다 — `elanous connector linear set-key`';
|
|
68
|
+
debug.log('decisions.linear', 'skipped', { decisionId: null, issue: null, reason: 'missing-key' });
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
const stateDir = deps.stateDir ?? elanousStateRoot();
|
|
72
|
+
const path = join(stateDir, 'decisions', 'linear-projection.json');
|
|
73
|
+
const ledger = deps.ledger ?? new DecisionLedger({ stateDir });
|
|
74
|
+
const entries = ledger.list({ status: 'all' });
|
|
75
|
+
const fetchFn = deps.fetch ?? fetch;
|
|
76
|
+
const lockPath = `${path}.lock`;
|
|
77
|
+
let lock: number | undefined;
|
|
78
|
+
if (!deps.dryRun) {
|
|
79
|
+
mkdirSync(join(path, '..'), { recursive: true });
|
|
80
|
+
const deadline = Date.now() + 10_000;
|
|
81
|
+
for (;;) {
|
|
82
|
+
try { lock = openSync(lockPath, 'wx', 0o600); writeSync(lock, `${process.pid}`); break; }
|
|
83
|
+
catch (error) {
|
|
84
|
+
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
|
|
85
|
+
try {
|
|
86
|
+
const pid = Number(readFileSync(lockPath, 'utf8'));
|
|
87
|
+
if (Number.isSafeInteger(pid) && pid > 0 && Date.now() - statSync(lockPath).mtimeMs > 30_000) {
|
|
88
|
+
try { process.kill(pid, 0); }
|
|
89
|
+
catch (e) { if ((e as NodeJS.ErrnoException).code === 'ESRCH') { unlinkSync(lockPath); continue; } }
|
|
90
|
+
}
|
|
91
|
+
} catch (e) { if ((e as NodeJS.ErrnoException).code === 'ENOENT') continue; }
|
|
92
|
+
if (Date.now() >= deadline) throw new Error('decision Linear projection lock timeout');
|
|
93
|
+
await Bun.sleep(25);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const state = readState(path);
|
|
99
|
+
if (deps.dryRun) result.plan = [];
|
|
100
|
+
let projectId: string | undefined;
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
let issue = state[entry.id]?.issue;
|
|
103
|
+
try {
|
|
104
|
+
const record = state[entry.id];
|
|
105
|
+
if (entry.status === 'open' && !record) {
|
|
106
|
+
if (deps.dryRun) {
|
|
107
|
+
result.plan!.push({ decisionId: entry.id, action: 'create' });
|
|
108
|
+
result.created++;
|
|
109
|
+
} else {
|
|
110
|
+
projectId ??= await resolveLinearProjectId({ apiKey, project: config.coo?.linearProject ?? '외부 행정·큰 일 (COO)', fetch: fetchFn });
|
|
111
|
+
const existing = await findLinearDecisionIssue({ apiKey, projectId, decisionId: entry.id, fetch: fetchFn });
|
|
112
|
+
issue = existing ?? await createLinearIssue({ apiKey, projectId, title: `[결정] ${entry.title}`, description: description(entry),
|
|
113
|
+
...(entry.dueAt ? { dueDate: entry.dueAt.slice(0, 10) } : {}), fetch: fetchFn });
|
|
114
|
+
state[entry.id] = { issue };
|
|
115
|
+
saveState(path, state);
|
|
116
|
+
if (existing) {
|
|
117
|
+
result.skipped++;
|
|
118
|
+
debug.log('decisions.linear', 'skipped', { decisionId: entry.id, issue, reason: 'recovered' });
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
result.created++;
|
|
122
|
+
}
|
|
123
|
+
debug.log('decisions.linear', 'created', { decisionId: entry.id, issue: issue ?? null, reason: deps.dryRun ? 'dry-run' : 'created' });
|
|
124
|
+
} else if (entry.status !== 'open' && record && !record.closedAt) {
|
|
125
|
+
if (deps.dryRun) {
|
|
126
|
+
result.plan!.push({ decisionId: entry.id, action: 'close', issue: record.issue });
|
|
127
|
+
} else {
|
|
128
|
+
if (!record.commentPosted) {
|
|
129
|
+
const body = entry.status === 'withdrawn' ? '철회됨'
|
|
130
|
+
: `결정됨: ${entry.choice ?? '선택 키 미상'} · ${entry.decidedBy?.kind ?? '주체 미상'} · ${entry.decidedAt ?? '시각 미상'}`;
|
|
131
|
+
await commentLinearIssue({ apiKey, issueId: record.issue, body, fetch: fetchFn });
|
|
132
|
+
record.commentPosted = true;
|
|
133
|
+
saveState(path, state);
|
|
134
|
+
}
|
|
135
|
+
await moveLinearIssueToStateType({ apiKey, issueId: record.issue, type: entry.status === 'decided' ? 'completed' : 'canceled', fetch: fetchFn });
|
|
136
|
+
record.closedAt = new Date().toISOString();
|
|
137
|
+
saveState(path, state);
|
|
138
|
+
}
|
|
139
|
+
result.closed++;
|
|
140
|
+
debug.log('decisions.linear', 'closed', { decisionId: entry.id, issue: record.issue, reason: deps.dryRun ? 'dry-run' : entry.status });
|
|
141
|
+
} else {
|
|
142
|
+
result.skipped++;
|
|
143
|
+
debug.log('decisions.linear', 'skipped', { decisionId: entry.id, issue: issue ?? null, reason: record?.closedAt ? 'already-closed' : record ? 'already-created' : 'not-projected' });
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
result.failed++;
|
|
147
|
+
const reason = error instanceof Error
|
|
148
|
+
? (error as NodeJS.ErrnoException).code ?? (/^Linear GraphQL HTTP \d{3}$/.test(error.message) ? error.message : error.name)
|
|
149
|
+
: 'unknown';
|
|
150
|
+
debug.log('decisions.linear', 'failed', { decisionId: entry.id, issue: issue ?? null, reason });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return result;
|
|
154
|
+
} finally {
|
|
155
|
+
if (lock !== undefined) { closeSync(lock); unlinkSync(lockPath); }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { debug } from '../debug/log.js';
|
|
2
|
+
import { listSubscriptions } from '../web-push/subscriptions.js';
|
|
3
|
+
import { sendPushToAll } from '../web-push/sender.js';
|
|
4
|
+
import { type CardTransport, type CardView } from './decision-cards.js';
|
|
5
|
+
import { DecisionLedger } from './decision-ledger.js';
|
|
6
|
+
|
|
7
|
+
const ID_IN_CARD = /^🗳 결정 요청 (D-\d{8}-\d+)(?: ·|\n)/;
|
|
8
|
+
const ID_IN_REMINDER = /^⏰ 기한 2시간 전 — (D-\d{8}-\d+) /;
|
|
9
|
+
const TITLE_LIMIT = 60;
|
|
10
|
+
|
|
11
|
+
export function webPushDecisionTransport(
|
|
12
|
+
ledger: DecisionLedger,
|
|
13
|
+
deps: { subscriptions?: typeof listSubscriptions; push?: typeof sendPushToAll } = {},
|
|
14
|
+
): CardTransport {
|
|
15
|
+
const subscriptions = deps.subscriptions ?? listSubscriptions;
|
|
16
|
+
const push = deps.push ?? sendPushToAll;
|
|
17
|
+
const title = (prefix: string, name: string) => prefix + Array.from(name).slice(0, TITLE_LIMIT - Array.from(prefix).length).join('');
|
|
18
|
+
const url = (id: string) => `/approvals?decision=${encodeURIComponent(id)}`;
|
|
19
|
+
const tag = (id: string) => `decision-${id}`;
|
|
20
|
+
return {
|
|
21
|
+
platform: 'webpush',
|
|
22
|
+
ownerChats: async () => subscriptions().length ? ['webpush'] : [],
|
|
23
|
+
send: async (_chat, view: CardView) => {
|
|
24
|
+
const id = ID_IN_CARD.exec(view.text)?.[1];
|
|
25
|
+
if (!id || !subscriptions().length) return null;
|
|
26
|
+
const entry = ledger.show(id);
|
|
27
|
+
if (entry.status !== 'open') return null;
|
|
28
|
+
const recommendation = 'skipped' in entry.recommendation ? '추천안 없음' : `추천안: ${entry.recommendation.option.toUpperCase()}`;
|
|
29
|
+
const body = `${recommendation}${entry.dueAt ? ` · 기한: ${new Intl.DateTimeFormat('ko-KR', { timeZone: 'Asia/Seoul', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false }).format(new Date(entry.dueAt))}` : ''}`;
|
|
30
|
+
const result = await push({ title: title('대표 결정 · ', entry.title), body, url: url(id), tag: tag(id) });
|
|
31
|
+
debug.log('decisions.webpush', 'card-sent', { id, delivered: result.delivered });
|
|
32
|
+
return result.delivered ? { chat: 'webpush', message: id } : null;
|
|
33
|
+
},
|
|
34
|
+
edit: async (ref, _view) => {
|
|
35
|
+
const id = ref.message;
|
|
36
|
+
const entry = ledger.show(id);
|
|
37
|
+
if (entry.status === 'open' || !subscriptions().length) return;
|
|
38
|
+
const result = await push({ title: title('결정됨 · ', entry.title), url: url(id), tag: tag(id), data: { silent: true } });
|
|
39
|
+
debug.log('decisions.webpush', 'card-closed', { id, delivered: result.delivered });
|
|
40
|
+
},
|
|
41
|
+
notify: async (_chat, text) => {
|
|
42
|
+
const id = ID_IN_REMINDER.exec(text)?.[1];
|
|
43
|
+
if (!id || !subscriptions().length) return;
|
|
44
|
+
const entry = ledger.show(id);
|
|
45
|
+
if (entry.status !== 'open') return;
|
|
46
|
+
const result = await push({ title: title('기한 알림 · ', entry.title), body: `기한 2시간 전 · ${id}`, url: url(id), tag: tag(id) });
|
|
47
|
+
debug.log('decisions.webpush', 'card-sent', { id, delivered: result.delivered });
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { listChecklist, type ChecklistItem } from '../release-loop/checklist.js';
|
|
2
|
+
|
|
3
|
+
export interface InterpretationCard {
|
|
4
|
+
what: string;
|
|
5
|
+
cell: string | null;
|
|
6
|
+
unknowns: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type CellCandidate = Pick<ChecklistItem, 'id' | 'title'>;
|
|
10
|
+
|
|
11
|
+
/** The same SQLite-backed cells exposed by `elanous release checklist list --version <v>`. */
|
|
12
|
+
export function readCellCandidates(version: string): CellCandidate[] {
|
|
13
|
+
return listChecklist(version).items.map(({ id, title }) => ({ id, title }));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Interpret a single directive without querying state or inventing a release cell. */
|
|
17
|
+
export function interpretDirective(line: string, candidates: readonly CellCandidate[]): InterpretationCard {
|
|
18
|
+
if (/\r|\n/.test(line)) throw new Error('지시는 한 줄이어야 한다');
|
|
19
|
+
const what = line.trim();
|
|
20
|
+
if (!what) throw new Error('빈 지시는 해석할 수 없다');
|
|
21
|
+
|
|
22
|
+
const matches = candidates.filter(({ id, title }) =>
|
|
23
|
+
(id && new RegExp(`(^|[^\\p{L}\\p{N}])${id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?=$|[^\\p{L}\\p{N}]|[을를은는이가에의와과])`, 'u').test(what)) ||
|
|
24
|
+
(title && what.includes(title)));
|
|
25
|
+
|
|
26
|
+
if (matches.length === 1) return { what, cell: matches[0]!.id, unknowns: [] };
|
|
27
|
+
return {
|
|
28
|
+
what,
|
|
29
|
+
cell: null,
|
|
30
|
+
unknowns: [matches.length ? `칸 후보가 여럿이다: ${matches.map(({ id }) => id).join(', ')}` : '연결할 칸을 확인해야 한다'],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function renderInterpretationCard(card: InterpretationCard): string {
|
|
35
|
+
return `무엇: ${card.what}\n칸: ${card.cell ?? '확인 필요'}\n미확인: ${card.unknowns.join(', ') || '없음'}`;
|
|
36
|
+
}
|
package/src/discord.ts
CHANGED
|
@@ -837,13 +837,14 @@ export class DiscordBot {
|
|
|
837
837
|
}
|
|
838
838
|
|
|
839
839
|
// Addressed work is submitted only by configured owners; ordinary chat keeps its existing path.
|
|
840
|
-
if (this.allowedUsers.has(author.id) && parseSeatAddress(ctx.text)) {
|
|
840
|
+
if (this.allowedUsers.has(author.id) && (parseSeatAddress(ctx.text) || (ctx.isDm && ctx.text && !ctx.text.startsWith('/')))) {
|
|
841
841
|
const threadId = typeof m.thread_id === 'string' && m.thread_id ? m.thread_id : undefined;
|
|
842
842
|
const replyChannelId = threadId ?? ctx.channelId;
|
|
843
843
|
try {
|
|
844
844
|
const seatReply = await handleDiscordSeatWork(ctx.text, {
|
|
845
845
|
channelId: replyChannelId,
|
|
846
846
|
messageId: ctx.messageId,
|
|
847
|
+
userId: ctx.userId, isDm: ctx.isDm,
|
|
847
848
|
...(threadId ? { threadId } : {}),
|
|
848
849
|
}, this.seatWorkDeps);
|
|
849
850
|
if (seatReply !== null) {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { LLMToolSpec } from '../llm.js';
|
|
2
|
+
import { debug } from '../debug/log.js';
|
|
3
|
+
import { getUserConfig } from '../user-config.js';
|
|
4
|
+
import { getSecretAsync } from '../nexus/config/secrets/index.js';
|
|
5
|
+
import { fetchLinearProjectIssues, type LinearProjectIssue } from '../connectors/linear.js';
|
|
6
|
+
import { formatElanousCard } from './elanous-card.js';
|
|
7
|
+
|
|
8
|
+
export const COO_ADMIN_SPEC: LLMToolSpec = {
|
|
9
|
+
name: 'coo_admin',
|
|
10
|
+
description: '읽기 전용 Linear COO 행정 조회 — «행정 뭐 남았어 · COO 할 일 · 마감 다가오는 행정» 질문에 사용. 외부 행정 프로젝트의 열린 일을 마감 순으로 읽는다.',
|
|
11
|
+
parameters: { type: 'object', properties: {}, required: [] },
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export interface CooAdminDeps {
|
|
15
|
+
getSecret?: (id: string) => Promise<string | undefined>;
|
|
16
|
+
fetch?: typeof fetch;
|
|
17
|
+
project?: string;
|
|
18
|
+
now?: Date;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Reads the configured Linear project without changing any Linear issue. */
|
|
22
|
+
export async function dispatchCooAdmin(_args: Record<string, unknown>, deps: CooAdminDeps = {}): Promise<string> {
|
|
23
|
+
let count = 0;
|
|
24
|
+
let overdue = 0;
|
|
25
|
+
try {
|
|
26
|
+
const apiKey = await (deps.getSecret ?? getSecretAsync)('connector.linear.apiKey');
|
|
27
|
+
if (!apiKey) {
|
|
28
|
+
debug.log('coo.admin', 'failed', { count, overdue, reason: 'missing-key' });
|
|
29
|
+
return 'Linear 키가 없습니다 — `elanous connector linear set-key`';
|
|
30
|
+
}
|
|
31
|
+
const project = deps.project ?? getUserConfig().coo?.linearProject ?? '외부 행정·큰 일 (COO)';
|
|
32
|
+
const issues = await fetchLinearProjectIssues({ apiKey, project, fetch: deps.fetch ?? fetch });
|
|
33
|
+
const today = deps.now ?? new Date();
|
|
34
|
+
const todayUtc = Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate());
|
|
35
|
+
const daysUntil = (date: string) => Math.round((Date.parse(`${date.slice(0, 10)}T00:00:00Z`) - todayUtc) / 86_400_000);
|
|
36
|
+
issues.sort((a, b) => {
|
|
37
|
+
const deadlineOrder = a.dueDate == null ? (b.dueDate == null ? 0 : 1)
|
|
38
|
+
: b.dueDate == null ? -1 : daysUntil(a.dueDate) - daysUntil(b.dueDate);
|
|
39
|
+
return deadlineOrder || (a.priority === 0 ? 5 : a.priority) - (b.priority === 0 ? 5 : b.priority);
|
|
40
|
+
});
|
|
41
|
+
count = issues.length;
|
|
42
|
+
const lines = issues.map((item: LinearProjectIssue) => {
|
|
43
|
+
const days = item.dueDate ? daysUntil(item.dueDate) : null;
|
|
44
|
+
if (days !== null && days < 0) overdue++;
|
|
45
|
+
const deadline = days === null ? '마감 없음' : days < 0 ? `지남 ${-days}일` : `D-${days}`;
|
|
46
|
+
return `${deadline} · ${item.title} · ${item.state.name} · ${item.assignee?.name ?? '미배정'}\n${item.url}`;
|
|
47
|
+
});
|
|
48
|
+
debug.log('coo.admin', 'read', { count, overdue, reason: 'ok' });
|
|
49
|
+
const scope = issues.truncated ? '최대 250건 중 조회한 항목만 마감 순으로 표시 (이후 항목은 포함되지 않음)\n' : '';
|
|
50
|
+
const prose = lines.length ? `${scope}${lines.join('\n')}` : '남은 행정 0건';
|
|
51
|
+
const card = formatElanousCard({ kind: 'coo-admin', items: issues.map(item => ({
|
|
52
|
+
title: item.title, due: item.dueDate?.slice(0, 10) ?? null,
|
|
53
|
+
daysLeft: item.dueDate ? daysUntil(item.dueDate) : null,
|
|
54
|
+
state: item.state.name, owner: item.assignee?.name ?? '미배정', url: item.url,
|
|
55
|
+
})) });
|
|
56
|
+
return `${prose}\n${card}`;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
59
|
+
debug.log('coo.admin', 'failed', { count, overdue, reason: 'linear-read-failed' });
|
|
60
|
+
return `못 읽었습니다(${reason})`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -28,6 +28,9 @@ import { OPS_STATUS_SPEC, dispatchOpsStatus } from './ops-status-tool.js';
|
|
|
28
28
|
import { SE_BUILD_SPEC, dispatchSeBuild } from './se-build-tool.js';
|
|
29
29
|
import { LOGS_QUERY_SPEC, dispatchLogsQuery } from './logs-tool.js';
|
|
30
30
|
import { MISSION_DECIDE_SPEC, dispatchMissionDecide } from '../autopilot/mission-decide-tool.js';
|
|
31
|
+
import { COO_ADMIN_SPEC, dispatchCooAdmin } from './coo-admin-tool.js';
|
|
32
|
+
import { RELEASE_STATUS_SPEC, RELEASE_CHANGE_SPEC, dispatchReleaseStatus, dispatchReleaseChange } from './release-tool.js';
|
|
33
|
+
import { OPS_SEATS_SPEC, DECISIONS_PENDING_SPEC, dispatchOpsSeats, dispatchDecisionsPending } from './ops-facts-tool.js';
|
|
31
34
|
|
|
32
35
|
/** memory_recall — 크로스서피스 기억(자기 발송 원장 회상). 도메인 무관(domain 필터는 옵션).
|
|
33
36
|
* finance-tools 에서 L2 로 이관(2026-07-08). */
|
|
@@ -118,6 +121,8 @@ const CORE_TOOL_HANDLERS: Record<string, (args: Record<string, unknown>, onParti
|
|
|
118
121
|
se_build: dispatchSeBuild, // SE 격리 빌드 관측(빌드 안 뭐 하나·로그 tail·worktree diff)·코어
|
|
119
122
|
logs_query: dispatchLogsQuery, // 크로스서피스 디버그 로그 조회(logs.db·LF3)·READ-ONLY 코어
|
|
120
123
|
mission_decide: dispatchMissionDecide, // 미션 결정 기록(re-ground·defer·boundary…)·3박자 주입·코어
|
|
124
|
+
coo_admin: (args) => dispatchCooAdmin(args),
|
|
125
|
+
release_status: async (args) => dispatchReleaseStatus(args),
|
|
121
126
|
};
|
|
122
127
|
|
|
123
128
|
/** Worker-side search entry: dispatch must not re-enter its own budget wrapper. */
|
|
@@ -136,7 +141,7 @@ const SELF_RECALL_BUDGET_SPEC: LLMToolSpec = {
|
|
|
136
141
|
},
|
|
137
142
|
},
|
|
138
143
|
};
|
|
139
|
-
export const CORE_TOOL_SPECS: LLMToolSpec[] = [SCHEDULE_MANAGE_SPEC, SESSION_MANAGE_SPEC, MEMORY_RECALL_SPEC, FACT_CHECK_SPEC, SELF_RECALL_BUDGET_SPEC, AUTOPILOT_MISSION_SPEC, OPS_STATUS_SPEC, SE_BUILD_SPEC, LOGS_QUERY_SPEC, MISSION_DECIDE_SPEC];
|
|
144
|
+
export const CORE_TOOL_SPECS: LLMToolSpec[] = [SCHEDULE_MANAGE_SPEC, SESSION_MANAGE_SPEC, MEMORY_RECALL_SPEC, FACT_CHECK_SPEC, SELF_RECALL_BUDGET_SPEC, AUTOPILOT_MISSION_SPEC, OPS_STATUS_SPEC, SE_BUILD_SPEC, LOGS_QUERY_SPEC, MISSION_DECIDE_SPEC, COO_ADMIN_SPEC, RELEASE_STATUS_SPEC, RELEASE_CHANGE_SPEC, OPS_SEATS_SPEC, DECISIONS_PENDING_SPEC];
|
|
140
145
|
|
|
141
146
|
export interface CoreTools {
|
|
142
147
|
specs: LLMToolSpec[];
|
|
@@ -153,11 +158,14 @@ export function buildCoreTools(recallSearch?: RecallSearch): CoreTools {
|
|
|
153
158
|
names,
|
|
154
159
|
dispatch: async (name, args, onPartial, context) => {
|
|
155
160
|
const h = CORE_TOOL_HANDLERS[name];
|
|
156
|
-
if (!h) return { error: `unknown core tool: ${name}` };
|
|
161
|
+
if (!h && name !== 'release_change' && name !== 'ops_seats' && name !== 'decisions_pending') return { error: `unknown core tool: ${name}` };
|
|
162
|
+
if (name === 'release_change') return dispatchReleaseChange(args, context);
|
|
163
|
+
if (name === 'ops_seats') return dispatchOpsSeats(args, context);
|
|
164
|
+
if (name === 'decisions_pending') return dispatchDecisionsPending(context);
|
|
157
165
|
if (name === 'memory_recall' || name === 'self_recall') {
|
|
158
166
|
return runRecall(name, args, context ?? { surface: 'skill' }, recallSearch, onPartial);
|
|
159
167
|
}
|
|
160
|
-
return h(args, onPartial);
|
|
168
|
+
return h!(args, onPartial);
|
|
161
169
|
},
|
|
162
170
|
};
|
|
163
171
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface ElanousCard {
|
|
2
|
+
kind: 'coo-admin' | 'release-schedule' | 'release-checklist';
|
|
3
|
+
items: Array<{ title: string; due: string | null; daysLeft: number | null; state: string; owner: string; url?: string }>;
|
|
4
|
+
meta?: Record<string, unknown>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function formatElanousCard(card: ElanousCard): string {
|
|
8
|
+
return `\`\`\`elanous-card\n${JSON.stringify(card)}\n\`\`\``;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Reads every valid card block in a tool response; ignores unrelated prose and malformed blocks. */
|
|
12
|
+
export function parseElanousCard(text: string): ElanousCard[] {
|
|
13
|
+
const cards: ElanousCard[] = [];
|
|
14
|
+
for (const match of text.matchAll(/```elanous-card\r?\n([^\r\n]+)\r?\n```/g)) {
|
|
15
|
+
try {
|
|
16
|
+
const value: unknown = JSON.parse(match[1]!);
|
|
17
|
+
if (!value || typeof value !== 'object') continue;
|
|
18
|
+
const card = value as Partial<ElanousCard>;
|
|
19
|
+
if (!['coo-admin', 'release-schedule', 'release-checklist'].includes(card.kind ?? '') || !Array.isArray(card.items)) continue;
|
|
20
|
+
if (!card.items.every(item => item && typeof item.title === 'string' &&
|
|
21
|
+
(item.due === null || /^\d{4}-\d{2}-\d{2}$/.test(item.due)) &&
|
|
22
|
+
(item.daysLeft === null || typeof item.daysLeft === 'number') &&
|
|
23
|
+
typeof item.state === 'string' && typeof item.owner === 'string' &&
|
|
24
|
+
(item.url === undefined || typeof item.url === 'string'))) continue;
|
|
25
|
+
cards.push(card as ElanousCard);
|
|
26
|
+
} catch { /* Other fenced content is not a card. */ }
|
|
27
|
+
}
|
|
28
|
+
return cards;
|
|
29
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { LLMToolSpec } from '../llm.js';
|
|
2
|
+
import type { ToolRuntimeContext } from '../tool-runtime/types.js';
|
|
3
|
+
import { debug } from '../debug/log.js';
|
|
4
|
+
import { DecisionLedger, type DecisionEntry } from '../decisions/decision-ledger.js';
|
|
5
|
+
import { buildSeatsBoard, todayKst, type SeatsSources } from '../nexus/api/ops-seats.js';
|
|
6
|
+
import { liveSeatsSources } from '../nexus/api/ops-seats-sources.js';
|
|
7
|
+
|
|
8
|
+
export const OPS_SEATS_SPEC: LLMToolSpec = {
|
|
9
|
+
name: 'ops_seats',
|
|
10
|
+
description: '오너 전용 읽기 — 지금 자리(OP·TC·MK·UX)별 진행(now), 오늘 착지(landed), 빨강(red), 미결 결정 수를 원장에서 확인한다. null은 못 읽은 원천이며 0이나 빈 목록이 아니다.',
|
|
11
|
+
parameters: { type: 'object', properties: { date: { type: 'string', description: '조회할 KST 날짜 YYYY-MM-DD. 생략하면 KST 오늘.' } }, required: [] },
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const DECISIONS_PENDING_SPEC: LLMToolSpec = {
|
|
15
|
+
name: 'decisions_pending',
|
|
16
|
+
description: '오너 전용 읽기 — 결정 원장의 열린 결정을 기한 순으로 조회한다. 결정 선택·기록은 하지 않는다.',
|
|
17
|
+
parameters: { type: 'object', properties: {}, required: [] },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export interface OpsFactsDeps {
|
|
21
|
+
seatsSources?: () => SeatsSources;
|
|
22
|
+
listDecisions?: (filters: { status: 'open' }) => DecisionEntry[];
|
|
23
|
+
today?: () => string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function ownerVerified(context?: ToolRuntimeContext): boolean {
|
|
27
|
+
return context?.requestOrigin !== 'external-agent' && !!context?.sessionId && !!context?.verifiedOwner?.id.trim();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function logCall(tool: 'ops_seats' | 'decisions_pending', ok: boolean, nulls: number): void {
|
|
31
|
+
debug.log('ops.facts', 'call', { tool, ok, nulls });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Unreadable seat sources remain null; only rename the board's `blocked` field to `red` for this tool. */
|
|
35
|
+
export async function dispatchOpsSeats(args: Record<string, unknown>, context?: ToolRuntimeContext, deps: OpsFactsDeps = {}) {
|
|
36
|
+
if (!ownerVerified(context)) {
|
|
37
|
+
logCall('ops_seats', false, 0);
|
|
38
|
+
return { error: '오너 확인이 필요한 조회다' };
|
|
39
|
+
}
|
|
40
|
+
const date = args.date === undefined ? (deps.today ?? todayKst)() : args.date;
|
|
41
|
+
if (typeof date !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(date) ||
|
|
42
|
+
!Number.isFinite(Date.parse(`${date}T00:00:00Z`)) || new Date(`${date}T00:00:00Z`).toISOString().slice(0, 10) !== date) {
|
|
43
|
+
logCall('ops_seats', false, 0);
|
|
44
|
+
return { error: 'KST 날짜는 YYYY-MM-DD 여야 한다' };
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const board = await buildSeatsBoard(date, (deps.seatsSources ?? liveSeatsSources)());
|
|
48
|
+
const seats = board.seats.map(({ seat, role, now, landed, blocked, pendingDecisions }) =>
|
|
49
|
+
({ seat, role, now, landed, red: blocked, pendingDecisions }));
|
|
50
|
+
const nulls = seats.reduce((n, row) => n + [row.now, row.landed, row.red, row.pendingDecisions].filter(v => v === null).length, 0);
|
|
51
|
+
logCall('ops_seats', true, nulls);
|
|
52
|
+
return { note: 'null = 못 읽음(없다가 아님); now는 오늘 기록이 없어도 null일 수 있다.', date: board.date, seats };
|
|
53
|
+
} catch {
|
|
54
|
+
logCall('ops_seats', false, 1);
|
|
55
|
+
return { error: '자리 현황을 읽지 못했다', seats: null };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Only the five requested fields leave the decision ledger. Undated entries follow dated entries. */
|
|
60
|
+
export function dispatchDecisionsPending(context?: ToolRuntimeContext, deps: OpsFactsDeps = {}) {
|
|
61
|
+
if (!ownerVerified(context)) {
|
|
62
|
+
logCall('decisions_pending', false, 0);
|
|
63
|
+
return { error: '오너 확인이 필요한 조회다' };
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const entries = (deps.listDecisions ?? ((filters) => new DecisionLedger().list(filters)))({ status: 'open' });
|
|
67
|
+
const items = entries.filter(e => e.status === 'open')
|
|
68
|
+
.sort((a, b) => (a.dueAt ?? '\uffff').localeCompare(b.dueAt ?? '\uffff') || a.id.localeCompare(b.id))
|
|
69
|
+
.map(({ id, title, category, dueAt, raisedBy }) => ({ id, title, category, dueAt: dueAt ?? null, raisedBy: { track: raisedBy.track ?? null } }));
|
|
70
|
+
logCall('decisions_pending', true, items.reduce((n, item) => n + Number(item.dueAt === null) + Number(item.raisedBy.track === null), 0));
|
|
71
|
+
return { items };
|
|
72
|
+
} catch {
|
|
73
|
+
logCall('decisions_pending', false, 1);
|
|
74
|
+
return { error: '결정 원장을 읽지 못했다', items: null };
|
|
75
|
+
}
|
|
76
|
+
}
|