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
@@ -60,7 +60,7 @@ import { pwaReachableSrcFiles } from './pwa-import-graph.js';
60
60
  import { stageNonGitDir, stageFile, applyShadowToTarget, applyFileToTarget } from './shadow-stage.js';
61
61
  import type { TargetKind } from './target-kind.js';
62
62
  import { basename } from 'node:path';
63
- import { makePrManager, type PrManager } from '../autopilot/pr-manager.js';
63
+ import { ghAutomationEnv, makePrManager, type PrManager } from '../autopilot/pr-manager.js';
64
64
  import { defaultSpawnGh, lookupOpenDraftPrs } from '../cli/logs-abandoned-draft-prs.js';
65
65
  import { loadRunLedger, runLedgerDir } from './run-ledger.js';
66
66
  import { readdirSync } from 'node:fs';
@@ -2347,8 +2347,10 @@ export function defaultSeams(o: DefaultSeamsOptions = {}): SelfImplementSeams {
2347
2347
  closePr: (prUrl, comment) => (o.prManager ?? makePrManager()).closePr(prUrl, comment),
2348
2348
 
2349
2349
  postPrComment: async ({ number, body, cwd }) => {
2350
+ // Round comments go out as the GitHub App like the PR itself — a personal `gh` login made the owner a
2351
+ // participant and mailed them on merge (#22089).
2350
2352
  await execFileAsync('gh', ['pr', 'comment', String(number), '--body', body], {
2351
- cwd, encoding: 'utf8', timeout: GIT_TIMEOUT, maxBuffer: 64 * 1024 * 1024,
2353
+ cwd, encoding: 'utf8', timeout: GIT_TIMEOUT, maxBuffer: 64 * 1024 * 1024, env: ghAutomationEnv(process.env),
2352
2354
  });
2353
2355
  },
2354
2356
 
@@ -434,12 +434,20 @@ function readIndex(root: string = sessionRoot()): SessionMeta[] {
434
434
  }
435
435
  }
436
436
 
437
- function writeIndex(entries: SessionMeta[], root: string = sessionRoot()): void {
437
+ // 여러 프로세스(`elanous ask` 동시 실행 등)가 같은 목록을 «읽고-고치고-통째로 쓰기» 한다.
438
+ // 쓰기 직전 잠금 안에서 디스크의 최신 목록을 다시 읽어 id 로 합치고, 임시 파일 이름은 프로세스마다 다르게 한다
439
+ // (10-01 MK 재현: 고정 `index.json.tmp` → 한쪽 ENOENT · 덮어쓰기로 남의 세션이 목록에서 사라짐).
440
+ function writeIndex(entries: SessionMeta[], root: string = sessionRoot(), removed: Iterable<string> = []): void {
438
441
  ensureRoots(root);
439
442
  const p = indexPath(root);
440
- const tmp = p + '.tmp';
441
- writeFileSync(tmp, JSON.stringify(entries, null, 2) + '\n', 'utf-8');
442
- renameSync(tmp, p);
443
+ withDirLock(join(root, '.index.lock'), INDEX_LOCK_TIMEOUT_MS, () => {
444
+ const mine = new Set(entries.map((m) => m.id));
445
+ const gone = new Set(removed);
446
+ const peers = readIndex(root).filter((m) => !mine.has(m.id) && !gone.has(m.id));
447
+ const tmp = `${p}.${process.pid}.${randomUUID().slice(0, 8)}.tmp`;
448
+ writeFileSync(tmp, JSON.stringify([...peers, ...entries], null, 2) + '\n', 'utf-8');
449
+ renameSync(tmp, p);
450
+ });
443
451
  }
444
452
 
445
453
  // ── Tier 1 Phase 3 — listener primitive ──────────────────────────────
@@ -661,7 +669,7 @@ export function rewriteSessionMessages(
661
669
  const body = messages
662
670
  .map(m => JSON.stringify(m.turn_id !== undefined ? m : { ...m, turn_id: mintTurnUri() }))
663
671
  .join('\n');
664
- const tmp = file + '.tmp';
672
+ const tmp = `${file}.${process.pid}.${randomUUID().slice(0, 8)}.tmp`;
665
673
  writeFileSync(tmp, body ? body + '\n' : '', 'utf-8');
666
674
  renameSync(tmp, file);
667
675
 
@@ -800,8 +808,13 @@ function residentSessionLockPath(root: string): string {
800
808
 
801
809
  function withResidentSessionLock<T>(root: string, action: () => T): T {
802
810
  ensureRoots(root);
803
- const lock = residentSessionLockPath(root);
804
- const deadline = Date.now() + RESIDENT_SESSION_LOCK_TIMEOUT_MS;
811
+ return withDirLock(residentSessionLockPath(root), RESIDENT_SESSION_LOCK_TIMEOUT_MS, action);
812
+ }
813
+
814
+ const INDEX_LOCK_TIMEOUT_MS = 10_000;
815
+
816
+ function withDirLock<T>(lock: string, timeoutMs: number, action: () => T): T {
817
+ const deadline = Date.now() + timeoutMs;
805
818
  for (;;) {
806
819
  try {
807
820
  mkdirSync(lock);
@@ -814,7 +827,7 @@ function withResidentSessionLock<T>(root: string, action: () => T): T {
814
827
  continue;
815
828
  }
816
829
  } catch { continue; }
817
- if (Date.now() >= deadline) throw new Error(`timed out acquiring resident session lock for ${root}`);
830
+ if (Date.now() >= deadline) throw new Error(`timed out acquiring lock ${lock}`);
818
831
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
819
832
  void error;
820
833
  }
@@ -855,7 +868,7 @@ export function deleteSession(id: string, root: string = sessionRoot()): boolean
855
868
  const before = idx.length;
856
869
  const next = idx.filter(m => m.id !== id);
857
870
  if (next.length === before) return false;
858
- writeIndex(next, root);
871
+ writeIndex(next, root, [id]);
859
872
  // Clear active marker if it pointed at the deleted session.
860
873
  if (getActiveSessionId() === id) clearActiveSessionId();
861
874
  return true;
@@ -876,7 +889,7 @@ export function deleteSessions(ids: string[], root: string = sessionRoot()): num
876
889
  const idx = readIndex(root);
877
890
  const next = idx.filter(m => !set.has(m.id));
878
891
  const removed = idx.length - next.length;
879
- if (removed > 0) writeIndex(next, root);
892
+ if (removed > 0) writeIndex(next, root, set);
880
893
  const active = getActiveSessionId();
881
894
  if (active && set.has(active)) clearActiveSessionId();
882
895
  return removed;
@@ -13,7 +13,9 @@
13
13
  // ★ 제1원칙: front door 결정 observe(harness.frontdoor) — objective digest·autoDrive·terminal.
14
14
  // autoDrive 'safe'(기본): 저위험 자율·고위험(PR open)만 ux.confirm(막 채널 없으면 fail-closed).
15
15
 
16
+ import { createHash } from 'node:crypto';
16
17
  import { tierModel } from '../../llm/model-defaults.js';
18
+ import { dispatchTask, type DispatchTaskDeps } from '../../execution-loop/dispatch-task.js';
17
19
  import type { LLMToolSpec } from '../../llm.js';
18
20
  import type { DaemonToolDispatchCtx } from '../../boot/daemon-tools/types.js';
19
21
  import type { SelfImplementSeams } from '../../self-implement/orchestrator.js';
@@ -96,6 +98,9 @@ export function buildRunDevHarnessTool(): LLMToolSpec {
96
98
  export interface DevHarnessDeps {
97
99
  /** 하니스 구동(기본 runStagedHarnessOnSurface). */
98
100
  runHarness?: (opts: RunHarnessOnSurfaceOptions) => Promise<HarnessResult>;
101
+ /** In-process 실행 직전의 관측 전용 디스패치 seam. */
102
+ dispatchTask?: typeof dispatchTask;
103
+ dispatchTaskDeps?: DispatchTaskDeps;
99
104
  /** seam 팩토리(기본 defaultSeams). */
100
105
  seamsFactory?: (o: DefaultSeamsOptions) => SelfImplementSeams;
101
106
  /** 비대화형 HITL relay 팩토리 seam(기본 buildLlmHitlRelay). */
@@ -468,6 +473,43 @@ export async function dispatchRunDevHarness(
468
473
  const runHarness = deps?.runHarness ?? runStagedHarnessOnSurface;
469
474
  const knobs = resolveHarnessKnobs(rawArgs, ctx?.userText); // ★ B1 관통 — C1~C4 노브를 surface 파라미터에서 막으로
470
475
  const harnessMention = harnessMentionState(ctx?.userText);
476
+ // Outer pipeline/Pod observations are not repeated; this decision never controls the harness.
477
+ const dispatchSkipped = rawArgs.plan_staged === true ? 'plan-staged'
478
+ : rawArgs.dispatch_recorded === true || process.env.ELANOUS_DISPATCH_RECORDED === '1' ? 'dispatch-recorded'
479
+ : process.env.ELANOUS_EXECUTION_LOOP_DISPATCH === 'off' && !deps?.dispatchTask && !deps?.dispatchTaskDeps ? 'disabled'
480
+ : undefined;
481
+ if (dispatchSkipped) {
482
+ try { debug.log('execution-loop.gate', 'dispatch-skipped', { reason: dispatchSkipped, runId }); } catch { /* fail-soft */ }
483
+ } else {
484
+ try {
485
+ const spec = { input: { text: objective }, executor: { kind: 'self' as const }, plan: true, runId };
486
+ const observer = new AbortController();
487
+ const timeoutMs = Math.min(5_000, Math.max(0, deps?.dispatchTaskDeps?.timeoutMs ?? 5_000));
488
+ let timer: ReturnType<typeof setTimeout> | undefined;
489
+ try {
490
+ await Promise.race([
491
+ Promise.resolve().then(() => (deps?.dispatchTask ?? dispatchTask)({
492
+ goalId: `request-${createHash('sha256').update(objective).digest('hex').slice(0, 32)}`,
493
+ title: objective.split(/\r?\n/, 1)[0]?.trim() || 'Untitled request',
494
+ goalText: objective,
495
+ targetPaths: [],
496
+ spec,
497
+ runKey: runId,
498
+ }, { ...deps?.dispatchTaskDeps, signal: observer.signal })),
499
+ new Promise<never>((_, reject) => {
500
+ timer = setTimeout(() => {
501
+ observer.abort();
502
+ reject(new Error(`dispatch observation timed out after ${timeoutMs}ms`));
503
+ }, timeoutMs);
504
+ }),
505
+ ]);
506
+ } finally {
507
+ if (timer) clearTimeout(timer);
508
+ }
509
+ } catch (error) {
510
+ try { debug.log('execution-loop.gate', 'dispatch-unavailable', { reason: String(error), runId }); } catch { /* fail-soft */ }
511
+ }
512
+ }
471
513
  const result = await runHarness({
472
514
  // ⛔⭐ `naturalLanguageDispatch` 를 «단정»하지 않는다 — 증거로 정한다.
473
515
  // 이 함수는 «둘» 이 지난다: ⑴ 모델이 부른 도구(사용자 턴 문면이 ctx.userText 로 온다)
@@ -0,0 +1,45 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { CardStore, redactSecrets, type AppendSectionInput, type CreateCardInput, type TaskCard } from '../task-cards/card-store.js';
3
+ import type { ScheduledDecision, TriageIssue } from './triage.js';
4
+
5
+ export interface StewardCardStore {
6
+ createCard(input: CreateCardInput): TaskCard;
7
+ appendSection(id: string, input: AppendSectionInput): TaskCard;
8
+ close(): void;
9
+ }
10
+
11
+ export interface StewardCardDeps {
12
+ store?: StewardCardStore;
13
+ root?: string;
14
+ now?: () => Date;
15
+ }
16
+
17
+ /** Record observe-only intake and the latest scheduled judgment without launching any work. */
18
+ export function recordTriageOnCards(decisions: ScheduledDecision[], issues: TriageIssue[], deps: StewardCardDeps = {}): void {
19
+ const store = deps.store ?? new CardStore(deps.root);
20
+ const byIssue = new Map(issues.map(issue => [issue.identifier, issue]));
21
+ try {
22
+ for (const decision of decisions) {
23
+ const issue = byIssue.get(decision.issue);
24
+ if (!issue) throw new Error(`Missing issue ${decision.issue}`);
25
+ const card = store.createCard({ goalId: `linear:${issue.identifier}`, title: redactSecrets(issue.title) });
26
+ const intakeKey = `intake:${issue.identifier}`;
27
+ if (!card.sections.some(section => section.key === intakeKey)) {
28
+ const directiveSource = /^출처: (telegram|pwa|tui|cli)$/m.exec(issue.body)?.[1];
29
+ const directiveTime = /^시각: (\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z)$/m.exec(issue.body)?.[1];
30
+ store.appendSection(card.id, {
31
+ key: intakeKey, owner: 'steward',
32
+ content: JSON.stringify({ summary: redactSecrets(issue.title), source: directiveSource ?? `linear:${issue.identifier}`, at: directiveTime ?? (deps.now ?? (() => new Date()))().toISOString() }),
33
+ });
34
+ }
35
+ const { rung, dependsOn, priority, disposition, hitlReason, why, owner } = decision;
36
+ const judgment = { rung, dependsOn, priority, disposition: rung === 'hitl' || hitlReason ? 'hitl' : disposition, hitlReason: hitlReason ?? null, why: redactSecrets(why), ...(owner ? { owner } : {}) };
37
+ const key = `triage:${createHash('sha256').update(JSON.stringify(judgment)).digest('hex')}`;
38
+ if (!card.sections.some(section => section.key === key)) {
39
+ store.appendSection(card.id, { key, owner: 'steward', content: JSON.stringify(judgment) });
40
+ }
41
+ }
42
+ } finally {
43
+ if (!deps.store) store.close();
44
+ }
45
+ }
@@ -4,30 +4,39 @@ import { fetchLinearIssues } from '../connectors/linear.js';
4
4
  import { getSecretAsync } from '../nexus/config/secrets/index.js';
5
5
  import { getUserConfig } from '../user-config.js';
6
6
  import { emitDecision } from '../live/detail-switch.js';
7
+ import { debug, redactSecretText } from '../debug/log.js';
7
8
  import { effectiveInstanceRoot } from '../instance/resolve.js';
9
+ import { redactSecrets } from '../task-cards/card-store.js';
8
10
  import { directiveHash } from './directive.js';
11
+ import { recordTriageOnCards, type StewardCardDeps } from './steward-cards.js';
9
12
 
10
13
  export type Rung = 0 | 1 | 2 | 3 | 4 | 5 | 'hitl';
11
- export type HitlReason = 'money' | 'public' | 'security' | 'irreversible';
14
+ /** `other` = the judge chose human review for a reason outside the four named ones (kept as HITL, never downgraded). */
15
+ export type HitlReason = 'money' | 'public' | 'security' | 'irreversible' | 'other';
12
16
  export interface TriageIssue { identifier: string; ref: string; title: string; body: string }
13
17
  export interface TriageDecision {
14
18
  issue: string; rung: Rung; dependsOn: string[]; priority: number; duplicateOf?: string;
15
19
  hitlReason?: HitlReason; why: string; role?: string; cost?: number;
20
+ /** Owner track from `loops.steward.tracks` (absent when no table is configured or the judge picked none of its keys). */
21
+ owner?: string;
16
22
  }
17
23
  export interface ScheduledDecision extends TriageDecision { disposition: 'now' | 'wait' | 'hitl' }
18
24
  export interface StewardSettings {
19
25
  mode?: 'observe' | 'act'; linearTeam?: string;
20
26
  roles?: Record<string, { maxConcurrent?: number }>;
21
27
  budget?: number;
28
+ tracks?: Record<string, string>;
22
29
  }
23
30
  export interface StewardDeps {
24
31
  fetch?: typeof fetch;
25
32
  getSecret?: (id: string) => Promise<string | undefined>;
26
- judge?: (issue: TriageIssue, issues: TriageIssue[]) => Promise<unknown>;
33
+ judge?: (issue: TriageIssue, issues: TriageIssue[], tracks?: Record<string, string>) => Promise<unknown>;
27
34
  decide?: typeof emitDecision;
28
35
  sendDigest?: (text: string) => Promise<void>;
29
36
  now?: () => Date;
30
37
  root?: string;
38
+ cardStore?: StewardCardDeps['store'];
39
+ warn?: (message: string) => void;
31
40
  }
32
41
 
33
42
  export function stewardSettings(): StewardSettings {
@@ -41,7 +50,7 @@ const unsafe: Array<[HitlReason, RegExp]> = [
41
50
  ['irreversible', /비가역|강제\s*푸시|force.push|영구\s*삭제|리셋권\s*소비|drop\s*table|초기화/i],
42
51
  ];
43
52
 
44
- function parseJudgment(issue: TriageIssue, raw: unknown): TriageDecision {
53
+ function parseJudgment(issue: TriageIssue, raw: unknown, tracks?: Record<string, string>): TriageDecision {
45
54
  const value = typeof raw === 'string' ? JSON.parse(raw) as unknown : raw;
46
55
  if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`Invalid triage judgment: ${issue.identifier}`);
47
56
  const d = value as Record<string, unknown>;
@@ -51,8 +60,9 @@ function parseJudgment(issue: TriageIssue, raw: unknown): TriageDecision {
51
60
  throw new Error(`Invalid triage judgment: ${issue.identifier}`);
52
61
  }
53
62
  const forced = unsafe.find(([, pattern]) => pattern.test(`${issue.title}\n${issue.body}`))?.[0];
54
- const hitlReason = forced ?? (unsafe.some(([reason]) => reason === d.hitlReason) ? d.hitlReason as HitlReason : undefined);
55
- if (d.rung === 'hitl' && !hitlReason) throw new Error(`HITL reason required: ${issue.identifier}`);
63
+ const named = forced ?? (unsafe.some(([reason]) => reason === d.hitlReason) ? d.hitlReason as HitlReason : undefined);
64
+ // A HITL judgment without one of the four named reasons stays HITL (`other`) — one issue must not stop the stage.
65
+ const hitlReason = named ?? (d.rung === 'hitl' ? 'other' as const : undefined);
56
66
  return {
57
67
  issue: issue.identifier, rung: hitlReason ? 'hitl' : d.rung as Rung,
58
68
  dependsOn: d.dependsOn as string[], priority: d.priority as number,
@@ -60,24 +70,40 @@ function parseJudgment(issue: TriageIssue, raw: unknown): TriageDecision {
60
70
  ...(hitlReason ? { hitlReason } : {}), why: (d.why as string).trim().replace(/\s+/g, ' '),
61
71
  ...(typeof d.role === 'string' ? { role: d.role } : {}),
62
72
  ...(typeof d.cost === 'number' && Number.isFinite(d.cost) && d.cost >= 0 ? { cost: d.cost } : {}),
73
+ ...(tracks && typeof d.owner === 'string' && Object.hasOwn(tracks, d.owner) ? { owner: d.owner } : {}),
63
74
  };
64
75
  }
65
76
 
66
- async function judgeStewardIssue(issue: TriageIssue, issues: TriageIssue[]): Promise<unknown> {
67
- const prompt = `스튜어드 트리아지. JSON 객체만 출력: {"rung":0|1|2|3|4|5|"hitl","dependsOn":[],"priority":1,"duplicateOf":null,"hitlReason":null,"why":"한 문장","role":"builder","cost":0}.\n0=이미됨/중복, 1=셸, 2=CLI, 3=조사, 4=하니스 골, 5=외부 에이전트. 돈·공개·보안·비가역은 hitl. 확인되지 않은 완료/중복은 추측하지 마라. 의존은 제공된 이슈 키만 써라.\n이슈 목록: ${JSON.stringify(issues.map(i => ({ key: i.identifier, title: i.title })))}\n판정 대상: ${JSON.stringify(issue)}`;
77
+ /** The role table as prompt lines — read from config so a reorg edits one place. */
78
+ export function trackTablePrompt(tracks?: Record<string, string>): string {
79
+ if (!tracks || !Object.keys(tracks).length) return '';
80
+ const keys = Object.keys(tracks);
81
+ return `\n담당 트랙(owner) — 아래 중 하나(${keys.map(k => JSON.stringify(k)).join('|')}) · 이슈가 어느 트랙 일인지:\n${keys.map(k => `- ${k}: ${tracks[k]}`).join('\n')}`;
82
+ }
83
+
84
+ async function judgeStewardIssue(issue: TriageIssue, issues: TriageIssue[], tracks?: Record<string, string>): Promise<unknown> {
85
+ const ownerField = tracks && Object.keys(tracks).length ? `,"owner":${JSON.stringify(Object.keys(tracks)[0])}` : '';
86
+ const prompt = `스튜어드 트리아지. JSON 객체만 출력: {"rung":0|1|2|3|4|5|"hitl","dependsOn":[],"priority":1,"duplicateOf":null,"hitlReason":null,"why":"한 문장","role":"builder","cost":0${ownerField}}.\n0=이미됨/중복, 1=셸, 2=CLI, 3=조사, 4=하니스 골, 5=외부 에이전트. 돈·공개·보안·비가역은 hitl(hitlReason = "money"|"public"|"security"|"irreversible" · 그 밖의 이유로 사람 확인이 필요하면 "other"). 확인되지 않은 완료/중복은 추측하지 마라. 의존은 제공된 이슈 키만 써라.${trackTablePrompt(tracks)}\n이슈 목록: ${JSON.stringify(issues.map(i => ({ key: i.identifier, title: i.title })))}\n판정 대상: ${JSON.stringify(issue)}`;
68
87
  const { streamLLM } = await import('../llm.js');
69
88
  const { tierModel } = await import('../llm/model-defaults.js');
70
89
  const result = await streamLLM([{ role: 'user', content: prompt }], () => {}, { model: tierModel('best') });
71
90
  return JSON.parse(result.replace(/^```(?:json)?\s*|\s*```$/g, '').trim()) as unknown;
72
91
  }
73
92
 
74
- export async function triageIssues(issues: TriageIssue[], judge: NonNullable<StewardDeps['judge']> = judgeStewardIssue, decide: typeof emitDecision = emitDecision): Promise<TriageDecision[]> {
93
+ export async function triageIssues(issues: TriageIssue[], judge: NonNullable<StewardDeps['judge']> = judgeStewardIssue, decide: typeof emitDecision = emitDecision, tracks?: Record<string, string>): Promise<TriageDecision[]> {
75
94
  const decisions: TriageDecision[] = [];
76
95
  for (const issue of issues) {
77
- const result = parseJudgment(issue, await judge(issue, issues));
96
+ let result: TriageDecision;
97
+ try {
98
+ result = parseJudgment(issue, await judge(issue, issues, tracks), tracks);
99
+ } catch (error) {
100
+ // A malformed or failed judgment for one issue goes to a human; the other issues are still triaged.
101
+ try { debug.log('steward.stage', 'judgment-invalid', { issue: issue.identifier, reason: redactSecrets(String(error)) }); } catch { /* fail-soft */ }
102
+ result = { issue: issue.identifier, rung: 'hitl', hitlReason: 'other', dependsOn: [], priority: 0, why: 'triage judgment unavailable — human review' };
103
+ }
78
104
  decisions.push(result);
79
105
  decide({ kind: result.rung === 'hitl' ? 'ESCALATE' : 'ROUTE', what: issue.identifier, reason: result.why,
80
- purpose: 'steward triage', target: String(result.rung), refs: { issue: issue.identifier } });
106
+ purpose: 'steward triage', target: result.owner ? `${result.rung} · ${result.owner}` : String(result.rung), refs: { issue: issue.identifier } });
81
107
  }
82
108
  return decisions;
83
109
  }
@@ -99,7 +125,7 @@ export function scheduleTriage(decisions: TriageDecision[], settings: StewardSet
99
125
  const slots = { ...running };
100
126
  let remaining = settings.budget ?? Infinity;
101
127
  return sorted.map(item => {
102
- const role = item.role ?? String(item.rung);
128
+ const role = item.owner ?? item.role ?? String(item.rung);
103
129
  const max = settings.roles?.[role]?.maxConcurrent ?? Infinity;
104
130
  const blocked = item.dependsOn.some(dep => !completed.has(dep));
105
131
  const status: ScheduledDecision['disposition'] = item.rung === 'hitl' || item.hitlReason ? 'hitl' :
@@ -146,7 +172,7 @@ export async function runStewardStage(stage: 'sync' | 'triage' | 'schedule' | 'r
146
172
  } else if (stage === 'triage') {
147
173
  const issues = JSON.parse(readFileSync(snapshot, 'utf8')) as TriageIssue[];
148
174
  const judge = deps.judge ?? judgeStewardIssue;
149
- writeFileSync(judgments, JSON.stringify(await triageIssues(issues, judge, deps.decide)));
175
+ writeFileSync(judgments, JSON.stringify(await triageIssues(issues, judge, deps.decide, settings.tracks)));
150
176
  } else if (stage === 'schedule') {
151
177
  const decisions = JSON.parse(readFileSync(judgments, 'utf8')) as TriageDecision[];
152
178
  const pending = new Set(decisions.flatMap(decision => decision.dependsOn));
@@ -163,6 +189,13 @@ export async function runStewardStage(stage: 'sync' | 'triage' | 'schedule' | 'r
163
189
  const state = readState(path);
164
190
  const at = (deps.now ?? (() => new Date()))().toISOString();
165
191
  const issues = JSON.parse(readFileSync(snapshot, 'utf8')) as TriageIssue[];
192
+ try {
193
+ recordTriageOnCards(rows, issues, { root, store: deps.cardStore, now: deps.now });
194
+ } catch (error) {
195
+ const message = `steward report: card write failed: ${redactSecrets(error instanceof Error ? error.message : String(error))}`;
196
+ debug.log('steward.cards', 'write-failed', { error: message });
197
+ (deps.warn ?? console.warn)(message);
198
+ }
166
199
  for (const row of rows) {
167
200
  const issue = issues.find(item => item.identifier === row.issue);
168
201
  if (!issue) throw new Error(`Missing issue ${row.issue}`);
@@ -195,8 +228,11 @@ if (import.meta.main) {
195
228
  console.error('steward: expected sync|triage|schedule|report');
196
229
  process.exitCode = 2;
197
230
  } else {
198
- runStewardStage(stage as 'sync' | 'triage' | 'schedule' | 'report').catch(() => {
199
- console.error(`steward ${stage}: stage failed`);
231
+ runStewardStage(stage as 'sync' | 'triage' | 'schedule' | 'report').catch((error: unknown) => {
232
+ // Say why (secrets masked) — 09-29 the first resident run failed as a bare «stage failed» on a missing Linear key.
233
+ const reason = redactSecretText(error instanceof Error ? error.message : String(error)).slice(0, 300);
234
+ console.error(`steward ${stage}: stage failed — ${reason}`);
235
+ debug.log('steward.stage', 'failed', { stage, reason });
200
236
  process.exitCode = 1;
201
237
  });
202
238
  }
@@ -2,6 +2,8 @@ import { lstatSync, mkdirSync, writeFileSync } from 'node:fs';
2
2
  import { isAbsolute, join, relative, resolve, sep } from 'node:path';
3
3
  import { gunzipSync } from 'node:zlib';
4
4
  import { debug } from '../../debug/log.js';
5
+ import { getDefaultLogStore, type LogStore } from '../../mss/logging/log-store.js';
6
+ import type { LogRecord } from '../../mss/logging/record.js';
5
7
 
6
8
  const FILE_LIMIT = 5 * 1024 * 1024;
7
9
  const TOTAL_LIMIT = 20 * 1024 * 1024;
@@ -79,14 +81,63 @@ export function parsePodArtifactChunks(logs: string): ParseResult {
79
81
  return error.length ? { error, artifacts } : artifacts;
80
82
  }
81
83
 
84
+ const POD_LOGS_PATH = 'pod-logs/logs.jsonl';
85
+
86
+ function childRunId(data: unknown): string | undefined {
87
+ if (!data || typeof data !== 'object' || Array.isArray(data)) return undefined;
88
+ const value = (data as Record<string, unknown>).runId;
89
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
90
+ }
91
+
92
+ function decisionRecord(line: string): { rec: LogRecord; surface: string } | undefined {
93
+ let parsed: unknown;
94
+ try { parsed = JSON.parse(line); } catch { return undefined; }
95
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return undefined;
96
+ const row = parsed as Record<string, unknown>;
97
+ if (row.category !== 'harness.decision') return undefined;
98
+ if (typeof row.ts !== 'string' || typeof row.event !== 'string') return undefined;
99
+ const runId = childRunId(row.data);
100
+ if (!runId) return undefined;
101
+ const data = { ...(row.data as Record<string, unknown>), podJob: '', importedFrom: 'pod' as const };
102
+ return {
103
+ rec: {
104
+ ts: row.ts,
105
+ category: 'harness.decision',
106
+ event: row.event,
107
+ data,
108
+ ...(typeof row.level === 'string' ? { level: row.level as LogRecord['level'] } : {}),
109
+ ...(typeof row.trace_id === 'string' ? { trace_id: row.trace_id } : {}),
110
+ },
111
+ surface: typeof row.surface === 'string' && row.surface ? row.surface : 'pod',
112
+ };
113
+ }
114
+
115
+ /** Import only harness.decision rows from a returned pod log into the launching host store. */
116
+ export function importPodDecisionLogs(bytes: Buffer, job: string, store: LogStore | null = getDefaultLogStore()): { imported: number; skipped: number } {
117
+ if (!store) return { imported: 0, skipped: 0 };
118
+ const text = bytes.toString('utf8');
119
+ const rows: Array<{ rec: LogRecord; surface: string }> = [];
120
+ let skipped = 0;
121
+ for (const line of text.split(/\r?\n/)) {
122
+ if (!line) continue;
123
+ const row = decisionRecord(line);
124
+ if (!row) { skipped += 1; continue; }
125
+ row.rec.data = { ...(row.rec.data as Record<string, unknown>), podJob: job };
126
+ rows.push(row);
127
+ }
128
+ if (rows.length > 0) store.insertBatch(rows);
129
+ return { imported: rows.length, skipped };
130
+ }
131
+
82
132
  /** Reject symlink parents as well as lexical traversal; never replace a host file. */
83
- export function collectPodArtifacts(logs: string, { dir, job, log = (c, e, d) => debug.log(c, e, d) }: {
133
+ export function collectPodArtifacts(logs: string, { dir, job, log = (c, e, d) => debug.log(c, e, d), logStore = getDefaultLogStore() }: {
84
134
  dir: string;
85
135
  job: string;
86
136
  log?: (category: string, event: string, data: Record<string, unknown>) => void;
137
+ logStore?: LogStore | null;
87
138
  }): void {
88
139
  const parsed = parsePodArtifactChunks(logs);
89
- const podLogsPath = 'pod-logs/logs.jsonl';
140
+ const podLogsPath = POD_LOGS_PATH;
90
141
  let podLogsReturned = false;
91
142
  let podLogsMissingReason = logs.split(/\r?\n/).find((line) => line.startsWith(`ELANOUS_POD_ARTIFACT_SKIPPED ${podLogsPath} `))
92
143
  ? 'artifact skipped (size limit or encoding failure)'
@@ -123,7 +174,14 @@ export function collectPodArtifacts(logs: string, { dir, job, log = (c, e, d) =>
123
174
  log('self-implement.pod', 'artifact-collected', { job, path, bytes: bytes.length });
124
175
  if (path === podLogsPath) {
125
176
  podLogsReturned = true;
126
- log('self-implement.pod', 'pod-logs-returned', { job, path: destination, lines: bytes.length ? bytes.toString('utf8').split('\n').length - (bytes.at(-1) === 10 ? 1 : 0) : 0 });
177
+ const lines = bytes.length ? bytes.toString('utf8').split('\n').length - (bytes.at(-1) === 10 ? 1 : 0) : 0;
178
+ log('self-implement.pod', 'pod-logs-returned', { job, path: destination, lines });
179
+ try {
180
+ const imported = importPodDecisionLogs(bytes, job, logStore);
181
+ log('self-implement.pod', 'pod-decisions-imported', { job, imported: imported.imported, skipped: imported.skipped });
182
+ } catch (error) {
183
+ log('self-implement.pod', 'pod-logs-import-failed', { job, reason: error instanceof Error ? error.message : String(error) });
184
+ }
127
185
  }
128
186
  } catch (e) {
129
187
  const reason = (e as NodeJS.ErrnoException).code === 'EEXIST' ? 'exists' : e instanceof Error ? e.message : String(e);
@@ -0,0 +1,18 @@
1
+ export const POD_BUN_CACHE_HOST_PATH = 'POD_BUN_CACHE_HOST_PATH';
2
+
3
+ /** A host-backed Bun install cache; only opt into it when the mounted directory is writable. */
4
+ export function podBunCacheVolume(hostPath: string) {
5
+ return {
6
+ volume: { name: 'bun-cache', hostPath: { path: hostPath, type: 'DirectoryOrCreate' } },
7
+ volumeMount: { name: 'bun-cache', mountPath: '/bun-cache', readOnly: false },
8
+ shellPrefix: 'if [ -d /bun-cache ] && [ -w /bun-cache ]; then export BUN_INSTALL_CACHE_DIR=/bun-cache; fi;',
9
+ };
10
+ }
11
+
12
+ /** First Bun install timing in a combined shard log (including a no-op install). */
13
+ export function parseInstallSeconds(log: string): number | null {
14
+ const match = /(?:\d+ packages? installed|Checked \d+ installs across \d+ packages(?: \(no changes\))?)\s*\[(\d+(?:\.\d+)?)(ms|s)\]/.exec(log);
15
+ if (!match) return null;
16
+ const duration = Number(match[1]);
17
+ return match[2] === 'ms' ? duration / 1000 : duration;
18
+ }
@@ -6,12 +6,15 @@
6
6
  // 명령 인자는 셸 문자열로 이어붙이지 않고 `"$@"` 로 따로 인용한다.
7
7
 
8
8
  import { spawnSync } from 'node:child_process';
9
+ import { isAbsolute } from 'node:path';
10
+ import { getUserConfig } from '../../user-config.js';
9
11
  import { debug } from '../../debug/log.js';
10
12
  import { effectiveInstanceRoot } from '../../instance/resolve.js';
11
13
  import { collectPodArtifacts } from './pod-artifact-return.js';
14
+ import { podBunCacheVolume } from './pod-bun-cache.js';
12
15
  import { type PodPoolMember, PodPoolScheduler, resolvePodPoolSpec, parsePodPool, checkPodPool, syncPoolImages, type PoolKubectl } from './pod-pool.js';
13
16
  import { readSkillEnvFiles } from './pod-skills.js';
14
- import { podSourceScript } from './pod-source-receive.js';
17
+ import { podSourceScript, type PodSource } from './pod-source-receive.js';
15
18
  import {
16
19
  POD_CHILD_REQUESTS,
17
20
  POD_JOB_DEADLINE_SECONDS,
@@ -73,6 +76,11 @@ export interface PodCommandJobInput {
73
76
  llm?: 'grok';
74
77
  /** 저장소를 clone 한다 — 비공개 저장소라 GitHub 토큰이 Secret 으로 간다(명시 opt-in). 기본은 clone 없이 `~/work` 에서 이미지의 `elanous` 로 돈다. */
75
78
  clone?: boolean;
79
+ hostMirror?: string;
80
+ bunCache?: string;
81
+ /** 컨테이너 메모리 한도(기본 16Gi) — 게이트가 파일 하나 격리 Job 에 올린다. */
82
+ memoryLimit?: string;
83
+ source?: PodSource;
76
84
  deadlineSeconds: number;
77
85
  runId?: string;
78
86
  }
@@ -88,6 +96,7 @@ export function podCommandJobManifest(o: PodCommandJobInput): Record<string, unk
88
96
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(name)) throw new Error(`pod command skill name rejected: ${name}`);
89
97
  }
90
98
  const grok = o.llm === 'grok';
99
+ const bunCache = o.bunCache ? podBunCacheVolume(o.bunCache) : undefined;
91
100
  const quotedArgs = o.command.map(bashSingleQuote).join(' ');
92
101
  const credLines = [
93
102
  ...(grok ? ['mkdir -p ~/.grok && install -m 600 /creds/grok-auth.json ~/.grok/auth.json'] : []),
@@ -100,8 +109,9 @@ export function podCommandJobManifest(o: PodCommandJobInput): Record<string, unk
100
109
  ...credLines,
101
110
  'curl -s -m 3 -o /dev/null http://host.orb.internal:31415/health && { echo "[pod] ISOLATION FAIL"; exit 3; }',
102
111
  ...(o.clone
103
- ? ['export GH_TOKEN="$(cat /creds/gh-token)" && gh auth setup-git', podSourceScript({ kind: 'default' }, o.repoUrl)]
112
+ ? ['export GH_TOKEN="$(cat /creds/gh-token)" && gh auth setup-git', podSourceScript(o.source ?? { kind: 'default' }, o.repoUrl)]
104
113
  : ['mkdir -p ~/work && cd ~/work']),
114
+ ...(bunCache ? [bunCache.shellPrefix] : []),
105
115
  `set -- ${quotedArgs}`,
106
116
  '"$@"',
107
117
  'rc=$?',
@@ -125,16 +135,24 @@ export function podCommandJobManifest(o: PodCommandJobInput): Record<string, unk
125
135
  initContainers: [{ name: 'isolation-gate', image: o.image, imagePullPolicy: o.imagePullPolicy ?? 'Never', command: ['bash', '-c'], args: [GATE] }],
126
136
  containers: [{
127
137
  name: 'child', image: o.image, imagePullPolicy: o.imagePullPolicy ?? 'Never',
128
- resources: { requests: { ...POD_CHILD_REQUESTS }, limits: { memory: '16Gi', cpu: '4' } },
138
+ resources: { requests: { ...POD_CHILD_REQUESTS }, limits: { memory: o.memoryLimit ?? '16Gi', cpu: '4' } },
129
139
  command: ['bash', '-c'], args: [script],
130
140
  env: [
131
141
  ...(o.runId ? [{ name: 'ELANOUS_RUN_ID', value: o.runId }] : []),
132
142
  { name: 'ELANOUS_SUBSTRATE', value: 'pod' },
133
143
  { name: 'ELANOUS_POD_NAME', valueFrom: { fieldRef: { fieldPath: 'metadata.name' } } },
134
144
  ],
135
- ...(secretKeys ? { volumeMounts: [{ name: 'creds', mountPath: '/creds', readOnly: true }] } : {}),
145
+ ...(secretKeys || o.hostMirror || bunCache ? { volumeMounts: [
146
+ ...(secretKeys ? [{ name: 'creds', mountPath: '/creds', readOnly: true }] : []),
147
+ ...(o.hostMirror ? [{ name: 'host-mirror', mountPath: '/host-mirror', readOnly: true }] : []),
148
+ ...(bunCache ? [bunCache.volumeMount] : []),
149
+ ] } : {}),
136
150
  }],
137
- ...(secretKeys ? { volumes: [{ name: 'creds', secret: { secretName: `${o.name}-creds`, defaultMode: 0o400 } }] } : {}),
151
+ ...(secretKeys || o.hostMirror || bunCache ? { volumes: [
152
+ ...(secretKeys ? [{ name: 'creds', secret: { secretName: `${o.name}-creds`, defaultMode: 0o400 } }] : []),
153
+ ...(o.hostMirror ? [{ name: 'host-mirror', hostPath: { path: o.hostMirror, type: 'Directory' } }] : []),
154
+ ...(bunCache ? [bunCache.volume] : []),
155
+ ] } : {}),
138
156
  },
139
157
  },
140
158
  },
@@ -188,6 +206,11 @@ export interface RunPodCommandOptions {
188
206
  skills?: readonly string[];
189
207
  llm?: 'grok';
190
208
  clone?: boolean;
209
+ hostMirror?: string;
210
+ bunCache?: string;
211
+ /** 컨테이너 메모리 한도(기본 16Gi). */
212
+ memoryLimit?: string;
213
+ source?: PodSource;
191
214
  ghToken?: () => string;
192
215
  deadlineSeconds?: number;
193
216
  namespace?: string;
@@ -207,6 +230,7 @@ export interface RunPodCommandOptions {
207
230
  sleep?: (ms: number) => Promise<void>;
208
231
  log?: (category: string, event: string, data: Record<string, unknown>) => void;
209
232
  env?: NodeJS.ProcessEnv;
233
+ configHostMirror?: () => string | undefined;
210
234
  }
211
235
 
212
236
  function jobFinished(types: string): 'complete' | 'failed' | null {
@@ -243,6 +267,11 @@ export async function runPodCommand(options: RunPodCommandOptions): Promise<PodC
243
267
  const deadlineSeconds = options.deadlineSeconds ?? POD_COMMAND_DEADLINE_SECONDS;
244
268
  const name = options.name ?? podJobName(`cmd-${Date.now().toString(36)}`);
245
269
  const env = options.env ?? process.env;
270
+ const hostMirror = (options.hostMirror ?? env.ELANOUS_POD_HOST_MIRROR
271
+ ?? (options.configHostMirror ?? (() => getUserConfig().pod?.hostMirror))())?.trim() || undefined;
272
+ if (hostMirror && !isAbsolute(hostMirror)) throw new Error('pod.hostMirror must be an absolute directory path');
273
+ const bunCache = options.bunCache?.trim() || undefined;
274
+ if (bunCache && !isAbsolute(bunCache)) throw new Error('pod.bunCache must be an absolute directory path');
246
275
 
247
276
  let member: PodPoolMember | null = null;
248
277
  let pool = options.poolScheduler;
@@ -307,7 +336,8 @@ export async function runPodCommand(options: RunPodCommandOptions): Promise<PodC
307
336
  const manifest = podCommandJobManifest({
308
337
  name, namespace, image: member?.imageRef ?? image,
309
338
  ...(member?.imageRef ? { imagePullPolicy: 'IfNotPresent' as const } : {}),
310
- repoUrl, command, skills, ...(llm ? { llm } : {}), ...(options.clone ? { clone: true } : {}), deadlineSeconds,
339
+ repoUrl, command, skills, ...(llm ? { llm } : {}), ...(options.clone ? { clone: true } : {}),
340
+ ...(options.source ? { source: options.source } : {}), ...(hostMirror ? { hostMirror } : {}), ...(bunCache ? { bunCache } : {}), ...(options.memoryLimit ? { memoryLimit: options.memoryLimit } : {}), deadlineSeconds,
311
341
  ...(options.runId ? { runId: options.runId } : {}),
312
342
  });
313
343
  kubectl(['-n', namespace, 'delete', 'job', name, '--ignore-not-found']);