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
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// 재사용: startPty(Bun 네이티브 PTY)·createWorktree·streamLLM·worktreeHasChanges·commitWorktree.
|
|
10
10
|
import { execFileSync, spawnSync, spawn as spawnChild } from 'node:child_process';
|
|
11
11
|
import { homedir } from 'node:os';
|
|
12
|
-
import { existsSync, writeFileSync, mkdirSync, readdirSync, readFileSync, statSync, readlinkSync, realpathSync, lstatSync } from 'node:fs';
|
|
12
|
+
import { existsSync, writeFileSync, mkdirSync, readdirSync, readFileSync, statSync, readlinkSync, realpathSync, lstatSync, unlinkSync } from 'node:fs';
|
|
13
13
|
import { createHash } from 'node:crypto';
|
|
14
14
|
import { join, relative, resolve, isAbsolute, sep } from 'node:path';
|
|
15
15
|
import { startPty, mintPtyId, onPtyEvent, type PtyHandle, type StartOpts } from '../pty-shell/registry.js';
|
|
@@ -34,8 +34,10 @@ import { runWithControlObserve } from '../capture/control-observe-adapter.js';
|
|
|
34
34
|
import { buildMissionProvision, sanitizeForPtyInput, type ProvisionRequest, type ProvisionResult } from './provision.js';
|
|
35
35
|
import { checkClaudeSubscription } from './claude-subscription.js';
|
|
36
36
|
import { handoffMission, waitForHumanLogin, type HandoffOperations } from './handoff.js';
|
|
37
|
+
import { dispatchOpenPullRequest } from '../tool-runtime/git-pr-runtime.js';
|
|
37
38
|
import { runClaudeHeadless } from './claude-headless.js';
|
|
38
39
|
import { installPluginInsideAgent, type PluginRequest, type PluginDeps } from './in-agent-plugin.js';
|
|
40
|
+
import { planMissionResources, type ResourceLadderDeps, type ResourcePlan } from './resource-ladder.js';
|
|
39
41
|
import { ensureRunId } from '../harness/harness-space.js';
|
|
40
42
|
import { recordHarnessWorktreeProvenance } from '../harness/harness-worktree-add.js';
|
|
41
43
|
import { resolveInstanceName } from '../instance-identity.js';
|
|
@@ -459,6 +461,10 @@ export interface AgentMissionSpec {
|
|
|
459
461
|
headless?: boolean;
|
|
460
462
|
/** Install in the agent's own UI before sending the mission. */
|
|
461
463
|
plugin?: Pick<PluginRequest, 'plugin' | 'marketplace'>;
|
|
464
|
+
/** Pre-mission resource judgment; off skips it. */
|
|
465
|
+
resources?: 'on' | 'off';
|
|
466
|
+
/** Only the caller's explicit backend selection, not the resolved default. */
|
|
467
|
+
backendExplicit?: string;
|
|
462
468
|
/** Explicit backend sequence; transitions use the same handoff coordinator as brain decisions. */
|
|
463
469
|
chain?: readonly ('codex' | 'claude' | 'elanous')[];
|
|
464
470
|
/**
|
|
@@ -496,7 +502,8 @@ export interface AgentMissionResult {
|
|
|
496
502
|
ok: boolean;
|
|
497
503
|
worktree: string;
|
|
498
504
|
branch: string | null;
|
|
499
|
-
reason?: 'aborted' | 'outside-write';
|
|
505
|
+
reason?: 'aborted' | 'outside-write' | 'origin-not-github';
|
|
506
|
+
pr?: 'skipped';
|
|
500
507
|
paths?: string[];
|
|
501
508
|
rounds: number;
|
|
502
509
|
evidencePath: string | null;
|
|
@@ -526,6 +533,7 @@ export interface AgentMissionDeps {
|
|
|
526
533
|
runClaudeHeadless?: typeof runClaudeHeadless;
|
|
527
534
|
checkEvidence?: typeof checkEvidence;
|
|
528
535
|
commitWorktree?: typeof commitWorktree;
|
|
536
|
+
dispatchOpenPullRequest?: typeof dispatchOpenPullRequest;
|
|
529
537
|
terminateProcessTree?: typeof terminateMissionProcessTree;
|
|
530
538
|
repoStatus?: (repo: string) => string | Promise<string>;
|
|
531
539
|
emitDecision?: typeof emitDecision;
|
|
@@ -546,6 +554,8 @@ export interface AgentMissionDeps {
|
|
|
546
554
|
initialTsc?: ReturnType<typeof collectTscDiagnostics> | null;
|
|
547
555
|
installPlugin?: typeof installPluginInsideAgent;
|
|
548
556
|
pluginDeps?: PluginDeps;
|
|
557
|
+
resourceLadder?: ResourceLadderDeps;
|
|
558
|
+
planMissionResources?: typeof planMissionResources;
|
|
549
559
|
}
|
|
550
560
|
|
|
551
561
|
const DEFAULT_OMNI = `${process.env.HOME}/.claude/skills/omni-crawl/scripts/main.ts`;
|
|
@@ -1644,16 +1654,55 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
|
|
|
1644
1654
|
screen = await (deps.awaitMission ? deps.awaitMission(trusted) : trusted);
|
|
1645
1655
|
}
|
|
1646
1656
|
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
+
let resourcePlan: ResourcePlan | undefined;
|
|
1658
|
+
// Only an explicit `on` runs the ladder (the CLI passes `on` by default). Library callers and fake-PTY
|
|
1659
|
+
// tests that leave it unset keep the pre-ladder behaviour — 09-29 #21849 made them wait on a real LLM.
|
|
1660
|
+
if (spec.resources === 'on') {
|
|
1661
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1662
|
+
try {
|
|
1663
|
+
resourcePlan = await Promise.race([
|
|
1664
|
+
(deps.planMissionResources ?? planMissionResources)(spec.mission, deps.resourceLadder ?? {}, {
|
|
1665
|
+
backend: spec.backendExplicit,
|
|
1666
|
+
plugin: spec.plugin ? `${spec.plugin.plugin}@${spec.plugin.marketplace}` : undefined,
|
|
1667
|
+
resources: spec.resources,
|
|
1668
|
+
deadlineMs: 20_000,
|
|
1669
|
+
}),
|
|
1670
|
+
new Promise<never>((_resolve, reject) => { timer = setTimeout(() => reject(new Error('resource ladder timeout (20s)')), 20_000); }),
|
|
1671
|
+
]);
|
|
1672
|
+
debug.log('agent-mission.resources', 'planned', {
|
|
1673
|
+
have: resourcePlan.have.length, gaps: resourcePlan.gaps.length,
|
|
1674
|
+
suggestedPlugin: resourcePlan.plugin?.plugin, suggestedBackend: resourcePlan.backend?.name,
|
|
1675
|
+
executedBackend: backend.name,
|
|
1676
|
+
});
|
|
1677
|
+
} catch (error) {
|
|
1678
|
+
debug.log('agent-mission.resources', 'plan-failed', { reason: error instanceof Error ? error.message : String(error) }, { level: 'warn' });
|
|
1679
|
+
} finally { if (timer) clearTimeout(timer); }
|
|
1680
|
+
}
|
|
1681
|
+
const suggestedPlugin = resourcePlan?.plugin?.source === 'official-index' && !spec.plugin
|
|
1682
|
+
&& !spec.headless && (backend.name === 'codex' || backend.name === 'claude')
|
|
1683
|
+
&& resourcePlan.plugin.marketplace === 'elanous'
|
|
1684
|
+
&& /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(resourcePlan.plugin.plugin)
|
|
1685
|
+
? { plugin: resourcePlan.plugin.plugin, marketplace: resourcePlan.plugin.marketplace } : undefined;
|
|
1686
|
+
const requestedPlugin = spec.plugin ?? suggestedPlugin;
|
|
1687
|
+
let installedPlugin: typeof requestedPlugin;
|
|
1688
|
+
if (requestedPlugin) {
|
|
1689
|
+
try {
|
|
1690
|
+
const plugin = await (deps.installPlugin ?? installPluginInsideAgent)(
|
|
1691
|
+
{ agent: backend.name as 'codex' | 'claude', ...requestedPlugin }, h,
|
|
1692
|
+
{ ...(backend.handleTrust ? { handleTrust: backend.handleTrust } : {}), ...deps.pluginDeps, write: drive },
|
|
1693
|
+
);
|
|
1694
|
+
if (plugin.outcome !== 'installed') {
|
|
1695
|
+
if (spec.plugin) {
|
|
1696
|
+
stopLive();
|
|
1697
|
+
try { h.kill(); } catch { /* already exited */ }
|
|
1698
|
+
return { ok: false, worktree: wt.path, branch: wt.branch, rounds: 0, evidencePath: null,
|
|
1699
|
+
committed: false, usedOmniCrawl: false, detail: `플러그인 설치 ESCALATE (${plugin.reason}) — 미션은 전송하지 않았다` };
|
|
1700
|
+
}
|
|
1701
|
+
debug.log('agent-mission.resources', 'install-failed', { plugin: requestedPlugin.plugin, reason: plugin.reason }, { level: 'warn' });
|
|
1702
|
+
} else installedPlugin = requestedPlugin;
|
|
1703
|
+
} catch (error) {
|
|
1704
|
+
if (spec.plugin) throw error;
|
|
1705
|
+
debug.log('agent-mission.resources', 'install-failed', { plugin: requestedPlugin.plugin, reason: 'installer-error' }, { level: 'warn' });
|
|
1657
1706
|
}
|
|
1658
1707
|
}
|
|
1659
1708
|
|
|
@@ -1683,7 +1732,15 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
|
|
|
1683
1732
|
else debug.log('agent-mission', 'memory', { injected: false });
|
|
1684
1733
|
}
|
|
1685
1734
|
|
|
1686
|
-
if (
|
|
1735
|
+
if (installedPlugin) missionText = `${missionText}\n\n설치된 ${installedPlugin.plugin} 플러그인에서 실제 스킬을 확인하고 ${installedPlugin.plugin}:<skill> 형식으로 해당 스킬을 사용하라.`;
|
|
1736
|
+
if (resourcePlan) {
|
|
1737
|
+
const safeName = (name: string) => sanitizeForPtyInput(name, 80)
|
|
1738
|
+
.replace(/\b(?:bearer|secret|password|token|api.?key|authorization)\s*[:=]\s*\S+|(?:sk-[\w-]+|gh[pousr]_[\w-]+|github_pat_[\w-]+|AIza[\w-]+|xox[baprs]-[\w-]+)/gi, '[redacted]')
|
|
1739
|
+
.replace(/https?:\/\/\S+/gi, '[redacted]');
|
|
1740
|
+
const have = resourcePlan.have.slice(0, 5).map(safeName);
|
|
1741
|
+
const gaps = resourcePlan.gaps.slice(0, 5).map(gap => safeName(gap.need));
|
|
1742
|
+
missionText = `${missionText}\n\n[elanous 자원]\n보유: ${have.join(', ') || '없음'}\n설치: ${installedPlugin ? `${installedPlugin.plugin}@${installedPlugin.marketplace}` : '없음'}\n미충족: ${gaps.join(', ') || '없음'}`;
|
|
1743
|
+
}
|
|
1687
1744
|
if (deps.isMissionAborted?.()) throw new Error('MISSION_ABORTED');
|
|
1688
1745
|
// 미션 전송 — 인핸싱/기억으로 가공됐으면(멀티라인) 파일로 떨궈 read 지시(TUI 멀티라인 위험·verbatim 보존).
|
|
1689
1746
|
// 원문 그대로면(단문) 타이핑.
|
|
@@ -1738,6 +1795,7 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
|
|
|
1738
1795
|
};
|
|
1739
1796
|
let pendingHandoff: { to: 'codex' | 'claude'; mission: string; summary: string } | undefined;
|
|
1740
1797
|
let gatedHandoff = false;
|
|
1798
|
+
let handoffPrSkipped = false;
|
|
1741
1799
|
let transitionFailure: string | undefined;
|
|
1742
1800
|
const handoffOps: HandoffOperations = {
|
|
1743
1801
|
start: async (to, mission) => { pendingHandoff = { to, mission, summary: controllerSummary }; },
|
|
@@ -1765,11 +1823,30 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
|
|
|
1765
1823
|
if (spec.commit === false) throw new Error('elanous PR handoff requires commit authorization');
|
|
1766
1824
|
const ev = (deps.checkEvidence ?? evidenceCheck)(worktree, spec.evidence);
|
|
1767
1825
|
if (!ev.ok) throw new Error(`handoff evidence gate failed: ${ev.retry ?? 'missing evidence'}`);
|
|
1826
|
+
const promptFile = join(worktree, '.mission-prompt.md');
|
|
1827
|
+
let promptExisted = false;
|
|
1828
|
+
try { lstatSync(promptFile); promptExisted = true; } catch (error) {
|
|
1829
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
1830
|
+
}
|
|
1831
|
+
if (promptExisted) unlinkSync(promptFile);
|
|
1832
|
+
debug.log('agent-mission', 'handoff-prompt-file-removed', { existed: promptExisted });
|
|
1833
|
+
const stagedPrompt = execFileSync('git', ['ls-files', '--cached', '--', '.mission-prompt.md'], { cwd: worktree, encoding: 'utf8' });
|
|
1834
|
+
if (stagedPrompt.trim()) {
|
|
1835
|
+
execFileSync('git', ['rm', '-f', '--cached', '--', '.mission-prompt.md'], { cwd: worktree, encoding: 'utf8' });
|
|
1836
|
+
}
|
|
1837
|
+
if (execFileSync('git', ['ls-files', '--cached', '--', '.mission-prompt.md'], { cwd: worktree, encoding: 'utf8' }).trim()) {
|
|
1838
|
+
throw new Error('handoff prompt file remains in commit index');
|
|
1839
|
+
}
|
|
1768
1840
|
const commit = (deps.commitWorktree ?? commitWorktree)(worktree, `chore(agent-mission): ${spec.branch} handoff`);
|
|
1769
1841
|
if (!commit.ok) throw new Error(`handoff commit failed: ${commit.out}`);
|
|
1770
1842
|
execFileSync('git', ['push', '-u', 'origin', `HEAD:${spec.branch}`], { cwd: worktree, encoding: 'utf8', timeout: 120_000 });
|
|
1771
|
-
const
|
|
1772
|
-
|
|
1843
|
+
const pushUrls = execFileSync('git', ['remote', 'get-url', '--push', '--all', 'origin'], { cwd: worktree, encoding: 'utf8' }).trim().split('\n');
|
|
1844
|
+
if (!pushUrls.length || !pushUrls.every((url) => /^(?:https:\/\/github\.com\/|ssh:\/\/git@github\.com\/|git@github\.com:)[^/]+\/[^/]+\/?$/i.test(url))) {
|
|
1845
|
+
handoffPrSkipped = true;
|
|
1846
|
+
debug.log('agent-mission', 'handoff-pr-skipped', { reason: 'origin-not-github' });
|
|
1847
|
+
} else {
|
|
1848
|
+
(deps.dispatchOpenPullRequest ?? dispatchOpenPullRequest)({ title: `agent-mission: ${spec.branch}`, body: deps.originalMission ?? spec.mission, head: spec.branch, base: spec.base ?? 'main' }, { cwd: worktree });
|
|
1849
|
+
}
|
|
1773
1850
|
gatedHandoff = true;
|
|
1774
1851
|
},
|
|
1775
1852
|
...deps.handoffOperations,
|
|
@@ -1879,7 +1956,9 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
|
|
|
1879
1956
|
if (gatedHandoff && control.termination.kind !== 'error') {
|
|
1880
1957
|
stopLive();
|
|
1881
1958
|
try { h.kill(); } catch { /* noop */ }
|
|
1882
|
-
return { ok: true, worktree: wt.path, branch: wt.branch, rounds: control.steps, evidencePath: wt.path, committed: true, usedOmniCrawl: usedOmni,
|
|
1959
|
+
return { ok: true, worktree: wt.path, branch: wt.branch, rounds: control.steps, evidencePath: wt.path, committed: true, usedOmniCrawl: usedOmni,
|
|
1960
|
+
...(handoffPrSkipped ? { pr: 'skipped' as const, reason: 'origin-not-github' as const } : {}),
|
|
1961
|
+
detail: handoffPrSkipped ? '완료(증거 게이트·push·PR 생략)' : '완료(증거 게이트·PR)' };
|
|
1883
1962
|
}
|
|
1884
1963
|
const done = control.termination.kind === 'success';
|
|
1885
1964
|
const round = control.steps;
|
|
@@ -18,6 +18,7 @@ export interface MissionCliOpts {
|
|
|
18
18
|
chain?: string;
|
|
19
19
|
headless?: boolean;
|
|
20
20
|
plugin?: string;
|
|
21
|
+
resources?: 'on' | 'off';
|
|
21
22
|
evidence?: string; // 'doc' | 'tsc' | 'test'
|
|
22
23
|
docDir?: string;
|
|
23
24
|
docGlob?: string;
|
|
@@ -61,6 +62,9 @@ export async function runAgentMissionCliCommand(
|
|
|
61
62
|
if (opts.plugin !== undefined && (!plugin || !['codex', 'claude'].includes(agentBackend.name) || opts.headless)) {
|
|
62
63
|
return { ok: false, message: '--plugin 은 codex/claude 대화형 backend 에서 <name>@<market> 형식이어야 합니다', exitCode: 1 };
|
|
63
64
|
}
|
|
65
|
+
if (opts.resources !== undefined && opts.resources !== 'on' && opts.resources !== 'off') {
|
|
66
|
+
return { ok: false, message: '--resources 는 on|off 여야 합니다', exitCode: 1 };
|
|
67
|
+
}
|
|
64
68
|
|
|
65
69
|
// ── mission 텍스트(verbatim: --mission-file 우선) ──
|
|
66
70
|
const readFile = deps.readFile ?? ((p: string) => readFileSync(p, 'utf8'));
|
|
@@ -97,6 +101,8 @@ export async function runAgentMissionCliCommand(
|
|
|
97
101
|
backend: agentBackend.name,
|
|
98
102
|
...(chain ? { chain: chain as ('codex' | 'claude' | 'elanous')[] } : {}),
|
|
99
103
|
...(plugin ? { plugin } : {}),
|
|
104
|
+
resources: opts.resources ?? 'on',
|
|
105
|
+
...(opts.backend ? { backendExplicit: opts.backend } : {}),
|
|
100
106
|
...(opts.headless ? { headless: true } : {}),
|
|
101
107
|
branch: opts.branch,
|
|
102
108
|
...(opts.base ? { base: opts.base } : {}),
|
|
@@ -15,6 +15,7 @@ import { ELANOUS_MARKET_URL } from '../plugins/install/market-fetch.js';
|
|
|
15
15
|
const MARKETPLACE = 'elanous';
|
|
16
16
|
const INDEX_URL = new URL('marketplace.json', ELANOUS_MARKET_URL).href; // one source of truth for the official market
|
|
17
17
|
const MAX_NEEDS = 5;
|
|
18
|
+
const MAX_VOCABULARY = 200;
|
|
18
19
|
|
|
19
20
|
export interface ResourcePlan {
|
|
20
21
|
needs: string[];
|
|
@@ -29,11 +30,13 @@ export interface MissionResources {
|
|
|
29
30
|
readonly backend?: string;
|
|
30
31
|
readonly plugin?: string;
|
|
31
32
|
readonly resources?: 'off' | 'on';
|
|
33
|
+
/** Total planner budget in milliseconds; discovery uses only the remaining time. */
|
|
34
|
+
readonly deadlineMs?: number;
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
export interface ResourceLadderDeps extends MissionResources {
|
|
35
38
|
/** One JSON-producing LLM call; the raw mission is never sent to logs. */
|
|
36
|
-
readonly inferNeeds?: (safeMission: string) => Promise<string>;
|
|
39
|
+
readonly inferNeeds?: (safeMission: string, vocabulary: readonly string[]) => Promise<string>;
|
|
37
40
|
readonly readers?: CapabilityReaders;
|
|
38
41
|
/** Read the official index and its detached signature as a pair; no other market is consulted. */
|
|
39
42
|
readonly readOfficialIndex?: () => Promise<{ marketplaceBytes: Uint8Array; signatureText: string }>;
|
|
@@ -60,22 +63,25 @@ function safeNeed(value: unknown): string | null {
|
|
|
60
63
|
return need;
|
|
61
64
|
}
|
|
62
65
|
|
|
63
|
-
function parseNeeds(raw: string): string[] {
|
|
66
|
+
function parseNeeds(raw: string, vocabulary: readonly string[]): string[] {
|
|
64
67
|
let parsed: unknown;
|
|
65
68
|
try { parsed = JSON.parse(raw); } catch { return []; }
|
|
66
69
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed) || !Array.isArray((parsed as { needs?: unknown }).needs)) return [];
|
|
67
70
|
const needs: string[] = [];
|
|
68
71
|
for (const item of (parsed as { needs: unknown[] }).needs) {
|
|
69
|
-
const
|
|
70
|
-
|
|
72
|
+
const label = typeof item === 'string' ? vocabulary.find(value => value === item) : undefined;
|
|
73
|
+
const need = label ?? safeNeed(item);
|
|
74
|
+
if (need && !needs.some(existing => normalizeServiceName(existing) === normalizeServiceName(need))) {
|
|
75
|
+
needs.push(label ?? vocabulary.find(value => normalizeServiceName(value) === normalizeServiceName(need)) ?? need);
|
|
76
|
+
}
|
|
71
77
|
if (needs.length === MAX_NEEDS) break;
|
|
72
78
|
}
|
|
73
79
|
return needs;
|
|
74
80
|
}
|
|
75
81
|
|
|
76
|
-
async function inferNeeds(mission: string): Promise<string> {
|
|
82
|
+
async function inferNeeds(mission: string, vocabulary: readonly string[]): Promise<string> {
|
|
77
83
|
return streamLLM([
|
|
78
|
-
{ role: 'system', content:
|
|
84
|
+
{ role: 'system', content: `Identify at most five concrete tool/service/skill capabilities required for this mission. Reply ONLY with JSON {"needs":["capability"]}. No credentials, tokens, URLs, or mission text in the output. If a relevant label appears in this verified official marketplace vocabulary, use that exact label; otherwise use a concrete capability. If uncertain, return an empty list.\nVerified labels: ${JSON.stringify(vocabulary)}` },
|
|
79
85
|
{ role: 'user', content: mission },
|
|
80
86
|
], () => {}, { maxTokens: 200, temperature: 0 });
|
|
81
87
|
}
|
|
@@ -104,20 +110,60 @@ function pluginLabels(plugin: MarketplaceIndex['plugins'][number]): string[] {
|
|
|
104
110
|
return [plugin.name, plugin.description ?? '', ...plugin['ai.elanous'].capabilities, ...(plugin['ai.elanous'].vocab ?? [])];
|
|
105
111
|
}
|
|
106
112
|
|
|
113
|
+
function officialVocabulary(index: MarketplaceIndex): string[] {
|
|
114
|
+
const labels: string[] = [];
|
|
115
|
+
for (const entry of index.plugins) {
|
|
116
|
+
for (const label of [entry.name, ...entry['ai.elanous'].capabilities, ...(entry['ai.elanous'].vocab ?? [])]) {
|
|
117
|
+
const safe = typeof label === 'string' && label === label.trim() && label.length <= 80
|
|
118
|
+
&& !/[\n\r{}<>@=/\\]/.test(label) && (safeMissionText(label) === label || /^secret:[a-zA-Z0-9._-]+$/.test(label)) ? label : null;
|
|
119
|
+
if (safe && !labels.includes(safe)) labels.push(safe);
|
|
120
|
+
if (labels.length === MAX_VOCABULARY) return labels;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return labels;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function mentionedPluginNames(mission: string, index: MarketplaceIndex): string[] {
|
|
127
|
+
return index.plugins.map(entry => entry.name).filter(name => {
|
|
128
|
+
if (!safeNeed(name) || safeMissionText(name) !== name || !/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name)) return false;
|
|
129
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
130
|
+
return new RegExp(`(^|[^\\p{L}\\p{N}_.-])${escaped}(?=$|[^\\p{L}\\p{N}_.-])`, 'iu').test(mission);
|
|
131
|
+
}).slice(0, MAX_NEEDS);
|
|
132
|
+
}
|
|
133
|
+
|
|
107
134
|
/** Pre-mission resource judgment. Only the optional, injected installer can cause installation. */
|
|
108
135
|
export async function planMissionResources(mission: string, deps: ResourceLadderDeps = {}, options: MissionResources = {}): Promise<ResourcePlan> {
|
|
109
136
|
const plan: ResourcePlan = { needs: [], have: [], gaps: [], decisions: [] };
|
|
110
137
|
if (options.resources === 'off' || deps.resources === 'off') return plan;
|
|
111
138
|
const backend = options.backend ?? deps.backend;
|
|
112
139
|
const plugin = options.plugin ?? deps.plugin;
|
|
140
|
+
const budgetMs = options.deadlineMs ?? deps.deadlineMs;
|
|
141
|
+
const deadline = budgetMs === undefined ? undefined : Date.now() + budgetMs;
|
|
113
142
|
const decide = (step: string, kind: DecisionEvent['kind'], what: string, why: string) => {
|
|
114
143
|
plan.decisions.push(`${step}: ${what} — ${why}`);
|
|
115
144
|
(deps.decide ?? emitDecision)({ kind, what, reason: why, purpose: 'pre-mission resource selection', target: 'agent-mission', phase: 'dispatch' });
|
|
116
145
|
(deps.log ?? ((event, data) => debug.log('agent-mission.resources', event, data)))(step, { kind, what, why });
|
|
117
146
|
};
|
|
118
147
|
|
|
119
|
-
|
|
148
|
+
let index: MarketplaceIndex | undefined;
|
|
149
|
+
try {
|
|
150
|
+
const pair = await (deps.readOfficialIndex ?? readOfficialIndex)();
|
|
151
|
+
const checked = verifyIndex({ ...pair, trustedKeys: OFFICIAL_INDEX_KEYS });
|
|
152
|
+
if (checked.ok && checked.index.name === MARKETPLACE) index = checked.index;
|
|
153
|
+
else decide('official-index', 'ESCALATE', '공식 마켓 거부', '서명 또는 마켓 이름 검증 실패');
|
|
154
|
+
} catch { decide('official-index', 'ESCALATE', '공식 마켓 조회 실패', '서명 검증 가능한 원본 인덱스 없음'); }
|
|
155
|
+
const safeMission = safeMissionText(mission);
|
|
156
|
+
const vocabulary = index ? officialVocabulary(index) : [];
|
|
157
|
+
try { plan.needs = parseNeeds(await (deps.inferNeeds ?? inferNeeds)(safeMission, vocabulary), vocabulary); }
|
|
120
158
|
catch { decide('needs', 'ESCALATE', '능력 판정 불가', 'LLM 응답 실패 — 추측하지 않음'); }
|
|
159
|
+
const mentioned = index ? mentionedPluginNames(safeMission, index) : [];
|
|
160
|
+
for (const name of mentioned) {
|
|
161
|
+
if (!plan.needs.some(need => matches(need, [name]))) {
|
|
162
|
+
if (plan.needs.length === MAX_NEEDS) plan.needs.pop();
|
|
163
|
+
plan.needs.push(name);
|
|
164
|
+
}
|
|
165
|
+
decide('needs', 'VERIFY', name, '이름 언급');
|
|
166
|
+
}
|
|
121
167
|
decide('needs', 'VERIFY', '필요 능력 판정', `${plan.needs.length}개 확인`);
|
|
122
168
|
if (!plan.needs.length) return plan;
|
|
123
169
|
|
|
@@ -132,16 +178,7 @@ export async function planMissionResources(mission: string, deps: ResourceLadder
|
|
|
132
178
|
});
|
|
133
179
|
decide('have', 'VERIFY', 'ready 서비스 대조', `${plan.have.length}개 확인`);
|
|
134
180
|
|
|
135
|
-
|
|
136
|
-
if (remaining.length) {
|
|
137
|
-
try {
|
|
138
|
-
const pair = await (deps.readOfficialIndex ?? readOfficialIndex)();
|
|
139
|
-
const checked = verifyIndex({ ...pair, trustedKeys: OFFICIAL_INDEX_KEYS });
|
|
140
|
-
if (checked.ok && checked.index.name === MARKETPLACE) index = checked.index;
|
|
141
|
-
else decide('official-index', 'ESCALATE', '공식 마켓 거부', '서명 또는 마켓 이름 검증 실패');
|
|
142
|
-
} catch { decide('official-index', 'ESCALATE', '공식 마켓 조회 실패', '서명 검증 가능한 원본 인덱스 없음'); }
|
|
143
|
-
}
|
|
144
|
-
if (index) {
|
|
181
|
+
if (index && remaining.length) {
|
|
145
182
|
let installed: readonly { name: string; market: string }[] = [];
|
|
146
183
|
try { installed = (deps.readInstalledPlugins ?? installedPlugins)(); }
|
|
147
184
|
catch { decide('have', 'ESCALATE', '설치 원장 읽기 실패', '설치된 스킬을 보유로 간주하지 않음'); }
|
|
@@ -153,7 +190,8 @@ export async function planMissionResources(mission: string, deps: ResourceLadder
|
|
|
153
190
|
});
|
|
154
191
|
decide('have', 'VERIFY', '설치된 플러그인 스킬 대조', `${plan.have.length}개 확인`);
|
|
155
192
|
if (!plugin) {
|
|
156
|
-
const
|
|
193
|
+
const named = index.plugins.filter(entry => mentioned.includes(entry.name) && remaining.some(need => matches(need, [entry.name])));
|
|
194
|
+
const choices = named.length ? named : index.plugins.filter(entry => remaining.some(need => matches(need, pluginLabels(entry))));
|
|
157
195
|
if (choices.length === 1) {
|
|
158
196
|
plan.plugin = { plugin: choices[0]!.name, marketplace: MARKETPLACE, source: 'official-index' };
|
|
159
197
|
decide('official-index', 'ROUTE', '공식 플러그인 선택', '서명 검증된 인덱스에서 일치하는 플러그인 한 개');
|
|
@@ -169,13 +207,6 @@ export async function planMissionResources(mission: string, deps: ResourceLadder
|
|
|
169
207
|
} else decide('official-index', 'ROUTE', '명시 플러그인 보존', '호출자 선택을 덮지 않음');
|
|
170
208
|
}
|
|
171
209
|
|
|
172
|
-
for (const need of remaining) {
|
|
173
|
-
let candidates: CapabilityCandidate[] = [];
|
|
174
|
-
try { candidates = await (deps.discover ?? discoverCapability)(need, { community: false }); }
|
|
175
|
-
catch { decide('discover', 'ESCALATE', '발굴 실패', '후보 없음 — 자동 설치하지 않음'); }
|
|
176
|
-
plan.gaps.push({ need, candidates });
|
|
177
|
-
decide('discover', 'ESCALATE', '미충족 능력 후보', `${candidates.length}개 후보 제안만 함`);
|
|
178
|
-
}
|
|
179
210
|
if (!backend) {
|
|
180
211
|
const chosen = plan.needs.map(need => ({ need, name: pickBackend(matrix, need) })).find(row => row.name);
|
|
181
212
|
if (chosen?.name) {
|
|
@@ -183,5 +214,22 @@ export async function planMissionResources(mission: string, deps: ResourceLadder
|
|
|
183
214
|
decide('backend', 'ROUTE', 'ready 에이전트 선택', plan.backend.why);
|
|
184
215
|
} else decide('backend', 'ESCALATE', '에이전트 자동 선택 보류', '필요 서비스의 ready 관측 없음');
|
|
185
216
|
} else decide('backend', 'ROUTE', '명시 에이전트 보존', '호출자 backend 를 덮지 않음');
|
|
217
|
+
plan.gaps = await Promise.all(remaining.map(async need => {
|
|
218
|
+
let candidates: CapabilityCandidate[] = [];
|
|
219
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
220
|
+
const controller = new AbortController();
|
|
221
|
+
try {
|
|
222
|
+
const remainingMs = deadline === undefined ? undefined : Math.max(0, deadline - Date.now() - 50);
|
|
223
|
+
if (remainingMs === 0) throw new Error('discovery deadline');
|
|
224
|
+
const discovery = (deps.discover ?? discoverCapability)(need, { community: false, signal: controller.signal });
|
|
225
|
+
candidates = remainingMs === undefined ? await discovery : await Promise.race([
|
|
226
|
+
discovery,
|
|
227
|
+
new Promise<CapabilityCandidate[]>((_resolve, reject) => { timer = setTimeout(() => reject(new Error('discovery deadline')), remainingMs); }),
|
|
228
|
+
]);
|
|
229
|
+
} catch { controller.abort(); decide('discover', 'ESCALATE', '발굴 실패', '후보 없음 — 자동 설치하지 않음'); }
|
|
230
|
+
finally { if (timer) clearTimeout(timer); }
|
|
231
|
+
decide('discover', 'ESCALATE', '미충족 능력 후보', `${candidates.length}개 후보 제안만 함`);
|
|
232
|
+
return { need, candidates };
|
|
233
|
+
}));
|
|
186
234
|
return plan;
|
|
187
235
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { generateAuthToken } from '../acp/transport/index.js';
|
|
4
|
+
import { getElanousConfigDir } from '../elanous-config-dir.js';
|
|
5
|
+
|
|
6
|
+
/** Load the persistent ACP bearer token, creating it on first boot. */
|
|
7
|
+
export function ensureAuthToken(configDir: string = getElanousConfigDir()): { token: string; path: string } {
|
|
8
|
+
const path = join(configDir, 'acp-token');
|
|
9
|
+
mkdirSync(configDir, { recursive: true });
|
|
10
|
+
if (existsSync(path)) {
|
|
11
|
+
return { token: readFileSync(path, 'utf-8').trim(), path };
|
|
12
|
+
}
|
|
13
|
+
const token = generateAuthToken();
|
|
14
|
+
writeFileSync(path, token, { mode: 0o600 });
|
|
15
|
+
return { token, path };
|
|
16
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { createHash, createSign } from 'node:crypto';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, isAbsolute, join } from 'node:path';
|
|
5
|
+
import { getElanousConfigDir, getElanousConfigDirOverride } from '../elanous-config-dir.js';
|
|
6
|
+
import { prodInstanceRoot } from '../instance/resolve.js';
|
|
7
|
+
import { debug } from '../debug/log.js';
|
|
8
|
+
|
|
9
|
+
interface AppConfig { id: number; installation_id: number; pem: string }
|
|
10
|
+
interface TokenResponse { token?: unknown; expires_at?: unknown }
|
|
11
|
+
export interface GithubInstallationTokenScope { repository: string }
|
|
12
|
+
interface InstallationTokenRequest {
|
|
13
|
+
repositories: [string];
|
|
14
|
+
permissions: { contents: 'write'; pull_requests: 'write' };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Synchronous HTTP seam: CmdRunner must remain synchronous for existing callers. */
|
|
18
|
+
export interface GithubAutomationTokenDeps {
|
|
19
|
+
configPath?: string;
|
|
20
|
+
now?: () => number;
|
|
21
|
+
scope?: GithubInstallationTokenScope;
|
|
22
|
+
fetch?: (url: string, jwt: string, body?: InstallationTokenRequest) => TokenResponse;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const REFRESH_MARGIN_MS = 10 * 60_000;
|
|
26
|
+
const JWT_LIFETIME_SECONDS = 9 * 60;
|
|
27
|
+
let cached: { fingerprint: string; token: string; expiresAt: number } | undefined;
|
|
28
|
+
|
|
29
|
+
function requestToken(url: string, jwt: string, body?: InstallationTokenRequest): TokenResponse {
|
|
30
|
+
const response = spawnSync('curl', [
|
|
31
|
+
'--silent', '--show-error', '--fail', '--max-time', '30', '--request', 'POST',
|
|
32
|
+
'--header', 'Accept: application/vnd.github+json',
|
|
33
|
+
'--header', '@-',
|
|
34
|
+
'--header', 'X-GitHub-Api-Version: 2022-11-28',
|
|
35
|
+
...(body ? ['--header', 'Content-Type: application/json', '--data-raw', JSON.stringify(body)] : []),
|
|
36
|
+
url,
|
|
37
|
+
], { encoding: 'utf8', timeout: 35_000, input: `Authorization: Bearer ${jwt}\n`, env: process.env });
|
|
38
|
+
// Never expose curl's stderr: it may contain request credentials.
|
|
39
|
+
if (response.error || response.status !== 0) throw new Error('http-failed');
|
|
40
|
+
return JSON.parse(response.stdout) as TokenResponse;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Missing App config is a no-op; an unusable configured App falls back to the caller's gh credentials. */
|
|
44
|
+
export function githubAutomationToken(deps: GithubAutomationTokenDeps = {}): string | null {
|
|
45
|
+
return githubInstallationCredential(deps)?.token ?? null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Scoped requests always mint anew; the returned expiry comes from GitHub's response. */
|
|
49
|
+
export function githubInstallationCredential(deps: GithubAutomationTokenDeps = {}): { token: string; expires_at: string } | null {
|
|
50
|
+
let path: string;
|
|
51
|
+
// The App credential is machine-level (like `gh auth` in ~/.config/gh), not per universe: a tree-derived test
|
|
52
|
+
// universe resolves its config dir to `<tree>/.elanous-test`, where the file never is — 09-29 #21879 was inert there.
|
|
53
|
+
// Order: explicit path → this universe's config dir → the prod root `~/.elanous`.
|
|
54
|
+
try {
|
|
55
|
+
const rel = join('secrets', 'github-app', 'app.json');
|
|
56
|
+
const own = join(getElanousConfigDir(), rel);
|
|
57
|
+
// An explicit config-dir override (tests · `--config-dir`) is a deliberate boundary — never reach past it.
|
|
58
|
+
path = deps.configPath ?? (existsSync(own) || getElanousConfigDirOverride() ? own : join(prodInstanceRoot(), rel));
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
try { debug.log('auth.github-app', 'token-failed', { reason: 'config-path-failed' }); } catch { /* fail open */ }
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
let configText: string;
|
|
65
|
+
try { configText = readFileSync(path, 'utf8'); }
|
|
66
|
+
catch (error) {
|
|
67
|
+
cached = undefined;
|
|
68
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
|
|
69
|
+
try { debug.log('auth.github-app', 'token-failed', { reason: 'config-read-failed' }); } catch { /* fail open */ }
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const now = (deps.now ?? Date.now)();
|
|
74
|
+
const config = JSON.parse(configText) as AppConfig;
|
|
75
|
+
if (!config || !Number.isSafeInteger(config.id) || config.id <= 0 ||
|
|
76
|
+
!Number.isSafeInteger(config.installation_id) || config.installation_id <= 0 ||
|
|
77
|
+
typeof config.pem !== 'string' || !config.pem) throw new Error('invalid-config');
|
|
78
|
+
const pem = isAbsolute(config.pem) ? readFileSync(config.pem, 'utf8')
|
|
79
|
+
: config.pem.includes('-----BEGIN') ? config.pem
|
|
80
|
+
: readFileSync(join(dirname(path), config.pem), 'utf8');
|
|
81
|
+
const repository = deps.scope?.repository;
|
|
82
|
+
if (deps.scope && (typeof repository !== 'string' || !/^[a-zA-Z0-9_.-]+$/.test(repository) || repository === '.' || repository === '..')) {
|
|
83
|
+
throw new Error('invalid-repository');
|
|
84
|
+
}
|
|
85
|
+
const request: InstallationTokenRequest | undefined = repository
|
|
86
|
+
? { repositories: [repository], permissions: { contents: 'write', pull_requests: 'write' } }
|
|
87
|
+
: undefined;
|
|
88
|
+
const fingerprint = createHash('sha256').update(JSON.stringify([path, config.id, config.installation_id, pem])).digest('hex');
|
|
89
|
+
if (!request && cached?.fingerprint === fingerprint && now < cached.expiresAt - REFRESH_MARGIN_MS) return { token: cached.token, expires_at: new Date(cached.expiresAt).toISOString() };
|
|
90
|
+
if (!request) cached = undefined;
|
|
91
|
+
const issuedAt = Math.floor(now / 1000) - 60;
|
|
92
|
+
const encode = (value: object) => Buffer.from(JSON.stringify(value)).toString('base64url');
|
|
93
|
+
const unsigned = `${encode({ alg: 'RS256', typ: 'JWT' })}.${encode({ iat: issuedAt, exp: issuedAt + JWT_LIFETIME_SECONDS, iss: String(config.id) })}`;
|
|
94
|
+
const signer = createSign('RSA-SHA256');
|
|
95
|
+
signer.update(unsigned);
|
|
96
|
+
const jwt = `${unsigned}.${signer.sign(pem, 'base64url')}`;
|
|
97
|
+
const url = `https://api.github.com/app/installations/${config.installation_id}/access_tokens`;
|
|
98
|
+
const response = (deps.fetch ?? requestToken)(url, jwt, request);
|
|
99
|
+
const expiresAt = typeof response.expires_at === 'string' ? Date.parse(response.expires_at) : NaN;
|
|
100
|
+
if (typeof response.token !== 'string' || !response.token || !Number.isFinite(expiresAt) ||
|
|
101
|
+
expiresAt <= now + REFRESH_MARGIN_MS) throw new Error('invalid-response');
|
|
102
|
+
if (!request) cached = { fingerprint, token: response.token, expiresAt };
|
|
103
|
+
try { debug.log('auth.github-app', 'token-minted', { installationId: config.installation_id, expiresAt: new Date(expiresAt).toISOString() }); }
|
|
104
|
+
catch { /* logging must not undo a valid token */ }
|
|
105
|
+
return { token: response.token, expires_at: response.expires_at as string };
|
|
106
|
+
} catch {
|
|
107
|
+
if (!deps.scope) cached = undefined;
|
|
108
|
+
// Do not log exceptions: crypto/HTTP failures can include key or bearer material.
|
|
109
|
+
try { debug.log('auth.github-app', 'token-failed', { reason: 'mint-failed' }); } catch { /* fail open */ }
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|