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
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
# Daemon port literal baseline: non-comment lines in non-test src/**/*.ts, per file.
|
|
2
2
|
# New files and increases fail. Migrate callers to the daemon endpoint resolver, then --update.
|
|
3
|
-
# total=
|
|
3
|
+
# total=12 files=5
|
|
4
4
|
1 src/autopilot/proposal/draft-plan.ts
|
|
5
5
|
6 src/index.ts
|
|
6
|
-
1 src/nexus/api/mcp-oauth-hint.ts
|
|
7
6
|
1 src/nexus/default-port.ts
|
|
8
7
|
2 src/task-orchestrator/surfaces/pod-command-job.ts
|
|
9
8
|
2 src/task-orchestrator/surfaces/self-implement-pod.ts
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// 봇 UX «그록봇 수준» 판별 — 같은 질문 묶음을 elanous chat(글자) · elanous agent(도구) · xAI Grok 에 던지고 원문·시간을 남긴다.
|
|
2
|
+
// 사용: bun scripts/eval-bot-vs-grok.ts <outDir> <repoCwd> · Grok 모델 = 사다리 `budget` 칸(tierModel) (키 값은 출력하지 않는다)
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { tierModel } from '../src/llm/model-defaults.js';
|
|
7
|
+
|
|
8
|
+
const [,, outDir, repoCwd] = process.argv;
|
|
9
|
+
mkdirSync(outDir, { recursive: true });
|
|
10
|
+
|
|
11
|
+
type Q = { id: string; axis: string; text: string; session?: string; needsTools?: boolean };
|
|
12
|
+
const QUESTIONS: Q[] = [
|
|
13
|
+
{ id: 'q01', axis: 'smalltalk', text: '안녕, 오늘 뭐 도와줄 수 있어? 세 줄 안으로.' },
|
|
14
|
+
{ id: 'q02', axis: 'smalltalk', text: '요즘 좀 지쳤어. 짧게 한마디 해줘.' },
|
|
15
|
+
{ id: 'q03', axis: 'fact', text: '서울에서 부산까지 KTX 로 대략 몇 시간 걸려? 한 줄로.' },
|
|
16
|
+
{ id: 'q04', axis: 'fact', text: 'HWPX 파일 형식이 뭐야? 두 줄로.' },
|
|
17
|
+
{ id: 'q05', axis: 'context', session: 'ctx', text: '내 프로젝트 이름은 elanous 야. 기억해 둬. «알겠다» 한 마디만.' },
|
|
18
|
+
{ id: 'q06', axis: 'context', session: 'ctx', text: '우리 팀은 네 세션으로 나뉘어 있어: OP, MK, TC, UX. 이것도 기억해. 한 마디만.' },
|
|
19
|
+
{ id: 'q07', axis: 'context', session: 'ctx', text: '내 프로젝트 이름이랑 팀 세션 약자 넷을 한 줄로 말해 봐.' },
|
|
20
|
+
{ id: 'q08', axis: 'tools', needsTools: true, text: '지금 작업 폴더(git 저장소)에서 가장 최근 커밋 제목 세 개를 알려줘.' },
|
|
21
|
+
{ id: 'q09', axis: 'tools', needsTools: true, text: 'apps/pwa 폴더에 파일이 대략 몇 개야? 숫자 하나로.' },
|
|
22
|
+
{ id: 'q10', axis: 'tools', needsTools: true, text: '오늘 날짜를 한국 시간으로 알려줘.' },
|
|
23
|
+
{ id: 'q11', axis: 'recovery', needsTools: true, text: 'docs/NOPE-does-not-exist-xyz.md 파일을 요약해줘.' },
|
|
24
|
+
{ id: 'q12', axis: 'recovery', text: '내가 어제 너한테 부탁한 일 결과 알려줘.' },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
type Row = { id: string; axis: string; target: string; ms: number; ok: boolean; reply: string; meta?: Record<string, unknown> };
|
|
28
|
+
const rows: Row[] = [];
|
|
29
|
+
|
|
30
|
+
function runElanous(mode: 'chat' | 'agent', q: Q, session?: string): Row {
|
|
31
|
+
const args = [mode, '--json', ...(session ? ['--session', session] : ['--new']), q.text];
|
|
32
|
+
const t0 = Date.now();
|
|
33
|
+
const r = spawnSync('elanous', args, { cwd: repoCwd, encoding: 'utf8', timeout: 240_000, maxBuffer: 64 << 20 });
|
|
34
|
+
const ms = Date.now() - t0;
|
|
35
|
+
const line = (r.stdout ?? '').trim().split('\n').reverse().find((l) => l.startsWith('{')) ?? '';
|
|
36
|
+
let parsed: Record<string, unknown> = {};
|
|
37
|
+
try { parsed = JSON.parse(line); } catch { /* keep empty */ }
|
|
38
|
+
const reply = typeof parsed.reply === 'string' ? parsed.reply : ((r.stderr ?? '').slice(-400) || '(no reply)');
|
|
39
|
+
return { id: q.id, axis: q.axis, target: `elanous-${mode}`, ms, ok: r.status === 0 && typeof parsed.reply === 'string', reply, meta: { sessionId: parsed.sessionId, provider: parsed.provider, model: parsed.model, status: r.status } };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function xaiKey(): string {
|
|
43
|
+
const env = readFileSync(join(process.env.HOME!, '.claude/skills/omni-crawl/.env'), 'utf8');
|
|
44
|
+
return /^XAI_API_KEY=(.*)$/m.exec(env)?.[1]?.trim().replace(/^['"]|['"]$/g, '') ?? '';
|
|
45
|
+
}
|
|
46
|
+
const grokHistory = new Map<string, Array<{ role: string; content: string }>>();
|
|
47
|
+
async function runGrok(q: Q): Promise<Row> {
|
|
48
|
+
if (q.needsTools) return { id: q.id, axis: q.axis, target: 'grok', ms: 0, ok: false, reply: 'n/a — 작업 폴더·도구 없음(대조 제외)' };
|
|
49
|
+
const history = q.session ? (grokHistory.get(q.session) ?? []) : [];
|
|
50
|
+
const messages = [...history, { role: 'user', content: q.text }];
|
|
51
|
+
const t0 = Date.now();
|
|
52
|
+
const res = await fetch('https://api.x.ai/v1/chat/completions', {
|
|
53
|
+
method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${xaiKey()}` },
|
|
54
|
+
body: JSON.stringify({ model: tierModel('budget', 'grok'), messages }),
|
|
55
|
+
});
|
|
56
|
+
const ms = Date.now() - t0;
|
|
57
|
+
const body = await res.json().catch(() => ({})) as { choices?: Array<{ message?: { content?: string } }> };
|
|
58
|
+
const reply = body.choices?.[0]?.message?.content ?? `(http ${res.status})`;
|
|
59
|
+
if (q.session) grokHistory.set(q.session, [...messages, { role: 'assistant', content: reply }]);
|
|
60
|
+
return { id: q.id, axis: q.axis, target: 'grok', ms, ok: res.ok, reply };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const mode of ['chat', 'agent'] as const) {
|
|
64
|
+
const sessions = new Map<string, string>();
|
|
65
|
+
for (const q of QUESTIONS) {
|
|
66
|
+
const row = runElanous(mode, q, q.session ? sessions.get(q.session) : undefined);
|
|
67
|
+
if (q.session && !sessions.has(q.session) && typeof row.meta?.sessionId === 'string') sessions.set(q.session, row.meta.sessionId as string);
|
|
68
|
+
rows.push(row);
|
|
69
|
+
console.log(`${mode} ${q.id} ${row.ok ? 'ok' : 'FAIL'} ${(row.ms / 1000).toFixed(1)}s`);
|
|
70
|
+
writeFileSync(join(outDir, 'rows.json'), JSON.stringify(rows, null, 2));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
for (const q of QUESTIONS) {
|
|
74
|
+
const row = await runGrok(q);
|
|
75
|
+
rows.push(row);
|
|
76
|
+
console.log(`grok ${q.id} ${row.ok ? 'ok' : 'skip/FAIL'} ${(row.ms / 1000).toFixed(1)}s`);
|
|
77
|
+
}
|
|
78
|
+
writeFileSync(join(outDir, 'rows.json'), JSON.stringify(rows, null, 2));
|
|
79
|
+
console.log('done', rows.length);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// 대조군 v2 — xAI Responses API · 추론 모델(저장소 GROK_SEARCH_MODEL) · web_search ⊕ x_search 켬. 그록봇 «제품» 은 아니다.
|
|
2
|
+
// 사용: bun eval-grok-live.ts <repo-checkout> <outDir> (키 값은 출력하지 않는다)
|
|
3
|
+
import { mkdirSync, writeFileSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
const [,, repo, outDir] = process.argv;
|
|
7
|
+
mkdirSync(outDir, { recursive: true });
|
|
8
|
+
const { grokAgentSearch, GROK_SEARCH_MODEL } = await import(join(repo, 'src/grok/agent-search.js'));
|
|
9
|
+
const env = readFileSync(join(process.env.HOME!, '.claude/skills/omni-crawl/.env'), 'utf8');
|
|
10
|
+
const apiKey = /^XAI_API_KEY=(.*)$/m.exec(env)?.[1]?.trim().replace(/^['"]|['"]$/g, '') ?? '';
|
|
11
|
+
|
|
12
|
+
type Q = { id: string; text: string; session?: string };
|
|
13
|
+
const QS: Q[] = [
|
|
14
|
+
{ id: 'q01', text: '안녕, 오늘 뭐 도와줄 수 있어? 세 줄 안으로.' },
|
|
15
|
+
{ id: 'q02', text: '요즘 좀 지쳤어. 짧게 한마디 해줘.' },
|
|
16
|
+
{ id: 'q03', text: '서울에서 부산까지 KTX 로 대략 몇 시간 걸려? 한 줄로.' },
|
|
17
|
+
{ id: 'q04', text: 'HWPX 파일 형식이 뭐야? 두 줄로.' },
|
|
18
|
+
{ id: 'q05', session: 'ctx', text: '내 프로젝트 이름은 elanous 야. 기억해 둬. «알겠다» 한 마디만.' },
|
|
19
|
+
{ id: 'q06', session: 'ctx', text: '우리 팀은 네 세션으로 나뉘어 있어: OP, MK, TC, UX. 이것도 기억해. 한 마디만.' },
|
|
20
|
+
{ id: 'q07', session: 'ctx', text: '내 프로젝트 이름이랑 팀 세션 약자 넷을 한 줄로 말해 봐.' },
|
|
21
|
+
{ id: 'q10', text: '오늘 날짜를 한국 시간으로 알려줘.' },
|
|
22
|
+
{ id: 'q12', text: '내가 어제 너한테 부탁한 일 결과 알려줘.' },
|
|
23
|
+
];
|
|
24
|
+
const history: string[] = [];
|
|
25
|
+
const rows: unknown[] = [];
|
|
26
|
+
for (const q of QS) {
|
|
27
|
+
const systemPrompt = q.session && history.length ? `이전 대화:\n${history.join('\n')}` : undefined;
|
|
28
|
+
const t0 = Date.now();
|
|
29
|
+
const r = await grokAgentSearch(q.text, { apiKey, tools: ['web_search', 'x_search'], ...(systemPrompt ? { systemPrompt } : {}), timeoutMs: 90_000 });
|
|
30
|
+
const ms = Date.now() - t0;
|
|
31
|
+
if (q.session) history.push(`사용자: ${q.text}`, `어시스턴트: ${r.text}`);
|
|
32
|
+
rows.push({ id: q.id, target: 'grok-live', model: GROK_SEARCH_MODEL, ms, ok: r.ok, reply: r.text, citations: (r.citations ?? []).length, error: r.error });
|
|
33
|
+
console.log(`grok-live ${q.id} ${r.ok ? 'ok' : 'FAIL'} ${(ms / 1000).toFixed(1)}s cites=${(r.citations ?? []).length}`);
|
|
34
|
+
}
|
|
35
|
+
writeFileSync(join(outDir, 'grok-live.rows.json'), JSON.stringify(rows, null, 2));
|
|
36
|
+
console.log('done', rows.length);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "손으로 쓴 개인정보 없는 앞 분류기 예시 — 규칙 미판정 문장",
|
|
3
|
+
"items": [
|
|
4
|
+
{ "id": "absorb-1", "text": "이 기술 발표에서 핵심 논점이 뭔지 알고 싶어.", "expected": "absorb" },
|
|
5
|
+
{ "id": "absorb-2", "text": "긴 글의 주요 주장만 파악하고 싶어.", "expected": "absorb" },
|
|
6
|
+
{ "id": "absorb-3", "text": "이 자료는 어떤 배경에서 나왔는지 살펴봐.", "expected": "absorb" },
|
|
7
|
+
{ "id": "tasks-1", "text": "문서 제목 확인하고 표 오타 검토한 다음 회의실 예약도 챙겨야 해.", "expected": "tasks" },
|
|
8
|
+
{ "id": "tasks-2", "text": "청구서 확인, 일정 변경, 메일 답장 각각 챙기자.", "expected": "tasks" },
|
|
9
|
+
{ "id": "tasks-3", "text": "오늘은 재고 파악하고 전등 교체 일정도 따로 잡아야겠어.", "expected": "tasks" },
|
|
10
|
+
{ "id": "graph-1", "text": "이 입력부터 결과까지 이어지는 처리 과정을 자동화하고 싶어.", "expected": "graph" },
|
|
11
|
+
{ "id": "graph-2", "text": "데이터가 들어오면 검사 후 분류해서 다음 단계로 넘기는 흐름이 필요해.", "expected": "graph" },
|
|
12
|
+
{ "id": "graph-3", "text": "요청 접수부터 승인과 배포까지 한 흐름으로 묶자.", "expected": "graph" },
|
|
13
|
+
{ "id": "ask-human-1", "text": "이거 어떻게 할까?", "expected": "ask-human" },
|
|
14
|
+
{ "id": "ask-human-2", "text": "지금은 어느 방향이 좋을지 모르겠어.", "expected": "ask-human" },
|
|
15
|
+
{ "id": "ask-human-3", "text": "그건 상황을 보고 결정하자.", "expected": "ask-human" }
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -71,11 +71,21 @@ else R "k3d registry create $REGISTRY --port 127.0.0.1:$REG_PORT >/tmp/fleet-reg
|
|
|
71
71
|
# `bridge` 에만 붙어 kubelet 이 `lookup k3d-elanous-registry: no such host` → Pod `ImagePullBackOff`(🅕 첫 Pod 사용에서 발견). 멱등.
|
|
72
72
|
[ $CHECK = 1 ] || R "docker network inspect k3d-$CLUSTER >/dev/null 2>&1 && (docker network connect k3d-$CLUSTER k3d-$REGISTRY 2>/dev/null || true)" >/dev/null 2>&1
|
|
73
73
|
fi
|
|
74
|
-
#
|
|
74
|
+
# 🪞 호스트 git 미러(대표 09-29) — Pod 는 GitHub 대신 이 미러에서 clone 한다(`ELANOUS_POD_HOST_MIRROR` · #21929).
|
|
75
|
+
# bare 미러 = 원격 ~/mirror/elanous-agent.git(없으면 만든다 · 갱신은 이 맥의 크론이 push). 클러스터 노드에 읽기 전용으로 붙인다.
|
|
76
|
+
MIRROR_DIR='$HOME/mirror'; MIRROR_MOUNT=/mirror-host
|
|
77
|
+
if R "test -d ~/mirror/elanous-agent.git"; then ok "호스트 git 미러 ~/mirror/elanous-agent.git"
|
|
78
|
+
elif [ $CHECK = 1 ]; then todo "호스트 git 미러 ~/mirror/elanous-agent.git 생성(git init --bare) ⊕ 이 맥에서 첫 push"
|
|
79
|
+
else R "mkdir -p ~/mirror && git init -q --bare ~/mirror/elanous-agent.git" && ok "호스트 git 미러 (생성 · 첫 push 는 이 맥 크론)" || { bad "미러 생성 실패"; exit 1; }; fi
|
|
80
|
+
# 클러스터가 그 레지스트리를 쓰나 ⊕ 미러가 붙어 있나 — 둘 중 하나라도 아니면(옛 클러스터) 돌고 있는 Job 이 없을 때만 다시 만든다.
|
|
81
|
+
# ⛔ k3d 는 볼륨을 «클러스터 생성 때만» 붙인다 — 붙이려면 다시 만들 수밖에 없다.
|
|
75
82
|
REG_WIRED=0; R "docker exec k3d-$CLUSTER-server-0 cat /etc/rancher/k3s/registries.yaml 2>/dev/null | grep -q k3d-$REGISTRY" && REG_WIRED=1
|
|
83
|
+
MIRROR_WIRED=0; R "docker exec k3d-$CLUSTER-server-0 test -d $MIRROR_MOUNT/elanous-agent.git" && MIRROR_WIRED=1
|
|
84
|
+
[ $MIRROR_WIRED = 1 ] || { [ $REG_WIRED = 1 ] && REG_WIRED=0 && MIRROR_ONLY=1; }
|
|
76
85
|
if R "k3d cluster list $CLUSTER" >/dev/null 2>&1 && [ $REG_WIRED = 0 ]; then
|
|
77
|
-
|
|
78
|
-
|
|
86
|
+
# «실행 중»만 센다 — 실패로 끝난 Job 은 Complete 가 아니어도 도는 것이 아니다(09-29: `grep -vc Complete` 는 Failed 까지 세서 영원히 0 이 안 됐다).
|
|
87
|
+
RUNNING="$(R "KUBECONFIG=\$(k3d kubeconfig write $CLUSTER) kubectl get jobs -A -o jsonpath='{range .items[*]}{.status.active}{\"\\n\"}{end}' 2>/dev/null | awk '\$1>0' | wc -l | tr -d ' '" 2>/dev/null | tail -1)"; RUNNING="${RUNNING:-0}"
|
|
88
|
+
if [ $CHECK = 1 ]; then todo "클러스터 $CLUSTER 가 $([ "${MIRROR_ONLY:-0}" = 1 ] && echo "미러($MIRROR_MOUNT)를 안 붙였다" || echo "레지스트리를 안 쓴다") — 다시 만든다(도는 Job ${RUNNING:-?}개)"
|
|
79
89
|
elif [ "${RUNNING:-0}" != "0" ]; then bad "클러스터 $CLUSTER 를 다시 만들어야 하는데 도는 Job 이 ${RUNNING}개 — 끝난 뒤 다시"; exit 1
|
|
80
90
|
else R "k3d cluster delete $CLUSTER >/dev/null 2>&1" && ok "클러스터 $CLUSTER (레지스트리 연결 위해 지움)"; fi
|
|
81
91
|
fi
|
|
@@ -83,7 +93,7 @@ if R "k3d cluster list $CLUSTER" >/dev/null 2>&1; then ok "클러스터 $CLUSTER
|
|
|
83
93
|
elif [ $CHECK = 1 ]; then todo "클러스터 $CLUSTER 생성($K3S_IMAGE · API :$API_PORT · SAN $HOST ${TSNAME:-} · 레지스트리 k3d-$REGISTRY:$REG_PORT)"
|
|
84
94
|
else
|
|
85
95
|
SAN="--k3s-arg --tls-san=$HOST@server:0"; [ -n "$TSNAME" ] && SAN="$SAN --k3s-arg --tls-san=$TSNAME@server:0"
|
|
86
|
-
R "k3d cluster create $CLUSTER --image $K3S_IMAGE --no-lb --api-port 0.0.0.0:$API_PORT $SAN --registry-use k3d-$REGISTRY:$REG_PORT --wait --timeout 300s >/tmp/fleet-k3d-create.log 2>&1" && ok "클러스터 $CLUSTER (생성 · 레지스트리 k3d-$REGISTRY)" || { bad "클러스터 생성 실패 — 원격 /tmp/fleet-k3d-create.log"; exit 1; }
|
|
96
|
+
R "k3d cluster create $CLUSTER --image $K3S_IMAGE --no-lb --api-port 0.0.0.0:$API_PORT $SAN --registry-use k3d-$REGISTRY:$REG_PORT --volume $MIRROR_DIR:$MIRROR_MOUNT:ro@server:0 --wait --timeout 300s >/tmp/fleet-k3d-create.log 2>&1" && ok "클러스터 $CLUSTER (생성 · 레지스트리 k3d-$REGISTRY)" || { bad "클러스터 생성 실패 — 원격 /tmp/fleet-k3d-create.log"; exit 1; }
|
|
87
97
|
fi
|
|
88
98
|
# 7. 이 맥 kubeconfig 에 컨텍스트 pool-<호스트>
|
|
89
99
|
# ⛔ «있다»로 끝내지 않는다 — 클러스터를 다시 만들면 인증서가 바뀌어 옛 컨텍스트가 x509 로 죽는다(2026-09-26 node-b:
|
|
@@ -114,9 +124,38 @@ PY
|
|
|
114
124
|
KUBECONFIG=~/.kube/config:"$TMP/kc.yaml" kubectl config view --flatten > "$TMP/merged.yaml" && chmod 600 "$TMP/merged.yaml" && mv "$TMP/merged.yaml" ~/.kube/config && ok "kubeconfig 컨텍스트 $CTX (추가 · 현재 컨텍스트는 그대로)" || bad "kubeconfig 병합 실패"
|
|
115
125
|
fi
|
|
116
126
|
# 8. 네임스페이스 ⊕ 격리 정책
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
127
|
+
apply_base() {
|
|
128
|
+
if [ "$CHECK" = 1 ]; then
|
|
129
|
+
if ! kubectl --context "$CTX" --request-timeout=10s get ns elanous-test >/dev/null 2>&1; then
|
|
130
|
+
todo "base.yaml ⊕ policy-internet.yaml 적용"
|
|
131
|
+
elif kubectl --context "$CTX" diff -f "$ROOT/docker/h1/base.yaml" -f "$ROOT/docker/h1/policy-internet.yaml" >/dev/null 2>&1; then
|
|
132
|
+
ok "elanous-test 네임스페이스 ⊕ 정책"
|
|
133
|
+
else
|
|
134
|
+
local diff_status=$?
|
|
135
|
+
if [ "$diff_status" = 1 ]; then todo "base.yaml ⊕ policy-internet.yaml 적용"
|
|
136
|
+
else todo "base.yaml ⊕ policy-internet.yaml 적용 (못 쟀다)"; fi
|
|
137
|
+
fi
|
|
138
|
+
return
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
local attempts=0 output retries
|
|
142
|
+
while [ "$attempts" -lt 6 ]; do
|
|
143
|
+
attempts=$((attempts+1))
|
|
144
|
+
if output=$(kubectl --context "$CTX" apply -f "$ROOT/docker/h1/base.yaml" -f "$ROOT/docker/h1/policy-internet.yaml" 2>&1); then
|
|
145
|
+
retries=$((attempts-1))
|
|
146
|
+
if [ "$retries" -eq 0 ]; then ok "elanous-test 네임스페이스 ⊕ 정책 (적용)"
|
|
147
|
+
else ok "elanous-test 네임스페이스 ⊕ 정책 (적용 · 재시도 $retries)"; fi
|
|
148
|
+
return
|
|
149
|
+
fi
|
|
150
|
+
if [[ "$output" != *'serviceaccount "default" not found'* ]] || [ "$attempts" -eq 6 ]; then
|
|
151
|
+
[ -z "$output" ] || printf '%s\n' "$output" >&2
|
|
152
|
+
bad "적용 실패 (재시도 $((attempts-1)))"
|
|
153
|
+
return
|
|
154
|
+
fi
|
|
155
|
+
sleep "${FLEET_APPLY_RETRY_SLEEP:-5}"
|
|
156
|
+
done
|
|
157
|
+
}
|
|
158
|
+
apply_base
|
|
120
159
|
# 9. 판 대조 — k3s · 노드 준비
|
|
121
160
|
v="$(kubectl --context "$CTX" --request-timeout=10s get nodes -o jsonpath='{.items[0].status.nodeInfo.kubeletVersion}' 2>/dev/null)"
|
|
122
161
|
want="${K3S_IMAGE##*:}"; want="${want/-k3s/+k3s}"
|
|
@@ -1,4 +1,30 @@
|
|
|
1
|
+
import { parse } from '../../src/agent-substrate/pr-comment-meta.js';
|
|
2
|
+
|
|
1
3
|
export type MustFixVerdict = 'resolved' | 'unresolved' | 'unknown';
|
|
4
|
+
export interface ReviewRoundMatch {
|
|
5
|
+
text: string;
|
|
6
|
+
verdict: 'rereviewed' | 'open';
|
|
7
|
+
run: string;
|
|
8
|
+
round: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** A later reviewer round in the same run is evidence of rereview, not proof of repair. */
|
|
12
|
+
const MUST_FIX_ROUND = /^Round \d+: reviewer requested \d+ must-fix change\(s\)\.$/m;
|
|
13
|
+
|
|
14
|
+
export function classifyReviewRounds(comments: Array<{ body?: string }>): ReviewRoundMatch[] {
|
|
15
|
+
const reviews = comments.flatMap(({ body }) => {
|
|
16
|
+
if (!body) return [];
|
|
17
|
+
const meta = parse(body);
|
|
18
|
+
return meta?.role === 'reviewer' && meta.run && meta.round !== undefined
|
|
19
|
+
? [{ body, run: meta.run, round: meta.round }] : [];
|
|
20
|
+
});
|
|
21
|
+
// 항목은 «must-fix 요청» 라운드에서만 센다 — «review completed (warn)» 의 요약 본문에도 «- » 글머리가 들어 있다(#21983 실물).
|
|
22
|
+
return reviews.flatMap(({ body, run, round }) => !MUST_FIX_ROUND.test(body) ? [] :
|
|
23
|
+
[...body.matchAll(/^- (.+)$/gm)].map((item) => ({
|
|
24
|
+
text: item[1]!, run, round,
|
|
25
|
+
verdict: reviews.some((review) => review.run === run && review.round > round) ? 'rereviewed' as const : 'open' as const,
|
|
26
|
+
})));
|
|
27
|
+
}
|
|
2
28
|
|
|
3
29
|
/** Match only named evidence in a PR's final patch, never an earlier review round. */
|
|
4
30
|
export function matchMustFix(mustFix: string, finalDiff: string): MustFixVerdict {
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { githubAutomationToken } from '../../src/auth/github-app-token.js';
|
|
4
6
|
import { debug } from '../../src/debug/log.js';
|
|
5
7
|
import { emitDecision } from '../../src/live/detail-switch.js';
|
|
6
|
-
import {
|
|
8
|
+
import { parse } from '../../src/agent-substrate/pr-comment-meta.js';
|
|
9
|
+
import { classifyReviewRounds, matchMustFix, type MustFixVerdict } from './must-fix-match.js';
|
|
7
10
|
|
|
8
11
|
interface PullRequest {
|
|
9
12
|
number: number;
|
|
@@ -13,11 +16,45 @@ interface PullRequest {
|
|
|
13
16
|
headRefName: string;
|
|
14
17
|
files: Array<{ path: string }>;
|
|
15
18
|
}
|
|
16
|
-
interface Match { pr: number; text: string; verdict: MustFixVerdict }
|
|
17
|
-
interface GraphContext { runId?: string; input?: { windowMinutes?: number; mustFixByPr?: Record<string, string[]> }; outputs?: Record<string, unknown
|
|
19
|
+
interface Match { pr: number; text: string; verdict: MustFixVerdict | 'rereviewed' | 'open'; run?: string; round?: number; resolvedIn?: number[] }
|
|
20
|
+
interface GraphContext { runId?: string; input?: { windowMinutes?: number; mustFixByPr?: Record<string, string[]> }; outputs?: Record<string, unknown>; decision?: typeof emitDecision }
|
|
18
21
|
export type RunCommand = (args: string[]) => string;
|
|
22
|
+
// 상주 루프(30분마다)는 사람 계정 한도를 쓰지 않는다 — 부른 쪽이 GH_TOKEN 을 주지 않았으면 GitHub App 설치 토큰으로(09-29 한도 소진).
|
|
23
|
+
// 발급 함수마다 한 번(프로세스 전역 한 칸이면 앞선 호출의 진짜 토큰이 주입한 가짜를 이긴다 — 09-29 시험 실패 출력에 토큰이 찍혔다).
|
|
24
|
+
const minted = new WeakMap<() => string | null, string | null>();
|
|
25
|
+
export function ghEnv(env: NodeJS.ProcessEnv = process.env, mint: () => string | null = githubAutomationToken): NodeJS.ProcessEnv {
|
|
26
|
+
if (Object.hasOwn(env, 'GH_TOKEN')) return env;
|
|
27
|
+
if (!minted.has(mint)) minted.set(mint, mint());
|
|
28
|
+
const appToken = minted.get(mint);
|
|
29
|
+
debug.log('landing-heal', 'gh-token', { source: appToken ? 'github-app' : 'caller' });
|
|
30
|
+
return appToken ? { ...env, GH_TOKEN: appToken } : env;
|
|
31
|
+
}
|
|
32
|
+
// 상주 루프는 설치본 폴더(git 저장소 아님)에서 돈다 — `gh` 가 저장소를 추론 못 해 첫 cron 바퀴가 collect 에서 죽었다(09-29 21:00).
|
|
33
|
+
// 부른 쪽이 GH_REPO 를 주지 않았고 cwd 가 git 저장소가 아니면, App 설치의 저장소가 «정확히 하나»일 때 그것을 쓴다(모르면 멈춘다).
|
|
34
|
+
export async function ghRepoEnv(env: NodeJS.ProcessEnv, cwd: string, list: (token: string) => Promise<string[]> = installationRepos): Promise<NodeJS.ProcessEnv> {
|
|
35
|
+
if (Object.hasOwn(env, 'GH_REPO') || insideGitCheckout(cwd)) return env;
|
|
36
|
+
const token = env.GH_TOKEN;
|
|
37
|
+
if (!token) throw new Error('GitHub repository unknown outside a git checkout — set GH_REPO');
|
|
38
|
+
const repos = await list(token);
|
|
39
|
+
if (repos.length !== 1) throw new Error(`GitHub repository unknown outside a git checkout — App installation has ${repos.length} repositories; set GH_REPO`);
|
|
40
|
+
debug.log('landing-heal', 'gh-repo', { source: 'github-app-installation', repo: repos[0] });
|
|
41
|
+
return { ...env, GH_REPO: repos[0] };
|
|
42
|
+
}
|
|
43
|
+
function insideGitCheckout(cwd: string): boolean {
|
|
44
|
+
for (let dir = resolve(cwd); ; dir = dirname(dir)) {
|
|
45
|
+
if (existsSync(join(dir, '.git'))) return true;
|
|
46
|
+
if (dirname(dir) === dir) return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function installationRepos(token: string): Promise<string[]> {
|
|
50
|
+
const response = await fetch('https://api.github.com/installation/repositories?per_page=2', { headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github+json' } });
|
|
51
|
+
if (!response.ok) throw new Error(`GitHub App installation repositories: HTTP ${response.status}`);
|
|
52
|
+
const body = await response.json() as { repositories?: Array<{ full_name?: string }> };
|
|
53
|
+
return (body.repositories ?? []).flatMap((repo) => repo.full_name ? [repo.full_name] : []);
|
|
54
|
+
}
|
|
55
|
+
let resolvedEnv: NodeJS.ProcessEnv | undefined;
|
|
19
56
|
const gh: RunCommand = (args) => {
|
|
20
|
-
const result = spawnSync('gh', args, { encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 });
|
|
57
|
+
const result = spawnSync('gh', args, { encoding: 'utf8', maxBuffer: 32 * 1024 * 1024, env: resolvedEnv ?? ghEnv() });
|
|
21
58
|
if (result.error || result.status !== 0) throw new Error(`gh ${args.slice(0, 2).join(' ')} failed: ${result.stderr || String(result.error)}`);
|
|
22
59
|
return result.stdout;
|
|
23
60
|
};
|
|
@@ -44,6 +81,18 @@ function pullRequests(ctx: GraphContext): PullRequest[] {
|
|
|
44
81
|
return value as PullRequest[];
|
|
45
82
|
}
|
|
46
83
|
|
|
84
|
+
/** 착지 뒤 검증 표식 — 코멘트 «첫 줄»이 `landing-verified:` 로 시작하고 뒤에 무엇이든 한 글자는 있어야 한다(빈 표식은 표식이 아니다). */
|
|
85
|
+
export function isLandingVerified(body: string | undefined): boolean {
|
|
86
|
+
return /^landing-verified:\s*\S/.test((body ?? '').trimStart().split('\n')[0] ?? '');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 코멘트 «첫 줄»이 `must-fix resolved in #N[, #M]` 이면 그 PR 번호들. */
|
|
90
|
+
export function resolvedInPrs(body: string | undefined): number[] {
|
|
91
|
+
const first = (body ?? '').trimStart().split('\n')[0] ?? '';
|
|
92
|
+
const match = /^must[-\s]?fix resolved in (#\d+(?:\s*,\s*#\d+)*)/i.exec(first);
|
|
93
|
+
return match ? [...match[1]!.matchAll(/#(\d+)/g)].map((m) => Number(m[1])) : [];
|
|
94
|
+
}
|
|
95
|
+
|
|
47
96
|
export function runMission(mission: string, ctx: GraphContext, run: RunCommand = gh, now = new Date()): Record<string, unknown> {
|
|
48
97
|
switch (mission) {
|
|
49
98
|
case 'collect': {
|
|
@@ -66,34 +115,79 @@ export function runMission(mission: string, ctx: GraphContext, run: RunCommand =
|
|
|
66
115
|
const pages = JSON.parse(run(['api', `repos/{owner}/{repo}/pulls/${pr.number}/comments`, '--paginate', '--slurp'])) as Array<Array<{ body?: string }>>;
|
|
67
116
|
if (!Array.isArray(pages) || !pages.every(Array.isArray)) throw new Error(`invalid review comments for PR #${pr.number}`);
|
|
68
117
|
const inline = pages.flat();
|
|
118
|
+
const comments = [...(details.reviews ?? []), ...(details.comments ?? []), ...inline];
|
|
119
|
+
const reviewRounds = comments.filter(({ body }) => {
|
|
120
|
+
const meta = body ? parse(body) : null;
|
|
121
|
+
return meta?.role === 'reviewer' && !!meta.run && meta.round !== undefined;
|
|
122
|
+
});
|
|
123
|
+
// 뒤 PR 로 고친 must-fix 는 원래 PR 에 첫 줄 `must-fix resolved in #N` 코멘트를 남긴다(🅞·🅣 09-29 합의) — 그 PR 의 «열린» 항목을 풀고, 그 코멘트 자체는 must-fix 로 세지 않는다.
|
|
124
|
+
const resolvedNotes = comments.filter(({ body }) => resolvedInPrs(body).length > 0);
|
|
125
|
+
const resolvedIn = [...new Set(resolvedNotes.flatMap(({ body }) => resolvedInPrs(body)))].sort((a, b) => a - b);
|
|
126
|
+
const classified = classifyReviewRounds(reviewRounds).map((item) => item.verdict === 'open' && resolvedIn.length
|
|
127
|
+
? { ...item, verdict: 'resolved' as const, resolvedIn } : item);
|
|
69
128
|
const mustFixes = [...(ctx.input?.mustFixByPr?.[String(pr.number)] ?? []),
|
|
70
|
-
...
|
|
71
|
-
.flatMap((
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
129
|
+
...comments.filter((comment) => !reviewRounds.includes(comment) && !resolvedNotes.includes(comment))
|
|
130
|
+
.flatMap(({ body }) => body && /must[-\s]?fix/i.test(body) ? [body] : [])];
|
|
131
|
+
const diff = mustFixes.length ? run(['pr', 'diff', String(pr.number), '--patch']) : '';
|
|
132
|
+
const prMatches: Match[] = [
|
|
133
|
+
...classified.map((item) => ({ pr: pr.number, ...item })),
|
|
134
|
+
...mustFixes.map((text) => ({ pr: pr.number, text, verdict: matchMustFix(text, diff) })),
|
|
135
|
+
];
|
|
136
|
+
matches.push(...prMatches);
|
|
137
|
+
debug.log('landing-heal', 'must-fix-classified', {
|
|
138
|
+
pr: pr.number,
|
|
139
|
+
rereviewed: prMatches.filter((item) => item.verdict === 'rereviewed').length,
|
|
140
|
+
open: prMatches.filter((item) => item.verdict === 'open').length,
|
|
141
|
+
unknown: prMatches.filter((item) => item.verdict === 'unknown').length,
|
|
142
|
+
});
|
|
75
143
|
}
|
|
76
144
|
return { outcome: 'ok', matches };
|
|
77
145
|
}
|
|
78
|
-
case 'verify-needed':
|
|
79
|
-
|
|
80
|
-
.filter((pr) => pr.state === 'MERGED' && pr.files.some((file) => file.path.startsWith('apps/pwa/') || file.path.startsWith('src/')))
|
|
81
|
-
|
|
146
|
+
case 'verify-needed': {
|
|
147
|
+
const candidates = pullRequests(ctx)
|
|
148
|
+
.filter((pr) => pr.state === 'MERGED' && pr.files.some((file) => file.path.startsWith('apps/pwa/') || file.path.startsWith('src/')));
|
|
149
|
+
// 착지 뒤 검증을 끝낸 쪽은 PR 코멘트 첫 줄 `landing-verified: <sha> · <무엇을 쟀나>` 를 남긴다(🅞·🅣 09-29 합의) — 그 PR 은 뺀다.
|
|
150
|
+
const verified: number[] = [];
|
|
151
|
+
const verifyNeeded = candidates.filter((pr) => {
|
|
152
|
+
const { comments } = JSON.parse(run(['pr', 'view', String(pr.number), '--json', 'comments'])) as { comments?: Array<{ body?: string }> };
|
|
153
|
+
const done = (comments ?? []).some((comment) => isLandingVerified(comment.body));
|
|
154
|
+
if (done) verified.push(pr.number);
|
|
155
|
+
return !done;
|
|
156
|
+
}).map((pr) => pr.number);
|
|
157
|
+
return { outcome: 'ok', verifyNeeded, verified };
|
|
158
|
+
}
|
|
82
159
|
case 'report': {
|
|
83
|
-
const
|
|
160
|
+
const collected = pullRequests(ctx);
|
|
161
|
+
const prs = collected.length;
|
|
84
162
|
const matches = output(ctx, 'must-fix-match').matches as Match[];
|
|
85
|
-
const
|
|
163
|
+
const mustFixOpen = matches.filter((item) => item.verdict === 'open' || item.verdict === 'unresolved').length;
|
|
164
|
+
const mustFixUnknown = matches.filter((item) => item.verdict === 'unknown').length;
|
|
165
|
+
const mustFixRereviewed = matches.filter((item) => item.verdict === 'rereviewed').length;
|
|
166
|
+
const mustFixUnresolved = mustFixOpen + mustFixUnknown;
|
|
86
167
|
const verifyNeeded = output(ctx, 'verify-needed').verifyNeeded as number[];
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
168
|
+
const mergedPrs = new Set(collected.filter((pr) => pr.state === 'MERGED').map((pr) => pr.number));
|
|
169
|
+
const landedOpen = matches.filter((item) => item.verdict === 'open' && mergedPrs.has(item.pr));
|
|
170
|
+
const unresolved = matches.filter((item) => item.verdict === 'unresolved').length;
|
|
171
|
+
const openPrs = [...new Set(landedOpen.map((item) => item.pr))].sort((a, b) => a - b);
|
|
172
|
+
const open = landedOpen.length;
|
|
173
|
+
debug.log('landing-heal', 'tick', { prs, mustFixUnresolved, mustFixOpen, mustFixUnknown, mustFixRereviewed, verifyNeeded });
|
|
174
|
+
(ctx.decision ?? emitDecision)({
|
|
175
|
+
kind: 'VERIFY',
|
|
176
|
+
what: `착지·치유 관측: PR ${prs}건${open ? ` — 열린 must-fix ${open}건(PR ${openPrs.map((pr) => `#${pr}`).join(', ')})` : ''}`,
|
|
177
|
+
reason: `열린 채 착지 ${open}건 · 미해결 ${unresolved}건 · 미상 ${mustFixUnknown}건 · 다시 봄 ${mustFixRereviewed}건 · 착지 뒤 검증 필요 ${verifyNeeded.length}건`,
|
|
178
|
+
purpose: '착지 후속 작업 관측', target: 'landing-heal', ...(ctx.runId ? { runId: ctx.runId } : {}),
|
|
179
|
+
});
|
|
180
|
+
return { outcome: 'ok', prs, mustFixUnresolved, mustFixOpen, mustFixUnknown, mustFixRereviewed, verifyNeeded };
|
|
90
181
|
}
|
|
91
182
|
default: throw new Error(`unknown landing-heal mission: ${mission}`);
|
|
92
183
|
}
|
|
93
184
|
}
|
|
94
185
|
|
|
95
186
|
if (import.meta.main) {
|
|
96
|
-
try {
|
|
187
|
+
try {
|
|
188
|
+
resolvedEnv = await ghRepoEnv(ghEnv(), process.cwd());
|
|
189
|
+
process.stdout.write(`${JSON.stringify(runMission(process.argv[2] ?? '', context()))}\n`);
|
|
190
|
+
}
|
|
97
191
|
catch (error) {
|
|
98
192
|
process.stderr.write(`${String(error)}\n`);
|
|
99
193
|
process.exitCode = 1;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// 계측 시험(`connectedDebugAndroidTest`)을 «어느 기기»에서 돌리나.
|
|
2
|
+
// ⛔ Gradle 은 시험이 끝나면 앱을 «지운다»(데이터까지). 실기기에서 돌면 대표 폰의 앱이 사라진다
|
|
3
|
+
// (2026-09-30 실측: pr land 의 android-gate 가 USB 로 꽂힌 폴드 8 에서 돌아 설치본이 지워졌다).
|
|
4
|
+
// ⇒ 에뮬레이터(`emulator-*`)만 고른다. 실기기를 쓰려면 serial 을 «명시»해야 한다.
|
|
5
|
+
|
|
6
|
+
export type TestDevicePick =
|
|
7
|
+
| { kind: 'use'; serial: string }
|
|
8
|
+
| { kind: 'skip'; reason: string };
|
|
9
|
+
|
|
10
|
+
/** `adb devices` 출력에서 «device» 상태인 serial 들. */
|
|
11
|
+
export function attachedSerials(adbDevicesOutput: string): string[] {
|
|
12
|
+
return adbDevicesOutput
|
|
13
|
+
.split('\n')
|
|
14
|
+
.slice(1)
|
|
15
|
+
.map((line) => line.trim().split(/\s+/))
|
|
16
|
+
.filter((cols) => cols.length >= 2 && cols[1] === 'device')
|
|
17
|
+
.map((cols) => cols[0]!);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function pickInstrumentedTestDevice(adbDevicesOutput: string, explicitSerial?: string): TestDevicePick {
|
|
21
|
+
const serials = attachedSerials(adbDevicesOutput);
|
|
22
|
+
if (explicitSerial) {
|
|
23
|
+
return serials.includes(explicitSerial)
|
|
24
|
+
? { kind: 'use', serial: explicitSerial }
|
|
25
|
+
: { kind: 'skip', reason: `지정한 기기 ${explicitSerial} 가 연결돼 있지 않다` };
|
|
26
|
+
}
|
|
27
|
+
const emulator = serials.find((s) => s.startsWith('emulator-'));
|
|
28
|
+
if (emulator) return { kind: 'use', serial: emulator };
|
|
29
|
+
return serials.length > 0
|
|
30
|
+
? { kind: 'skip', reason: `에뮬레이터가 없다 — 실기기 ${serials.length}대에서는 돌리지 않는다(시험이 앱과 데이터를 지운다) · 실기기를 쓰려면 ELANOUS_ANDROID_TEST_SERIAL=<serial>` }
|
|
31
|
+
: { kind: 'skip', reason: '연결된 기기·에뮬레이터가 없다' };
|
|
32
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { classifyIntakeFrontRoute } from '../../src/intake-plane/front-route-classifier.js';
|
|
2
|
+
import { INTAKE_FRONT_ROUTE_TRACKS, type IntakeFrontRouteDecision, type IntakeFrontRouteTrack } from '../../src/intake-plane/front-route-rules.js';
|
|
3
|
+
import type { ResolveRoleProviderFn } from '../../src/intake-plane/runtime-callables.js';
|
|
4
|
+
import { wilsonInterval, type WilsonInterval } from './nl-routing-measurement.js';
|
|
5
|
+
|
|
6
|
+
export interface FrontRouteItem { id: string; text: string; expected: IntakeFrontRouteTrack }
|
|
7
|
+
export interface FrontRouteCorpus { description: string; items: FrontRouteItem[] }
|
|
8
|
+
export type FrontRouteConfusion = Record<IntakeFrontRouteTrack, Record<IntakeFrontRouteTrack, number>>;
|
|
9
|
+
export interface FrontRouteProviderResult {
|
|
10
|
+
label: string;
|
|
11
|
+
runs: number;
|
|
12
|
+
agree: number;
|
|
13
|
+
agreement: number;
|
|
14
|
+
wilson: WilsonInterval | null;
|
|
15
|
+
askHuman: number;
|
|
16
|
+
confusion: FrontRouteConfusion;
|
|
17
|
+
meanLatencyMs: number;
|
|
18
|
+
perItem: { id: string; expected: IntakeFrontRouteTrack; got: IntakeFrontRouteTrack[] }[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
22
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isTrack(value: unknown): value is IntakeFrontRouteTrack {
|
|
26
|
+
return typeof value === 'string' && (INTAKE_FRONT_ROUTE_TRACKS as readonly string[]).includes(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function loadFrontRouteCorpus(json: unknown): FrontRouteCorpus {
|
|
30
|
+
const parsed: unknown = typeof json === 'string' ? JSON.parse(json) : json;
|
|
31
|
+
if (!isRecord(parsed) || typeof parsed.description !== 'string' || !Array.isArray(parsed.items) || parsed.items.length === 0) {
|
|
32
|
+
throw new Error('Front-route corpus requires a description and nonempty items array');
|
|
33
|
+
}
|
|
34
|
+
const ids = new Set<string>();
|
|
35
|
+
const items: FrontRouteItem[] = parsed.items.map((raw: unknown, index: number) => {
|
|
36
|
+
if (!isRecord(raw) || typeof raw.id !== 'string' || !raw.id.trim() ||
|
|
37
|
+
typeof raw.text !== 'string' || !raw.text.trim() || !isTrack(raw.expected)) {
|
|
38
|
+
throw new Error(`Invalid front-route corpus item at index ${index} (id, text, expected track required)`);
|
|
39
|
+
}
|
|
40
|
+
if (ids.has(raw.id)) throw new Error(`Duplicate front-route corpus id at index ${index}`);
|
|
41
|
+
ids.add(raw.id);
|
|
42
|
+
return { id: raw.id, text: raw.text, expected: raw.expected };
|
|
43
|
+
});
|
|
44
|
+
return { description: parsed.description, items };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function emptyConfusion(): FrontRouteConfusion {
|
|
48
|
+
return Object.fromEntries(INTAKE_FRONT_ROUTE_TRACKS.map((expected) => [expected,
|
|
49
|
+
Object.fromEntries(INTAKE_FRONT_ROUTE_TRACKS.map((got) => [got, 0])),
|
|
50
|
+
])) as FrontRouteConfusion;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function measureFrontRoute(
|
|
54
|
+
items: readonly FrontRouteItem[],
|
|
55
|
+
{ providers, runs, classify = classifyIntakeFrontRoute, now = () => performance.now() }: {
|
|
56
|
+
providers: readonly { label: string; resolve: ResolveRoleProviderFn }[];
|
|
57
|
+
runs: number;
|
|
58
|
+
classify?: typeof classifyIntakeFrontRoute;
|
|
59
|
+
now?: () => number;
|
|
60
|
+
},
|
|
61
|
+
): Promise<FrontRouteProviderResult[]> {
|
|
62
|
+
if (!Number.isSafeInteger(runs) || runs < 1) throw new Error('runs must be a positive integer');
|
|
63
|
+
if (!items.length || !providers.length) throw new Error('items and providers must not be empty');
|
|
64
|
+
if (new Set(providers.map(({ label }) => label)).size !== providers.length || providers.some(({ label }) => !label.trim())) {
|
|
65
|
+
throw new Error('provider labels must be unique and nonempty');
|
|
66
|
+
}
|
|
67
|
+
// An id can contain another item's text, too: sanitize against the whole corpus.
|
|
68
|
+
const texts = items.map(({ text }) => text);
|
|
69
|
+
const exposesText = (id: string) => texts.some((text) => id.includes(text));
|
|
70
|
+
const safeIds = new Set(items.filter(({ id }) => !exposesText(id)).map(({ id }) => id));
|
|
71
|
+
const reportIds = items.map(({ id }, index) => {
|
|
72
|
+
if (!exposesText(id)) return id;
|
|
73
|
+
let candidate = `item-${index + 1}`;
|
|
74
|
+
if (exposesText(candidate) || safeIds.has(candidate)) {
|
|
75
|
+
let codePoint = 0xe000;
|
|
76
|
+
while (texts.some((text) => text.includes(String.fromCodePoint(codePoint)))) codePoint++;
|
|
77
|
+
const unused = String.fromCodePoint(codePoint);
|
|
78
|
+
candidate = unused;
|
|
79
|
+
while (safeIds.has(candidate)) candidate += unused;
|
|
80
|
+
}
|
|
81
|
+
safeIds.add(candidate);
|
|
82
|
+
return candidate;
|
|
83
|
+
});
|
|
84
|
+
const results: FrontRouteProviderResult[] = [];
|
|
85
|
+
for (const { label, resolve } of providers) {
|
|
86
|
+
const confusion = emptyConfusion();
|
|
87
|
+
const perItem: FrontRouteProviderResult['perItem'] = [];
|
|
88
|
+
let agree = 0;
|
|
89
|
+
let askHuman = 0;
|
|
90
|
+
let latencyMs = 0;
|
|
91
|
+
for (const [index, item] of items.entries()) {
|
|
92
|
+
const got: IntakeFrontRouteTrack[] = [];
|
|
93
|
+
for (let repeat = 0; repeat < runs; repeat++) {
|
|
94
|
+
const started = now();
|
|
95
|
+
let decision: IntakeFrontRouteDecision;
|
|
96
|
+
try {
|
|
97
|
+
decision = await classify(item.text, { resolveRoleProvider: resolve });
|
|
98
|
+
} catch {
|
|
99
|
+
decision = { track: 'ask-human', confidence: 0, reason: 'classifier-failed:llm-error', decidedBy: 'classifier' };
|
|
100
|
+
} finally {
|
|
101
|
+
latencyMs += now() - started;
|
|
102
|
+
}
|
|
103
|
+
if (!isTrack(decision.track)) throw new Error(`Invalid classifier track for item ${reportIds[index]}`);
|
|
104
|
+
got.push(decision.track);
|
|
105
|
+
confusion[item.expected][decision.track]++;
|
|
106
|
+
if (decision.track === item.expected) agree++;
|
|
107
|
+
if (decision.track === 'ask-human' &&
|
|
108
|
+
(decision.reason.startsWith('classifier-low-confidence:') || decision.reason.startsWith('classifier-failed:'))) askHuman++;
|
|
109
|
+
}
|
|
110
|
+
perItem.push({ id: reportIds[index]!, expected: item.expected, got });
|
|
111
|
+
}
|
|
112
|
+
const total = items.length * runs;
|
|
113
|
+
results.push({ label, runs: total, agree, agreement: agree / total, wilson: wilsonInterval(agree, total), askHuman,
|
|
114
|
+
confusion, meanLatencyMs: latencyMs / total, perItem });
|
|
115
|
+
}
|
|
116
|
+
return results;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function formatFrontRouteReport(result: readonly FrontRouteProviderResult[]): string {
|
|
120
|
+
const percent = (n: number) => `${(100 * n).toFixed(1)}%`;
|
|
121
|
+
const lines = ['Provider | Agreement | 95% Wilson | ask-human (low confidence/failure) | Mean latency ms'];
|
|
122
|
+
for (const provider of result) {
|
|
123
|
+
const interval = provider.wilson;
|
|
124
|
+
lines.push(`${provider.label} | ${provider.agree}/${provider.runs} (${percent(provider.agreement)}) | ${interval ? `${percent(interval.lower)}–${percent(interval.upper)}` : 'n/a'} | ${provider.askHuman} | ${provider.meanLatencyMs.toFixed(1)}`);
|
|
125
|
+
lines.push(`Confusion (${provider.label}): expected \\ got | ${INTAKE_FRONT_ROUTE_TRACKS.join(' | ')}`);
|
|
126
|
+
for (const expected of INTAKE_FRONT_ROUTE_TRACKS) {
|
|
127
|
+
lines.push(`${expected} | ${INTAKE_FRONT_ROUTE_TRACKS.map((got) => provider.confusion[expected][got]).join(' | ')}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return lines.join('\n');
|
|
131
|
+
}
|