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.
Files changed (412) hide show
  1. package/README.md +3 -3
  2. package/apps/pwa/out/404/index.html +1 -1
  3. package/apps/pwa/out/404.html +1 -1
  4. package/apps/pwa/out/_next/static/chunks/06ef2eba-d1b99ac6d2ea663c.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/1413-7179a49332db5a80.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/150-25c277137a9638ab.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/1640-bc99ec23bbe29d12.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/{2415.ed6428ff334c7ce2.js → 1751.50b96ea9c37b49b6.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/2300-de059d0ab2a16630.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/2684-05b0a81e75298c16.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/3026-2ed7e317d9b02102.js +25 -0
  12. package/apps/pwa/out/_next/static/chunks/3611-77496ada38cd6358.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/3717-68957c4dc9ceec90.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/3929-46ba9af6f9d895cc.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/4015-62c5a3744698a8da.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/4217-3c4317a7905f9eeb.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/4240-3230f230fae2d7d5.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/4271-e53e075ebd48593b.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/4377.3b125fef79576dfd.js +5 -0
  20. package/apps/pwa/out/_next/static/chunks/4440-18bb160d95819f4e.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/4853-35fdbe6bdbcdc0fc.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/{2ea96958-e8c7e112de50b067.js → 4daa2798-1363d725c79f3593.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/5007-6a3b30e23ff68a6e.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/5244-12b424c42070f5ff.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/{5791-bb82743fa6299562.js → 5348-bbf838e119661eee.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/5391-8793f88b03b30161.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/5611-df00af3e4e4a61bb.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/5890-dca2321773be829a.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/6123-37c9dc57ddaa7ab0.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/6588-b7975304a302ab91.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/6604-c03961962b6273b6.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/7109-cf8c87e968e83a65.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/7178-30f77877525a2efb.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/7262-871563fc9663730b.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/7294-f0eeebb92aa93459.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/7429-881af44deed6958f.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/7527.943753b6ed829f09.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/764-a01e858525a190c0.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/7868-e35d5f7985ecd3f1.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/8320-51484af7b01fdfa5.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/{4323.c5dfb6f503304775.js → 8617.465ca22a319c95b3.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/{5080-1613752891fcadb4.js → 8918-482b30ce2a8ebf4c.js} +3 -3
  43. package/apps/pwa/out/_next/static/chunks/9131-52f5fad51cceca04.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/9238.2cd5f47f4bf7a2c3.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.js → 9704-3221c102623c8d73.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-70c404c7bb6d7f0f.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/approvals/page-3ffd80166b1050aa.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f89e9a303515a033.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/board/page-4f403cd060442c95.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-9fa879ebf5959096.js → page-0ac5d053f0444d21.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/bots/page-4bcc63e272ae8205.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/chat/page-880734bd07d62882.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/control/{page-4472ddf1e2458e69.js → page-72bd384898d88955.js} +1 -1
  54. package/apps/pwa/out/_next/static/chunks/app/design-check/page-1018bbc9586ecae5.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/editor/page-de86f8c38076dd7b.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/healthz/route-70c404c7bb6d7f0f.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/intake/page-cd6447196d8bafe6.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/layout-e733630c47fa102b.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/live/page-d1c1a8357b161875.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/market/page-60c514128e81d787.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-8b637b7e325e13c1.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/morning/{page-112f7e477bda77fd.js → page-bccce0babceae30f.js} +1 -1
  63. package/apps/pwa/out/_next/static/chunks/app/not-found-63301d11dbe73836.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/observatory/page-a8cf0dc472060db2.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/page-02fba54881ccfa49.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/reflection/page-d5e8e90f0038f8de.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-b1ef6824f494466a.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/sessions/page-d589bb0f233b1009.js +1 -0
  69. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-c9bbaebef4cda059.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/settings/page-3639ce7916b92fed.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-7450ae7478955653.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/setup/layout-70c404c7bb6d7f0f.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/setup/page-b41ca4abfd618452.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/share/page-b333dd79ebee3fa9.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/showroom/page-8be1227492793655.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/tasks/page-6e03e9371b552e0d.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/term/page-911411e9fca18d03.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/trace/page-792170e649b37088.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/vault/page-c40463466937bc91.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-509c798b80384536.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/workflows/page-1cac2aa21d77c417.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/workspace/page-9626799d9b4a2cd1.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-84cd419494e16b0f.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/{8046cadf-211ca3c0e00f2b72.js → f6f3850d-7f324c6566002113.js} +1 -1
  85. package/apps/pwa/out/_next/static/chunks/framework-361487d2da603917.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/main-app-607f5beb94cb2e07.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/main-d0222d219e29dddf.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/pages/_app-a26980a31126decc.js +1 -0
  89. package/apps/pwa/out/_next/static/chunks/pages/_error-edb2b37decf23e4d.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/{webpack-e5bea36f183b25e6.js → webpack-2bf59c720fe9542c.js} +1 -1
  91. package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +1 -0
  92. package/apps/pwa/out/_next/static/{zoQX5iTvXOsIvCjxCe1Ji → ktMqfrBbf8BMGac0jqLrb}/_buildManifest.js +1 -1
  93. package/apps/pwa/out/approvals/index.html +1 -1
  94. package/apps/pwa/out/approvals/index.txt +29 -27
  95. package/apps/pwa/out/autopilot/index.html +1 -1
  96. package/apps/pwa/out/autopilot/index.txt +29 -27
  97. package/apps/pwa/out/board/index.html +1 -1
  98. package/apps/pwa/out/board/index.txt +29 -27
  99. package/apps/pwa/out/botlab/index.html +1 -1
  100. package/apps/pwa/out/botlab/index.txt +26 -24
  101. package/apps/pwa/out/bots/index.html +1 -1
  102. package/apps/pwa/out/bots/index.txt +26 -24
  103. package/apps/pwa/out/chat/index.html +1 -1
  104. package/apps/pwa/out/chat/index.txt +26 -24
  105. package/apps/pwa/out/control/index.html +1 -1
  106. package/apps/pwa/out/control/index.txt +29 -27
  107. package/apps/pwa/out/design-check/index.html +1 -1
  108. package/apps/pwa/out/design-check/index.txt +29 -27
  109. package/apps/pwa/out/editor/index.html +1 -1
  110. package/apps/pwa/out/editor/index.txt +29 -27
  111. package/apps/pwa/out/healthz +1 -1
  112. package/apps/pwa/out/icons/elanous-16.png +0 -0
  113. package/apps/pwa/out/icons/elanous-180.png +0 -0
  114. package/apps/pwa/out/icons/elanous-192.png +0 -0
  115. package/apps/pwa/out/icons/elanous-32.png +0 -0
  116. package/apps/pwa/out/icons/elanous-512.png +0 -0
  117. package/apps/pwa/out/index.html +1 -1
  118. package/apps/pwa/out/index.txt +26 -24
  119. package/apps/pwa/out/intake/index.html +1 -1
  120. package/apps/pwa/out/intake/index.txt +29 -27
  121. package/apps/pwa/out/live/index.html +1 -1
  122. package/apps/pwa/out/live/index.txt +29 -27
  123. package/apps/pwa/out/manifest.webmanifest +5 -2
  124. package/apps/pwa/out/market/index.html +1 -1
  125. package/apps/pwa/out/market/index.txt +26 -24
  126. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  127. package/apps/pwa/out/missions/placeholder/index.txt +26 -24
  128. package/apps/pwa/out/morning/index.html +1 -1
  129. package/apps/pwa/out/morning/index.txt +29 -27
  130. package/apps/pwa/out/observatory/index.html +1 -1
  131. package/apps/pwa/out/observatory/index.txt +29 -27
  132. package/apps/pwa/out/reflection/index.html +1 -1
  133. package/apps/pwa/out/reflection/index.txt +27 -25
  134. package/apps/pwa/out/scheduler/index.html +1 -1
  135. package/apps/pwa/out/scheduler/index.txt +29 -27
  136. package/apps/pwa/out/sessions/index.html +1 -1
  137. package/apps/pwa/out/sessions/index.txt +26 -24
  138. package/apps/pwa/out/settings/devices/index.html +1 -1
  139. package/apps/pwa/out/settings/devices/index.txt +29 -27
  140. package/apps/pwa/out/settings/index.html +1 -1
  141. package/apps/pwa/out/settings/index.txt +29 -27
  142. package/apps/pwa/out/setup/done/index.html +1 -1
  143. package/apps/pwa/out/setup/done/index.txt +29 -27
  144. package/apps/pwa/out/setup/index.html +1 -1
  145. package/apps/pwa/out/setup/index.txt +29 -27
  146. package/apps/pwa/out/share/index.html +1 -1
  147. package/apps/pwa/out/share/index.txt +29 -27
  148. package/apps/pwa/out/showroom/index.html +1 -1
  149. package/apps/pwa/out/showroom/index.txt +26 -24
  150. package/apps/pwa/out/sw-strategies.js +44 -0
  151. package/apps/pwa/out/sw.js +6 -44
  152. package/apps/pwa/out/tasks/index.html +1 -1
  153. package/apps/pwa/out/tasks/index.txt +29 -27
  154. package/apps/pwa/out/term/index.html +1 -1
  155. package/apps/pwa/out/term/index.txt +29 -27
  156. package/apps/pwa/out/trace/index.html +1 -1
  157. package/apps/pwa/out/trace/index.txt +29 -27
  158. package/apps/pwa/out/vault/index.html +1 -1
  159. package/apps/pwa/out/vault/index.txt +29 -27
  160. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  161. package/apps/pwa/out/workflows/chat-ui/index.txt +29 -27
  162. package/apps/pwa/out/workflows/index.html +1 -1
  163. package/apps/pwa/out/workflows/index.txt +29 -27
  164. package/apps/pwa/out/workspace/index.html +1 -1
  165. package/apps/pwa/out/workspace/index.txt +29 -27
  166. package/apps/pwa/out/worktrees/index.html +1 -1
  167. package/apps/pwa/out/worktrees/index.txt +29 -27
  168. package/catalog/resources.yaml +6 -6
  169. package/docs/goal-context/steward-directives-2026-09-28.md +35 -37
  170. package/graphs/landing/landing-heal.yaml +1 -1
  171. package/graphs/release/recipes.yaml +8 -1
  172. package/graphs/release/release-loop.yaml +6 -1
  173. package/graphs/video/README.md +1 -0
  174. package/graphs/video/explainer-line.yaml +58 -0
  175. package/package.json +3 -1
  176. package/plugins/job-coach/.codex-plugin/plugin.json +18 -1
  177. package/plugins/job-coach/README.md +12 -0
  178. package/plugins/job-coach/connectors/ncs/server.ts +4 -3
  179. package/plugins/job-coach/graphs/gap-judge.ts +44 -0
  180. package/plugins/job-coach/graphs/recipes.yaml +27 -0
  181. package/plugins/job-coach/graphs/report-enterprise.yaml +28 -0
  182. package/plugins/job-coach/graphs/run-step.ts +211 -17
  183. package/plugins/job-coach/plugin.json +16 -3
  184. package/plugins/job-coach/skills/enterprise-ai-fit/SKILL.md +8 -0
  185. package/plugins/job-coach/skills/enterprise-competency/SKILL.md +8 -0
  186. package/plugins/job-coach/skills/enterprise-needs/SKILL.md +8 -0
  187. package/plugins/job-coach/skills/enterprise-roadmap/SKILL.md +8 -0
  188. package/plugins/job-coach/skills/enterprise-task-analysis/SKILL.md +8 -0
  189. package/plugins/job-coach/skills/interview-to-profile/SKILL.md +18 -1
  190. package/scripts/backup/elanous-backup.sh +4 -1
  191. package/scripts/build-docs-site.ts +4 -3
  192. package/scripts/coord-channel-watch.sh +23 -4
  193. package/scripts/coord-post.sh +49 -12
  194. package/scripts/coord-tracks.json +5 -5
  195. package/scripts/eval-bot-vs-grok.ts +79 -0
  196. package/scripts/eval-grok-live-baseline.ts +36 -0
  197. package/scripts/fixtures/front-route-corpus.sample.json +17 -0
  198. package/scripts/fleet/node-setup.sh +46 -7
  199. package/scripts/landing-heal/must-fix-match.ts +26 -0
  200. package/scripts/landing-heal/run-mission.ts +114 -20
  201. package/scripts/lib/front-route-measurement.ts +131 -0
  202. package/scripts/lib/tui-sim-keys.ts +42 -0
  203. package/scripts/measure-front-route-classifier.ts +56 -0
  204. package/scripts/media/ocr-leak-scan.sh +21 -0
  205. package/scripts/public-export.ts +2 -3
  206. package/scripts/public-import-pr.ts +80 -6
  207. package/scripts/publish-internal-dist.ts +2 -3
  208. package/scripts/red-sweep-diff.ts +1 -1
  209. package/scripts/release-loop/auto-approve-node.ts +72 -0
  210. package/scripts/release-loop/gate-node.ts +332 -44
  211. package/scripts/release-loop/publish-node.ts +7 -3
  212. package/scripts/release-loop/release-docs.ts +20 -13
  213. package/scripts/release-loop/test-durations.ts +63 -0
  214. package/scripts/release-loop/upgrade-node.ts +3 -2
  215. package/scripts/test-audit-coverage-per-file.sh +16 -0
  216. package/scripts/test-audit-meaningless.ts +88 -0
  217. package/scripts/test-audit-overlap-and-weak.ts +73 -0
  218. package/scripts/test-deterministic.ts +27 -5
  219. package/scripts/tui-sim.ts +59 -14
  220. package/scripts/ux/bump-installed.sh +20 -0
  221. package/scripts/ux/lib/cdp.ts +72 -0
  222. package/scripts/ux/record-page.ts +50 -0
  223. package/skills/explainer-video/SKILL.md +99 -0
  224. package/skills/explainer-video/engine/brand/elanous-mark-on-dark.svg +1 -0
  225. package/skills/explainer-video/engine/build.mjs +431 -0
  226. package/skills/explainer-video/engine/vo.ts +37 -0
  227. package/skills/explainer-video/examples/elanous-0.2.5/script.json +155 -0
  228. package/src/agent/nested-agent-env.ts +1 -0
  229. package/src/agent-mission/driver.ts +95 -16
  230. package/src/agent-mission/mission-cli.ts +6 -0
  231. package/src/agent-mission/resource-ladder.ts +73 -25
  232. package/src/auth/acp-token.ts +16 -0
  233. package/src/auth/github-app-token.ts +112 -0
  234. package/src/autopilot/gh-rest-fallback.ts +177 -0
  235. package/src/autopilot/pr-manager.ts +51 -4
  236. package/src/boot/acp-server.ts +2 -16
  237. package/src/boot/daemon-tools/web-search.ts +2 -1
  238. package/src/boot/ws-bridge.ts +11 -0
  239. package/src/chat/index.ts +37 -8
  240. package/src/chat/log-scroll-key.ts +12 -0
  241. package/src/chat/submit-trace.ts +30 -0
  242. package/src/cli/agent-cli.ts +7 -2
  243. package/src/cli/decisions-cli.ts +94 -0
  244. package/src/cli/doctor-cli.ts +40 -54
  245. package/src/cli/doctor-readiness.ts +16 -24
  246. package/src/cli/intake-cli.ts +1 -1
  247. package/src/cli/mcp-call.ts +9 -0
  248. package/src/cli/mcp-gateway-cli.ts +7 -3
  249. package/src/cli/plugin-cli.ts +109 -11
  250. package/src/cli/pr-lineage.ts +2 -1
  251. package/src/cli/pwa-start.ts +3 -0
  252. package/src/cli/release-cli.ts +94 -7
  253. package/src/cli/self-update.ts +7 -4
  254. package/src/cli/start-cli.ts +33 -14
  255. package/src/dashboard/acp-boot.ts +3 -0
  256. package/src/dashboard/index.ts +62 -13
  257. package/src/dashboard/input/no-provider-reply.ts +13 -0
  258. package/src/dashboard/slash-runtime/unknown-slash-reply.ts +78 -0
  259. package/src/dashboard/stale-install-check.ts +77 -0
  260. package/src/decisions/decision-ledger.ts +404 -0
  261. package/src/domains/outbound-alert.ts +8 -2
  262. package/src/domains/telegram-kind-route.ts +21 -0
  263. package/src/execution-loop/dispatch-task.ts +139 -0
  264. package/src/execution-loop/gate-decision-summary.ts +29 -0
  265. package/src/execution-loop/gates.ts +197 -0
  266. package/src/graph-kinds/installed-plugin-nodes.ts +95 -0
  267. package/src/graph-kinds/plugin-nodes.ts +10 -2
  268. package/src/graph-runner/runner.ts +35 -5
  269. package/src/grounding/token.ts +12 -6
  270. package/src/harness/harness-cli-command.ts +68 -9
  271. package/src/harness/harness-pod-dispatch.ts +8 -2
  272. package/src/harness/harness-stop.ts +147 -18
  273. package/src/harness/self-send-decide.ts +26 -17
  274. package/src/harness/self-send-target.ts +22 -1
  275. package/src/harness/worktree-branch-prefix.ts +4 -3
  276. package/src/index.ts +56 -13
  277. package/src/instance/resolve.ts +6 -0
  278. package/src/intake-plane/collect-telegram-saved.ts +5 -0
  279. package/src/intake-plane/digest.ts +11 -2
  280. package/src/live/detail-switch.ts +8 -3
  281. package/src/llm/usable-llm.ts +32 -0
  282. package/src/loops/registry.ts +27 -5
  283. package/src/market/publish.ts +59 -3
  284. package/src/native-tool-catalog.ts +2 -2
  285. package/src/nexus/api/graph-approvals.ts +125 -0
  286. package/src/nexus/api/http-server.ts +60 -6
  287. package/src/nexus/api/log-fabric.ts +10 -1
  288. package/src/nexus/api/mcp-oauth-hint.ts +10 -9
  289. package/src/nexus/api/merge-approvals.ts +2 -1
  290. package/src/nexus/api/meta-api.ts +14 -0
  291. package/src/nexus/api/plugins-credentials-api.ts +47 -0
  292. package/src/nexus/api/plugins-market.ts +87 -3
  293. package/src/nexus/api/pod-credential-api.ts +68 -14
  294. package/src/nexus/api/public-routes.ts +7 -0
  295. package/src/nexus/api/trace.ts +57 -6
  296. package/src/nexus/boot/register-mcp-clients.ts +99 -14
  297. package/src/nexus/index.ts +33 -32
  298. package/src/nexus/outbound/router.ts +1 -1
  299. package/src/onboarding.ts +25 -2
  300. package/src/plugins/core/host.ts +14 -0
  301. package/src/plugins/install/market-fetch.ts +4 -3
  302. package/src/plugins/install/plugin-credentials.ts +106 -0
  303. package/src/plugins/install/plugin-install.ts +163 -32
  304. package/src/prompt-library/universal-preamble.ts +8 -2
  305. package/src/release-loop/checklist.ts +191 -0
  306. package/src/release-loop/release-note.ts +6 -3
  307. package/src/self-dev/dev-cli.ts +40 -20
  308. package/src/self-dev/dev-pipeline.ts +77 -1
  309. package/src/self-implement/gate-baseline.ts +3 -1
  310. package/src/self-implement/goal-author.ts +18 -1
  311. package/src/self-implement/host-regate.ts +6 -2
  312. package/src/self-implement/orchestrator.ts +7 -2
  313. package/src/self-implement/seams.ts +4 -2
  314. package/src/skills/tools/dev-harness.ts +42 -0
  315. package/src/steward/steward-cards.ts +45 -0
  316. package/src/steward/triage.ts +50 -14
  317. package/src/task-orchestrator/surfaces/pod-artifact-return.ts +61 -3
  318. package/src/task-orchestrator/surfaces/pod-command-job.ts +27 -6
  319. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +39 -9
  320. package/src/task-orchestrator/surfaces/pod-source-bundle.ts +16 -1
  321. package/src/task-orchestrator/surfaces/pod-source-receive.ts +51 -13
  322. package/src/task-orchestrator/surfaces/self-implement-pod.ts +172 -28
  323. package/src/telegram-report.ts +12 -6
  324. package/src/tui-client/acp-transport-ws-client.ts +72 -5
  325. package/src/tui.ts +3 -2
  326. package/src/user-config.ts +28 -2
  327. package/src/util/spawn-sync-output.ts +15 -0
  328. package/src/version/code-revision.ts +1 -1
  329. package/src/version/packed-revision.json +1 -1
  330. package/src/web-search/free.ts +77 -0
  331. package/src/web-search/index.ts +2 -0
  332. package/src/workflow-runtime/deps-bridge.ts +4 -3
  333. package/src/workflow-runtime/plugin-kind-node.ts +32 -4
  334. package/src/workflow-runtime/types.ts +2 -1
  335. package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/1191-7b13a24117a689fa.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/1377-1efeffbbac0963ff.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/1470.98a6ab61ccb4131c.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/2375-3fcae5186c64fe6b.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/2977-e38442e86289f1d3.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/2982-2a2df543be3d2e5a.js +0 -25
  346. package/apps/pwa/out/_next/static/chunks/319-d37df1b6e0cbeff4.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +0 -5
  348. package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +0 -1
  349. package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +0 -1
  350. package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +0 -1
  351. package/apps/pwa/out/_next/static/chunks/4836-3c93795ab834381e.js +0 -1
  352. package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +0 -1
  353. package/apps/pwa/out/_next/static/chunks/6090-1bd6077591603947.js +0 -1
  354. package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.js +0 -1
  355. package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +0 -1
  356. package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +0 -1
  357. package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +0 -1
  358. package/apps/pwa/out/_next/static/chunks/7457-f5cb1cc8db9f7453.js +0 -1
  359. package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.js +0 -1
  360. package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +0 -1
  361. package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +0 -1
  362. package/apps/pwa/out/_next/static/chunks/8067-f74bf14d67ad98f1.js +0 -1
  363. package/apps/pwa/out/_next/static/chunks/8142-011e1f474efb8b33.js +0 -1
  364. package/apps/pwa/out/_next/static/chunks/8519-0059d82bdb4812bf.js +0 -1
  365. package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +0 -1
  366. package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +0 -1
  367. package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +0 -1
  368. package/apps/pwa/out/_next/static/chunks/9740-52467f244755917f.js +0 -1
  369. package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.js +0 -1
  370. package/apps/pwa/out/_next/static/chunks/ae787e4c-f2998de7f1ba72a4.js +0 -1
  371. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +0 -1
  372. package/apps/pwa/out/_next/static/chunks/app/approvals/page-21c3261af6ab377c.js +0 -1
  373. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-1991445b8ea86274.js +0 -1
  374. package/apps/pwa/out/_next/static/chunks/app/board/page-4ebfaee2369fe248.js +0 -1
  375. package/apps/pwa/out/_next/static/chunks/app/bots/page-ca7c3cc305c3285a.js +0 -1
  376. package/apps/pwa/out/_next/static/chunks/app/chat/page-9951930add3d2051.js +0 -1
  377. package/apps/pwa/out/_next/static/chunks/app/design-check/page-4de08d4b766f134b.js +0 -1
  378. package/apps/pwa/out/_next/static/chunks/app/editor/page-7a975e2b9db1dfb7.js +0 -1
  379. package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +0 -1
  380. package/apps/pwa/out/_next/static/chunks/app/intake/page-49270714ef35bb96.js +0 -1
  381. package/apps/pwa/out/_next/static/chunks/app/layout-33fbdf353e2a5982.js +0 -1
  382. package/apps/pwa/out/_next/static/chunks/app/live/page-259f122e2dd64317.js +0 -1
  383. package/apps/pwa/out/_next/static/chunks/app/market/page-65b82726753658e1.js +0 -1
  384. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-4b8df3748dc06285.js +0 -1
  385. package/apps/pwa/out/_next/static/chunks/app/not-found-542a5c0006da637c.js +0 -1
  386. package/apps/pwa/out/_next/static/chunks/app/observatory/page-63ffb347831262ea.js +0 -1
  387. package/apps/pwa/out/_next/static/chunks/app/page-98e4e359d97263b4.js +0 -1
  388. package/apps/pwa/out/_next/static/chunks/app/reflection/page-97e24ac675ce576e.js +0 -1
  389. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-ac6391f26391d89d.js +0 -1
  390. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9f45c9d45ffc43bd.js +0 -1
  391. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-36995b9cb5d98218.js +0 -1
  392. package/apps/pwa/out/_next/static/chunks/app/settings/page-0c795b577988d63f.js +0 -1
  393. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b4311e2f71c6735a.js +0 -1
  394. package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +0 -1
  395. package/apps/pwa/out/_next/static/chunks/app/setup/page-eb2381074df20dba.js +0 -1
  396. package/apps/pwa/out/_next/static/chunks/app/share/page-b01336e64eba01ae.js +0 -1
  397. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7af7f21c1f2858ef.js +0 -1
  398. package/apps/pwa/out/_next/static/chunks/app/tasks/page-a98ecd364586c89b.js +0 -1
  399. package/apps/pwa/out/_next/static/chunks/app/term/page-57f0502dfbfa089b.js +0 -1
  400. package/apps/pwa/out/_next/static/chunks/app/trace/page-3467d2e1abd8a14a.js +0 -1
  401. package/apps/pwa/out/_next/static/chunks/app/vault/page-7842ca23d53ba682.js +0 -1
  402. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-ae9ac882912b0198.js +0 -1
  403. package/apps/pwa/out/_next/static/chunks/app/workflows/page-3a0d72e3f783bcef.js +0 -1
  404. package/apps/pwa/out/_next/static/chunks/app/workspace/page-f3b39fe8358c2a97.js +0 -1
  405. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-5a22af3181ac2afb.js +0 -1
  406. package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +0 -1
  407. package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +0 -1
  408. package/apps/pwa/out/_next/static/chunks/main-app-1457627bb302a22c.js +0 -1
  409. package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +0 -1
  410. package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +0 -1
  411. package/apps/pwa/out/_next/static/css/e66b7f5efc8df3f3.css +0 -1
  412. /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
- if (spec.plugin) {
1648
- const plugin = await (deps.installPlugin ?? installPluginInsideAgent)(
1649
- { agent: backend.name as 'codex' | 'claude', ...spec.plugin }, h,
1650
- { ...(backend.handleTrust ? { handleTrust: backend.handleTrust } : {}), ...deps.pluginDeps, write: drive },
1651
- );
1652
- if (plugin.outcome !== 'installed') {
1653
- stopLive();
1654
- try { h.kill(); } catch { /* already exited */ }
1655
- return { ok: false, worktree: wt.path, branch: wt.branch, rounds: 0, evidencePath: null,
1656
- committed: false, usedOmniCrawl: false, detail: `플러그인 설치 ESCALATE (${plugin.reason}) — 미션은 전송하지 않았다` };
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 (spec.plugin) missionText = `${missionText}\n\n설치된 ${spec.plugin.plugin} 플러그인에서 실제 스킬을 확인하고 ${spec.plugin.plugin}:<skill> 형식으로 해당 스킬을 사용하라.`;
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 { dispatchOpenPullRequest } = await import('../tool-runtime/git-pr-runtime.js');
1772
- dispatchOpenPullRequest({ title: `agent-mission: ${spec.branch}`, body: deps.originalMission ?? spec.mission, head: spec.branch, base: spec.base ?? 'main' }, { cwd: worktree });
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, detail: '완료(증거 게이트·PR)' };
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 need = safeNeed(item);
70
- if (need && !needs.some(existing => normalizeServiceName(existing) === normalizeServiceName(need))) needs.push(need);
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: '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 uncertain, return an empty list.' },
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
- try { plan.needs = parseNeeds(await (deps.inferNeeds ?? inferNeeds)(safeMissionText(mission))); }
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
- let index: MarketplaceIndex | undefined;
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 choices = index.plugins.filter(entry => remaining.some(need => matches(need, pluginLabels(entry))));
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
+ }