elanous 0.2.7 → 0.2.9
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/bFI5EEwg7T6aG88ibX4Zq/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7032-f4ef698518fba61c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8067-d976b441a104ab74.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8519-5632377cce994f86.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8630-cdfc61e82c653ffc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8894-e263b332208e3957.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9454-139040d37c79dbc9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9740-8f5ff96c522b5f55.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-fb7c2d458f32fcad.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-35e2713597dd24d1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-918b656f064577ba.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-624f7a2f89b60c8f.js → page-f972fc1c00a62e88.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-5a5fb385a4df0ffe.js → page-5f30e00693527ebd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-80497e93a878ddc5.js → page-7fbae173b3c103a7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/{page-7457f52683562161.js → page-3b4edcce364062ea.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-c5794f484c251f44.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-1130d8eefcd87e76.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-973996d584f008c1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-b7dcf5d48a854b76.js → page-016e2bda04eecb1f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-51480757e7d68b92.js → page-b4d6d9a3b56f2940.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-ec49a466121233ba.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-f72db356744ceade.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-a6fd9b50d1b85378.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-8b0402a46a749a3d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-07267f1391b96aba.js → page-037f83e2a1edc8e2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-55d50e9b0006a91d.js → page-adfbd2ba9598e59a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-46fedff57d4bec67.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-0cf318e9dcee2bbf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-899b327cb8b6845c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-6c1e6ba743a29dfe.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/{page-a7e37af7d0399cb8.js → page-1a8e4807292f6b17.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-28deba6bcb834916.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-de68e2205794037d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-15e5a15db84c56aa.js → page-c71c33a573230672.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-a2e572e4511f2a16.js → page-0df30e0c7e50148a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-4fc56790fe0d2682.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-59df2aebb38a7c1e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-4647be5dbcbd0795.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-e025892bda74b743.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-a0955de73b89f078.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-c1c95739c0bff3b8.js → page-369846d2921af584.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-72cbc1c8cdd0f65d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-71d18eec33b9c667.js → page-77b28bfb0996d5ea.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-3220c4934f49c6c2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-2c4e59079ee357ac.js → page-90670589b87abea8.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-7df9243ed021a4cc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-5d422f38c052967e.js → page-4498c3853f4f688d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-35bedde6d885f08f.js +1 -0
- package/apps/pwa/out/_next/static/css/f92f63a5df0ba229.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +30 -29
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +30 -29
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +30 -29
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +27 -26
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +27 -26
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +27 -26
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +30 -29
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +30 -29
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +30 -29
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +30 -29
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +30 -29
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +30 -29
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +27 -26
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +30 -29
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +30 -29
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +27 -26
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +27 -26
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +30 -29
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +30 -29
- package/apps/pwa/out/ops/checklist/index.html +1 -0
- package/apps/pwa/out/ops/checklist/index.txt +30 -0
- package/apps/pwa/out/ops/release/index.html +1 -0
- package/apps/pwa/out/ops/release/index.txt +30 -0
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +28 -27
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +30 -29
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +27 -26
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +30 -29
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +30 -29
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +30 -29
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +30 -29
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +30 -29
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +27 -26
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +30 -29
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +30 -29
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +30 -29
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +30 -29
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +30 -29
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +30 -29
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +30 -29
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +30 -29
- package/graphs/release/recipes.yaml +3 -0
- package/graphs/release/release-loop.yaml +3 -1
- package/graphs/test-diet/nightly-audit.yaml +18 -0
- package/graphs/test-diet/recipes.yaml +12 -0
- package/graphs/test-diet/test-diet.yaml +22 -0
- package/package.json +2 -1
- 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/doc-draft/graphs/run-step.ts +1 -1
- package/plugins/geo-check/graphs/run-step.ts +34 -10
- package/scripts/field-feed/deliver-node.ts +8 -3
- package/scripts/field-feed/lib.ts +2 -0
- package/scripts/install.sh +20 -11
- package/scripts/persona-presets/lint.ts +79 -0
- package/scripts/release-loop/checklist-gate-node.ts +2 -2
- package/scripts/release-loop/cutoff.ts +20 -2
- package/scripts/release-loop/export-check-node.ts +85 -0
- package/scripts/release-loop/gate-node.ts +45 -7
- package/scripts/release-loop/node-verdict.ts +8 -1
- package/scripts/release-loop/npm-publish-node.ts +10 -4
- package/scripts/release-loop/ops-upgrade-node.ts +11 -3
- package/scripts/release-loop/publish-node.ts +23 -1
- package/scripts/release-loop/shard-plan.ts +17 -0
- package/scripts/storyboard/storyboard.ts +167 -0
- package/scripts/test-diet/lib.ts +89 -0
- package/scripts/test-diet/node.ts +144 -0
- package/src/acp/auth-methods.ts +30 -0
- package/src/acp/server.ts +5 -5
- package/src/auth/setup-link-tokens.fixture.ts +19 -0
- package/src/auth/setup-link-tokens.ts +153 -0
- package/src/budget/unified-usage.ts +75 -0
- package/src/channel-bot-token.ts +37 -10
- package/src/channel-import/detect.ts +191 -0
- package/src/channel-import/run.ts +111 -0
- package/src/chat/index.ts +9 -9
- package/src/cli/config-test-sync.ts +4 -0
- package/src/cli/decisions-cli.ts +13 -3
- package/src/cli/doctor-bundle.ts +128 -0
- package/src/cli/doctor-cli.ts +36 -1
- package/src/cli/doctor-local-llm.ts +95 -0
- package/src/cli/import-cli.ts +60 -0
- package/src/cli/mcp-login.ts +13 -4
- package/src/cli/op-proxy-sync.ts +83 -0
- package/src/cli/persona-cli.ts +116 -0
- package/src/cli/pr-cli.ts +6 -2
- package/src/cli/release-cli.ts +49 -8
- package/src/cli/skills-cli.ts +173 -0
- package/src/cli/start-cli.ts +13 -4
- package/src/codex/setup.ts +15 -13
- package/src/dashboard/dashboard-status-lines.ts +45 -0
- package/src/dashboard/first-screen-band.ts +35 -0
- package/src/dashboard/index.ts +87 -14
- package/src/dashboard/provider-slash-runtime.ts +28 -11
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +12 -2
- package/src/dashboard/slash-runtime/help-from-registry.ts +4 -2
- package/src/dashboard/turn-stream-formatter.ts +1 -1
- 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 +17 -0
- package/src/domains/outbound-alert.ts +44 -1
- package/src/domains/repeated-failure.ts +1 -1
- package/src/error-report/report.ts +200 -0
- package/src/exec-requests/planner.ts +8 -2
- package/src/exec-requests/runner.ts +64 -24
- package/src/exec-requests/store.ts +2 -0
- package/src/field/field-feed-auto.ts +48 -0
- package/src/field/field-reel.ts +8 -1
- 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 +98 -11
- package/src/input-core/text-entry.ts +13 -5
- package/src/intake-plane/seat-answer.ts +96 -0
- package/src/intake-plane/seat-doc-route.ts +17 -3
- package/src/maturity/feature-maturity.ts +2 -0
- package/src/nexus/api/consult-requests.ts +19 -8
- package/src/nexus/api/error-reports.ts +108 -0
- package/src/nexus/api/http-server.ts +41 -1
- package/src/nexus/api/meta-api.ts +27 -0
- package/src/nexus/api/operator.ts +61 -0
- package/src/nexus/api/ops-api.ts +139 -0
- package/src/nexus/api/ops-seats-sources.ts +65 -0
- package/src/nexus/api/ops-seats.ts +114 -0
- package/src/nexus/api/outbound-report.ts +7 -0
- package/src/nexus/api/public-routes.ts +14 -1
- package/src/nexus/api/running-runs-background.ts +68 -0
- package/src/nexus/api/running-runs-child.ts +6 -0
- package/src/nexus/api/setup-claim.ts +21 -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/config/secrets/file-backend.ts +56 -18
- package/src/nexus/index.ts +25 -1
- package/src/nexus/setup-status.ts +7 -2
- package/src/oauth/browser-availability.ts +14 -0
- package/src/oauth/codex-quota-policy.ts +16 -0
- package/src/onboarding/continue-here.ts +13 -8
- package/src/onboarding/first-run-doctor.ts +54 -0
- package/src/onboarding/web-first.ts +8 -1
- package/src/onboarding.ts +66 -12
- package/src/persona/presets.ts +105 -0
- package/src/release-loop/checklist.ts +33 -69
- package/src/release-loop/feature-store.ts +327 -0
- package/src/release-loop/manifest.ts +59 -5
- package/src/seat-dispatch/ceo-commands.ts +65 -0
- package/src/self-dev/orchestrate.ts +18 -11
- 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/tools/usage-slash.ts +8 -7
- package/src/skills/trigger-extract.ts +1 -1
- package/src/task-orchestrator/surfaces/pod-command-job.ts +16 -8
- package/src/telegram-commands.ts +29 -0
- package/src/telegram-run.ts +83 -13
- package/src/telegram.ts +34 -0
- package/src/thinking-line.ts +28 -20
- package/src/tui/chat/execution-badge.ts +1 -1
- package/src/user-config.ts +111 -13
- package/src/ux-sim/turn-states.ts +1 -1
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/4836-2eba6079f31b88d4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8067-f74bf14d67ad98f1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8519-f6b5d8a5833a7603.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8894-e92491a164192cc1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9454-a3e0953307216c58.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9740-67051141115ce775.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-d5ebde04844689e6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-10ed24f413ed66e9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-211810b6c93ab66a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-cee86394de407aa6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-4ff8edab44196b8c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-92e4f44428d5e290.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-2a05dfe80c8bdf16.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-d9979633aa82f8e6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-30533117beb53625.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-f7857826c2f97ad7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-9699b1786be6171d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-43dd772aee8af75f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-973ab7af59c718b7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a9f8d3865b91e436.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-dd107bf1c7d52a90.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-d42b99cdfb9f27c6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-2764ff40dad5a995.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-c5f1152db116ff96.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-f933bca81ea38a6f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-a7aaffcf771662d8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-56adbe1a1f1379bd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-a81f113f3ef89d72.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-fc27b00e20733146.js +0 -1
- package/apps/pwa/out/_next/static/css/f537bdb19c69306a.css +0 -1
- package/apps/pwa/out/_next/static/keajW7DDyghEs1y8oJe6T/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{keajW7DDyghEs1y8oJe6T → bFI5EEwg7T6aG88ibX4Zq}/_ssgManifest.js +0 -0
package/src/dashboard/index.ts
CHANGED
|
@@ -725,7 +725,8 @@ import { SELF_IMPLEMENT_TOOL_NAMES } from '../boot/daemon-tools/self-implement-n
|
|
|
725
725
|
import { FOLD_LIMITS, type FoldMode } from '../log-entry.js';
|
|
726
726
|
import { renderToolCallEvent, renderToolResultVariants } from '../chat/tool-render/index.js';
|
|
727
727
|
import { FoldStack } from '../fold-stack.js';
|
|
728
|
-
import { getSkillIndex, reloadSkillIndex, applySkillFilter } from '../skills/index.js';
|
|
728
|
+
import { getSkillIndex, reloadSkillIndex, applySkillFilter, skillIndexProblems } from '../skills/index.js';
|
|
729
|
+
import { skillRepairNotice } from '../skills/repair.js';
|
|
729
730
|
import type { DetectResult } from '../skills/router.js';
|
|
730
731
|
import { detectUrlRoute } from '../skills/url-router.js';
|
|
731
732
|
import {
|
|
@@ -880,6 +881,10 @@ import { getModelFamily, getModelTier } from '../models/prompts.js';
|
|
|
880
881
|
import { buildPromptInjection, getPromptBankStore } from '../prompt-bank/index.js';
|
|
881
882
|
import { getInputHistoryStore, inputHistoryDbPath, inputHistoryJsonPath } from '../input-history.js';
|
|
882
883
|
import { inspectActiveProvider } from '../provider-summary.js';
|
|
884
|
+
import { activeCodexAccountView } from '../oauth/codex-account-store.js';
|
|
885
|
+
import { buildDashboardStatusLines } from './dashboard-status-lines.js';
|
|
886
|
+
import { buildFirstScreenBand } from './first-screen-band.js';
|
|
887
|
+
import { runNexusShow, type NexusShowResult } from '../cli/nexus-show.js';
|
|
883
888
|
import {
|
|
884
889
|
workingDirSegment, sessionCwdSegment, modelSegment, gitSegment,
|
|
885
890
|
ctxBarSegment, costSegment, speedSegment, elapsedSegment, cacheSegment,
|
|
@@ -2099,6 +2104,35 @@ export function createDashboardLogSearchOpener(deps: {
|
|
|
2099
2104
|
};
|
|
2100
2105
|
}
|
|
2101
2106
|
|
|
2107
|
+
export function renderDashboardFirstScreenBand(input: {
|
|
2108
|
+
readonly history: ReadonlyArray<{ role: string }>;
|
|
2109
|
+
readonly lines: string[];
|
|
2110
|
+
readonly width: number;
|
|
2111
|
+
readonly draw: () => void;
|
|
2112
|
+
readonly observe: (data: { daemon: boolean; hasPwa: boolean; hasTailnet: boolean }) => void;
|
|
2113
|
+
readonly show?: () => Promise<Pick<NexusShowResult, 'status' | 'urls'>>;
|
|
2114
|
+
}): Promise<void> {
|
|
2115
|
+
if (input.history.some((message) => message.role !== 'system')) return Promise.resolve();
|
|
2116
|
+
const band = buildFirstScreenBand({ width: input.width, daemon: false });
|
|
2117
|
+
input.lines.unshift(...band);
|
|
2118
|
+
const show = input.show ?? (() => runNexusShow({ format: 'json', out: { log: () => {}, error: () => {} } }));
|
|
2119
|
+
const observe = (data: { daemon: boolean; hasPwa: boolean; hasTailnet: boolean }): void => {
|
|
2120
|
+
try { input.observe(data); } catch { /* observation cannot interrupt the transcript */ }
|
|
2121
|
+
};
|
|
2122
|
+
return Promise.resolve().then(show).then((result) => {
|
|
2123
|
+
const daemon = result.status !== 'absent';
|
|
2124
|
+
const pwa = result.urls?.pwa;
|
|
2125
|
+
const hasPwa = !!pwa;
|
|
2126
|
+
const hasTailnet = !!pwa?.tailnet;
|
|
2127
|
+
observe({ daemon, hasPwa, hasTailnet });
|
|
2128
|
+
if (input.lines[1] !== band[1]) return;
|
|
2129
|
+
input.lines[1] = buildFirstScreenBand({ width: input.width, daemon, pwa })[1]!;
|
|
2130
|
+
try { input.draw(); } catch { /* screen may have closed */ }
|
|
2131
|
+
}).catch(() => {
|
|
2132
|
+
observe({ daemon: false, hasPwa: false, hasTailnet: false });
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2102
2136
|
export async function showDashboard(opts: ShowDashboardOptions = {}): Promise<DashboardAction> {
|
|
2103
2137
|
const dashboardQuitConfirmationState = createQuitConfirmationState();
|
|
2104
2138
|
const signalQuitConfirmationState = createQuitConfirmationState();
|
|
@@ -6329,19 +6363,34 @@ Mode- and sync-specific instructions are injected per-turn when relevant — do
|
|
|
6329
6363
|
draw,
|
|
6330
6364
|
});
|
|
6331
6365
|
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
|
|
6336
|
-
)
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6366
|
+
let daemonAttached = false;
|
|
6367
|
+
const getDashboardStatusLines = () => {
|
|
6368
|
+
const cfg = getUserConfig();
|
|
6369
|
+
const active = inspectActiveProvider(cfg);
|
|
6370
|
+
const reasoningProvider = (['openai-codex', 'openai', 'anthropic', 'gemini', 'grok', 'local', 'openrouter'] as const)
|
|
6371
|
+
.find(provider => active.provider === provider || active.provider === `auto:${provider}`);
|
|
6372
|
+
return buildDashboardStatusLines({
|
|
6373
|
+
provider: active.provider,
|
|
6374
|
+
model: active.model,
|
|
6375
|
+
reasoning: reasoningProvider
|
|
6376
|
+
? modelSupportsReasoning(reasoningProvider, active.model)
|
|
6377
|
+
? reasoningLevelLabel(effectiveReasoningLevel(cfg.llm, reasoningProvider, active.model))
|
|
6378
|
+
: '미지원'
|
|
6379
|
+
: undefined,
|
|
6380
|
+
daemonAddress: opts.remote?.url,
|
|
6381
|
+
daemonConnected: daemonAttached,
|
|
6382
|
+
accountName: active.provider === 'openai-codex' || active.provider === 'auto:openai-codex'
|
|
6383
|
+
? activeCodexAccountView().name : undefined,
|
|
6384
|
+
sessionSurfaceLines: buildSessionDashboardStatusLines(resolveSessionSurfaceStatus({ chatModeState })),
|
|
6385
|
+
view: String(workingDir.view),
|
|
6386
|
+
focus: workingDir.focus,
|
|
6387
|
+
cwd: workingDir.cwd,
|
|
6388
|
+
preview: dockedPreview.sourceMode,
|
|
6389
|
+
starterClosed: closedStarterPanes().length ? closedStarterPanes().map((pane) => paneLabel(pane)).join(', ') : 'none',
|
|
6390
|
+
chatOnly: `${effectiveChatOnlyMode() ? 'on' : 'off'}${chatOnlyMode ? ' (manual)' : ''}`,
|
|
6391
|
+
acp: dashboardAcpChat.isBusy() ? 'busy' : 'idle',
|
|
6392
|
+
});
|
|
6393
|
+
};
|
|
6345
6394
|
|
|
6346
6395
|
bootDashboardSlashExecutor({
|
|
6347
6396
|
initDashboardSlashExecutor,
|
|
@@ -7190,6 +7239,21 @@ Mode- and sync-specific instructions are injected per-turn when relevant — do
|
|
|
7190
7239
|
// and after each user-initiated skill cursor change.
|
|
7191
7240
|
const skillView: SkillViewState = createSkillViewState(LOCAL_SKILLS_DIR);
|
|
7192
7241
|
try { refreshSkills(skillView); refreshSkillFiles(skillView); } catch { /* missing root fine */ }
|
|
7242
|
+
// SK2 — a skill the index could not read is a one-line notice with the repair command, never a crash.
|
|
7243
|
+
try {
|
|
7244
|
+
getSkillIndex();
|
|
7245
|
+
const problems = skillIndexProblems();
|
|
7246
|
+
const notice = skillRepairNotice(problems);
|
|
7247
|
+
if (notice) chatLines.push(C.info(notice));
|
|
7248
|
+
// ER1 — tell operations too (consented · scrubbed · async); a kept unsent report is retried once a day.
|
|
7249
|
+
void import('../error-report/report.js').then(async ({ reportError, retryPendingErrorReport }) => {
|
|
7250
|
+
await retryPendingErrorReport();
|
|
7251
|
+
for (const p of problems.slice(0, 3)) {
|
|
7252
|
+
const outcome = await reportError({ code: 'skill-index', message: `${p.name}: ${p.code ?? ''} ${p.error}`.trim(), surface: 'tui' });
|
|
7253
|
+
if (outcome.sent && outcome.reportId) { chatLines.push(C.muted(` 운영팀에 알렸습니다 (보고 번호 ${outcome.reportId})`)); }
|
|
7254
|
+
}
|
|
7255
|
+
}).catch(() => { /* reporting never breaks the dashboard */ });
|
|
7256
|
+
} catch { /* the notice is advisory */ }
|
|
7193
7257
|
|
|
7194
7258
|
// Scratch visibility (Phase S5). When closed, the workingDir
|
|
7195
7259
|
// layouts drop the scratch cell so the remaining panes expand
|
|
@@ -15381,6 +15445,7 @@ Mode- and sync-specific instructions are injected per-turn when relevant — do
|
|
|
15381
15445
|
...(opts.remote ? {
|
|
15382
15446
|
remote: opts.remote,
|
|
15383
15447
|
onRemoteDisconnect: () => {
|
|
15448
|
+
daemonAttached = false;
|
|
15384
15449
|
pushChatLine(C.warning(' ⚠ 데몬 연결이 끊겼다 · 입력 중인 글은 그대로다 · 다시 붙으려면 TUI 를 다시 띄워라'));
|
|
15385
15450
|
draw();
|
|
15386
15451
|
},
|
|
@@ -15594,6 +15659,7 @@ Mode- and sync-specific instructions are injected per-turn when relevant — do
|
|
|
15594
15659
|
},
|
|
15595
15660
|
});
|
|
15596
15661
|
const dashboardAcpSession: DashboardSession = dashboardAcpBootResult.session;
|
|
15662
|
+
daemonAttached = !!opts.remote && dashboardAcpBootResult.mode !== 'in-process';
|
|
15597
15663
|
// ⭐ `B3` — 턴 중 발화를 «도는 턴»에 넣을 때 쓰는 키.
|
|
15598
15664
|
try { acpSessionIdRef.current = dashboardAcpSession.currentSessionId ?? null; } catch { /* 없으면 넣지 않는다 */ }
|
|
15599
15665
|
pushDebugLine(C.muted('[acp-boot] dashboard ACP session ready'));
|
|
@@ -15686,6 +15752,13 @@ Mode- and sync-specific instructions are injected per-turn when relevant — do
|
|
|
15686
15752
|
}
|
|
15687
15753
|
}
|
|
15688
15754
|
chatScrollOffset = -1;
|
|
15755
|
+
void renderDashboardFirstScreenBand({
|
|
15756
|
+
history: chat.history,
|
|
15757
|
+
lines: chatLines,
|
|
15758
|
+
width: Math.max(0, termSize().cols - 4),
|
|
15759
|
+
draw: () => draw(),
|
|
15760
|
+
observe: (data) => { try { debug.log('dashboard.first-screen-band', 'resolved', data); } catch { /* observation is best-effort */ } },
|
|
15761
|
+
});
|
|
15689
15762
|
|
|
15690
15763
|
while (true) {
|
|
15691
15764
|
draw();
|
|
@@ -8,7 +8,7 @@ export interface DashboardProviderRotationEntryView {
|
|
|
8
8
|
export interface DashboardProviderAvailabilityView {
|
|
9
9
|
available: boolean;
|
|
10
10
|
name: string;
|
|
11
|
-
model: string;
|
|
11
|
+
model: string | undefined;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export interface DashboardProviderSlashRuntimeDeps {
|
|
@@ -31,6 +31,7 @@ export interface DashboardProviderSlashRuntime {
|
|
|
31
31
|
overviewLines(
|
|
32
32
|
rotationEntries: readonly DashboardProviderRotationEntryView[],
|
|
33
33
|
providers: readonly DashboardProviderAvailabilityView[],
|
|
34
|
+
catalogModelIds?: ReadonlySet<string>,
|
|
34
35
|
): string[];
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -48,25 +49,41 @@ export function createDashboardProviderSlashRuntime(
|
|
|
48
49
|
switchedLine: (label, provider, model) => deps.success(` ✓ switched → ${label} (${providerDetail(provider, model)})`),
|
|
49
50
|
resetEmptyLine: () => deps.warning(' rotation empty — nothing to reset'),
|
|
50
51
|
resetLine: (label) => deps.success(` ✓ reset → ${label}`),
|
|
51
|
-
overviewLines: (rotationEntries, providers) => {
|
|
52
|
+
overviewLines: (rotationEntries, providers, catalogModelIds) => {
|
|
52
53
|
const lines = ['', deps.accent('\u276f /provider')];
|
|
53
54
|
if (rotationEntries.length > 0) {
|
|
54
|
-
lines.push(deps.muted('
|
|
55
|
+
lines.push(deps.muted(' 회전(바꾸기: /provider next · /provider use <이름>)'));
|
|
55
56
|
for (const entry of rotationEntries) {
|
|
56
57
|
const marker = entry.current ? deps.success('▸') : ' ';
|
|
57
|
-
|
|
58
|
+
const missing = catalogModelIds && entry.model !== '(provider default)' && !catalogModelIds.has(entry.model)
|
|
59
|
+
? deps.muted(' · 목록에 없는 모델 — /provider use 로 바꾸기')
|
|
60
|
+
: '';
|
|
61
|
+
lines.push(` ${marker} ${entry.label.padEnd(14)} ${deps.subtext(entry.provider)} ${deps.muted(entry.model)}${missing}`);
|
|
58
62
|
}
|
|
59
63
|
lines.push('');
|
|
60
64
|
}
|
|
61
|
-
lines.push(deps.muted('
|
|
65
|
+
lines.push(deps.muted(' 쓸 수 있는 공급자'));
|
|
66
|
+
// PROVIDERS has anthropic, not claude; collapse the legacy claude display alias.
|
|
67
|
+
const canonicalName = (name: string): string => {
|
|
68
|
+
const lower = name.toLowerCase();
|
|
69
|
+
return lower === 'claude' ? 'anthropic' : lower;
|
|
70
|
+
};
|
|
71
|
+
const unique = new Map<string, DashboardProviderAvailabilityView>();
|
|
62
72
|
for (const provider of providers) {
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
73
|
+
const key = canonicalName(provider.name);
|
|
74
|
+
const previous = unique.get(key);
|
|
75
|
+
if (!previous || (!previous.available && provider.available) || (key === provider.name && previous.name !== key && previous.available === provider.available)) {
|
|
76
|
+
unique.set(key, provider);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
let unavailable = 0;
|
|
80
|
+
for (const provider of unique.values()) {
|
|
81
|
+
if (!provider.available) { unavailable++; continue; }
|
|
82
|
+
lines.push(` ${deps.success('\u2713')} ${deps.text(provider.name).padEnd(20)} ${deps.subtext(provider.model ?? '')}`);
|
|
69
83
|
}
|
|
84
|
+
if (unavailable > 0) lines.push(deps.muted(` 그 밖 ${unavailable}개는 키·로그인 필요 — elanous setup llm`));
|
|
85
|
+
// Keep the old English search terms as a subordinate hint, not section headings.
|
|
86
|
+
lines.push(deps.muted(' · Rotation (cycle with /provider next) · Available providers'));
|
|
70
87
|
return lines;
|
|
71
88
|
},
|
|
72
89
|
};
|
|
@@ -301,6 +301,7 @@ import {
|
|
|
301
301
|
import type { LLMProviderName } from '../../user-config.js';
|
|
302
302
|
import { lookupLlmTierSpec } from '../../model-tier/index.js';
|
|
303
303
|
import { listProviders } from '../../llm.js';
|
|
304
|
+
import { enabledModels, loadCatalog } from '../../intelligence-map/model-catalog.js';
|
|
304
305
|
import { selfOrchestrateRuntime } from '../../self-dev/self-orchestrate-runtime.js';
|
|
305
306
|
import type { ToolRuntime } from '../../tool-runtime/types.js';
|
|
306
307
|
import { getSessionCwd } from '../../session/working-dir.js';
|
|
@@ -742,6 +743,7 @@ export interface DashboardSlashContext {
|
|
|
742
743
|
overviewLines(
|
|
743
744
|
rotation: readonly { label: string; provider: string; model: string; current: boolean }[],
|
|
744
745
|
providers: readonly { available: boolean; name: string; model: string | undefined }[],
|
|
746
|
+
catalogModelIds?: ReadonlySet<string>,
|
|
745
747
|
): readonly string[];
|
|
746
748
|
};
|
|
747
749
|
/** 2026-05-05 — open the visual model picker programmatically.
|
|
@@ -1370,8 +1372,8 @@ export function buildDashboardSlashRegistry(): SlashCommandRegistry<DashboardSla
|
|
|
1370
1372
|
ctx.attachmentRowMap.clear();
|
|
1371
1373
|
ctx.clearLogSearch();
|
|
1372
1374
|
ctx.clearLogFilter();
|
|
1373
|
-
ctx.pushDebugLine(ctx.muted('
|
|
1374
|
-
ctx.chatLines.push(ctx.muted('
|
|
1375
|
+
ctx.pushDebugLine(ctx.muted('Chat log cleared'));
|
|
1376
|
+
ctx.chatLines.push(ctx.muted('Chat log cleared'));
|
|
1375
1377
|
ctx.setChatScrollOffset(-1);
|
|
1376
1378
|
});
|
|
1377
1379
|
|
|
@@ -5281,6 +5283,13 @@ export function buildDashboardSlashRegistry(): SlashCommandRegistry<DashboardSla
|
|
|
5281
5283
|
const cfgNow = getUserConfig();
|
|
5282
5284
|
const rot = cfgNow.llm.rotation;
|
|
5283
5285
|
const curIdx = rot && rot.length > 0 ? currentRotationIndex(cfgNow) : -1;
|
|
5286
|
+
let catalogModelIds: Set<string> | undefined;
|
|
5287
|
+
try {
|
|
5288
|
+
const loaded = loadCatalog();
|
|
5289
|
+
if (loaded.source !== 'fallback') {
|
|
5290
|
+
catalogModelIds = new Set(enabledModels(loaded.catalog).map((model) => model.id));
|
|
5291
|
+
}
|
|
5292
|
+
} catch { /* catalog unreadable: no stale-model verdict */ }
|
|
5284
5293
|
ctx.chatLines.push(...ctx.provider.slashRuntime.overviewLines(
|
|
5285
5294
|
(rot ?? []).map((entry, index) => ({
|
|
5286
5295
|
label: rotationEntryLabel(entry),
|
|
@@ -5293,6 +5302,7 @@ export function buildDashboardSlashRegistry(): SlashCommandRegistry<DashboardSla
|
|
|
5293
5302
|
name: provider.name,
|
|
5294
5303
|
model: provider.model,
|
|
5295
5304
|
})),
|
|
5305
|
+
catalogModelIds,
|
|
5296
5306
|
));
|
|
5297
5307
|
const currentRoute = currentRouteDecision('dashboard');
|
|
5298
5308
|
if (currentRoute) ctx.chatLines.push(ctx.muted(` current route: ${formatRouteDecisionSummary(currentRoute)}`));
|
|
@@ -3,7 +3,9 @@ import { visibleWidth } from '../../tui.js';
|
|
|
3
3
|
|
|
4
4
|
const ESSENTIAL_KEYS = [
|
|
5
5
|
['Enter', 'Send message'],
|
|
6
|
-
|
|
6
|
+
// U1 · 2026-10-02 — stopping and queueing while an answer streams were missing from help (same line count:
|
|
7
|
+
// the modal has a row budget and falls back to the chat log when it is exceeded).
|
|
8
|
+
['Esc', 'Stop the answer (Esc again = rewind) · clear input / close help'],
|
|
7
9
|
['Ctrl+U', 'Clear text before cursor'],
|
|
8
10
|
['Ctrl+W', 'Delete previous word'],
|
|
9
11
|
['Ctrl+← / →', 'Move by word'],
|
|
@@ -11,7 +13,7 @@ const ESSENTIAL_KEYS = [
|
|
|
11
13
|
['Home / End', 'Move to start / end of line'],
|
|
12
14
|
['Ctrl+A / E', 'Move to start / end of line'],
|
|
13
15
|
['PgUp / PgDn', 'Scroll chat log'],
|
|
14
|
-
['Ctrl+↑', 'Recall
|
|
16
|
+
['Ctrl+↑', 'Recall a queued line (Enter while answering queues it)'],
|
|
15
17
|
] as const;
|
|
16
18
|
|
|
17
19
|
function fitLine(text: string, width: number): string {
|
|
@@ -236,7 +236,7 @@ export function createTurnStreamFormatter(
|
|
|
236
236
|
},
|
|
237
237
|
onToolCall(call: TurnStreamCall): void {
|
|
238
238
|
toolCallCount += 1;
|
|
239
|
-
deps.thinking.update(`Streaming ${call.name} (${toolCallCount} tools)`);
|
|
239
|
+
deps.thinking.update(`Streaming ${call.name} (${toolCallCount} ${toolCallCount === 1 ? 'tool' : 'tools'})`);
|
|
240
240
|
const ptyLine = deps.ptyCallLine(call.name, call.args);
|
|
241
241
|
if (ptyLine) {
|
|
242
242
|
nonGenericCallIds.add(call.id);
|
|
@@ -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
|
+
export 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 {
|