elanous 0.2.4 → 0.2.6
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/README.md +7 -6
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/1169-696263ac7af8c4d4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1883.c91a2648af4842fd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1947-9d64872eeb9cfdec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2808-dc06482994b40f70.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{5080-1613752891fcadb4.js → 2984-977cfa339fcd65a5.js} +3 -3
- package/apps/pwa/out/_next/static/chunks/3165-95515ca5f299a120.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8142-011e1f474efb8b33.js → 3190-e9282753e7ebde85.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/3995-bee512a7063036ce.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.js → 4240-7153881b0b377306.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/4259-99614149577e77cb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4283-329eabeaf233e6cd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4328-cb5058b1acc2de5e.js +25 -0
- package/apps/pwa/out/_next/static/chunks/4801-ecee6ef62b349b18.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5110-8058791dd25a2e14.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{3403.4bc97b4066945a94.js → 5329.8b7e4bca684b6eec.js} +2 -2
- package/apps/pwa/out/_next/static/chunks/5330-c41e9670f594c38f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5423-e95fd125d1794349.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5478-b5eae07dc79ea619.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5787-96ee6642b9a661e5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6448-79d72368589f98bc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6522-9866c002b5531836.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6729-b4a558b7aa891e0c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6969-cb03f9410b7d2e03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7007-3b9861091bb8dd21.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{5791-bb82743fa6299562.js → 763-7d0e1913b5074e98.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{4323.c5dfb6f503304775.js → 7633.0654fe6ea259c1ee.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/7930-8dc7e20fc6fc19ca.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8071-187edbc22a86741f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8198-d4101ad70cb9a579.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8250.4a2c5553a07cdf47.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8293-123b91e7b0330dc0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8296-d6d5d87dfa911d41.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8305-603963c33ee8eec3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8612-4a0066f20951b8fd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{2ea96958-e8c7e112de50b067.js → 8d0d9faf-97887ff99713b84e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/9181-cf4e1b61c4783f92.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8046cadf-211ca3c0e00f2b72.js → 91ff6dda-3b33d42d20657429.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/9262-26c2ecbd5a67e4a2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9328-45abdebce4fbecb7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9355-dbc67205adf8745f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9763.255b90dd69875689.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9933-66d31ec25c0231af.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-bd06fb55f4da0381.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-4e586142da455a25.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-889ad5cddcf45b0d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-47ba401d56572c27.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-0b54f9c83458e038.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-4fcc296ef6c8ed90.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-fe9fda494a87f4e5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-f9642e25ce129590.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-e1063e8ba5f3e2e7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-9d0df30027d8aa9d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-bd06fb55f4da0381.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-d97adf2c9455b8d0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-236b80d202110362.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-21a30f35f1a276ad.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-b1a5be357c4fa25d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-1705a60303f7f573.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/page-76b337c11ad0e237.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/not-found-7c0f85fbb56f64c6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-65ca16b20617b44d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-e7f9b8fc29411c1d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-92ea6213d1aeec04.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a913a005aacb5476.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-2d78804a40dfdef6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-08a99148e780ac00.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-deb674e7ed36f815.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-49f8de0534d192d7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-bd06fb55f4da0381.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-ba162a28d2176e18.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-1ff1a37b550ab41a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-ccaa698f56c0fbdb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-53b944082de1d7db.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-9b70adf1552789bf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-30d482aa63241fe8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-aecf786f54651d8a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-6ab9b62c7b6e08f2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-08949735455e0c9a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-a1479005234a46c2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3890e90bd232732.js +1 -0
- package/apps/pwa/out/_next/static/chunks/beb5cf10-9adcc10b83930090.js +1 -0
- package/apps/pwa/out/_next/static/chunks/framework-d68d41c88e05c801.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-11c63971b7a9593b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-d09452353bd0d9cc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_app-184d727cebeca767.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_error-21bbbbac99b720b1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{webpack-e5bea36f183b25e6.js → webpack-37b8bf78a3c6eec5.js} +1 -1
- package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +1 -0
- package/apps/pwa/out/_next/static/{zoQX5iTvXOsIvCjxCe1Ji → qMI0jnlZaBjjm4aIDYNGM}/_buildManifest.js +1 -1
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +29 -27
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +29 -27
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +29 -27
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +26 -24
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +26 -24
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +26 -24
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +29 -27
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +29 -27
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +29 -27
- 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 +26 -24
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +29 -27
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +29 -27
- package/apps/pwa/out/manifest.webmanifest +5 -2
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +26 -24
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +26 -24
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +29 -27
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +29 -27
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +27 -25
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +29 -27
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +26 -24
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +29 -27
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +29 -27
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +29 -27
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +29 -27
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +29 -27
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +26 -24
- package/apps/pwa/out/sw-strategies.js +44 -0
- package/apps/pwa/out/sw.js +6 -44
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +29 -27
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +29 -27
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +29 -27
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +29 -27
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +29 -27
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +29 -27
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +29 -27
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +29 -27
- package/catalog/resources.yaml +6 -6
- package/docs/goal-context/steward-directives-2026-09-28.md +35 -37
- package/graphs/landing/landing-heal.yaml +1 -1
- package/graphs/release/recipes.yaml +11 -1
- package/graphs/release/release-loop.yaml +9 -2
- package/graphs/video/README.md +1 -0
- package/graphs/video/explainer-line.yaml +58 -0
- package/package.json +3 -1
- package/plugins/card-followup/README.md +33 -0
- package/plugins/card-followup/examples/card-sample.json +7 -0
- package/plugins/card-followup/graphs/card-followup.yaml +16 -0
- package/plugins/card-followup/graphs/recipes.yaml +12 -0
- package/plugins/card-followup/graphs/run-step.ts +172 -0
- package/plugins/card-followup/plugin.json +25 -0
- package/plugins/geo-check/README.md +13 -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 +177 -0
- package/plugins/geo-check/plugin.json +24 -0
- package/plugins/job-coach/.codex-plugin/plugin.json +18 -1
- package/plugins/job-coach/README.md +42 -0
- package/plugins/job-coach/connectors/ncs/server.ts +4 -3
- package/plugins/job-coach/graphs/gap-judge.ts +44 -0
- package/plugins/job-coach/graphs/recipes.yaml +27 -0
- package/plugins/job-coach/graphs/report-enterprise.yaml +28 -0
- package/plugins/job-coach/graphs/run-step.ts +211 -17
- package/plugins/job-coach/plugin.json +16 -3
- package/plugins/job-coach/skills/enterprise-ai-fit/SKILL.md +8 -0
- package/plugins/job-coach/skills/enterprise-competency/SKILL.md +8 -0
- package/plugins/job-coach/skills/enterprise-needs/SKILL.md +8 -0
- package/plugins/job-coach/skills/enterprise-roadmap/SKILL.md +8 -0
- package/plugins/job-coach/skills/enterprise-task-analysis/SKILL.md +8 -0
- package/plugins/job-coach/skills/interview-to-profile/SKILL.md +18 -1
- package/scripts/backup/elanous-backup.sh +4 -1
- package/scripts/build-docs-site.ts +4 -3
- package/scripts/coord-channel-watch.sh +23 -4
- package/scripts/coord-post.sh +49 -12
- package/scripts/coord-tracks.json +5 -5
- package/scripts/daemon-port-baseline.txt +1 -2
- package/scripts/eval-bot-vs-grok.ts +79 -0
- package/scripts/eval-grok-live-baseline.ts +36 -0
- package/scripts/fixtures/front-route-corpus.sample.json +17 -0
- package/scripts/fleet/node-setup.sh +46 -7
- package/scripts/landing-heal/must-fix-match.ts +26 -0
- package/scripts/landing-heal/run-mission.ts +114 -20
- package/scripts/lib/android-test-device.ts +32 -0
- package/scripts/lib/front-route-measurement.ts +131 -0
- package/scripts/lib/tui-sim-keys.ts +42 -0
- package/scripts/measure-front-route-classifier.ts +56 -0
- package/scripts/media/ocr-leak-scan.sh +21 -0
- package/scripts/public-export.ts +2 -3
- package/scripts/public-import-pr.ts +80 -6
- package/scripts/publish-internal-dist.ts +2 -3
- package/scripts/red-sweep-diff.ts +1 -1
- package/scripts/release-loop/auto-approve-node.ts +72 -0
- package/scripts/release-loop/checklist-gate-node.ts +35 -0
- package/scripts/release-loop/gate-diff.ts +29 -0
- package/scripts/release-loop/gate-node.ts +366 -45
- package/scripts/release-loop/publish-node.ts +7 -3
- package/scripts/release-loop/release-docs.ts +20 -13
- package/scripts/release-loop/shard-plan.ts +47 -0
- package/scripts/release-loop/test-durations.ts +63 -0
- package/scripts/release-loop/upgrade-node.ts +3 -2
- package/scripts/test-audit-coverage-per-file.sh +16 -0
- package/scripts/test-audit-meaningless.ts +88 -0
- package/scripts/test-audit-overlap-and-weak.ts +73 -0
- package/scripts/test-deterministic.ts +27 -5
- package/scripts/tui-sim.ts +59 -14
- package/scripts/ux/bump-installed.sh +20 -0
- package/scripts/ux/lib/cdp.ts +72 -0
- package/scripts/ux/record-page.ts +53 -0
- package/skills/explainer-video/SKILL.md +117 -0
- package/skills/explainer-video/engine/brand/elanous-mark-on-dark.svg +1 -0
- package/skills/explainer-video/engine/build.mjs +431 -0
- package/skills/explainer-video/engine/reel.mjs +128 -0
- package/skills/explainer-video/engine/reel.sh +8 -0
- package/skills/explainer-video/engine/vo.ts +79 -0
- package/skills/explainer-video/examples/elanous-0.2.5/script.json +155 -0
- package/src/agent/nested-agent-env.ts +1 -0
- package/src/agent-mission/driver.ts +95 -16
- package/src/agent-mission/mission-cli.ts +6 -0
- package/src/agent-mission/resource-ladder.ts +73 -25
- package/src/auth/acp-token.ts +16 -0
- package/src/auth/github-app-token.ts +112 -0
- package/src/autopilot/gh-rest-fallback.ts +177 -0
- package/src/autopilot/mission-origin.ts +20 -4
- package/src/autopilot/mission-phase-track.ts +11 -1
- package/src/autopilot/pr-manager.ts +51 -4
- package/src/autopilot/track-agent.ts +3 -3
- package/src/autopilot/track-courier.ts +2 -2
- package/src/boot/acp-server.ts +2 -16
- package/src/boot/daemon-tools/web-search.ts +2 -1
- package/src/boot/ws-bridge.ts +11 -0
- package/src/chat/index.ts +37 -8
- package/src/chat/log-scroll-key.ts +12 -0
- package/src/chat/submit-trace.ts +30 -0
- package/src/cli/agent-cli.ts +7 -2
- package/src/cli/decisions-cli.ts +94 -0
- package/src/cli/doctor-cli.ts +40 -54
- package/src/cli/doctor-readiness.ts +16 -24
- package/src/cli/intake-cli.ts +1 -1
- package/src/cli/mcp-call.ts +9 -0
- package/src/cli/mcp-gateway-cli.ts +7 -3
- package/src/cli/nexus-ios-bind.ts +25 -8
- package/src/cli/phone-link.ts +117 -0
- package/src/cli/plugin-cli.ts +136 -11
- package/src/cli/pr-lineage.ts +2 -1
- package/src/cli/private-ledger-commands.ts +31 -0
- package/src/cli/pwa-start.ts +3 -0
- package/src/cli/release-cli.ts +97 -7
- package/src/cli/self-update.ts +7 -4
- package/src/cli/start-cli.ts +33 -14
- package/src/dashboard/acp-boot.ts +3 -0
- package/src/dashboard/index.ts +62 -13
- package/src/dashboard/input/no-provider-reply.ts +13 -0
- package/src/dashboard/slash-runtime/unknown-slash-reply.ts +78 -0
- package/src/dashboard/stale-install-check.ts +77 -0
- package/src/decisions/decision-ledger.ts +404 -0
- package/src/domains/outbound-alert.ts +13 -7
- package/src/domains/telegram-kind-route.ts +21 -0
- package/src/execution-loop/dispatch-task.ts +139 -0
- package/src/execution-loop/gate-decision-summary.ts +29 -0
- package/src/execution-loop/gates.ts +197 -0
- package/src/field/field-media.ts +245 -0
- package/src/graph-kinds/installed-plugin-nodes.ts +95 -0
- package/src/graph-kinds/plugin-nodes.ts +10 -2
- package/src/graph-runner/runner.ts +35 -5
- package/src/grounding/token.ts +12 -6
- package/src/harness/harness-cli-command.ts +68 -9
- package/src/harness/harness-pod-dispatch.ts +8 -2
- package/src/harness/harness-stop.ts +147 -18
- package/src/harness/self-send-decide.ts +26 -17
- package/src/harness/self-send-target.ts +22 -1
- package/src/harness/worktree-branch-prefix.ts +4 -3
- package/src/index.ts +50 -13
- package/src/instance/resolve.ts +6 -0
- package/src/intake-plane/collect-telegram-saved.ts +5 -0
- package/src/intake-plane/digest.ts +11 -2
- package/src/live/detail-switch.ts +8 -3
- package/src/llm/usable-llm.ts +32 -0
- package/src/loops/registry.ts +27 -5
- package/src/market/publish.ts +59 -3
- package/src/native-tool-catalog.ts +2 -2
- package/src/nexus/api/field-uploads.ts +143 -0
- package/src/nexus/api/graph-approvals.ts +174 -0
- package/src/nexus/api/harness-api.ts +219 -5
- package/src/nexus/api/http-server.ts +69 -6
- package/src/nexus/api/log-fabric.ts +10 -1
- package/src/nexus/api/mcp-oauth-hint.ts +10 -9
- package/src/nexus/api/merge-approvals.ts +2 -1
- package/src/nexus/api/meta-api.ts +14 -0
- package/src/nexus/api/plugins-credentials-api.ts +47 -0
- package/src/nexus/api/plugins-market.ts +87 -3
- package/src/nexus/api/pod-credential-api.ts +68 -14
- package/src/nexus/api/public-routes.ts +7 -0
- package/src/nexus/api/trace.ts +57 -6
- package/src/nexus/boot/register-mcp-clients.ts +99 -14
- package/src/nexus/index.ts +33 -32
- package/src/nexus/outbound/router.ts +1 -1
- package/src/notify/llm-budget-alert.ts +119 -0
- package/src/onboarding.ts +25 -2
- package/src/plugins/core/host.ts +14 -0
- package/src/plugins/install/market-fetch.ts +4 -3
- package/src/plugins/install/plugin-credentials.ts +106 -0
- package/src/plugins/install/plugin-install.ts +163 -32
- package/src/plugins/maker/plugin-maker.ts +208 -0
- package/src/prompt-library/universal-preamble.ts +8 -2
- package/src/relay/channel.ts +184 -0
- package/src/relay/mux.ts +182 -0
- package/src/relay/test-vectors.json +29 -0
- package/src/release-loop/checklist.ts +217 -0
- package/src/release-loop/release-note.ts +6 -3
- package/src/seat-address/seat-address.ts +34 -0
- package/src/self-dev/dev-cli.ts +40 -20
- package/src/self-dev/dev-pipeline.ts +77 -1
- package/src/self-implement/gate-baseline.ts +3 -1
- package/src/self-implement/goal-author.ts +18 -1
- package/src/self-implement/host-regate.ts +6 -2
- package/src/self-implement/orchestrator.ts +7 -2
- package/src/self-implement/run-ledger-compact.ts +1 -1
- package/src/self-implement/seams.ts +4 -2
- package/src/session/index.ts +23 -10
- package/src/skills/tools/dev-harness.ts +42 -0
- package/src/steward/steward-cards.ts +45 -0
- package/src/steward/triage.ts +50 -14
- package/src/task-orchestrator/surfaces/pod-artifact-return.ts +61 -3
- package/src/task-orchestrator/surfaces/pod-bun-cache.ts +18 -0
- package/src/task-orchestrator/surfaces/pod-command-job.ts +36 -6
- package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +39 -9
- package/src/task-orchestrator/surfaces/pod-source-bundle.ts +16 -1
- package/src/task-orchestrator/surfaces/pod-source-receive.ts +51 -13
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +172 -28
- package/src/telegram-report.ts +12 -6
- package/src/telegram.ts +108 -1
- package/src/tui-client/acp-transport-ws-client.ts +72 -5
- package/src/tui.ts +3 -2
- package/src/user-config.ts +32 -2
- package/src/util/spawn-sync-output.ts +15 -0
- package/src/version/code-revision.ts +1 -1
- package/src/version/packed-revision.json +1 -1
- package/src/video-pipeline/recipes/explainer.ts +213 -0
- package/src/video-pipeline/walk-line.ts +3 -2
- package/src/web-search/free.ts +77 -0
- package/src/web-search/index.ts +2 -0
- package/src/workflow-runtime/deps-bridge.ts +4 -3
- package/src/workflow-runtime/plugin-kind-node.ts +32 -4
- package/src/workflow-runtime/types.ts +2 -1
- package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1191-7b13a24117a689fa.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1377-1efeffbbac0963ff.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1470.98a6ab61ccb4131c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2375-3fcae5186c64fe6b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2977-e38442e86289f1d3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2982-2a2df543be3d2e5a.js +0 -25
- package/apps/pwa/out/_next/static/chunks/319-d37df1b6e0cbeff4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +0 -1
- package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4836-3c93795ab834381e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6090-1bd6077591603947.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7457-f5cb1cc8db9f7453.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8067-f74bf14d67ad98f1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8519-0059d82bdb4812bf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9740-52467f244755917f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.js +0 -1
- package/apps/pwa/out/_next/static/chunks/ae787e4c-f2998de7f1ba72a4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-21c3261af6ab377c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-1991445b8ea86274.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-4ebfaee2369fe248.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-9fa879ebf5959096.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-ca7c3cc305c3285a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-9951930add3d2051.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-4472ddf1e2458e69.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-4de08d4b766f134b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-7a975e2b9db1dfb7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-49270714ef35bb96.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-33fbdf353e2a5982.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-259f122e2dd64317.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-65b82726753658e1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-4b8df3748dc06285.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-112f7e477bda77fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-542a5c0006da637c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-63ffb347831262ea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-98e4e359d97263b4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-97e24ac675ce576e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-ac6391f26391d89d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-9f45c9d45ffc43bd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-36995b9cb5d98218.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-0c795b577988d63f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b4311e2f71c6735a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-eb2381074df20dba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-b01336e64eba01ae.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-7af7f21c1f2858ef.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-a98ecd364586c89b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-57f0502dfbfa089b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-3467d2e1abd8a14a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-7842ca23d53ba682.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-ae9ac882912b0198.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-3a0d72e3f783bcef.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-f3b39fe8358c2a97.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-5a22af3181ac2afb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-1457627bb302a22c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +0 -1
- package/apps/pwa/out/_next/static/css/e66b7f5efc8df3f3.css +0 -1
- /package/apps/pwa/out/_next/static/{zoQX5iTvXOsIvCjxCe1Ji → qMI0jnlZaBjjm4aIDYNGM}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { redactSecrets } from '../task-cards/card-store.js';
|
|
2
|
+
import type { BudgetDecision } from '../self-implement/budget-gate.js';
|
|
3
|
+
import type { GateResult, MemoryDecision, PlacementDecision, RelationDecision } from './gates.js';
|
|
4
|
+
|
|
5
|
+
export type GateName = 'budget' | 'placement' | 'relation' | 'memory';
|
|
6
|
+
|
|
7
|
+
/** Only decision metadata belongs in the live log; full results remain in the card. */
|
|
8
|
+
export function summarizeGateDecision(gate: GateName, result: GateResult<unknown>): string {
|
|
9
|
+
const { decision } = result;
|
|
10
|
+
let summary: string;
|
|
11
|
+
if (typeof decision === 'string') {
|
|
12
|
+
summary = gate === 'memory' ? 'unavailable' : `unavailable · explanation=${redactSecrets(result.explanation).slice(0, 80)}`;
|
|
13
|
+
} else if (gate === 'budget') {
|
|
14
|
+
const budget = decision as BudgetDecision;
|
|
15
|
+
summary = `action=${budget.action}${budget.provider ? ` · provider=${budget.provider}` : ''} · reason=${redactSecrets(budget.reasons[0] ?? '').slice(0, 80)}`;
|
|
16
|
+
} else if (gate === 'placement') {
|
|
17
|
+
const placement = decision as PlacementDecision;
|
|
18
|
+
summary = `substrate=${placement.substrate} · pool=${placement.pool ?? 'none'}${placement.memory ? ` · memory=${placement.memory.limit}` : ''} · unknown=${placement.unknownInputs.join(',')}`;
|
|
19
|
+
} else if (gate === 'relation') {
|
|
20
|
+
const relation = decision as RelationDecision;
|
|
21
|
+
const unknown = (['preflightOverlaps', 'dependsOn', 'similarCards', 'sameGoalActiveRuns'] as const)
|
|
22
|
+
.filter((key) => relation[key] === 'unknown');
|
|
23
|
+
summary = `overlap=${relation.overlappingCards.length} · unknown=${unknown.join(',')}`;
|
|
24
|
+
} else {
|
|
25
|
+
const memory = decision as MemoryDecision;
|
|
26
|
+
summary = `context=${memory.context ? 'recalled' : 'none'} · fragments=${memory.fragmentIds.length}`;
|
|
27
|
+
}
|
|
28
|
+
return redactSecrets(summary).slice(0, 250);
|
|
29
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import type { TaskCard } from '../task-cards/card-store.js';
|
|
2
|
+
import { decideBudget, readBudgetInputsLive, type BudgetDecision, type BudgetInputs } from '../self-implement/budget-gate.js';
|
|
3
|
+
import { resolveHarnessSubstrate, type ResolvedHarnessSubstrate } from '../harness/harness-substrate-default.js';
|
|
4
|
+
import { podMemoryLimitFor } from '../task-orchestrator/surfaces/self-implement-pod.js';
|
|
5
|
+
import { recallMemoryContext } from '../agent-substrate/execution/memory-context.js';
|
|
6
|
+
|
|
7
|
+
export interface GateResult<T> {
|
|
8
|
+
decision: T;
|
|
9
|
+
explanation: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface BudgetGateDeps {
|
|
13
|
+
readInputs: () => Promise<BudgetInputs>;
|
|
14
|
+
decide: (inputs: BudgetInputs) => BudgetDecision;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Observe the existing quota decision; callers record it, not apply it to the launch. */
|
|
18
|
+
export async function budgetGate(
|
|
19
|
+
deps: BudgetGateDeps = { readInputs: readBudgetInputsLive, decide: decideBudget },
|
|
20
|
+
): Promise<GateResult<BudgetDecision>> {
|
|
21
|
+
const decision = deps.decide(await deps.readInputs());
|
|
22
|
+
return { decision, explanation: decision.reasons.join(' · ') || `budget: ${decision.action}` };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface PlacementGateInput {
|
|
26
|
+
goal: string;
|
|
27
|
+
/** A local-only capability or file is an observation, not a reason to probe a remote pool. */
|
|
28
|
+
needsBrowser?: boolean;
|
|
29
|
+
needsVideo?: boolean;
|
|
30
|
+
needsAppSigning?: boolean;
|
|
31
|
+
localFiles?: readonly string[];
|
|
32
|
+
flag?: { substrate?: 'pod' | 'local'; podPool?: string };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface PlacementGateDeps {
|
|
36
|
+
resolveSubstrate: (input: { flag?: { substrate?: 'pod' | 'local'; podPool?: string } }) => ResolvedHarnessSubstrate;
|
|
37
|
+
memoryLimit: (goal: string) => { limit: string; tier: string; source: string };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type PlacementDecision = (ResolvedHarnessSubstrate | {
|
|
41
|
+
substrate: 'unknown';
|
|
42
|
+
pool: null;
|
|
43
|
+
source: 'unknown';
|
|
44
|
+
}) & {
|
|
45
|
+
/** Reachability is deliberately not probed by this observational gate. */
|
|
46
|
+
poolReachability: 'unknown' | 'not-applicable';
|
|
47
|
+
memory?: { limit: string; tier: string; source: string };
|
|
48
|
+
localReasons: string[];
|
|
49
|
+
/** Omitted observations must not be silently treated as negative evidence. */
|
|
50
|
+
unknownInputs: string[];
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const defaultPlacementDeps: PlacementGateDeps = {
|
|
54
|
+
resolveSubstrate: ({ flag }) => resolveHarnessSubstrate({ flag, env: process.env, currentContext: () => undefined }),
|
|
55
|
+
memoryLimit: (goal) => podMemoryLimitFor(goal, process.env),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export function placementGate(
|
|
59
|
+
input: PlacementGateInput,
|
|
60
|
+
deps: PlacementGateDeps = defaultPlacementDeps,
|
|
61
|
+
): GateResult<PlacementDecision> {
|
|
62
|
+
const unknownInputs = [
|
|
63
|
+
...(input.needsBrowser === undefined ? ['needsBrowser'] : []),
|
|
64
|
+
...(input.needsVideo === undefined ? ['needsVideo'] : []),
|
|
65
|
+
...(input.needsAppSigning === undefined ? ['needsAppSigning'] : []),
|
|
66
|
+
...(input.localFiles === undefined ? ['localFiles'] : []),
|
|
67
|
+
...(input.flag?.substrate === undefined ? ['substrate'] : []),
|
|
68
|
+
];
|
|
69
|
+
const localReasons = [
|
|
70
|
+
...(input.needsBrowser ? ['browser'] : []),
|
|
71
|
+
...(input.needsVideo ? ['video'] : []),
|
|
72
|
+
...(input.needsAppSigning ? ['app signing'] : []),
|
|
73
|
+
...(input.localFiles?.length ? [`local files: ${input.localFiles.join(', ')}`] : []),
|
|
74
|
+
];
|
|
75
|
+
// A measured local-only need wins; otherwise unknown needs cannot justify a remote assertion.
|
|
76
|
+
if (!localReasons.length && input.flag?.substrate !== 'local' && unknownInputs.length) {
|
|
77
|
+
return {
|
|
78
|
+
decision: { substrate: 'unknown', pool: null, source: 'unknown', poolReachability: 'unknown', localReasons, unknownInputs },
|
|
79
|
+
explanation: `placement=unknown; unmeasured=${unknownInputs.join(', ')}; pool reachability not checked`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const flag = localReasons.length ? { substrate: 'local' as const } : input.flag;
|
|
83
|
+
let resolved: ResolvedHarnessSubstrate;
|
|
84
|
+
try {
|
|
85
|
+
resolved = deps.resolveSubstrate({ flag });
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (flag?.substrate !== 'pod' || !(error instanceof Error) || !error.message.includes('Pod 풀 또는 현재 컨텍스트에 닿지 못했다')) throw error;
|
|
88
|
+
// No configured pool is not evidence that a remote pool is unreachable.
|
|
89
|
+
resolved = { substrate: 'pod', pool: null, source: 'flag' };
|
|
90
|
+
}
|
|
91
|
+
const poolReachability = resolved.substrate === 'pod' ? 'unknown' : 'not-applicable';
|
|
92
|
+
const memory = resolved.substrate === 'pod' ? deps.memoryLimit(input.goal) : undefined;
|
|
93
|
+
const decision: PlacementDecision = {
|
|
94
|
+
...resolved, poolReachability, localReasons, unknownInputs,
|
|
95
|
+
...(memory ? { memory } : {}),
|
|
96
|
+
};
|
|
97
|
+
return {
|
|
98
|
+
decision,
|
|
99
|
+
explanation: (resolved.substrate === 'local'
|
|
100
|
+
? `local: ${localReasons.join(' · ') || `substrate ${resolved.source}`}`
|
|
101
|
+
: `pod: pool=${resolved.pool ?? 'unknown'}; reachability=unknown (not checked); memory=${memory?.tier ?? 'unknown'} (${memory?.limit ?? 'unknown'})`)
|
|
102
|
+
+ (unknownInputs.length ? `; unmeasured=${unknownInputs.join(', ')}` : ''),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface RelationGateDeps {
|
|
107
|
+
openCards: () => Promise<readonly TaskCard[]> | readonly TaskCard[];
|
|
108
|
+
/** A previous completed run is reported as a fact, never inferred from a matching title. */
|
|
109
|
+
priorTermination: (goalId: string) => Promise<string | undefined> | string | undefined;
|
|
110
|
+
/** 'unknown' = the caller cannot observe running runs; recorded as unknown, never as none. */
|
|
111
|
+
activeRuns: (goalId: string) => Promise<readonly string[] | 'unknown'> | readonly string[] | 'unknown';
|
|
112
|
+
/** Optional preflight overlap observations are kept separate from open-card overlap. */
|
|
113
|
+
preflightOverlaps?: (paths: readonly string[]) => Promise<readonly string[]> | readonly string[];
|
|
114
|
+
dependsOn?: (goalId: string) => Promise<readonly string[]> | readonly string[];
|
|
115
|
+
similarCards?: (card: TaskCard, openCards: readonly TaskCard[]) => Promise<readonly string[]> | readonly string[];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface RelationDecision {
|
|
119
|
+
overlappingCards: string[];
|
|
120
|
+
preflightOverlaps: string[] | 'unknown';
|
|
121
|
+
dependsOn: string[] | 'unknown';
|
|
122
|
+
similarCards: string[] | 'unknown';
|
|
123
|
+
sameGoalActiveRuns: string[] | 'unknown';
|
|
124
|
+
priorTermination?: string;
|
|
125
|
+
/** Overlaps and similarity are observations, not dispatch locks. */
|
|
126
|
+
action: 'record';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function relationGate(
|
|
130
|
+
card: TaskCard,
|
|
131
|
+
targetPaths: readonly string[],
|
|
132
|
+
deps: RelationGateDeps,
|
|
133
|
+
): Promise<GateResult<RelationDecision>> {
|
|
134
|
+
const [cards, priorTermination, sameGoalActiveRuns] = await Promise.all([
|
|
135
|
+
deps.openCards(), deps.priorTermination(card.goalId), deps.activeRuns(card.goalId),
|
|
136
|
+
]);
|
|
137
|
+
const [preflightOverlaps, dependsOn, similarCards] = await Promise.all([
|
|
138
|
+
deps.preflightOverlaps?.(targetPaths) ?? 'unknown',
|
|
139
|
+
deps.dependsOn?.(card.goalId) ?? 'unknown',
|
|
140
|
+
deps.similarCards?.(card, cards) ?? 'unknown',
|
|
141
|
+
]);
|
|
142
|
+
const target = new Set(targetPaths);
|
|
143
|
+
const overlappingCards = cards
|
|
144
|
+
.filter((other) => other.id !== card.id && other.status === 'open' && other.sections.some((section) => {
|
|
145
|
+
if (!section.key.startsWith('workspace')) return false;
|
|
146
|
+
try {
|
|
147
|
+
const workspace = JSON.parse(section.content) as { targetPaths?: unknown; paths?: unknown };
|
|
148
|
+
const paths = workspace.targetPaths ?? workspace.paths;
|
|
149
|
+
return Array.isArray(paths) && paths.some((path) => typeof path === 'string' && target.has(path));
|
|
150
|
+
} catch { return false; }
|
|
151
|
+
}))
|
|
152
|
+
.map((other) => other.id);
|
|
153
|
+
const observed = (value: readonly string[] | string): string =>
|
|
154
|
+
typeof value === 'string' ? value : value.join(',') || 'none';
|
|
155
|
+
const decision: RelationDecision = {
|
|
156
|
+
action: 'record', overlappingCards,
|
|
157
|
+
preflightOverlaps: preflightOverlaps === 'unknown' ? 'unknown' : [...preflightOverlaps],
|
|
158
|
+
dependsOn: dependsOn === 'unknown' ? 'unknown' : [...dependsOn],
|
|
159
|
+
similarCards: similarCards === 'unknown' ? 'unknown' : [...similarCards],
|
|
160
|
+
sameGoalActiveRuns: sameGoalActiveRuns === 'unknown' ? 'unknown' : [...sameGoalActiveRuns],
|
|
161
|
+
...(priorTermination === undefined ? {} : { priorTermination }),
|
|
162
|
+
};
|
|
163
|
+
return {
|
|
164
|
+
decision,
|
|
165
|
+
explanation: `overlap=${observed(overlappingCards)}; preflight=${observed(preflightOverlaps)}; dependsOn=${observed(dependsOn)}; similar=${observed(similarCards)}; active=${observed(sameGoalActiveRuns)}; prior=${priorTermination ?? 'unknown'}; overlaps do not block launch`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface MemoryGateDeps {
|
|
170
|
+
recall: (query: string) => Promise<string>;
|
|
171
|
+
priorAbandonment: (goalId: string) => Promise<string | undefined> | string | undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface MemoryDecision {
|
|
175
|
+
context: string;
|
|
176
|
+
priorAbandonment?: string;
|
|
177
|
+
/** The existing recall API returns formatted context, not stable fragment IDs. */
|
|
178
|
+
fragmentIds: string[];
|
|
179
|
+
action: 'record';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function memoryGate(
|
|
183
|
+
card: Pick<TaskCard, 'goalId' | 'title'>,
|
|
184
|
+
deps: MemoryGateDeps = { recall: recallMemoryContext, priorAbandonment: () => undefined },
|
|
185
|
+
): Promise<GateResult<MemoryDecision>> {
|
|
186
|
+
const [context, priorAbandonment] = await Promise.all([
|
|
187
|
+
deps.recall(card.title), deps.priorAbandonment(card.goalId),
|
|
188
|
+
]);
|
|
189
|
+
const decision: MemoryDecision = {
|
|
190
|
+
action: 'record', context, fragmentIds: [],
|
|
191
|
+
...(priorAbandonment === undefined ? {} : { priorAbandonment }),
|
|
192
|
+
};
|
|
193
|
+
return {
|
|
194
|
+
decision,
|
|
195
|
+
explanation: `memory=${context ? 'recalled' : 'none or unavailable'}; prior abandonment=${priorAbandonment ?? 'unknown'}; fragment IDs unavailable from recall API`,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// 현장(field) 업로드 폴더 계약 — HTTP 입구(`/v1/field/uploads`)와 텔레그램 `#현장` 이 «같은» 저장 함수를 쓴다.
|
|
2
|
+
//
|
|
3
|
+
// 폴더 계약(2026-09-30 합의 · 소비자 = reel 파이프라인):
|
|
4
|
+
// <configDir>/field/<event>/<ISO 기본형 타임스탬프>-<device>-<원래 이름>
|
|
5
|
+
// 타임스탬프 = 클라이언트가 준 촬영 시각(있으면) · 없으면 업로드 시각 · UTC · `20261015T193012Z`(ISO 8601 기본형 —
|
|
6
|
+
// 파일 이름에 콜론을 넣지 않으려고 기본형을 쓴다. 사전순 = 시간순).
|
|
7
|
+
// 배치가 끝나면 `<folder>/.ready` 를 한 줄(= 폴더의 미디어 파일 수)로 덮어쓴다.
|
|
8
|
+
|
|
9
|
+
import { appendFileSync, mkdirSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { randomUUID } from 'node:crypto';
|
|
12
|
+
|
|
13
|
+
export const FIELD_SLUG_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
14
|
+
export const FIELD_MAX_FILE_BYTES = 100 * 1024 * 1024; // 폰 현장 사진·짧은 영상 — 데몬 전역 요청 한도를 과하게 올리지 않으려고 낮게(2026-09-30)
|
|
15
|
+
export const FIELD_MAX_REQUEST_BYTES = 250 * 1024 * 1024;
|
|
16
|
+
export const FIELD_READY_FILE = '.ready';
|
|
17
|
+
const MAX_ORIGINAL_NAME = 80;
|
|
18
|
+
|
|
19
|
+
/** 허용 MIME → 대표 확장자. heif 는 heic 의 별칭으로 받는다. */
|
|
20
|
+
const MIME_EXT: Record<string, string> = {
|
|
21
|
+
'image/jpeg': 'jpg',
|
|
22
|
+
'image/jpg': 'jpg',
|
|
23
|
+
'image/png': 'png',
|
|
24
|
+
'image/heic': 'heic',
|
|
25
|
+
'image/heif': 'heic',
|
|
26
|
+
'image/webp': 'webp',
|
|
27
|
+
'video/mp4': 'mp4',
|
|
28
|
+
'video/quicktime': 'mov',
|
|
29
|
+
};
|
|
30
|
+
const EXT_MIME: Record<string, string> = {
|
|
31
|
+
jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', heic: 'image/heic', heif: 'image/heif',
|
|
32
|
+
webp: 'image/webp', mp4: 'video/mp4', mov: 'video/quicktime',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type FieldUploadErrorCode = 'bad-slug' | 'unsupported-media-type' | 'too-large' | 'no-files' | 'bad-request';
|
|
36
|
+
|
|
37
|
+
export class FieldUploadError extends Error {
|
|
38
|
+
constructor(readonly code: FieldUploadErrorCode, message: string) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = 'FieldUploadError';
|
|
41
|
+
}
|
|
42
|
+
get status(): number {
|
|
43
|
+
if (this.code === 'unsupported-media-type') return 415;
|
|
44
|
+
if (this.code === 'too-large') return 413;
|
|
45
|
+
return 400;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface FieldMediaInput {
|
|
50
|
+
originalName: string;
|
|
51
|
+
mimeType?: string;
|
|
52
|
+
bytes: Uint8Array;
|
|
53
|
+
/** 촬영 시각(ISO 문자열·epoch ms). 못 읽으면 업로드 시각. */
|
|
54
|
+
capturedAt?: string | number | Date;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface FieldSaveOptions {
|
|
58
|
+
/** config 뿌리(`~/.elanous` 또는 격리 뿌리). 폴더 = `<rootDir>/field/<event>`. */
|
|
59
|
+
rootDir: string;
|
|
60
|
+
event: string;
|
|
61
|
+
device: string;
|
|
62
|
+
now?: () => Date;
|
|
63
|
+
/** 사람이 쓴 한 줄(텔레그램 캡션·앱 입력칸) — 있으면 `captions.txt` 에 «<첫 파일 이름> | <글>» 로 덧붙인다(reel 자막 · MK 2026-09-30). */
|
|
64
|
+
caption?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface FieldSaveResult {
|
|
68
|
+
event: string;
|
|
69
|
+
dir: string;
|
|
70
|
+
saved: Array<{ name: string; bytes: number }>;
|
|
71
|
+
/** 저장 «후» 폴더의 미디어 파일 수(= `.ready` 에 쓴 값). */
|
|
72
|
+
count: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function isFieldSlug(value: unknown): value is string {
|
|
76
|
+
return typeof value === 'string' && FIELD_SLUG_RE.test(value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function fieldEventDir(rootDir: string, event: string): string {
|
|
80
|
+
if (!isFieldSlug(event)) throw new FieldUploadError('bad-slug', 'event slug must match ^[a-z0-9][a-z0-9._-]{0,63}$');
|
|
81
|
+
return join(rootDir, 'field', event);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function extOf(name: string): string {
|
|
85
|
+
const dot = name.lastIndexOf('.');
|
|
86
|
+
return dot > 0 ? name.slice(dot + 1).toLowerCase() : '';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 허용 MIME 이면 정규화한 MIME, 아니면 null. MIME 이 비었거나 octet-stream 이면 확장자로 추정한다. */
|
|
90
|
+
export function resolveFieldMime(mimeType: string | undefined, originalName: string): string | null {
|
|
91
|
+
const mime = (mimeType ?? '').split(';')[0]!.trim().toLowerCase();
|
|
92
|
+
if (mime && mime !== 'application/octet-stream') return MIME_EXT[mime] ? mime : null;
|
|
93
|
+
return EXT_MIME[extOf(originalName)] ?? null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** basename · `[A-Za-z0-9._-]` 외는 `_` · 앞 점 제거 · 최대 80자(확장자 보존) · 확장자가 MIME 과 안 맞으면 붙인다. */
|
|
97
|
+
export function sanitizeFieldName(originalName: string, mime?: string): string {
|
|
98
|
+
const base = originalName.split(/[\\/]/).pop() ?? '';
|
|
99
|
+
let name = base.replace(/[^A-Za-z0-9._-]/g, '_').replace(/_+/g, '_').replace(/^\.+/, '');
|
|
100
|
+
const wantExt = mime ? MIME_EXT[mime] : undefined;
|
|
101
|
+
const ext = extOf(name);
|
|
102
|
+
if (wantExt && EXT_MIME[ext] === undefined) name = name ? `${name}.${wantExt}` : `media.${wantExt}`;
|
|
103
|
+
if (!name) name = 'media';
|
|
104
|
+
if (name.length > MAX_ORIGINAL_NAME) {
|
|
105
|
+
const e = extOf(name);
|
|
106
|
+
const suffix = e && e.length < 10 ? `.${e}` : '';
|
|
107
|
+
name = name.slice(0, MAX_ORIGINAL_NAME - suffix.length) + suffix;
|
|
108
|
+
}
|
|
109
|
+
return name;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** `20261015T193012Z` — ISO 8601 기본형 UTC(초 단위). */
|
|
113
|
+
export function fieldTimestamp(capturedAt: FieldMediaInput['capturedAt'], now: Date): string {
|
|
114
|
+
let at: Date | null = null;
|
|
115
|
+
if (capturedAt instanceof Date) at = capturedAt;
|
|
116
|
+
else if (typeof capturedAt === 'number') at = new Date(capturedAt);
|
|
117
|
+
else if (typeof capturedAt === 'string' && capturedAt.trim()) {
|
|
118
|
+
const trimmed = capturedAt.trim();
|
|
119
|
+
at = /^\d{10,13}$/.test(trimmed) ? new Date(Number(trimmed) * (trimmed.length === 10 ? 1000 : 1)) : new Date(trimmed);
|
|
120
|
+
}
|
|
121
|
+
if (!at || Number.isNaN(at.getTime())) at = now;
|
|
122
|
+
return at.toISOString().replace(/\.\d{3}Z$/, 'Z').replace(/[-:]/g, '');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isMediaFile(name: string): boolean {
|
|
126
|
+
return !name.startsWith('.') && EXT_MIME[extOf(name)] !== undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function listFieldMedia(dir: string): Array<{ name: string; bytes: number }> {
|
|
130
|
+
let names: string[];
|
|
131
|
+
try { names = readdirSync(dir); }
|
|
132
|
+
catch (error) {
|
|
133
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return [];
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
const out: Array<{ name: string; bytes: number }> = [];
|
|
137
|
+
for (const name of names.sort()) {
|
|
138
|
+
if (!isMediaFile(name)) continue;
|
|
139
|
+
try {
|
|
140
|
+
const st = statSync(join(dir, name));
|
|
141
|
+
if (st.isFile()) out.push({ name, bytes: st.size });
|
|
142
|
+
} catch { /* 사이에 지워졌다 */ }
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function atomicWrite(dir: string, name: string, bytes: Uint8Array | string): void {
|
|
148
|
+
const tmp = join(dir, `.tmp-${randomUUID()}`);
|
|
149
|
+
try {
|
|
150
|
+
writeFileSync(tmp, bytes);
|
|
151
|
+
renameSync(tmp, join(dir, name));
|
|
152
|
+
} catch (error) {
|
|
153
|
+
try { unlinkSync(tmp); } catch { /* 없음 */ }
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** `.ready` 를 «지금» 폴더의 미디어 파일 수로 덮어쓴다. */
|
|
159
|
+
export function refreshFieldReady(dir: string): number {
|
|
160
|
+
const count = listFieldMedia(dir).length;
|
|
161
|
+
atomicWrite(dir, FIELD_READY_FILE, `${count}\n`);
|
|
162
|
+
return count;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** 검증(전부) → 쓰기(전부, 원자적) → `.ready` 갱신. 하나라도 검증에 걸리면 아무것도 안 쓴다. */
|
|
166
|
+
export function saveFieldMedia(inputs: FieldMediaInput[], opts: FieldSaveOptions): FieldSaveResult {
|
|
167
|
+
const dir = fieldEventDir(opts.rootDir, opts.event);
|
|
168
|
+
if (!isFieldSlug(opts.device)) throw new FieldUploadError('bad-slug', 'device slug must match ^[a-z0-9][a-z0-9._-]{0,63}$');
|
|
169
|
+
if (inputs.length === 0) throw new FieldUploadError('no-files', 'at least one file is required');
|
|
170
|
+
const now = (opts.now ?? (() => new Date()))();
|
|
171
|
+
let total = 0;
|
|
172
|
+
const planned = inputs.map((input) => {
|
|
173
|
+
const mime = resolveFieldMime(input.mimeType, input.originalName);
|
|
174
|
+
if (!mime) throw new FieldUploadError('unsupported-media-type', 'only jpeg/png/heic/webp images and mp4/mov videos are accepted');
|
|
175
|
+
const size = input.bytes.byteLength;
|
|
176
|
+
if (size === 0) throw new FieldUploadError('bad-request', 'empty file');
|
|
177
|
+
if (size > FIELD_MAX_FILE_BYTES) throw new FieldUploadError('too-large', 'a file exceeds 100 MB');
|
|
178
|
+
total += size;
|
|
179
|
+
if (total > FIELD_MAX_REQUEST_BYTES) throw new FieldUploadError('too-large', 'the request exceeds 250 MB');
|
|
180
|
+
const name = `${fieldTimestamp(input.capturedAt, now)}-${opts.device}-${sanitizeFieldName(input.originalName, mime)}`;
|
|
181
|
+
return { name, bytes: input.bytes };
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
mkdirSync(dir, { recursive: true });
|
|
185
|
+
const taken = new Set<string>();
|
|
186
|
+
const saved: FieldSaveResult['saved'] = [];
|
|
187
|
+
for (const item of planned) {
|
|
188
|
+
const name = uniqueName(dir, item.name, item.bytes.byteLength, taken);
|
|
189
|
+
taken.add(name);
|
|
190
|
+
atomicWrite(dir, name, item.bytes);
|
|
191
|
+
saved.push({ name, bytes: item.bytes.byteLength });
|
|
192
|
+
}
|
|
193
|
+
const line = normalizeFieldCaption(opts.caption);
|
|
194
|
+
if (line && saved[0]) appendFileSync(join(dir, FIELD_CAPTIONS_FILE), `${saved[0].name} | ${line}\n`);
|
|
195
|
+
const count = refreshFieldReady(dir);
|
|
196
|
+
return { event: opts.event, dir, saved, count };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** 같은 이름·같은 크기면 재전송으로 보고 덮는다(중복 방지). 크기가 다르면 `-1`,`-2`… 를 붙인다. */
|
|
200
|
+
function uniqueName(dir: string, name: string, size: number, taken: Set<string>): string {
|
|
201
|
+
const dot = name.lastIndexOf('.');
|
|
202
|
+
const stem = dot > 0 ? name.slice(0, dot) : name;
|
|
203
|
+
const ext = dot > 0 ? name.slice(dot) : '';
|
|
204
|
+
for (let i = 0; ; i += 1) {
|
|
205
|
+
const candidate = i === 0 ? name : `${stem}-${i}${ext}`;
|
|
206
|
+
if (taken.has(candidate)) continue;
|
|
207
|
+
let existing: number | null = null;
|
|
208
|
+
try { existing = statSync(join(dir, candidate)).size; } catch { existing = null; }
|
|
209
|
+
if (existing === null || existing === size) return candidate;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ── 텔레그램 캡션 문법 ──────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
export const FIELD_CAPTION_TAG = '#현장';
|
|
216
|
+
|
|
217
|
+
/** 캡션에 독립 토큰 `#현장` 이 있으면 `{ event? }`, 없으면 null.
|
|
218
|
+
* `#현장 <slug>` — 바로 다음 토큰을 소문자로 바꿔 슬러그 규칙에 맞으면 행사 이름, 아니면 기본 행사. */
|
|
219
|
+
export function parseFieldCaption(caption: string | undefined | null): { event?: string; text?: string } | null {
|
|
220
|
+
if (!caption) return null;
|
|
221
|
+
const tokens = caption.trim().split(/\s+/);
|
|
222
|
+
const at = tokens.indexOf(FIELD_CAPTION_TAG);
|
|
223
|
+
if (at < 0) return null;
|
|
224
|
+
const next = tokens[at + 1]?.toLowerCase();
|
|
225
|
+
const hasEvent = !!next && isFieldSlug(next);
|
|
226
|
+
// 태그·행사 이름을 뺀 나머지가 «글»(자막) — 태그 앞에 쓴 글도 같이.
|
|
227
|
+
const rest = tokens.filter((_, i) => i !== at && !(hasEvent && i === at + 1)).join(' ');
|
|
228
|
+
const text = normalizeFieldCaption(rest);
|
|
229
|
+
return { ...(hasEvent ? { event: next } : {}), ...(text ? { text } : {}) };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export const FIELD_CAPTIONS_FILE = 'captions.txt';
|
|
233
|
+
|
|
234
|
+
/** 자막 한 줄 — 줄바꿈은 공백으로, `|` 는 구분자라 전각 `|` 로, 280자까지. 비면 빈 값. */
|
|
235
|
+
export function normalizeFieldCaption(value: string | undefined | null): string {
|
|
236
|
+
if (typeof value !== 'string') return '';
|
|
237
|
+
return value.replace(/[\r\n]+/g, ' ').replace(/\|/g, '|').replace(/\s+/g, ' ').trim().slice(0, 280);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** 기본 행사 = 유효한 config 값 · 없으면 `field-<로컬 YYYY-MM-DD>`. */
|
|
241
|
+
export function defaultFieldEvent(configured: string | undefined, now: Date = new Date()): string {
|
|
242
|
+
if (configured && isFieldSlug(configured)) return configured;
|
|
243
|
+
const pad = (n: number) => String(n).padStart(2, '0');
|
|
244
|
+
return `field-${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
|
245
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFileSync, realpathSync, statSync } from 'node:fs';
|
|
3
|
+
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
4
|
+
import { debug } from '../debug/log.js';
|
|
5
|
+
import { listInstalledPlugins, type InstalledPlugin } from '../plugins/install/plugin-install.js';
|
|
6
|
+
import { loadPluginManifestFromDir, type PluginManifest } from '../plugins/core/manifest.js';
|
|
7
|
+
import { loadPluginNodes } from './plugin-nodes.js';
|
|
8
|
+
import { getNodeKindRegistration, listNodeKinds, unregisterPluginNodeKind, type GraphKind, type NodeKindEntry } from './registry.js';
|
|
9
|
+
|
|
10
|
+
export interface InstalledPluginNodeSyncResult {
|
|
11
|
+
registered: number;
|
|
12
|
+
removed: number;
|
|
13
|
+
errors: Array<{ plugin: string; reason: string }>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type OwnedNode = { graph: GraphKind; kind: string; plugin: string; registered: NodeKindEntry };
|
|
17
|
+
type InstalledNodeState = { stamp: string; owned: Map<string, { fingerprint: string; nodes: OwnedNode[] }> };
|
|
18
|
+
const states = new Map<string, InstalledNodeState>();
|
|
19
|
+
|
|
20
|
+
function fingerprint(item: InstalledPlugin, manifest: PluginManifest): string {
|
|
21
|
+
const root = realpathSync(item.path);
|
|
22
|
+
const hash = createHash('sha256');
|
|
23
|
+
hash.update(JSON.stringify([item.name, item.version, item.market, item.sha256, item.installedAt, manifest]));
|
|
24
|
+
for (const path of manifest.contributes.nodes ?? []) {
|
|
25
|
+
const file = realpathSync(resolve(root, path));
|
|
26
|
+
const rel = relative(root, file);
|
|
27
|
+
if (!path.startsWith('./') || !rel || rel === '..' || rel.startsWith('../') || isAbsolute(rel)) {
|
|
28
|
+
throw new Error(`node path must remain inside the plugin dir: ${path}`);
|
|
29
|
+
}
|
|
30
|
+
hash.update(path);
|
|
31
|
+
hash.update(readFileSync(file));
|
|
32
|
+
}
|
|
33
|
+
return hash.digest('hex');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Reconcile only registrations made by this sync; the registry's identity check protects other owners. */
|
|
37
|
+
export function syncInstalledPluginNodes(root: string): InstalledPluginNodeSyncResult {
|
|
38
|
+
const result: InstalledPluginNodeSyncResult = { registered: 0, removed: 0, errors: [] };
|
|
39
|
+
const stateRoot = resolve(root);
|
|
40
|
+
const ledger = statSync(join(stateRoot, 'plugins', 'installed.json'), { throwIfNoEntry: false, bigint: true });
|
|
41
|
+
// The installer atomically replaces the ledger, so mtime and size alone can
|
|
42
|
+
// coincide across installs. File identity and ctime detect that replacement.
|
|
43
|
+
const stamp = ledger ? `${ledger.dev}:${ledger.ino}:${ledger.ctimeNs}:${ledger.mtimeNs}:${ledger.size}` : 'missing';
|
|
44
|
+
const previous = states.get(stateRoot);
|
|
45
|
+
if (previous?.stamp === stamp) return result;
|
|
46
|
+
|
|
47
|
+
// The installer commits a new ledger via rename after the package directory is in place.
|
|
48
|
+
// If discovery itself fails, the stamp is not advanced and the next request retries.
|
|
49
|
+
const installed = listInstalledPlugins(stateRoot);
|
|
50
|
+
const current = new Set(installed.map(item => item.path));
|
|
51
|
+
const owned = previous?.owned ?? new Map<string, { fingerprint: string; nodes: OwnedNode[] }>();
|
|
52
|
+
const removeOwned = (path: string): void => {
|
|
53
|
+
for (const node of owned.get(path)?.nodes ?? []) {
|
|
54
|
+
if (unregisterPluginNodeKind(node.graph, node.kind, node.plugin, node.registered)) result.removed++;
|
|
55
|
+
}
|
|
56
|
+
owned.delete(path);
|
|
57
|
+
};
|
|
58
|
+
for (const path of owned.keys()) {
|
|
59
|
+
if (!current.has(path)) removeOwned(path);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const item of installed) {
|
|
63
|
+
try {
|
|
64
|
+
const manifest = loadPluginManifestFromDir(item.path, { id: item.name }).manifest;
|
|
65
|
+
if (!manifest.contributes.nodes?.length) {
|
|
66
|
+
removeOwned(item.path);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const version = fingerprint(item, manifest);
|
|
70
|
+
if (owned.get(item.path)?.fingerprint === version) continue;
|
|
71
|
+
removeOwned(item.path);
|
|
72
|
+
const before = new Set(listNodeKinds().filter(entry => entry.plugin === manifest.id).map(entry => `${entry.graph}:${entry.kind}`));
|
|
73
|
+
const loaded = loadPluginNodes(item.path, manifest);
|
|
74
|
+
const nodes: OwnedNode[] = [];
|
|
75
|
+
for (const entry of listNodeKinds().filter(entry => entry.plugin === manifest.id && !before.has(`${entry.graph}:${entry.kind}`))) {
|
|
76
|
+
const registered = getNodeKindRegistration(entry.graph, entry.kind);
|
|
77
|
+
if (registered) nodes.push({ graph: entry.graph, kind: entry.kind, plugin: manifest.id, registered });
|
|
78
|
+
}
|
|
79
|
+
if (loaded.errors.length) {
|
|
80
|
+
for (const node of nodes) unregisterPluginNodeKind(node.graph, node.kind, node.plugin, node.registered);
|
|
81
|
+
result.errors.push({ plugin: item.name, reason: loaded.errors.join('; ') });
|
|
82
|
+
owned.set(item.path, { fingerprint: version, nodes: [] });
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
result.registered += nodes.length;
|
|
86
|
+
owned.set(item.path, { fingerprint: version, nodes });
|
|
87
|
+
} catch (error) {
|
|
88
|
+
removeOwned(item.path);
|
|
89
|
+
result.errors.push({ plugin: item.name, reason: error instanceof Error ? error.message : String(error) });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
states.set(stateRoot, { stamp, owned });
|
|
93
|
+
debug.log('graph.kinds', 'installed-sync', { root: stateRoot, ...result });
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
@@ -34,8 +34,16 @@ export function loadPluginNodes(pluginDir: string, manifest: PluginManifest): Pl
|
|
|
34
34
|
if (node.graph === 'workflow') {
|
|
35
35
|
if (!node.run || typeof node.run !== 'object' || Array.isArray(node.run)) throw new Error('run must be an object');
|
|
36
36
|
const spec = node.run as Record<string, unknown>;
|
|
37
|
-
if (Object.keys(spec).length !== 1 || !['bash', 'http', 'skill'].some((key) => key in spec)) {
|
|
38
|
-
throw new Error('run must contain exactly one bash, http or
|
|
37
|
+
if (Object.keys(spec).length !== 1 || !['bash', 'http', 'skill', 'mcp'].some((key) => key in spec)) {
|
|
38
|
+
throw new Error('run must contain exactly one bash, http, skill or mcp executor');
|
|
39
|
+
}
|
|
40
|
+
if ('mcp' in spec) {
|
|
41
|
+
const mcp = spec.mcp;
|
|
42
|
+
if (!mcp || typeof mcp !== 'object' || Array.isArray(mcp)
|
|
43
|
+
|| typeof (mcp as Record<string, unknown>).server !== 'string'
|
|
44
|
+
|| typeof (mcp as Record<string, unknown>).tool !== 'string') {
|
|
45
|
+
throw new Error('mcp run requires server and tool');
|
|
46
|
+
}
|
|
39
47
|
}
|
|
40
48
|
run = spec as NodeKindRun;
|
|
41
49
|
} else if (node.run !== undefined) {
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { execFile } from 'node:child_process';
|
|
3
|
-
import { linkSync, mkdirSync, readFileSync, readdirSync, renameSync, rmdirSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { existsSync, linkSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmdirSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, join, resolve, sep } from 'node:path';
|
|
5
5
|
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
6
|
+
import { getElanousConfigDir, getElanousConfigDirOverride } from '../elanous-config-dir.js';
|
|
6
7
|
import { debug } from '../debug/log.js';
|
|
8
|
+
import { elanousStateRoot } from '../autopilot/state-paths.js';
|
|
9
|
+
import { listInstalledPlugins } from '../plugins/install/plugin-install.js';
|
|
10
|
+
import { credentialStatus, pluginEnv } from '../plugins/install/plugin-credentials.js';
|
|
7
11
|
import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
8
12
|
import { loadNodeCatalog } from '../self-implement/graph-catalog.js';
|
|
9
13
|
import { parseGraphTemplateYaml, type GraphEdgeSpec, type GraphNodeSpec } from '../self-implement/graph-yaml.js';
|
|
@@ -215,6 +219,17 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
|
|
|
215
219
|
const parsed = parseGraphTemplateYaml(stringifyYaml(document), path);
|
|
216
220
|
if (!parsed.template || parsed.errors.length) throw new Error(parsed.errors.map((e) => `${e.path}: ${e.message}`).join('\n'));
|
|
217
221
|
const graph = parsed.template;
|
|
222
|
+
const pluginRoot = elanousStateRoot();
|
|
223
|
+
const graphFile = realpathSync(path);
|
|
224
|
+
const installed = listInstalledPlugins(pluginRoot);
|
|
225
|
+
const owner = installed.find(item => {
|
|
226
|
+
const graphsDir = join(item.path, 'graphs');
|
|
227
|
+
return existsSync(graphsDir) && graphFile.startsWith(`${realpathSync(graphsDir)}${sep}`);
|
|
228
|
+
});
|
|
229
|
+
const credentials = owner ? pluginEnv(owner.name, pluginRoot) : {};
|
|
230
|
+
const declaredEnv = [...new Set(installed.map(item => item.name))]
|
|
231
|
+
.flatMap(name => credentialStatus(name, pluginRoot).fields.map(field => field.env));
|
|
232
|
+
const credentialFields = owner ? credentialStatus(owner.name, pluginRoot).fields.filter(field => field.set).map(field => field.name) : [];
|
|
218
233
|
for (const terminal of graph.terminalNodes) {
|
|
219
234
|
if (terminal !== 'done' && terminal !== 'failed') {
|
|
220
235
|
throw new Error(`unsupported terminal node: ${terminal} (expected done or failed)`);
|
|
@@ -390,17 +405,32 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
|
|
|
390
405
|
mkdirSync(dirname(contextPath), { recursive: true });
|
|
391
406
|
writeFileSync(contextPath, JSON.stringify({ graphId, runId, nodeId: current, input: state.input ?? null, outputs }, null, 2) + '\n');
|
|
392
407
|
let code: number | null = null;
|
|
408
|
+
const redactCredentials = (text: string) => Object.values(credentials).filter(value => value.length > 0)
|
|
409
|
+
.reduce((safe, value) => safe.replaceAll(value, '[REDACTED]'), text);
|
|
393
410
|
const runBash: BashRun = async (body, opts) => {
|
|
394
411
|
const result = await (options.deps?.runBash ?? realRunBash)(body, opts);
|
|
395
412
|
code = result.exitCode;
|
|
396
|
-
return result;
|
|
413
|
+
return { ...result, stdout: redactCredentials(result.stdout), stderr: redactCredentials(result.stderr) };
|
|
397
414
|
};
|
|
398
|
-
|
|
415
|
+
if (owner) debug.log('plugin.credentials', 'injected', { plugin: owner.name, fields: credentialFields, count: credentialFields.length });
|
|
416
|
+
const env = { ...process.env };
|
|
417
|
+
for (const key of declaredEnv) delete env[key];
|
|
418
|
+
Object.assign(env, credentials);
|
|
419
|
+
// 러너가 명시로 받은 우주(`--config-dir`)를 `cmd:` 자식에 못 박는다 — 자식이 코드 위치(소스 트리)로 시험 우주를 새로 고르던 구멍
|
|
420
|
+
// (09-30 🅢 스튜어드: 작업 트리 `graph run … --config-dir ~/.elanous` 의 자식 triage 가 «apiKey missing» · 0.2.5 컷 릴리스 루프도 같은 발사 줄).
|
|
421
|
+
if (getElanousConfigDirOverride()) {
|
|
422
|
+
env.ELANOUS_CONFIG_DIR = getElanousConfigDir();
|
|
423
|
+
env.ELANOUS_STATE_DIR = effectiveInstanceRoot();
|
|
424
|
+
debug.log('graph-runner', 'child-universe-pinned', { graphId, runId, node: current, configDir: env.ELANOUS_CONFIG_DIR, stateDir: env.ELANOUS_STATE_DIR });
|
|
425
|
+
}
|
|
426
|
+
env.ELANOUS_GRAPH_CONTEXT = contextPath;
|
|
427
|
+
env.ELANOUS_GRAPH_DIR = dirname(resolve(path));
|
|
428
|
+
const ctx = { arguments: '', artifactsDir: dirname(statePath), outputs: {}, resolvedProvider: undefined, resolvedModel: undefined, toolPolicy: {}, env } as NodeExecContext;
|
|
399
429
|
const result = await executeBashNode({ id: current, type: 'bash', bash: command.command, ...(command.timeout_ms ? { idle_timeout: command.timeout_ms } : {}) } as BashNode, ctx, { runBash } as WorkflowDeps);
|
|
400
430
|
output = result.output;
|
|
401
431
|
ok = result.ok;
|
|
402
432
|
exit = code;
|
|
403
|
-
error = result.error;
|
|
433
|
+
error = result.error ? redactCredentials(result.error) : undefined;
|
|
404
434
|
state.executed++;
|
|
405
435
|
}
|
|
406
436
|
if (resumingCompleted && completed) {
|