elanous 0.2.6 → 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 +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-2eba6079f31b88d4.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/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-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-10ed24f413ed66e9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-211810b6c93ab66a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-624f7a2f89b60c8f.js +1 -0
- 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-55d50e9b0006a91d.js +1 -0
- 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-37b8bf78a3c6eec5.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/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/package.json +5 -5
- 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 +24 -3
- 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 +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/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/env-known-failures.ts +31 -0
- package/scripts/release-loop/gate-node.ts +81 -19
- 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/unattended-release.ts +93 -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/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/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/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/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 +42 -1
- 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 +50 -11
- 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 +8 -3
- 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 +86 -14
- 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 +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 +61 -7
- 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 +16 -10
- 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 +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 +9 -8
- 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/session/chat.ts +17 -5
- 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-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 +38 -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 +80 -4
- 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 → keajW7DDyghEs1y8oJe6T}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// EV10d — «게시 대기» 피드 초안: 승인 노드가 `{"kind":"feed-preview"}` 이면 그 런의 현장폴더 초안
|
|
2
|
+
// (`<folder>/feed/feed-draft.json`)이 정본이다(계약 = MK 10-01 · field-feed 그래프). 경로는 «런 상태의
|
|
3
|
+
// input.folder» 에서만 구하고, 그 폴더는 `<configDir>/field/<slug>` 꼴일 때만 받는다(realpath 로 탈출 거부).
|
|
4
|
+
// 사람의 «수정 저장»은 허용된 칸만 고쳐 그 파일을 덮는다 — 승인 상태는 바꾸지 않는다.
|
|
5
|
+
import { existsSync, readFileSync, realpathSync, renameSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { basename, dirname, join, normalize, sep } from 'node:path';
|
|
7
|
+
import { fieldEventDir, isFieldSlug } from '../../field/field-media.js';
|
|
8
|
+
import type { GraphRunState } from '../../graph-runner/runner.js';
|
|
9
|
+
|
|
10
|
+
export interface FeedSlide { image: string; caption: string; include: boolean; [key: string]: unknown }
|
|
11
|
+
export interface FeedDraft {
|
|
12
|
+
kind: 'feed-draft';
|
|
13
|
+
version: 1;
|
|
14
|
+
revision: number;
|
|
15
|
+
updatedAt: string;
|
|
16
|
+
updatedBy: string;
|
|
17
|
+
slides: FeedSlide[];
|
|
18
|
+
caption: { hook: string; body: string };
|
|
19
|
+
hashtags: string[];
|
|
20
|
+
cover: { text: string; sub: string; image?: string; [key: string]: unknown };
|
|
21
|
+
location: string | null;
|
|
22
|
+
reel: string | null;
|
|
23
|
+
brand?: { name?: string; handle?: string; avatar?: string | null };
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Fields a person may change from the PWA — everything else (rendered*, source, folder…) is preserved. */
|
|
28
|
+
export interface FeedEdit {
|
|
29
|
+
slides: Array<{ image: string; caption?: string; include?: boolean }>;
|
|
30
|
+
caption: { hook: string; body: string };
|
|
31
|
+
hashtags: string[];
|
|
32
|
+
cover: { text: string; sub: string };
|
|
33
|
+
location: string | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const FEED_DRAFT_FILE = join('feed', 'feed-draft.json');
|
|
37
|
+
|
|
38
|
+
export function isFeedPreviewMessage(message: string): boolean {
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(message) as unknown;
|
|
41
|
+
return !!parsed && typeof parsed === 'object' && (parsed as { kind?: unknown }).kind === 'feed-preview';
|
|
42
|
+
} catch { return false; }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function real(path: string): string | null {
|
|
46
|
+
try { return realpathSync(path); } catch { return null; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The run's field folder when it is exactly `<configDir>/field/<slug>` (after resolving links), else null. */
|
|
50
|
+
export function feedFolderFor(state: Pick<GraphRunState, 'input'>, configDir: string): string | null {
|
|
51
|
+
const input = state.input as { folder?: unknown } | undefined;
|
|
52
|
+
const folder = typeof input?.folder === 'string' ? input.folder : null;
|
|
53
|
+
if (!folder) return null;
|
|
54
|
+
const slug = basename(folder);
|
|
55
|
+
if (!isFieldSlug(slug)) return null;
|
|
56
|
+
const resolved = real(folder);
|
|
57
|
+
const expected = real(fieldEventDir(configDir, slug));
|
|
58
|
+
if (!resolved || !expected || resolved !== expected) return null;
|
|
59
|
+
if (dirname(resolved) !== real(join(configDir, 'field'))) return null;
|
|
60
|
+
return resolved;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function str(value: unknown): string { return typeof value === 'string' ? value : ''; }
|
|
64
|
+
|
|
65
|
+
export function readFeedDraft(folder: string): FeedDraft | null {
|
|
66
|
+
try {
|
|
67
|
+
const raw = JSON.parse(readFileSync(join(folder, FEED_DRAFT_FILE), 'utf8')) as Partial<FeedDraft>;
|
|
68
|
+
if (raw.kind !== 'feed-draft' || raw.version !== 1 || !Array.isArray(raw.slides)) return null;
|
|
69
|
+
return raw as FeedDraft;
|
|
70
|
+
} catch { return null; }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type FeedEditResult = { ok: true; draft: FeedDraft } | { ok: false; error: string };
|
|
74
|
+
|
|
75
|
+
/** Apply a person's edit to the current draft. Slides must be the same set of images (order may change,
|
|
76
|
+
* removal is `include:false`), so no new path can enter the draft. */
|
|
77
|
+
export function applyFeedEdit(current: FeedDraft, edit: unknown, now = new Date()): FeedEditResult {
|
|
78
|
+
if (!edit || typeof edit !== 'object') return { ok: false, error: 'bad_request' };
|
|
79
|
+
const e = edit as Partial<FeedEdit>;
|
|
80
|
+
if (!Array.isArray(e.slides) || !e.caption || typeof e.caption !== 'object' || !Array.isArray(e.hashtags)
|
|
81
|
+
|| !e.cover || typeof e.cover !== 'object') return { ok: false, error: 'bad_request' };
|
|
82
|
+
const original = new Map(current.slides.map((slide) => [slide.image, slide]));
|
|
83
|
+
const seen = new Set<string>();
|
|
84
|
+
const slides: FeedSlide[] = [];
|
|
85
|
+
for (const raw of e.slides) {
|
|
86
|
+
const image = str((raw as { image?: unknown })?.image);
|
|
87
|
+
const base = original.get(image);
|
|
88
|
+
if (!base || seen.has(image)) return { ok: false, error: 'unknown-image' };
|
|
89
|
+
seen.add(image);
|
|
90
|
+
const caption = (raw as { caption?: unknown }).caption;
|
|
91
|
+
const include = (raw as { include?: unknown }).include;
|
|
92
|
+
slides.push({ ...base, ...(typeof caption === 'string' ? { caption } : {}), ...(typeof include === 'boolean' ? { include } : {}) });
|
|
93
|
+
}
|
|
94
|
+
if (seen.size !== original.size) return { ok: false, error: 'missing-image' };
|
|
95
|
+
const location = e.location === null ? null : typeof e.location === 'string' ? e.location : current.location;
|
|
96
|
+
return {
|
|
97
|
+
ok: true,
|
|
98
|
+
draft: {
|
|
99
|
+
...current,
|
|
100
|
+
slides,
|
|
101
|
+
caption: { ...current.caption, hook: str(e.caption.hook), body: str(e.caption.body) },
|
|
102
|
+
hashtags: e.hashtags.filter((tag): tag is string => typeof tag === 'string').map((tag) => tag.trim()).filter(Boolean),
|
|
103
|
+
cover: { ...current.cover, text: str(e.cover.text), sub: str(e.cover.sub) },
|
|
104
|
+
location,
|
|
105
|
+
revision: (Number.isFinite(current.revision) ? current.revision : 0) + 1,
|
|
106
|
+
updatedBy: 'human',
|
|
107
|
+
updatedAt: now.toISOString(),
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function writeFeedDraft(folder: string, draft: FeedDraft): void {
|
|
113
|
+
const target = join(folder, FEED_DRAFT_FILE);
|
|
114
|
+
const temp = `${target}.tmp-${process.pid}`;
|
|
115
|
+
writeFileSync(temp, `${JSON.stringify(draft, null, 2)}\n`);
|
|
116
|
+
renameSync(temp, target);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const MEDIA_RE = /^(?:feed\/[^/\\]+\.(?:png|jpe?g|webp)|reel\/reel-9x16\.mp4)$/i;
|
|
120
|
+
|
|
121
|
+
/** Absolute file for a folder-relative media path, only `feed/*.png|jpg|jpeg|webp` and `reel/reel-9x16.mp4`. */
|
|
122
|
+
export function resolveFeedMedia(folder: string, relative: string): string | null {
|
|
123
|
+
if (!relative || relative.startsWith('/') || relative.includes('\\') || relative.includes('\0')) return null;
|
|
124
|
+
const clean = normalize(relative).split(sep).join('/');
|
|
125
|
+
if (clean !== relative || !MEDIA_RE.test(clean)) return null;
|
|
126
|
+
const file = join(folder, clean);
|
|
127
|
+
if (!existsSync(file)) return null;
|
|
128
|
+
const resolved = real(file);
|
|
129
|
+
const root = real(folder);
|
|
130
|
+
if (!resolved || !root || !resolved.startsWith(`${root}${sep}`)) return null;
|
|
131
|
+
return resolved;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function mediaContentType(path: string): string {
|
|
135
|
+
const lower = path.toLowerCase();
|
|
136
|
+
if (lower.endsWith('.png')) return 'image/png';
|
|
137
|
+
if (lower.endsWith('.webp')) return 'image/webp';
|
|
138
|
+
if (lower.endsWith('.mp4')) return 'video/mp4';
|
|
139
|
+
return 'image/jpeg';
|
|
140
|
+
}
|
|
@@ -3,7 +3,9 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { debug } from '../../debug/log.js';
|
|
4
4
|
import { decideGraphApproval, lastJsonObject, type GraphRunState } from '../../graph-runner/runner.js';
|
|
5
5
|
import { effectiveInstanceRoot } from '../../instance/resolve.js';
|
|
6
|
+
import { getElanousConfigDir } from '../../elanous-config-dir.js';
|
|
6
7
|
import { jsonResponse } from './json-response.js';
|
|
8
|
+
import { applyFeedEdit, feedFolderFor, isFeedPreviewMessage, mediaContentType, readFeedDraft, resolveFeedMedia, writeFeedDraft, type FeedDraft } from './graph-approvals-feed.js';
|
|
7
9
|
|
|
8
10
|
export const GRAPH_APPROVALS_PATH = '/v1/graph-approvals';
|
|
9
11
|
const SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
@@ -16,6 +18,8 @@ interface GraphApprovalItem {
|
|
|
16
18
|
since: string;
|
|
17
19
|
path: string[];
|
|
18
20
|
recent: Array<{ nodeId: string; ok: boolean; outcome?: string; summary?: string }>;
|
|
21
|
+
/** EV10d — present only for a `{"kind":"feed-preview"}` approval whose run folder holds a valid draft. */
|
|
22
|
+
feed?: Omit<FeedDraft, 'folder'>;
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
/** One decision this daemon applied through `POST /v1/graph-approvals/:graphId/:runId`. */
|
|
@@ -72,6 +76,8 @@ interface GraphApprovalsDeps {
|
|
|
72
76
|
* `tailscale serve` every tailnet peer (Pod included) arrives from loopback. */
|
|
73
77
|
authReason?: (request: Request) => string | undefined;
|
|
74
78
|
root?: string;
|
|
79
|
+
/** Config dir whose `field/<slug>` folders may hold feed drafts (EV10d). */
|
|
80
|
+
configDir?: string;
|
|
75
81
|
}
|
|
76
82
|
|
|
77
83
|
const DECISION_AUTH_REASONS = new Set(['bearer-match', 'temp-token', 'noauth']);
|
|
@@ -96,7 +102,16 @@ function undecided(state: GraphRunState, root: string): boolean {
|
|
|
96
102
|
&& Array.isArray(state.path) && !existsSync(join(root, 'graph-runs', state.graphId, `${state.runId}.json.${state.path.length}.decision.json`));
|
|
97
103
|
}
|
|
98
104
|
|
|
99
|
-
function
|
|
105
|
+
function feedFor(state: GraphRunState, configDir: string): Omit<FeedDraft, 'folder'> | undefined {
|
|
106
|
+
if (!isFeedPreviewMessage(state.pending?.message ?? '')) return undefined;
|
|
107
|
+
const folder = feedFolderFor(state, configDir);
|
|
108
|
+
const draft = folder ? readFeedDraft(folder) : null;
|
|
109
|
+
if (!draft) return undefined;
|
|
110
|
+
const { folder: _folder, ...rest } = draft;
|
|
111
|
+
return rest;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function list(root: string, configDir: string): GraphApprovalItem[] {
|
|
100
115
|
const dir = join(root, 'graph-runs');
|
|
101
116
|
let graphs: Dirent[];
|
|
102
117
|
try { graphs = readdirSync(dir, { withFileTypes: true }); }
|
|
@@ -113,6 +128,7 @@ function list(root: string): GraphApprovalItem[] {
|
|
|
113
128
|
const state = readRun(root, graph.name, runId);
|
|
114
129
|
if (!state || !undecided(state, root) || !Array.isArray(state.nodes)) continue;
|
|
115
130
|
const pending = state.pending!;
|
|
131
|
+
const feed = feedFor(state, configDir);
|
|
116
132
|
items.push({
|
|
117
133
|
graphId: state.graphId, runId: state.runId, nodeId: pending.nodeId, message: pending.message,
|
|
118
134
|
since: pending.since, path: state.path,
|
|
@@ -124,6 +140,7 @@ function list(root: string): GraphApprovalItem[] {
|
|
|
124
140
|
...(typeof output?.summary === 'string' ? { summary: output.summary } : {}),
|
|
125
141
|
};
|
|
126
142
|
}),
|
|
143
|
+
...(feed ? { feed } : {}),
|
|
127
144
|
});
|
|
128
145
|
}
|
|
129
146
|
}
|
|
@@ -131,14 +148,17 @@ function list(root: string): GraphApprovalItem[] {
|
|
|
131
148
|
}
|
|
132
149
|
|
|
133
150
|
export async function handleGraphApprovals(req: Request, deps: GraphApprovalsDeps = {}): Promise<Response> {
|
|
134
|
-
const
|
|
151
|
+
const url = new URL(req.url);
|
|
152
|
+
const pathname = url.pathname;
|
|
153
|
+
const sub = /^\/v1\/graph-approvals\/([^/]+)\/([^/]+)\/(feed-draft|media)$/.exec(pathname);
|
|
135
154
|
const match = /^\/v1\/graph-approvals\/([^/]+)\/([^/]+)$/.exec(pathname);
|
|
136
|
-
if (pathname !== GRAPH_APPROVALS_PATH && !match) return jsonResponse({ error: 'not-found' }, 404);
|
|
155
|
+
if (pathname !== GRAPH_APPROVALS_PATH && !match && !sub) return jsonResponse({ error: 'not-found' }, 404);
|
|
137
156
|
if (!deps.authorize?.(req)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
157
|
+
if (sub) return handleFeed(req, url, sub[1]!, sub[2]!, sub[3] as 'feed-draft' | 'media', deps);
|
|
138
158
|
if (pathname === GRAPH_APPROVALS_PATH) {
|
|
139
159
|
if (req.method !== 'GET') return jsonResponse({ error: 'method-not-allowed' }, 405);
|
|
140
160
|
// ⭐ `items` is unchanged; `decided` is additive (newest first) for other devices' cards.
|
|
141
|
-
return jsonResponse({ items: list(deps.root ?? effectiveInstanceRoot()), decided: (deps.decisions ?? defaultDecisionRing).list() });
|
|
161
|
+
return jsonResponse({ items: list(deps.root ?? effectiveInstanceRoot(), deps.configDir ?? getElanousConfigDir()), decided: (deps.decisions ?? defaultDecisionRing).list() });
|
|
142
162
|
}
|
|
143
163
|
if (req.method !== 'POST') return jsonResponse({ error: 'method-not-allowed' }, 405);
|
|
144
164
|
const authReason = deps.authReason?.(req);
|
|
@@ -172,3 +192,50 @@ export async function handleGraphApprovals(req: Request, deps: GraphApprovalsDep
|
|
|
172
192
|
debug.log('approvals.graph', 'decided', { graphId, runId, nodeId, decision, by: `pwa:${authReason}` });
|
|
173
193
|
return jsonResponse({ graphId, runId, decision });
|
|
174
194
|
}
|
|
195
|
+
|
|
196
|
+
/** EV10d — `PUT …/:graphId/:runId/feed-draft` (사람 수정 저장) · `GET …/:graphId/:runId/media?path=` (초안 미디어). */
|
|
197
|
+
async function handleFeed(req: Request, url: URL, rawGraphId: string, rawRunId: string, kind: 'feed-draft' | 'media', deps: GraphApprovalsDeps): Promise<Response> {
|
|
198
|
+
let graphId: string;
|
|
199
|
+
let runId: string;
|
|
200
|
+
try { graphId = decodeURIComponent(rawGraphId); runId = decodeURIComponent(rawRunId); }
|
|
201
|
+
catch { return jsonResponse({ error: 'not-found' }, 404); }
|
|
202
|
+
if (!validSegment(graphId) || !validSegment(runId)) return jsonResponse({ error: 'not-found' }, 404);
|
|
203
|
+
const root = deps.root ?? effectiveInstanceRoot();
|
|
204
|
+
const state = readRun(root, graphId, runId);
|
|
205
|
+
const folder = state && isFeedPreviewMessage(state.pending?.message ?? '') ? feedFolderFor(state, deps.configDir ?? getElanousConfigDir()) : null;
|
|
206
|
+
if (!state || !folder) {
|
|
207
|
+
debug.log('approvals.feed', 'refused', { graphId, reason: 'not-feed' });
|
|
208
|
+
return jsonResponse({ error: 'not-found' }, 404);
|
|
209
|
+
}
|
|
210
|
+
if (kind === 'media') {
|
|
211
|
+
if (req.method !== 'GET') return jsonResponse({ error: 'method-not-allowed' }, 405);
|
|
212
|
+
const file = resolveFeedMedia(folder, url.searchParams.get('path') ?? '');
|
|
213
|
+
if (!file) {
|
|
214
|
+
debug.log('approvals.feed', 'refused', { graphId, reason: 'media-path' });
|
|
215
|
+
return jsonResponse({ error: 'not-found' }, 404);
|
|
216
|
+
}
|
|
217
|
+
debug.log('approvals.feed', 'media-served', { graphId });
|
|
218
|
+
return new Response(Bun.file(file), { headers: { 'content-type': mediaContentType(file), 'cache-control': 'private, max-age=60' } });
|
|
219
|
+
}
|
|
220
|
+
if (req.method !== 'PUT') return jsonResponse({ error: 'method-not-allowed' }, 405);
|
|
221
|
+
const authReason = deps.authReason?.(req);
|
|
222
|
+
if (!authReason || !DECISION_AUTH_REASONS.has(authReason)) {
|
|
223
|
+
debug.log('approvals.feed', 'refused', { graphId, reason: 'pairing-required' });
|
|
224
|
+
return jsonResponse({ error: 'pairing-required' }, 403);
|
|
225
|
+
}
|
|
226
|
+
if (!undecided(state, root)) return jsonResponse({ error: 'already-decided' }, 409);
|
|
227
|
+
const current = readFeedDraft(folder);
|
|
228
|
+
if (!current) return jsonResponse({ error: 'not-found' }, 404);
|
|
229
|
+
let body: unknown;
|
|
230
|
+
try { body = await req.json(); }
|
|
231
|
+
catch { return jsonResponse({ error: 'bad_request' }, 400); }
|
|
232
|
+
const result = applyFeedEdit(current, body);
|
|
233
|
+
if (!result.ok) {
|
|
234
|
+
debug.log('approvals.feed', 'refused', { graphId, reason: result.error });
|
|
235
|
+
return jsonResponse({ error: result.error }, 400);
|
|
236
|
+
}
|
|
237
|
+
writeFeedDraft(folder, result.draft);
|
|
238
|
+
debug.log('approvals.feed', 'draft-saved', { graphId, revision: result.draft.revision });
|
|
239
|
+
const { folder: _folder, ...feed } = result.draft;
|
|
240
|
+
return jsonResponse({ graphId, runId, feed });
|
|
241
|
+
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// Port policy (N1-2 decision): auto-pick from 31415 upward. Actual port
|
|
16
16
|
// is recorded in runtime.json + printed in boot banner.
|
|
17
17
|
|
|
18
|
+
import { getUserConfig } from '../../user-config.js';
|
|
18
19
|
import type { TelegramEvent } from '../../workflow-runtime/triggers/telegram-source.js';
|
|
19
20
|
import { spawnSync } from 'node:child_process';
|
|
20
21
|
import { compareTokenConstTime } from '../../acp/transport/auth.js';
|
|
@@ -198,7 +199,11 @@ import { handleMcpResourceGet, MCP_RESOURCE_ROUTE_PATH } from './mcp-resource-ro
|
|
|
198
199
|
import { APPROVALS_MERGES_PATH, DESIGN_DIRECTION_PATH, DESIGN_PREVIEW_PATH_PREFIX, DESIGN_PREVIEWS_PATH, DESIGN_SYSTEM_PATH, IPA_PATH_PREFIX, MANIFEST_PATH } from './rest-route-paths.js';
|
|
199
200
|
import { handleMergeApprovals } from './merge-approvals.js';
|
|
200
201
|
import { GRAPH_APPROVALS_PATH, handleGraphApprovals } from './graph-approvals.js';
|
|
201
|
-
import {
|
|
202
|
+
import { handleExecRequests } from './exec-requests.js';
|
|
203
|
+
import { handleSeatRequests, SEAT_REQUESTS_PATH, type SeatRequestsDeps } from './seat-requests.js';
|
|
204
|
+
import { handleConsultRequests, CONSULT_REQUESTS_PATH, type ConsultRequestsDeps } from './consult-requests.js';
|
|
205
|
+
import { ExecRequestRunner } from '../../exec-requests/runner.js';
|
|
206
|
+
import { FIELD_SERVER_MAX_BODY_BYTES, FIELD_UPLOADS_PATH, FIELD_REEL_PATH, FIELD_REEL_FILE_PATH, handleFieldUploads } from './field-uploads.js';
|
|
202
207
|
import { handleLiveDetail, LIVE_DETAIL_PATH } from './live-detail.js';
|
|
203
208
|
import { handleLiveShipped, LIVE_SHIPPED_PATH } from './live-shipped.js';
|
|
204
209
|
import { handleMcpWidgetCall, MCP_WIDGET_CALL_ROUTE_PATH, persistWidgetTurnToSessionStore } from './mcp-widget-call-route.js';
|
|
@@ -236,6 +241,7 @@ import {
|
|
|
236
241
|
parseErrorsPath,
|
|
237
242
|
} from './errors.js';
|
|
238
243
|
import { handleChatBackendDetection } from './chat-backend-detection.js';
|
|
244
|
+
import { handleChatFastPathConfig } from './chat-fast-path-config.js';
|
|
239
245
|
import { connectInfoTokenAllowed, handleConnectInfoGet, handleConnectTokenMint, type ConnectInfoCtx } from './connect-info.js';
|
|
240
246
|
import { handleStaticAppRequest, pathMatchesStaticPrefix } from './static-app.js';
|
|
241
247
|
import {
|
|
@@ -446,6 +452,9 @@ export type NexusWsBridgeInit = Omit<WsBridgeOpts, 'hostname' | 'port'>;
|
|
|
446
452
|
export interface NexusHttpServerOpts {
|
|
447
453
|
/** Optional installer state root for isolated API consumers and tests. */
|
|
448
454
|
pluginStateRoot?: string;
|
|
455
|
+
execRequests?: ExecRequestRunner;
|
|
456
|
+
seatRequests?: SeatRequestsDeps;
|
|
457
|
+
consultRequests?: ConsultRequestsDeps;
|
|
449
458
|
state: NexusState;
|
|
450
459
|
registry: TabRegistry;
|
|
451
460
|
eventBus: NexusEventBus;
|
|
@@ -748,6 +757,8 @@ export function startNexusHttpServer(opts: NexusHttpServerOpts): NexusHttpServer
|
|
|
748
757
|
...(websocketHandler ? { websocket: websocketHandler } : {}),
|
|
749
758
|
} as unknown as Parameters<typeof Bun.serve>[0];
|
|
750
759
|
server = Bun.serve(fetchOpts);
|
|
760
|
+
// 데몬이 «실제로» 떴을 때만 재시작 복구(맡긴 일) — 주입된 러너(시험)는 건드리지 않는다.
|
|
761
|
+
if (!opts.execRequests) { try { execRequestRunner().reconcileInterrupted(); } catch { /* 복구 실패가 서버 기동을 막지 않는다 */ } }
|
|
751
762
|
resolvedPort = port;
|
|
752
763
|
break;
|
|
753
764
|
} catch (err) {
|
|
@@ -786,6 +797,13 @@ function isPortBusy(err: unknown): boolean {
|
|
|
786
797
|
return /EADDRINUSE|address already in use/i.test(msg);
|
|
787
798
|
}
|
|
788
799
|
|
|
800
|
+
let execRequestRunnerInstance: ExecRequestRunner | undefined;
|
|
801
|
+
/** 게으른 한 벌 — import 만으로는 만들지 않는다(재시작 복구가 import 에 묶이지 않게). */
|
|
802
|
+
function execRequestRunner(): ExecRequestRunner {
|
|
803
|
+
execRequestRunnerInstance ??= new ExecRequestRunner();
|
|
804
|
+
return execRequestRunnerInstance;
|
|
805
|
+
}
|
|
806
|
+
|
|
789
807
|
export async function routeRequest(
|
|
790
808
|
req: Request,
|
|
791
809
|
opts: NexusHttpServerOpts,
|
|
@@ -834,16 +852,26 @@ export async function routeRequest(
|
|
|
834
852
|
// Per-route checkAuth calls below stay in place.
|
|
835
853
|
// OPTIONS is a CORS preflight and cannot carry Authorization; existing
|
|
836
854
|
// handlers answer it themselves, so the gate does not swallow it.
|
|
837
|
-
//
|
|
838
|
-
//
|
|
839
|
-
const
|
|
840
|
-
|
|
855
|
+
// Without metaApi, only the owner bearer may reach these runtime-less
|
|
856
|
+
// meta-API stubs. All other private paths remain default-denied.
|
|
857
|
+
const runtimeLessMetaRoute = !opts.metaApi && (
|
|
858
|
+
(method !== 'GET' && method !== 'OPTIONS'
|
|
859
|
+
&& (pathname === '/v1/nexus/tabs' || pathname.startsWith('/v1/nexus/tabs/')))
|
|
860
|
+
|| (method === 'GET' && (
|
|
861
|
+
['/v1/simulations', '/v1/tools', '/v1/intake', '/v1/sessions', '/v1/control-signals', '/v1/turns/last/screenshot'].includes(pathname)
|
|
862
|
+
|| pathname.startsWith('/v1/recordings/')
|
|
863
|
+
))
|
|
864
|
+
|| (method === 'POST' && (
|
|
865
|
+
['/v1/intake', '/v1/sessions/external', '/v1/control-signals', '/v1/prompt'].includes(pathname)
|
|
866
|
+
|| pathname.startsWith('/v1/hitl/callback/')
|
|
867
|
+
))
|
|
868
|
+
);
|
|
841
869
|
if (
|
|
842
870
|
method !== 'OPTIONS'
|
|
843
871
|
&& pathname.startsWith('/v1/')
|
|
844
872
|
&& !isPublicRoute(method, pathname, { setupMode: false })
|
|
845
873
|
) {
|
|
846
|
-
const fallbackToken =
|
|
874
|
+
const fallbackToken = runtimeLessMetaRoute ? ensureAuthToken().token : undefined;
|
|
847
875
|
const offered = req.headers.get('authorization');
|
|
848
876
|
const fallbackAuthorized = fallbackToken !== undefined && offered?.startsWith('Bearer ') === true
|
|
849
877
|
&& compareTokenConstTime(offered.slice('Bearer '.length).trim(), fallbackToken);
|
|
@@ -919,6 +947,25 @@ export async function routeRequest(
|
|
|
919
947
|
});
|
|
920
948
|
}
|
|
921
949
|
|
|
950
|
+
if (pathname === SEAT_REQUESTS_PATH) {
|
|
951
|
+
if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
952
|
+
return handleSeatRequests(req, opts.seatRequests);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
if (pathname === CONSULT_REQUESTS_PATH) {
|
|
956
|
+
if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
957
|
+
return handleConsultRequests(req, opts.consultRequests);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
if (pathname === '/v1/exec-requests' || pathname.startsWith('/v1/exec-requests/')) {
|
|
961
|
+
if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
962
|
+
if (/^\/v1\/exec-requests\/[^/]+\/files\//.test(pathname)) {
|
|
963
|
+
const credential = bearerCredential(req, opts.metaApi);
|
|
964
|
+
if (credential !== 'bearer-match' && credential !== 'temp-token') return jsonResponse({ error: 'unauthorized' }, 401);
|
|
965
|
+
}
|
|
966
|
+
return handleExecRequests(req, opts.execRequests ?? execRequestRunner());
|
|
967
|
+
}
|
|
968
|
+
|
|
922
969
|
if (pathname === GRAPH_APPROVALS_PATH || pathname.startsWith(`${GRAPH_APPROVALS_PATH}/`)) {
|
|
923
970
|
return handleGraphApprovals(req, {
|
|
924
971
|
authorize: (request) => !!opts.metaApi && checkAuth(request, opts.metaApi),
|
|
@@ -926,9 +973,11 @@ export async function routeRequest(
|
|
|
926
973
|
});
|
|
927
974
|
}
|
|
928
975
|
// 현장 업로드(owner 전용) — GET 목록 · POST 저장. 저장 규칙 정본 = src/field/field-media.ts.
|
|
929
|
-
if (pathname === FIELD_UPLOADS_PATH && (method === 'GET' || method === 'POST'))
|
|
976
|
+
if ((pathname === FIELD_UPLOADS_PATH && (method === 'GET' || method === 'POST'))
|
|
977
|
+
|| ((pathname === FIELD_REEL_PATH || pathname === FIELD_REEL_FILE_PATH) && method === 'GET')) {
|
|
930
978
|
return handleFieldUploads(req, {
|
|
931
979
|
authorize: (request) => !!opts.metaApi && checkAuth(request, opts.metaApi),
|
|
980
|
+
configuredEvent: () => { try { return getUserConfig().telegram?.fieldDefaultEvent; } catch { return undefined; } },
|
|
932
981
|
});
|
|
933
982
|
}
|
|
934
983
|
if (pathname === APPROVALS_MERGES_PATH || pathname.startsWith(`${APPROVALS_MERGES_PATH}/`)) {
|
|
@@ -1217,6 +1266,11 @@ export async function routeRequest(
|
|
|
1217
1266
|
return handleGraphsValidatePost(req, opts.metaApi);
|
|
1218
1267
|
}
|
|
1219
1268
|
|
|
1269
|
+
if (pathname === '/v1/config/chat-fast-path' && (method === 'GET' || method === 'PUT')) {
|
|
1270
|
+
if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
1271
|
+
return handleChatFastPathConfig(req);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1220
1274
|
// Mutation routes (PR ι) — POST/PATCH/DELETE on /v1/nexus/tabs[/:id[/action]].
|
|
1221
1275
|
// Templates POST handled here too (PR κ).
|
|
1222
1276
|
// Config / secrets PUT/POST/DELETE here too (PR μ).
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { debug } from '../../debug/log.js';
|
|
5
|
+
import { effectiveInstanceRoot } from '../../instance/resolve.js';
|
|
6
|
+
import { submitIntakeWork } from '../../intake-plane/submit-intake-work.js';
|
|
7
|
+
import { parseSeatAddress, resolveSeat } from '../../seat-address/seat-address.js';
|
|
8
|
+
import trackData from '../../../scripts/coord-tracks.json';
|
|
9
|
+
import { jsonResponse } from './json-response.js';
|
|
10
|
+
|
|
11
|
+
export const SEAT_REQUESTS_PATH = '/v1/seat-requests';
|
|
12
|
+
|
|
13
|
+
interface SeatRequestItem {
|
|
14
|
+
receiptId: string;
|
|
15
|
+
seat: string;
|
|
16
|
+
text: string;
|
|
17
|
+
queuedAt: string;
|
|
18
|
+
status: 'queued';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface PendingRequest {
|
|
22
|
+
status: 'pending';
|
|
23
|
+
key: string;
|
|
24
|
+
seat: string;
|
|
25
|
+
text: string;
|
|
26
|
+
queuedAt: string;
|
|
27
|
+
ref: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface RejectedRequest extends Omit<PendingRequest, 'status'> {
|
|
31
|
+
status: 'rejected';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type RecordEntry = (SeatRequestItem & { key: string }) | PendingRequest | RejectedRequest;
|
|
35
|
+
|
|
36
|
+
export interface SeatRequestsDeps {
|
|
37
|
+
root?: () => string;
|
|
38
|
+
submit?: typeof submitIntakeWork;
|
|
39
|
+
now?: () => string;
|
|
40
|
+
append?: (path: string, entry: RecordEntry) => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const seats = trackData.tracks.filter((entry): entry is typeof entry & { title: string } => 'title' in entry)
|
|
44
|
+
.map(({ id, title }) => ({ id, title }));
|
|
45
|
+
const inFlight = new Map<string, { payload: string; task: Promise<Response> }>();
|
|
46
|
+
|
|
47
|
+
function unknownSeat(): Response {
|
|
48
|
+
debug.log('seat-address.pwa', 'rejected', { reason: 'unknown-seat' });
|
|
49
|
+
return jsonResponse({ error: 'unknown-seat', seats }, 400);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function file(root: string): string {
|
|
53
|
+
return join(root, 'seat-requests', 'requests.jsonl');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function append(path: string, entry: RecordEntry): void {
|
|
57
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
58
|
+
// Atomically replace the journal so a failed write cannot leave a truncated JSON line.
|
|
59
|
+
const previous = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
60
|
+
const temp = `${path}.${randomUUID()}.tmp`;
|
|
61
|
+
try {
|
|
62
|
+
writeFileSync(temp, `${previous}${JSON.stringify(entry)}\n`);
|
|
63
|
+
const fd = openSync(temp, 'r');
|
|
64
|
+
try { fsyncSync(fd); } finally { closeSync(fd); }
|
|
65
|
+
renameSync(temp, path);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
try { unlinkSync(temp); } catch { /* no temp file to remove */ }
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function records(path: string): Map<string, RecordEntry> {
|
|
73
|
+
let data: string;
|
|
74
|
+
try { data = readFileSync(path, 'utf8'); }
|
|
75
|
+
catch (error) {
|
|
76
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT' && !existsSync(path)) data = '';
|
|
77
|
+
else throw error;
|
|
78
|
+
}
|
|
79
|
+
const entries = new Map<string, RecordEntry>();
|
|
80
|
+
for (const line of data.split('\n')) {
|
|
81
|
+
if (!line) continue;
|
|
82
|
+
const entry = JSON.parse(line) as RecordEntry;
|
|
83
|
+
if (typeof entry.key !== 'string' || typeof entry.seat !== 'string' || typeof entry.text !== 'string'
|
|
84
|
+
|| typeof entry.queuedAt !== 'string' || !['pending', 'queued', 'rejected'].includes(entry.status)
|
|
85
|
+
|| (entry.status === 'queued' && typeof entry.receiptId !== 'string')
|
|
86
|
+
|| (entry.status !== 'queued' && typeof entry.ref !== 'string')) {
|
|
87
|
+
throw new Error('invalid seat request journal');
|
|
88
|
+
}
|
|
89
|
+
entries.set(entry.key, entry);
|
|
90
|
+
}
|
|
91
|
+
const dir = join(dirname(path), 'outcomes');
|
|
92
|
+
let names: string[];
|
|
93
|
+
try { names = readdirSync(dir); }
|
|
94
|
+
catch (error) {
|
|
95
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return entries;
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
for (const name of names) {
|
|
99
|
+
if (!/^[a-f0-9]{64}\.json$/.test(name)) continue;
|
|
100
|
+
const item = JSON.parse(readFileSync(join(dir, name), 'utf8')) as RecordEntry;
|
|
101
|
+
if (item.key !== name.slice(0, -5) || (item.status !== 'queued' && item.status !== 'rejected')) {
|
|
102
|
+
throw new Error('invalid seat request outcome');
|
|
103
|
+
}
|
|
104
|
+
const current = entries.get(item.key);
|
|
105
|
+
if (current?.status === 'queued') continue;
|
|
106
|
+
if (current?.status === 'pending') {
|
|
107
|
+
if (item.status === 'rejected' && current.ref !== item.ref) continue;
|
|
108
|
+
if (item.status === 'queued' && current.queuedAt !== item.queuedAt) continue;
|
|
109
|
+
}
|
|
110
|
+
entries.set(item.key, item);
|
|
111
|
+
}
|
|
112
|
+
return entries;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function outcomeFile(path: string, key: string): string {
|
|
116
|
+
return join(dirname(path), 'outcomes', `${key}.json`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function persistOutcome(path: string, entry: RecordEntry, write: typeof append): void {
|
|
120
|
+
try {
|
|
121
|
+
write(path, entry);
|
|
122
|
+
const sidecar = outcomeFile(path, entry.key);
|
|
123
|
+
if (existsSync(sidecar)) unlinkSync(sidecar);
|
|
124
|
+
} catch {
|
|
125
|
+
// The primary journal may be unavailable while the outcome is already committed
|
|
126
|
+
// to the intake queue. Keep a durable sidecar rather than a process-local receipt.
|
|
127
|
+
const target = outcomeFile(path, entry.key);
|
|
128
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
129
|
+
const temp = `${target}.${randomUUID()}.tmp`;
|
|
130
|
+
try {
|
|
131
|
+
writeFileSync(temp, JSON.stringify(entry));
|
|
132
|
+
const fd = openSync(temp, 'r');
|
|
133
|
+
try { fsyncSync(fd); } finally { closeSync(fd); }
|
|
134
|
+
renameSync(temp, target);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
try { unlinkSync(temp); } catch { /* no temp file */ }
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function receipt(item: SeatRequestItem): Response {
|
|
143
|
+
return jsonResponse({ receiptId: item.receiptId, seat: item.seat, queuedAt: item.queuedAt }, 202);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function unavailable(): Response {
|
|
147
|
+
debug.log('seat-address.pwa', 'rejected', { reason: 'journal-unavailable' });
|
|
148
|
+
return jsonResponse({ error: 'enqueue-failed' }, 503);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function handleSeatRequests(req: Request, deps: SeatRequestsDeps = {}): Promise<Response> {
|
|
152
|
+
const path = file((deps.root ?? effectiveInstanceRoot)());
|
|
153
|
+
if (req.method === 'GET') {
|
|
154
|
+
const params = new URL(req.url).searchParams;
|
|
155
|
+
const requestedSeat = params.get('seat');
|
|
156
|
+
const seat = requestedSeat ? resolveSeat(requestedSeat) : undefined;
|
|
157
|
+
if (requestedSeat && !seat) return unknownSeat();
|
|
158
|
+
const rawLimit = params.get('limit');
|
|
159
|
+
const limit = rawLimit === null ? 20 : Number(rawLimit);
|
|
160
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100) return jsonResponse({ error: 'bad_request' }, 400);
|
|
161
|
+
try {
|
|
162
|
+
const items = [...records(path).values()].filter((entry): entry is RecordEntry & SeatRequestItem => entry.status === 'queued')
|
|
163
|
+
.reverse().filter((item) => !seat || item.seat === seat.id).slice(0, limit)
|
|
164
|
+
.map(({ receiptId, seat: id, text, queuedAt, status }) => ({ receiptId, seat: id, text, queuedAt, status }));
|
|
165
|
+
return jsonResponse({ items, seats });
|
|
166
|
+
} catch { return unavailable(); }
|
|
167
|
+
}
|
|
168
|
+
if (req.method !== 'POST') return jsonResponse({ error: 'method-not-allowed' }, 405);
|
|
169
|
+
|
|
170
|
+
let body: unknown;
|
|
171
|
+
try { body = await req.json(); }
|
|
172
|
+
catch { return jsonResponse({ error: 'bad_request' }, 400); }
|
|
173
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) return jsonResponse({ error: 'bad_request' }, 400);
|
|
174
|
+
const input = body as Record<string, unknown>;
|
|
175
|
+
if (typeof input.text !== 'string' || !input.text.trim()
|
|
176
|
+
|| (input.seat !== undefined && typeof input.seat !== 'string')) {
|
|
177
|
+
debug.log('seat-address.pwa', 'rejected', { reason: 'invalid-body' });
|
|
178
|
+
return jsonResponse({ error: 'bad_request' }, 400);
|
|
179
|
+
}
|
|
180
|
+
// An implicit address must start the text; parseSeatAddress also accepts later lines.
|
|
181
|
+
const parsed = input.text.startsWith('@') ? parseSeatAddress(input.text) : null;
|
|
182
|
+
const prefix = parsed ? `@${parsed.seats.join(',')}` : '';
|
|
183
|
+
const firstAddress = parsed && input.text.startsWith(prefix)
|
|
184
|
+
&& (input.text.length === prefix.length || /[ \t\r\n]/.test(input.text[prefix.length]!)) ? parsed : null;
|
|
185
|
+
const address = input.seat ?? (firstAddress?.seats.length === 1 ? firstAddress.seats[0] : undefined);
|
|
186
|
+
const seat = address ? resolveSeat(address) : undefined;
|
|
187
|
+
if (!seat) return unknownSeat();
|
|
188
|
+
const text = input.seat === undefined ? firstAddress!.body.trim() : input.text.trim();
|
|
189
|
+
if (!text) {
|
|
190
|
+
debug.log('seat-address.pwa', 'rejected', { reason: 'empty-text', seat: seat.id });
|
|
191
|
+
return jsonResponse({ error: 'bad_request' }, 400);
|
|
192
|
+
}
|
|
193
|
+
debug.log('seat-address.pwa', 'parsed', { seat: seat.id, textLength: text.length });
|
|
194
|
+
const now = (deps.now ?? (() => new Date().toISOString()))();
|
|
195
|
+
const header = req.headers.get('idempotency-key');
|
|
196
|
+
if (header !== null && (!header.trim() || header.length > 200)) return jsonResponse({ error: 'bad_request' }, 400);
|
|
197
|
+
const payload = `${seat.id}\0${text}`;
|
|
198
|
+
const key = header === null ? createHash('sha256').update(randomUUID()).digest('hex')
|
|
199
|
+
: createHash('sha256').update(`client\0${header}`).digest('hex');
|
|
200
|
+
const lock = `${path}\0${key}`;
|
|
201
|
+
const pending = inFlight.get(lock);
|
|
202
|
+
if (pending) {
|
|
203
|
+
if (pending.payload !== payload) return jsonResponse({ error: 'idempotency-conflict' }, 409);
|
|
204
|
+
return pending.task.then((response) => response.clone());
|
|
205
|
+
}
|
|
206
|
+
// Install the lock before starting disk or queue work; a second caller must
|
|
207
|
+
// wait for the first instead of independently submitting the same key.
|
|
208
|
+
let start!: () => void;
|
|
209
|
+
const gate = new Promise<void>((resolve) => { start = resolve; });
|
|
210
|
+
const task = (async (): Promise<Response> => {
|
|
211
|
+
await gate;
|
|
212
|
+
let existing: RecordEntry | undefined;
|
|
213
|
+
try { existing = records(path).get(key); }
|
|
214
|
+
catch { return unavailable(); }
|
|
215
|
+
if (existing) {
|
|
216
|
+
if (existing.seat !== seat.id || existing.text !== text) return jsonResponse({ error: 'idempotency-conflict' }, 409);
|
|
217
|
+
if (existing.status === 'queued') return receipt(existing);
|
|
218
|
+
}
|
|
219
|
+
const intent: PendingRequest = existing?.status === 'pending'
|
|
220
|
+
? existing : { key, seat: seat.id, text, queuedAt: now, status: 'pending', ref: `pwa:${randomUUID()}` };
|
|
221
|
+
if (existing?.status !== 'pending') {
|
|
222
|
+
try {
|
|
223
|
+
(deps.append ?? append)(path, intent);
|
|
224
|
+
if (existing?.status === 'rejected') {
|
|
225
|
+
const sidecar = outcomeFile(path, key);
|
|
226
|
+
if (existsSync(sidecar)) unlinkSync(sidecar);
|
|
227
|
+
}
|
|
228
|
+
} catch { return unavailable(); }
|
|
229
|
+
}
|
|
230
|
+
let result: Awaited<ReturnType<typeof submitIntakeWork>>;
|
|
231
|
+
try {
|
|
232
|
+
result = await (deps.submit ?? submitIntakeWork)({
|
|
233
|
+
text: `@${seat.title ?? seat.id} ${text}`,
|
|
234
|
+
track: 'graph',
|
|
235
|
+
origin: { kind: 'external', ledgerSource: 'pwa', provider: 'other', ref: intent.ref, reportTo: { channel: 'pwa' } },
|
|
236
|
+
});
|
|
237
|
+
} catch { return unavailable(); }
|
|
238
|
+
if (!result.ok || result.track !== 'graph') {
|
|
239
|
+
debug.log('seat-address.pwa', 'rejected', { reason: result.ok ? 'unexpected-track' : result.reason, seat: seat.id });
|
|
240
|
+
try { persistOutcome(path, { ...intent, status: 'rejected' }, deps.append ?? append); }
|
|
241
|
+
catch { return unavailable(); }
|
|
242
|
+
return unavailable();
|
|
243
|
+
}
|
|
244
|
+
const item: RecordEntry & SeatRequestItem = { key, receiptId: result.acceptanceId, seat: seat.id, text, queuedAt: intent.queuedAt, status: 'queued' };
|
|
245
|
+
try { persistOutcome(path, item, deps.append ?? append); }
|
|
246
|
+
catch { return unavailable(); }
|
|
247
|
+
debug.log('seat-address.pwa', 'enqueued', { seat: seat.id, receiptId: item.receiptId });
|
|
248
|
+
return receipt(item);
|
|
249
|
+
})();
|
|
250
|
+
inFlight.set(lock, { payload, task });
|
|
251
|
+
start();
|
|
252
|
+
try { return await task; }
|
|
253
|
+
finally { inFlight.delete(lock); }
|
|
254
|
+
}
|