elanous 0.2.5 → 0.2.7
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 +9 -4
- 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/{5391-8793f88b03b30161.js → 2375-cb6debea56cce4fb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{1751.50b96ea9c37b49b6.js → 2415.ed6428ff334c7ce2.js} +1 -1
- 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/{4daa2798-1363d725c79f3593.js → 2ea96958-e8c7e112de50b067.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +5 -0
- 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/{8617.465ca22a319c95b3.js → 4323.c5dfb6f503304775.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/4836-2eba6079f31b88d4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8918-482b30ce2a8ebf4c.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/{5348-bbf838e119661eee.js → 5791-bb82743fa6299562.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{9704-3221c102623c8d73.js → 6062-73e68343a9814f6a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{4440-18bb160d95819f4e.js → 6090-85a3f05f954e5697.js} +1 -1
- 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/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/{f6f3850d-7f324c6566002113.js → 8046cadf-211ca3c0e00f2b72.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/8067-f74bf14d67ad98f1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8519-f6b5d8a5833a7603.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8894-e92491a164192cc1.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-67051141115ce775.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.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-d5ebde04844689e6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-f89e9a303515a033.js → page-10ed24f413ed66e9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/{page-4f403cd060442c95.js → page-211810b6c93ab66a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-0ac5d053f0444d21.js → page-624f7a2f89b60c8f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-5a5fb385a4df0ffe.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-80497e93a878ddc5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-7457f52683562161.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-cee86394de407aa6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-4ff8edab44196b8c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-92e4f44428d5e290.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/page-b7dcf5d48a854b76.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-51480757e7d68b92.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-2a05dfe80c8bdf16.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-d9979633aa82f8e6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-30533117beb53625.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-f7857826c2f97ad7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-07267f1391b96aba.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-bccce0babceae30f.js → page-55d50e9b0006a91d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-9699b1786be6171d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-43dd772aee8af75f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-a7e37af7d0399cb8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-973ab7af59c718b7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a9f8d3865b91e436.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-15e5a15db84c56aa.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-a2e572e4511f2a16.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-dd107bf1c7d52a90.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-d42b99cdfb9f27c6.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-2764ff40dad5a995.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-c5f1152db116ff96.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-f933bca81ea38a6f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-c1c95739c0bff3b8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-a7aaffcf771662d8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-71d18eec33b9c667.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-56adbe1a1f1379bd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-2c4e59079ee357ac.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-a81f113f3ef89d72.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-5d422f38c052967e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-fc27b00e20733146.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-2bf59c720fe9542c.js → webpack-e5bea36f183b25e6.js} +1 -1
- package/apps/pwa/out/_next/static/css/f537bdb19c69306a.css +1 -0
- package/apps/pwa/out/_next/static/keajW7DDyghEs1y8oJe6T/_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/icons/elanous-16.png +0 -0
- package/apps/pwa/out/icons/elanous-180.png +0 -0
- package/apps/pwa/out/icons/elanous-192.png +0 -0
- package/apps/pwa/out/icons/elanous-32.png +0 -0
- package/apps/pwa/out/icons/elanous-512.png +0 -0
- 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 +13 -0
- package/graphs/release/release-loop.yaml +13 -4
- package/package.json +5 -5
- package/plugins/card-followup/README.md +42 -0
- package/plugins/card-followup/examples/card-sample.json +7 -0
- package/plugins/card-followup/graphs/card-followup.yaml +18 -0
- package/plugins/card-followup/graphs/recipes.yaml +16 -0
- package/plugins/card-followup/graphs/run-step.ts +276 -0
- package/plugins/card-followup/graphs/sales-strategy.ts +173 -0
- package/plugins/card-followup/plugin.json +25 -0
- 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 +15 -0
- package/plugins/geo-check/examples/brand-sample.json +8 -0
- package/plugins/geo-check/graphs/geo-check.yaml +20 -0
- package/plugins/geo-check/graphs/recipes.yaml +18 -0
- package/plugins/geo-check/graphs/run-step.ts +198 -0
- package/plugins/geo-check/plugin.json +24 -0
- package/plugins/job-coach/.codex-plugin/plugin.json +1 -1
- package/plugins/job-coach/README.md +31 -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/daemon-port-baseline.txt +1 -2
- package/scripts/field-feed/deliver-node.ts +52 -0
- package/scripts/field-feed/feed-node.ts +67 -0
- package/scripts/field-feed/lib.ts +162 -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 +92 -13
- package/scripts/landing-heal/clean-checkout.ts +114 -0
- package/scripts/landing-heal/run-mission.ts +38 -3
- package/scripts/lib/android-test-device.ts +32 -0
- package/scripts/lib/pwa-scenarios/scenarios.ts +2 -1
- 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 +35 -0
- package/scripts/release-loop/env-known-failures.ts +31 -0
- package/scripts/release-loop/gate-diff.ts +29 -0
- package/scripts/release-loop/gate-node.ts +124 -29
- package/scripts/release-loop/known-issues-node.ts +159 -0
- package/scripts/release-loop/npm-publish-node.ts +118 -0
- package/scripts/release-loop/ops-upgrade-node.ts +87 -0
- package/scripts/release-loop/release-readiness.ts +58 -0
- package/scripts/release-loop/shard-plan.ts +47 -0
- package/scripts/release-loop/unattended-release.ts +93 -0
- package/scripts/ux/measure-first-text.ts +102 -0
- package/scripts/ux/record-page.ts +4 -1
- 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 +24 -3
- package/skills/explainer-video/engine/brand/elanous-mark-on-dark.svg +1 -1
- package/skills/explainer-video/engine/build.mjs +2 -2
- package/skills/explainer-video/engine/reel.mjs +235 -0
- package/skills/explainer-video/engine/reel.sh +9 -0
- package/skills/explainer-video/engine/vo.ts +58 -16
- 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/elanous-aware-peers.ts +30 -0
- package/src/acp/server.ts +91 -15
- 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/mission-origin.ts +20 -4
- package/src/autopilot/mission-phase-track.ts +11 -1
- package/src/autopilot/track-agent-shadow.ts +151 -0
- package/src/autopilot/track-agent.ts +26 -6
- package/src/autopilot/track-courier.ts +2 -2
- 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/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/dependence-gauge.ts +237 -0
- package/src/cli/doctor-cli.ts +5 -0
- package/src/cli/doctor-modes.ts +48 -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/msg-cli.ts +137 -0
- package/src/cli/nexus-ios-bind.ts +25 -8
- package/src/cli/phone-link.ts +117 -0
- package/src/cli/plugin-cli.ts +52 -0
- package/src/cli/pr-cli.ts +36 -0
- package/src/cli/private-ledger-commands.ts +31 -0
- package/src/cli/provider-cli.ts +5 -0
- package/src/cli/release-cli.ts +56 -12
- 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/usage-cli.ts +11 -1
- package/src/codex/setup.ts +27 -8
- package/src/dashboard/index.ts +1 -1
- package/src/discord.ts +56 -6
- package/src/domains/core-tools.ts +37 -14
- package/src/domains/outbound-alert.ts +55 -16
- package/src/exec-requests/planner.ts +140 -0
- package/src/exec-requests/runner.ts +338 -0
- package/src/exec-requests/store.ts +76 -0
- package/src/field/field-media.ts +250 -0
- package/src/field/field-reel.ts +196 -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/index.ts +104 -38
- 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-doc-route.ts +207 -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 +117 -0
- package/src/nexus/api/discord-trigger-bot.ts +3 -0
- package/src/nexus/api/exec-requests.ts +51 -0
- package/src/nexus/api/field-uploads.ts +180 -0
- package/src/nexus/api/graph-approvals-feed.ts +140 -0
- package/src/nexus/api/graph-approvals.ts +120 -4
- package/src/nexus/api/harness-api.ts +219 -5
- package/src/nexus/api/http-server.ts +68 -5
- package/src/nexus/api/seat-requests.ts +254 -0
- package/src/nexus/api/trace-run-scan.ts +37 -0
- package/src/nexus/api/trace.ts +19 -3
- package/src/nexus/index.ts +3 -0
- package/src/nexus/setup-status.ts +4 -5
- package/src/notify/llm-budget-alert.ts +125 -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.ts +4 -1
- package/src/oauth/fallback-chain.ts +5 -5
- package/src/oauth/store.ts +42 -5
- package/src/onboarding/continue-here.ts +29 -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 +83 -0
- package/src/onboarding.ts +230 -23
- 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 +220 -0
- 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/channel.ts +184 -0
- package/src/relay/mux.ts +182 -0
- package/src/relay/relay-cli.ts +20 -0
- package/src/relay/relay-server.ts +220 -0
- package/src/relay/test-vectors.json +29 -0
- package/src/release-loop/checklist.ts +36 -9
- package/src/seat-address/seat-address.ts +34 -0
- package/src/self-dev/decompose.ts +1 -0
- package/src/self-dev/dev-pipeline.ts +6 -3
- 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/self-implement/run-ledger-compact.ts +1 -1
- package/src/session/chat.ts +17 -5
- package/src/session/index.ts +23 -10
- package/src/skills/shared-agent-skill-roots.ts +25 -0
- 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-bun-cache.ts +18 -0
- package/src/task-orchestrator/surfaces/pod-command-job.ts +12 -3
- 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.ts +145 -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 +84 -4
- package/src/version/packed-revision.json +1 -1
- package/src/video-pipeline/recipes/character.ts +42 -5
- package/src/video-pipeline/recipes/explainer.ts +213 -0
- 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/src/video-pipeline/walk-line.ts +3 -2
- package/apps/pwa/out/_next/static/chunks/06ef2eba-d1b99ac6d2ea663c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1413-7179a49332db5a80.js +0 -1
- package/apps/pwa/out/_next/static/chunks/150-25c277137a9638ab.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1640-bc99ec23bbe29d12.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2300-de059d0ab2a16630.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2684-05b0a81e75298c16.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3026-2ed7e317d9b02102.js +0 -25
- package/apps/pwa/out/_next/static/chunks/3611-77496ada38cd6358.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3717-68957c4dc9ceec90.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3929-46ba9af6f9d895cc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4015-62c5a3744698a8da.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4217-3c4317a7905f9eeb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4240-3230f230fae2d7d5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4271-e53e075ebd48593b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4377.3b125fef79576dfd.js +0 -5
- package/apps/pwa/out/_next/static/chunks/4853-35fdbe6bdbcdc0fc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5007-6a3b30e23ff68a6e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5244-12b424c42070f5ff.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5611-df00af3e4e4a61bb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5890-dca2321773be829a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6123-37c9dc57ddaa7ab0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6588-b7975304a302ab91.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6604-c03961962b6273b6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7109-cf8c87e968e83a65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7178-30f77877525a2efb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7262-871563fc9663730b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7294-f0eeebb92aa93459.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7429-881af44deed6958f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7527.943753b6ed829f09.js +0 -1
- package/apps/pwa/out/_next/static/chunks/764-a01e858525a190c0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7868-e35d5f7985ecd3f1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8320-51484af7b01fdfa5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9131-52f5fad51cceca04.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9238.2cd5f47f4bf7a2c3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-70c404c7bb6d7f0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-3ffd80166b1050aa.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-4bcc63e272ae8205.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-880734bd07d62882.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-72bd384898d88955.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-1018bbc9586ecae5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-de86f8c38076dd7b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-70c404c7bb6d7f0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-cd6447196d8bafe6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-e733630c47fa102b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-d1c1a8357b161875.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-60c514128e81d787.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-8b637b7e325e13c1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-63301d11dbe73836.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-a8cf0dc472060db2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-02fba54881ccfa49.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-d5e8e90f0038f8de.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-b1ef6824f494466a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-d589bb0f233b1009.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-c9bbaebef4cda059.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-3639ce7916b92fed.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-7450ae7478955653.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-70c404c7bb6d7f0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-b41ca4abfd618452.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-b333dd79ebee3fa9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-8be1227492793655.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-6e03e9371b552e0d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-911411e9fca18d03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-792170e649b37088.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-c40463466937bc91.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-509c798b80384536.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-1cac2aa21d77c417.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-9626799d9b4a2cd1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-84cd419494e16b0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/framework-361487d2da603917.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-607f5beb94cb2e07.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-d0222d219e29dddf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_app-a26980a31126decc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_error-edb2b37decf23e4d.js +0 -1
- package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +0 -1
- package/apps/pwa/out/_next/static/ktMqfrBbf8BMGac0jqLrb/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{ktMqfrBbf8BMGac0jqLrb → keajW7DDyghEs1y8oJe6T}/_ssgManifest.js +0 -0
|
@@ -11,6 +11,7 @@ import { getUserConfig } from '../../user-config.js';
|
|
|
11
11
|
import { debug } from '../../debug/log.js';
|
|
12
12
|
import { effectiveInstanceRoot } from '../../instance/resolve.js';
|
|
13
13
|
import { collectPodArtifacts } from './pod-artifact-return.js';
|
|
14
|
+
import { podBunCacheVolume } from './pod-bun-cache.js';
|
|
14
15
|
import { type PodPoolMember, PodPoolScheduler, resolvePodPoolSpec, parsePodPool, checkPodPool, syncPoolImages, type PoolKubectl } from './pod-pool.js';
|
|
15
16
|
import { readSkillEnvFiles } from './pod-skills.js';
|
|
16
17
|
import { podSourceScript, type PodSource } from './pod-source-receive.js';
|
|
@@ -76,6 +77,7 @@ export interface PodCommandJobInput {
|
|
|
76
77
|
/** 저장소를 clone 한다 — 비공개 저장소라 GitHub 토큰이 Secret 으로 간다(명시 opt-in). 기본은 clone 없이 `~/work` 에서 이미지의 `elanous` 로 돈다. */
|
|
77
78
|
clone?: boolean;
|
|
78
79
|
hostMirror?: string;
|
|
80
|
+
bunCache?: string;
|
|
79
81
|
/** 컨테이너 메모리 한도(기본 16Gi) — 게이트가 파일 하나 격리 Job 에 올린다. */
|
|
80
82
|
memoryLimit?: string;
|
|
81
83
|
source?: PodSource;
|
|
@@ -94,6 +96,7 @@ export function podCommandJobManifest(o: PodCommandJobInput): Record<string, unk
|
|
|
94
96
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(name)) throw new Error(`pod command skill name rejected: ${name}`);
|
|
95
97
|
}
|
|
96
98
|
const grok = o.llm === 'grok';
|
|
99
|
+
const bunCache = o.bunCache ? podBunCacheVolume(o.bunCache) : undefined;
|
|
97
100
|
const quotedArgs = o.command.map(bashSingleQuote).join(' ');
|
|
98
101
|
const credLines = [
|
|
99
102
|
...(grok ? ['mkdir -p ~/.grok && install -m 600 /creds/grok-auth.json ~/.grok/auth.json'] : []),
|
|
@@ -108,6 +111,7 @@ export function podCommandJobManifest(o: PodCommandJobInput): Record<string, unk
|
|
|
108
111
|
...(o.clone
|
|
109
112
|
? ['export GH_TOKEN="$(cat /creds/gh-token)" && gh auth setup-git', podSourceScript(o.source ?? { kind: 'default' }, o.repoUrl)]
|
|
110
113
|
: ['mkdir -p ~/work && cd ~/work']),
|
|
114
|
+
...(bunCache ? [bunCache.shellPrefix] : []),
|
|
111
115
|
`set -- ${quotedArgs}`,
|
|
112
116
|
'"$@"',
|
|
113
117
|
'rc=$?',
|
|
@@ -138,14 +142,16 @@ export function podCommandJobManifest(o: PodCommandJobInput): Record<string, unk
|
|
|
138
142
|
{ name: 'ELANOUS_SUBSTRATE', value: 'pod' },
|
|
139
143
|
{ name: 'ELANOUS_POD_NAME', valueFrom: { fieldRef: { fieldPath: 'metadata.name' } } },
|
|
140
144
|
],
|
|
141
|
-
...(secretKeys || o.hostMirror ? { volumeMounts: [
|
|
145
|
+
...(secretKeys || o.hostMirror || bunCache ? { volumeMounts: [
|
|
142
146
|
...(secretKeys ? [{ name: 'creds', mountPath: '/creds', readOnly: true }] : []),
|
|
143
147
|
...(o.hostMirror ? [{ name: 'host-mirror', mountPath: '/host-mirror', readOnly: true }] : []),
|
|
148
|
+
...(bunCache ? [bunCache.volumeMount] : []),
|
|
144
149
|
] } : {}),
|
|
145
150
|
}],
|
|
146
|
-
...(secretKeys || o.hostMirror ? { volumes: [
|
|
151
|
+
...(secretKeys || o.hostMirror || bunCache ? { volumes: [
|
|
147
152
|
...(secretKeys ? [{ name: 'creds', secret: { secretName: `${o.name}-creds`, defaultMode: 0o400 } }] : []),
|
|
148
153
|
...(o.hostMirror ? [{ name: 'host-mirror', hostPath: { path: o.hostMirror, type: 'Directory' } }] : []),
|
|
154
|
+
...(bunCache ? [bunCache.volume] : []),
|
|
149
155
|
] } : {}),
|
|
150
156
|
},
|
|
151
157
|
},
|
|
@@ -201,6 +207,7 @@ export interface RunPodCommandOptions {
|
|
|
201
207
|
llm?: 'grok';
|
|
202
208
|
clone?: boolean;
|
|
203
209
|
hostMirror?: string;
|
|
210
|
+
bunCache?: string;
|
|
204
211
|
/** 컨테이너 메모리 한도(기본 16Gi). */
|
|
205
212
|
memoryLimit?: string;
|
|
206
213
|
source?: PodSource;
|
|
@@ -263,6 +270,8 @@ export async function runPodCommand(options: RunPodCommandOptions): Promise<PodC
|
|
|
263
270
|
const hostMirror = (options.hostMirror ?? env.ELANOUS_POD_HOST_MIRROR
|
|
264
271
|
?? (options.configHostMirror ?? (() => getUserConfig().pod?.hostMirror))())?.trim() || undefined;
|
|
265
272
|
if (hostMirror && !isAbsolute(hostMirror)) throw new Error('pod.hostMirror must be an absolute directory path');
|
|
273
|
+
const bunCache = options.bunCache?.trim() || undefined;
|
|
274
|
+
if (bunCache && !isAbsolute(bunCache)) throw new Error('pod.bunCache must be an absolute directory path');
|
|
266
275
|
|
|
267
276
|
let member: PodPoolMember | null = null;
|
|
268
277
|
let pool = options.poolScheduler;
|
|
@@ -328,7 +337,7 @@ export async function runPodCommand(options: RunPodCommandOptions): Promise<PodC
|
|
|
328
337
|
name, namespace, image: member?.imageRef ?? image,
|
|
329
338
|
...(member?.imageRef ? { imagePullPolicy: 'IfNotPresent' as const } : {}),
|
|
330
339
|
repoUrl, command, skills, ...(llm ? { llm } : {}), ...(options.clone ? { clone: true } : {}),
|
|
331
|
-
...(options.source ? { source: options.source } : {}), ...(hostMirror ? { hostMirror } : {}), ...(options.memoryLimit ? { memoryLimit: options.memoryLimit } : {}), deadlineSeconds,
|
|
340
|
+
...(options.source ? { source: options.source } : {}), ...(hostMirror ? { hostMirror } : {}), ...(bunCache ? { bunCache } : {}), ...(options.memoryLimit ? { memoryLimit: options.memoryLimit } : {}), deadlineSeconds,
|
|
332
341
|
...(options.runId ? { runId: options.runId } : {}),
|
|
333
342
|
});
|
|
334
343
|
kubectl(['-n', namespace, 'delete', 'job', name, '--ignore-not-found']);
|
|
@@ -2,6 +2,7 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } fr
|
|
|
2
2
|
import { gunzipSync } from 'node:zlib';
|
|
3
3
|
import { debug } from '../../debug/log.js';
|
|
4
4
|
import { runLedgerDir, runLedgerPath } from '../../self-implement/run-ledger.js';
|
|
5
|
+
import { ledgerLineToLogEvent } from './pod-ledger-events.js';
|
|
5
6
|
|
|
6
7
|
type Ledger = { runId: string; jsonl: string };
|
|
7
8
|
type Incomplete = { runId: string; reason: string };
|
|
@@ -124,7 +125,7 @@ export function createPodLedgerFollower(opts: {
|
|
|
124
125
|
let owned = false;
|
|
125
126
|
let disabled = false;
|
|
126
127
|
let lastProgressAt = now();
|
|
127
|
-
let
|
|
128
|
+
let visibleProgressEvent: string | null = null;
|
|
128
129
|
let lastActivityMeasuredAt = lastProgressAt;
|
|
129
130
|
let lastActivityLogAt = -Infinity;
|
|
130
131
|
let nextStallMinute = stallMinutes;
|
|
@@ -132,11 +133,11 @@ export function createPodLedgerFollower(opts: {
|
|
|
132
133
|
function checkStall(ledgerRead: boolean, activityMeasured: boolean) {
|
|
133
134
|
const idleMinutes = Math.max(0, (now() - lastProgressAt) / 60_000);
|
|
134
135
|
if (idleMinutes >= nextStallMinute) {
|
|
135
|
-
log('self-implement.pod', 'stalled', { runId: opts.runId, lastProgressEvent, idleMinutes });
|
|
136
|
+
log('self-implement.pod', 'stalled', { runId: opts.runId, lastProgressEvent: visibleProgressEvent, idleMinutes });
|
|
136
137
|
const observation = ledgerRead
|
|
137
138
|
? (activityMeasured ? '원장·작업 트리 모두 조용함' : '원장 조용함')
|
|
138
139
|
: '원장 못 잼';
|
|
139
|
-
onStall(`[pod] 진행 없음 ${Math.floor(idleMinutes)}분 — ${observation} · 마지막 진행 ${
|
|
140
|
+
onStall(`[pod] 진행 없음 ${Math.floor(idleMinutes)}분 — ${observation} · 마지막 진행 ${visibleProgressEvent ?? '없음'}${activityMeasured ? '' : ' (작업 트리 못 잼)'}`);
|
|
140
141
|
stalled = true;
|
|
141
142
|
nextStallMinute = (Math.floor(idleMinutes / stallMinutes) + 1) * stallMinutes;
|
|
142
143
|
}
|
|
@@ -169,7 +170,7 @@ export function createPodLedgerFollower(opts: {
|
|
|
169
170
|
if (stalled) log('self-implement.pod', 'stall-cleared', { runId: opts.runId, idleMinutes });
|
|
170
171
|
stalled = false;
|
|
171
172
|
lastProgressAt = now();
|
|
172
|
-
|
|
173
|
+
visibleProgressEvent = 'worktree-activity';
|
|
173
174
|
nextStallMinute = stallMinutes;
|
|
174
175
|
if (now() - lastActivityLogAt >= 5 * 60_000) {
|
|
175
176
|
log('self-implement.pod', 'worktree-activity', { runId: opts.runId, files });
|
|
@@ -190,6 +191,11 @@ export function createPodLedgerFollower(opts: {
|
|
|
190
191
|
log('self-implement.pod', 'ledger-live-appended', { runId: opts.runId, lines: complete.split('\n').filter(Boolean).length, bytes: Buffer.byteLength(complete), offset });
|
|
191
192
|
for (const line of complete.split('\n')) {
|
|
192
193
|
if (!line) continue;
|
|
194
|
+
let logEvent: ReturnType<typeof ledgerLineToLogEvent>;
|
|
195
|
+
try {
|
|
196
|
+
logEvent = ledgerLineToLogEvent(line, opts.runId);
|
|
197
|
+
if (logEvent) log(logEvent.category, logEvent.event, logEvent.data);
|
|
198
|
+
} catch { /* A failed host log write must not interrupt ledger following or stall accounting. */ }
|
|
193
199
|
let entry: unknown;
|
|
194
200
|
try { entry = JSON.parse(line); } catch { continue; }
|
|
195
201
|
const event = (entry && typeof entry === 'object' && 'event' in entry) ? entry.event : undefined;
|
|
@@ -198,7 +204,7 @@ export function createPodLedgerFollower(opts: {
|
|
|
198
204
|
if (stalled) log('self-implement.pod', 'stall-cleared', { runId: opts.runId, idleMinutes });
|
|
199
205
|
stalled = false;
|
|
200
206
|
lastProgressAt = now();
|
|
201
|
-
|
|
207
|
+
visibleProgressEvent = logEvent?.event ?? '[redacted]';
|
|
202
208
|
nextStallMinute = stallMinutes;
|
|
203
209
|
}
|
|
204
210
|
checkStall(true, activityMeasured);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const SECRET_FIELD = /token|secret|password|authorization|key/i;
|
|
2
|
+
const SAFE_FIELD = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
|
|
3
|
+
const SECRET_VALUE = /(?:\b(?:bearer|basic)\s+\S+|\b(?:token|secret|password|authorization|api[-_]?key|access[-_]?key|private[-_]?key)\b\s*(?:is|[=:])\s*["']?\S+|\b(?:sk-[A-Za-z0-9_-]{8,}|gh[pousr]_[A-Za-z0-9_]{8,}))\b/i;
|
|
4
|
+
const MAX_FIELDS = 32;
|
|
5
|
+
// Review R2: only lifecycle events the host needs to see, and no free text. Event names must start with a known
|
|
6
|
+
// lifecycle prefix; string fields survive only when they are short code-like values in a fixed set of keys.
|
|
7
|
+
const EVENT_ALLOWED = /^(?:gate|gated|review|reviewed|repair|repaired|rework|merge|merged|pr|land|landed|stage|round|supervisor|child|plan|planned|decompose|decomposed|shard|stall|harvest|decision|run|pipeline|progress|human|start|started|finish|finished|done|implement|implemented|commit|test|typecheck)(?:-(?:started|finished|round|opened|closed|failed|passed|blocked|done|entry|node|outcome|delivery|decision|stop|merged|ready|requested|resolved|result|fix|must-fix|verdict|skipped|origin|cleared))*$/;
|
|
8
|
+
const CODE_FIELDS = new Set(['stage', 'outcome', 'verdict', 'status', 'kind', 'phase', 'role', 'result']);
|
|
9
|
+
const CODE_VALUE = /^[a-z0-9_.:-]{1,40}$/i;
|
|
10
|
+
|
|
11
|
+
function secretValues(root: Record<string, unknown>): string[] {
|
|
12
|
+
const secrets: string[] = [];
|
|
13
|
+
const pending: unknown[] = [root];
|
|
14
|
+
while (pending.length) {
|
|
15
|
+
const current = pending.pop();
|
|
16
|
+
if (!current || typeof current !== 'object') continue;
|
|
17
|
+
if (Array.isArray(current)) {
|
|
18
|
+
pending.push(...current);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
for (const [name, value] of Object.entries(current)) {
|
|
22
|
+
if (SECRET_FIELD.test(name) && typeof value === 'string' && value.length >= 3) secrets.push(value);
|
|
23
|
+
if (value && typeof value === 'object') pending.push(value);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return secrets;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Project bounded ledger metadata, excluding named secrets and recognizable credential values. */
|
|
30
|
+
export function ledgerLineToLogEvent(line: string, runId: string): {
|
|
31
|
+
category: string;
|
|
32
|
+
event: string;
|
|
33
|
+
data: Record<string, unknown>;
|
|
34
|
+
} | undefined {
|
|
35
|
+
let entry: unknown;
|
|
36
|
+
try { entry = JSON.parse(line); } catch { return undefined; }
|
|
37
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry) || !('event' in entry) || typeof entry.event !== 'string') return undefined;
|
|
38
|
+
// The real run-ledger writer wraps observer fields in `data`; older/synthetic lines can be flat.
|
|
39
|
+
const details = 'data' in entry && entry.data && typeof entry.data === 'object' && !Array.isArray(entry.data)
|
|
40
|
+
? entry.data as Record<string, unknown> : undefined;
|
|
41
|
+
const sources = [entry, details].filter((source): source is Record<string, unknown> => source !== undefined);
|
|
42
|
+
const knownSecrets = secretValues(entry);
|
|
43
|
+
const containsCredential = (value: string) => SECRET_VALUE.test(value) || knownSecrets.some((secret) => value.includes(secret));
|
|
44
|
+
if (containsCredential(entry.event) || !EVENT_ALLOWED.test(entry.event)) return undefined;
|
|
45
|
+
|
|
46
|
+
const data: Record<string, unknown> = { runId };
|
|
47
|
+
let fields = 0;
|
|
48
|
+
for (const source of sources) {
|
|
49
|
+
for (const [name, value] of Object.entries(source)) {
|
|
50
|
+
if (name === 'event' || name === 'runId' || name === 'data' || !SAFE_FIELD.test(name) || SECRET_FIELD.test(name)) continue;
|
|
51
|
+
if (fields >= MAX_FIELDS) break;
|
|
52
|
+
const countName = `${name}Count`;
|
|
53
|
+
const outputName = Array.isArray(value) || value && typeof value === 'object' ? countName : name;
|
|
54
|
+
if (!SAFE_FIELD.test(outputName) || SECRET_FIELD.test(outputName) || Object.hasOwn(data, outputName)) continue;
|
|
55
|
+
if (typeof value === 'string') {
|
|
56
|
+
// Free text (reasons, messages, patches) never leaves the pod ledger; only short code values in known keys.
|
|
57
|
+
if (!CODE_FIELDS.has(name) || !CODE_VALUE.test(value) || containsCredential(value)) continue;
|
|
58
|
+
data[name] = value;
|
|
59
|
+
} else if (typeof value === 'number' && Number.isFinite(value) || typeof value === 'boolean' || value === null) {
|
|
60
|
+
data[name] = value;
|
|
61
|
+
} else if (Array.isArray(value) || value && typeof value === 'object') {
|
|
62
|
+
data[countName] = Array.isArray(value) ? value.length : Object.keys(value).length;
|
|
63
|
+
} else continue;
|
|
64
|
+
fields += 1;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { category: 'self-implement.pod.ledger', event: entry.event, data };
|
|
68
|
+
}
|
|
@@ -50,7 +50,7 @@ import { findGitDir } from '../../git-fs/locate.js';
|
|
|
50
50
|
import { debug } from '../../debug/log.js';
|
|
51
51
|
import { selectLiveDetail } from '../../live/detail-switch.js';
|
|
52
52
|
import { authStorePath } from '../../oauth/store.js';
|
|
53
|
-
import { grokAuthFilePath, resolveGrokCredential } from '../../grok/credential.js';
|
|
53
|
+
import { grokAuthFilePath, isGrokSubscriptionExpiring, refreshGrokSubscriptionToken, resolveGrokCredential } from '../../grok/credential.js';
|
|
54
54
|
import { defaultGrokModel } from '../../grok/models.js';
|
|
55
55
|
import { resolveHostId } from '../../platform/host-id.js';
|
|
56
56
|
import { envLiteral } from '../../platform/env-literal.js';
|
|
@@ -171,12 +171,26 @@ export function hostCredentials(account: string, storePath: string = authStorePa
|
|
|
171
171
|
return { elanousAuth, codexAuth, ghToken: ghToken() };
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
/**
|
|
174
|
+
/** A Pod gets a refresh-less access copy, so the copy must outlive the Job: same rule as the Codex path (3 h). */
|
|
175
|
+
export const POD_GROK_MIN_VALIDITY_MS = 3 * 3600_000;
|
|
176
|
+
|
|
177
|
+
/** resolveGrokCredential 과 동일한 출처를 읽되 Pod 로는 refresh 없는 access 사본만 보낸다.
|
|
178
|
+
* POD4 (10-01): the copy used to ship whatever the host had — an AUTH1 shard copied a token that had expired
|
|
179
|
+
* 4 minutes earlier and died at `grok-credential-preflight status=expired`. Refresh on the host first. */
|
|
175
180
|
export function hostGrokCredentials(opts: {
|
|
176
181
|
home?: string; env?: NodeJS.ProcessEnv; apiKeyOptIn?: boolean; ghToken?: () => string;
|
|
182
|
+
/** Test seams — the host refresh (read-only `grok models`) and the expiry probe. */
|
|
183
|
+
refresh?: () => void; isExpiring?: () => boolean;
|
|
177
184
|
} = {}): { grokAuth?: string; grokApiKey?: string; ghToken: string } {
|
|
178
|
-
|
|
185
|
+
let credential = resolveGrokCredential({ home: opts.home, env: opts.env });
|
|
179
186
|
if (credential?.kind === 'subscription') {
|
|
187
|
+
const expiring = opts.isExpiring ?? (() => isGrokSubscriptionExpiring({ home: opts.home, bufferMs: POD_GROK_MIN_VALIDITY_MS }));
|
|
188
|
+
if (expiring()) {
|
|
189
|
+
try { (opts.refresh ?? (() => { refreshGrokSubscriptionToken({ home: opts.home }); }))(); } catch { /* judged below */ }
|
|
190
|
+
if (expiring()) throw new Error('grok: 구독 access 토큰이 3시간 안에 만료 — 호스트 갱신(grok models)도 못 늘렸다 · 호스트에서 grok login 후 다시');
|
|
191
|
+
credential = resolveGrokCredential({ home: opts.home, env: opts.env });
|
|
192
|
+
if (credential?.kind !== 'subscription') throw new Error('grok: 갱신 뒤 구독 자격을 다시 읽지 못했다');
|
|
193
|
+
}
|
|
180
194
|
const scopes = JSON.parse(readFileSync(grokAuthFilePath(opts.home), 'utf8')) as Record<string, unknown>;
|
|
181
195
|
const redacted: Record<string, { key: string; expires_at?: string; user_id?: string }> = {};
|
|
182
196
|
for (const [name, value] of Object.entries(scopes)) {
|
|
@@ -219,6 +233,64 @@ function githubRepositoryFromUrl(repoUrl: string): string | null {
|
|
|
219
233
|
|
|
220
234
|
/** Job 스크립트의 salvage 단계 — 자식 rc 가 0 이 아닐 때만, 변경·미푸시 커밋이 있는 격리 워크트리를 `salvage/<job>/<worktree>` 로 push.
|
|
221
235
|
* main·self-impl/ 로는 절대 안 민다. 실패해도 제어 흐름을 바꾸지 않는다(호출자는 이 뒤에 `exit $rc`). */
|
|
236
|
+
/**
|
|
237
|
+
* App installation tokens live one hour and only the host process renews them (at 10 minutes left).
|
|
238
|
+
* If that process dies (host reboot), nothing renews the Pod's token. hosts.yml unchanged for this long
|
|
239
|
+
* means the host is gone. Push a snapshot while the token still works.
|
|
240
|
+
*/
|
|
241
|
+
export const POD_GH_STALE_SECONDS = 53 * 60;
|
|
242
|
+
|
|
243
|
+
/** Snapshot every worktree to salvage/<job>/<wt>-early without touching the child's index, worktree or HEAD. */
|
|
244
|
+
export function podEarlySalvageScript(): string {
|
|
245
|
+
return [
|
|
246
|
+
'job_name="${ELANOUS_POD_NAME:-unknown-job}"',
|
|
247
|
+
'while IFS= read -r wt; do',
|
|
248
|
+
' [ -n "$wt" ] || continue',
|
|
249
|
+
' case "$wt" in /*) ;; *) continue ;; esac',
|
|
250
|
+
' wt_name=$(basename -- "$wt")',
|
|
251
|
+
" case \"$wt_name\" in ''|.*|*/*|*'..'*) continue ;; esac",
|
|
252
|
+
' branch="salvage/${job_name}/${wt_name}-early"',
|
|
253
|
+
' (',
|
|
254
|
+
' set +e',
|
|
255
|
+
' cd -- "$wt" || exit 0',
|
|
256
|
+
' head=$(git rev-parse --verify -q HEAD) || { printf \'ELANOUS_POD_SALVAGE_NONE early-no-head\\n\'; exit 0; }',
|
|
257
|
+
' idx=$(mktemp) || exit 0',
|
|
258
|
+
' cp -- "$(git rev-parse --git-path index)" "$idx" 2>/dev/null || :',
|
|
259
|
+
' GIT_INDEX_FILE="$idx" git add -A -- . >/dev/null 2>&1',
|
|
260
|
+
' tree=$(GIT_INDEX_FILE="$idx" git write-tree 2>/dev/null)',
|
|
261
|
+
' rm -f -- "$idx"',
|
|
262
|
+
' [ -n "$tree" ] || { printf \'ELANOUS_POD_SALVAGE_NONE early-write-tree-failed\\n\'; exit 0; }',
|
|
263
|
+
' base=$(git rev-parse --verify -q origin/HEAD || git rev-parse --verify -q origin/main || true)',
|
|
264
|
+
' if [ "$tree" = "$(git rev-parse "${head}^{tree}")" ]; then',
|
|
265
|
+
' if [ -z "$base" ] || [ "$(git rev-list --count "${base}..${head}" 2>/dev/null || echo 1)" = 0 ]; then printf \'ELANOUS_POD_SALVAGE_NONE clean\\n\'; exit 0; fi',
|
|
266
|
+
' commit=$head',
|
|
267
|
+
' else',
|
|
268
|
+
' commit=$(git commit-tree "$tree" -p "$head" -m "salvage(early): ${job_name} host token refresh stopped") || { printf \'ELANOUS_POD_SALVAGE_NONE commit-failed\\n\'; exit 0; }',
|
|
269
|
+
' fi',
|
|
270
|
+
' git push --force origin "${commit}:refs/heads/${branch}" || { printf \'ELANOUS_POD_SALVAGE_NONE push-failed\\n\'; exit 0; }',
|
|
271
|
+
" printf 'ELANOUS_POD_SALVAGE %s %s\\n' \"$branch\" \"$commit\"",
|
|
272
|
+
' )',
|
|
273
|
+
"done < <(git worktree list --porcelain 2>/dev/null | awk '/^worktree / { sub(/^worktree /, \"\"); print }')",
|
|
274
|
+
].join('\n');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Background watchdog: fires the early snapshot once when hosts.yml has not been rewritten for staleSeconds. */
|
|
278
|
+
export function podGithubWatchdogScript(staleSeconds: number, intervalSeconds = 30): string {
|
|
279
|
+
return [
|
|
280
|
+
'(',
|
|
281
|
+
` while sleep ${intervalSeconds}; do`,
|
|
282
|
+
' f="$GH_CONFIG_DIR/hosts.yml"',
|
|
283
|
+
' m=$(stat -c %Y "$f" 2>/dev/null || stat -f %m "$f" 2>/dev/null) || continue',
|
|
284
|
+
' age=$(( $(date +%s) - m ))',
|
|
285
|
+
` [ "$age" -ge ${staleSeconds} ] || continue`,
|
|
286
|
+
" printf 'ELANOUS_POD_GH_STALE %s\\n' \"$age\"",
|
|
287
|
+
` ${podEarlySalvageScript().split('\n').join('\n ')}`,
|
|
288
|
+
' break',
|
|
289
|
+
' done',
|
|
290
|
+
') & gh_watch_pid=$!',
|
|
291
|
+
].join('\n');
|
|
292
|
+
}
|
|
293
|
+
|
|
222
294
|
export function podSalvageScript(): string {
|
|
223
295
|
return [
|
|
224
296
|
'if [ "${rc:-0}" -ne 0 ]; then',
|
|
@@ -368,7 +440,7 @@ export function podMemoryLimitFor(feature: string, env: NodeJS.ProcessEnv | Read
|
|
|
368
440
|
return { limit: a !== null && b !== null && a > b ? base : high, tier, source };
|
|
369
441
|
}
|
|
370
442
|
|
|
371
|
-
export function podJobManifest(o: { name: string; namespace: string; image: string; /** 레지스트리 이미지면 IfNotPresent(노드가 pull) · 반입 이미지면 Never. */ imagePullPolicy?: 'Never' | 'IfNotPresent'; repoUrl: string; source?: PodSource; hostMirror?: string; args: readonly string[]; passEnv: readonly string[]; deadlineSeconds: number; runId?: string; parentRunId?: string; armEnv?: Readonly<Record<string, string>>; hostId?: string; imageCommit?: string | null; skillEnvs?: readonly string[]; memoryLimit?: string; goalDoc?: string; grokCredential?: 'subscription' | 'api_key'; codexAccounts?: readonly string[]; appCredential?: boolean }): Record<string, unknown> {
|
|
443
|
+
export function podJobManifest(o: { name: string; namespace: string; image: string; /** 레지스트리 이미지면 IfNotPresent(노드가 pull) · 반입 이미지면 Never. */ imagePullPolicy?: 'Never' | 'IfNotPresent'; repoUrl: string; source?: PodSource; hostMirror?: string; args: readonly string[]; passEnv: readonly string[]; deadlineSeconds: number; runId?: string; parentRunId?: string; armEnv?: Readonly<Record<string, string>>; hostId?: string; imageCommit?: string | null; skillEnvs?: readonly string[]; memoryLimit?: string; goalDoc?: string; grokCredential?: 'subscription' | 'api_key'; codexAccounts?: readonly string[]; appCredential?: boolean; /** Test seam — default POD_GH_STALE_SECONDS. */ githubStaleSeconds?: number }): Record<string, unknown> {
|
|
372
444
|
const quoted = o.args.map((a) => `'${a.replace(/'/g, `'\\''`)}'`).join(' ');
|
|
373
445
|
const goalPath = o.goalDoc ? `'${(o.goalDoc.startsWith('-') ? `./${o.goalDoc}` : o.goalDoc).replace(/'/g, `'\\''`)}'` : undefined;
|
|
374
446
|
const askBaseIndex = o.goalDoc ? o.args.indexOf('--base') : -1;
|
|
@@ -394,6 +466,7 @@ export function podJobManifest(o: { name: string; namespace: string; image: stri
|
|
|
394
466
|
] : []),
|
|
395
467
|
// Only fixed command shapes leave the Pod; arbitrary argv (including shell -c text) is never emitted.
|
|
396
468
|
`(while :; do { mem=$(if [ -r /sys/fs/cgroup/memory.current ]; then cat /sys/fs/cgroup/memory.current; elif [ -r /sys/fs/cgroup/memory/memory.usage_in_bytes ]; then cat /sys/fs/cgroup/memory/memory.usage_in_bytes; else printf -- -; fi); top=$(ps -eo rss=,comm=,args= --sort=-rss | head -5 | awk 'function encode(s) { gsub(/%/, "%25", s); gsub(/:/, "%3A", s); gsub(/ /, "%20", s); gsub(/\\t/, "%09", s); return s } { rss=$1; name=$2; sub(/^[[:space:]]*[0-9]+[[:space:]]+[^[:space:]]+[[:space:]]*/, ""); n=split($0, a, /[[:space:]]+/); cmd="<redacted>"; if (name=="sleep" && a[2]=="30") cmd="sleep 30" (n>2 ? " <redacted>" : ""); else if (name=="bun") { cmd="bun <redacted>"; if (a[2]=="test") cmd="bun test <redacted>"; else if (a[2]=="run") cmd="bun run <redacted>"; else if ((a[2]=="x" || a[2]=="exec") && a[3]=="tsc") cmd="bun x tsc <redacted>" } else if (name=="tsc") cmd="tsc <redacted>"; else if (name=="elanous") { cmd="elanous <redacted>"; if (a[2]=="self") cmd="elanous self <redacted>"; else if (a[2]=="harness") cmd="elanous harness <redacted>" } else if (name=="node") cmd="node <redacted>"; if (name=="sleep" && cmd=="<redacted>") cmd="sleep <redacted>"; else if (cmd=="<redacted>" && name!="bash" && name!="sh") name="other"; printf " %s:%s:%s", rss, encode(name), encode(substr(cmd,1,120)) }'); printf "ELANOUS_MEM %s %s%s\\n" "$(date +%s)" "$mem" "$top"; } || true; sleep 15 || break; done) & mem_sampler_pid=$!`,
|
|
469
|
+
...(o.appCredential ? [podGithubWatchdogScript(o.githubStaleSeconds ?? POD_GH_STALE_SECONDS)] : []),
|
|
397
470
|
// 마지막 줄 JSON 이 «맨 끝»이어야 한다(parseSelfImplementJson) — rollup 은 그 앞에.
|
|
398
471
|
goalPath
|
|
399
472
|
? `elanous harness ask ${goalPath} --json${quotedAsk} > /tmp/si.out 2>&1; rc=$?; bun -e 'const fs=require("fs");const p="/tmp/si.out";const s=fs.readFileSync(p,"utf8");const line=s.trimEnd().split("\\n").at(-1);try{const o=JSON.parse(line);if(o.kind==="self"&&o.result&&typeof o.result==="object")fs.appendFileSync(p,"\\n"+JSON.stringify({...o.result,...(typeof o.ok==="boolean"?{ok:o.ok}:{})})+"\\n")}catch{}'`
|
|
@@ -454,6 +527,7 @@ for ledger in "\${ELANOUS_STATE_DIR:-$HOME/.elanous}"/run-ledger/*.jsonl; do
|
|
|
454
527
|
done
|
|
455
528
|
if [ "$found" -eq 0 ]; then echo ELANOUS_RUN_LEDGER_NONE; fi`,
|
|
456
529
|
'kill "$mem_sampler_pid" 2>/dev/null || true',
|
|
530
|
+
...(o.appCredential ? ['kill "$gh_watch_pid" 2>/dev/null || true'] : []),
|
|
457
531
|
'wait "$mem_sampler_pid" 2>/dev/null || true',
|
|
458
532
|
'tail -n 1 /tmp/si.out',
|
|
459
533
|
podSalvageScript(),
|
package/src/telegram.ts
CHANGED
|
@@ -66,7 +66,11 @@ import { observeDevRequestRouteFailSoft } from './skills/dev-request-router.js';
|
|
|
66
66
|
import { runUrlRoute } from './skills/url-route-exec.js';
|
|
67
67
|
import { recordRoutedLinkAbsorbed } from './intake-plane/link-ledger.js';
|
|
68
68
|
import { classifyFrontInput } from './intake-plane/front-classifier.js';
|
|
69
|
+
import { handleTelegramSeatWork, type TelegramSeatWorkDeps } from './intake-plane/telegram-seat-work.js';
|
|
69
70
|
import { effectiveInstanceRoot } from './instance/resolve.js';
|
|
71
|
+
import { getElanousConfigDir } from './elanous-config-dir.js';
|
|
72
|
+
import { defaultFieldEvent, FieldUploadError, parseFieldCaption, resolveFieldMime, saveFieldMedia } from './field/field-media.js';
|
|
73
|
+
import { scheduleFieldReel, type FieldReelOptions } from './field/field-reel.js';
|
|
70
74
|
|
|
71
75
|
export type TgAttachmentKind = 'photo' | 'voice' | 'audio' | 'document';
|
|
72
76
|
|
|
@@ -105,6 +109,10 @@ export interface TgIncoming {
|
|
|
105
109
|
botId?: string;
|
|
106
110
|
/** 답장 대상 메시지의 텍스트(force_reply 정정 가로채기용). parseUpdate 가 채운다. */
|
|
107
111
|
replyToText?: string;
|
|
112
|
+
/** 앨범(media group) id — `#현장` 캡션은 앨범의 첫 항목에만 붙으므로 나머지를 같은 행사로 묶는 데 쓴다. */
|
|
113
|
+
mediaGroupId?: string;
|
|
114
|
+
/** 영상 — `#현장` 저장 전용. `attachments` 에 넣지 않는다(LLM 경로의 동작을 안 바꾸려고). */
|
|
115
|
+
video?: TgIncomingAttachment;
|
|
108
116
|
}
|
|
109
117
|
|
|
110
118
|
/** Streaming surface passed to the message handler. The handler can
|
|
@@ -200,6 +208,14 @@ export interface TelegramBotOpts {
|
|
|
200
208
|
* independently of the LLM chat path. Best-effort: errors are
|
|
201
209
|
* swallowed by the caller — never blocks chat reply. */
|
|
202
210
|
onTriggerTap?: (event: TgTriggerEvent) => void | Promise<void>;
|
|
211
|
+
/** Test seam for addressed seat intake; defaults to the real intake door. */
|
|
212
|
+
seatWorkDeps?: TelegramSeatWorkDeps;
|
|
213
|
+
/** `#현장` 기본 행사 — 없으면 `slashContext.userConfig.telegram.fieldDefaultEvent` · 그것도 없으면 `field-<로컬 날짜>`. */
|
|
214
|
+
fieldDefaultEvent?: string;
|
|
215
|
+
/** 시험 seam — 현장 폴더 뿌리(기본 `getElanousConfigDir()`). */
|
|
216
|
+
fieldRootDir?: () => string;
|
|
217
|
+
/** 시험에서는 실제 렌더 대신 가짜 실행기를 주입한다. */
|
|
218
|
+
fieldReel?: FieldReelOptions;
|
|
203
219
|
}
|
|
204
220
|
|
|
205
221
|
/** Surface-unification v2 (FU-2) — Telegram trigger event passed to
|
|
@@ -314,6 +330,15 @@ export class TelegramBot {
|
|
|
314
330
|
* the bot under Telegram's ~30/sec global cap without blocking
|
|
315
331
|
* further if load is bursty but average-safe. */
|
|
316
332
|
private readonly globalWindow: number[] = [];
|
|
333
|
+
private readonly fieldDefaultEventOpt?: string;
|
|
334
|
+
private readonly fieldRootDir: () => string;
|
|
335
|
+
private readonly fieldReel?: FieldReelOptions;
|
|
336
|
+
private readonly seatWorkDeps: TelegramSeatWorkDeps;
|
|
337
|
+
/** 앨범 id → `#현장` 행사 · 답장 디바운스. 태그된 앨범만 들어온다. */
|
|
338
|
+
private readonly fieldGroups = new Map<string, {
|
|
339
|
+
event: string; at: number; chatId: number; threadId?: number; replyTo: number; count: number;
|
|
340
|
+
timer?: ReturnType<typeof setTimeout>;
|
|
341
|
+
}>();
|
|
317
342
|
|
|
318
343
|
constructor(opts: TelegramBotOpts) {
|
|
319
344
|
if (!opts.token) throw new Error('TelegramBot: token required');
|
|
@@ -340,6 +365,10 @@ export class TelegramBot {
|
|
|
340
365
|
this.slashCommands = opts.slashCommands ?? [];
|
|
341
366
|
this.slashContext = opts.slashContext;
|
|
342
367
|
this.voiceAdapter = opts.voiceAdapter;
|
|
368
|
+
this.fieldDefaultEventOpt = opts.fieldDefaultEvent;
|
|
369
|
+
this.fieldRootDir = opts.fieldRootDir ?? getElanousConfigDir;
|
|
370
|
+
this.fieldReel = opts.fieldReel;
|
|
371
|
+
this.seatWorkDeps = opts.seatWorkDeps ?? {};
|
|
343
372
|
if (opts.onTriggerTap) {
|
|
344
373
|
// Assign through `as unknown` to bypass `readonly` + private —
|
|
345
374
|
// the tap is set once at construction and never reassigned.
|
|
@@ -777,6 +806,7 @@ export class TelegramBot {
|
|
|
777
806
|
replyTo?: number;
|
|
778
807
|
threadId?: number;
|
|
779
808
|
caption?: string;
|
|
809
|
+
mimeType?: string;
|
|
780
810
|
} = {}): Promise<{ messageId: number } | undefined> {
|
|
781
811
|
// Per-chat gap mirror — keep telegram happy on rapid document spills.
|
|
782
812
|
const last = this.chatLastSentAt.get(chatId) ?? 0;
|
|
@@ -797,7 +827,7 @@ export class TelegramBot {
|
|
|
797
827
|
new Uint8Array(blobBytes).set(bytes);
|
|
798
828
|
form.set(
|
|
799
829
|
'document',
|
|
800
|
-
new Blob([blobBytes], { type: 'text/plain' }),
|
|
830
|
+
new Blob([blobBytes], { type: opts.mimeType ?? 'text/plain' }),
|
|
801
831
|
filename,
|
|
802
832
|
);
|
|
803
833
|
|
|
@@ -1347,11 +1377,107 @@ export class TelegramBot {
|
|
|
1347
1377
|
return false;
|
|
1348
1378
|
}
|
|
1349
1379
|
|
|
1380
|
+
/** `#현장` 캡션 사진·영상(이미지·영상 문서 포함)을 현장 폴더에 저장하고 한 줄로 답한다.
|
|
1381
|
+
* 처리했으면 true. 저장 규칙은 HTTP 입구와 같은 `saveFieldMedia` 다. */
|
|
1382
|
+
private async tryHandleFieldUpload(ctx: TgIncoming): Promise<boolean> {
|
|
1383
|
+
const media = [
|
|
1384
|
+
...ctx.attachments.filter((a) => a.kind === 'photo'
|
|
1385
|
+
|| (a.kind === 'document' && resolveFieldMime(a.mimeType, a.fileName ?? '') !== null)),
|
|
1386
|
+
...(ctx.video ? [ctx.video] : []),
|
|
1387
|
+
];
|
|
1388
|
+
if (media.length === 0) return false;
|
|
1389
|
+
const now = this.nowImpl();
|
|
1390
|
+
for (const [id, g] of this.fieldGroups) if (now - g.at > 10 * 60_000 && !g.timer) this.fieldGroups.delete(id);
|
|
1391
|
+
const tag = parseFieldCaption(ctx.text);
|
|
1392
|
+
const group = ctx.mediaGroupId ? this.fieldGroups.get(ctx.mediaGroupId) : undefined;
|
|
1393
|
+
if (!tag && !group) return false;
|
|
1394
|
+
const event = tag
|
|
1395
|
+
? tag.event ?? defaultFieldEvent(
|
|
1396
|
+
this.fieldDefaultEventOpt ?? this.slashContext?.userConfig?.telegram?.fieldDefaultEvent, new Date(now))
|
|
1397
|
+
: group!.event;
|
|
1398
|
+
|
|
1399
|
+
let count: number | null = null;
|
|
1400
|
+
let failure: string | null = null;
|
|
1401
|
+
try {
|
|
1402
|
+
const inputs = [];
|
|
1403
|
+
for (const att of media) {
|
|
1404
|
+
const downloaded = await this.downloadFile(att.fileId);
|
|
1405
|
+
try {
|
|
1406
|
+
inputs.push({
|
|
1407
|
+
originalName: att.fileName ?? downloaded.fileName ?? 'media',
|
|
1408
|
+
mimeType: att.mimeType ?? (att.kind === 'photo' ? 'image/jpeg' : undefined),
|
|
1409
|
+
bytes: new Uint8Array(readFileSync(downloaded.localPath)),
|
|
1410
|
+
});
|
|
1411
|
+
} finally {
|
|
1412
|
+
try { unlinkSync(downloaded.localPath); } catch { /* 이미 없음 */ }
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
// 글(자막)은 캡션이 달린 항목에만 있다 — 앨범이면 첫 장에만 붙는다(MK 2026-09-30).
|
|
1416
|
+
const saved = saveFieldMedia(inputs, { rootDir: this.fieldRootDir(), event, device: 'telegram', ...(tag?.text ? { caption: tag.text } : {}) });
|
|
1417
|
+
count = saved.count;
|
|
1418
|
+
scheduleFieldReel(saved.dir, {
|
|
1419
|
+
...this.fieldReel,
|
|
1420
|
+
notificationKey: `telegram:${this.botId}:${ctx.chatId}:${ctx.threadId ?? ''}:${ctx.mediaGroupId ?? ctx.messageId}`,
|
|
1421
|
+
onDone: async (result) => {
|
|
1422
|
+
const reply = { replyTo: ctx.messageId, threadId: ctx.threadId };
|
|
1423
|
+
if (result.ok && result.file) {
|
|
1424
|
+
const sent = await this.sendDocument(ctx.chatId, readFileSync(result.file), 'reel-9x16.mp4', {
|
|
1425
|
+
...reply, mimeType: 'video/mp4', caption: `현장 영상 · ${event} · ${result.seconds}초`,
|
|
1426
|
+
});
|
|
1427
|
+
if (!sent) throw new Error('sendDocument failed');
|
|
1428
|
+
} else {
|
|
1429
|
+
await this.sendMessage(ctx.chatId, `현장 영상 실패 · ${event} · ${result.error ?? 'render failed'}`, reply);
|
|
1430
|
+
}
|
|
1431
|
+
},
|
|
1432
|
+
});
|
|
1433
|
+
debug.log('field.upload', 'saved', { surface: 'telegram', files: inputs.length, count, album: !!ctx.mediaGroupId });
|
|
1434
|
+
} catch (err) {
|
|
1435
|
+
failure = err instanceof FieldUploadError ? err.code : 'download-failed';
|
|
1436
|
+
debug.log('field.upload', 'refused', { surface: 'telegram', reason: failure });
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
if (failure !== null) {
|
|
1440
|
+
await this.sendMessage(ctx.chatId, `현장 저장 실패 · ${event} · ${failure}`, { replyTo: ctx.messageId, threadId: ctx.threadId });
|
|
1441
|
+
return true;
|
|
1442
|
+
}
|
|
1443
|
+
const line = (n: number) => `현장 폴더에 저장 · ${event} · 지금 ${n}장`;
|
|
1444
|
+
if (!ctx.mediaGroupId) {
|
|
1445
|
+
await this.sendMessage(ctx.chatId, line(count!), { replyTo: ctx.messageId, threadId: ctx.threadId });
|
|
1446
|
+
return true;
|
|
1447
|
+
}
|
|
1448
|
+
// 앨범은 항목마다 답하지 않는다 — 마지막 항목 뒤 한 번(디바운스)만 최신 수로 답한다.
|
|
1449
|
+
const entry = group ?? { event, at: now, chatId: ctx.chatId, threadId: ctx.threadId, replyTo: ctx.messageId, count: 0 };
|
|
1450
|
+
entry.at = now;
|
|
1451
|
+
entry.count = count!;
|
|
1452
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
1453
|
+
entry.timer = setTimeout(() => {
|
|
1454
|
+
entry.timer = undefined;
|
|
1455
|
+
void this.sendMessage(entry.chatId, line(entry.count), { replyTo: entry.replyTo, threadId: entry.threadId })
|
|
1456
|
+
.catch((e) => this.log(`field reply failed: ${e instanceof Error ? e.message : String(e)}`));
|
|
1457
|
+
}, 1500);
|
|
1458
|
+
(entry.timer as { unref?: () => void }).unref?.();
|
|
1459
|
+
this.fieldGroups.set(ctx.mediaGroupId, entry);
|
|
1460
|
+
return true;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1350
1463
|
private async handleIncoming(ctx: TgIncoming): Promise<void> {
|
|
1351
1464
|
if (!this.isOwnerAllowed(ctx.userId)) {
|
|
1352
1465
|
await this.refuseMessage(ctx);
|
|
1353
1466
|
return;
|
|
1354
1467
|
}
|
|
1468
|
+
// 현장 업로드 — `#현장` 캡션(또는 태그된 앨범의 나머지)의 사진·영상은 LLM 대신 현장 폴더로.
|
|
1469
|
+
if (await this.tryHandleFieldUpload(ctx)) return;
|
|
1470
|
+
if (ctx.text.startsWith('@')) {
|
|
1471
|
+
const reply = await handleTelegramSeatWork(ctx.text, {
|
|
1472
|
+
chatId: ctx.chatId, messageId: ctx.messageId, threadId: ctx.threadId, botId: ctx.botId,
|
|
1473
|
+
}, this.seatWorkDeps);
|
|
1474
|
+
if (reply !== null) {
|
|
1475
|
+
await this.sendMessage(ctx.chatId, reply, { replyTo: ctx.messageId, threadId: ctx.threadId });
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
// 영상은 `#현장` 저장 전용으로만 파싱한다 — 그 밖의 영상은 종전처럼 무시한다.
|
|
1480
|
+
if (ctx.video && ctx.attachments.length === 0) return;
|
|
1355
1481
|
// Cascade-zyu U2 — capture utterance intent at update ingress.
|
|
1356
1482
|
try {
|
|
1357
1483
|
const { userIntentLogger } = await import('./user-intent/index.js');
|
|
@@ -2069,6 +2195,7 @@ export class TelegramBot {
|
|
|
2069
2195
|
interface PhotoSize { file_id: string; file_size?: number; width: number; height: number }
|
|
2070
2196
|
interface VoiceLike { file_id: string; file_size?: number; mime_type?: string; duration?: number }
|
|
2071
2197
|
interface DocumentLike { file_id: string; file_size?: number; mime_type?: string; file_name?: string }
|
|
2198
|
+
interface VideoLike { file_id: string; file_size?: number; mime_type?: string; file_name?: string; duration?: number }
|
|
2072
2199
|
|
|
2073
2200
|
interface RawUpdate {
|
|
2074
2201
|
update_id: number;
|
|
@@ -2083,6 +2210,8 @@ interface RawUpdate {
|
|
|
2083
2210
|
voice?: VoiceLike;
|
|
2084
2211
|
audio?: VoiceLike;
|
|
2085
2212
|
document?: DocumentLike;
|
|
2213
|
+
video?: VideoLike;
|
|
2214
|
+
media_group_id?: string;
|
|
2086
2215
|
reply_to_message?: { text?: string; message_id?: number };
|
|
2087
2216
|
};
|
|
2088
2217
|
callback_query?: {
|
|
@@ -2175,8 +2304,20 @@ export function parseUpdate(u: RawUpdate): TgIncoming | null {
|
|
|
2175
2304
|
});
|
|
2176
2305
|
}
|
|
2177
2306
|
|
|
2307
|
+
// 영상은 `#현장` 저장 전용 — 태그된 캡션이거나 앨범 항목일 때만 싣는다(그 밖의 영상은 종전처럼 무시).
|
|
2308
|
+
const video = m.video && (parseFieldCaption(m.caption) || m.media_group_id)
|
|
2309
|
+
? {
|
|
2310
|
+
kind: 'document' as const,
|
|
2311
|
+
fileId: m.video.file_id,
|
|
2312
|
+
fileSize: m.video.file_size,
|
|
2313
|
+
mimeType: m.video.mime_type ?? 'video/mp4',
|
|
2314
|
+
fileName: m.video.file_name,
|
|
2315
|
+
duration: m.video.duration,
|
|
2316
|
+
}
|
|
2317
|
+
: undefined;
|
|
2318
|
+
|
|
2178
2319
|
// Skip updates with zero signal: no text AND no attachments.
|
|
2179
|
-
if (typeof m.text !== 'string' && attachments.length === 0) return null;
|
|
2320
|
+
if (typeof m.text !== 'string' && attachments.length === 0 && !video) return null;
|
|
2180
2321
|
|
|
2181
2322
|
const isDm = m.chat.type === 'private';
|
|
2182
2323
|
return {
|
|
@@ -2191,6 +2332,8 @@ export function parseUpdate(u: RawUpdate): TgIncoming | null {
|
|
|
2191
2332
|
isGroup: !isDm,
|
|
2192
2333
|
attachments,
|
|
2193
2334
|
...(m.reply_to_message?.text ? { replyToText: m.reply_to_message.text } : {}),
|
|
2335
|
+
...(m.media_group_id ? { mediaGroupId: m.media_group_id } : {}),
|
|
2336
|
+
...(video ? { video } : {}),
|
|
2194
2337
|
};
|
|
2195
2338
|
}
|
|
2196
2339
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// `elanous_skills_list` MCP tool. Mirrors the daemon-side ACP method
|
|
3
3
|
// `elanous/skills/list` (src/acp/server.ts:2241) but session-agnostic so
|
|
4
4
|
// the codex app-server can call it through the elanous-tools MCP server
|
|
5
|
-
// without first opening an ACP session. Enumerates
|
|
5
|
+
// without first opening an ACP session. Enumerates configured skill roots
|
|
6
6
|
// and surfaces the first non-heading line of each SKILL.md as the
|
|
7
7
|
// description.
|
|
8
8
|
//
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { join } from 'node:path';
|
|
15
15
|
import { readdir, readFile } from 'node:fs/promises';
|
|
16
16
|
import { defaultSkillDirs } from '../user-config.js';
|
|
17
|
+
import { SKILLS_LIST_DESCRIPTION } from '../onboarding/entry-hints.js';
|
|
17
18
|
import type { LLMToolSpec } from '../llm.js';
|
|
18
19
|
import type { ToolRuntime } from './types.js';
|
|
19
20
|
|
|
@@ -41,8 +42,7 @@ export interface ElanousSkillsListResult extends Record<string, unknown> {
|
|
|
41
42
|
export function buildElanousSkillsListTool(): LLMToolSpec {
|
|
42
43
|
return {
|
|
43
44
|
name: 'elanous_skills_list',
|
|
44
|
-
description:
|
|
45
|
-
'List installed elanous skills (sub-directories of ~/.elanous/skills with optional SKILL.md). Codex app-server callback via the elanous-tools MCP server. Read-only.',
|
|
45
|
+
description: SKILLS_LIST_DESCRIPTION,
|
|
46
46
|
parameters: {
|
|
47
47
|
type: 'object',
|
|
48
48
|
properties: {
|