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,143 @@
|
|
|
1
|
+
// 현장 업로드 입구 — POST/GET /v1/field/uploads (owner 전용).
|
|
2
|
+
// 저장 규칙은 src/field/field-media.ts 가 유일한 정본이고 텔레그램 `#현장` 도 같은 함수를 쓴다.
|
|
3
|
+
|
|
4
|
+
import { debug } from '../../debug/log.js';
|
|
5
|
+
import { getElanousConfigDir } from '../../elanous-config-dir.js';
|
|
6
|
+
import {
|
|
7
|
+
FIELD_MAX_FILE_BYTES,
|
|
8
|
+
FIELD_MAX_REQUEST_BYTES,
|
|
9
|
+
FieldUploadError,
|
|
10
|
+
fieldEventDir,
|
|
11
|
+
isFieldSlug,
|
|
12
|
+
listFieldMedia,
|
|
13
|
+
saveFieldMedia,
|
|
14
|
+
type FieldMediaInput,
|
|
15
|
+
} from '../../field/field-media.js';
|
|
16
|
+
import { jsonResponse } from './json-response.js';
|
|
17
|
+
|
|
18
|
+
export const FIELD_UPLOADS_PATH = '/v1/field/uploads';
|
|
19
|
+
|
|
20
|
+
/** 멀티파트 경계·헤더 여유. */
|
|
21
|
+
const MULTIPART_SLACK_BYTES = 4 * 1024 * 1024;
|
|
22
|
+
/** JSON 본문은 base64(×4/3) 라 더 크다. */
|
|
23
|
+
const JSON_MAX_BODY_BYTES = Math.ceil((FIELD_MAX_REQUEST_BYTES * 4) / 3) + MULTIPART_SLACK_BYTES;
|
|
24
|
+
/** Bun.serve 의 `maxRequestBodySize`(기본 128 MB) — 이 입구의 JSON 상한까지 받도록 서버가 이 값을 쓴다. */
|
|
25
|
+
export const FIELD_SERVER_MAX_BODY_BYTES = JSON_MAX_BODY_BYTES;
|
|
26
|
+
|
|
27
|
+
interface FieldUploadsDeps {
|
|
28
|
+
authorize?: (request: Request) => boolean;
|
|
29
|
+
/** config 뿌리 — 기본 `getElanousConfigDir()`(데몬의 다른 저장소와 같은 해석). 시험은 temp 를 준다. */
|
|
30
|
+
rootDir?: () => string;
|
|
31
|
+
now?: () => Date;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function errorResponse(error: FieldUploadError): Response {
|
|
35
|
+
return jsonResponse({ ok: false, error: error.code, reason: error.message }, error.status);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parseCapturedAtList(values: FormDataEntryValue[]): Array<string | undefined> {
|
|
39
|
+
const strings = values.filter((v): v is string => typeof v === 'string');
|
|
40
|
+
if (strings.length === 1 && strings[0]!.trim().startsWith('[')) {
|
|
41
|
+
try {
|
|
42
|
+
const arr = JSON.parse(strings[0]!) as unknown;
|
|
43
|
+
if (Array.isArray(arr)) return arr.map((v) => (typeof v === 'string' || typeof v === 'number' ? String(v) : undefined));
|
|
44
|
+
} catch { /* 배열이 아니면 단일 값으로 */ }
|
|
45
|
+
}
|
|
46
|
+
return strings;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function readMultipart(req: Request): Promise<FieldMediaInput[]> {
|
|
50
|
+
let form: FormData;
|
|
51
|
+
try { form = await req.formData(); }
|
|
52
|
+
catch { throw new FieldUploadError('bad-request', 'invalid multipart body'); }
|
|
53
|
+
const files = form.getAll('file').filter((v): v is File => typeof v !== 'string');
|
|
54
|
+
const captured = parseCapturedAtList(form.getAll('capturedAt'));
|
|
55
|
+
let total = 0;
|
|
56
|
+
const inputs: FieldMediaInput[] = [];
|
|
57
|
+
for (const [i, file] of files.entries()) {
|
|
58
|
+
if (file.size > FIELD_MAX_FILE_BYTES) throw new FieldUploadError('too-large', 'a file exceeds 100 MB');
|
|
59
|
+
total += file.size;
|
|
60
|
+
if (total > FIELD_MAX_REQUEST_BYTES) throw new FieldUploadError('too-large', 'the request exceeds 250 MB');
|
|
61
|
+
inputs.push({
|
|
62
|
+
originalName: file.name || 'media',
|
|
63
|
+
mimeType: file.type,
|
|
64
|
+
bytes: new Uint8Array(await file.arrayBuffer()),
|
|
65
|
+
...(captured[i] ? { capturedAt: captured[i] } : {}),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return inputs;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function readJson(req: Request): Promise<FieldMediaInput[]> {
|
|
72
|
+
let body: unknown;
|
|
73
|
+
try { body = await req.json(); }
|
|
74
|
+
catch { throw new FieldUploadError('bad-request', 'JSON body required'); }
|
|
75
|
+
const files = body && typeof body === 'object' && !Array.isArray(body) ? (body as { files?: unknown }).files : undefined;
|
|
76
|
+
if (!Array.isArray(files)) throw new FieldUploadError('bad-request', 'files[] required');
|
|
77
|
+
return files.map((raw) => {
|
|
78
|
+
const f = (raw ?? {}) as Record<string, unknown>;
|
|
79
|
+
if (typeof f.name !== 'string' || typeof f.dataBase64 !== 'string') {
|
|
80
|
+
throw new FieldUploadError('bad-request', 'each file needs name and dataBase64');
|
|
81
|
+
}
|
|
82
|
+
// base64 길이로 먼저 거른다 — 파일 상한(FIELD_MAX_FILE_BYTES)을 넘는 것을 디코드하지 않는다.
|
|
83
|
+
if (Math.floor((f.dataBase64.length * 3) / 4) > FIELD_MAX_FILE_BYTES + 3) {
|
|
84
|
+
throw new FieldUploadError('too-large', 'a file exceeds 100 MB');
|
|
85
|
+
}
|
|
86
|
+
const capturedAt = typeof f.capturedAt === 'string' || typeof f.capturedAt === 'number' ? f.capturedAt : undefined;
|
|
87
|
+
return {
|
|
88
|
+
originalName: f.name,
|
|
89
|
+
...(typeof f.mimeType === 'string' ? { mimeType: f.mimeType } : {}),
|
|
90
|
+
bytes: new Uint8Array(Buffer.from(f.dataBase64, 'base64')),
|
|
91
|
+
...(capturedAt !== undefined ? { capturedAt } : {}),
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function handleFieldUploads(req: Request, deps: FieldUploadsDeps = {}): Promise<Response> {
|
|
97
|
+
const url = new URL(req.url);
|
|
98
|
+
if (url.pathname !== FIELD_UPLOADS_PATH) return jsonResponse({ error: 'not-found' }, 404);
|
|
99
|
+
if (!deps.authorize?.(req)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
100
|
+
const rootDir = (deps.rootDir ?? getElanousConfigDir)();
|
|
101
|
+
const event = url.searchParams.get('event') ?? '';
|
|
102
|
+
if (!isFieldSlug(event)) return errorResponse(new FieldUploadError('bad-slug', 'event slug must match ^[a-z0-9][a-z0-9._-]{0,63}$'));
|
|
103
|
+
|
|
104
|
+
if (req.method === 'GET') {
|
|
105
|
+
const files = listFieldMedia(fieldEventDir(rootDir, event));
|
|
106
|
+
debug.log('field.upload', 'listed', { count: files.length });
|
|
107
|
+
return jsonResponse({ event, count: files.length, files });
|
|
108
|
+
}
|
|
109
|
+
if (req.method !== 'POST') return jsonResponse({ error: 'method-not-allowed' }, 405);
|
|
110
|
+
|
|
111
|
+
const device = url.searchParams.get('device') ?? '';
|
|
112
|
+
if (!isFieldSlug(device)) return errorResponse(new FieldUploadError('bad-slug', 'device slug must match ^[a-z0-9][a-z0-9._-]{0,63}$'));
|
|
113
|
+
|
|
114
|
+
const contentType = (req.headers.get('content-type') ?? '').toLowerCase();
|
|
115
|
+
const isMultipart = contentType.startsWith('multipart/form-data');
|
|
116
|
+
const isJson = contentType.startsWith('application/json');
|
|
117
|
+
if (!isMultipart && !isJson) {
|
|
118
|
+
return errorResponse(new FieldUploadError('unsupported-media-type', 'use multipart/form-data or application/json'));
|
|
119
|
+
}
|
|
120
|
+
const declared = Number(req.headers.get('content-length') ?? '');
|
|
121
|
+
const bodyCap = isJson ? JSON_MAX_BODY_BYTES : FIELD_MAX_REQUEST_BYTES + MULTIPART_SLACK_BYTES;
|
|
122
|
+
if (Number.isFinite(declared) && declared > bodyCap) {
|
|
123
|
+
debug.log('field.upload', 'refused', { reason: 'too-large', declaredBytes: declared });
|
|
124
|
+
return errorResponse(new FieldUploadError('too-large', 'the request exceeds 250 MB'));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const inputs = isMultipart ? await readMultipart(req) : await readJson(req);
|
|
129
|
+
const caption = url.searchParams.get('caption') ?? undefined;
|
|
130
|
+
const result = saveFieldMedia(inputs, { rootDir, event, device, ...(caption ? { caption } : {}), ...(deps.now ? { now: deps.now } : {}) });
|
|
131
|
+
debug.log('field.upload', 'saved', {
|
|
132
|
+
surface: 'http', files: result.saved.length, bytes: result.saved.reduce((n, s) => n + s.bytes, 0), count: result.count,
|
|
133
|
+
});
|
|
134
|
+
return jsonResponse({ ok: true, event, saved: result.saved, count: result.count });
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (error instanceof FieldUploadError) {
|
|
137
|
+
debug.log('field.upload', 'refused', { surface: 'http', reason: error.code });
|
|
138
|
+
return errorResponse(error);
|
|
139
|
+
}
|
|
140
|
+
debug.log('field.upload', 'failed', { surface: 'http' });
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, type Dirent } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { debug } from '../../debug/log.js';
|
|
4
|
+
import { decideGraphApproval, lastJsonObject, type GraphRunState } from '../../graph-runner/runner.js';
|
|
5
|
+
import { effectiveInstanceRoot } from '../../instance/resolve.js';
|
|
6
|
+
import { jsonResponse } from './json-response.js';
|
|
7
|
+
|
|
8
|
+
export const GRAPH_APPROVALS_PATH = '/v1/graph-approvals';
|
|
9
|
+
const SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
10
|
+
|
|
11
|
+
interface GraphApprovalItem {
|
|
12
|
+
graphId: string;
|
|
13
|
+
runId: string;
|
|
14
|
+
nodeId: string;
|
|
15
|
+
message: string;
|
|
16
|
+
since: string;
|
|
17
|
+
path: string[];
|
|
18
|
+
recent: Array<{ nodeId: string; ok: boolean; outcome?: string; summary?: string }>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** One decision this daemon applied through `POST /v1/graph-approvals/:graphId/:runId`. */
|
|
22
|
+
interface GraphApprovalDecided {
|
|
23
|
+
graphId: string;
|
|
24
|
+
runId: string;
|
|
25
|
+
nodeId: string;
|
|
26
|
+
decision: 'approved' | 'rejected';
|
|
27
|
+
decidedAt: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const GRAPH_DECISION_RING_CAPACITY = 100;
|
|
31
|
+
export const GRAPH_DECISION_RING_MAX_AGE_MS = 30 * 60 * 1000;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Recent decisions, in memory. The undecided list drops a run the moment it is decided, so
|
|
35
|
+
* another device (Fold ↔ iPhone) could only see «gone» — this ring lets it say «승인됨/거절됨».
|
|
36
|
+
* ⚠️ In-memory: a daemon restart forgets it, and a decision made outside this process
|
|
37
|
+
* (`elanous graph approve` CLI) is not recorded — clients keep a «decided elsewhere» fallback.
|
|
38
|
+
*/
|
|
39
|
+
export function createGraphDecisionRing(opts: { capacity?: number; maxAgeMs?: number; now?: () => number } = {}) {
|
|
40
|
+
const capacity = opts.capacity ?? GRAPH_DECISION_RING_CAPACITY;
|
|
41
|
+
const maxAgeMs = opts.maxAgeMs ?? GRAPH_DECISION_RING_MAX_AGE_MS;
|
|
42
|
+
const now = opts.now ?? Date.now;
|
|
43
|
+
let entries: Array<{ atMs: number; record: GraphApprovalDecided }> = [];
|
|
44
|
+
const prune = () => {
|
|
45
|
+
const cutoff = now() - maxAgeMs;
|
|
46
|
+
entries = entries.filter((entry) => entry.atMs > cutoff).slice(-capacity);
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
record(decision: Omit<GraphApprovalDecided, 'decidedAt'>): void {
|
|
50
|
+
const atMs = now();
|
|
51
|
+
entries.push({ atMs, record: { ...decision, decidedAt: new Date(atMs).toISOString() } });
|
|
52
|
+
prune();
|
|
53
|
+
},
|
|
54
|
+
/** Newest first, pruned by age and capacity. */
|
|
55
|
+
list(): GraphApprovalDecided[] {
|
|
56
|
+
prune();
|
|
57
|
+
return entries.map((entry) => entry.record).reverse();
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type GraphDecisionRing = ReturnType<typeof createGraphDecisionRing>;
|
|
63
|
+
|
|
64
|
+
const defaultDecisionRing = createGraphDecisionRing();
|
|
65
|
+
|
|
66
|
+
interface GraphApprovalsDeps {
|
|
67
|
+
/** Injected in tests; production shares one per-process ring. */
|
|
68
|
+
decisions?: GraphDecisionRing;
|
|
69
|
+
authorize?: (request: Request) => boolean;
|
|
70
|
+
/** Credential the request carries, checked independently of same-origin (`bearerCredential`).
|
|
71
|
+
* A decision needs a bearer token: same-origin alone is not trusted because behind
|
|
72
|
+
* `tailscale serve` every tailnet peer (Pod included) arrives from loopback. */
|
|
73
|
+
authReason?: (request: Request) => string | undefined;
|
|
74
|
+
root?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const DECISION_AUTH_REASONS = new Set(['bearer-match', 'temp-token', 'noauth']);
|
|
78
|
+
|
|
79
|
+
function validSegment(value: string): boolean {
|
|
80
|
+
return SEGMENT.test(value) && value !== '.' && value !== '..';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function readRun(root: string, graphId: string, runId: string): GraphRunState | null {
|
|
84
|
+
if (!validSegment(graphId) || !validSegment(runId)) return null;
|
|
85
|
+
try {
|
|
86
|
+
const state = JSON.parse(readFileSync(join(root, 'graph-runs', graphId, `${runId}.json`), 'utf8')) as GraphRunState;
|
|
87
|
+
return state.graphId === graphId && state.runId === runId ? state : null;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function undecided(state: GraphRunState, root: string): boolean {
|
|
95
|
+
return state.status === 'awaiting-approval' && !!state.pending && !state.pending.decision
|
|
96
|
+
&& Array.isArray(state.path) && !existsSync(join(root, 'graph-runs', state.graphId, `${state.runId}.json.${state.path.length}.decision.json`));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function list(root: string): GraphApprovalItem[] {
|
|
100
|
+
const dir = join(root, 'graph-runs');
|
|
101
|
+
let graphs: Dirent[];
|
|
102
|
+
try { graphs = readdirSync(dir, { withFileTypes: true }); }
|
|
103
|
+
catch (error) {
|
|
104
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return [];
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
const items: GraphApprovalItem[] = [];
|
|
108
|
+
for (const graph of graphs) {
|
|
109
|
+
if (!graph.isDirectory() || !validSegment(graph.name)) continue;
|
|
110
|
+
for (const file of readdirSync(join(dir, graph.name), { withFileTypes: true })) {
|
|
111
|
+
if (!file.isFile() || !file.name.endsWith('.json') || file.name.endsWith('.decision.json')) continue;
|
|
112
|
+
const runId = file.name.slice(0, -5);
|
|
113
|
+
const state = readRun(root, graph.name, runId);
|
|
114
|
+
if (!state || !undecided(state, root) || !Array.isArray(state.nodes)) continue;
|
|
115
|
+
const pending = state.pending!;
|
|
116
|
+
items.push({
|
|
117
|
+
graphId: state.graphId, runId: state.runId, nodeId: pending.nodeId, message: pending.message,
|
|
118
|
+
since: pending.since, path: state.path,
|
|
119
|
+
recent: state.nodes.slice(-5).map((node) => {
|
|
120
|
+
const output = lastJsonObject(node.output);
|
|
121
|
+
return {
|
|
122
|
+
nodeId: node.nodeId, ok: node.ok,
|
|
123
|
+
...(typeof output?.outcome === 'string' ? { outcome: output.outcome } : {}),
|
|
124
|
+
...(typeof output?.summary === 'string' ? { summary: output.summary } : {}),
|
|
125
|
+
};
|
|
126
|
+
}),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return items.sort((a, b) => b.since.localeCompare(a.since));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function handleGraphApprovals(req: Request, deps: GraphApprovalsDeps = {}): Promise<Response> {
|
|
134
|
+
const pathname = new URL(req.url).pathname;
|
|
135
|
+
const match = /^\/v1\/graph-approvals\/([^/]+)\/([^/]+)$/.exec(pathname);
|
|
136
|
+
if (pathname !== GRAPH_APPROVALS_PATH && !match) return jsonResponse({ error: 'not-found' }, 404);
|
|
137
|
+
if (!deps.authorize?.(req)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
138
|
+
if (pathname === GRAPH_APPROVALS_PATH) {
|
|
139
|
+
if (req.method !== 'GET') return jsonResponse({ error: 'method-not-allowed' }, 405);
|
|
140
|
+
// ⭐ `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() });
|
|
142
|
+
}
|
|
143
|
+
if (req.method !== 'POST') return jsonResponse({ error: 'method-not-allowed' }, 405);
|
|
144
|
+
const authReason = deps.authReason?.(req);
|
|
145
|
+
if (!authReason || !DECISION_AUTH_REASONS.has(authReason)) {
|
|
146
|
+
debug.log('approvals.graph', 'refused', { reason: 'pairing-required', auth: authReason ?? null });
|
|
147
|
+
return jsonResponse({ error: 'pairing-required' }, 403);
|
|
148
|
+
}
|
|
149
|
+
let graphId: string;
|
|
150
|
+
let runId: string;
|
|
151
|
+
try { graphId = decodeURIComponent(match![1]!); runId = decodeURIComponent(match![2]!); }
|
|
152
|
+
catch { return jsonResponse({ error: 'not-found' }, 404); }
|
|
153
|
+
if (!validSegment(graphId) || !validSegment(runId)) return jsonResponse({ error: 'not-found' }, 404);
|
|
154
|
+
let body: unknown;
|
|
155
|
+
try { body = await req.json(); }
|
|
156
|
+
catch { return jsonResponse({ error: 'bad_request' }, 400); }
|
|
157
|
+
const decision = body && typeof body === 'object' && !Array.isArray(body) && 'decision' in body ? body.decision : undefined;
|
|
158
|
+
if (decision !== 'approved' && decision !== 'rejected') return jsonResponse({ error: 'bad_request' }, 400);
|
|
159
|
+
const root = deps.root ?? effectiveInstanceRoot();
|
|
160
|
+
const state = readRun(root, graphId, runId);
|
|
161
|
+
if (!state) return jsonResponse({ error: 'not-found' }, 404);
|
|
162
|
+
if (!undecided(state, root)) return jsonResponse({ error: 'already-decided' }, 409);
|
|
163
|
+
const nodeId = state.pending!.nodeId;
|
|
164
|
+
try {
|
|
165
|
+
decideGraphApproval(graphId, runId, decision, `pwa:${authReason}`, root);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return jsonResponse({ error: 'not-found' }, 404);
|
|
168
|
+
if (error instanceof Error && error.message.startsWith('run is not awaiting an undecided approval:')) return jsonResponse({ error: 'already-decided' }, 409);
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
(deps.decisions ?? defaultDecisionRing).record({ graphId, runId, nodeId, decision });
|
|
172
|
+
debug.log('approvals.graph', 'decided', { graphId, runId, nodeId, decision, by: `pwa:${authReason}` });
|
|
173
|
+
return jsonResponse({ graphId, runId, decision });
|
|
174
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { relative } from 'node:path';
|
|
1
|
+
import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
import { hasTerminalRunStatus, loadRunLedger, runLedgerDir, runLedgerPath, type RunLedgerEntry } from '../../self-implement/run-ledger.js';
|
|
4
|
+
import { summarizeTerminalLedger } from '../../self-implement/run-ledger-compact.js';
|
|
3
5
|
import { debug } from '../../debug/log.js';
|
|
4
6
|
import { enqueueSoftStop } from '../../harness/control-inbox.js';
|
|
5
7
|
import { listHarnessScreens, readHarnessScreenTail } from '../../harness/harness-screen.js';
|
|
@@ -103,6 +105,10 @@ export interface HarnessApiDeps {
|
|
|
103
105
|
readonly createAcceptanceId?: () => string;
|
|
104
106
|
readonly log?: (event: string, data: Record<string, unknown>) => void;
|
|
105
107
|
readonly createFeedbackEmitter?: (acceptanceId: string) => (env: FeedbackEnvelope) => void | Promise<void>;
|
|
108
|
+
/** 시험 심 — `?finishedSince=` 가 읽는 원장 디렉터리(기본 `runLedgerDir()`). */
|
|
109
|
+
readonly ledgerDir?: string;
|
|
110
|
+
/** 시험 심 — `?finishedSince=` 의 `landed`(gh 실행기·저장소·시간 초과). */
|
|
111
|
+
readonly landed?: LandedPrsDeps;
|
|
106
112
|
}
|
|
107
113
|
|
|
108
114
|
function json(body: unknown, status = 200): Response {
|
|
@@ -338,10 +344,218 @@ export function handleHarnessAskStatusGet(req: Request, _metaApi: HarnessMetaApi
|
|
|
338
344
|
});
|
|
339
345
|
}
|
|
340
346
|
|
|
341
|
-
/**
|
|
342
|
-
export
|
|
347
|
+
/** One terminated run for `GET /v1/harness/runs?finishedSince=` (iPhone «오늘 끝난 일» · 2026-09-30). */
|
|
348
|
+
export interface HarnessFinishedRun {
|
|
349
|
+
runId: string;
|
|
350
|
+
status: string;
|
|
351
|
+
endedAt: string;
|
|
352
|
+
/** 마지막 `run-status` 의 `stage`(예: `merged`·`pr-declined`) — 칩을 더 정확히 고를 때. */
|
|
353
|
+
stage?: string;
|
|
354
|
+
objective?: string;
|
|
355
|
+
prUrl?: string;
|
|
356
|
+
/** `merged` 이벤트의 값 — 이벤트가 없으면 칸이 없다(«병합 안 됨»으로 접지 않는다). */
|
|
357
|
+
merged?: boolean;
|
|
358
|
+
/** 마지막 `merge-decision` 의 `decision`(`auto`·`hitl`) — 병합 «결정»이지 병합 «사실»이 아니다. */
|
|
359
|
+
mergeDecision?: string;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export const FINISHED_RUNS_LIMIT = 20;
|
|
363
|
+
export const FINISHED_RUNS_MAX_FILES = 300;
|
|
364
|
+
export const FINISHED_RUNS_MAX_FILE_BYTES = 5 * 1024 * 1024;
|
|
365
|
+
const FINISHED_OBJECTIVE_MAX_CHARS = 160;
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* 원장 디렉터리에서 `sinceMs` 이후에 쓰인 «끝난» 런을 요약한다. 읽기는 묶여 있다 —
|
|
369
|
+
* mtime 최신 {@link FINISHED_RUNS_MAX_FILES} 개만 보고, {@link FINISHED_RUNS_MAX_FILE_BYTES} 를 넘는 파일은 건너뛴다.
|
|
370
|
+
*/
|
|
371
|
+
export function collectFinishedRuns(dir: string, sinceMs: number): { runs: HarnessFinishedRun[]; scannedFiles: number; skippedFiles: number } {
|
|
372
|
+
let names: string[];
|
|
373
|
+
try { names = readdirSync(dir); }
|
|
374
|
+
catch { return { runs: [], scannedFiles: 0, skippedFiles: 0 }; }
|
|
375
|
+
const candidates: Array<{ runId: string; mtimeMs: number; size: number }> = [];
|
|
376
|
+
for (const name of names) {
|
|
377
|
+
if (!name.endsWith('.jsonl')) continue;
|
|
378
|
+
try {
|
|
379
|
+
const st = statSync(join(dir, name));
|
|
380
|
+
if (st.isFile() && st.mtimeMs >= sinceMs) candidates.push({ runId: name.slice(0, -'.jsonl'.length), mtimeMs: st.mtimeMs, size: st.size });
|
|
381
|
+
} catch { /* 사이에 지워졌다 */ }
|
|
382
|
+
}
|
|
383
|
+
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
384
|
+
const scanned = candidates.slice(0, FINISHED_RUNS_MAX_FILES);
|
|
385
|
+
let skippedFiles = candidates.length - scanned.length;
|
|
386
|
+
const runs: HarnessFinishedRun[] = [];
|
|
387
|
+
for (const file of scanned) {
|
|
388
|
+
if (file.size > FINISHED_RUNS_MAX_FILE_BYTES) { skippedFiles += 1; continue; }
|
|
389
|
+
let entries: RunLedgerEntry[] | null;
|
|
390
|
+
try { entries = loadRunLedger(file.runId, dir); }
|
|
391
|
+
catch { skippedFiles += 1; continue; }
|
|
392
|
+
if (!entries?.length || !hasTerminalRunStatus(entries)) continue;
|
|
393
|
+
let summary: ReturnType<typeof summarizeTerminalLedger>;
|
|
394
|
+
try { summary = summarizeTerminalLedger(file.runId, entries); }
|
|
395
|
+
catch { skippedFiles += 1; continue; }
|
|
396
|
+
if (!(Date.parse(summary.endedAt) >= sinceMs)) continue;
|
|
397
|
+
const feature = entries.find((entry) => entry.event === 'start' && typeof entry.data.feature === 'string')?.data.feature as string | undefined;
|
|
398
|
+
const objective = feature?.trim().slice(0, FINISHED_OBJECTIVE_MAX_CHARS);
|
|
399
|
+
const mergedEvent = [...entries].reverse().find((entry) => entry.event === 'merged' && typeof entry.data.merged === 'boolean');
|
|
400
|
+
const decision = [...entries].reverse().find((entry) => entry.event === 'merge-decision' && typeof entry.data.decision === 'string');
|
|
401
|
+
const statusEntry = [...entries].reverse().find((entry) => entry.event === 'run-status' && typeof entry.data.stage === 'string');
|
|
402
|
+
runs.push({
|
|
403
|
+
runId: summary.runId,
|
|
404
|
+
status: summary.status,
|
|
405
|
+
endedAt: summary.endedAt,
|
|
406
|
+
...(statusEntry ? { stage: statusEntry.data.stage as string } : {}),
|
|
407
|
+
...(objective ? { objective } : {}),
|
|
408
|
+
...(summary.prUrl ? { prUrl: summary.prUrl } : {}),
|
|
409
|
+
...(mergedEvent ? { merged: mergedEvent.data.merged as boolean } : {}),
|
|
410
|
+
...(decision ? { mergeDecision: decision.data.decision as string } : {}),
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
runs.sort((a, b) => Date.parse(b.endedAt) - Date.parse(a.endedAt));
|
|
414
|
+
return { runs: runs.slice(0, FINISHED_RUNS_LIMIT), scannedFiles: scanned.length, skippedFiles };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** 도는 런의 목표 — 그 런 원장의 첫 `start.data.feature`(≤160자). 원장 경로는 그 항목이 준 디렉터리들만 본다(런 수만큼만 읽는다). */
|
|
418
|
+
export function runObjectiveFromLedgers(runId: string, directories: readonly string[]): string | undefined {
|
|
419
|
+
for (const dir of directories) {
|
|
420
|
+
try {
|
|
421
|
+
if (statSync(runLedgerPath(runId, dir)).size > FINISHED_RUNS_MAX_FILE_BYTES) continue;
|
|
422
|
+
const feature = loadRunLedger(runId, dir)?.find((entry) => entry.event === 'start' && typeof entry.data.feature === 'string')?.data.feature;
|
|
423
|
+
const objective = typeof feature === 'string' ? feature.trim().slice(0, FINISHED_OBJECTIVE_MAX_CHARS) : '';
|
|
424
|
+
if (objective) return objective;
|
|
425
|
+
} catch { /* 없거나 못 읽음 — 다음 디렉터리 */ }
|
|
426
|
+
}
|
|
427
|
+
return undefined;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ── 오늘 착지(= main 에 병합된 PR) — 원장은 Pod·시험 우주의 런을 못 보므로 GitHub 이 정본이다 ──
|
|
431
|
+
|
|
432
|
+
/** `landed` 한 줄 — `gh pr list --json number,title,url,mergedAt` 모양 그대로. */
|
|
433
|
+
export interface HarnessLandedPr {
|
|
434
|
+
number: number;
|
|
435
|
+
title: string;
|
|
436
|
+
url: string;
|
|
437
|
+
mergedAt: string;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export const LANDED_PRS_LIMIT = 30;
|
|
441
|
+
export const LANDED_PRS_TIMEOUT_MS = 4_000;
|
|
442
|
+
const LANDED_CACHE_MS = 60_000;
|
|
443
|
+
const landedCache = new Map<string, { at: number; prs: HarnessLandedPr[]; truncated: boolean }>();
|
|
444
|
+
let cachedRepoSlug: string | null | undefined;
|
|
445
|
+
|
|
446
|
+
export function _resetLandedPrsCacheForTest(): void {
|
|
447
|
+
landedCache.clear();
|
|
448
|
+
cachedRepoSlug = undefined;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export interface LandedPrsDeps {
|
|
452
|
+
/** gh 실행기(기본 = `git-fs/gh-cli` 의 재시도 게이트웨이 · 비동기). */
|
|
453
|
+
readonly runGh?: (args: string[]) => Promise<{ ok: boolean; exitCode: number; stdout: Buffer | string; stderr: Buffer | string }>;
|
|
454
|
+
/** 저장소 `owner/name` — 기본은 데몬이 아는 저장소 뿌리(`harness.defaultRepo` → cwd)의 origin. */
|
|
455
|
+
readonly repoSlug?: () => Promise<string | null>;
|
|
456
|
+
readonly timeoutMs?: number;
|
|
457
|
+
readonly now?: () => number;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async function defaultRepoSlug(): Promise<string | null> {
|
|
461
|
+
if (cachedRepoSlug !== undefined) return cachedRepoSlug;
|
|
462
|
+
const [{ resolveWorktreesRepoRoot }, { slugFromRemote }] = await Promise.all([import('./worktrees.js'), import('./live-shipped.js')]);
|
|
463
|
+
const root = resolveWorktreesRepoRoot();
|
|
464
|
+
if (!root) return (cachedRepoSlug = null);
|
|
465
|
+
const proc = Bun.spawn(['git', '-C', root, 'remote', 'get-url', 'origin'], { stdout: 'pipe', stderr: 'ignore' });
|
|
466
|
+
const [out, code] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
|
467
|
+
return (cachedRepoSlug = code === 0 ? slugFromRemote(out) : null);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function defaultRunGh(args: string[]) {
|
|
471
|
+
const { runGhCliWithResultAsync } = await import('../../git-fs/gh-cli.js');
|
|
472
|
+
return runGhCliWithResultAsync(args);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function isLandedPr(value: unknown): value is HarnessLandedPr {
|
|
476
|
+
const v = value as Partial<HarnessLandedPr> | null;
|
|
477
|
+
return !!v && typeof v.number === 'number' && typeof v.title === 'string' && typeof v.url === 'string' && typeof v.mergedAt === 'string';
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** `sinceMs` 이후 main 에 병합된 PR(최신순 ≤30). 실패·시간 초과는 던지지 않고 `error` 로 돌려준다. 성공만 60초 캐시한다. */
|
|
481
|
+
export async function listLandedPrs(sinceMs: number, deps: LandedPrsDeps = {}): Promise<{ prs: HarnessLandedPr[]; error?: string; cached?: boolean; truncated?: boolean }> {
|
|
482
|
+
const now = deps.now?.() ?? Date.now();
|
|
483
|
+
const sinceIso = new Date(Math.floor(sinceMs / 60_000) * 60_000).toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
484
|
+
const hit = landedCache.get(sinceIso);
|
|
485
|
+
if (hit && now - hit.at < LANDED_CACHE_MS) return { prs: hit.prs, cached: true, ...(hit.truncated ? { truncated: true } : {}) };
|
|
486
|
+
const timeoutMs = deps.timeoutMs ?? LANDED_PRS_TIMEOUT_MS;
|
|
487
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
488
|
+
const timeout = new Promise<{ prs: HarnessLandedPr[]; error: string }>((resolve) => {
|
|
489
|
+
timer = setTimeout(() => resolve({ prs: [], error: `timeout ${timeoutMs}ms` }), timeoutMs);
|
|
490
|
+
});
|
|
491
|
+
const work = (async (): Promise<{ prs: HarnessLandedPr[]; error?: string; truncated?: boolean }> => {
|
|
492
|
+
try {
|
|
493
|
+
const slug = await (deps.repoSlug ?? defaultRepoSlug)();
|
|
494
|
+
if (!slug) return { prs: [], error: 'no-repository' };
|
|
495
|
+
const result = await (deps.runGh ?? defaultRunGh)([
|
|
496
|
+
'pr', 'list', '--repo', slug, '--state', 'merged', '--base', 'main',
|
|
497
|
+
'--search', `merged:>=${sinceIso}`,
|
|
498
|
+
'--json', 'number,title,url,mergedAt', '--limit', String(LANDED_PRS_LIMIT),
|
|
499
|
+
]);
|
|
500
|
+
if (!result.ok) {
|
|
501
|
+
const stderr = String(result.stderr).trim().split('\n')[0] ?? '';
|
|
502
|
+
return { prs: [], error: `gh failed rc=${result.exitCode}${stderr ? `: ${redactSecretText(stderr).slice(0, 120)}` : ''}` };
|
|
503
|
+
}
|
|
504
|
+
const parsed: unknown = JSON.parse(String(result.stdout));
|
|
505
|
+
if (!Array.isArray(parsed)) return { prs: [], error: 'gh returned non-array' };
|
|
506
|
+
const prs = parsed.filter(isLandedPr)
|
|
507
|
+
.filter((pr) => Date.parse(pr.mergedAt) >= sinceMs)
|
|
508
|
+
.sort((a, b) => Date.parse(b.mergedAt) - Date.parse(a.mergedAt));
|
|
509
|
+
// gh 가 상한만큼 줬으면 더 있을 수 있다 — «전부»라고 말하지 않는다.
|
|
510
|
+
const truncated = parsed.length >= LANDED_PRS_LIMIT;
|
|
511
|
+
landedCache.set(sinceIso, { at: now, prs, truncated });
|
|
512
|
+
return { prs, ...(truncated ? { truncated: true } : {}) };
|
|
513
|
+
} catch (error) {
|
|
514
|
+
return { prs: [], error: message(error).slice(0, 120) };
|
|
515
|
+
}
|
|
516
|
+
})();
|
|
517
|
+
try {
|
|
518
|
+
return await Promise.race([work, timeout]);
|
|
519
|
+
} finally {
|
|
520
|
+
if (timer) clearTimeout(timer);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** Return the structured shared running-runs observation without human rendering.
|
|
525
|
+
* `?finishedSince=<epoch ms>` 가 있으면(iPhone «오늘» · 2026-09-30) 셋을 더한다 —
|
|
526
|
+
* `finished`(이 우주 원장의 끝난 런 · 최신순 ≤20) · `landed`(그 시각 뒤 main 에 병합된 PR · ≤30 · 실패면 `[]` ⊕ `landedError`) ·
|
|
527
|
+
* 도는 항목마다 `objective`(그 런 원장의 `start.feature`). 없으면 응답은 종전 그대로다(동기 응답). */
|
|
528
|
+
export function handleHarnessRunsGet(req: Request, _metaApi: HarnessMetaApi, deps: HarnessApiDeps = {}): Response | Promise<Response> {
|
|
343
529
|
const runs: RunningRunsResult = (deps.queryRunningRuns ?? queryRunningRuns)({ includeTest: false });
|
|
344
|
-
|
|
530
|
+
const raw = new URL(req.url).searchParams.get('finishedSince');
|
|
531
|
+
if (raw === null) return json(runs);
|
|
532
|
+
const sinceMs = Number(raw);
|
|
533
|
+
if (!raw.trim() || !Number.isFinite(sinceMs) || sinceMs < 0) {
|
|
534
|
+
return json({ error: 'usage: GET /v1/harness/runs?finishedSince=<epoch ms>' }, 400);
|
|
535
|
+
}
|
|
536
|
+
return (async () => {
|
|
537
|
+
const landedWork = listLandedPrs(sinceMs, deps.landed ?? {});
|
|
538
|
+
const dir = deps.ledgerDir ?? runLedgerDir();
|
|
539
|
+
const finished = collectFinishedRuns(dir, sinceMs);
|
|
540
|
+
const entries = runs.entries.map((entry) => {
|
|
541
|
+
const objective = runObjectiveFromLedgers(entry.runId, entry.ledgerDirectories?.length ? entry.ledgerDirectories : [dir]);
|
|
542
|
+
return objective ? { ...entry, objective } : entry;
|
|
543
|
+
});
|
|
544
|
+
const landed = await landedWork;
|
|
545
|
+
debug.log('harness-http', 'runs-finished', {
|
|
546
|
+
sinceMs, count: finished.runs.length, scannedFiles: finished.scannedFiles, skippedFiles: finished.skippedFiles,
|
|
547
|
+
landed: landed.prs.length, landedCached: landed.cached === true, ...(landed.error ? { landedError: landed.error } : {}),
|
|
548
|
+
});
|
|
549
|
+
return json({
|
|
550
|
+
...runs,
|
|
551
|
+
entries,
|
|
552
|
+
finished: finished.runs,
|
|
553
|
+
finishedObservation: { sinceMs, scannedFiles: finished.scannedFiles, skippedFiles: finished.skippedFiles, limit: FINISHED_RUNS_LIMIT },
|
|
554
|
+
landed: landed.prs,
|
|
555
|
+
...(landed.truncated ? { landedTruncated: true } : {}),
|
|
556
|
+
...(landed.error ? { landedError: landed.error } : {}),
|
|
557
|
+
});
|
|
558
|
+
})();
|
|
345
559
|
}
|
|
346
560
|
|
|
347
561
|
/** Return the human-readable skeleton events persisted for one harness run. */
|