elanous 0.2.6 → 0.2.8
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/.env.example +10 -7
- package/README.md +4 -0
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1377-1efeffbbac0963ff.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1470.98a6ab61ccb4131c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{3995-bee512a7063036ce.js → 2375-cb6debea56cce4fb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2977-e38442e86289f1d3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2982-2a2df543be3d2e5a.js +25 -0
- package/apps/pwa/out/_next/static/chunks/{8d0d9faf-97887ff99713b84e.js → 2ea96958-e8c7e112de50b067.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{5329.8b7e4bca684b6eec.js → 3403.4bc97b4066945a94.js} +2 -2
- package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +1 -0
- package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{7633.0654fe6ea259c1ee.js → 4323.c5dfb6f503304775.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/4836-3084c58c1725a40b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{2984-977cfa339fcd65a5.js → 5080-1613752891fcadb4.js} +3 -3
- package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{763-7d0e1913b5074e98.js → 5791-bb82743fa6299562.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{4240-7153881b0b377306.js → 6062-73e68343a9814f6a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/6090-85a3f05f954e5697.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6284-69db415193d5f8bd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7032-f4ef698518fba61c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{91ff6dda-3b33d42d20657429.js → 8046cadf-211ca3c0e00f2b72.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/8067-d976b441a104ab74.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8519-5632377cce994f86.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8894-7317998f6cb97d8c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9454-a3e0953307216c58.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9740-8f5ff96c522b5f55.js +1 -0
- package/apps/pwa/out/_next/static/chunks/ae787e4c-f2998de7f1ba72a4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-9aa47b0bc6d570ff.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f4d2ecbbae6088d6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-2053e49930c82748.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-d4b6df905f185055.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-a70ba27c33003dd0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-186fc6952f027acc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-18166f885d831d3e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-1c9d6cfe68b121c2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-cbbe03c0da7b081a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-c108ff1fdb665c97.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/page-147a328753d983e2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-799395a9df7d95aa.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-6a8158b71d1fe9d7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-9567560270857531.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-e7ed3d010153d818.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-c0c0d19d51342d3e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-2e002ad8faa43291.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/page-b672e02e4bec8deb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/not-found-1ba4a5d857276334.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-9db2226f38c542fb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-70ee65f5f7a151dc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-0bb8e43203efb23b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-092e1bcf27d751cc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-eb52580c4b75b905.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-7464c1987dd76e6d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-e573437b0a1c728f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-294300625e49e4d3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-085847b088675978.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-9fe9d38c36999eb4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-d8ab4d1489223459.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-6141f67f531d8b6a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-295a02b1026b8255.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-1eb9a0e1e054209d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-82efa177a30f3741.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d0be51cec4f62606.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-2e367075ffdbc062.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-647ed2b66c1d49f1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ba058c56e38f2792.js +1 -0
- package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-6870f30ea99f8346.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{webpack-37b8bf78a3c6eec5.js → webpack-e5bea36f183b25e6.js} +1 -1
- package/apps/pwa/out/_next/static/css/5ef72a618a8994f3.css +1 -0
- package/apps/pwa/out/_next/static/lywc8m_HDzH9MKSDQhyEj/_buildManifest.js +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +22 -22
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +22 -22
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +22 -22
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +21 -21
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +21 -21
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +21 -21
- package/apps/pwa/out/consult/index.html +1 -0
- package/apps/pwa/out/consult/index.txt +32 -0
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +22 -22
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +22 -22
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +22 -22
- package/apps/pwa/out/exec/index.html +1 -0
- package/apps/pwa/out/exec/index.txt +32 -0
- package/apps/pwa/out/field/index.html +1 -0
- package/apps/pwa/out/field/index.txt +32 -0
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +21 -21
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +22 -22
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +22 -22
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +21 -21
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +21 -21
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +22 -22
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +22 -22
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +22 -22
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +22 -22
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +21 -21
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +22 -22
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +22 -22
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +22 -22
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +22 -22
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +22 -22
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +21 -21
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +22 -22
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +22 -22
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +22 -22
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +22 -22
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +22 -22
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +22 -22
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +22 -22
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +22 -22
- package/bin/elanous.cjs +107 -0
- package/graphs/field/field-feed.yaml +24 -0
- package/graphs/field/recipes.yaml +12 -0
- package/graphs/landing/landing-heal.yaml +3 -1
- package/graphs/landing/recipes.yaml +3 -0
- package/graphs/release/recipes.yaml +10 -0
- package/graphs/release/release-loop.yaml +10 -3
- package/graphs/test-diet/recipes.yaml +9 -0
- package/graphs/test-diet/test-diet.yaml +22 -0
- package/package.json +6 -5
- package/persona-presets/_index.yaml +10 -0
- package/persona-presets/_schema.json +146 -0
- package/persona-presets/dana.yaml +26 -0
- package/persona-presets/haru.yaml +27 -0
- package/persona-presets/kai.yaml +27 -0
- package/persona-presets/leo.yaml +27 -0
- package/persona-presets/mira.yaml +30 -0
- package/persona-presets/nari.yaml +27 -0
- package/persona-presets/sora.yaml +27 -0
- package/persona-presets/yuna.yaml +27 -0
- package/plugins/card-followup/README.md +13 -4
- package/plugins/card-followup/graphs/card-followup.yaml +3 -1
- package/plugins/card-followup/graphs/recipes.yaml +5 -1
- package/plugins/card-followup/graphs/run-step.ts +122 -18
- package/plugins/card-followup/graphs/sales-strategy.ts +173 -0
- package/plugins/card-followup/plugin.json +1 -1
- package/plugins/doc-draft/README.md +13 -0
- package/plugins/doc-draft/examples/brief-sample.json +8 -0
- package/plugins/doc-draft/graphs/doc-draft.yaml +14 -0
- package/plugins/doc-draft/graphs/recipes.yaml +9 -0
- package/plugins/doc-draft/graphs/run-step.ts +113 -0
- package/plugins/doc-draft/plugin.json +24 -0
- package/plugins/geo-check/README.md +3 -1
- package/plugins/geo-check/graphs/run-step.ts +58 -13
- package/plugins/geo-check/plugin.json +1 -1
- package/plugins/job-coach/.codex-plugin/plugin.json +1 -1
- package/plugins/job-coach/README.md +1 -1
- package/plugins/job-coach/graphs/run-step.ts +2 -2
- package/plugins/job-coach/plugin.json +1 -1
- package/plugins/job-coach/plugin.ts +1 -1
- package/scripts/affinity/endcard.sh +3 -2
- package/scripts/affinity/reframe.sh +3 -1
- package/scripts/ci-public-export-leak-gate.ts +48 -0
- package/scripts/codex-quota-alert.ts +122 -258
- package/scripts/coord-tracks.json +4 -4
- package/scripts/field-feed/deliver-node.ts +57 -0
- package/scripts/field-feed/feed-node.ts +67 -0
- package/scripts/field-feed/lib.ts +164 -0
- package/scripts/field-feed/reel-node.ts +24 -0
- package/scripts/field-feed/start.ts +62 -0
- package/scripts/fixtures/vflow-sample.ndjson +6 -0
- package/scripts/install.sh +109 -21
- package/scripts/landing-heal/clean-checkout.ts +114 -0
- package/scripts/landing-heal/run-mission.ts +38 -3
- package/scripts/lib/pwa-scenarios/scenarios.ts +2 -1
- package/scripts/persona-presets/lint.ts +79 -0
- package/scripts/public-export.ts +8 -4
- package/scripts/release-loop/baseline-from-logs.ts +92 -0
- package/scripts/release-loop/checklist-gate-node.ts +2 -2
- package/scripts/release-loop/env-known-failures.ts +31 -0
- package/scripts/release-loop/gate-node.ts +108 -22
- package/scripts/release-loop/known-issues-node.ts +159 -0
- package/scripts/release-loop/npm-publish-node.ts +124 -0
- package/scripts/release-loop/ops-upgrade-node.ts +95 -0
- package/scripts/release-loop/publish-node.ts +23 -1
- package/scripts/release-loop/release-readiness.ts +58 -0
- package/scripts/release-loop/shard-plan.ts +17 -0
- package/scripts/release-loop/unattended-release.ts +93 -0
- package/scripts/storyboard/storyboard.ts +167 -0
- package/scripts/test-diet/lib.ts +88 -0
- package/scripts/test-diet/node.ts +102 -0
- package/scripts/ux/measure-first-text.ts +102 -0
- package/scripts/vflow-collect.ts +20 -7
- package/scripts/vflow-search.ts +73 -0
- package/scripts/video-free-line-test-walker.ts +36 -0
- package/scripts/video-pipeline.ts +2 -2
- package/skills/explainer-video/SKILL.md +12 -9
- package/skills/explainer-video/engine/build.mjs +1 -1
- package/skills/explainer-video/engine/reel.mjs +138 -31
- package/skills/explainer-video/engine/reel.sh +2 -1
- package/skills/explainer-video/examples/elanous-0.2.5/script.json +1 -1
- package/skills/grill-me/SKILL.md +1 -1
- package/skills/omni-digest/docs/diagrams/noncode-terminal-promotion-path-2026-08-13.mmd +1 -1
- package/src/a2a/server.ts +2 -0
- package/src/acp/auth-methods.ts +30 -0
- package/src/acp/elanous-aware-peers.ts +30 -0
- package/src/acp/server.ts +96 -20
- package/src/agent/monad-agent-turn.ts +25 -6
- package/src/agent/shared-app-tools.ts +5 -3
- package/src/agent-substrate/review-intent.ts +6 -2
- package/src/agent-substrate/shard-boundary-intent.ts +70 -0
- package/src/auth/github-app-token.ts +4 -1
- package/src/autopilot/track-agent-shadow.ts +151 -0
- package/src/autopilot/track-agent.ts +23 -3
- package/src/boot/acp-server-help.ts +16 -0
- package/src/boot/daemon-prompt-turn.ts +31 -10
- package/src/boot/daemon-tools/index.ts +7 -1
- package/src/budget/codex-credit-pace.ts +84 -0
- package/src/budget/codex-credit-plan.ts +154 -0
- package/src/budget/codex-reset-credit-state.ts +2 -0
- package/src/channel-import/detect.ts +191 -0
- package/src/channel-import/run.ts +111 -0
- package/src/chat/chat-cli.ts +4 -1
- package/src/chat/short-question.ts +40 -0
- package/src/cli/agent-cli.ts +24 -15
- package/src/cli/ask-bare.ts +42 -0
- package/src/cli/cli-user-error.ts +25 -0
- package/src/cli/codex-credit-plan-view.ts +25 -0
- package/src/cli/config-test-sync.ts +4 -0
- package/src/cli/decisions-cli.ts +13 -3
- package/src/cli/dependence-gauge.ts +237 -0
- package/src/cli/doctor-bundle.ts +128 -0
- package/src/cli/doctor-cli.ts +41 -1
- package/src/cli/doctor-local-llm.ts +95 -0
- package/src/cli/doctor-modes.ts +48 -0
- package/src/cli/import-cli.ts +60 -0
- package/src/cli/intake-cli.ts +12 -2
- package/src/cli/intake-source-cli.ts +58 -0
- package/src/cli/machine-cli.ts +23 -0
- package/src/cli/mcp-login.ts +13 -4
- package/src/cli/msg-cli.ts +137 -0
- package/src/cli/persona-cli.ts +116 -0
- package/src/cli/plugin-cli.ts +25 -0
- package/src/cli/pr-cli.ts +36 -0
- package/src/cli/provider-cli.ts +5 -0
- package/src/cli/release-cli.ts +50 -8
- package/src/cli/release-run-if-ready.ts +48 -0
- package/src/cli/setup-cli.ts +27 -6
- package/src/cli/setup-recommend.ts +8 -0
- package/src/cli/skills-cli.ts +173 -0
- package/src/cli/start-cli.ts +13 -4
- package/src/cli/usage-cli.ts +11 -1
- package/src/codex/setup.ts +37 -16
- package/src/dashboard/index.ts +18 -2
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +2 -2
- package/src/dashboard/slash-runtime/help-from-registry.ts +4 -2
- package/src/decisions/decision-cards.ts +291 -0
- package/src/decisions/decision-ledger.ts +5 -2
- package/src/decisions/discord-decision-cards.ts +133 -0
- package/src/decisions/telegram-decision-cards.ts +100 -0
- package/src/directives/version-at.ts +77 -0
- package/src/discord.ts +73 -6
- package/src/domains/core-tools.ts +37 -14
- package/src/domains/outbound-alert.ts +94 -12
- package/src/domains/repeated-failure.ts +1 -1
- package/src/error-report/report.ts +200 -0
- package/src/exec-requests/planner.ts +146 -0
- package/src/exec-requests/runner.ts +378 -0
- package/src/exec-requests/store.ts +78 -0
- package/src/field/field-feed-auto.ts +48 -0
- package/src/field/field-media.ts +8 -3
- package/src/field/field-reel.ts +203 -0
- package/src/grok/credential.ts +7 -5
- package/src/harness/aside-run-status.ts +33 -0
- package/src/harness/browser-verify.ts +48 -17
- package/src/harness/harness-cli-command.ts +25 -3
- package/src/harness/image-load-state.ts +21 -0
- package/src/harness/process-garbage.ts +45 -0
- package/src/hooks/error-report.ts +210 -0
- package/src/hooks/receiver.ts +41 -1
- package/src/import/apply.ts +128 -0
- package/src/import/plan.ts +197 -0
- package/src/index.ts +168 -21
- package/src/input-core/text-entry.ts +13 -5
- package/src/instance/leader.ts +13 -1
- package/src/intake-plane/collect-github.ts +5 -3
- package/src/intake-plane/collect-telegram-saved.ts +15 -3
- package/src/intake-plane/discord-seat-work.ts +56 -0
- package/src/intake-plane/intake-sources.ts +202 -0
- package/src/intake-plane/items.ts +28 -12
- package/src/intake-plane/seat-answer.ts +96 -0
- package/src/intake-plane/seat-doc-route.ts +221 -0
- package/src/intake-plane/submit-intake-work.ts +9 -1
- package/src/intake-plane/telegram-seat-work.ts +72 -0
- package/src/llm/model-defaults.ts +24 -1
- package/src/llm.ts +5 -4
- package/src/machines/machine-ledger.ts +127 -0
- package/src/market/pack-internal-refs.ts +28 -0
- package/src/market/publish.ts +31 -4
- package/src/maturity/feature-maturity.ts +65 -0
- package/src/model-tier/turn-tier.ts +24 -0
- package/src/msg/msg-store.ts +149 -0
- package/src/native-tool-catalog.ts +2 -1
- package/src/nexus/api/chat-fast-path-config.ts +28 -0
- package/src/nexus/api/consult-requests.ts +128 -0
- package/src/nexus/api/discord-trigger-bot.ts +3 -0
- package/src/nexus/api/error-reports.ts +108 -0
- package/src/nexus/api/exec-requests.ts +51 -0
- package/src/nexus/api/field-uploads.ts +40 -3
- package/src/nexus/api/graph-approvals-feed.ts +140 -0
- package/src/nexus/api/graph-approvals.ts +71 -4
- package/src/nexus/api/http-server.ts +82 -8
- package/src/nexus/api/outbound-report.ts +7 -0
- package/src/nexus/api/running-runs-background.ts +68 -0
- package/src/nexus/api/running-runs-child.ts +6 -0
- package/src/nexus/api/seat-requests.ts +254 -0
- package/src/nexus/api/share-captures.ts +91 -0
- package/src/nexus/api/skill-problems.ts +56 -0
- package/src/nexus/api/terminals.ts +24 -8
- package/src/nexus/api/trace-run-scan.ts +37 -0
- package/src/nexus/api/trace.ts +19 -3
- package/src/nexus/index.ts +28 -1
- package/src/nexus/setup-status.ts +11 -7
- package/src/notify/llm-budget-alert.ts +16 -10
- package/src/oauth/browser-availability.ts +14 -0
- package/src/oauth/codex-account-rotation.ts +26 -4
- package/src/oauth/codex-account-store.ts +20 -2
- package/src/oauth/codex-browser-login.ts +173 -0
- package/src/oauth/codex-quota-policy.ts +16 -0
- package/src/oauth/codex.ts +4 -1
- package/src/oauth/fallback-chain.ts +5 -5
- package/src/oauth/store.ts +42 -5
- package/src/onboarding/continue-here.ts +34 -0
- package/src/onboarding/entry-hints.ts +8 -0
- package/src/onboarding/full-screen-io.ts +35 -3
- package/src/onboarding/help/skills.md +1 -1
- package/src/onboarding/non-interactive.ts +1 -0
- package/src/onboarding/subscription-hint.ts +27 -0
- package/src/onboarding/web-first.ts +90 -0
- package/src/onboarding.ts +287 -26
- package/src/persona/presets.ts +105 -0
- package/src/plugins/maker/node-install.ts +92 -0
- package/src/plugins/maker/node-maker.ts +139 -0
- package/src/plugins/maker/plugin-maker.ts +19 -7
- package/src/policy/llm-policy.ts +268 -0
- package/src/prompt-library/session-guidance.ts +7 -1
- package/src/prompt-library/universal-preamble.ts +2 -2
- package/src/relay/relay-cli.ts +20 -0
- package/src/relay/relay-server.ts +220 -0
- package/src/release-loop/checklist.ts +37 -13
- package/src/self-dev/decompose.ts +1 -0
- package/src/self-dev/dev-pipeline.ts +6 -3
- package/src/self-dev/orchestrate.ts +18 -11
- package/src/self-dev/run-record-mirror.ts +44 -0
- package/src/self-dev/run-store.ts +12 -3
- package/src/self-implement/harness-policy-gates.ts +4 -1
- package/src/self-implement/report-origin.ts +12 -1
- package/src/session/chat.ts +17 -5
- package/src/shell-env-bootstrap.ts +44 -7
- package/src/skills/connect-claude-plugin.ts +142 -0
- package/src/skills/connect.ts +70 -0
- package/src/skills/index.ts +29 -1
- package/src/skills/repair.ts +124 -0
- package/src/skills/runner.ts +11 -2
- package/src/skills/shared-agent-skill-roots.ts +25 -0
- package/src/skills/trigger-extract.ts +1 -1
- package/src/steward/failure-streak.ts +78 -0
- package/src/steward/steward-cards.ts +24 -0
- package/src/steward/triage-budget.ts +127 -0
- package/src/steward/triage-plan.ts +75 -0
- package/src/steward/triage.ts +244 -42
- package/src/task-cards/card-store.ts +1 -1
- package/src/task-orchestrator/surfaces/pod-command-job.ts +16 -8
- package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +11 -5
- package/src/task-orchestrator/surfaces/pod-ledger-events.ts +68 -0
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +78 -4
- package/src/telegram-commands.ts +13 -0
- package/src/telegram.ts +72 -2
- package/src/tool-runtime/elanous-skills-list-runtime.ts +3 -3
- package/src/tool-runtime/self-cognition-runtimes.ts +160 -2
- package/src/user-config.ts +189 -15
- package/src/version/packed-revision.json +1 -1
- package/src/video-pipeline/recipes/character.ts +42 -5
- package/src/video-pipeline/recipes/ffmpeg.ts +1 -1
- package/src/video-pipeline/recipes/free-line.ts +16 -3
- package/src/video-pipeline/recipes/remote.ts +7 -1
- package/src/video-pipeline/vflow-reference.ts +109 -0
- package/apps/pwa/out/_next/static/chunks/1169-696263ac7af8c4d4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1883.c91a2648af4842fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1947-9d64872eeb9cfdec.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2808-dc06482994b40f70.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3165-95515ca5f299a120.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3190-e9282753e7ebde85.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4259-99614149577e77cb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4283-329eabeaf233e6cd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4328-cb5058b1acc2de5e.js +0 -25
- package/apps/pwa/out/_next/static/chunks/4801-ecee6ef62b349b18.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5110-8058791dd25a2e14.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5330-c41e9670f594c38f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5423-e95fd125d1794349.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5478-b5eae07dc79ea619.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5787-96ee6642b9a661e5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6448-79d72368589f98bc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6522-9866c002b5531836.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6729-b4a558b7aa891e0c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6969-cb03f9410b7d2e03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7007-3b9861091bb8dd21.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7930-8dc7e20fc6fc19ca.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8071-187edbc22a86741f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8198-d4101ad70cb9a579.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8250.4a2c5553a07cdf47.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8293-123b91e7b0330dc0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8296-d6d5d87dfa911d41.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8305-603963c33ee8eec3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8612-4a0066f20951b8fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9181-cf4e1b61c4783f92.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9262-26c2ecbd5a67e4a2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9328-45abdebce4fbecb7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9355-dbc67205adf8745f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9763.255b90dd69875689.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9933-66d31ec25c0231af.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-bd06fb55f4da0381.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-4e586142da455a25.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-889ad5cddcf45b0d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-47ba401d56572c27.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-0b54f9c83458e038.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-4fcc296ef6c8ed90.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-fe9fda494a87f4e5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-f9642e25ce129590.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-e1063e8ba5f3e2e7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-9d0df30027d8aa9d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-bd06fb55f4da0381.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-d97adf2c9455b8d0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-236b80d202110362.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-21a30f35f1a276ad.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-b1a5be357c4fa25d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-1705a60303f7f573.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-76b337c11ad0e237.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-7c0f85fbb56f64c6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-65ca16b20617b44d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-e7f9b8fc29411c1d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-92ea6213d1aeec04.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a913a005aacb5476.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-2d78804a40dfdef6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-08a99148e780ac00.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-deb674e7ed36f815.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-49f8de0534d192d7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-bd06fb55f4da0381.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-ba162a28d2176e18.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-1ff1a37b550ab41a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-ccaa698f56c0fbdb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-53b944082de1d7db.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-9b70adf1552789bf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-30d482aa63241fe8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-aecf786f54651d8a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-6ab9b62c7b6e08f2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-08949735455e0c9a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-a1479005234a46c2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3890e90bd232732.js +0 -1
- package/apps/pwa/out/_next/static/chunks/beb5cf10-9adcc10b83930090.js +0 -1
- package/apps/pwa/out/_next/static/chunks/framework-d68d41c88e05c801.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-11c63971b7a9593b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-d09452353bd0d9cc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_app-184d727cebeca767.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_error-21bbbbac99b720b1.js +0 -1
- package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +0 -1
- package/apps/pwa/out/_next/static/qMI0jnlZaBjjm4aIDYNGM/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{qMI0jnlZaBjjm4aIDYNGM → lywc8m_HDzH9MKSDQhyEj}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// DEC-TG(대표 10-02 07:1x) — «밖에 있어도 주요 결정을 텔레그램·디스코드로 다 끝낸다».
|
|
2
|
+
// `elanous decisions raise` 로 올라온 결정 → 대표 DM 카드(SCQA 요약 ⊕ 선택지 버튼 ⊕ 메모 달기) → 누르면 CLI `decide` 와 같은
|
|
3
|
+
// 원장 메서드로 기록 → 올린 자리에 회신 → 카드는 «결정됨: A · 시각»으로 바뀐다.
|
|
4
|
+
// ⛔ 보안: 버튼은 대표 id 만 받는다(그 밖은 무시 ⊕ 관측). 되돌릴 수 없는 범주(결제·외부 게시·보안·비가역)는 «되돌릴 수 없음» ⊕ 두 번 확인.
|
|
5
|
+
// 플랫폼(텔레그램·디스코드)은 `CardTransport` 하나만 구현한다 — 판단은 전부 여기 있다.
|
|
6
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { debug } from '../debug/log.js';
|
|
9
|
+
import { DecisionLedger, type DecisionEntry } from './decision-ledger.js';
|
|
10
|
+
|
|
11
|
+
export type CardPlatform = 'telegram' | 'discord';
|
|
12
|
+
export interface CardButton { label: string; data: string }
|
|
13
|
+
export interface CardView { text: string; buttons: CardButton[][] }
|
|
14
|
+
export interface CardRef { chat: string; message: string }
|
|
15
|
+
|
|
16
|
+
/** What a platform must provide. Every method is best-effort; failures are observed, never thrown to the poller. */
|
|
17
|
+
export interface CardTransport {
|
|
18
|
+
platform: CardPlatform;
|
|
19
|
+
/** Owner DM destinations (Telegram chat id = user id for a DM; Discord DM channel id). */
|
|
20
|
+
ownerChats(): Promise<string[]>;
|
|
21
|
+
send(chat: string, view: CardView): Promise<CardRef | null>;
|
|
22
|
+
edit(ref: CardRef, view: CardView): Promise<void>;
|
|
23
|
+
/** Plain text (reminders, list replies). */
|
|
24
|
+
notify(chat: string, text: string): Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const IRREVERSIBLE = new Set(['money', 'publish', 'security', 'irreversible', 'secret']);
|
|
28
|
+
export const DECISION_DATA_PREFIX = 'dec:';
|
|
29
|
+
const REMIND_BEFORE_MS = 2 * 3600_000;
|
|
30
|
+
|
|
31
|
+
export function isIrreversible(entry: Pick<DecisionEntry, 'category'>): boolean {
|
|
32
|
+
return IRREVERSIBLE.has(entry.category);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const kst = (at: string) => new Intl.DateTimeFormat('ko-KR', { timeZone: 'Asia/Seoul', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false }).format(new Date(at));
|
|
36
|
+
|
|
37
|
+
/** The card body: title, SCQA in four short lines, options with consequences, recommendation, deadline. */
|
|
38
|
+
export function renderCardText(e: DecisionEntry, extra: { note?: string; confirm?: string } = {}): string {
|
|
39
|
+
const lines = [
|
|
40
|
+
`🗳 결정 요청 ${e.id}${isIrreversible(e) ? ' · ⚠️ 되돌릴 수 없음' : ''}`,
|
|
41
|
+
`«${e.title}» · 올린 자리: ${e.raisedBy.agent}${e.raisedBy.track ? `(${e.raisedBy.track})` : ''}`,
|
|
42
|
+
'',
|
|
43
|
+
`상황: ${e.scqa.s}`,
|
|
44
|
+
`문제: ${e.scqa.c}`,
|
|
45
|
+
...(e.scqa.q ? [`질문: ${e.scqa.q}`] : []),
|
|
46
|
+
...(e.scqa.a ? [`제안: ${e.scqa.a}`] : []),
|
|
47
|
+
'',
|
|
48
|
+
...e.options.map((o) => `${o.key.toUpperCase()}) ${o.label} — ${o.consequence}`),
|
|
49
|
+
'skipped' in e.recommendation ? `권고 없음: ${e.recommendation.reason}` : `권고: ${e.recommendation.option.toUpperCase()} — ${e.recommendation.why}`,
|
|
50
|
+
...(e.dueAt ? [`기한: ${kst(e.dueAt)}`] : []),
|
|
51
|
+
...(extra.note ? ['', `📝 메모: ${extra.note}`] : []),
|
|
52
|
+
];
|
|
53
|
+
if (extra.confirm) {
|
|
54
|
+
const option = e.options.find((o) => o.key === extra.confirm);
|
|
55
|
+
lines.push('', `⚠️ ${extra.confirm.toUpperCase()}) ${option?.label ?? ''} — 되돌릴 수 없습니다. 정말 이것으로 정할까요?`);
|
|
56
|
+
}
|
|
57
|
+
return lines.join('\n');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function renderCard(e: DecisionEntry, extra: { note?: string; confirm?: string } = {}): CardView {
|
|
61
|
+
const text = renderCardText(e, extra);
|
|
62
|
+
if (e.status === 'decided') {
|
|
63
|
+
const option = e.options.find((o) => o.key === e.choice);
|
|
64
|
+
return { text: `${text}\n\n✅ 결정됨: ${e.choice?.toUpperCase() ?? '?'}) ${option?.label ?? ''} · ${e.decidedAt ? kst(e.decidedAt) : ''}${e.note ? `\n📝 ${e.note}` : ''}`, buttons: [] };
|
|
65
|
+
}
|
|
66
|
+
if (e.status === 'withdrawn') return { text: `${text}\n\n↩️ 철회됨${e.withdrawReason ? `: ${e.withdrawReason}` : ''}`, buttons: [] };
|
|
67
|
+
if (extra.confirm) {
|
|
68
|
+
return { text, buttons: [[
|
|
69
|
+
{ label: `✅ 예, ${extra.confirm.toUpperCase()} 로 정한다`, data: `${DECISION_DATA_PREFIX}${e.id}:${extra.confirm}:ok` },
|
|
70
|
+
{ label: '취소', data: `${DECISION_DATA_PREFIX}${e.id}:-` },
|
|
71
|
+
]] };
|
|
72
|
+
}
|
|
73
|
+
const choices = e.options.map((o) => ({ label: Array.from(`${o.key.toUpperCase()}) ${o.label}`).slice(0, 60).join(''), data: `${DECISION_DATA_PREFIX}${e.id}:${o.key}` }));
|
|
74
|
+
const rows: CardButton[][] = [];
|
|
75
|
+
for (let i = 0; i < choices.length; i += 2) rows.push(choices.slice(i, i + 2));
|
|
76
|
+
rows.push([{ label: '📝 메모 달기', data: `${DECISION_DATA_PREFIX}${e.id}:memo` }]);
|
|
77
|
+
return { text, buttons: rows };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type ParsedTap = { id: string; action: 'choose'; key: string; confirmed: boolean } | { id: string; action: 'memo' } | { id: string; action: 'cancel' };
|
|
81
|
+
|
|
82
|
+
export function parseTap(data: string): ParsedTap | null {
|
|
83
|
+
const m = /^dec:(D-\d{8}-\d+):([a-z]|memo|-)(:ok)?$/.exec(data);
|
|
84
|
+
if (!m) return null;
|
|
85
|
+
if (m[2] === 'memo') return { id: m[1]!, action: 'memo' };
|
|
86
|
+
if (m[2] === '-') return { id: m[1]!, action: 'cancel' };
|
|
87
|
+
return { id: m[1]!, action: 'choose', key: m[2]!, confirmed: Boolean(m[3]) };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface CardState {
|
|
91
|
+
/** First start of this platform's service — decisions raised before it are not pushed (no burst of old items at
|
|
92
|
+
* deploy); they stay reachable through `/decisions`. */
|
|
93
|
+
since?: string;
|
|
94
|
+
cards: Record<string, { refs: CardRef[]; closed?: boolean; remindedAt?: string; note?: string }>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface DecisionCardServiceOptions {
|
|
98
|
+
transport: CardTransport;
|
|
99
|
+
/** Owner user ids for this platform — taps from anyone else are refused and observed. */
|
|
100
|
+
ownerIds: readonly string[];
|
|
101
|
+
ledger?: DecisionLedger;
|
|
102
|
+
/** Where this platform keeps which card it sent (one file per platform — the two bots may run in different processes). */
|
|
103
|
+
statePath?: string;
|
|
104
|
+
now?: () => Date;
|
|
105
|
+
/** Tell the seat that raised it. Default: none (observed only). */
|
|
106
|
+
replyToRaiser?: (entry: DecisionEntry, via: CardPlatform) => Promise<void>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export type TapOutcome =
|
|
110
|
+
| { kind: 'refused' } | { kind: 'ignored' } | { kind: 'unknown' }
|
|
111
|
+
| { kind: 'confirm'; view: CardView; toast: string }
|
|
112
|
+
| { kind: 'decided'; view: CardView; toast: string }
|
|
113
|
+
| { kind: 'cancelled'; view: CardView; toast: string }
|
|
114
|
+
| { kind: 'memo-requested'; toast: string }
|
|
115
|
+
| { kind: 'closed'; view: CardView; toast: string };
|
|
116
|
+
|
|
117
|
+
/** One per platform bot. `tick()` sends new cards, closes decided ones, sends reminders; `tap()` handles a button. */
|
|
118
|
+
export class DecisionCardService {
|
|
119
|
+
private readonly ledger: DecisionLedger;
|
|
120
|
+
private readonly statePath: string;
|
|
121
|
+
private readonly now: () => Date;
|
|
122
|
+
constructor(private readonly opts: DecisionCardServiceOptions) {
|
|
123
|
+
this.ledger = opts.ledger ?? new DecisionLedger();
|
|
124
|
+
this.statePath = opts.statePath ?? join(this.ledger.path, '..', `cards-${opts.transport.platform}.json`);
|
|
125
|
+
this.now = opts.now ?? (() => new Date());
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private load(): CardState {
|
|
129
|
+
try { return JSON.parse(readFileSync(this.statePath, 'utf8')) as CardState; } catch { return { cards: {} }; }
|
|
130
|
+
}
|
|
131
|
+
private save(state: CardState): void {
|
|
132
|
+
mkdirSync(join(this.statePath, '..'), { recursive: true });
|
|
133
|
+
const tmp = `${this.statePath}.${process.pid}.tmp`;
|
|
134
|
+
writeFileSync(tmp, JSON.stringify(state, null, 2), { mode: 0o600 });
|
|
135
|
+
renameSync(tmp, this.statePath);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
isOwner(userId: string): boolean { return this.opts.ownerIds.includes(userId); }
|
|
139
|
+
|
|
140
|
+
/** Send cards for new open decisions · close cards decided elsewhere · remind two hours before a deadline. */
|
|
141
|
+
async tick(): Promise<{ sent: number; closed: number; reminded: number }> {
|
|
142
|
+
const platform = this.opts.transport.platform;
|
|
143
|
+
const state = this.load();
|
|
144
|
+
let sent = 0; let closed = 0; let reminded = 0;
|
|
145
|
+
let all: DecisionEntry[];
|
|
146
|
+
try { all = this.ledger.list({ status: 'all' }); } catch (error) {
|
|
147
|
+
debug.log('decisions.telegram', 'ledger-unreadable', { platform, reason: error instanceof Error ? error.message.slice(0, 80) : 'unknown' }, { level: 'warn' });
|
|
148
|
+
return { sent, closed, reminded };
|
|
149
|
+
}
|
|
150
|
+
let dirty = false;
|
|
151
|
+
if (!state.since) { state.since = this.now().toISOString(); dirty = true; debug.log('decisions.telegram', 'baseline', { platform, since: state.since }); }
|
|
152
|
+
const chats = await this.opts.transport.ownerChats().catch(() => [] as string[]);
|
|
153
|
+
for (const e of all) {
|
|
154
|
+
const card = state.cards[e.id];
|
|
155
|
+
if (e.status === 'open' && !card && e.options.length >= 2 && e.raisedAt !== undefined && e.raisedAt >= state.since) {
|
|
156
|
+
const refs: CardRef[] = [];
|
|
157
|
+
for (const chat of chats) {
|
|
158
|
+
const ref = await this.opts.transport.send(chat, renderCard(e)).catch(() => null);
|
|
159
|
+
if (ref) refs.push(ref);
|
|
160
|
+
}
|
|
161
|
+
if (refs.length) { state.cards[e.id] = { refs }; sent++; }
|
|
162
|
+
debug.log('decisions.telegram', 'card-sent', { platform, id: e.id, category: e.category, chats: chats.length, delivered: refs.length });
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (card && !card.closed && e.status !== 'open') {
|
|
166
|
+
for (const ref of card.refs) await this.opts.transport.edit(ref, renderCard(e)).catch(() => undefined);
|
|
167
|
+
card.closed = true; closed++;
|
|
168
|
+
debug.log('decisions.telegram', 'card-closed', { platform, id: e.id, status: e.status });
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (e.status === 'open' && card && e.dueAt && !card.remindedAt && this.now().getTime() >= Date.parse(e.dueAt) - REMIND_BEFORE_MS) {
|
|
172
|
+
for (const chat of chats) await this.opts.transport.notify(chat, `⏰ 기한 2시간 전 — ${e.id} «${e.title}» (기한 ${kst(e.dueAt)}) · 카드에서 정하거나 /decisions`).catch(() => undefined);
|
|
173
|
+
card.remindedAt = this.now().toISOString(); reminded++;
|
|
174
|
+
debug.log('decisions.telegram', 'reminded', { platform, id: e.id });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (dirty || sent || closed || reminded) this.save(state);
|
|
178
|
+
return { sent, closed, reminded };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** A button tap. Never throws. */
|
|
182
|
+
async tap(userId: string, data: string): Promise<TapOutcome> {
|
|
183
|
+
const platform = this.opts.transport.platform;
|
|
184
|
+
const parsed = parseTap(data);
|
|
185
|
+
if (!parsed) return { kind: 'ignored' };
|
|
186
|
+
if (!this.isOwner(userId)) {
|
|
187
|
+
debug.log('decisions.telegram', 'tap-refused', { platform, id: parsed.id }, { level: 'warn' });
|
|
188
|
+
return { kind: 'refused' };
|
|
189
|
+
}
|
|
190
|
+
let entry: DecisionEntry;
|
|
191
|
+
try { entry = this.ledger.show(parsed.id); } catch { return { kind: 'unknown' }; }
|
|
192
|
+
const state = this.load();
|
|
193
|
+
const card = state.cards[entry.id];
|
|
194
|
+
if (entry.status !== 'open') return { kind: 'closed', view: renderCard(entry), toast: '이미 정해진 결정입니다' };
|
|
195
|
+
if (parsed.action === 'memo') {
|
|
196
|
+
debug.log('decisions.telegram', 'memo-requested', { platform, id: entry.id });
|
|
197
|
+
return { kind: 'memo-requested', toast: '메모를 보내 주세요 — 다음 메시지가 메모가 됩니다' };
|
|
198
|
+
}
|
|
199
|
+
if (parsed.action === 'cancel') return { kind: 'cancelled', view: renderCard(entry, card?.note ? { note: card.note } : {}), toast: '취소했습니다' };
|
|
200
|
+
if (!entry.options.some((o) => o.key === parsed.key)) return { kind: 'unknown' };
|
|
201
|
+
if (isIrreversible(entry) && !parsed.confirmed) {
|
|
202
|
+
debug.log('decisions.telegram', 'confirm-asked', { platform, id: entry.id, key: parsed.key });
|
|
203
|
+
return { kind: 'confirm', view: renderCard(entry, { confirm: parsed.key, ...(card?.note ? { note: card.note } : {}) }), toast: '되돌릴 수 없는 결정 — 한 번 더 확인해 주세요' };
|
|
204
|
+
}
|
|
205
|
+
let decided: DecisionEntry;
|
|
206
|
+
try {
|
|
207
|
+
// Same ledger method the CLI `decide` uses — one record path.
|
|
208
|
+
decided = this.ledger.decide(entry.id, parsed.key, { kind: 'human' }, card?.note);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
debug.log('decisions.telegram', 'decide-failed', { platform, id: entry.id, reason: error instanceof Error ? error.message.slice(0, 80) : 'unknown' }, { level: 'warn' });
|
|
211
|
+
try { const now = this.ledger.show(entry.id); if (now.status !== 'open') return { kind: 'closed', view: renderCard(now), toast: '이미 정해진 결정입니다' }; } catch { /* fall through */ }
|
|
212
|
+
return { kind: 'unknown' };
|
|
213
|
+
}
|
|
214
|
+
const view = renderCard(decided);
|
|
215
|
+
if (card) {
|
|
216
|
+
for (const ref of card.refs) await this.opts.transport.edit(ref, view).catch(() => undefined);
|
|
217
|
+
card.closed = true;
|
|
218
|
+
this.save(state);
|
|
219
|
+
}
|
|
220
|
+
debug.log('decisions.telegram', 'decided', { platform, id: decided.id, choice: decided.choice, category: decided.category, noted: Boolean(decided.note) });
|
|
221
|
+
await this.opts.replyToRaiser?.(decided, platform).catch((error: unknown) => {
|
|
222
|
+
debug.log('decisions.telegram', 'reply-failed', { platform, id: decided.id, reason: error instanceof Error ? error.message.slice(0, 80) : 'unknown' }, { level: 'warn' });
|
|
223
|
+
});
|
|
224
|
+
return { kind: 'decided', view, toast: `결정했습니다: ${decided.choice?.toUpperCase()}` };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Store a memo typed after «메모 달기»; the card shows it and the eventual decision carries it. */
|
|
228
|
+
async setNote(userId: string, id: string, note: string): Promise<CardView | null> {
|
|
229
|
+
if (!this.isOwner(userId)) return null;
|
|
230
|
+
let entry: DecisionEntry;
|
|
231
|
+
try { entry = this.ledger.show(id); } catch { return null; }
|
|
232
|
+
const clean = note.replace(/\s+/g, ' ').trim().slice(0, 300);
|
|
233
|
+
if (!clean || entry.status !== 'open') return null;
|
|
234
|
+
const state = this.load();
|
|
235
|
+
const card = state.cards[id] ?? { refs: [] };
|
|
236
|
+
card.note = clean;
|
|
237
|
+
state.cards[id] = card;
|
|
238
|
+
this.save(state);
|
|
239
|
+
const view = renderCard(entry, { note: clean });
|
|
240
|
+
for (const ref of card.refs) await this.opts.transport.edit(ref, view).catch(() => undefined);
|
|
241
|
+
debug.log('decisions.telegram', 'memo-set', { platform: this.opts.transport.platform, id, chars: clean.length });
|
|
242
|
+
return view;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** `/decisions` — open decisions as one list; each card is sent again so the buttons are at hand. */
|
|
246
|
+
async listOpen(chat: string): Promise<string> {
|
|
247
|
+
let open: DecisionEntry[];
|
|
248
|
+
try { open = this.ledger.list({ status: 'open' }).filter((e) => e.options.length >= 2); } catch { return '결정 원장을 읽지 못했습니다.'; }
|
|
249
|
+
if (!open.length) return '열린 결정이 없습니다.';
|
|
250
|
+
const state = this.load();
|
|
251
|
+
for (const e of open) {
|
|
252
|
+
const ref = await this.opts.transport.send(chat, renderCard(e, state.cards[e.id]?.note ? { note: state.cards[e.id]!.note } : {})).catch(() => null);
|
|
253
|
+
if (ref) { const card = state.cards[e.id] ?? { refs: [] }; card.refs.push(ref); state.cards[e.id] = card; }
|
|
254
|
+
}
|
|
255
|
+
this.save(state);
|
|
256
|
+
debug.log('decisions.telegram', 'listed', { platform: this.opts.transport.platform, open: open.length });
|
|
257
|
+
return `열린 결정 ${open.length}건 — ${open.map((e) => `${e.id}${e.dueAt ? `(기한 ${kst(e.dueAt)})` : ''}`).join(' · ')}`;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Reply line for the seat that raised it. Posted to the coordination PR when `decisions.replyGhPr` = "owner/repo#N". */
|
|
262
|
+
export function raiserReplyText(e: DecisionEntry, via: CardPlatform): string {
|
|
263
|
+
const option = e.options.find((o) => o.key === e.choice);
|
|
264
|
+
const at = e.decidedAt ? new Intl.DateTimeFormat('sv-SE', { timeZone: 'Asia/Seoul', dateStyle: 'short', timeStyle: 'short' }).format(new Date(e.decidedAt)) : '';
|
|
265
|
+
return `**[대표]** ${at} KST → ${e.raisedBy.agent} · 결정 ${e.id} «${e.title}» = ${e.choice?.toUpperCase()}) ${option?.label ?? ''}${e.note ? ` · 메모: ${e.note}` : ''} (${via === 'telegram' ? '텔레그램' : '디스코드'})`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function ghPrReplier(target: string, run: (args: string[], stdin: string) => Promise<number> = runGh): (e: DecisionEntry, via: CardPlatform) => Promise<void> {
|
|
269
|
+
const m = /^([\w.-]+\/[\w.-]+)#(\d+)$/.exec(target.trim());
|
|
270
|
+
return async (e, via) => {
|
|
271
|
+
if (!m) throw new Error('decisions.replyGhPr must be owner/repo#N');
|
|
272
|
+
const code = await run(['pr', 'comment', m[2]!, '--repo', m[1]!, '--body-file', '-'], raiserReplyText(e, via));
|
|
273
|
+
debug.log('decisions.telegram', 'replied', { id: e.id, via, target: 'gh-pr', ok: code === 0 });
|
|
274
|
+
if (code !== 0) throw new Error(`gh exit ${code}`);
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Post as the automation App (not the machine's personal gh login — that would make 대표 a participant and mail them),
|
|
279
|
+
* with proxy variables removed (they break gh on the ops host). TC review #22660. */
|
|
280
|
+
async function runGh(args: string[], stdin: string): Promise<number> {
|
|
281
|
+
const env: Record<string, string> = {};
|
|
282
|
+
for (const [k, v] of Object.entries(process.env)) if (v !== undefined && !/^(https?|all|no)_proxy$/i.test(k)) env[k] = v;
|
|
283
|
+
try {
|
|
284
|
+
const { githubAutomationToken } = await import('../auth/github-app-token.js');
|
|
285
|
+
const token = githubAutomationToken();
|
|
286
|
+
if (token) env.GH_TOKEN = token;
|
|
287
|
+
debug.log('decisions.telegram', 'reply-identity', { app: Boolean(token) });
|
|
288
|
+
} catch { /* falls back to the machine login */ }
|
|
289
|
+
const proc = Bun.spawn(['gh', ...args], { stdin: new Blob([stdin]), stdout: 'ignore', stderr: 'ignore', env });
|
|
290
|
+
return proc.exited;
|
|
291
|
+
}
|
|
@@ -15,11 +15,13 @@ export interface DecisionEntry {
|
|
|
15
15
|
options: DecisionOption[]; recommendation: Recommendation; raisedAt?: string; importedAt?: string;
|
|
16
16
|
raisedBy: { agent: string; track?: DecisionTrack; session?: string }; version?: Versions;
|
|
17
17
|
status: 'open' | 'decided' | 'withdrawn'; refs?: string[];
|
|
18
|
+
/** DEC-TG — optional deadline (UTC). Cards remind the owner two hours before it. */
|
|
19
|
+
dueAt?: string;
|
|
18
20
|
decidedAt?: string; decidedBy?: DecisionActor; choice?: string; note?: string; versionAtDecision?: Versions;
|
|
19
21
|
withdrawnAt?: string; withdrawReason?: string;
|
|
20
22
|
history: Array<{ type: 'raised' | 'options-added' | 'decided' | 'withdrawn'; at?: string; by: string; version?: Versions; choice?: string; reason?: string }>;
|
|
21
23
|
}
|
|
22
|
-
export type RaiseInput = Pick<DecisionEntry, 'title' | 'category' | 'scqa' | 'options' | 'recommendation' | 'raisedBy' | 'refs'> & { raisedAt?: string };
|
|
24
|
+
export type RaiseInput = Pick<DecisionEntry, 'title' | 'category' | 'scqa' | 'options' | 'recommendation' | 'raisedBy' | 'refs' | 'dueAt'> & { raisedAt?: string };
|
|
23
25
|
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 };
|
|
24
26
|
export interface DecisionLedgerOptions extends VersionOptions { stateDir?: string; now?: () => Date; resolveVersion?: (at: string) => Versions }
|
|
25
27
|
const CATEGORIES: readonly string[] = ['secret', 'publish', 'money', 'security', 'scope', 'irreversible', 'other'];
|
|
@@ -73,7 +75,8 @@ function validate(input: RaiseInput, historical = false): RaiseInput {
|
|
|
73
75
|
if ('option' in recommendation && !options.some(o => o.key === recommendation.option)) throw new Error('recommended option not found');
|
|
74
76
|
return { ...input, title, scqa: { s, c, ...(q ? { q } : {}), ...(a ? { a } : {}) }, options, recommendation,
|
|
75
77
|
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')) } : {}) },
|
|
76
|
-
...(input.refs ? { refs: input.refs.map(r => safe(single(r, 'ref'))) } : {})
|
|
78
|
+
...(input.refs ? { refs: input.refs.map(r => safe(single(r, 'ref'))) } : {}),
|
|
79
|
+
...(input.dueAt ? { dueAt: utc(input.dueAt) } : {}) };
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
export class DecisionLedger {
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// DEC-TG — 디스코드 운반(운영 봇 · 대표 DM). 판단은 decision-cards.ts. 버튼 = MESSAGE_COMPONENT · 메모 = 모달.
|
|
2
|
+
import type { DiscordBot } from '../discord.js';
|
|
3
|
+
import type { UserConfig } from '../user-config.js';
|
|
4
|
+
import { debug } from '../debug/log.js';
|
|
5
|
+
import { DECISION_DATA_PREFIX, DecisionCardService, ghPrReplier, type CardTransport, type CardView } from './decision-cards.js';
|
|
6
|
+
|
|
7
|
+
const TICK_MS = 20_000;
|
|
8
|
+
const MEMO_PREFIX = 'decmemo:';
|
|
9
|
+
const EPHEMERAL = 64;
|
|
10
|
+
let current: DecisionCardService | null = null;
|
|
11
|
+
|
|
12
|
+
export function discordDecisionService(): DecisionCardService | null { return current; }
|
|
13
|
+
|
|
14
|
+
export function discordDecisionOwner(cfg: UserConfig): string | null {
|
|
15
|
+
const raw = (cfg.raw?.decisions as { discordOwnerId?: unknown } | undefined)?.discordOwnerId;
|
|
16
|
+
if (typeof raw === 'string' && /^\d{5,}$/.test(raw)) {
|
|
17
|
+
debug.log('decisions.telegram', 'owner-source', { platform: 'discord', source: 'config' });
|
|
18
|
+
return raw;
|
|
19
|
+
}
|
|
20
|
+
debug.log('decisions.telegram', 'owner-source', { platform: 'discord', source: 'default-first', allowlist: cfg.discord.allowedUsers.length }, cfg.discord.allowedUsers.length > 1 ? { level: 'warn' } : undefined);
|
|
21
|
+
return cfg.discord.allowedUsers[0] ?? null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** `/decisions` typed in Discord — owner only, and only in a DM (a guild channel would show the cards to everyone). */
|
|
25
|
+
export async function discordDecisionsCommand(service: DecisionCardService, ctx: { userId: string; channelId: string; isDm: boolean }): Promise<string> {
|
|
26
|
+
if (!service.isOwner(ctx.userId)) return '결정은 소유자만 볼 수 있습니다.';
|
|
27
|
+
if (!ctx.isDm) return '결정은 개인 대화(DM)에서만 볼 수 있습니다.';
|
|
28
|
+
return service.listOpen(ctx.channelId);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Card buttons → Discord action rows (choices primary · memo secondary · confirm success). */
|
|
32
|
+
export function discordComponents(view: CardView): Array<Record<string, unknown>> {
|
|
33
|
+
return view.buttons.map((row) => ({
|
|
34
|
+
type: 1,
|
|
35
|
+
components: row.map((b) => ({
|
|
36
|
+
type: 2,
|
|
37
|
+
style: b.data.endsWith(':ok') ? 3 : b.data.endsWith(':memo') || b.data.endsWith(':-') ? 2 : 1,
|
|
38
|
+
label: Array.from(b.label).slice(0, 80).join(''),
|
|
39
|
+
custom_id: b.data.slice(0, 100),
|
|
40
|
+
})),
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type Bot = Pick<DiscordBot, 'openDmChannel' | 'sendMessageWithComponents' | 'editMessageWithComponents' | 'sendMessage' | 'respondToInteraction'>;
|
|
45
|
+
|
|
46
|
+
export function discordTransport(bot: Bot, owner: string): CardTransport {
|
|
47
|
+
let dm: string | null = null;
|
|
48
|
+
return {
|
|
49
|
+
platform: 'discord',
|
|
50
|
+
ownerChats: async () => {
|
|
51
|
+
dm ??= await bot.openDmChannel(owner);
|
|
52
|
+
return dm ? [dm] : [];
|
|
53
|
+
},
|
|
54
|
+
send: async (chat, view) => {
|
|
55
|
+
const sent = await bot.sendMessageWithComponents(chat, view.text, discordComponents(view));
|
|
56
|
+
return sent ? { chat, message: sent.id } : null;
|
|
57
|
+
},
|
|
58
|
+
edit: async (ref, view) => { await bot.editMessageWithComponents(ref.chat, ref.message, view.text, discordComponents(view)); },
|
|
59
|
+
notify: async (chat, text) => { await bot.sendMessage(chat, text); },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function userOf(raw: Record<string, unknown>): string {
|
|
64
|
+
const member = raw.member as { user?: { id?: string } } | undefined;
|
|
65
|
+
const user = raw.user as { id?: string } | undefined;
|
|
66
|
+
return member?.user?.id ?? user?.id ?? '';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Handle a decision button or memo modal. Returns true when the interaction was ours (so nothing else answers it). */
|
|
70
|
+
export async function handleDiscordDecisionInteraction(bot: Bot, service: DecisionCardService, raw: Record<string, unknown>): Promise<boolean> {
|
|
71
|
+
const data = raw.data as { custom_id?: string; components?: Array<{ components?: Array<{ value?: string }> }> } | undefined;
|
|
72
|
+
const customId = data?.custom_id ?? '';
|
|
73
|
+
const id = String(raw.id ?? '');
|
|
74
|
+
const token = String(raw.token ?? '');
|
|
75
|
+
const reply = (content: string) => bot.respondToInteraction(id, token, { type: 4, data: { content, flags: EPHEMERAL } });
|
|
76
|
+
if (raw.type === 5 && customId.startsWith(MEMO_PREFIX)) {
|
|
77
|
+
const value = data?.components?.[0]?.components?.[0]?.value ?? '';
|
|
78
|
+
const view = await service.setNote(userOf(raw), customId.slice(MEMO_PREFIX.length), value);
|
|
79
|
+
await reply(view ? '메모를 달았습니다 — 카드에서 선택지를 누르면 함께 기록됩니다.' : '메모를 달지 못했습니다(권한 또는 이미 정해진 결정).');
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
if (raw.type !== 3 || !customId.startsWith(DECISION_DATA_PREFIX)) return false;
|
|
83
|
+
const outcome = await service.tap(userOf(raw), customId);
|
|
84
|
+
switch (outcome.kind) {
|
|
85
|
+
case 'refused': await reply('권한이 없습니다.'); return true;
|
|
86
|
+
case 'ignored': case 'unknown': await reply('알 수 없는 결정입니다.'); return true;
|
|
87
|
+
case 'memo-requested': {
|
|
88
|
+
const decisionId = /^dec:([^:]+):memo$/.exec(customId)?.[1] ?? '';
|
|
89
|
+
await bot.respondToInteraction(id, token, { type: 9, data: {
|
|
90
|
+
custom_id: `${MEMO_PREFIX}${decisionId}`.slice(0, 100), title: Array.from(`메모 달기 · ${decisionId}`).slice(0, 45).join(''),
|
|
91
|
+
components: [{ type: 1, components: [{ type: 4, custom_id: 'note', style: 2, label: '메모', max_length: 300, required: true }] }],
|
|
92
|
+
} });
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
default:
|
|
96
|
+
// UPDATE_MESSAGE on the tapped card (decided: the service already edited every card it sent — same content).
|
|
97
|
+
await bot.respondToInteraction(id, token, { type: 7, data: { content: outcome.view.text, components: discordComponents(outcome.view) } });
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface DiscordDecisionWire {
|
|
103
|
+
service: DecisionCardService;
|
|
104
|
+
onInteraction: (raw: Record<string, unknown>) => Promise<boolean>;
|
|
105
|
+
stop: () => void;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function attachDiscordDecisionCards(bot: Bot, cfg: UserConfig, opts: { tickMs?: number } = {}): DiscordDecisionWire | null {
|
|
109
|
+
const owner = discordDecisionOwner(cfg);
|
|
110
|
+
if (!owner) {
|
|
111
|
+
debug.log('decisions.telegram', 'not-attached', { platform: 'discord', reason: 'no-owner' });
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
const replyTarget = (cfg.raw?.decisions as { replyGhPr?: unknown } | undefined)?.replyGhPr;
|
|
115
|
+
const service = new DecisionCardService({
|
|
116
|
+
transport: discordTransport(bot, owner),
|
|
117
|
+
ownerIds: [owner],
|
|
118
|
+
...(typeof replyTarget === 'string' && replyTarget ? { replyToRaiser: ghPrReplier(replyTarget) } : {}),
|
|
119
|
+
});
|
|
120
|
+
current = service;
|
|
121
|
+
const timer = setInterval(() => {
|
|
122
|
+
void service.tick().catch((error: unknown) => {
|
|
123
|
+
debug.log('decisions.telegram', 'tick-failed', { platform: 'discord', reason: error instanceof Error ? error.message.slice(0, 80) : 'unknown' }, { level: 'warn' });
|
|
124
|
+
});
|
|
125
|
+
}, opts.tickMs ?? TICK_MS);
|
|
126
|
+
(timer as { unref?: () => void }).unref?.();
|
|
127
|
+
debug.log('decisions.telegram', 'attached', { platform: 'discord', reply: typeof replyTarget === 'string' && Boolean(replyTarget) });
|
|
128
|
+
return {
|
|
129
|
+
service,
|
|
130
|
+
onInteraction: (raw) => handleDiscordDecisionInteraction(bot, service, raw),
|
|
131
|
+
stop: () => { clearInterval(timer); if (current === service) current = null; },
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// DEC-TG — 텔레그램 운반(운영 봇 DM). 판단은 decision-cards.ts 에 있고 여기는 «보내고·고치고·누름을 넘긴다»만 한다.
|
|
2
|
+
// ⛔ 카드는 «폴링하는» 본 봇만 보낸다(넥서스가 폴링 없는 봇을 따로 세울 수 있다 — 두 번 가면 안 된다).
|
|
3
|
+
import type { TelegramBot, TgCallbackQuery } from '../telegram.js';
|
|
4
|
+
import type { UserConfig } from '../user-config.js';
|
|
5
|
+
import { debug } from '../debug/log.js';
|
|
6
|
+
import { DECISION_DATA_PREFIX, DecisionCardService, ghPrReplier, type CardTransport, type CardView } from './decision-cards.js';
|
|
7
|
+
|
|
8
|
+
const TICK_MS = 20_000;
|
|
9
|
+
let current: DecisionCardService | null = null;
|
|
10
|
+
|
|
11
|
+
/** The running Telegram card service (for the `/decisions` command), or null when none is attached. */
|
|
12
|
+
export function telegramDecisionService(): DecisionCardService | null { return current; }
|
|
13
|
+
|
|
14
|
+
function buttons(view: CardView): Array<Array<{ text: string; data: string }>> {
|
|
15
|
+
return view.buttons.map((row) => row.map((b) => ({ text: b.label, data: b.data })));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Owner = `decisions.telegramOwnerId` when set, else the first allowlisted user (the bot owner). */
|
|
19
|
+
export function telegramDecisionOwner(cfg: UserConfig): string | null {
|
|
20
|
+
const raw = (cfg.raw?.decisions as { telegramOwnerId?: unknown } | undefined)?.telegramOwnerId;
|
|
21
|
+
if (typeof raw === 'number' || (typeof raw === 'string' && /^\d+$/.test(raw))) {
|
|
22
|
+
debug.log('decisions.telegram', 'owner-source', { platform: 'telegram', source: 'config' });
|
|
23
|
+
return String(raw);
|
|
24
|
+
}
|
|
25
|
+
const first = cfg.telegram.allowedUsers[0];
|
|
26
|
+
debug.log('decisions.telegram', 'owner-source', { platform: 'telegram', source: 'default-first', allowlist: cfg.telegram.allowedUsers.length }, cfg.telegram.allowedUsers.length > 1 ? { level: 'warn' } : undefined);
|
|
27
|
+
return typeof first === 'number' ? String(first) : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function telegramTransport(bot: Pick<TelegramBot, 'sendInlineKeyboard' | 'editMessageWithKeyboard' | 'sendMessage'>, owner: string): CardTransport {
|
|
31
|
+
return {
|
|
32
|
+
platform: 'telegram',
|
|
33
|
+
ownerChats: async () => [owner],
|
|
34
|
+
send: async (chat, view) => {
|
|
35
|
+
const sent = await bot.sendInlineKeyboard(Number(chat), view.text, buttons(view));
|
|
36
|
+
return sent ? { chat, message: String(sent.messageId) } : null;
|
|
37
|
+
},
|
|
38
|
+
edit: async (ref, view) => { await bot.editMessageWithKeyboard(Number(ref.chat), Number(ref.message), view.text, buttons(view)); },
|
|
39
|
+
notify: async (chat, text) => { await bot.sendMessage(Number(chat), text); },
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface AttachOptions { tickMs?: number; service?: DecisionCardService }
|
|
44
|
+
|
|
45
|
+
/** Wire cards onto a bot: callback taps, memo capture, and a ticker that runs only while this bot polls. Returns a stop fn. */
|
|
46
|
+
export function attachTelegramDecisionCards(bot: TelegramBot, cfg: UserConfig, opts: AttachOptions = {}): () => void {
|
|
47
|
+
const owner = telegramDecisionOwner(cfg);
|
|
48
|
+
if (!owner) {
|
|
49
|
+
debug.log('decisions.telegram', 'not-attached', { platform: 'telegram', reason: 'no-owner' });
|
|
50
|
+
return () => undefined;
|
|
51
|
+
}
|
|
52
|
+
const replyTarget = (cfg.raw?.decisions as { replyGhPr?: unknown } | undefined)?.replyGhPr;
|
|
53
|
+
const service = opts.service ?? new DecisionCardService({
|
|
54
|
+
transport: telegramTransport(bot, owner),
|
|
55
|
+
ownerIds: [owner],
|
|
56
|
+
...(typeof replyTarget === 'string' && replyTarget ? { replyToRaiser: ghPrReplier(replyTarget) } : {}),
|
|
57
|
+
});
|
|
58
|
+
current = service;
|
|
59
|
+
const unsubscribe = bot.onCallbackQuery(async (q: TgCallbackQuery) => {
|
|
60
|
+
if (!q.data.startsWith(DECISION_DATA_PREFIX)) return;
|
|
61
|
+
const outcome = await service.tap(String(q.userId), q.data);
|
|
62
|
+
const where = q.chatId !== undefined && q.messageId !== undefined ? { chat: q.chatId, message: q.messageId } : null;
|
|
63
|
+
switch (outcome.kind) {
|
|
64
|
+
case 'refused': await bot.answerCallbackQuery(q.id, { text: '권한이 없습니다' }); return;
|
|
65
|
+
case 'ignored': case 'unknown': await bot.answerCallbackQuery(q.id, { text: '알 수 없는 결정입니다' }); return;
|
|
66
|
+
case 'memo-requested': {
|
|
67
|
+
// The next text in a GROUP could come from another allow-listed member — capture only in the owner's private chat.
|
|
68
|
+
if (q.chatId === undefined || q.chatId !== q.userId) {
|
|
69
|
+
debug.log('decisions.telegram', 'memo-refused', { platform: 'telegram', reason: 'not-private-chat' });
|
|
70
|
+
await bot.answerCallbackQuery(q.id, { text: '메모는 개인 대화에서만 달 수 있습니다' });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
await bot.answerCallbackQuery(q.id, { text: outcome.toast });
|
|
74
|
+
{
|
|
75
|
+
const id = /^dec:([^:]+):memo$/.exec(q.data)?.[1];
|
|
76
|
+
await bot.sendMessage(q.chatId, `📝 ${id} 메모를 한 줄로 보내 주세요(취소: /cancel).`);
|
|
77
|
+
bot.captureNextText(q.chatId, undefined, (text) => {
|
|
78
|
+
if (text && id) void service.setNote(String(q.userId), id, text).then((view) => {
|
|
79
|
+
if (view && q.chatId !== undefined) void bot.sendMessage(q.chatId, '메모를 달았습니다 — 카드에서 선택지를 누르면 함께 기록됩니다.');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
default:
|
|
86
|
+
await bot.answerCallbackQuery(q.id, { text: outcome.toast, ...(outcome.kind === 'confirm' ? { alert: true } : {}) });
|
|
87
|
+
// decided: the service already edited every card it sent; confirm/cancel/closed: edit the tapped one.
|
|
88
|
+
if (outcome.kind !== 'decided' && where) await bot.editMessageWithKeyboard(where.chat, where.message, outcome.view.text, buttons(outcome.view)).catch(() => undefined);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
const timer = setInterval(() => {
|
|
92
|
+
if (!bot.isPolling()) return;
|
|
93
|
+
void service.tick().catch((error: unknown) => {
|
|
94
|
+
debug.log('decisions.telegram', 'tick-failed', { reason: error instanceof Error ? error.message.slice(0, 80) : 'unknown' }, { level: 'warn' });
|
|
95
|
+
});
|
|
96
|
+
}, opts.tickMs ?? TICK_MS);
|
|
97
|
+
(timer as { unref?: () => void }).unref?.();
|
|
98
|
+
debug.log('decisions.telegram', 'attached', { platform: 'telegram', reply: typeof replyTarget === 'string' && Boolean(replyTarget) });
|
|
99
|
+
return () => { clearInterval(timer); unsubscribe(); if (current === service) current = null; };
|
|
100
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { elanousStateRoot } from '../autopilot/state-paths.js';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
|
|
6
|
+
export interface Versions { released: string | null; dev: string | null; codename: string | null }
|
|
7
|
+
export interface VersionOptions { releaseRoot?: string; repoRoot?: string; codenames?: Record<string, string> }
|
|
8
|
+
|
|
9
|
+
/** SemVer order for release ledgers: numeric core, then a prerelease sorts before its release. */
|
|
10
|
+
export function compareVersions(a: string, b: string): number {
|
|
11
|
+
const parse = (v: string) => { const [core, pre] = v.split('-', 2); return { nums: core!.split('.').map(n => Number(n) || 0), pre: pre ?? null }; };
|
|
12
|
+
const x = parse(a); const y = parse(b);
|
|
13
|
+
for (let i = 0; i < Math.max(x.nums.length, y.nums.length); i++) {
|
|
14
|
+
const d = (x.nums[i] ?? 0) - (y.nums[i] ?? 0);
|
|
15
|
+
if (d) return d;
|
|
16
|
+
}
|
|
17
|
+
if (x.pre === y.pre) return 0;
|
|
18
|
+
if (x.pre === null) return 1;
|
|
19
|
+
if (y.pre === null) return -1;
|
|
20
|
+
return x.pre.localeCompare(y.pre, undefined, { numeric: true });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createVersionResolver(opts: VersionOptions = {}): (ts: string) => Versions {
|
|
24
|
+
const releases: Array<{ at: number; version: string }> = [];
|
|
25
|
+
const root = opts.releaseRoot ?? join(elanousStateRoot(), 'release');
|
|
26
|
+
if (existsSync(root)) for (const dir of readdirSync(root, { withFileTypes: true })) {
|
|
27
|
+
if (!dir.isDirectory()) continue;
|
|
28
|
+
const path = join(root, dir.name, 'release.json');
|
|
29
|
+
if (!existsSync(path)) continue;
|
|
30
|
+
try {
|
|
31
|
+
const record = JSON.parse(readFileSync(path, 'utf8')) as Record<string, unknown>;
|
|
32
|
+
const stamp = record.publishedAt ?? record.published_at ?? record.releasedAt;
|
|
33
|
+
const at = Date.parse(String(stamp ?? ''));
|
|
34
|
+
if (typeof record.version === 'string' && Number.isFinite(at)) releases.push({ at, version: record.version });
|
|
35
|
+
} catch { /* a malformed ledger cannot supply a release boundary */ }
|
|
36
|
+
}
|
|
37
|
+
releases.sort((a, b) => a.at - b.at);
|
|
38
|
+
const repo = opts.repoRoot ?? process.cwd();
|
|
39
|
+
// A current-branch commit date is not proof of when that commit entered origin/main.
|
|
40
|
+
// Reflog updates are observed branch snapshots; before the first retained update the answer is unknown.
|
|
41
|
+
const reflog = spawnSync('git', ['reflog', 'show', '--date=unix', '--format=%H%x09%gd', 'refs/remotes/origin/main'], { cwd: repo, encoding: 'utf8', timeout: 10000 });
|
|
42
|
+
const snapshots: Array<{ at: number; commit: string; order: number }> = [];
|
|
43
|
+
if (reflog.status === 0) for (const line of reflog.stdout.split('\n')) {
|
|
44
|
+
const match = /^([0-9a-f]+)\torigin\/main@\{(\d+)\}$/.exec(line.trim());
|
|
45
|
+
// Reflog lists newest first; keep that order so two updates in the same second resolve to the later one.
|
|
46
|
+
if (match) snapshots.push({ at: Number(match[2]) * 1000, commit: match[1]!, order: snapshots.length });
|
|
47
|
+
}
|
|
48
|
+
snapshots.sort((a, b) => a.at - b.at || b.order - a.order);
|
|
49
|
+
const versions = new Map<string, string | null>();
|
|
50
|
+
for (const { commit } of snapshots) {
|
|
51
|
+
if (versions.has(commit)) continue;
|
|
52
|
+
const show = spawnSync('git', ['show', `${commit}:package.json`], { cwd: repo, encoding: 'utf8', timeout: 5000 });
|
|
53
|
+
let version: string | null = null;
|
|
54
|
+
if (show.status === 0) {
|
|
55
|
+
try {
|
|
56
|
+
const parsed = JSON.parse(show.stdout).version;
|
|
57
|
+
if (typeof parsed === 'string') version = parsed;
|
|
58
|
+
} catch { /* unreadable package at this snapshot */ }
|
|
59
|
+
}
|
|
60
|
+
versions.set(commit, version);
|
|
61
|
+
}
|
|
62
|
+
const cache = new Map<string, Versions>();
|
|
63
|
+
return ts => {
|
|
64
|
+
const existing = cache.get(ts);
|
|
65
|
+
if (existing) return existing;
|
|
66
|
+
const time = Date.parse(ts);
|
|
67
|
+
// The public version at a moment = the highest version published by then (a re-written old record must not win by date).
|
|
68
|
+
const released = Number.isFinite(time) ? releases.filter(x => x.at <= time).map(x => x.version).sort(compareVersions).at(-1) ?? null : null;
|
|
69
|
+
const snapshot = Number.isFinite(time) ? [...snapshots].reverse().find(x => x.at <= time) : undefined;
|
|
70
|
+
const dev = snapshot ? versions.get(snapshot.commit) ?? null : null;
|
|
71
|
+
const codenames = opts.codenames ?? {};
|
|
72
|
+
const codename = codenames[dev?.replace(/-.*/, '') ?? ''] ?? codenames[released ?? ''] ?? null;
|
|
73
|
+
const value = { released, dev, codename };
|
|
74
|
+
cache.set(ts, value);
|
|
75
|
+
return value;
|
|
76
|
+
};
|
|
77
|
+
}
|