elanous 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (455) hide show
  1. package/README.md +7 -6
  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/1169-696263ac7af8c4d4.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/1883.c91a2648af4842fd.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/1947-9d64872eeb9cfdec.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/2808-dc06482994b40f70.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/{5080-1613752891fcadb4.js → 2984-977cfa339fcd65a5.js} +3 -3
  9. package/apps/pwa/out/_next/static/chunks/3165-95515ca5f299a120.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/{8142-011e1f474efb8b33.js → 3190-e9282753e7ebde85.js} +1 -1
  11. package/apps/pwa/out/_next/static/chunks/3995-bee512a7063036ce.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.js → 4240-7153881b0b377306.js} +1 -1
  13. package/apps/pwa/out/_next/static/chunks/4259-99614149577e77cb.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/4283-329eabeaf233e6cd.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/4328-cb5058b1acc2de5e.js +25 -0
  16. package/apps/pwa/out/_next/static/chunks/4801-ecee6ef62b349b18.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/5110-8058791dd25a2e14.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/{3403.4bc97b4066945a94.js → 5329.8b7e4bca684b6eec.js} +2 -2
  19. package/apps/pwa/out/_next/static/chunks/5330-c41e9670f594c38f.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/5423-e95fd125d1794349.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/5478-b5eae07dc79ea619.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/5787-96ee6642b9a661e5.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/6448-79d72368589f98bc.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/6522-9866c002b5531836.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/6729-b4a558b7aa891e0c.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/6969-cb03f9410b7d2e03.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/7007-3b9861091bb8dd21.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/{5791-bb82743fa6299562.js → 763-7d0e1913b5074e98.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/{4323.c5dfb6f503304775.js → 7633.0654fe6ea259c1ee.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/7930-8dc7e20fc6fc19ca.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/8071-187edbc22a86741f.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/8198-d4101ad70cb9a579.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/8250.4a2c5553a07cdf47.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/8293-123b91e7b0330dc0.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/8296-d6d5d87dfa911d41.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/8305-603963c33ee8eec3.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/8612-4a0066f20951b8fd.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/{2ea96958-e8c7e112de50b067.js → 8d0d9faf-97887ff99713b84e.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/9181-cf4e1b61c4783f92.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/{8046cadf-211ca3c0e00f2b72.js → 91ff6dda-3b33d42d20657429.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/9262-26c2ecbd5a67e4a2.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/9328-45abdebce4fbecb7.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/9355-dbc67205adf8745f.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/9763.255b90dd69875689.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/9933-66d31ec25c0231af.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-bd06fb55f4da0381.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/approvals/page-4e586142da455a25.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-889ad5cddcf45b0d.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/board/page-47ba401d56572c27.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/botlab/page-0b54f9c83458e038.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/bots/page-4fcc296ef6c8ed90.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/chat/page-fe9fda494a87f4e5.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/control/page-f9642e25ce129590.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/design-check/page-e1063e8ba5f3e2e7.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/editor/page-9d0df30027d8aa9d.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/healthz/route-bd06fb55f4da0381.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/intake/page-d97adf2c9455b8d0.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/layout-236b80d202110362.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/live/page-21a30f35f1a276ad.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/market/page-b1a5be357c4fa25d.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-1705a60303f7f573.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/morning/page-76b337c11ad0e237.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/not-found-7c0f85fbb56f64c6.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/observatory/page-65ca16b20617b44d.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/page-e7f9b8fc29411c1d.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/reflection/page-92ea6213d1aeec04.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a913a005aacb5476.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/sessions/page-2d78804a40dfdef6.js +1 -0
  69. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-08a99148e780ac00.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/settings/page-deb674e7ed36f815.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-49f8de0534d192d7.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/setup/layout-bd06fb55f4da0381.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/setup/page-ba162a28d2176e18.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/share/page-1ff1a37b550ab41a.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/showroom/page-ccaa698f56c0fbdb.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/tasks/page-53b944082de1d7db.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/term/page-9b70adf1552789bf.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/trace/page-30d482aa63241fe8.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/vault/page-aecf786f54651d8a.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-6ab9b62c7b6e08f2.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/workflows/page-08949735455e0c9a.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/workspace/page-a1479005234a46c2.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3890e90bd232732.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/beb5cf10-9adcc10b83930090.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/framework-d68d41c88e05c801.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/main-app-11c63971b7a9593b.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/main-d09452353bd0d9cc.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/pages/_app-184d727cebeca767.js +1 -0
  89. package/apps/pwa/out/_next/static/chunks/pages/_error-21bbbbac99b720b1.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/{webpack-e5bea36f183b25e6.js → webpack-37b8bf78a3c6eec5.js} +1 -1
  91. package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +1 -0
  92. package/apps/pwa/out/_next/static/{zoQX5iTvXOsIvCjxCe1Ji → qMI0jnlZaBjjm4aIDYNGM}/_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 +11 -1
  172. package/graphs/release/release-loop.yaml +9 -2
  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/card-followup/README.md +33 -0
  177. package/plugins/card-followup/examples/card-sample.json +7 -0
  178. package/plugins/card-followup/graphs/card-followup.yaml +16 -0
  179. package/plugins/card-followup/graphs/recipes.yaml +12 -0
  180. package/plugins/card-followup/graphs/run-step.ts +172 -0
  181. package/plugins/card-followup/plugin.json +25 -0
  182. package/plugins/geo-check/README.md +13 -0
  183. package/plugins/geo-check/examples/brand-sample.json +8 -0
  184. package/plugins/geo-check/graphs/geo-check.yaml +20 -0
  185. package/plugins/geo-check/graphs/recipes.yaml +18 -0
  186. package/plugins/geo-check/graphs/run-step.ts +177 -0
  187. package/plugins/geo-check/plugin.json +24 -0
  188. package/plugins/job-coach/.codex-plugin/plugin.json +18 -1
  189. package/plugins/job-coach/README.md +42 -0
  190. package/plugins/job-coach/connectors/ncs/server.ts +4 -3
  191. package/plugins/job-coach/graphs/gap-judge.ts +44 -0
  192. package/plugins/job-coach/graphs/recipes.yaml +27 -0
  193. package/plugins/job-coach/graphs/report-enterprise.yaml +28 -0
  194. package/plugins/job-coach/graphs/run-step.ts +211 -17
  195. package/plugins/job-coach/plugin.json +16 -3
  196. package/plugins/job-coach/skills/enterprise-ai-fit/SKILL.md +8 -0
  197. package/plugins/job-coach/skills/enterprise-competency/SKILL.md +8 -0
  198. package/plugins/job-coach/skills/enterprise-needs/SKILL.md +8 -0
  199. package/plugins/job-coach/skills/enterprise-roadmap/SKILL.md +8 -0
  200. package/plugins/job-coach/skills/enterprise-task-analysis/SKILL.md +8 -0
  201. package/plugins/job-coach/skills/interview-to-profile/SKILL.md +18 -1
  202. package/scripts/backup/elanous-backup.sh +4 -1
  203. package/scripts/build-docs-site.ts +4 -3
  204. package/scripts/coord-channel-watch.sh +23 -4
  205. package/scripts/coord-post.sh +49 -12
  206. package/scripts/coord-tracks.json +5 -5
  207. package/scripts/daemon-port-baseline.txt +1 -2
  208. package/scripts/eval-bot-vs-grok.ts +79 -0
  209. package/scripts/eval-grok-live-baseline.ts +36 -0
  210. package/scripts/fixtures/front-route-corpus.sample.json +17 -0
  211. package/scripts/fleet/node-setup.sh +46 -7
  212. package/scripts/landing-heal/must-fix-match.ts +26 -0
  213. package/scripts/landing-heal/run-mission.ts +114 -20
  214. package/scripts/lib/android-test-device.ts +32 -0
  215. package/scripts/lib/front-route-measurement.ts +131 -0
  216. package/scripts/lib/tui-sim-keys.ts +42 -0
  217. package/scripts/measure-front-route-classifier.ts +56 -0
  218. package/scripts/media/ocr-leak-scan.sh +21 -0
  219. package/scripts/public-export.ts +2 -3
  220. package/scripts/public-import-pr.ts +80 -6
  221. package/scripts/publish-internal-dist.ts +2 -3
  222. package/scripts/red-sweep-diff.ts +1 -1
  223. package/scripts/release-loop/auto-approve-node.ts +72 -0
  224. package/scripts/release-loop/checklist-gate-node.ts +35 -0
  225. package/scripts/release-loop/gate-diff.ts +29 -0
  226. package/scripts/release-loop/gate-node.ts +366 -45
  227. package/scripts/release-loop/publish-node.ts +7 -3
  228. package/scripts/release-loop/release-docs.ts +20 -13
  229. package/scripts/release-loop/shard-plan.ts +47 -0
  230. package/scripts/release-loop/test-durations.ts +63 -0
  231. package/scripts/release-loop/upgrade-node.ts +3 -2
  232. package/scripts/test-audit-coverage-per-file.sh +16 -0
  233. package/scripts/test-audit-meaningless.ts +88 -0
  234. package/scripts/test-audit-overlap-and-weak.ts +73 -0
  235. package/scripts/test-deterministic.ts +27 -5
  236. package/scripts/tui-sim.ts +59 -14
  237. package/scripts/ux/bump-installed.sh +20 -0
  238. package/scripts/ux/lib/cdp.ts +72 -0
  239. package/scripts/ux/record-page.ts +53 -0
  240. package/skills/explainer-video/SKILL.md +117 -0
  241. package/skills/explainer-video/engine/brand/elanous-mark-on-dark.svg +1 -0
  242. package/skills/explainer-video/engine/build.mjs +431 -0
  243. package/skills/explainer-video/engine/reel.mjs +128 -0
  244. package/skills/explainer-video/engine/reel.sh +8 -0
  245. package/skills/explainer-video/engine/vo.ts +79 -0
  246. package/skills/explainer-video/examples/elanous-0.2.5/script.json +155 -0
  247. package/src/agent/nested-agent-env.ts +1 -0
  248. package/src/agent-mission/driver.ts +95 -16
  249. package/src/agent-mission/mission-cli.ts +6 -0
  250. package/src/agent-mission/resource-ladder.ts +73 -25
  251. package/src/auth/acp-token.ts +16 -0
  252. package/src/auth/github-app-token.ts +112 -0
  253. package/src/autopilot/gh-rest-fallback.ts +177 -0
  254. package/src/autopilot/mission-origin.ts +20 -4
  255. package/src/autopilot/mission-phase-track.ts +11 -1
  256. package/src/autopilot/pr-manager.ts +51 -4
  257. package/src/autopilot/track-agent.ts +3 -3
  258. package/src/autopilot/track-courier.ts +2 -2
  259. package/src/boot/acp-server.ts +2 -16
  260. package/src/boot/daemon-tools/web-search.ts +2 -1
  261. package/src/boot/ws-bridge.ts +11 -0
  262. package/src/chat/index.ts +37 -8
  263. package/src/chat/log-scroll-key.ts +12 -0
  264. package/src/chat/submit-trace.ts +30 -0
  265. package/src/cli/agent-cli.ts +7 -2
  266. package/src/cli/decisions-cli.ts +94 -0
  267. package/src/cli/doctor-cli.ts +40 -54
  268. package/src/cli/doctor-readiness.ts +16 -24
  269. package/src/cli/intake-cli.ts +1 -1
  270. package/src/cli/mcp-call.ts +9 -0
  271. package/src/cli/mcp-gateway-cli.ts +7 -3
  272. package/src/cli/nexus-ios-bind.ts +25 -8
  273. package/src/cli/phone-link.ts +117 -0
  274. package/src/cli/plugin-cli.ts +136 -11
  275. package/src/cli/pr-lineage.ts +2 -1
  276. package/src/cli/private-ledger-commands.ts +31 -0
  277. package/src/cli/pwa-start.ts +3 -0
  278. package/src/cli/release-cli.ts +97 -7
  279. package/src/cli/self-update.ts +7 -4
  280. package/src/cli/start-cli.ts +33 -14
  281. package/src/dashboard/acp-boot.ts +3 -0
  282. package/src/dashboard/index.ts +62 -13
  283. package/src/dashboard/input/no-provider-reply.ts +13 -0
  284. package/src/dashboard/slash-runtime/unknown-slash-reply.ts +78 -0
  285. package/src/dashboard/stale-install-check.ts +77 -0
  286. package/src/decisions/decision-ledger.ts +404 -0
  287. package/src/domains/outbound-alert.ts +13 -7
  288. package/src/domains/telegram-kind-route.ts +21 -0
  289. package/src/execution-loop/dispatch-task.ts +139 -0
  290. package/src/execution-loop/gate-decision-summary.ts +29 -0
  291. package/src/execution-loop/gates.ts +197 -0
  292. package/src/field/field-media.ts +245 -0
  293. package/src/graph-kinds/installed-plugin-nodes.ts +95 -0
  294. package/src/graph-kinds/plugin-nodes.ts +10 -2
  295. package/src/graph-runner/runner.ts +35 -5
  296. package/src/grounding/token.ts +12 -6
  297. package/src/harness/harness-cli-command.ts +68 -9
  298. package/src/harness/harness-pod-dispatch.ts +8 -2
  299. package/src/harness/harness-stop.ts +147 -18
  300. package/src/harness/self-send-decide.ts +26 -17
  301. package/src/harness/self-send-target.ts +22 -1
  302. package/src/harness/worktree-branch-prefix.ts +4 -3
  303. package/src/index.ts +50 -13
  304. package/src/instance/resolve.ts +6 -0
  305. package/src/intake-plane/collect-telegram-saved.ts +5 -0
  306. package/src/intake-plane/digest.ts +11 -2
  307. package/src/live/detail-switch.ts +8 -3
  308. package/src/llm/usable-llm.ts +32 -0
  309. package/src/loops/registry.ts +27 -5
  310. package/src/market/publish.ts +59 -3
  311. package/src/native-tool-catalog.ts +2 -2
  312. package/src/nexus/api/field-uploads.ts +143 -0
  313. package/src/nexus/api/graph-approvals.ts +174 -0
  314. package/src/nexus/api/harness-api.ts +219 -5
  315. package/src/nexus/api/http-server.ts +69 -6
  316. package/src/nexus/api/log-fabric.ts +10 -1
  317. package/src/nexus/api/mcp-oauth-hint.ts +10 -9
  318. package/src/nexus/api/merge-approvals.ts +2 -1
  319. package/src/nexus/api/meta-api.ts +14 -0
  320. package/src/nexus/api/plugins-credentials-api.ts +47 -0
  321. package/src/nexus/api/plugins-market.ts +87 -3
  322. package/src/nexus/api/pod-credential-api.ts +68 -14
  323. package/src/nexus/api/public-routes.ts +7 -0
  324. package/src/nexus/api/trace.ts +57 -6
  325. package/src/nexus/boot/register-mcp-clients.ts +99 -14
  326. package/src/nexus/index.ts +33 -32
  327. package/src/nexus/outbound/router.ts +1 -1
  328. package/src/notify/llm-budget-alert.ts +119 -0
  329. package/src/onboarding.ts +25 -2
  330. package/src/plugins/core/host.ts +14 -0
  331. package/src/plugins/install/market-fetch.ts +4 -3
  332. package/src/plugins/install/plugin-credentials.ts +106 -0
  333. package/src/plugins/install/plugin-install.ts +163 -32
  334. package/src/plugins/maker/plugin-maker.ts +208 -0
  335. package/src/prompt-library/universal-preamble.ts +8 -2
  336. package/src/relay/channel.ts +184 -0
  337. package/src/relay/mux.ts +182 -0
  338. package/src/relay/test-vectors.json +29 -0
  339. package/src/release-loop/checklist.ts +217 -0
  340. package/src/release-loop/release-note.ts +6 -3
  341. package/src/seat-address/seat-address.ts +34 -0
  342. package/src/self-dev/dev-cli.ts +40 -20
  343. package/src/self-dev/dev-pipeline.ts +77 -1
  344. package/src/self-implement/gate-baseline.ts +3 -1
  345. package/src/self-implement/goal-author.ts +18 -1
  346. package/src/self-implement/host-regate.ts +6 -2
  347. package/src/self-implement/orchestrator.ts +7 -2
  348. package/src/self-implement/run-ledger-compact.ts +1 -1
  349. package/src/self-implement/seams.ts +4 -2
  350. package/src/session/index.ts +23 -10
  351. package/src/skills/tools/dev-harness.ts +42 -0
  352. package/src/steward/steward-cards.ts +45 -0
  353. package/src/steward/triage.ts +50 -14
  354. package/src/task-orchestrator/surfaces/pod-artifact-return.ts +61 -3
  355. package/src/task-orchestrator/surfaces/pod-bun-cache.ts +18 -0
  356. package/src/task-orchestrator/surfaces/pod-command-job.ts +36 -6
  357. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +39 -9
  358. package/src/task-orchestrator/surfaces/pod-source-bundle.ts +16 -1
  359. package/src/task-orchestrator/surfaces/pod-source-receive.ts +51 -13
  360. package/src/task-orchestrator/surfaces/self-implement-pod.ts +172 -28
  361. package/src/telegram-report.ts +12 -6
  362. package/src/telegram.ts +108 -1
  363. package/src/tui-client/acp-transport-ws-client.ts +72 -5
  364. package/src/tui.ts +3 -2
  365. package/src/user-config.ts +32 -2
  366. package/src/util/spawn-sync-output.ts +15 -0
  367. package/src/version/code-revision.ts +1 -1
  368. package/src/version/packed-revision.json +1 -1
  369. package/src/video-pipeline/recipes/explainer.ts +213 -0
  370. package/src/video-pipeline/walk-line.ts +3 -2
  371. package/src/web-search/free.ts +77 -0
  372. package/src/web-search/index.ts +2 -0
  373. package/src/workflow-runtime/deps-bridge.ts +4 -3
  374. package/src/workflow-runtime/plugin-kind-node.ts +32 -4
  375. package/src/workflow-runtime/types.ts +2 -1
  376. package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +0 -1
  377. package/apps/pwa/out/_next/static/chunks/1191-7b13a24117a689fa.js +0 -1
  378. package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +0 -1
  379. package/apps/pwa/out/_next/static/chunks/1377-1efeffbbac0963ff.js +0 -1
  380. package/apps/pwa/out/_next/static/chunks/1470.98a6ab61ccb4131c.js +0 -1
  381. package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +0 -1
  382. package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +0 -1
  383. package/apps/pwa/out/_next/static/chunks/2375-3fcae5186c64fe6b.js +0 -1
  384. package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +0 -1
  385. package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +0 -1
  386. package/apps/pwa/out/_next/static/chunks/2977-e38442e86289f1d3.js +0 -1
  387. package/apps/pwa/out/_next/static/chunks/2982-2a2df543be3d2e5a.js +0 -25
  388. package/apps/pwa/out/_next/static/chunks/319-d37df1b6e0cbeff4.js +0 -1
  389. package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +0 -1
  390. package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +0 -1
  391. package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +0 -1
  392. package/apps/pwa/out/_next/static/chunks/4836-3c93795ab834381e.js +0 -1
  393. package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +0 -1
  394. package/apps/pwa/out/_next/static/chunks/6090-1bd6077591603947.js +0 -1
  395. package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.js +0 -1
  396. package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +0 -1
  397. package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +0 -1
  398. package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +0 -1
  399. package/apps/pwa/out/_next/static/chunks/7457-f5cb1cc8db9f7453.js +0 -1
  400. package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.js +0 -1
  401. package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +0 -1
  402. package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +0 -1
  403. package/apps/pwa/out/_next/static/chunks/8067-f74bf14d67ad98f1.js +0 -1
  404. package/apps/pwa/out/_next/static/chunks/8519-0059d82bdb4812bf.js +0 -1
  405. package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +0 -1
  406. package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +0 -1
  407. package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +0 -1
  408. package/apps/pwa/out/_next/static/chunks/9740-52467f244755917f.js +0 -1
  409. package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.js +0 -1
  410. package/apps/pwa/out/_next/static/chunks/ae787e4c-f2998de7f1ba72a4.js +0 -1
  411. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +0 -1
  412. package/apps/pwa/out/_next/static/chunks/app/approvals/page-21c3261af6ab377c.js +0 -1
  413. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-1991445b8ea86274.js +0 -1
  414. package/apps/pwa/out/_next/static/chunks/app/board/page-4ebfaee2369fe248.js +0 -1
  415. package/apps/pwa/out/_next/static/chunks/app/botlab/page-9fa879ebf5959096.js +0 -1
  416. package/apps/pwa/out/_next/static/chunks/app/bots/page-ca7c3cc305c3285a.js +0 -1
  417. package/apps/pwa/out/_next/static/chunks/app/chat/page-9951930add3d2051.js +0 -1
  418. package/apps/pwa/out/_next/static/chunks/app/control/page-4472ddf1e2458e69.js +0 -1
  419. package/apps/pwa/out/_next/static/chunks/app/design-check/page-4de08d4b766f134b.js +0 -1
  420. package/apps/pwa/out/_next/static/chunks/app/editor/page-7a975e2b9db1dfb7.js +0 -1
  421. package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +0 -1
  422. package/apps/pwa/out/_next/static/chunks/app/intake/page-49270714ef35bb96.js +0 -1
  423. package/apps/pwa/out/_next/static/chunks/app/layout-33fbdf353e2a5982.js +0 -1
  424. package/apps/pwa/out/_next/static/chunks/app/live/page-259f122e2dd64317.js +0 -1
  425. package/apps/pwa/out/_next/static/chunks/app/market/page-65b82726753658e1.js +0 -1
  426. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-4b8df3748dc06285.js +0 -1
  427. package/apps/pwa/out/_next/static/chunks/app/morning/page-112f7e477bda77fd.js +0 -1
  428. package/apps/pwa/out/_next/static/chunks/app/not-found-542a5c0006da637c.js +0 -1
  429. package/apps/pwa/out/_next/static/chunks/app/observatory/page-63ffb347831262ea.js +0 -1
  430. package/apps/pwa/out/_next/static/chunks/app/page-98e4e359d97263b4.js +0 -1
  431. package/apps/pwa/out/_next/static/chunks/app/reflection/page-97e24ac675ce576e.js +0 -1
  432. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-ac6391f26391d89d.js +0 -1
  433. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9f45c9d45ffc43bd.js +0 -1
  434. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-36995b9cb5d98218.js +0 -1
  435. package/apps/pwa/out/_next/static/chunks/app/settings/page-0c795b577988d63f.js +0 -1
  436. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b4311e2f71c6735a.js +0 -1
  437. package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +0 -1
  438. package/apps/pwa/out/_next/static/chunks/app/setup/page-eb2381074df20dba.js +0 -1
  439. package/apps/pwa/out/_next/static/chunks/app/share/page-b01336e64eba01ae.js +0 -1
  440. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7af7f21c1f2858ef.js +0 -1
  441. package/apps/pwa/out/_next/static/chunks/app/tasks/page-a98ecd364586c89b.js +0 -1
  442. package/apps/pwa/out/_next/static/chunks/app/term/page-57f0502dfbfa089b.js +0 -1
  443. package/apps/pwa/out/_next/static/chunks/app/trace/page-3467d2e1abd8a14a.js +0 -1
  444. package/apps/pwa/out/_next/static/chunks/app/vault/page-7842ca23d53ba682.js +0 -1
  445. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-ae9ac882912b0198.js +0 -1
  446. package/apps/pwa/out/_next/static/chunks/app/workflows/page-3a0d72e3f783bcef.js +0 -1
  447. package/apps/pwa/out/_next/static/chunks/app/workspace/page-f3b39fe8358c2a97.js +0 -1
  448. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-5a22af3181ac2afb.js +0 -1
  449. package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +0 -1
  450. package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +0 -1
  451. package/apps/pwa/out/_next/static/chunks/main-app-1457627bb302a22c.js +0 -1
  452. package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +0 -1
  453. package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +0 -1
  454. package/apps/pwa/out/_next/static/css/e66b7f5efc8df3f3.css +0 -1
  455. /package/apps/pwa/out/_next/static/{zoQX5iTvXOsIvCjxCe1Ji → qMI0jnlZaBjjm4aIDYNGM}/_ssgManifest.js +0 -0
@@ -125,13 +125,18 @@ export function createPodLedgerFollower(opts: {
125
125
  let disabled = false;
126
126
  let lastProgressAt = now();
127
127
  let lastProgressEvent: string | null = null;
128
+ let lastActivityMeasuredAt = lastProgressAt;
129
+ let lastActivityLogAt = -Infinity;
128
130
  let nextStallMinute = stallMinutes;
129
131
  let stalled = false;
130
- function checkStall() {
132
+ function checkStall(ledgerRead: boolean, activityMeasured: boolean) {
131
133
  const idleMinutes = Math.max(0, (now() - lastProgressAt) / 60_000);
132
134
  if (idleMinutes >= nextStallMinute) {
133
135
  log('self-implement.pod', 'stalled', { runId: opts.runId, lastProgressEvent, idleMinutes });
134
- onStall(`[pod] 진행 없음 ${Math.floor(idleMinutes)}분 — 마지막 진행 ${lastProgressEvent ?? '없음'}`);
136
+ const observation = ledgerRead
137
+ ? (activityMeasured ? '원장·작업 트리 모두 조용함' : '원장 조용함')
138
+ : '원장 못 잼';
139
+ onStall(`[pod] 진행 없음 ${Math.floor(idleMinutes)}분 — ${observation} · 마지막 진행 ${lastProgressEvent ?? '없음'}${activityMeasured ? '' : ' (작업 트리 못 잼)'}`);
135
140
  stalled = true;
136
141
  nextStallMinute = (Math.floor(idleMinutes / stallMinutes) + 1) * stallMinutes;
137
142
  }
@@ -145,13 +150,38 @@ export function createPodLedgerFollower(opts: {
145
150
  log('self-implement.pod', 'ledger-live-skipped', { runId: opts.runId, reason: 'host-ledger-exists' });
146
151
  return;
147
152
  }
148
- const r = opts.exec(`f="\${ELANOUS_STATE_DIR:-$HOME/.elanous}/run-ledger/${opts.runId}.jsonl"; [ -f "$f" ] || exit 0; tail -c +${offset + 1} "$f"`);
149
- if (r.status !== 0) { log('self-implement.pod', 'ledger-live-unavailable', { runId: opts.runId, status: r.status, stderr: r.stderr.trim().slice(0, 200) }); checkStall(); return; }
150
- if (!r.stdout) { checkStall(); return; }
151
- offset += Buffer.byteLength(r.stdout);
152
- const text = pending + r.stdout;
153
+ const since = `${Math.floor(lastActivityMeasuredAt / 1000)}.${String(lastActivityMeasuredAt % 1000).padStart(3, '0')}`;
154
+ const measurementStartedAt = now();
155
+ const until = `${Math.floor(measurementStartedAt / 1000)}.${String(measurementStartedAt % 1000).padStart(3, '0')}`;
156
+ const r = opts.exec(`s="\${ELANOUS_STATE_DIR:-$HOME/.elanous}"; f="$s/run-ledger/${opts.runId}.jsonl"; w="$s/worktrees"; if [ -f "$f" ]; then tail -c +${offset + 1} "$f" || exit 1; fi; target=''; matches=0; for tree in "$w"/*/*.worktrees/*; do [ -e "$tree/.git" ] || continue; owner=$(git -C "$tree" config --worktree --get elanous.harness.owner 2>/dev/null) || continue; [ "$owner" = 'dev:${opts.runId}' ] || continue; target="$tree"; matches=$((matches + 1)); done; if [ "$matches" -eq 1 ]; then activity=$(find "$target" -type d \\( -name .git -o -name node_modules \\) -prune -o -type f -newermt '@${since}' ! -newermt '@${until}' -printf . 2>/dev/null) && printf '\\nELANOUS_ACTIVITY %s\\n' "\${#activity}" || printf '\\nELANOUS_ACTIVITY_ERROR\\n'; else printf '\\nELANOUS_ACTIVITY_ERROR\\n'; fi`);
157
+ if (r.status !== 0) { log('self-implement.pod', 'ledger-live-unavailable', { runId: opts.runId, status: r.status, stderr: r.stderr.trim().slice(0, 200) }); checkStall(false, false); return; }
158
+ const marker = r.stdout.lastIndexOf('\nELANOUS_ACTIVITY');
159
+ const activityLine = marker < 0 ? '' : r.stdout.slice(marker + 1);
160
+ const activityMatch = /^ELANOUS_ACTIVITY (0|[1-9]\d*)\n$/.exec(activityLine);
161
+ const activityMeasured = !!activityMatch && Number.isSafeInteger(Number(activityMatch[1]));
162
+ // A missing measurement trailer is still a usable ledger response unless it contains activity protocol bytes.
163
+ const ledgerBytes = marker < 0 ? (r.stdout.includes('ELANOUS_ACTIVITY') ? '' : r.stdout) : r.stdout.slice(0, marker);
164
+ if (activityMeasured) {
165
+ lastActivityMeasuredAt = measurementStartedAt;
166
+ const files = Number(activityMatch![1]);
167
+ if (files > 0) {
168
+ const idleMinutes = Math.max(0, (now() - lastProgressAt) / 60_000);
169
+ if (stalled) log('self-implement.pod', 'stall-cleared', { runId: opts.runId, idleMinutes });
170
+ stalled = false;
171
+ lastProgressAt = now();
172
+ lastProgressEvent = 'worktree-activity';
173
+ nextStallMinute = stallMinutes;
174
+ if (now() - lastActivityLogAt >= 5 * 60_000) {
175
+ log('self-implement.pod', 'worktree-activity', { runId: opts.runId, files });
176
+ lastActivityLogAt = now();
177
+ }
178
+ }
179
+ }
180
+ if (!ledgerBytes) { checkStall(true, activityMeasured); return; }
181
+ offset += Buffer.byteLength(ledgerBytes);
182
+ const text = pending + ledgerBytes;
153
183
  const cut = text.lastIndexOf('\n');
154
- if (cut < 0) { pending = text; checkStall(); return; }
184
+ if (cut < 0) { pending = text; checkStall(true, activityMeasured); return; }
155
185
  const complete = text.slice(0, cut + 1);
156
186
  pending = text.slice(cut + 1);
157
187
  mkdirSync(dir, { recursive: true });
@@ -171,7 +201,7 @@ export function createPodLedgerFollower(opts: {
171
201
  lastProgressEvent = event;
172
202
  nextStallMinute = stallMinutes;
173
203
  }
174
- checkStall();
204
+ checkStall(true, activityMeasured);
175
205
  },
176
206
  };
177
207
  }
@@ -13,6 +13,7 @@ type PackOptions = {
13
13
  paths?: string[];
14
14
  outDir: string;
15
15
  maxSizeBytes?: number;
16
+ mirrorHead?: string;
16
17
  log?: (category: string, event: string, data: Record<string, unknown>) => void;
17
18
  };
18
19
 
@@ -108,7 +109,21 @@ export function packSourceBundle(options: PackOptions): PackedBundle {
108
109
  ref = `refs/elanous/pod-source/${randomUUID()}`;
109
110
  git(root, env, 'update-ref', ref, headCommit);
110
111
  createdBundle = true;
111
- git(root, env, 'bundle', 'create', bundlePath, `${baseCommit}..${ref}`);
112
+ let bundleRevision = ref;
113
+ if (options.mirrorHead !== undefined) {
114
+ // A thin bundle is usable only when its excluded history belongs to the packed base's lineage.
115
+ const mirrorHead = options.mirrorHead;
116
+ let sharedHistory = false;
117
+ if (/^[0-9a-f]{40,64}$/.test(mirrorHead)) {
118
+ try {
119
+ git(root, env, 'cat-file', '-e', `${mirrorHead}^{commit}`);
120
+ git(root, env, 'merge-base', '--is-ancestor', mirrorHead, baseCommit);
121
+ sharedHistory = true;
122
+ } catch { /* The mirror is unavailable or unrelated; include all history instead. */ }
123
+ }
124
+ bundleRevision = sharedHistory ? `${mirrorHead}..${ref}` : ref;
125
+ }
126
+ git(root, env, 'bundle', 'create', bundlePath, bundleRevision);
112
127
  git(root, env, 'update-ref', '-d', ref, headCommit);
113
128
  ref = undefined;
114
129
  const sizeBytes = statSync(bundlePath).size;
@@ -30,11 +30,33 @@ function assertSize(value: number): number {
30
30
  }
31
31
 
32
32
  /** Job 스크립트의 원천 단계. 성공 갈래는 `ELANOUS_POD_SOURCE <kind> <HEAD>` 로 끝난다. */
33
- export function podSourceScript(source: PodSource, repoUrl: string): string {
34
- const clone = `git clone -q --depth 50 '${repoUrl}' repo`;
33
+ export function podSourceScript(source: PodSource, repoUrl: string, mirrorPath = '/host-mirror'): string {
34
+ const quotedUrl = `'${repoUrl.replace(/'/g, `'\\''`)}'`;
35
+ const quotedMirror = `'${mirrorPath.replace(/'/g, `'\\''`)}'`;
36
+ const clone = `git clone -q --depth 50 ${quotedUrl} repo`;
37
+ const start = [
38
+ 'if [ -e repo ]; then exit 5; fi',
39
+ `if [ -d ${quotedMirror} ] && git -C ${quotedMirror} rev-parse --verify HEAD >/dev/null 2>&1 && git clone -q --shared -- ${quotedMirror} repo && git -C repo rev-parse --verify HEAD >/dev/null 2>&1; then`,
40
+ ` git -C repo remote set-url origin ${quotedUrl} || exit 5`,
41
+ ' source_via=mirror',
42
+ 'else',
43
+ ' if [ -e repo ]; then rm -r -- repo || exit 5; fi',
44
+ ` ${clone} || exit 5`,
45
+ ' source_via=github',
46
+ 'fi',
47
+ "printf 'ELANOUS_POD_SOURCE_VIA %s\\n' \"$source_via\"",
48
+ 'cd repo || exit 5',
49
+ ];
35
50
  if (source.kind === 'default') {
36
51
  return [
37
- `${clone} && cd repo || exit 5`,
52
+ ...start,
53
+ 'if [ "$source_via" = mirror ]; then',
54
+ ' remote_head=$(git ls-remote origin HEAD | cut -f1) || exit 5',
55
+ ' if ! [[ "$remote_head" =~ ^[0-9a-f]{40}$ ]]; then exit 5; fi',
56
+ ' if [ "$remote_head" != "$(git rev-parse HEAD)" ]; then',
57
+ ' git fetch --depth 50 origin HEAD && git reset --hard FETCH_HEAD || exit 5',
58
+ ' fi',
59
+ 'fi',
38
60
  'head=$(git rev-parse HEAD)',
39
61
  "printf 'ELANOUS_POD_SOURCE default %s\\n' \"$head\"",
40
62
  ].join('\n');
@@ -42,9 +64,9 @@ export function podSourceScript(source: PodSource, repoUrl: string): string {
42
64
  if (source.kind === 'commit') {
43
65
  const sha = assertSha(source.sha, 'sha');
44
66
  return [
45
- `${clone} && cd repo || exit 5`,
46
- `git fetch --depth 1 origin ${sha}`,
47
- `git checkout --detach ${sha}`,
67
+ ...start,
68
+ `if ! git cat-file -e '${sha}^{commit}' 2>/dev/null; then git fetch --depth 1 origin ${sha} || exit 5; fi`,
69
+ `git checkout --detach ${sha} || exit 5`,
48
70
  'head=$(git rev-parse HEAD)',
49
71
  "printf 'ELANOUS_POD_SOURCE commit %s\\n' \"$head\"",
50
72
  ].join('\n');
@@ -52,9 +74,11 @@ export function podSourceScript(source: PodSource, repoUrl: string): string {
52
74
  if (source.kind === 'pr') {
53
75
  const n = assertPositiveInt(source.number);
54
76
  return [
55
- `${clone} && cd repo || exit 5`,
56
- `git fetch --depth 50 origin pull/${n}/head:pr-${n}`,
57
- `git checkout pr-${n}`,
77
+ ...start,
78
+ `pr_sha=$(git ls-remote origin refs/pull/${n}/head | cut -f1) || exit 5`,
79
+ 'if ! [[ "$pr_sha" =~ ^[0-9a-f]{40}$ ]]; then exit 5; fi',
80
+ `if ! git cat-file -e "$pr_sha^{commit}" 2>/dev/null; then git fetch --depth 50 origin pull/${n}/head || exit 5; fi`,
81
+ `git checkout -B pr-${n} "$pr_sha" || exit 5`,
58
82
  'head=$(git rev-parse HEAD)',
59
83
  "printf 'ELANOUS_POD_SOURCE pr %s\\n' \"$head\"",
60
84
  ].join('\n');
@@ -82,10 +106,24 @@ export function podSourceScript(source: PodSource, repoUrl: string): string {
82
106
  " printf 'ELANOUS_POD_SOURCE_MISMATCH sha256\\n'",
83
107
  ' exit 3',
84
108
  'fi',
85
- 'git clone /tmp/source.bundle repo',
86
- `git -C repo checkout --detach ${headCommit}`,
87
- 'head=$(git -C repo rev-parse HEAD)',
109
+ 'if [ -e repo ]; then exit 5; fi',
110
+ `if [ -d ${quotedMirror} ] && git -C ${quotedMirror} rev-parse --verify HEAD >/dev/null 2>&1 && git clone -q --shared -- ${quotedMirror} repo && git -C repo rev-parse --verify HEAD >/dev/null 2>&1; then`,
111
+ ` git -C repo remote set-url origin ${quotedUrl} || exit 5`,
112
+ ' if git -C repo bundle verify /tmp/source.bundle >/dev/null 2>&1; then',
113
+ ' git -C repo fetch /tmp/source.bundle "refs/elanous/pod-source/*:refs/remotes/bundle/*" || exit 5',
114
+ ' else',
115
+ ' rm -r -- repo || exit 5',
116
+ ' git clone /tmp/source.bundle repo || exit 5',
117
+ ' git -C repo fetch /tmp/source.bundle "refs/elanous/pod-source/*:refs/remotes/bundle/*" || exit 5',
118
+ ' fi',
119
+ 'else',
120
+ ' if [ -e repo ]; then rm -r -- repo || exit 5; fi',
121
+ ' git clone /tmp/source.bundle repo || exit 5',
122
+ ' git -C repo fetch /tmp/source.bundle "refs/elanous/pod-source/*:refs/remotes/bundle/*" || exit 5',
123
+ 'fi',
124
+ `git -C repo checkout --detach ${headCommit} || exit 5`,
125
+ 'head=$(git -C repo rev-parse HEAD) || exit 5',
88
126
  "printf 'ELANOUS_POD_SOURCE bundle %s\\n' \"$head\"",
89
- 'cd repo',
127
+ 'cd repo || exit 5',
90
128
  ].join('\n');
91
129
  }
@@ -14,7 +14,8 @@
14
14
  import { podSkillsDigest, readSkillEnvFiles, resolvePodSkills } from './pod-skills.js';
15
15
  import { podSourceScript, type PodSource } from './pod-source-receive.js';
16
16
  import { GROUNDING_TOKEN_ENV, GROUNDING_URL_ENV, mintGroundingToken, revokeGroundingRun, type GroundingTokenScope } from '../../grounding/token.js';
17
- import { POD_CREDENTIAL_GROK_PATH } from '../../nexus/api/pod-credential-api.js';
17
+ import { POD_CREDENTIAL_GROK_PATH, POD_CREDENTIAL_GITHUB_PATH, POD_GITHUB_CREDENTIAL_TOKEN_ENV, POD_GITHUB_CREDENTIAL_URL_ENV, installationRepositories } from '../../nexus/api/pod-credential-api.js';
18
+ import { githubInstallationCredential } from '../../auth/github-app-token.js';
18
19
  import { POD_CREDENTIAL_TOKEN_ENV, POD_CREDENTIAL_URL_ENV } from '../../grok/credential.js';
19
20
  import { collectPodLedgers, createPodLedgerFollower } from './pod-ledger-collect.js';
20
21
  import { collectPodArtifacts } from './pod-artifact-return.js';
@@ -47,6 +48,7 @@ import { homedir } from 'node:os';
47
48
  import { isAbsolute, join, normalize, relative, resolve, sep } from 'node:path';
48
49
  import { findGitDir } from '../../git-fs/locate.js';
49
50
  import { debug } from '../../debug/log.js';
51
+ import { selectLiveDetail } from '../../live/detail-switch.js';
50
52
  import { authStorePath } from '../../oauth/store.js';
51
53
  import { grokAuthFilePath, resolveGrokCredential } from '../../grok/credential.js';
52
54
  import { defaultGrokModel } from '../../grok/models.js';
@@ -58,6 +60,14 @@ import { codexQuotaPolicyFromConfig } from '../../oauth/codex-account-store.js';
58
60
 
59
61
  export type Kubectl = (args: readonly string[], input?: string) => { status: number | null; stdout: string; stderr: string };
60
62
 
63
+ // gh auth login may reject installation tokens when validating /user. Its config is
64
+ // still the credential source for gh and setup-git's helper. Install it privately
65
+ // on that failure; rename keeps concurrent readers from seeing a partial token.
66
+ const APP_GH_CONFIG_PATH = '$HOME/.config/gh';
67
+ const APP_GH_AUTH_SCRIPT = 'const fs=require("node:fs"),path=require("node:path"),crypto=require("node:crypto");const token=fs.readFileSync(0,"utf8").trim();if(!/^[A-Za-z0-9_]+$/.test(token))process.exit(1);const dir=process.env.GH_CONFIG_DIR||path.join(process.env.HOME,".config/gh");fs.mkdirSync(dir,{recursive:true,mode:0o700});const dest=path.join(dir,"hosts.yml"),temp=path.join(dir,".hosts.yml."+crypto.randomUUID());try{fs.writeFileSync(temp,"github.com:\\n oauth_token: "+token+"\\n git_protocol: https\\n",{mode:0o600,flag:"wx"});fs.renameSync(temp,dest);fs.chmodSync(dest,0o600)}finally{try{fs.unlinkSync(temp)}catch{}}';
68
+ const APP_GH_AUTH_COMMAND = `bun -e '${APP_GH_AUTH_SCRIPT}'`;
69
+ const APP_GH_EXEC = `unset GH_TOKEN GITHUB_TOKEN; GH_CONFIG_DIR="${APP_GH_CONFIG_PATH}" exec "$@"`;
70
+
61
71
  export interface PodSpawnOptions {
62
72
  /** 이미지 판(라벨 elanous.commit) — 주입(시험·감독기가 이미 잰 값). 없으면 docker 로 잰다. */
63
73
  imageCommit?: string | null;
@@ -75,6 +85,10 @@ export interface PodSpawnOptions {
75
85
  /** 클러스터 안 이미지(`docker/harness/run.sh` 가 만드는 `elanous-harness:local`). */
76
86
  image?: string;
77
87
  repoUrl?: string;
88
+ /** 호스트 Git 미러 디렉터리 — 명시 옵션 > 환경변수 > pod.hostMirror. */
89
+ hostMirror?: string;
90
+ /** 설정 읽기(시험 주입). kubectl 주입 시험에서는 기본 사용자 설정을 읽지 않는다. */
91
+ configHostMirror?: () => string | undefined;
78
92
  /** 원천 — 없으면 종전 `git clone --depth 50`. bundle 이면 apply 뒤 kubectl cp 로 싣는다. */
79
93
  source?: PodSource;
80
94
  /** 호스트 환경에서 읽어 Pod env 로 넣을 키 이름(예: OPENROUTER_API_KEY · ANTHROPIC_API_KEY) — 벤치마크 과금 경로. */
@@ -101,15 +115,22 @@ export interface PodSpawnOptions {
101
115
  pool?: PodPoolScheduler;
102
116
  /** ☸️ 원격 그라운딩 주소(P13) — 없으면 호스트 env ELANOUS_GROUNDING_URL → 설정 pod.groundingUrl. 셋 다 없으면 토큰을 안 만든다. */
103
117
  groundingUrl?: string;
104
- /** 시험 심 — 토큰 발급·회수. scope 는 그라운딩 발급에는 안 넘기고, grok 구독 중계 발급에만 'llm-credential' 로 넘긴다. */
105
- mintGrounding?: (claims: { runId: string; job: string; ttlMs: number; scope?: GroundingTokenScope }) => Promise<{ token: string; exp: number }>;
118
+ /** 시험 심 — 토큰 발급·회수. GitHub credential 토큰에는 호스트가 검증한 저장소를 서명한다. */
119
+ mintGrounding?: (claims: { runId: string; job: string; ttlMs: number; scope?: GroundingTokenScope; repository?: string }) => Promise<{ token: string; exp: number }>;
106
120
  revokeGrounding?: (runId: string) => void;
121
+ /** Test seam for the host's repository-scoped GitHub App installation token. */
122
+ githubInstallation?: (repository: string) => { token: string; expires_at?: string; expiresAt?: string | number } | null;
123
+ /** GitHub installation repository lookup; required to prove the scoped token matches owner/name. */
124
+ githubRepositories?: (token: string) => Promise<readonly string[] | null>;
125
+ now?: () => number;
107
126
  sleep?: (ms: number) => Promise<void>;
108
127
  credentials?: (account: string) => { elanousAuth: string; codexAuth: string; ghToken: string };
109
128
  grokCredentials?: () => { grokAuth?: string; grokApiKey?: string; ghToken: string };
110
129
  /** 호스트 키 캐시(`~/.cache/<소문자 이름>`)에서 키를 읽는다(시험 주입) — env 에 없을 때. */
111
130
  readKeyCache?: (name: string) => string | undefined;
112
131
  env?: NodeJS.ProcessEnv;
132
+ /** Detail switch file (test injection; defaults to the host's local/production selection). */
133
+ liveDetailFile?: string;
113
134
  }
114
135
 
115
136
  export function defaultKubectl(args: readonly string[], input?: string): { status: number | null; stdout: string; stderr: string } {
@@ -188,6 +209,14 @@ export function defaultGhToken(): string {
188
209
  return r.stdout.trim();
189
210
  }
190
211
 
212
+ /** Bind credential issuance to the repository the host actually supplied to the Pod. */
213
+ function githubRepositoryFromUrl(repoUrl: string): string | null {
214
+ const match = /^(?:https:\/\/github\.com\/|git@github\.com:)([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+?)(?:\.git)?\/?$/.exec(repoUrl);
215
+ const owner = match?.[1];
216
+ const name = match?.[2];
217
+ return owner && name && ![owner, name].some((part) => part === '.' || part === '..') ? `${owner}/${name}` : null;
218
+ }
219
+
191
220
  /** Job 스크립트의 salvage 단계 — 자식 rc 가 0 이 아닐 때만, 변경·미푸시 커밋이 있는 격리 워크트리를 `salvage/<job>/<worktree>` 로 push.
192
221
  * main·self-impl/ 로는 절대 안 민다. 실패해도 제어 흐름을 바꾸지 않는다(호출자는 이 뒤에 `exit $rc`). */
193
222
  export function podSalvageScript(): string {
@@ -297,13 +326,13 @@ export function podRunLabels(o: { runId?: string; parentRunId?: string }): Recor
297
326
 
298
327
  /** Pod 메모리 «등급» — 대표 09-27: PWA Pod 골이 16Gi 에서 OOM 으로 죽는다 ⇒ «high» 등급을 옵션으로 고른다.
299
328
  * 등급 = `standard`(`ELANOUS_POD_MEMORY` 또는 16Gi) · `high`(`ELANOUS_POD_MEMORY_HIGH` 또는 32Gi · standard 보다 작아지지 않는다).
300
- * 고르는 순서(앞이 이긴다): ① 발사 옵션 `--pod-memory <등급>`(→ `ELANOUS_POD_MEMORY_TIER`) ② 골 문면 한 줄 `Pod 메모리: high` ③ 자동 — 골 문면에 `apps/pwa/` 면 high ④ standard.
329
+ * 고르는 순서(앞이 이긴다): ① 발사 옵션 `--pod-memory <등급>`(→ `ELANOUS_POD_MEMORY_TIER`) ② 조각 문면 한 줄 `Pod 메모리: high|standard` ③ 조각의 `apps/pwa/` ④ 부모 골 본문의 같은 두 규칙 ⑤ standard.
301
330
  * node-b 노드 할당 가능 약 251Gi. */
302
331
  export const POD_MEMORY_DEFAULT = '16Gi';
303
332
  export const POD_MEMORY_HIGH_DEFAULT = '32Gi';
304
333
  export const POD_MEMORY_TIERS = ['standard', 'high'] as const;
305
334
  export type PodMemoryTier = (typeof POD_MEMORY_TIERS)[number];
306
- export type PodMemorySource = 'option' | 'goal-line' | 'pwa-auto' | 'default';
335
+ export type PodMemorySource = 'option' | 'goal-line' | 'pwa-auto' | 'default' | 'parent-goal-line' | 'parent-pwa-auto';
307
336
  const PWA_PATH = /(?:^|[\s`'"(,·])apps\/pwa\//m;
308
337
  const GOAL_LINE = /^\s*(?:Pod 메모리|pod-memory)\s*:\s*(standard|high)\s*$/im;
309
338
 
@@ -322,12 +351,15 @@ function memoryGi(v: string): number | null {
322
351
  return m[2] === 'Gi' ? Number(m[1]) : Number(m[1]) / 1024;
323
352
  }
324
353
 
325
- export function podMemoryLimitFor(feature: string, env: NodeJS.ProcessEnv | Readonly<Record<string, string | undefined>>): { limit: string; tier: PodMemoryTier; source: PodMemorySource } {
354
+ export function podMemoryLimitFor(feature: string, env: NodeJS.ProcessEnv | Readonly<Record<string, string | undefined>>, parentGoal?: string): { limit: string; tier: PodMemoryTier; source: PodMemorySource } {
326
355
  const option = parsePodMemoryTier(env.ELANOUS_POD_MEMORY_TIER);
327
356
  const line = parsePodMemoryTier(GOAL_LINE.exec(feature)?.[1]);
357
+ const parentLine = !option && !line && !goalTouchesPwa(feature) ? parsePodMemoryTier(GOAL_LINE.exec(parentGoal ?? '')?.[1]) : null;
328
358
  const [tier, source]: [PodMemoryTier, PodMemorySource] = option ? [option, 'option']
329
359
  : line ? [line, 'goal-line']
330
360
  : goalTouchesPwa(feature) ? ['high', 'pwa-auto']
361
+ : parentLine ? [parentLine, 'parent-goal-line']
362
+ : parentGoal && goalTouchesPwa(parentGoal) ? ['high', 'parent-pwa-auto']
331
363
  : ['standard', 'default'];
332
364
  const base = env.ELANOUS_POD_MEMORY?.trim() || POD_MEMORY_DEFAULT;
333
365
  if (tier === 'standard') return { limit: base, tier, source };
@@ -336,7 +368,7 @@ export function podMemoryLimitFor(feature: string, env: NodeJS.ProcessEnv | Read
336
368
  return { limit: a !== null && b !== null && a > b ? base : high, tier, source };
337
369
  }
338
370
 
339
- export function podJobManifest(o: { name: string; namespace: string; image: string; /** 레지스트리 이미지면 IfNotPresent(노드가 pull) · 반입 이미지면 Never. */ imagePullPolicy?: 'Never' | 'IfNotPresent'; repoUrl: string; source?: PodSource; args: readonly string[]; passEnv: readonly string[]; deadlineSeconds: number; runId?: string; parentRunId?: string; armEnv?: Readonly<Record<string, string>>; hostId?: string; imageCommit?: string | null; skillEnvs?: readonly string[]; memoryLimit?: string; goalDoc?: string; grokCredential?: 'subscription' | 'api_key'; codexAccounts?: readonly string[] }): Record<string, unknown> {
371
+ export function podJobManifest(o: { name: string; namespace: string; image: string; /** 레지스트리 이미지면 IfNotPresent(노드가 pull) · 반입 이미지면 Never. */ imagePullPolicy?: 'Never' | 'IfNotPresent'; repoUrl: string; source?: PodSource; hostMirror?: string; args: readonly string[]; passEnv: readonly string[]; deadlineSeconds: number; runId?: string; parentRunId?: string; armEnv?: Readonly<Record<string, string>>; hostId?: string; imageCommit?: string | null; skillEnvs?: readonly string[]; memoryLimit?: string; goalDoc?: string; grokCredential?: 'subscription' | 'api_key'; codexAccounts?: readonly string[]; appCredential?: boolean }): Record<string, unknown> {
340
372
  const quoted = o.args.map((a) => `'${a.replace(/'/g, `'\\''`)}'`).join(' ');
341
373
  const goalPath = o.goalDoc ? `'${(o.goalDoc.startsWith('-') ? `./${o.goalDoc}` : o.goalDoc).replace(/'/g, `'\\''`)}'` : undefined;
342
374
  const askBaseIndex = o.goalDoc ? o.args.indexOf('--base') : -1;
@@ -351,7 +383,7 @@ export function podJobManifest(o: { name: string; namespace: string; image: stri
351
383
  : o.grokCredential === 'api_key'
352
384
  ? 'mkdir -p ~/.grok && install -m 600 /creds/grok-api-key ~/.grok/api-key && export XAI_API_KEY="$(cat ~/.grok/api-key)" && export ELANOUS_LLM_PROVIDER=grok'
353
385
  : o.codexAccounts ? podCodexAccountScript(o.codexAccounts) : `mkdir -p ~/.elanous ~/.codex && cp /creds/elanous-auth.json ~/.elanous/auth.json && cp /creds/codex-auth.json ~/.codex/auth.json && chmod 600 ~/.elanous/auth.json ~/.codex/auth.json\n${podQuotaPolicyExport()}`,
354
- 'export GH_TOKEN="$(cat /creds/gh-token)"',
386
+ o.appCredential ? `unset GH_TOKEN GITHUB_TOKEN; export GH_CONFIG_DIR="${APP_GH_CONFIG_PATH}"; mkdir -p "$GH_CONFIG_DIR" && chmod 700 "$GH_CONFIG_DIR" && { gh auth login --with-token < /creds/gh-token >/dev/null 2>&1 || ${APP_GH_AUTH_COMMAND} < /creds/gh-token; } && chmod 600 "$GH_CONFIG_DIR/hosts.yml" && chmod 700 "$GH_CONFIG_DIR" || exit 7` : 'export GH_TOKEN="$(cat /creds/gh-token)"',
355
387
  // 🔑 스킬 키(.env) — 이미지엔 없다. 이 런의 Secret 에서 각 스킬 폴더로 0600 복사(값은 로그에 안 나온다).
356
388
  ...(o.skillEnvs?.length ? [`for n in ${o.skillEnvs.join(' ')}; do [ -d ~/.claude/skills/$n ] && install -m 600 /creds/skillenv-$n ~/.claude/skills/$n/.env; done; echo "[pod] skill env: ${o.skillEnvs.join(',')}"`] : []),
357
389
  'git config --global user.name "elanous pod child" && git config --global user.email "noreply@anthropic.com" && gh auth setup-git',
@@ -465,9 +497,9 @@ if [ "$found" -eq 0 ]; then echo ELANOUS_RUN_LEDGER_NONE; fi`,
465
497
  ...Object.entries(o.armEnv ?? {}).map(([name, value]) => ({ name, value })),
466
498
  ...o.passEnv.map((key) => ({ name: key, valueFrom: { secretKeyRef: { name: `${o.name}-creds`, key: `env-${key}` } } })),
467
499
  ],
468
- volumeMounts: [{ name: 'creds', mountPath: '/creds', readOnly: true }],
500
+ volumeMounts: [{ name: 'creds', mountPath: '/creds', readOnly: true }, ...(o.hostMirror ? [{ name: 'host-mirror', mountPath: '/host-mirror', readOnly: true }] : [])],
469
501
  }],
470
- volumes: [{ name: 'creds', secret: { secretName: `${o.name}-creds`, defaultMode: 0o400 } }],
502
+ volumes: [{ name: 'creds', secret: { secretName: `${o.name}-creds`, defaultMode: 0o400 } }, ...(o.hostMirror ? [{ name: 'host-mirror', hostPath: { path: o.hostMirror, type: 'Directory' } }] : [])],
471
503
  },
472
504
  },
473
505
  },
@@ -495,12 +527,34 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
495
527
  const namespace = options.namespace ?? 'elanous-test';
496
528
  const image = options.image ?? 'elanous-harness:local';
497
529
  const repoUrl = options.repoUrl ?? 'https://github.com/ElanvitalAI/elanous';
530
+ const repository = githubRepositoryFromUrl(repoUrl);
498
531
  const env = options.env ?? process.env;
532
+ const hostMirror = (options.hostMirror ?? env.ELANOUS_POD_HOST_MIRROR ?? (options.configHostMirror ?? (options.kubectl ? () => undefined : configHostMirror))())?.trim() || undefined;
533
+ if (hostMirror && !isAbsolute(hostMirror)) throw new Error('pod.hostMirror must be an absolute directory path');
499
534
  const hostSupervised = options.hostSupervised ?? true;
500
535
  return (input) => {
501
536
  const name = podJobName(input.spaceId);
502
- const { limit: memoryLimit, tier: memoryTier, source: memorySource } = podMemoryLimitFor(input.feature, env);
503
- debug.log('self-implement.pod', 'memory-limit', { spaceId: input.spaceId, memoryLimit, tier: memoryTier, source: memorySource });
537
+ const featureMemory = podMemoryLimitFor(input.feature, env);
538
+ let parentGoal: string | undefined;
539
+ let parentGoalUnreadable = false;
540
+ let inheritedFrom: string | undefined;
541
+ if (featureMemory.source === 'default' && env.ELANOUS_POD_GOAL_DOC) {
542
+ try {
543
+ const requestedPath = normalize(env.ELANOUS_POD_GOAL_DOC);
544
+ if (isAbsolute(requestedPath) || requestedPath === '..' || requestedPath.startsWith(`..${sep}`) || requestedPath === '.') throw new Error('invalid goal path');
545
+ const root = findGitDir(process.cwd())?.root;
546
+ if (!root) throw new Error('no repository');
547
+ const realRoot = realpathSync(root);
548
+ const goalFile = realpathSync(resolve(realRoot, requestedPath));
549
+ const withinRoot = relative(realRoot, goalFile);
550
+ if (withinRoot === '..' || withinRoot.startsWith(`..${sep}`) || isAbsolute(withinRoot)) throw new Error('outside repository');
551
+ parentGoal = readFileSync(goalFile, 'utf8').replace(/\r\n/g, '\n').split(/^## 실행 기록(?:\n|$)/m, 1)[0];
552
+ inheritedFrom = withinRoot;
553
+ } catch { parentGoalUnreadable = true; }
554
+ }
555
+ const { limit: memoryLimit, tier: memoryTier, source: memorySource } = parentGoal === undefined ? featureMemory : podMemoryLimitFor(input.feature, env, parentGoal);
556
+ debug.log('self-implement.pod', 'memory-limit', { spaceId: input.spaceId, memoryLimit, tier: memoryTier, source: memorySource,
557
+ ...(memorySource.startsWith('parent-') ? { inheritedFrom } : {}), ...(parentGoalUnreadable ? { parentGoal: 'unreadable' } : {}) });
504
558
  const childRunId = mintRunId();
505
559
  const parentRunId = env.ELANOUS_RUN_ID?.trim();
506
560
  const address = `self-impl:${input.spaceId}`;
@@ -548,6 +602,8 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
548
602
  debug.log('self-implement.pod', unavailableEvent, { job: name, reason: error instanceof Error ? error.message : String(error) });
549
603
  return { status: 'unavailable', childLedgerIncomplete: false, samples: [] };
550
604
  }
605
+ const via = /^ELANOUS_POD_SOURCE_VIA (mirror|github)\r?$/m.exec(full.stdout)?.[1];
606
+ if (via) debug.log('self-implement.pod', 'source-via', { job: name, via });
551
607
  let ledgerIncomplete = false;
552
608
  let childLedgerIncomplete = false;
553
609
  let artifactIncomplete = false;
@@ -604,9 +660,9 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
604
660
  }
605
661
  }
606
662
  // Grok 자격은 전용 Secret 키 하나로만 보낸다 — passEnv 의 API 키 중복 전달은 막는다.
607
- const passKeys = grok
608
- ? (options.passEnv ?? []).filter((key) => !['XAI_API_KEY', 'GROK_API_KEY', 'GROK_CODE_XAI_API_KEY'].includes(key))
609
- : options.passEnv ?? [];
663
+ const passKeys = (options.passEnv ?? []).filter((key) => key !== 'ELANOUS_LIVE_DETAIL_UNTIL'
664
+ && key !== POD_GITHUB_CREDENTIAL_TOKEN_ENV && key !== POD_GITHUB_CREDENTIAL_URL_ENV
665
+ && (!grok || !['XAI_API_KEY', 'GROK_API_KEY', 'GROK_CODE_XAI_API_KEY'].includes(key)));
610
666
  const skillEnvs: Record<string, string> = options.skillEnv
611
667
  ? (options.readSkillEnv ?? (() => readSkillEnvFiles(resolvePodSkills(env).skills)))()
612
668
  : {};
@@ -632,6 +688,34 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
632
688
  } else if (grokSubscription && !groundingUrl) {
633
689
  debug.log('self-implement.pod', 'credential-relay-skipped', { reason: 'no-host-url' });
634
690
  }
691
+ const now = options.now ?? Date.now;
692
+ const issueVerifiedAppCredential = async (): Promise<{ token: string; expiresAt: number } | null> => {
693
+ if (!repository) return null;
694
+ // This is the same repository-scoped, contents+pull_requests installation request on launch and refresh.
695
+ const candidate = (options.githubInstallation ?? ((repo) => githubInstallationCredential({ scope: { repository: repo.split('/')[1]! } })))(repository);
696
+ if (!candidate?.token) return null;
697
+ const expiry = candidate.expires_at ?? ('expiresAt' in candidate ? candidate.expiresAt : undefined);
698
+ const expiresAt = typeof expiry === 'number' ? expiry : Date.parse(expiry ?? '');
699
+ if (!Number.isFinite(expiresAt) || expiresAt <= now()) return null;
700
+ const available = await (options.githubRepositories ?? installationRepositories)(candidate.token);
701
+ return available?.length === 1 && available[0]?.toLowerCase() === repository.toLowerCase()
702
+ ? { token: candidate.token, expiresAt } : null;
703
+ };
704
+ let appCredential: Awaited<ReturnType<typeof issueVerifiedAppCredential>> = null;
705
+ try { appCredential = await issueVerifiedAppCredential(); }
706
+ catch { debug.log('self-implement.pod', 'github-app-unavailable', { job: name, reason: 'mint-or-verification-failed' }); }
707
+ const githubRelay = appCredential && groundingUrl && repository
708
+ ? await mint({ runId: groundingRunId, job: name, ttlMs: groundingTtlMs, scope: 'gh-credential', repository })
709
+ : null;
710
+ if (githubRelay) {
711
+ groundingMinted = groundingRunId;
712
+ debug.log('self-implement.pod', 'github-credential-relay', { job: name, runId: groundingRunId, exp: githubRelay.exp });
713
+ }
714
+ // The host polling loop is a refresh path even when the Pod cannot reach the relay.
715
+ const hostRefresh = Boolean(appCredential);
716
+ const podGhToken = hostRefresh ? appCredential!.token : creds.ghToken;
717
+ const githubPassKeys = hostRefresh ? passKeys.filter((key) => !['GH_TOKEN', 'GITHUB_TOKEN', 'GH_CONFIG_DIR'].includes(key)) : passKeys;
718
+ let ghTokenExpiresAt = hostRefresh ? appCredential!.expiresAt : null;
635
719
  const requestedGoalDoc = env.ELANOUS_POD_GOAL_DOC;
636
720
  const requestedPath = requestedGoalDoc ? normalize(requestedGoalDoc) : undefined;
637
721
  if (requestedPath && (isAbsolute(requestedPath) || requestedPath === '..' || requestedPath.startsWith(`..${sep}`) || requestedPath === '.')) {
@@ -700,17 +784,18 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
700
784
  : codexCredentials
701
785
  ? Object.fromEntries(codexCredentials.map((candidate, i) => [`codex-${i}.json`, JSON.stringify({ elanousAuth: candidate.elanousAuth, codexAuth: candidate.codexAuth })]))
702
786
  : { 'elanous-auth.json': (creds as ReturnType<typeof hostCredentials>).elanousAuth, 'codex-auth.json': (creds as ReturnType<typeof hostCredentials>).codexAuth }),
703
- 'gh-token': creds.ghToken, feature: input.feature,
787
+ 'gh-token': podGhToken, feature: input.feature,
704
788
  ...(goalDocument !== undefined ? { 'goal-doc': goalDocument } : {}),
705
789
  ...Object.fromEntries(Object.entries(skillEnvs).map(([n, text]) => [`skillenv-${n}`, text])),
706
790
  ...(grounding ? { [`env-${GROUNDING_TOKEN_ENV}`]: grounding.token } : {}),
707
791
  ...(credentialRelay ? { [`env-${POD_CREDENTIAL_TOKEN_ENV}`]: credentialRelay.token } : {}),
708
- ...Object.fromEntries(passKeys.map((k) => [k, env[k] ?? (options.readKeyCache ?? defaultReadKeyCache)(k)] as const).filter(([, v]) => v).map(([k, v]) => [`env-${k}`, v!])),
792
+ ...(githubRelay ? { [`env-${POD_GITHUB_CREDENTIAL_TOKEN_ENV}`]: githubRelay.token } : {}),
793
+ ...Object.fromEntries(githubPassKeys.map((k) => [k, env[k] ?? (options.readKeyCache ?? defaultReadKeyCache)(k)] as const).filter(([, v]) => v).map(([k, v]) => [`env-${k}`, v!])),
709
794
  },
710
795
  };
711
796
  const hostKey = (k: string): string | undefined => env[k] ?? (options.readKeyCache ?? defaultReadKeyCache)(k);
712
- const passEnv = passKeys.filter((k) => hostKey(k));
713
- const missing = passKeys.filter((k) => !hostKey(k));
797
+ const passEnv = githubPassKeys.filter((k) => hostKey(k));
798
+ const missing = githubPassKeys.filter((k) => !hostKey(k));
714
799
  if (missing.length) debug.log('self-implement.pod', 'pass-env-missing', { job: name, missing }, { level: 'warn' });
715
800
  // ☸️ 재개(P3 · 2026-09-26): 같은 이름의 Job 이 이미 있으면 «지우지 않고» 붙는다 — 호스트가 끊긴 동안에도 원격은 계속 돌았다.
716
801
  // 🩸 종전엔 여기서 delete 부터 해서, 재개가 원격에서 멀쩡히 돌던 Job 을 죽였다. 실패로 끝난 Job 만 지우고 다시 만든다.
@@ -726,9 +811,12 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
726
811
  const existingJob = kubectl(['-n', namespace, 'get', 'job', name, '-o', 'json']);
727
812
  if (existingJob.status === 0) {
728
813
  try {
729
- const job = JSON.parse(existingJob.stdout) as { spec?: { template?: { spec?: { containers?: Array<{ name?: string; env?: Array<{ name: string; value?: string }> }> } } } };
730
- const inherited = job.spec?.template?.spec?.containers?.find((c) => c.name === 'child')?.env?.find((e) => e.name === 'ELANOUS_RUN_ID')?.value;
814
+ const job = JSON.parse(existingJob.stdout) as { spec?: { template?: { spec?: { containers?: Array<{ name?: string; args?: string[]; env?: Array<{ name: string; value?: string }> }> } } } };
815
+ const child = job.spec?.template?.spec?.containers?.find((c) => c.name === 'child');
816
+ const inherited = child?.env?.find((e) => e.name === 'ELANOUS_RUN_ID')?.value;
731
817
  if (inherited && inherited !== parentRunId && inherited === normalizeRunId(inherited)) liveChildRunId = inherited;
818
+ // A resumed App Job has an unknown remaining lifetime; refresh its gh config on the first live poll.
819
+ ghTokenExpiresAt = child?.args?.[0]?.includes('gh auth login --with-token < /creds/gh-token') ? now() : null;
732
820
  } catch { /* The existing Job's identity must be verified before following it. */ }
733
821
  }
734
822
  if (liveChildRunId === launchRunId) {
@@ -737,6 +825,7 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
737
825
  }
738
826
  }
739
827
  if (reattach) debug.log('self-implement.pod', 'job-reattach', { job: name, conditions: existingConditions.join(' ') || 'running', spaceId: input.spaceId });
828
+ if (!hostRefresh && ghTokenExpiresAt === null && !githubRelay) debug.log('self-implement.pod', 'gh-token-app-skipped', { job: name, reason: 'no-verified-app-or-refresh-path' });
740
829
  if (!reattach) {
741
830
  if (/Failed|FailureTarget/.test(existingConditions.join(' ')) && collectFullLogs('failed-job-logs-unavailable').status === 'incomplete') {
742
831
  return { exitCode: 1, output: '', error: { code: 'pod-collection-incomplete', message: `Job ${name} recovery incomplete — old Job retained` } };
@@ -748,15 +837,23 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
748
837
  ...passEnv,
749
838
  ...(grounding ? [GROUNDING_TOKEN_ENV] : []),
750
839
  ...(credentialRelay ? [POD_CREDENTIAL_TOKEN_ENV] : []),
840
+ ...(githubRelay ? [POD_GITHUB_CREDENTIAL_TOKEN_ENV] : []),
751
841
  ];
752
- const jobArmEnv = grounding || credentialRelay
842
+ const detail = selectLiveDetail(options.liveDetailFile ? { path: options.liveDetailFile } : {}).state;
843
+ const detailUntil = detail && (detail.scope === 'all' || detail.scope === parentRunId || detail.scope === launchRunId) && detail.until > Date.now()
844
+ ? String(detail.until) : undefined;
845
+ const armEnv = Object.fromEntries(Object.entries(options.armEnv ?? {}).filter(([key]) => key !== 'ELANOUS_LIVE_DETAIL_UNTIL' && key !== POD_GITHUB_CREDENTIAL_TOKEN_ENV && key !== POD_GITHUB_CREDENTIAL_URL_ENV && (!hostRefresh || !['GH_TOKEN', 'GITHUB_TOKEN', 'GH_CONFIG_DIR'].includes(key))));
846
+ const jobArmEnv = grounding || credentialRelay || githubRelay || env.ELANOUS_DISPATCH_RECORDED === '1' || options.armEnv || detailUntil
753
847
  ? {
754
- ...(options.armEnv ?? {}),
848
+ ...armEnv,
849
+ ...(detailUntil ? { ELANOUS_LIVE_DETAIL_UNTIL: detailUntil } : {}),
850
+ ...(env.ELANOUS_DISPATCH_RECORDED === '1' ? { ELANOUS_DISPATCH_RECORDED: '1' } : {}),
755
851
  ...(grounding ? { [GROUNDING_URL_ENV]: groundingUrl! } : {}),
756
852
  ...(credentialRelay ? { [POD_CREDENTIAL_URL_ENV]: `${new URL(groundingUrl!).origin}${POD_CREDENTIAL_GROK_PATH}` } : {}),
853
+ ...(githubRelay ? { [POD_GITHUB_CREDENTIAL_URL_ENV]: `${new URL(groundingUrl!).origin}${POD_CREDENTIAL_GITHUB_PATH}` } : {}),
757
854
  }
758
- : options.armEnv;
759
- const job = podJobManifest({ name, namespace, image: jobImage, ...(member?.imageRef ? { imagePullPolicy: 'IfNotPresent' as const } : {}), repoUrl, ...(options.source ? { source: options.source } : {}), args, passEnv: jobPassEnv, deadlineSeconds: options.deadlineSeconds ?? POD_JOB_DEADLINE_SECONDS, ...(goalDoc ? { goalDoc } : {}), runId: launchRunId, ...(parentRunId ? { parentRunId } : {}), ...(jobArmEnv ? { armEnv: jobArmEnv } : {}), hostId: resolveHostId(env), skillEnvs: Object.keys(skillEnvs), memoryLimit, imageCommit: options.imageCommit !== undefined ? options.imageCommit : options.kubectl ? null : podImageFreshness({ image }).imageCommit, ...(grok ? { grokCredential: 'grokAuth' in creds && creds.grokAuth ? 'subscription' as const : 'api_key' as const } : {}), ...(codexAccounts ? { codexAccounts } : {}) }); // kubectl 주입(=시험)이면 docker 를 부르지 않는다
855
+ : undefined;
856
+ const job = podJobManifest({ name, namespace, image: jobImage, ...(member?.imageRef ? { imagePullPolicy: 'IfNotPresent' as const } : {}), repoUrl, ...(options.source ? { source: options.source } : {}), ...(hostMirror ? { hostMirror } : {}), args, passEnv: jobPassEnv, deadlineSeconds: options.deadlineSeconds ?? POD_JOB_DEADLINE_SECONDS, ...(goalDoc ? { goalDoc } : {}), runId: launchRunId, ...(parentRunId ? { parentRunId } : {}), ...(jobArmEnv ? { armEnv: jobArmEnv } : {}), hostId: resolveHostId(env), skillEnvs: Object.keys(skillEnvs), memoryLimit, imageCommit: options.imageCommit !== undefined ? options.imageCommit : options.kubectl ? null : podImageFreshness({ image }).imageCommit, ...(grok ? { grokCredential: 'grokAuth' in creds && creds.grokAuth ? 'subscription' as const : 'api_key' as const } : {}), ...(codexAccounts ? { codexAccounts } : {}), ...(hostRefresh ? { appCredential: true } : {}) }); // kubectl 주입(=시험)이면 docker 를 부르지 않는다
760
857
  const a = kubectl(['apply', '-f', '-'], JSON.stringify(job));
761
858
  if (a.status !== 0) { cleanupSecret(); return { exitCode: 1, output: a.stderr, error: { code: 'pod-apply', message: a.stderr.trim() } }; }
762
859
  if (options.source?.kind === 'bundle') {
@@ -791,7 +888,7 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
791
888
  try { appendRunLedgerEntry({ runId: parentRunId, event: 'pod-child-run', data: { childRunId: liveChildRunId, job: name, ...(goalFile ? { goalFile } : {}) } }, ledgerDir); }
792
889
  catch (error) { debug.log('self-implement.pod', 'parent-ledger-unavailable', { job: name, reason: error instanceof Error ? error.message : String(error) }); }
793
890
  }
794
- writePodFragment({ spaceId: input.spaceId, context, namespace, job: name, inboxDir: POD_CONTROL_INBOX_DIR }, env);
891
+ writePodFragment({ spaceId: input.spaceId, context, namespace, job: name, inboxDir: POD_CONTROL_INBOX_DIR, ...(liveChildRunId === normalizeRunId(liveChildRunId) ? { runId: liveChildRunId } : {}), ...(parentRunId && normalizeRunId(parentRunId) === parentRunId ? { parentRunId } : {}) }, env);
795
892
  recorded = true;
796
893
  debug.log('self-implement.pod', 'job-applied', { job: name, namespace, ...(member ? { context: member.context } : {}), image: jobImage, spaceId: input.spaceId, passEnv, extraArgs: options.extraArgs ?? [], ...(options.armEnv?.ELANOUS_ARM_ID ? { armId: options.armEnv.ELANOUS_ARM_ID } : {}) });
797
894
  let state: 'complete' | 'failed' | 'aborted' = 'failed';
@@ -829,6 +926,22 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
829
926
  debug.log('self-implement.pod', 'container-terminated', { job: name, container: 'child', reason: containerReason, exitCode: containerExitCode, jobReason: failedReason || null, memoryLimit });
830
927
  break;
831
928
  }
929
+ if (ghTokenExpiresAt !== null && ghTokenExpiresAt - now() <= 10 * 60_000) {
930
+ try {
931
+ const fresh = await issueVerifiedAppCredential();
932
+ if (!fresh) throw new Error('mint-or-verification-failed');
933
+ // No bearer in argv, stdout, stderr, or the run ledger. kubectl receives it only over stdin.
934
+ const installed = kubectl(['-n', namespace, 'exec', '-i', `job/${name}`, '-c', 'child', '--', 'sh', '-c', APP_GH_EXEC, 'sh', 'gh', 'auth', 'login', '--with-token'], `${fresh.token}\n`);
935
+ if (installed.status !== 0) {
936
+ const fallback = kubectl(['-n', namespace, 'exec', '-i', `job/${name}`, '-c', 'child', '--', 'sh', '-c', APP_GH_EXEC, 'sh', 'bun', '-e', APP_GH_AUTH_SCRIPT], `${fresh.token}\n`);
937
+ if (fallback.status !== 0) throw new Error('exec-failed');
938
+ }
939
+ ghTokenExpiresAt = fresh.expiresAt;
940
+ debug.log('self-implement.pod', 'gh-token-refreshed', { job: name, expiresAt: new Date(fresh.expiresAt).toISOString() });
941
+ } catch {
942
+ debug.log('self-implement.pod', 'gh-token-refresh-failed', { job: name, reason: 'mint-verification-or-exec-failed' });
943
+ }
944
+ }
832
945
  try { follower?.poll(); } catch (e) { debug.log('self-implement.pod', 'ledger-live-unavailable', { job: name, reason: e instanceof Error ? e.message : String(e) }); }
833
946
  await sleep(options.pollMs ?? 15_000);
834
947
  }
@@ -919,7 +1032,13 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
919
1032
  try { const { appendGoalExecutionRecord } = await import('../../self-implement/orchestrator.js'); appendGoalExecutionRecord(goalFile, record); }
920
1033
  catch (error) { debug.log('self-implement.pod', 'goal-record-unavailable', { job: name, childRunId: liveChildRunId, reason: error instanceof Error ? error.message : String(error) }); }
921
1034
  }
922
- debug.log('self-implement.pod', 'job-finished', { job: name, ...(member ? { context: member.context } : {}), state, containerReason, stage: disposition?.stage ?? null, prUrl: disposition?.prUrl ?? null, childRunId: liveChildRunId, ledgerCompleteness });
1035
+ const childFailure = state === 'failed' && containerReason !== 'OOMKilled' && failedReason !== 'DeadlineExceeded'
1036
+ ? lastPodChildFailure(logs) : null;
1037
+ if (childFailure) debug.log('self-implement.pod', 'child-error', { job: name, stage: childFailure.stage, error: childFailure.error }, { compact: { stringMax: 500 } });
1038
+ debug.log('self-implement.pod', 'job-finished', { job: name, ...(member ? { context: member.context } : {}), state, containerReason, stage: disposition?.stage ?? null, prUrl: disposition?.prUrl ?? null, childRunId: liveChildRunId, ledgerCompleteness,
1039
+ ...(state === 'failed' && containerReason !== 'OOMKilled' && failedReason !== 'DeadlineExceeded'
1040
+ ? { ...(childFailure ? { childStage: childFailure.stage } : {}), childError: childFailure?.error ?? 'no-result-line' } : {}) },
1041
+ state === 'failed' && containerReason !== 'OOMKilled' && failedReason !== 'DeadlineExceeded' ? { compact: { stringMax: 500 } } : undefined);
923
1042
  const tail = podRunResultLine(logs, salvage);
924
1043
  if (state === 'aborted') return { exitCode: null, output: tail, error: { code: 'aborted', message: 'aborted — Job deleted' }, ...(disposition ? { disposition } : {}) };
925
1044
  const deadlineExceeded = state === 'failed' && failedReason === 'DeadlineExceeded';
@@ -944,7 +1063,7 @@ export function podSelfImplementSpawn(options: PodSpawnOptions = {}): SelfImplem
944
1063
  : state === 'failed'
945
1064
  ? containerReason === 'OOMKilled'
946
1065
  ? { error: { code: 'pod-oom-killed', message: `Job ${name} failed (OOMKilled/${containerExitCode ?? 'unknown'}, memoryLimit=${memoryLimit})\n${formatLastMemSample(samples.at(-1))}` } }
947
- : { error: { code: 'pod-job-failed', message: `Job ${name} failed${failedReason || containerReason ? ` (${[failedReason, containerReason ? `container=${containerReason}/${containerExitCode ?? 'unknown'}` : ''].filter(Boolean).join(', ')})` : ''}` } }
1066
+ : { error: { code: 'pod-job-failed', message: `Job ${name} failed${failedReason || containerReason ? ` (${[failedReason, containerReason ? `container=${containerReason}/${containerExitCode ?? 'unknown'}` : ''].filter(Boolean).join(', ')})` : ''}${childFailure ? ` — childStage=${childFailure.stage} · childError=${childFailure.error}` : ' — childError=no-result-line'}` } }
948
1067
  : {}),
949
1068
  ...(finishedDisposition ? { disposition: finishedDisposition } : {}),
950
1069
  };
@@ -1006,6 +1125,23 @@ export function recordPodSalvage(logs: string, job: string, log: (category: stri
1006
1125
  return rows.map((row) => row.branch);
1007
1126
  }
1008
1127
 
1128
+ function lastPodChildFailure(logs: string): { stage: string; error: string } | null {
1129
+ for (const line of logs.split('\n').reverse()) {
1130
+ if (!line.trimStart().startsWith('{')) continue;
1131
+ let result: unknown;
1132
+ try { result = JSON.parse(line); } catch { continue; }
1133
+ if (!result || typeof result !== 'object' || Array.isArray(result)) continue;
1134
+ const row = result as Record<string, unknown>;
1135
+ if (typeof row.stage !== 'string' || typeof row.ok !== 'boolean') continue;
1136
+ if (row.ok !== false || typeof row.error !== 'string') return null;
1137
+ const lines = row.error.replace(/\/home\/[^/\s]+\//g, '~/').split(/\r\n|\n|\r/);
1138
+ const first = lines[0]!.slice(0, 240);
1139
+ const last = lines.at(-1)!.slice(0, 240);
1140
+ return { stage: row.stage, error: first === last ? first : `${first}\n${last}` };
1141
+ }
1142
+ return null;
1143
+ }
1144
+
1009
1145
  /** 사람용 런 결과 한 줄 — 수확 브랜치가 있으면 «수확할 브랜치: …» 를 붙인다. */
1010
1146
  export function podRunResultLine(logs: string, branches: readonly string[]): string {
1011
1147
  const result = logs.split('\n').filter((line) => !line.startsWith('ELANOUS_MEM ')).join('\n').slice(-4000);
@@ -1183,6 +1319,14 @@ export function podImageFreshness(deps: {
1183
1319
  : { imageCommit, headCommit, fresh: false, reason: `이미지 ${imageCommit.slice(0, 12)} ≠ HEAD ${headCommit.slice(0, 12)}` };
1184
1320
  }
1185
1321
 
1322
+ function configHostMirror(): string | undefined {
1323
+ try {
1324
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
1325
+ const { getUserConfig } = require('../../user-config.js') as typeof import('../../user-config.js');
1326
+ return getUserConfig().pod?.hostMirror;
1327
+ } catch { return undefined; }
1328
+ }
1329
+
1186
1330
  function configGroundingUrl(): string | undefined {
1187
1331
  try {
1188
1332
  // eslint-disable-next-line @typescript-eslint/no-require-imports