elanous 0.2.4 → 0.2.5
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 +3 -3
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/06ef2eba-d1b99ac6d2ea663c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1413-7179a49332db5a80.js +1 -0
- package/apps/pwa/out/_next/static/chunks/150-25c277137a9638ab.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1640-bc99ec23bbe29d12.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{2415.ed6428ff334c7ce2.js → 1751.50b96ea9c37b49b6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/2300-de059d0ab2a16630.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2684-05b0a81e75298c16.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3026-2ed7e317d9b02102.js +25 -0
- package/apps/pwa/out/_next/static/chunks/3611-77496ada38cd6358.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3717-68957c4dc9ceec90.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3929-46ba9af6f9d895cc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4015-62c5a3744698a8da.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4217-3c4317a7905f9eeb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4240-3230f230fae2d7d5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4271-e53e075ebd48593b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4377.3b125fef79576dfd.js +5 -0
- package/apps/pwa/out/_next/static/chunks/4440-18bb160d95819f4e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4853-35fdbe6bdbcdc0fc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{2ea96958-e8c7e112de50b067.js → 4daa2798-1363d725c79f3593.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/5007-6a3b30e23ff68a6e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5244-12b424c42070f5ff.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{5791-bb82743fa6299562.js → 5348-bbf838e119661eee.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/5391-8793f88b03b30161.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5611-df00af3e4e4a61bb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5890-dca2321773be829a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6123-37c9dc57ddaa7ab0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6588-b7975304a302ab91.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6604-c03961962b6273b6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7109-cf8c87e968e83a65.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7178-30f77877525a2efb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7262-871563fc9663730b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7294-f0eeebb92aa93459.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7429-881af44deed6958f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7527.943753b6ed829f09.js +1 -0
- package/apps/pwa/out/_next/static/chunks/764-a01e858525a190c0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7868-e35d5f7985ecd3f1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8320-51484af7b01fdfa5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{4323.c5dfb6f503304775.js → 8617.465ca22a319c95b3.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{5080-1613752891fcadb4.js → 8918-482b30ce2a8ebf4c.js} +3 -3
- package/apps/pwa/out/_next/static/chunks/9131-52f5fad51cceca04.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9238.2cd5f47f4bf7a2c3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.js → 9704-3221c102623c8d73.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-70c404c7bb6d7f0f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-3ffd80166b1050aa.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f89e9a303515a033.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-4f403cd060442c95.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-9fa879ebf5959096.js → page-0ac5d053f0444d21.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-4bcc63e272ae8205.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-880734bd07d62882.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-4472ddf1e2458e69.js → page-72bd384898d88955.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-1018bbc9586ecae5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-de86f8c38076dd7b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-70c404c7bb6d7f0f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-cd6447196d8bafe6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-e733630c47fa102b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-d1c1a8357b161875.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-60c514128e81d787.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-8b637b7e325e13c1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-112f7e477bda77fd.js → page-bccce0babceae30f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-63301d11dbe73836.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-a8cf0dc472060db2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-02fba54881ccfa49.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-d5e8e90f0038f8de.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-b1ef6824f494466a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-d589bb0f233b1009.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-c9bbaebef4cda059.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-3639ce7916b92fed.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-7450ae7478955653.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-70c404c7bb6d7f0f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-b41ca4abfd618452.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-b333dd79ebee3fa9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-8be1227492793655.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-6e03e9371b552e0d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-911411e9fca18d03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-792170e649b37088.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-c40463466937bc91.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-509c798b80384536.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-1cac2aa21d77c417.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-9626799d9b4a2cd1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-84cd419494e16b0f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8046cadf-211ca3c0e00f2b72.js → f6f3850d-7f324c6566002113.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/framework-361487d2da603917.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-607f5beb94cb2e07.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-d0222d219e29dddf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_app-a26980a31126decc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_error-edb2b37decf23e4d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{webpack-e5bea36f183b25e6.js → webpack-2bf59c720fe9542c.js} +1 -1
- package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +1 -0
- package/apps/pwa/out/_next/static/{zoQX5iTvXOsIvCjxCe1Ji → ktMqfrBbf8BMGac0jqLrb}/_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 +8 -1
- package/graphs/release/release-loop.yaml +6 -1
- package/graphs/video/README.md +1 -0
- package/graphs/video/explainer-line.yaml +58 -0
- package/package.json +3 -1
- package/plugins/job-coach/.codex-plugin/plugin.json +18 -1
- package/plugins/job-coach/README.md +12 -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/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/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/gate-node.ts +332 -44
- package/scripts/release-loop/publish-node.ts +7 -3
- package/scripts/release-loop/release-docs.ts +20 -13
- 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 +50 -0
- package/skills/explainer-video/SKILL.md +99 -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/vo.ts +37 -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/pr-manager.ts +51 -4
- 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/plugin-cli.ts +109 -11
- package/src/cli/pr-lineage.ts +2 -1
- package/src/cli/pwa-start.ts +3 -0
- package/src/cli/release-cli.ts +94 -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 +8 -2
- 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/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 +56 -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/graph-approvals.ts +125 -0
- package/src/nexus/api/http-server.ts +60 -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/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/prompt-library/universal-preamble.ts +8 -2
- package/src/release-loop/checklist.ts +191 -0
- package/src/release-loop/release-note.ts +6 -3
- 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/seams.ts +4 -2
- 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-command-job.ts +27 -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/tui-client/acp-transport-ws-client.ts +72 -5
- package/src/tui.ts +3 -2
- package/src/user-config.ts +28 -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/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/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/3403.4bc97b4066945a94.js +0 -5
- 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/8142-011e1f474efb8b33.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/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/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/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 → ktMqfrBbf8BMGac0jqLrb}/_ssgManifest.js +0 -0
|
@@ -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,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
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Named tui-sim keys and raw PTY input bytes.
|
|
2
|
+
const KEYS: Record<string, string> = {
|
|
3
|
+
enter: '\r', return: '\r', tab: '\t', 'shift-tab': '\x1b[Z', esc: '\x1b', escape: '\x1b',
|
|
4
|
+
space: ' ', backspace: '\x7f', delete: '\x1b[3~',
|
|
5
|
+
up: '\x1b[A', down: '\x1b[B', right: '\x1b[C', left: '\x1b[D',
|
|
6
|
+
pageup: '\x1b[5~', pagedown: '\x1b[6~', home: '\x1b[H', end: '\x1b[F',
|
|
7
|
+
'shift-enter': '\x1b[13;2u',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function resolveKeyBytes(name: string): string | null {
|
|
11
|
+
const key = name.toLowerCase();
|
|
12
|
+
if (Object.hasOwn(KEYS, key)) return KEYS[key]!;
|
|
13
|
+
if (/^ctrl-[a-z]$/.test(key)) return String.fromCharCode(key.charCodeAt(5) - 96);
|
|
14
|
+
if (key === 'ctrl-\\') return '\x1c';
|
|
15
|
+
if (key === 'ctrl-]') return '\x1d';
|
|
16
|
+
if (key.startsWith('hex:')) {
|
|
17
|
+
const hex = key.slice(4);
|
|
18
|
+
return hex.length > 0 && hex.length % 2 === 0 && /^[0-9a-f]+$/.test(hex)
|
|
19
|
+
? Buffer.from(hex, 'hex').toString('latin1')
|
|
20
|
+
: null;
|
|
21
|
+
}
|
|
22
|
+
if (key.startsWith('alt-')) {
|
|
23
|
+
const suffix = name.slice(4);
|
|
24
|
+
const bytes = Object.hasOwn(KEYS, suffix.toLowerCase()) ? KEYS[suffix.toLowerCase()]!
|
|
25
|
+
: suffix.toLowerCase().startsWith('ctrl-') ? resolveKeyBytes(suffix)
|
|
26
|
+
: [...suffix].length === 1 ? suffix : null;
|
|
27
|
+
return bytes === null ? null : `\x1b${bytes}`;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Keep raw hex as a Buffer: passing a latin1 string to the PTY would UTF-8 encode bytes >= 0x80. */
|
|
33
|
+
export function resolveKeyInput(name: string): string | Buffer | null {
|
|
34
|
+
const bytes = resolveKeyBytes(name);
|
|
35
|
+
if (bytes === null) return null;
|
|
36
|
+
return name.toLowerCase().startsWith('hex:') ? Buffer.from(name.slice(4), 'hex') : bytes;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Bun's native PTY passes Buffer bytes through unchanged; its typed registry API only declares strings. */
|
|
40
|
+
export function writeKeyInput(handle: { write(chars: string): void }, input: string | Buffer): void {
|
|
41
|
+
handle.write(input as string);
|
|
42
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import type { ResolveRoleProviderFn } from '../src/intake-plane/runtime-callables.js';
|
|
4
|
+
import type { LLMProvider } from '../src/llm.js';
|
|
5
|
+
import { formatFrontRouteReport, loadFrontRouteCorpus, measureFrontRoute } from './lib/front-route-measurement.js';
|
|
6
|
+
|
|
7
|
+
interface RunnerOptions { providers: string[]; runs: number; json: boolean }
|
|
8
|
+
|
|
9
|
+
export function parseFrontRouteArgs(args: readonly string[]): RunnerOptions {
|
|
10
|
+
const options: RunnerOptions = { providers: [], runs: 1, json: false };
|
|
11
|
+
for (let i = 0; i < args.length; i++) {
|
|
12
|
+
const arg = args[i];
|
|
13
|
+
if (arg === '--json') options.json = true;
|
|
14
|
+
else if (arg === '--provider') {
|
|
15
|
+
const value = args[++i];
|
|
16
|
+
if (!value || value.startsWith('--') || !/^[\w-]+(?:\/\S+)?$/.test(value)) throw new Error('--provider requires <name>[/<model>]');
|
|
17
|
+
options.providers.push(value);
|
|
18
|
+
} else if (arg === '--runs') {
|
|
19
|
+
const value = args[++i];
|
|
20
|
+
if (!value || !/^[1-9]\d*$/.test(value) || !Number.isSafeInteger(Number(value))) throw new Error('--runs requires a positive integer');
|
|
21
|
+
options.runs = Number(value);
|
|
22
|
+
} else throw new Error(`Unknown option: ${arg}`);
|
|
23
|
+
}
|
|
24
|
+
return options;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function frontRouteProviders(specs: readonly string[], registry: Record<string, LLMProvider>): { label: string; resolve: ResolveRoleProviderFn }[] {
|
|
28
|
+
return specs.map((label) => {
|
|
29
|
+
const slash = label.indexOf('/');
|
|
30
|
+
const name = slash < 0 ? label : label.slice(0, slash);
|
|
31
|
+
const model = slash < 0 ? undefined : label.slice(slash + 1);
|
|
32
|
+
const provider = registry[name];
|
|
33
|
+
if (!provider) throw new Error(`Unknown provider: ${name}`);
|
|
34
|
+
const selectedModel = model ?? provider.defaultModel;
|
|
35
|
+
const resolveRole: ResolveRoleProviderFn = (_role) => ({ provider: provider as unknown as ReturnType<ResolveRoleProviderFn>['provider'], ...(typeof selectedModel === 'string' ? { model: selectedModel } : {}) });
|
|
36
|
+
return { label, resolve: resolveRole };
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function main(args: readonly string[] = process.argv.slice(2), env: NodeJS.ProcessEnv = process.env): Promise<void> {
|
|
41
|
+
const options = parseFrontRouteArgs(args);
|
|
42
|
+
if (!env.CORPUS_PATH?.trim()) throw new Error('CORPUS_PATH is required (path to front-route corpus JSON)');
|
|
43
|
+
const corpus = loadFrontRouteCorpus(readFileSync(resolve(process.cwd(), env.CORPUS_PATH), 'utf8'));
|
|
44
|
+
const { PROVIDERS } = await import('../src/llm.js');
|
|
45
|
+
let selected = options.providers;
|
|
46
|
+
if (!selected.length) {
|
|
47
|
+
const { resolveRoleLlm } = await import('../src/user-config.js');
|
|
48
|
+
const { provider, model } = resolveRoleLlm('classify');
|
|
49
|
+
selected = [model ? `${provider}/${model}` : provider];
|
|
50
|
+
}
|
|
51
|
+
const providers = frontRouteProviders(selected, PROVIDERS);
|
|
52
|
+
const result = await measureFrontRoute(corpus.items, { providers, runs: options.runs });
|
|
53
|
+
console.log(options.json ? JSON.stringify(result, null, 2) : formatFrontRouteReport(result));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (import.meta.main) await main();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/bin/zsh
|
|
2
|
+
# 공개 영상 누출 관문 — 1초 한 장(또는 --fps N) 전수 OCR → 금지 목록 → 초 단위 표(hits.tsv).
|
|
3
|
+
# 사용: scripts/media/ocr-leak-scan.sh <영상> <산출 폴더> [--fps N]
|
|
4
|
+
# 한계: tesseract eng — 한글 칸·이모지는 사람 눈으로 따로 본다(내부 문서 `TC` §2). 걸림은 오독이 많다 → 그 초 프레임을 눈으로 대조한다.
|
|
5
|
+
V=$1; O=$2; FPS=1
|
|
6
|
+
[ "${3:-}" = "--fps" ] && FPS=${4:-1}
|
|
7
|
+
[ -f "$V" ] && [ -n "$O" ] || { echo "사용: $0 <영상> <산출 폴더> [--fps N]" >&2; exit 2; }
|
|
8
|
+
command -v tesseract >/dev/null && command -v ffmpeg >/dev/null || { echo "tesseract·ffmpeg 가 필요하다" >&2; exit 2; }
|
|
9
|
+
mkdir -p "$O/f" && ffmpeg -loglevel error -i "$V" -vf "fps=$FPS" "$O/f/%05d.png" || exit 2
|
|
10
|
+
# 금지: 홈 경로 · 실명 계정 표지 · 사설 호스트 · tailnet · 금액 · 크레딧 수 · 메일 · 키·토큰 모양(elanous 단기 토큰 elt_ · bearer · GitHub 토큰 · 40자+ 연속)
|
|
11
|
+
PAT='/Users/|msb[0-9]|\bmbp\b|tail[0-9a-f]{4,}|ts\.net|\$[0-9]|USD|credit[s]? *[0-9]|@gmail|api[_-]?key|sk-[A-Za-z0-9]{8}|elt_[A-Za-z0-9_-]{6}|bearer|ghs_|gh[po]_|[A-Za-z0-9_-]{40,}'
|
|
12
|
+
[ -n "${OCR_EXTRA_PATTERN:-}" ] && PAT="$PAT|$OCR_EXTRA_PATTERN" # 계정 실명 등은 저장소에 적지 않고 환경변수로 준다
|
|
13
|
+
: > "$O/hits.tsv"
|
|
14
|
+
for f in "$O"/f/*.png; do
|
|
15
|
+
n=${${f:t:r}#0*}; s=$(( (n - 1) / FPS ))
|
|
16
|
+
t=$(tesseract "$f" - -l eng --psm 11 2>/dev/null)
|
|
17
|
+
print -r -- "$t" > "$O/f/${f:t:r}.txt"
|
|
18
|
+
print -r -- "$t" | grep -inE "$PAT" | grep -vE '^[0-9]+:[^ ]*run-[0-9a-f]{8}-[0-9a-f-]{27}$' \
|
|
19
|
+
| while read -r line; do print -r -- "$s\t$line" >> "$O/hits.tsv"; done
|
|
20
|
+
done
|
|
21
|
+
print "frames=$(ls "$O"/f/*.png | wc -l | tr -d ' ') hits=$(wc -l < "$O/hits.tsv" | tr -d ' ')"
|
package/scripts/public-export.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { userInfo } from 'node:os';
|
|
|
23
23
|
import { dirname, join, posix, resolve } from 'node:path';
|
|
24
24
|
import ts from 'typescript';
|
|
25
25
|
import { parse } from 'yaml';
|
|
26
|
+
import { spawnSyncText } from '../src/util/spawn-sync-output.js';
|
|
26
27
|
|
|
27
28
|
export interface ExportTransform {
|
|
28
29
|
readonly id: string;
|
|
@@ -369,9 +370,7 @@ export function summarizeLeaks(hits: readonly LeakHit[]): Record<string, { hits:
|
|
|
369
370
|
}
|
|
370
371
|
|
|
371
372
|
function trackedFiles(root: string): string[] {
|
|
372
|
-
|
|
373
|
-
if (listed.status !== 0) throw new Error(`git ls-files failed: ${listed.stderr.trim()}`);
|
|
374
|
-
return listed.stdout.split('\0').filter(Boolean);
|
|
373
|
+
return spawnSyncText('git', ['-C', root, 'ls-files', '-z']).split('\0').filter(Boolean);
|
|
375
374
|
}
|
|
376
375
|
|
|
377
376
|
export function run(argv: readonly string[], root = resolve(import.meta.dir, '..')): number {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
/** Import a public PR patch without
|
|
2
|
+
/** Import a public PR patch without committing or publishing it. */
|
|
3
3
|
import { spawnSync } from 'node:child_process';
|
|
4
4
|
import { existsSync, lstatSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync, unlinkSync } from 'node:fs';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
6
|
import { dirname, isAbsolute, join } from 'node:path';
|
|
7
7
|
import { loadExportConfig, selectExportFiles, transformExportFiles } from './public-export.js';
|
|
8
|
+
import { debug } from '../src/debug/log.js';
|
|
9
|
+
import { spawnSyncText } from '../src/util/spawn-sync-output.js';
|
|
8
10
|
|
|
9
11
|
export type ImportKind = 'clean' | 'renamed' | 'transformed' | 'outside-export';
|
|
10
12
|
export interface ImportFile { publicPath: string; sourcePath?: string; kind: ImportKind; reason?: string }
|
|
@@ -83,9 +85,7 @@ function gitApply(root: string, patch: string, check = false): string | undefine
|
|
|
83
85
|
}
|
|
84
86
|
|
|
85
87
|
function trackedFiles(root: string): string[] {
|
|
86
|
-
|
|
87
|
-
if (result.status !== 0) throw new Error(`git ls-files failed: ${result.stderr}`);
|
|
88
|
-
return result.stdout.split('\0').filter(Boolean);
|
|
88
|
+
return spawnSyncText('git', ['ls-files', '-z'], { cwd: root, encoding: 'utf8' }).split('\0').filter(Boolean);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
/** Classify against the real export, stage eligible edits, then write only on --apply. */
|
|
@@ -177,8 +177,82 @@ export function importPublicPatch(patch: string, root: string, apply = false, au
|
|
|
177
177
|
} finally { rmSync(staging, { recursive: true, force: true }); }
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
|
|
181
|
-
|
|
180
|
+
interface GhResult { status: number | null; stdout: string; stderr?: string }
|
|
181
|
+
interface RunDeps { gh?: (argv: readonly string[], root: string) => GhResult }
|
|
182
|
+
|
|
183
|
+
function prAuthors(raw: string, databaseIdForLogin: (login: string) => number): string[] {
|
|
184
|
+
const parsed: unknown = JSON.parse(raw);
|
|
185
|
+
if (!parsed || typeof parsed !== 'object' || !('commits' in parsed) || !Array.isArray(parsed.commits) || !parsed.commits.length) {
|
|
186
|
+
throw new Error('PR has no commits');
|
|
187
|
+
}
|
|
188
|
+
const unique = new Map<string, string>();
|
|
189
|
+
const seenEmails = new Set<string>();
|
|
190
|
+
for (const commit of parsed.commits) {
|
|
191
|
+
const identities = commit?.authors ?? (commit?.author ? [commit.author] : []);
|
|
192
|
+
if (!Array.isArray(identities) || !identities.length) throw new Error('PR commit has no author');
|
|
193
|
+
for (const identity of identities) {
|
|
194
|
+
const name = identity?.name?.trim();
|
|
195
|
+
const email = identity?.email?.trim();
|
|
196
|
+
const login = identity?.login?.trim() ?? identity?.user?.login?.trim();
|
|
197
|
+
const key = login ? `login:${login.toLowerCase()}` : `email:${email?.toLowerCase()}`;
|
|
198
|
+
if (unique.has(key) || (email && seenEmails.has(email.toLowerCase()))) continue;
|
|
199
|
+
const id = identity?.user?.databaseId ?? identity?.id;
|
|
200
|
+
const numericId = /^[1-9]\d*$/.test(String(id)) ? Number(id) : undefined;
|
|
201
|
+
const address = email || (login && /^[A-Za-z0-9-]+$/.test(login)
|
|
202
|
+
? `${numericId && Number.isSafeInteger(numericId) ? numericId : databaseIdForLogin(login)}+${login}@users.noreply.github.com`
|
|
203
|
+
: undefined);
|
|
204
|
+
if (typeof name !== 'string' || !/^[^<>\r\n]+$/.test(name) || !address || !/^[^<>\s@]+@[^<>\s@]+$/.test(address)) {
|
|
205
|
+
throw new Error('PR commit author lacks a valid name or email/identity');
|
|
206
|
+
}
|
|
207
|
+
unique.set(key, `${name} <${address}>`);
|
|
208
|
+
seenEmails.add(address.toLowerCase());
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return [...unique.values()];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** CLI entrypoint: patch + --author, or --pr <number> --repo <owner/name>. */
|
|
215
|
+
export function run(argv: readonly string[], root = process.cwd(), stdin?: string, deps: RunDeps = {}): number {
|
|
216
|
+
const prMode = argv.includes('--pr') || argv.includes('--repo');
|
|
217
|
+
if (prMode) {
|
|
218
|
+
const prIndex = argv.indexOf('--pr');
|
|
219
|
+
const repoIndex = argv.indexOf('--repo');
|
|
220
|
+
const pr = argv[prIndex + 1];
|
|
221
|
+
const repo = argv[repoIndex + 1];
|
|
222
|
+
const flags = argv.filter((arg) => arg === '--apply' || arg === '--dry-run');
|
|
223
|
+
if (prIndex < 0 || repoIndex < 0 || !pr || !/^[1-9]\d*$/.test(pr) || !repo ||
|
|
224
|
+
!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo) ||
|
|
225
|
+
argv.length !== 4 + flags.length || flags.length > 1 ||
|
|
226
|
+
argv.some((arg, i) => (arg === '--pr' && i !== prIndex) || (arg === '--repo' && i !== repoIndex))) {
|
|
227
|
+
console.error('usage: bun scripts/public-import-pr.ts --pr <number> --repo <owner/name> [--dry-run|--apply]');
|
|
228
|
+
return 2;
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
const gh = deps.gh ?? ((args: readonly string[], cwd: string) => spawnSync('gh', [...args], { cwd, encoding: 'utf8' }));
|
|
232
|
+
const fetch = (args: string[]): string => {
|
|
233
|
+
const result = gh(args, root);
|
|
234
|
+
if (result.status !== 0) throw new Error(result.stderr || `gh ${args[1]} failed`);
|
|
235
|
+
return result.stdout;
|
|
236
|
+
};
|
|
237
|
+
const patch = fetch(['pr', 'diff', pr, '--repo', repo]);
|
|
238
|
+
const authors = prAuthors(fetch(['pr', 'view', pr, '--repo', repo, '--json', 'commits']), (login) => {
|
|
239
|
+
const user: unknown = JSON.parse(fetch(['api', `users/${login}`]));
|
|
240
|
+
const id = user && typeof user === 'object' && 'id' in user ? user.id : undefined;
|
|
241
|
+
const resolvedLogin = user && typeof user === 'object' && 'login' in user ? user.login : undefined;
|
|
242
|
+
if (typeof id !== 'number' || !Number.isSafeInteger(id) || id <= 0 ||
|
|
243
|
+
typeof resolvedLogin !== 'string' || resolvedLogin.toLowerCase() !== login.toLowerCase()) {
|
|
244
|
+
throw new Error('GitHub user lacks a matching login or numeric database ID');
|
|
245
|
+
}
|
|
246
|
+
return id;
|
|
247
|
+
});
|
|
248
|
+
const result = importPublicPatch(patch, root, flags.includes('--apply'));
|
|
249
|
+
debug.log('public-import.pr', 'fetched', { repo, pr: Number(pr), files: result.files.length, authors: authors.length });
|
|
250
|
+
for (const file of result.files) console.log(`${file.kind}\t${file.publicPath}\t${file.sourcePath ?? '-'}${file.reason ? `\t${file.reason}` : ''}`);
|
|
251
|
+
console.log(`\nCommit message draft (not committed):\nImport public PR ${repo}#${pr}\n\nhttps://github.com/${repo}/pull/${pr}\n\n${authors.map((author) => `Co-Authored-By: ${author}`).join('\n')}`);
|
|
252
|
+
if (result.rc) console.error('import incomplete: one or more files were not applied');
|
|
253
|
+
return result.rc;
|
|
254
|
+
} catch (error) { console.error(`import failed: ${String(error)}`); return 1; }
|
|
255
|
+
}
|
|
182
256
|
const apply = argv.includes('--apply');
|
|
183
257
|
const authorIndex = argv.indexOf('--author');
|
|
184
258
|
const author = authorIndex >= 0 ? argv[authorIndex + 1] : undefined;
|
|
@@ -3,6 +3,7 @@ import { createHash } from 'node:crypto';
|
|
|
3
3
|
import { spawnSync } from 'node:child_process';
|
|
4
4
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { join, resolve } from 'node:path';
|
|
6
|
+
import { spawnSyncText } from '../src/util/spawn-sync-output.js';
|
|
6
7
|
|
|
7
8
|
export function publishInternalDist(checkout: string, out: string, keep = 3): { ok: true; version: string; commit: string; sha256: string; out: string; pwaBuild: 'present' | 'missing' } {
|
|
8
9
|
checkout = resolve(checkout);
|
|
@@ -42,9 +43,7 @@ export function publishInternalDist(checkout: string, out: string, keep = 3): {
|
|
|
42
43
|
const isBuildOutput = (path: string): boolean => path.startsWith('apps/pwa/out/') || path === 'src/version/packed-revision.json';
|
|
43
44
|
const publishedPaths = [...new Set([...packedPaths, 'scripts/install.sh'])].filter((path) => !isBuildOutput(path));
|
|
44
45
|
const pwaBuild: 'present' | 'missing' = packedPaths.includes('apps/pwa/out/index.html') ? 'present' : 'missing';
|
|
45
|
-
const
|
|
46
|
-
if (tracked.status !== 0) throw new Error(`cannot list committed files: ${tracked.stderr}`);
|
|
47
|
-
const committed = new Set(tracked.stdout.split('\0').filter(Boolean));
|
|
46
|
+
const committed = new Set(spawnSyncText('git', ['ls-files', '--cached', '-z'], { cwd: checkout, encoding: 'utf8' }).split('\0').filter(Boolean));
|
|
48
47
|
const uncommitted = publishedPaths.filter((path) => !committed.has(path));
|
|
49
48
|
if (uncommitted.length) throw new Error(`packed files not committed at HEAD: ${uncommitted.join(', ')}`);
|
|
50
49
|
const changed = spawnSync('git', ['diff', '--quiet', 'HEAD', '--', ...publishedPaths], { cwd: checkout, encoding: 'utf8' });
|
|
@@ -71,7 +71,7 @@ export async function main(argv: readonly string[]): Promise<number> {
|
|
|
71
71
|
const grown = diffs.filter((d) => d.newRed.length > 0);
|
|
72
72
|
if (argv.includes('--notify') && grown.length > 0) {
|
|
73
73
|
const { sendOutbound } = await import('../src/domains/outbound-alert.js');
|
|
74
|
-
sendOutbound(`⚠️ **파일별 빨강 스윕 — 새 빨강**\n\n${grown.map(renderDiff).join('\n\n')}`, 'alert');
|
|
74
|
+
sendOutbound(`⚠️ **파일별 빨강 스윕 — 새 빨강**\n\n${grown.map(renderDiff).join('\n\n')}`, 'ops-alert');
|
|
75
75
|
}
|
|
76
76
|
return diffs.length === 0 ? 2 : 0;
|
|
77
77
|
}
|