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,202 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
6
|
+
import { findTrack } from '../autopilot/mission-phase-track.js';
|
|
7
|
+
import { appendGithubStarSnapshots, collectGithubStars, ghApiSearchRepos, type SearchGithubRepos } from './collect-github.js';
|
|
8
|
+
import { ingestIntakeItems, parseRawIntakeJsonl, type IntakeSource, type RegisteredIntakeSourceKind, type RawIntakeItem } from './items.js';
|
|
9
|
+
|
|
10
|
+
export type SourceKind = 'github-query' | 'rss' | 'command';
|
|
11
|
+
export interface RegisteredIntakeSource {
|
|
12
|
+
id: string;
|
|
13
|
+
seat: string;
|
|
14
|
+
kind: SourceKind;
|
|
15
|
+
spec: string;
|
|
16
|
+
every: string;
|
|
17
|
+
until?: string;
|
|
18
|
+
why?: string;
|
|
19
|
+
lastRunAt?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function canonicalSeat(value: string): string {
|
|
23
|
+
const seat = findTrack(value.trim());
|
|
24
|
+
if (!seat) throw new Error(`알 수 없는 자리: ${value}`);
|
|
25
|
+
return seat.id;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function cadenceMs(every: string): number {
|
|
29
|
+
const match = /^([1-9]\d*)([hdw])$/.exec(every);
|
|
30
|
+
if (!match) throw new Error(`--every 는 Nh · Nd · Nw 형식이어야 한다: ${every}`);
|
|
31
|
+
const ms = Number(match[1]) * ({ h: 3600_000, d: 86_400_000, w: 604_800_000 }[match[2]!] ?? 0);
|
|
32
|
+
if (!Number.isSafeInteger(ms)) throw new Error(`--every 가 너무 크다: ${every}`);
|
|
33
|
+
return ms;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function untilMs(until: string): number {
|
|
37
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(until) || !Number.isFinite(Date.parse(`${until}T00:00:00Z`))
|
|
38
|
+
|| new Date(`${until}T00:00:00Z`).toISOString().slice(0, 10) !== until) throw new Error(`--until 날짜 형식 오류: ${until}`);
|
|
39
|
+
return Date.parse(`${until}T00:00:00Z`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function expiredSource(source: RegisteredIntakeSource, now: Date): boolean {
|
|
43
|
+
return source.until !== undefined && now.getTime() >= untilMs(source.until);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function sourceIsDue(source: RegisteredIntakeSource, now: Date): boolean {
|
|
47
|
+
return !expiredSource(source, now) && (source.lastRunAt === undefined
|
|
48
|
+
|| Date.parse(source.lastRunAt) + cadenceMs(source.every) <= now.getTime());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function sourcesFile(stateDir: string): string { return join(stateDir, 'intake', 'sources.json'); }
|
|
52
|
+
|
|
53
|
+
function readSources(stateDir: string): RegisteredIntakeSource[] {
|
|
54
|
+
const file = sourcesFile(stateDir);
|
|
55
|
+
if (!existsSync(file)) return [];
|
|
56
|
+
const parsed: unknown = JSON.parse(readFileSync(file, 'utf8'));
|
|
57
|
+
if (!Array.isArray(parsed)) throw new Error(`원천 등록부 모양 오류: ${file}`);
|
|
58
|
+
return parsed as RegisteredIntakeSource[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function saveSources(stateDir: string, sources: RegisteredIntakeSource[]): void {
|
|
62
|
+
const file = sourcesFile(stateDir);
|
|
63
|
+
mkdirSync(join(stateDir, 'intake'), { recursive: true });
|
|
64
|
+
const temp = `${file}.${randomUUID()}.tmp`;
|
|
65
|
+
try {
|
|
66
|
+
const fd = openSync(temp, 'wx', 0o600);
|
|
67
|
+
try { writeFileSync(fd, JSON.stringify(sources, null, 2) + '\n'); } finally { closeSync(fd); }
|
|
68
|
+
chmodSync(temp, 0o600);
|
|
69
|
+
renameSync(temp, file);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (existsSync(temp)) unlinkSync(temp);
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function commandArgv(spec: string): string[] {
|
|
77
|
+
// Shell syntax and interpreter wrappers are rejected; the allowed invocation uses execFileSync without a shell.
|
|
78
|
+
if (!spec.trim() || /["'\\`$;|&<>(){}\[\]*?\r\n]/.test(spec)) throw new Error('command spec 은 셸 구문 없이 실행 파일과 인자만 허용한다');
|
|
79
|
+
const argv = spec.trim().split(/\s+/);
|
|
80
|
+
// Allow-list, not deny-list (review R3): any other program (`nodejs scripts/x.ts`, a renamed wrapper) could emit
|
|
81
|
+
// private memos without a privacy mark. 1st edition: only the elanous CLI itself, whose intake commands mark privacy.
|
|
82
|
+
const executable = argv[0]!.split('/').pop()!.toLowerCase();
|
|
83
|
+
if (executable !== 'elanous' && executable !== 'eln') throw new Error('command 원천은 1판에서 elanous CLI(elanous·eln)만 받는다');
|
|
84
|
+
if (argv.some((arg) => /collect-telegram-saved/i.test(arg))) throw new Error('개인 메모(collect-telegram-saved)는 command 원천으로 등록할 수 없다');
|
|
85
|
+
return argv;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function addSource(input: RegisteredIntakeSource, stateDir = effectiveInstanceRoot()): RegisteredIntakeSource {
|
|
89
|
+
const seat = canonicalSeat(input.seat);
|
|
90
|
+
if (!['github-query', 'rss', 'command'].includes(input.kind)) throw new Error(`알 수 없는 원천 종류: ${input.kind}`);
|
|
91
|
+
cadenceMs(input.every);
|
|
92
|
+
if (input.until !== undefined) untilMs(input.until);
|
|
93
|
+
if (!input.id?.trim() || !/^[\w.-]+$/.test(input.id)) throw new Error('원천 id 는 영문·숫자·_·-·. 만 허용한다');
|
|
94
|
+
if (!input.spec?.trim()) throw new Error('원천 spec 이 비었다');
|
|
95
|
+
if (input.kind === 'rss') {
|
|
96
|
+
let protocol = '';
|
|
97
|
+
try { protocol = new URL(input.spec).protocol; } catch { /* validation below */ }
|
|
98
|
+
if (protocol !== 'http:' && protocol !== 'https:') throw new Error('RSS spec 은 HTTP(S) URL 이어야 한다');
|
|
99
|
+
}
|
|
100
|
+
if (input.kind === 'command') commandArgv(input.spec);
|
|
101
|
+
const sources = readSources(stateDir);
|
|
102
|
+
if (sources.some((source) => source.id === input.id)) throw new Error(`이미 등록된 원천 id: ${input.id}`);
|
|
103
|
+
const source = { ...input, seat };
|
|
104
|
+
saveSources(stateDir, [...sources, source]);
|
|
105
|
+
return source;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function listSources(filter: { seat?: string } = {}, stateDir = effectiveInstanceRoot()): RegisteredIntakeSource[] {
|
|
109
|
+
const seat = filter.seat === undefined ? undefined : canonicalSeat(filter.seat);
|
|
110
|
+
return readSources(stateDir).filter((source) => seat === undefined || source.seat === seat);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function removeSource(id: string, stateDir = effectiveInstanceRoot()): boolean {
|
|
114
|
+
const sources = readSources(stateDir);
|
|
115
|
+
if (!sources.some((source) => source.id === id)) return false;
|
|
116
|
+
saveSources(stateDir, sources.filter((source) => source.id !== id));
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function dueSources(now: Date = new Date(), stateDir = effectiveInstanceRoot()): RegisteredIntakeSource[] {
|
|
121
|
+
return listSources({}, stateDir).filter((source) => sourceIsDue(source, now));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface RunDueDeps {
|
|
125
|
+
stateDir?: string;
|
|
126
|
+
searchGithub?: SearchGithubRepos;
|
|
127
|
+
fetchFeed?: (url: string) => Promise<string>;
|
|
128
|
+
runCommand?: (command: string) => Promise<string>;
|
|
129
|
+
ingest?: typeof ingestIntakeItems;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function xmlText(value: string): string {
|
|
133
|
+
return value.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1').replace(/<[^>]+>/g, '')
|
|
134
|
+
.replace(/&(?:amp|lt|gt|quot|apos|#(\d+)|#x([a-f\d]+));/gi, (entity, dec: string | undefined, hex: string | undefined) => {
|
|
135
|
+
if (dec || hex) return String.fromCodePoint(Number.parseInt(dec ?? hex!, dec ? 10 : 16));
|
|
136
|
+
return ({ '&': '&', '<': '<', '>': '>', '"': '"', ''': "'" } as Record<string, string>)[entity.toLowerCase()] ?? entity;
|
|
137
|
+
}).trim();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function parseFeed(xml: string): RawIntakeItem[] {
|
|
141
|
+
// A login page or broken XML is a failure, not «0 items» — otherwise lastRunAt moves and the retry waits a whole cycle (review R3).
|
|
142
|
+
const head = xml.replace(/^\uFEFF/, '').replace(/<\?xml[^>]*\?>/i, '').replace(/<!--[\s\S]*?-->/g, '').trimStart();
|
|
143
|
+
if (!/^<(?:rss|feed|rdf:RDF)\b/i.test(head)) throw new Error('RSS/Atom 피드가 아니다(루트가 rss·feed·rdf:RDF 아님)');
|
|
144
|
+
const entries = xml.match(/<(?:item|entry)\b[^>]*>[\s\S]*?<\/(?:item|entry)>/gi) ?? [];
|
|
145
|
+
return entries.map((entry) => {
|
|
146
|
+
const field = (name: string): string => xmlText(entry.match(new RegExp(`<${name}\\b[^>]*>([\\s\\S]*?)<\\/${name}>`, 'i'))?.[1] ?? '');
|
|
147
|
+
const link = entry.match(/<link\b[^>]*\bhref=["']([^"']+)["'][^>]*\/?\s*>/i)?.[1] ?? field('link');
|
|
148
|
+
return { url: xmlText(link), title: field('title'), text: field('description') || field('summary') || field('content'), kind: 'rss' as const };
|
|
149
|
+
}).filter((entry) => !!entry.url);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function runDue({ now = new Date(), seat, deps = {} }: { now?: Date; seat?: string; deps?: RunDueDeps } = {}): Promise<{ ran: Array<{ id: string; items: number; error?: string }> }> {
|
|
153
|
+
const stateDir = deps.stateDir ?? effectiveInstanceRoot();
|
|
154
|
+
const selectedSeat = seat === undefined ? undefined : canonicalSeat(seat);
|
|
155
|
+
const ran: Array<{ id: string; items: number; error?: string }> = [];
|
|
156
|
+
for (const source of dueSources(now, stateDir).filter((source) => selectedSeat === undefined || source.seat === selectedSeat)) {
|
|
157
|
+
try {
|
|
158
|
+
let raws: RawIntakeItem[];
|
|
159
|
+
let githubSnapshots: { day: string; repo: string; stars: number }[] | undefined;
|
|
160
|
+
let intakeSource: IntakeSource | RegisteredIntakeSourceKind;
|
|
161
|
+
if (source.kind === 'github-query') {
|
|
162
|
+
const collected = await collectGithubStars(stateDir, deps.searchGithub ?? ghApiSearchRepos, {
|
|
163
|
+
queries: [source.spec], day: now.toISOString().slice(0, 10), dryRun: true,
|
|
164
|
+
});
|
|
165
|
+
githubSnapshots = collected.repos.map((repo) => ({ day: collected.day, repo: repo.repo, stars: repo.stars }));
|
|
166
|
+
raws = collected.repos.map((repo) => ({ url: repo.url, title: repo.title, text: repo.text, kind: 'repo',
|
|
167
|
+
signals: { stars: repo.stars, ...(repo.starsDelta === undefined ? {} : { starsDelta: repo.starsDelta }),
|
|
168
|
+
...(repo.starsPerDay === undefined ? {} : { starsPerDay: repo.starsPerDay }) } }));
|
|
169
|
+
intakeSource = 'github';
|
|
170
|
+
} else if (source.kind === 'rss') {
|
|
171
|
+
const feed = await (deps.fetchFeed ?? (async (url: string) => {
|
|
172
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(30_000) });
|
|
173
|
+
if (!response.ok) throw new Error(`RSS HTTP ${response.status}`);
|
|
174
|
+
return response.text();
|
|
175
|
+
}))(source.spec);
|
|
176
|
+
raws = parseFeed(feed);
|
|
177
|
+
intakeSource = 'rss';
|
|
178
|
+
} else {
|
|
179
|
+
const argv = commandArgv(source.spec);
|
|
180
|
+
const stdout = await (deps.runCommand ?? (async () => execFileSync(argv[0]!, argv.slice(1), { encoding: 'utf8', timeout: 600_000, maxBuffer: 16 * 1024 * 1024 })))(source.spec);
|
|
181
|
+
const parsed = parseRawIntakeJsonl(stdout);
|
|
182
|
+
if (parsed.bad) throw new Error(`잘못된 JSONL ${parsed.bad}줄`);
|
|
183
|
+
if (parsed.raws.some((raw) => (raw as RawIntakeItem & { privacy?: string; source?: string }).privacy === 'user-private'
|
|
184
|
+
|| ['memo', 'telegram-saved'].includes((raw as RawIntakeItem & { source?: string }).source ?? ''))) {
|
|
185
|
+
throw new Error('개인 메모(user-private)는 command 원천에서 받을 수 없다');
|
|
186
|
+
}
|
|
187
|
+
raws = parsed.raws;
|
|
188
|
+
intakeSource = 'command';
|
|
189
|
+
}
|
|
190
|
+
const items = (deps.ingest ?? ingestIntakeItems)(stateDir, intakeSource, raws.map((raw) => ({ ...raw, seat: source.seat })), now.toISOString()).shaped;
|
|
191
|
+
if (githubSnapshots) appendGithubStarSnapshots(stateDir, githubSnapshots);
|
|
192
|
+
// Re-read after each run so one failure cannot overwrite another source's successful timestamp.
|
|
193
|
+
const current = readSources(stateDir);
|
|
194
|
+
const found = current.find((entry) => entry.id === source.id);
|
|
195
|
+
if (found) saveSources(stateDir, current.map((entry) => entry.id === source.id ? { ...entry, lastRunAt: now.toISOString() } : entry));
|
|
196
|
+
ran.push({ id: source.id, items });
|
|
197
|
+
} catch (error) {
|
|
198
|
+
ran.push({ id: source.id, items: 0, error: error instanceof Error ? error.message : String(error) });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return { ran };
|
|
202
|
+
}
|
|
@@ -4,19 +4,22 @@ import { createHash } from 'node:crypto';
|
|
|
4
4
|
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { debug } from '../debug/log.js';
|
|
7
|
+
import { findTrack } from '../autopilot/mission-phase-track.js';
|
|
7
8
|
|
|
8
9
|
export const INTAKE_SOURCES = ['x', 'youtube', 'github', 'telegram-saved', 'telegram-bot', 'memo', 'pwa'] as const;
|
|
10
|
+
export type RegisteredIntakeSourceKind = 'rss' | 'command';
|
|
9
11
|
export type IntakeSource = typeof INTAKE_SOURCES[number];
|
|
10
|
-
export type IntakeKind = 'video' | 'repo' | 'post' | 'article' | 'note';
|
|
12
|
+
export type IntakeKind = 'video' | 'repo' | 'post' | 'article' | 'note' | 'rss';
|
|
11
13
|
export type IntakePrivacy = 'public' | 'user-private';
|
|
12
14
|
export const INTAKE_STATUSES = ['new', 'queued', 'absorbed', 'checked', 'routed', 'discarded', 'deferred'] as const;
|
|
13
15
|
export type IntakeStatus = typeof INTAKE_STATUSES[number];
|
|
14
16
|
|
|
15
17
|
export interface IntakeItem {
|
|
16
18
|
id: string;
|
|
17
|
-
source: IntakeSource;
|
|
19
|
+
source: IntakeSource | RegisteredIntakeSourceKind;
|
|
18
20
|
/** 같은 항목이 다른 입력원에서도 왔으면 여기 모인다 — «두 입력원에서 동시에 뜬 것»이 신호다. */
|
|
19
|
-
sources: IntakeSource[];
|
|
21
|
+
sources: (IntakeSource | RegisteredIntakeSourceKind)[];
|
|
22
|
+
seat?: string;
|
|
20
23
|
url?: string;
|
|
21
24
|
kind: IntakeKind;
|
|
22
25
|
title?: string;
|
|
@@ -34,6 +37,7 @@ export interface IntakeItem {
|
|
|
34
37
|
|
|
35
38
|
/** 수집기가 내는 한 줄 — 모양 맞추기 전. */
|
|
36
39
|
export interface RawIntakeItem {
|
|
40
|
+
seat?: string;
|
|
37
41
|
url?: string;
|
|
38
42
|
title?: string;
|
|
39
43
|
text?: string;
|
|
@@ -76,13 +80,23 @@ export function canonicalUrl(raw: string): string | undefined {
|
|
|
76
80
|
return `https://${host}${path}${q}`;
|
|
77
81
|
}
|
|
78
82
|
|
|
79
|
-
|
|
83
|
+
function normalizedSeat(seat: unknown): string | undefined {
|
|
84
|
+
if (seat === undefined) return undefined;
|
|
85
|
+
if (typeof seat !== 'string') throw new Error(`알 수 없는 자리: ${String(seat)}`);
|
|
86
|
+
const track = findTrack(seat.trim());
|
|
87
|
+
if (!track) throw new Error(`알 수 없는 자리: ${seat}`);
|
|
88
|
+
return track.id;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function intakeItemId(source: IntakeSource | RegisteredIntakeSourceKind, raw: Pick<RawIntakeItem, 'url' | 'text'> & { seat?: string }): string {
|
|
92
|
+
const seat = normalizedSeat(raw.seat);
|
|
80
93
|
const url = raw.url ? canonicalUrl(raw.url) : undefined;
|
|
81
94
|
const basis = url ?? `${source}:${(raw.text ?? '').trim().slice(0, 200)}`;
|
|
82
|
-
|
|
95
|
+
// Unassigned items retain their original IDs; assigned items have an independent lifecycle per seat.
|
|
96
|
+
return createHash('sha256').update(seat ? JSON.stringify([seat, basis]) : basis).digest('hex').slice(0, 16);
|
|
83
97
|
}
|
|
84
98
|
|
|
85
|
-
function kindFor(source: IntakeSource, url: string | undefined): IntakeKind {
|
|
99
|
+
function kindFor(source: IntakeSource | RegisteredIntakeSourceKind, url: string | undefined): IntakeKind {
|
|
86
100
|
if (url?.startsWith('https://www.youtube.com/')) return 'video';
|
|
87
101
|
if (url?.startsWith('https://github.com/')) return 'repo';
|
|
88
102
|
if (url?.startsWith('https://x.com/')) return 'post';
|
|
@@ -91,12 +105,13 @@ function kindFor(source: IntakeSource, url: string | undefined): IntakeKind {
|
|
|
91
105
|
}
|
|
92
106
|
|
|
93
107
|
/** 개인 메모 입력원은 user-private — 원문을 바깥 LLM 에 보내지 않는다(RFC §3.4). PWA 칸에 사람이 쓴 글도 같다(RFC-pwa-intake-front-door). */
|
|
94
|
-
export function privacyFor(source: IntakeSource): IntakePrivacy {
|
|
108
|
+
export function privacyFor(source: IntakeSource | RegisteredIntakeSourceKind): IntakePrivacy {
|
|
95
109
|
return source === 'telegram-saved' || source === 'memo' || source === 'pwa' ? 'user-private' : 'public';
|
|
96
110
|
}
|
|
97
111
|
|
|
98
112
|
/** ① 모양 맞추기 — 수집기 한 줄 → IntakeItem. 비어 있으면(URL·본문 둘 다 없음) null. */
|
|
99
|
-
export function shapeIntakeItem(source: IntakeSource, raw: RawIntakeItem, now: string): IntakeItem | null {
|
|
113
|
+
export function shapeIntakeItem(source: IntakeSource | RegisteredIntakeSourceKind, raw: RawIntakeItem, now: string): IntakeItem | null {
|
|
114
|
+
const seat = normalizedSeat(raw.seat);
|
|
100
115
|
const url = raw.url ? canonicalUrl(raw.url) : undefined;
|
|
101
116
|
const text = raw.text?.trim() || undefined;
|
|
102
117
|
if (!url && !text) return null;
|
|
@@ -104,9 +119,10 @@ export function shapeIntakeItem(source: IntakeSource, raw: RawIntakeItem, now: s
|
|
|
104
119
|
const signals: Record<string, number> = {};
|
|
105
120
|
for (const [k, v] of Object.entries(raw.signals ?? {})) if (Number.isFinite(v)) signals[`${source}.${k}`] = v;
|
|
106
121
|
return {
|
|
107
|
-
id: intakeItemId(source, { url, text }),
|
|
122
|
+
id: intakeItemId(source, { url, text, seat }),
|
|
108
123
|
source,
|
|
109
124
|
sources: [source],
|
|
125
|
+
...(seat ? { seat } : {}),
|
|
110
126
|
...(url ? { url } : {}),
|
|
111
127
|
kind: raw.kind ?? kindFor(source, url),
|
|
112
128
|
...(raw.title?.trim() ? { title: raw.title.trim().slice(0, 300) } : {}),
|
|
@@ -155,7 +171,7 @@ const SETTLED: ReadonlySet<IntakeStatus> = new Set(['absorbed', 'discarded', 'ro
|
|
|
155
171
|
* ② 중복 없애기 — 같은 id 는 새 항목을 만들지 않고 신호·입력원을 합친다.
|
|
156
172
|
* 이미 흡수·버림·갈래가 끝난 것은 «seen» 으로 세고 상태를 되돌리지 않는다(다시 올리지 않는다).
|
|
157
173
|
*/
|
|
158
|
-
export function ingestIntakeItems(instanceRoot: string, source: IntakeSource, raws: RawIntakeItem[], now = new Date().toISOString(),
|
|
174
|
+
export function ingestIntakeItems(instanceRoot: string, source: IntakeSource | RegisteredIntakeSourceKind, raws: RawIntakeItem[], now = new Date().toISOString(),
|
|
159
175
|
log: (event: string, data: Record<string, unknown>) => void = (e, d) => { debug.log('intake.normalize', e, d); }): IngestResult {
|
|
160
176
|
const { items: existing, badLines } = loadIntakeLedger(instanceRoot);
|
|
161
177
|
const result: IngestResult = { shaped: 0, added: 0, merged: 0, seen: 0, skipped: 0, badLines };
|
|
@@ -203,9 +219,9 @@ export function markIntakeItem(instanceRoot: string, id: string, patch: { status
|
|
|
203
219
|
return true;
|
|
204
220
|
}
|
|
205
221
|
|
|
206
|
-
export function listIntakeItems(instanceRoot: string, filter: { status?: IntakeStatus; source?: IntakeSource } = {}): IntakeItem[] {
|
|
222
|
+
export function listIntakeItems(instanceRoot: string, filter: { status?: IntakeStatus; source?: IntakeSource; seat?: string } = {}): IntakeItem[] {
|
|
207
223
|
return [...loadIntakeLedger(instanceRoot).items.values()]
|
|
208
|
-
.filter((i) => (!filter.status || i.status === filter.status) && (!filter.source || i.sources.includes(filter.source)))
|
|
224
|
+
.filter((i) => (!filter.status || i.status === filter.status) && (!filter.source || i.sources.includes(filter.source)) && (!filter.seat || i.seat === filter.seat))
|
|
209
225
|
.sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt));
|
|
210
226
|
}
|
|
211
227
|
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// EV12d — 계획기가 맞는 그래프를 못 고르면(«요청에 맞는 설치된 실행 그래프가 없습니다») «문서 요청 실패» 대신
|
|
2
|
+
// 그 자리가 직접 답한다: 역할 파일(내부 문서 `<id>`) ⊕ 그 자리가 owner 인 체크리스트 칸 ⊕ 최근 요청 몇 줄을
|
|
3
|
+
// 맥락으로 LLM 한 번 → 같은 채널 회신(본문). 맥락에 없는 것은 «모른다»고 답하게 한다(지어내지 않는다).
|
|
4
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { debug } from '../debug/log.js';
|
|
7
|
+
import { ExecRequestStore, type ExecRequest } from '../exec-requests/store.js';
|
|
8
|
+
import { devVersion, listChecklist, type ChecklistItem } from '../release-loop/checklist.js';
|
|
9
|
+
import { resolveSeat } from '../seat-address/seat-address.js';
|
|
10
|
+
|
|
11
|
+
const ROLE_CHARS = 6_000;
|
|
12
|
+
|
|
13
|
+
/** Cut by code points, never inside a surrogate pair. A `.slice` through an emoji (🎉 · 🟢) leaves a lone surrogate and
|
|
14
|
+
* the model API rejects the whole request («Codex API 400: Bad Request» · 10-02 07:4x — every «@CMO …» question failed). */
|
|
15
|
+
function cut(text: string, max: number): string {
|
|
16
|
+
const chars = Array.from(text);
|
|
17
|
+
return chars.length > max ? chars.slice(0, max).join('') : text;
|
|
18
|
+
}
|
|
19
|
+
const ITEM_LIMIT = 25;
|
|
20
|
+
const RECENT_LIMIT = 5;
|
|
21
|
+
|
|
22
|
+
export interface SeatAnswerDeps {
|
|
23
|
+
readRole?: (seatId: string) => string | null;
|
|
24
|
+
ownedItems?: (seatId: string) => ChecklistItem[];
|
|
25
|
+
recentRequests?: () => ExecRequest[];
|
|
26
|
+
complete?: (prompt: string) => Promise<string>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function defaultReadRole(seatId: string): string | null {
|
|
30
|
+
const path = fileURLToPath(new URL(`../../docs/roles/${seatId}.md`, import.meta.url));
|
|
31
|
+
return existsSync(path) ? readFileSync(path, 'utf8') : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function nextPatch(version: string): string | null {
|
|
35
|
+
const m = /^(\d+)\.(\d+)\.(\d+)/.exec(version);
|
|
36
|
+
return m ? `${m[1]}.${m[2]}.${Number(m[3]) + 1}` : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Items this seat owns in the dev version's checklist and the next one (where 10-28 work lives). */
|
|
40
|
+
function defaultOwnedItems(seatId: string): ChecklistItem[] {
|
|
41
|
+
const versions = [devVersion().replace(/-.*$/, '')];
|
|
42
|
+
const next = nextPatch(versions[0]!);
|
|
43
|
+
if (next) versions.push(next);
|
|
44
|
+
const items: ChecklistItem[] = [];
|
|
45
|
+
for (const version of versions) {
|
|
46
|
+
try { items.push(...listChecklist(version).items.filter((item) => item.owner === seatId)); }
|
|
47
|
+
catch { /* a version without a checklist contributes nothing */ }
|
|
48
|
+
}
|
|
49
|
+
return items;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function defaultComplete(prompt: string): Promise<string> {
|
|
53
|
+
const { streamLLM } = await import('../llm.js');
|
|
54
|
+
const { tierModel } = await import('../llm/model-defaults.js');
|
|
55
|
+
return streamLLM([{ role: 'user', content: prompt }], () => {}, { model: tierModel('better') });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const STATUS_MARK: Record<string, string> = { green: '🟢', yellow: '🟡', red: '🔴', done: '✅' };
|
|
59
|
+
|
|
60
|
+
export function seatAnswerPrompt(input: { title: string; seatId: string; question: string; role: string | null; items: ChecklistItem[]; recent: ExecRequest[] }): string {
|
|
61
|
+
const items = input.items.slice(0, ITEM_LIMIT)
|
|
62
|
+
.map((item) => `${STATUS_MARK[item.status] ?? item.status} ${item.id} ${cut(item.title, 140)}${item.evidence ? ` — 근거: ${cut(item.evidence, 160)}` : ''}`)
|
|
63
|
+
.join('\n') || '(담당 칸 없음)';
|
|
64
|
+
const recent = input.recent.slice(0, RECENT_LIMIT)
|
|
65
|
+
.map((request) => `${request.createdAt.slice(0, 16)} ${request.status} · ${cut(request.text.replace(/\s+/g, ' '), 120)}`)
|
|
66
|
+
.join('\n') || '(최근 요청 없음)';
|
|
67
|
+
return [
|
|
68
|
+
`너는 엘라누스 팀의 ${input.title}(${input.seatId}) 자리다. 아래 «역할 문서 · 담당 칸 · 최근 요청»만 근거로 질문에 답한다.`,
|
|
69
|
+
'규칙: 한국어 존댓말 · 12줄 이내 · 맥락에 없는 사실·수치·일정은 지어내지 말고 «모른다/확인 필요»라고 쓴다 · 담당 칸을 인용할 땐 칸 ID 를 붙인다 · 내부 파일 경로·토큰·계정은 쓰지 않는다.',
|
|
70
|
+
`\n## 역할 문서\n${cut(input.role ?? '(역할 문서 없음)', ROLE_CHARS)}`,
|
|
71
|
+
`\n## 담당 칸(체크리스트 owner=${input.seatId})\n${items}`,
|
|
72
|
+
`\n## 최근 자리 요청\n${recent}`,
|
|
73
|
+
`\n## 질문\n${input.question}`,
|
|
74
|
+
].join('\n');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The seat's own answer, or null when the seat is unknown or the model gave nothing usable. */
|
|
78
|
+
export async function answerAsSeat(seatAddress: string, question: string, deps: SeatAnswerDeps = {}): Promise<{ title: string; text: string } | null> {
|
|
79
|
+
const seat = resolveSeat(seatAddress);
|
|
80
|
+
if (!seat?.title) return null;
|
|
81
|
+
const role = (deps.readRole ?? defaultReadRole)(seat.id);
|
|
82
|
+
const items = (deps.ownedItems ?? defaultOwnedItems)(seat.id);
|
|
83
|
+
const recent = (deps.recentRequests ?? (() => new ExecRequestStore().list()))();
|
|
84
|
+
const raw = await (deps.complete ?? defaultComplete)(seatAnswerPrompt({ title: seat.title, seatId: seat.id, question, role, items, recent }));
|
|
85
|
+
const text = raw.trim();
|
|
86
|
+
debug.log('intake.seat-doc', 'seat-answer', { seat: seat.id, roleFile: role !== null, items: items.length, recent: Math.min(recent.length, RECENT_LIMIT), chars: text.length });
|
|
87
|
+
return text ? { title: seat.title, text: cut(text, 3_500) } : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** «No installed graph fits» — the planner's own failure wording, so the seat should answer instead. */
|
|
91
|
+
export function isNoGraphFailure(result: { status: string; summary?: string; seats?: Array<{ graphId: string; reason?: string }> } | null): boolean {
|
|
92
|
+
if (!result || result.status !== 'failed' || !result.seats?.length) return false;
|
|
93
|
+
if (!result.seats.every((seat) => !seat.graphId)) return false;
|
|
94
|
+
const NO_GRAPH = /설치된 실행 그래프가 없습니다/;
|
|
95
|
+
return NO_GRAPH.test(result.summary ?? '') || result.seats.some((seat) => NO_GRAPH.test(seat.reason ?? ''));
|
|
96
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import type { MissionOrigin } from '../autopilot/mission-origin.js';
|
|
6
|
+
import { debug } from '../debug/log.js';
|
|
7
|
+
import { sendOutbound } from '../domains/outbound-alert.js';
|
|
8
|
+
import { defaultSpillUpload } from '../storage/content-spill.js';
|
|
9
|
+
import { getElanousConfigDir } from '../elanous-config-dir.js';
|
|
10
|
+
import type { ExecRequest } from '../exec-requests/store.js';
|
|
11
|
+
import { resolveDaemonEndpoint } from '../nexus/daemon-endpoint.js';
|
|
12
|
+
import { parseSeatAddress, resolveSeat } from '../seat-address/seat-address.js';
|
|
13
|
+
import { answerAsSeat, isNoGraphFailure } from './seat-answer.js';
|
|
14
|
+
|
|
15
|
+
const POLL_MS = 5_000;
|
|
16
|
+
const TIMEOUT_MS = 30 * 60_000;
|
|
17
|
+
const DOC = /전략|한\s*장|원페이저|기획|보도자료|글|메모|초안|\b(?:one-pager|strategy|post|memo|draft)\b/i;
|
|
18
|
+
const CODE = /구현|버그|고쳐|\b(?:fix|implement|PR)\b/i;
|
|
19
|
+
const RESEARCH = /전략|시장|경쟁|\bstrategy\b/i;
|
|
20
|
+
/** EV12a — a question or a request for status / a report is answered, never turned into a code goal. */
|
|
21
|
+
const ASK = /상황|현황|진행|준비|어때|어떻게|알려|보고|정리|요약|공유|\?|?|\b(?:status|update|report|summary|summari[sz]e|how|what)\b/i;
|
|
22
|
+
/** Only an explicit build/fix verb makes a question a code request («버그 현황 알려줘» is still a report). */
|
|
23
|
+
const CODE_ACTION = /구현|고쳐|수정해|만들어\s*줘|\b(?:fix|implement)\b/i;
|
|
24
|
+
|
|
25
|
+
type ExecSnapshot = Pick<ExecRequest, 'status' | 'summary' | 'results'> & { seats?: ExecRequest['seats'] };
|
|
26
|
+
type Source = { title: string; url: string };
|
|
27
|
+
|
|
28
|
+
export interface SeatDocDeps {
|
|
29
|
+
research?: (body: string) => Promise<Source[]>;
|
|
30
|
+
submitExec?: (text: string) => Promise<{ id: string }>;
|
|
31
|
+
getExec?: (id: string) => Promise<ExecSnapshot | null>;
|
|
32
|
+
getFile?: (path: string) => Promise<string>;
|
|
33
|
+
publishFile?: (content: string, key: string) => string | null;
|
|
34
|
+
sendOutbound?: typeof sendOutbound;
|
|
35
|
+
sleep?: (ms: number) => Promise<void>;
|
|
36
|
+
now?: () => number;
|
|
37
|
+
pwaUrl?: () => string | undefined;
|
|
38
|
+
log?: (event: 'submitted' | 'research' | 'settled' | 'delivered' | 'delivery-failed' | 'seat-answer', data: Record<string, unknown>) => void;
|
|
39
|
+
/** EV12d — the seat's own answer when no installed graph fits (default = role file ⊕ owned checklist ⊕ recent, one LLM call). */
|
|
40
|
+
seatAnswer?: (seatAddress: string, question: string) => Promise<{ title: string; text: string } | null>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function isSeatDocRequest(text: string): boolean {
|
|
44
|
+
const addressed = parseSeatAddress(text);
|
|
45
|
+
if (!addressed || !addressed.seats.every(seat => !!resolveSeat(seat))) return false;
|
|
46
|
+
if (DOC.test(addressed.body) && !CODE.test(addressed.body)) return true;
|
|
47
|
+
return ASK.test(addressed.body) && !CODE_ACTION.test(addressed.body);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** CO1 — anything addressed to the COO seat that is not a code request goes to the planner, which splits it across
|
|
51
|
+
* seats (the A5 exec-request path), instead of becoming a code goal. */
|
|
52
|
+
export function isCooPlannerRequest(text: string): boolean {
|
|
53
|
+
const addressed = parseSeatAddress(text);
|
|
54
|
+
return !!addressed && addressed.seats.some(seat => resolveSeat(seat)?.title === 'COO')
|
|
55
|
+
&& addressed.seats.every(seat => !!resolveSeat(seat)) && !!addressed.body.trim() && !CODE.test(addressed.body);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function research(body: string): Promise<Source[]> {
|
|
59
|
+
const cli = fileURLToPath(new URL('../../bin/elanous.mjs', import.meta.url));
|
|
60
|
+
const child = Bun.spawn([process.execPath, cli, '--test', 'research', '--json', '--limit', '5', body], {
|
|
61
|
+
stdout: 'pipe', stderr: 'ignore',
|
|
62
|
+
});
|
|
63
|
+
const [out, code] = await Promise.all([new Response(child.stdout).text(), child.exited]);
|
|
64
|
+
if (code !== 0) throw new Error(`research exit ${code}`);
|
|
65
|
+
const payload = JSON.parse(out) as { output?: unknown; metadata?: { totalHits?: number; perEngine?: Record<string, { error?: string }> } };
|
|
66
|
+
if (typeof payload.output !== 'string') throw new Error('research JSON output missing');
|
|
67
|
+
const sources = [...payload.output.matchAll(/^- \[([^\]\n]+)\]\((https?:\/\/[^\s)]+)\)/gm)]
|
|
68
|
+
.slice(0, 5).map(([, title, url]) => ({ title: title!, url: url! }));
|
|
69
|
+
if (!sources.length && (!payload.metadata?.totalHits || Object.values(payload.metadata.perEngine ?? {}).some(engine => engine.error))) {
|
|
70
|
+
throw new Error('research unavailable');
|
|
71
|
+
}
|
|
72
|
+
return sources;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function daemonUrl(): string {
|
|
76
|
+
const base = resolveDaemonEndpoint({ purpose: 'write' })?.baseUrl;
|
|
77
|
+
if (!base) throw new Error('실행 요청 데몬을 찾을 수 없습니다');
|
|
78
|
+
return base;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function execResponse(path: string, text?: string): Promise<Response> {
|
|
82
|
+
const token = readFileSync(join(getElanousConfigDir(), 'acp-token'), 'utf8').trim();
|
|
83
|
+
const response = await fetch(`${daemonUrl()}/v1/exec-requests${path}`, {
|
|
84
|
+
method: text === undefined ? 'GET' : 'POST',
|
|
85
|
+
headers: { Authorization: `Bearer ${token}`, ...(text === undefined ? {} : { 'Content-Type': 'application/json' }) },
|
|
86
|
+
...(text === undefined ? {} : { body: JSON.stringify({ text }) }),
|
|
87
|
+
});
|
|
88
|
+
if (!response.ok) throw new Error(`실행 요청 HTTP ${response.status}`);
|
|
89
|
+
return response;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function execApi(path: string, text?: string): Promise<ExecRequest> {
|
|
93
|
+
return await (await execResponse(path, text)).json() as ExecRequest;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function execFile(path: string): Promise<string> {
|
|
97
|
+
return (await execResponse(path.slice('/v1/exec-requests'.length))).text();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function chatPwaUrl(): string | undefined {
|
|
101
|
+
const endpoint = resolveDaemonEndpoint({ purpose: 'write' });
|
|
102
|
+
return endpoint?.pwaUrl;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function accessiblePwaUrl(value: string | undefined): string | undefined {
|
|
106
|
+
if (!value) return undefined;
|
|
107
|
+
try {
|
|
108
|
+
const url = new URL(value);
|
|
109
|
+
// The owner opens the authenticated PWA, not the bearer-only /v1/exec-requests/files endpoint.
|
|
110
|
+
// A loopback address works on the daemon host, not on the sender's phone.
|
|
111
|
+
if (url.protocol !== 'https:' || !url.hostname || url.hostname === 'localhost'
|
|
112
|
+
|| url.hostname === '127.0.0.1' || url.hostname === '0.0.0.0'
|
|
113
|
+
|| url.username || url.password || url.pathname !== '/app/' || url.search || url.hash) return undefined;
|
|
114
|
+
return url.toString();
|
|
115
|
+
} catch { return undefined; }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function fileLinks(id: string, result: ExecSnapshot, deps: SeatDocDeps): Promise<Map<string, string>> {
|
|
119
|
+
const links = new Map<string, string>();
|
|
120
|
+
for (const entry of result.results) {
|
|
121
|
+
if (entry.kind !== 'report' && entry.kind !== 'text') continue;
|
|
122
|
+
if (!entry.url.startsWith(`/v1/exec-requests/${encodeURIComponent(id)}/files/`)) continue;
|
|
123
|
+
try {
|
|
124
|
+
// The authenticated daemon route is the only source of bytes; a raw file path is never trusted.
|
|
125
|
+
const content = await (deps.getFile ?? execFile)(entry.url);
|
|
126
|
+
const ext = entry.kind === 'report' ? 'md' : 'txt';
|
|
127
|
+
const digest = createHash('sha256').update(content).digest('hex');
|
|
128
|
+
const key = `seat-doc/${encodeURIComponent(id)}/${digest}.${ext}`;
|
|
129
|
+
const url = (deps.publishFile ?? ((value, path) => defaultSpillUpload(value, path, ext)))(content, key);
|
|
130
|
+
if (url && /^https:\/\/[^/]+/.test(url)) links.set(entry.url, url);
|
|
131
|
+
else links.set(entry.url, `\n${content}`);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
debug.log('intake.seat-doc', 'file-link-failed', { id, reason: String(error).slice(0, 200) });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return links;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function message(id: string, result: ExecSnapshot | null, withoutResearch: boolean, lookupAvailable: boolean, pwaUrl?: string, links = new Map<string, string>()): string {
|
|
140
|
+
const suffix = withoutResearch ? '\n조사 없이 진행했습니다.' : '';
|
|
141
|
+
const location = pwaUrl ? `PWA 에서 확인: ${pwaUrl} (접수번호 ${id})` : `PWA 에서 확인 (접수번호 ${id} · 외부 접속 주소 없음)`;
|
|
142
|
+
if (!result) return `${lookupAvailable ? '아직 도는 중' : '상태를 확인할 수 없어 확인을 종료했습니다'} · ${location}${suffix}`;
|
|
143
|
+
if (result.status === 'failed') {
|
|
144
|
+
const reason = result.summary || result.seats?.find(seat => seat.reason)?.reason || '실행 요청 실패';
|
|
145
|
+
return `문서 요청 실패 — ${reason.split('\n')[0]!.slice(0, 200)} · ${location}${suffix}`;
|
|
146
|
+
}
|
|
147
|
+
const summary = result.summary?.split('\n')[0]?.slice(0, 200) || '문서 요청 완료';
|
|
148
|
+
// File endpoints require an Authorization header even when opened from the same origin.
|
|
149
|
+
// Only send externally usable links for genuinely external artifacts; file names remain findable in the PWA.
|
|
150
|
+
const artifacts = result.results.map(entry => `${entry.title} (${entry.kind})${links.has(entry.url) ? ` · ${links.get(entry.url)}` : entry.kind === 'link' && /^https?:\/\//i.test(entry.url) ? ` · ${entry.url}` : ''}`);
|
|
151
|
+
return `${summary}\n${artifacts.join('\n')}${artifacts.length ? '\n' : ''}${location}${suffix}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function submitSeatDocRequest({ text, reportTo, deps = {} }: {
|
|
155
|
+
text: string; reportTo?: MissionOrigin; deps?: SeatDocDeps;
|
|
156
|
+
}): Promise<{ id: string }> {
|
|
157
|
+
const log = deps.log ?? ((event: Parameters<NonNullable<SeatDocDeps['log']>>[0], data: Record<string, unknown>) => debug.log('intake.seat-doc', event, data));
|
|
158
|
+
const body = parseSeatAddress(text)?.body.trim() ?? text.trim();
|
|
159
|
+
let withoutResearch = false;
|
|
160
|
+
let requestText = text;
|
|
161
|
+
if (RESEARCH.test(body)) {
|
|
162
|
+
try {
|
|
163
|
+
const sources = (await (deps.research ?? research)(body)).slice(0, 5);
|
|
164
|
+
if (sources.length) requestText += `\n\n참고 자료\n${sources.map(source => `${source.title} · ${source.url}`).join('\n')}`;
|
|
165
|
+
log('research', { count: sources.length });
|
|
166
|
+
} catch (error) {
|
|
167
|
+
withoutResearch = true;
|
|
168
|
+
log('research', { failed: true, reason: String(error).slice(0, 200) });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const { id } = await (deps.submitExec ?? ((value) => execApi('', value)))(requestText);
|
|
172
|
+
if (!id) throw new Error('실행 요청 접수번호가 없습니다');
|
|
173
|
+
log('submitted', { id });
|
|
174
|
+
|
|
175
|
+
const watch = async () => {
|
|
176
|
+
const now = deps.now ?? Date.now;
|
|
177
|
+
const sleep = deps.sleep ?? ((ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms)));
|
|
178
|
+
const deadline = now() + TIMEOUT_MS;
|
|
179
|
+
let result: ExecSnapshot | null = null;
|
|
180
|
+
let lookupAvailable = false;
|
|
181
|
+
let lastConfirmedStatus: ExecSnapshot['status'] | undefined;
|
|
182
|
+
while (now() < deadline) {
|
|
183
|
+
try {
|
|
184
|
+
const snapshot = await (deps.getExec ?? ((key) => execApi(`/${encodeURIComponent(key)}`)))(id);
|
|
185
|
+
if (!snapshot) throw new Error('실행 요청을 찾을 수 없습니다');
|
|
186
|
+
lookupAvailable = true;
|
|
187
|
+
lastConfirmedStatus = snapshot.status;
|
|
188
|
+
if (snapshot.status === 'done' || snapshot.status === 'failed') {
|
|
189
|
+
result = snapshot;
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
} catch (error) {
|
|
193
|
+
lookupAvailable = false;
|
|
194
|
+
debug.log('intake.seat-doc', 'poll-error', { id, reason: String(error).slice(0, 200) });
|
|
195
|
+
}
|
|
196
|
+
if (now() < deadline) await sleep(Math.min(POLL_MS, Math.max(0, deadline - now())));
|
|
197
|
+
}
|
|
198
|
+
log('settled', { id, status: result?.status ?? (lookupAvailable ? 'timeout' : 'lookup-unavailable'), lastConfirmedStatus });
|
|
199
|
+
const links = result?.status === 'done' ? await fileLinks(id, result, deps) : new Map<string, string>();
|
|
200
|
+
let reply = message(id, result, withoutResearch, lookupAvailable, accessiblePwaUrl((deps.pwaUrl ?? chatPwaUrl)()), links);
|
|
201
|
+
if (isNoGraphFailure(result)) {
|
|
202
|
+
// EV12d — a question with no fitting graph gets the seat's own answer, not «문서 요청 실패».
|
|
203
|
+
const seat = result!.seats![0]!.seat;
|
|
204
|
+
try {
|
|
205
|
+
const answer = await (deps.seatAnswer ?? answerAsSeat)(seat, body);
|
|
206
|
+
if (answer) reply = `${answer.title} 답 (접수번호 ${id})\n${answer.text}`;
|
|
207
|
+
log('seat-answer', { id, seat, answered: !!answer });
|
|
208
|
+
} catch (error) {
|
|
209
|
+
log('seat-answer', { id, seat, answered: false, reason: String(error).slice(0, 200) });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
const sent = await (deps.sendOutbound ?? sendOutbound)(reply, 'report', reportTo);
|
|
214
|
+
log(sent ? 'delivered' : 'delivery-failed', { id });
|
|
215
|
+
} catch (error) {
|
|
216
|
+
log('delivery-failed', { id, reason: String(error).slice(0, 200) });
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
void watch();
|
|
220
|
+
return { id };
|
|
221
|
+
}
|