elanous 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (412) hide show
  1. package/README.md +3 -3
  2. package/apps/pwa/out/404/index.html +1 -1
  3. package/apps/pwa/out/404.html +1 -1
  4. package/apps/pwa/out/_next/static/chunks/06ef2eba-d1b99ac6d2ea663c.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/1413-7179a49332db5a80.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/150-25c277137a9638ab.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/1640-bc99ec23bbe29d12.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/{2415.ed6428ff334c7ce2.js → 1751.50b96ea9c37b49b6.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/2300-de059d0ab2a16630.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/2684-05b0a81e75298c16.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/3026-2ed7e317d9b02102.js +25 -0
  12. package/apps/pwa/out/_next/static/chunks/3611-77496ada38cd6358.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/3717-68957c4dc9ceec90.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/3929-46ba9af6f9d895cc.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/4015-62c5a3744698a8da.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/4217-3c4317a7905f9eeb.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/4240-3230f230fae2d7d5.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/4271-e53e075ebd48593b.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/4377.3b125fef79576dfd.js +5 -0
  20. package/apps/pwa/out/_next/static/chunks/4440-18bb160d95819f4e.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/4853-35fdbe6bdbcdc0fc.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/{2ea96958-e8c7e112de50b067.js → 4daa2798-1363d725c79f3593.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/5007-6a3b30e23ff68a6e.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/5244-12b424c42070f5ff.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/{5791-bb82743fa6299562.js → 5348-bbf838e119661eee.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/5391-8793f88b03b30161.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/5611-df00af3e4e4a61bb.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/5890-dca2321773be829a.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/6123-37c9dc57ddaa7ab0.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/6588-b7975304a302ab91.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/6604-c03961962b6273b6.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/7109-cf8c87e968e83a65.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/7178-30f77877525a2efb.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/7262-871563fc9663730b.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/7294-f0eeebb92aa93459.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/7429-881af44deed6958f.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/7527.943753b6ed829f09.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/764-a01e858525a190c0.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/7868-e35d5f7985ecd3f1.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/8320-51484af7b01fdfa5.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/{4323.c5dfb6f503304775.js → 8617.465ca22a319c95b3.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/{5080-1613752891fcadb4.js → 8918-482b30ce2a8ebf4c.js} +3 -3
  43. package/apps/pwa/out/_next/static/chunks/9131-52f5fad51cceca04.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/9238.2cd5f47f4bf7a2c3.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.js → 9704-3221c102623c8d73.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-70c404c7bb6d7f0f.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/approvals/page-3ffd80166b1050aa.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f89e9a303515a033.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/board/page-4f403cd060442c95.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-9fa879ebf5959096.js → page-0ac5d053f0444d21.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/bots/page-4bcc63e272ae8205.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/chat/page-880734bd07d62882.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/control/{page-4472ddf1e2458e69.js → page-72bd384898d88955.js} +1 -1
  54. package/apps/pwa/out/_next/static/chunks/app/design-check/page-1018bbc9586ecae5.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/editor/page-de86f8c38076dd7b.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/healthz/route-70c404c7bb6d7f0f.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/intake/page-cd6447196d8bafe6.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/layout-e733630c47fa102b.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/live/page-d1c1a8357b161875.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/market/page-60c514128e81d787.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-8b637b7e325e13c1.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/morning/{page-112f7e477bda77fd.js → page-bccce0babceae30f.js} +1 -1
  63. package/apps/pwa/out/_next/static/chunks/app/not-found-63301d11dbe73836.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/observatory/page-a8cf0dc472060db2.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/page-02fba54881ccfa49.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/reflection/page-d5e8e90f0038f8de.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-b1ef6824f494466a.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/sessions/page-d589bb0f233b1009.js +1 -0
  69. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-c9bbaebef4cda059.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/settings/page-3639ce7916b92fed.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-7450ae7478955653.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/setup/layout-70c404c7bb6d7f0f.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/setup/page-b41ca4abfd618452.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/share/page-b333dd79ebee3fa9.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/showroom/page-8be1227492793655.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/tasks/page-6e03e9371b552e0d.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/term/page-911411e9fca18d03.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/trace/page-792170e649b37088.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/vault/page-c40463466937bc91.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-509c798b80384536.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/workflows/page-1cac2aa21d77c417.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/workspace/page-9626799d9b4a2cd1.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-84cd419494e16b0f.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/{8046cadf-211ca3c0e00f2b72.js → f6f3850d-7f324c6566002113.js} +1 -1
  85. package/apps/pwa/out/_next/static/chunks/framework-361487d2da603917.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/main-app-607f5beb94cb2e07.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/main-d0222d219e29dddf.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/pages/_app-a26980a31126decc.js +1 -0
  89. package/apps/pwa/out/_next/static/chunks/pages/_error-edb2b37decf23e4d.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/{webpack-e5bea36f183b25e6.js → webpack-2bf59c720fe9542c.js} +1 -1
  91. package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +1 -0
  92. package/apps/pwa/out/_next/static/{zoQX5iTvXOsIvCjxCe1Ji → ktMqfrBbf8BMGac0jqLrb}/_buildManifest.js +1 -1
  93. package/apps/pwa/out/approvals/index.html +1 -1
  94. package/apps/pwa/out/approvals/index.txt +29 -27
  95. package/apps/pwa/out/autopilot/index.html +1 -1
  96. package/apps/pwa/out/autopilot/index.txt +29 -27
  97. package/apps/pwa/out/board/index.html +1 -1
  98. package/apps/pwa/out/board/index.txt +29 -27
  99. package/apps/pwa/out/botlab/index.html +1 -1
  100. package/apps/pwa/out/botlab/index.txt +26 -24
  101. package/apps/pwa/out/bots/index.html +1 -1
  102. package/apps/pwa/out/bots/index.txt +26 -24
  103. package/apps/pwa/out/chat/index.html +1 -1
  104. package/apps/pwa/out/chat/index.txt +26 -24
  105. package/apps/pwa/out/control/index.html +1 -1
  106. package/apps/pwa/out/control/index.txt +29 -27
  107. package/apps/pwa/out/design-check/index.html +1 -1
  108. package/apps/pwa/out/design-check/index.txt +29 -27
  109. package/apps/pwa/out/editor/index.html +1 -1
  110. package/apps/pwa/out/editor/index.txt +29 -27
  111. package/apps/pwa/out/healthz +1 -1
  112. package/apps/pwa/out/icons/elanous-16.png +0 -0
  113. package/apps/pwa/out/icons/elanous-180.png +0 -0
  114. package/apps/pwa/out/icons/elanous-192.png +0 -0
  115. package/apps/pwa/out/icons/elanous-32.png +0 -0
  116. package/apps/pwa/out/icons/elanous-512.png +0 -0
  117. package/apps/pwa/out/index.html +1 -1
  118. package/apps/pwa/out/index.txt +26 -24
  119. package/apps/pwa/out/intake/index.html +1 -1
  120. package/apps/pwa/out/intake/index.txt +29 -27
  121. package/apps/pwa/out/live/index.html +1 -1
  122. package/apps/pwa/out/live/index.txt +29 -27
  123. package/apps/pwa/out/manifest.webmanifest +5 -2
  124. package/apps/pwa/out/market/index.html +1 -1
  125. package/apps/pwa/out/market/index.txt +26 -24
  126. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  127. package/apps/pwa/out/missions/placeholder/index.txt +26 -24
  128. package/apps/pwa/out/morning/index.html +1 -1
  129. package/apps/pwa/out/morning/index.txt +29 -27
  130. package/apps/pwa/out/observatory/index.html +1 -1
  131. package/apps/pwa/out/observatory/index.txt +29 -27
  132. package/apps/pwa/out/reflection/index.html +1 -1
  133. package/apps/pwa/out/reflection/index.txt +27 -25
  134. package/apps/pwa/out/scheduler/index.html +1 -1
  135. package/apps/pwa/out/scheduler/index.txt +29 -27
  136. package/apps/pwa/out/sessions/index.html +1 -1
  137. package/apps/pwa/out/sessions/index.txt +26 -24
  138. package/apps/pwa/out/settings/devices/index.html +1 -1
  139. package/apps/pwa/out/settings/devices/index.txt +29 -27
  140. package/apps/pwa/out/settings/index.html +1 -1
  141. package/apps/pwa/out/settings/index.txt +29 -27
  142. package/apps/pwa/out/setup/done/index.html +1 -1
  143. package/apps/pwa/out/setup/done/index.txt +29 -27
  144. package/apps/pwa/out/setup/index.html +1 -1
  145. package/apps/pwa/out/setup/index.txt +29 -27
  146. package/apps/pwa/out/share/index.html +1 -1
  147. package/apps/pwa/out/share/index.txt +29 -27
  148. package/apps/pwa/out/showroom/index.html +1 -1
  149. package/apps/pwa/out/showroom/index.txt +26 -24
  150. package/apps/pwa/out/sw-strategies.js +44 -0
  151. package/apps/pwa/out/sw.js +6 -44
  152. package/apps/pwa/out/tasks/index.html +1 -1
  153. package/apps/pwa/out/tasks/index.txt +29 -27
  154. package/apps/pwa/out/term/index.html +1 -1
  155. package/apps/pwa/out/term/index.txt +29 -27
  156. package/apps/pwa/out/trace/index.html +1 -1
  157. package/apps/pwa/out/trace/index.txt +29 -27
  158. package/apps/pwa/out/vault/index.html +1 -1
  159. package/apps/pwa/out/vault/index.txt +29 -27
  160. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  161. package/apps/pwa/out/workflows/chat-ui/index.txt +29 -27
  162. package/apps/pwa/out/workflows/index.html +1 -1
  163. package/apps/pwa/out/workflows/index.txt +29 -27
  164. package/apps/pwa/out/workspace/index.html +1 -1
  165. package/apps/pwa/out/workspace/index.txt +29 -27
  166. package/apps/pwa/out/worktrees/index.html +1 -1
  167. package/apps/pwa/out/worktrees/index.txt +29 -27
  168. package/catalog/resources.yaml +6 -6
  169. package/docs/goal-context/steward-directives-2026-09-28.md +35 -37
  170. package/graphs/landing/landing-heal.yaml +1 -1
  171. package/graphs/release/recipes.yaml +8 -1
  172. package/graphs/release/release-loop.yaml +6 -1
  173. package/graphs/video/README.md +1 -0
  174. package/graphs/video/explainer-line.yaml +58 -0
  175. package/package.json +3 -1
  176. package/plugins/job-coach/.codex-plugin/plugin.json +18 -1
  177. package/plugins/job-coach/README.md +12 -0
  178. package/plugins/job-coach/connectors/ncs/server.ts +4 -3
  179. package/plugins/job-coach/graphs/gap-judge.ts +44 -0
  180. package/plugins/job-coach/graphs/recipes.yaml +27 -0
  181. package/plugins/job-coach/graphs/report-enterprise.yaml +28 -0
  182. package/plugins/job-coach/graphs/run-step.ts +211 -17
  183. package/plugins/job-coach/plugin.json +16 -3
  184. package/plugins/job-coach/skills/enterprise-ai-fit/SKILL.md +8 -0
  185. package/plugins/job-coach/skills/enterprise-competency/SKILL.md +8 -0
  186. package/plugins/job-coach/skills/enterprise-needs/SKILL.md +8 -0
  187. package/plugins/job-coach/skills/enterprise-roadmap/SKILL.md +8 -0
  188. package/plugins/job-coach/skills/enterprise-task-analysis/SKILL.md +8 -0
  189. package/plugins/job-coach/skills/interview-to-profile/SKILL.md +18 -1
  190. package/scripts/backup/elanous-backup.sh +4 -1
  191. package/scripts/build-docs-site.ts +4 -3
  192. package/scripts/coord-channel-watch.sh +23 -4
  193. package/scripts/coord-post.sh +49 -12
  194. package/scripts/coord-tracks.json +5 -5
  195. package/scripts/eval-bot-vs-grok.ts +79 -0
  196. package/scripts/eval-grok-live-baseline.ts +36 -0
  197. package/scripts/fixtures/front-route-corpus.sample.json +17 -0
  198. package/scripts/fleet/node-setup.sh +46 -7
  199. package/scripts/landing-heal/must-fix-match.ts +26 -0
  200. package/scripts/landing-heal/run-mission.ts +114 -20
  201. package/scripts/lib/front-route-measurement.ts +131 -0
  202. package/scripts/lib/tui-sim-keys.ts +42 -0
  203. package/scripts/measure-front-route-classifier.ts +56 -0
  204. package/scripts/media/ocr-leak-scan.sh +21 -0
  205. package/scripts/public-export.ts +2 -3
  206. package/scripts/public-import-pr.ts +80 -6
  207. package/scripts/publish-internal-dist.ts +2 -3
  208. package/scripts/red-sweep-diff.ts +1 -1
  209. package/scripts/release-loop/auto-approve-node.ts +72 -0
  210. package/scripts/release-loop/gate-node.ts +332 -44
  211. package/scripts/release-loop/publish-node.ts +7 -3
  212. package/scripts/release-loop/release-docs.ts +20 -13
  213. package/scripts/release-loop/test-durations.ts +63 -0
  214. package/scripts/release-loop/upgrade-node.ts +3 -2
  215. package/scripts/test-audit-coverage-per-file.sh +16 -0
  216. package/scripts/test-audit-meaningless.ts +88 -0
  217. package/scripts/test-audit-overlap-and-weak.ts +73 -0
  218. package/scripts/test-deterministic.ts +27 -5
  219. package/scripts/tui-sim.ts +59 -14
  220. package/scripts/ux/bump-installed.sh +20 -0
  221. package/scripts/ux/lib/cdp.ts +72 -0
  222. package/scripts/ux/record-page.ts +50 -0
  223. package/skills/explainer-video/SKILL.md +99 -0
  224. package/skills/explainer-video/engine/brand/elanous-mark-on-dark.svg +1 -0
  225. package/skills/explainer-video/engine/build.mjs +431 -0
  226. package/skills/explainer-video/engine/vo.ts +37 -0
  227. package/skills/explainer-video/examples/elanous-0.2.5/script.json +155 -0
  228. package/src/agent/nested-agent-env.ts +1 -0
  229. package/src/agent-mission/driver.ts +95 -16
  230. package/src/agent-mission/mission-cli.ts +6 -0
  231. package/src/agent-mission/resource-ladder.ts +73 -25
  232. package/src/auth/acp-token.ts +16 -0
  233. package/src/auth/github-app-token.ts +112 -0
  234. package/src/autopilot/gh-rest-fallback.ts +177 -0
  235. package/src/autopilot/pr-manager.ts +51 -4
  236. package/src/boot/acp-server.ts +2 -16
  237. package/src/boot/daemon-tools/web-search.ts +2 -1
  238. package/src/boot/ws-bridge.ts +11 -0
  239. package/src/chat/index.ts +37 -8
  240. package/src/chat/log-scroll-key.ts +12 -0
  241. package/src/chat/submit-trace.ts +30 -0
  242. package/src/cli/agent-cli.ts +7 -2
  243. package/src/cli/decisions-cli.ts +94 -0
  244. package/src/cli/doctor-cli.ts +40 -54
  245. package/src/cli/doctor-readiness.ts +16 -24
  246. package/src/cli/intake-cli.ts +1 -1
  247. package/src/cli/mcp-call.ts +9 -0
  248. package/src/cli/mcp-gateway-cli.ts +7 -3
  249. package/src/cli/plugin-cli.ts +109 -11
  250. package/src/cli/pr-lineage.ts +2 -1
  251. package/src/cli/pwa-start.ts +3 -0
  252. package/src/cli/release-cli.ts +94 -7
  253. package/src/cli/self-update.ts +7 -4
  254. package/src/cli/start-cli.ts +33 -14
  255. package/src/dashboard/acp-boot.ts +3 -0
  256. package/src/dashboard/index.ts +62 -13
  257. package/src/dashboard/input/no-provider-reply.ts +13 -0
  258. package/src/dashboard/slash-runtime/unknown-slash-reply.ts +78 -0
  259. package/src/dashboard/stale-install-check.ts +77 -0
  260. package/src/decisions/decision-ledger.ts +404 -0
  261. package/src/domains/outbound-alert.ts +8 -2
  262. package/src/domains/telegram-kind-route.ts +21 -0
  263. package/src/execution-loop/dispatch-task.ts +139 -0
  264. package/src/execution-loop/gate-decision-summary.ts +29 -0
  265. package/src/execution-loop/gates.ts +197 -0
  266. package/src/graph-kinds/installed-plugin-nodes.ts +95 -0
  267. package/src/graph-kinds/plugin-nodes.ts +10 -2
  268. package/src/graph-runner/runner.ts +35 -5
  269. package/src/grounding/token.ts +12 -6
  270. package/src/harness/harness-cli-command.ts +68 -9
  271. package/src/harness/harness-pod-dispatch.ts +8 -2
  272. package/src/harness/harness-stop.ts +147 -18
  273. package/src/harness/self-send-decide.ts +26 -17
  274. package/src/harness/self-send-target.ts +22 -1
  275. package/src/harness/worktree-branch-prefix.ts +4 -3
  276. package/src/index.ts +56 -13
  277. package/src/instance/resolve.ts +6 -0
  278. package/src/intake-plane/collect-telegram-saved.ts +5 -0
  279. package/src/intake-plane/digest.ts +11 -2
  280. package/src/live/detail-switch.ts +8 -3
  281. package/src/llm/usable-llm.ts +32 -0
  282. package/src/loops/registry.ts +27 -5
  283. package/src/market/publish.ts +59 -3
  284. package/src/native-tool-catalog.ts +2 -2
  285. package/src/nexus/api/graph-approvals.ts +125 -0
  286. package/src/nexus/api/http-server.ts +60 -6
  287. package/src/nexus/api/log-fabric.ts +10 -1
  288. package/src/nexus/api/mcp-oauth-hint.ts +10 -9
  289. package/src/nexus/api/merge-approvals.ts +2 -1
  290. package/src/nexus/api/meta-api.ts +14 -0
  291. package/src/nexus/api/plugins-credentials-api.ts +47 -0
  292. package/src/nexus/api/plugins-market.ts +87 -3
  293. package/src/nexus/api/pod-credential-api.ts +68 -14
  294. package/src/nexus/api/public-routes.ts +7 -0
  295. package/src/nexus/api/trace.ts +57 -6
  296. package/src/nexus/boot/register-mcp-clients.ts +99 -14
  297. package/src/nexus/index.ts +33 -32
  298. package/src/nexus/outbound/router.ts +1 -1
  299. package/src/onboarding.ts +25 -2
  300. package/src/plugins/core/host.ts +14 -0
  301. package/src/plugins/install/market-fetch.ts +4 -3
  302. package/src/plugins/install/plugin-credentials.ts +106 -0
  303. package/src/plugins/install/plugin-install.ts +163 -32
  304. package/src/prompt-library/universal-preamble.ts +8 -2
  305. package/src/release-loop/checklist.ts +191 -0
  306. package/src/release-loop/release-note.ts +6 -3
  307. package/src/self-dev/dev-cli.ts +40 -20
  308. package/src/self-dev/dev-pipeline.ts +77 -1
  309. package/src/self-implement/gate-baseline.ts +3 -1
  310. package/src/self-implement/goal-author.ts +18 -1
  311. package/src/self-implement/host-regate.ts +6 -2
  312. package/src/self-implement/orchestrator.ts +7 -2
  313. package/src/self-implement/seams.ts +4 -2
  314. package/src/skills/tools/dev-harness.ts +42 -0
  315. package/src/steward/steward-cards.ts +45 -0
  316. package/src/steward/triage.ts +50 -14
  317. package/src/task-orchestrator/surfaces/pod-artifact-return.ts +61 -3
  318. package/src/task-orchestrator/surfaces/pod-command-job.ts +27 -6
  319. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +39 -9
  320. package/src/task-orchestrator/surfaces/pod-source-bundle.ts +16 -1
  321. package/src/task-orchestrator/surfaces/pod-source-receive.ts +51 -13
  322. package/src/task-orchestrator/surfaces/self-implement-pod.ts +172 -28
  323. package/src/telegram-report.ts +12 -6
  324. package/src/tui-client/acp-transport-ws-client.ts +72 -5
  325. package/src/tui.ts +3 -2
  326. package/src/user-config.ts +28 -2
  327. package/src/util/spawn-sync-output.ts +15 -0
  328. package/src/version/code-revision.ts +1 -1
  329. package/src/version/packed-revision.json +1 -1
  330. package/src/web-search/free.ts +77 -0
  331. package/src/web-search/index.ts +2 -0
  332. package/src/workflow-runtime/deps-bridge.ts +4 -3
  333. package/src/workflow-runtime/plugin-kind-node.ts +32 -4
  334. package/src/workflow-runtime/types.ts +2 -1
  335. package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/1191-7b13a24117a689fa.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/1377-1efeffbbac0963ff.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/1470.98a6ab61ccb4131c.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/2375-3fcae5186c64fe6b.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/2977-e38442e86289f1d3.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/2982-2a2df543be3d2e5a.js +0 -25
  346. package/apps/pwa/out/_next/static/chunks/319-d37df1b6e0cbeff4.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +0 -5
  348. package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +0 -1
  349. package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +0 -1
  350. package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +0 -1
  351. package/apps/pwa/out/_next/static/chunks/4836-3c93795ab834381e.js +0 -1
  352. package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +0 -1
  353. package/apps/pwa/out/_next/static/chunks/6090-1bd6077591603947.js +0 -1
  354. package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.js +0 -1
  355. package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +0 -1
  356. package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +0 -1
  357. package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +0 -1
  358. package/apps/pwa/out/_next/static/chunks/7457-f5cb1cc8db9f7453.js +0 -1
  359. package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.js +0 -1
  360. package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +0 -1
  361. package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +0 -1
  362. package/apps/pwa/out/_next/static/chunks/8067-f74bf14d67ad98f1.js +0 -1
  363. package/apps/pwa/out/_next/static/chunks/8142-011e1f474efb8b33.js +0 -1
  364. package/apps/pwa/out/_next/static/chunks/8519-0059d82bdb4812bf.js +0 -1
  365. package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +0 -1
  366. package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +0 -1
  367. package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +0 -1
  368. package/apps/pwa/out/_next/static/chunks/9740-52467f244755917f.js +0 -1
  369. package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.js +0 -1
  370. package/apps/pwa/out/_next/static/chunks/ae787e4c-f2998de7f1ba72a4.js +0 -1
  371. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +0 -1
  372. package/apps/pwa/out/_next/static/chunks/app/approvals/page-21c3261af6ab377c.js +0 -1
  373. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-1991445b8ea86274.js +0 -1
  374. package/apps/pwa/out/_next/static/chunks/app/board/page-4ebfaee2369fe248.js +0 -1
  375. package/apps/pwa/out/_next/static/chunks/app/bots/page-ca7c3cc305c3285a.js +0 -1
  376. package/apps/pwa/out/_next/static/chunks/app/chat/page-9951930add3d2051.js +0 -1
  377. package/apps/pwa/out/_next/static/chunks/app/design-check/page-4de08d4b766f134b.js +0 -1
  378. package/apps/pwa/out/_next/static/chunks/app/editor/page-7a975e2b9db1dfb7.js +0 -1
  379. package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +0 -1
  380. package/apps/pwa/out/_next/static/chunks/app/intake/page-49270714ef35bb96.js +0 -1
  381. package/apps/pwa/out/_next/static/chunks/app/layout-33fbdf353e2a5982.js +0 -1
  382. package/apps/pwa/out/_next/static/chunks/app/live/page-259f122e2dd64317.js +0 -1
  383. package/apps/pwa/out/_next/static/chunks/app/market/page-65b82726753658e1.js +0 -1
  384. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-4b8df3748dc06285.js +0 -1
  385. package/apps/pwa/out/_next/static/chunks/app/not-found-542a5c0006da637c.js +0 -1
  386. package/apps/pwa/out/_next/static/chunks/app/observatory/page-63ffb347831262ea.js +0 -1
  387. package/apps/pwa/out/_next/static/chunks/app/page-98e4e359d97263b4.js +0 -1
  388. package/apps/pwa/out/_next/static/chunks/app/reflection/page-97e24ac675ce576e.js +0 -1
  389. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-ac6391f26391d89d.js +0 -1
  390. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9f45c9d45ffc43bd.js +0 -1
  391. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-36995b9cb5d98218.js +0 -1
  392. package/apps/pwa/out/_next/static/chunks/app/settings/page-0c795b577988d63f.js +0 -1
  393. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b4311e2f71c6735a.js +0 -1
  394. package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +0 -1
  395. package/apps/pwa/out/_next/static/chunks/app/setup/page-eb2381074df20dba.js +0 -1
  396. package/apps/pwa/out/_next/static/chunks/app/share/page-b01336e64eba01ae.js +0 -1
  397. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7af7f21c1f2858ef.js +0 -1
  398. package/apps/pwa/out/_next/static/chunks/app/tasks/page-a98ecd364586c89b.js +0 -1
  399. package/apps/pwa/out/_next/static/chunks/app/term/page-57f0502dfbfa089b.js +0 -1
  400. package/apps/pwa/out/_next/static/chunks/app/trace/page-3467d2e1abd8a14a.js +0 -1
  401. package/apps/pwa/out/_next/static/chunks/app/vault/page-7842ca23d53ba682.js +0 -1
  402. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-ae9ac882912b0198.js +0 -1
  403. package/apps/pwa/out/_next/static/chunks/app/workflows/page-3a0d72e3f783bcef.js +0 -1
  404. package/apps/pwa/out/_next/static/chunks/app/workspace/page-f3b39fe8358c2a97.js +0 -1
  405. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-5a22af3181ac2afb.js +0 -1
  406. package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +0 -1
  407. package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +0 -1
  408. package/apps/pwa/out/_next/static/chunks/main-app-1457627bb302a22c.js +0 -1
  409. package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +0 -1
  410. package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +0 -1
  411. package/apps/pwa/out/_next/static/css/e66b7f5efc8df3f3.css +0 -1
  412. /package/apps/pwa/out/_next/static/{zoQX5iTvXOsIvCjxCe1Ji → ktMqfrBbf8BMGac0jqLrb}/_ssgManifest.js +0 -0
@@ -0,0 +1,197 @@
1
+ import type { TaskCard } from '../task-cards/card-store.js';
2
+ import { decideBudget, readBudgetInputsLive, type BudgetDecision, type BudgetInputs } from '../self-implement/budget-gate.js';
3
+ import { resolveHarnessSubstrate, type ResolvedHarnessSubstrate } from '../harness/harness-substrate-default.js';
4
+ import { podMemoryLimitFor } from '../task-orchestrator/surfaces/self-implement-pod.js';
5
+ import { recallMemoryContext } from '../agent-substrate/execution/memory-context.js';
6
+
7
+ export interface GateResult<T> {
8
+ decision: T;
9
+ explanation: string;
10
+ }
11
+
12
+ export interface BudgetGateDeps {
13
+ readInputs: () => Promise<BudgetInputs>;
14
+ decide: (inputs: BudgetInputs) => BudgetDecision;
15
+ }
16
+
17
+ /** Observe the existing quota decision; callers record it, not apply it to the launch. */
18
+ export async function budgetGate(
19
+ deps: BudgetGateDeps = { readInputs: readBudgetInputsLive, decide: decideBudget },
20
+ ): Promise<GateResult<BudgetDecision>> {
21
+ const decision = deps.decide(await deps.readInputs());
22
+ return { decision, explanation: decision.reasons.join(' · ') || `budget: ${decision.action}` };
23
+ }
24
+
25
+ export interface PlacementGateInput {
26
+ goal: string;
27
+ /** A local-only capability or file is an observation, not a reason to probe a remote pool. */
28
+ needsBrowser?: boolean;
29
+ needsVideo?: boolean;
30
+ needsAppSigning?: boolean;
31
+ localFiles?: readonly string[];
32
+ flag?: { substrate?: 'pod' | 'local'; podPool?: string };
33
+ }
34
+
35
+ export interface PlacementGateDeps {
36
+ resolveSubstrate: (input: { flag?: { substrate?: 'pod' | 'local'; podPool?: string } }) => ResolvedHarnessSubstrate;
37
+ memoryLimit: (goal: string) => { limit: string; tier: string; source: string };
38
+ }
39
+
40
+ export type PlacementDecision = (ResolvedHarnessSubstrate | {
41
+ substrate: 'unknown';
42
+ pool: null;
43
+ source: 'unknown';
44
+ }) & {
45
+ /** Reachability is deliberately not probed by this observational gate. */
46
+ poolReachability: 'unknown' | 'not-applicable';
47
+ memory?: { limit: string; tier: string; source: string };
48
+ localReasons: string[];
49
+ /** Omitted observations must not be silently treated as negative evidence. */
50
+ unknownInputs: string[];
51
+ };
52
+
53
+ const defaultPlacementDeps: PlacementGateDeps = {
54
+ resolveSubstrate: ({ flag }) => resolveHarnessSubstrate({ flag, env: process.env, currentContext: () => undefined }),
55
+ memoryLimit: (goal) => podMemoryLimitFor(goal, process.env),
56
+ };
57
+
58
+ export function placementGate(
59
+ input: PlacementGateInput,
60
+ deps: PlacementGateDeps = defaultPlacementDeps,
61
+ ): GateResult<PlacementDecision> {
62
+ const unknownInputs = [
63
+ ...(input.needsBrowser === undefined ? ['needsBrowser'] : []),
64
+ ...(input.needsVideo === undefined ? ['needsVideo'] : []),
65
+ ...(input.needsAppSigning === undefined ? ['needsAppSigning'] : []),
66
+ ...(input.localFiles === undefined ? ['localFiles'] : []),
67
+ ...(input.flag?.substrate === undefined ? ['substrate'] : []),
68
+ ];
69
+ const localReasons = [
70
+ ...(input.needsBrowser ? ['browser'] : []),
71
+ ...(input.needsVideo ? ['video'] : []),
72
+ ...(input.needsAppSigning ? ['app signing'] : []),
73
+ ...(input.localFiles?.length ? [`local files: ${input.localFiles.join(', ')}`] : []),
74
+ ];
75
+ // A measured local-only need wins; otherwise unknown needs cannot justify a remote assertion.
76
+ if (!localReasons.length && input.flag?.substrate !== 'local' && unknownInputs.length) {
77
+ return {
78
+ decision: { substrate: 'unknown', pool: null, source: 'unknown', poolReachability: 'unknown', localReasons, unknownInputs },
79
+ explanation: `placement=unknown; unmeasured=${unknownInputs.join(', ')}; pool reachability not checked`,
80
+ };
81
+ }
82
+ const flag = localReasons.length ? { substrate: 'local' as const } : input.flag;
83
+ let resolved: ResolvedHarnessSubstrate;
84
+ try {
85
+ resolved = deps.resolveSubstrate({ flag });
86
+ } catch (error) {
87
+ if (flag?.substrate !== 'pod' || !(error instanceof Error) || !error.message.includes('Pod 풀 또는 현재 컨텍스트에 닿지 못했다')) throw error;
88
+ // No configured pool is not evidence that a remote pool is unreachable.
89
+ resolved = { substrate: 'pod', pool: null, source: 'flag' };
90
+ }
91
+ const poolReachability = resolved.substrate === 'pod' ? 'unknown' : 'not-applicable';
92
+ const memory = resolved.substrate === 'pod' ? deps.memoryLimit(input.goal) : undefined;
93
+ const decision: PlacementDecision = {
94
+ ...resolved, poolReachability, localReasons, unknownInputs,
95
+ ...(memory ? { memory } : {}),
96
+ };
97
+ return {
98
+ decision,
99
+ explanation: (resolved.substrate === 'local'
100
+ ? `local: ${localReasons.join(' · ') || `substrate ${resolved.source}`}`
101
+ : `pod: pool=${resolved.pool ?? 'unknown'}; reachability=unknown (not checked); memory=${memory?.tier ?? 'unknown'} (${memory?.limit ?? 'unknown'})`)
102
+ + (unknownInputs.length ? `; unmeasured=${unknownInputs.join(', ')}` : ''),
103
+ };
104
+ }
105
+
106
+ export interface RelationGateDeps {
107
+ openCards: () => Promise<readonly TaskCard[]> | readonly TaskCard[];
108
+ /** A previous completed run is reported as a fact, never inferred from a matching title. */
109
+ priorTermination: (goalId: string) => Promise<string | undefined> | string | undefined;
110
+ /** 'unknown' = the caller cannot observe running runs; recorded as unknown, never as none. */
111
+ activeRuns: (goalId: string) => Promise<readonly string[] | 'unknown'> | readonly string[] | 'unknown';
112
+ /** Optional preflight overlap observations are kept separate from open-card overlap. */
113
+ preflightOverlaps?: (paths: readonly string[]) => Promise<readonly string[]> | readonly string[];
114
+ dependsOn?: (goalId: string) => Promise<readonly string[]> | readonly string[];
115
+ similarCards?: (card: TaskCard, openCards: readonly TaskCard[]) => Promise<readonly string[]> | readonly string[];
116
+ }
117
+
118
+ export interface RelationDecision {
119
+ overlappingCards: string[];
120
+ preflightOverlaps: string[] | 'unknown';
121
+ dependsOn: string[] | 'unknown';
122
+ similarCards: string[] | 'unknown';
123
+ sameGoalActiveRuns: string[] | 'unknown';
124
+ priorTermination?: string;
125
+ /** Overlaps and similarity are observations, not dispatch locks. */
126
+ action: 'record';
127
+ }
128
+
129
+ export async function relationGate(
130
+ card: TaskCard,
131
+ targetPaths: readonly string[],
132
+ deps: RelationGateDeps,
133
+ ): Promise<GateResult<RelationDecision>> {
134
+ const [cards, priorTermination, sameGoalActiveRuns] = await Promise.all([
135
+ deps.openCards(), deps.priorTermination(card.goalId), deps.activeRuns(card.goalId),
136
+ ]);
137
+ const [preflightOverlaps, dependsOn, similarCards] = await Promise.all([
138
+ deps.preflightOverlaps?.(targetPaths) ?? 'unknown',
139
+ deps.dependsOn?.(card.goalId) ?? 'unknown',
140
+ deps.similarCards?.(card, cards) ?? 'unknown',
141
+ ]);
142
+ const target = new Set(targetPaths);
143
+ const overlappingCards = cards
144
+ .filter((other) => other.id !== card.id && other.status === 'open' && other.sections.some((section) => {
145
+ if (!section.key.startsWith('workspace')) return false;
146
+ try {
147
+ const workspace = JSON.parse(section.content) as { targetPaths?: unknown; paths?: unknown };
148
+ const paths = workspace.targetPaths ?? workspace.paths;
149
+ return Array.isArray(paths) && paths.some((path) => typeof path === 'string' && target.has(path));
150
+ } catch { return false; }
151
+ }))
152
+ .map((other) => other.id);
153
+ const observed = (value: readonly string[] | string): string =>
154
+ typeof value === 'string' ? value : value.join(',') || 'none';
155
+ const decision: RelationDecision = {
156
+ action: 'record', overlappingCards,
157
+ preflightOverlaps: preflightOverlaps === 'unknown' ? 'unknown' : [...preflightOverlaps],
158
+ dependsOn: dependsOn === 'unknown' ? 'unknown' : [...dependsOn],
159
+ similarCards: similarCards === 'unknown' ? 'unknown' : [...similarCards],
160
+ sameGoalActiveRuns: sameGoalActiveRuns === 'unknown' ? 'unknown' : [...sameGoalActiveRuns],
161
+ ...(priorTermination === undefined ? {} : { priorTermination }),
162
+ };
163
+ return {
164
+ decision,
165
+ explanation: `overlap=${observed(overlappingCards)}; preflight=${observed(preflightOverlaps)}; dependsOn=${observed(dependsOn)}; similar=${observed(similarCards)}; active=${observed(sameGoalActiveRuns)}; prior=${priorTermination ?? 'unknown'}; overlaps do not block launch`,
166
+ };
167
+ }
168
+
169
+ export interface MemoryGateDeps {
170
+ recall: (query: string) => Promise<string>;
171
+ priorAbandonment: (goalId: string) => Promise<string | undefined> | string | undefined;
172
+ }
173
+
174
+ export interface MemoryDecision {
175
+ context: string;
176
+ priorAbandonment?: string;
177
+ /** The existing recall API returns formatted context, not stable fragment IDs. */
178
+ fragmentIds: string[];
179
+ action: 'record';
180
+ }
181
+
182
+ export async function memoryGate(
183
+ card: Pick<TaskCard, 'goalId' | 'title'>,
184
+ deps: MemoryGateDeps = { recall: recallMemoryContext, priorAbandonment: () => undefined },
185
+ ): Promise<GateResult<MemoryDecision>> {
186
+ const [context, priorAbandonment] = await Promise.all([
187
+ deps.recall(card.title), deps.priorAbandonment(card.goalId),
188
+ ]);
189
+ const decision: MemoryDecision = {
190
+ action: 'record', context, fragmentIds: [],
191
+ ...(priorAbandonment === undefined ? {} : { priorAbandonment }),
192
+ };
193
+ return {
194
+ decision,
195
+ explanation: `memory=${context ? 'recalled' : 'none or unavailable'}; prior abandonment=${priorAbandonment ?? 'unknown'}; fragment IDs unavailable from recall API`,
196
+ };
197
+ }
@@ -0,0 +1,95 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFileSync, realpathSync, statSync } from 'node:fs';
3
+ import { isAbsolute, join, relative, resolve } from 'node:path';
4
+ import { debug } from '../debug/log.js';
5
+ import { listInstalledPlugins, type InstalledPlugin } from '../plugins/install/plugin-install.js';
6
+ import { loadPluginManifestFromDir, type PluginManifest } from '../plugins/core/manifest.js';
7
+ import { loadPluginNodes } from './plugin-nodes.js';
8
+ import { getNodeKindRegistration, listNodeKinds, unregisterPluginNodeKind, type GraphKind, type NodeKindEntry } from './registry.js';
9
+
10
+ export interface InstalledPluginNodeSyncResult {
11
+ registered: number;
12
+ removed: number;
13
+ errors: Array<{ plugin: string; reason: string }>;
14
+ }
15
+
16
+ type OwnedNode = { graph: GraphKind; kind: string; plugin: string; registered: NodeKindEntry };
17
+ type InstalledNodeState = { stamp: string; owned: Map<string, { fingerprint: string; nodes: OwnedNode[] }> };
18
+ const states = new Map<string, InstalledNodeState>();
19
+
20
+ function fingerprint(item: InstalledPlugin, manifest: PluginManifest): string {
21
+ const root = realpathSync(item.path);
22
+ const hash = createHash('sha256');
23
+ hash.update(JSON.stringify([item.name, item.version, item.market, item.sha256, item.installedAt, manifest]));
24
+ for (const path of manifest.contributes.nodes ?? []) {
25
+ const file = realpathSync(resolve(root, path));
26
+ const rel = relative(root, file);
27
+ if (!path.startsWith('./') || !rel || rel === '..' || rel.startsWith('../') || isAbsolute(rel)) {
28
+ throw new Error(`node path must remain inside the plugin dir: ${path}`);
29
+ }
30
+ hash.update(path);
31
+ hash.update(readFileSync(file));
32
+ }
33
+ return hash.digest('hex');
34
+ }
35
+
36
+ /** Reconcile only registrations made by this sync; the registry's identity check protects other owners. */
37
+ export function syncInstalledPluginNodes(root: string): InstalledPluginNodeSyncResult {
38
+ const result: InstalledPluginNodeSyncResult = { registered: 0, removed: 0, errors: [] };
39
+ const stateRoot = resolve(root);
40
+ const ledger = statSync(join(stateRoot, 'plugins', 'installed.json'), { throwIfNoEntry: false, bigint: true });
41
+ // The installer atomically replaces the ledger, so mtime and size alone can
42
+ // coincide across installs. File identity and ctime detect that replacement.
43
+ const stamp = ledger ? `${ledger.dev}:${ledger.ino}:${ledger.ctimeNs}:${ledger.mtimeNs}:${ledger.size}` : 'missing';
44
+ const previous = states.get(stateRoot);
45
+ if (previous?.stamp === stamp) return result;
46
+
47
+ // The installer commits a new ledger via rename after the package directory is in place.
48
+ // If discovery itself fails, the stamp is not advanced and the next request retries.
49
+ const installed = listInstalledPlugins(stateRoot);
50
+ const current = new Set(installed.map(item => item.path));
51
+ const owned = previous?.owned ?? new Map<string, { fingerprint: string; nodes: OwnedNode[] }>();
52
+ const removeOwned = (path: string): void => {
53
+ for (const node of owned.get(path)?.nodes ?? []) {
54
+ if (unregisterPluginNodeKind(node.graph, node.kind, node.plugin, node.registered)) result.removed++;
55
+ }
56
+ owned.delete(path);
57
+ };
58
+ for (const path of owned.keys()) {
59
+ if (!current.has(path)) removeOwned(path);
60
+ }
61
+
62
+ for (const item of installed) {
63
+ try {
64
+ const manifest = loadPluginManifestFromDir(item.path, { id: item.name }).manifest;
65
+ if (!manifest.contributes.nodes?.length) {
66
+ removeOwned(item.path);
67
+ continue;
68
+ }
69
+ const version = fingerprint(item, manifest);
70
+ if (owned.get(item.path)?.fingerprint === version) continue;
71
+ removeOwned(item.path);
72
+ const before = new Set(listNodeKinds().filter(entry => entry.plugin === manifest.id).map(entry => `${entry.graph}:${entry.kind}`));
73
+ const loaded = loadPluginNodes(item.path, manifest);
74
+ const nodes: OwnedNode[] = [];
75
+ for (const entry of listNodeKinds().filter(entry => entry.plugin === manifest.id && !before.has(`${entry.graph}:${entry.kind}`))) {
76
+ const registered = getNodeKindRegistration(entry.graph, entry.kind);
77
+ if (registered) nodes.push({ graph: entry.graph, kind: entry.kind, plugin: manifest.id, registered });
78
+ }
79
+ if (loaded.errors.length) {
80
+ for (const node of nodes) unregisterPluginNodeKind(node.graph, node.kind, node.plugin, node.registered);
81
+ result.errors.push({ plugin: item.name, reason: loaded.errors.join('; ') });
82
+ owned.set(item.path, { fingerprint: version, nodes: [] });
83
+ continue;
84
+ }
85
+ result.registered += nodes.length;
86
+ owned.set(item.path, { fingerprint: version, nodes });
87
+ } catch (error) {
88
+ removeOwned(item.path);
89
+ result.errors.push({ plugin: item.name, reason: error instanceof Error ? error.message : String(error) });
90
+ }
91
+ }
92
+ states.set(stateRoot, { stamp, owned });
93
+ debug.log('graph.kinds', 'installed-sync', { root: stateRoot, ...result });
94
+ return result;
95
+ }
@@ -34,8 +34,16 @@ export function loadPluginNodes(pluginDir: string, manifest: PluginManifest): Pl
34
34
  if (node.graph === 'workflow') {
35
35
  if (!node.run || typeof node.run !== 'object' || Array.isArray(node.run)) throw new Error('run must be an object');
36
36
  const spec = node.run as Record<string, unknown>;
37
- if (Object.keys(spec).length !== 1 || !['bash', 'http', 'skill'].some((key) => key in spec)) {
38
- throw new Error('run must contain exactly one bash, http or skill executor');
37
+ if (Object.keys(spec).length !== 1 || !['bash', 'http', 'skill', 'mcp'].some((key) => key in spec)) {
38
+ throw new Error('run must contain exactly one bash, http, skill or mcp executor');
39
+ }
40
+ if ('mcp' in spec) {
41
+ const mcp = spec.mcp;
42
+ if (!mcp || typeof mcp !== 'object' || Array.isArray(mcp)
43
+ || typeof (mcp as Record<string, unknown>).server !== 'string'
44
+ || typeof (mcp as Record<string, unknown>).tool !== 'string') {
45
+ throw new Error('mcp run requires server and tool');
46
+ }
39
47
  }
40
48
  run = spec as NodeKindRun;
41
49
  } else if (node.run !== undefined) {
@@ -1,9 +1,13 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import { execFile } from 'node:child_process';
3
- import { linkSync, mkdirSync, readFileSync, readdirSync, renameSync, rmdirSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
4
- import { dirname, join, resolve } from 'node:path';
3
+ import { existsSync, linkSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmdirSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
4
+ import { dirname, join, resolve, sep } from 'node:path';
5
5
  import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
6
+ import { getElanousConfigDir, getElanousConfigDirOverride } from '../elanous-config-dir.js';
6
7
  import { debug } from '../debug/log.js';
8
+ import { elanousStateRoot } from '../autopilot/state-paths.js';
9
+ import { listInstalledPlugins } from '../plugins/install/plugin-install.js';
10
+ import { credentialStatus, pluginEnv } from '../plugins/install/plugin-credentials.js';
7
11
  import { effectiveInstanceRoot } from '../instance/resolve.js';
8
12
  import { loadNodeCatalog } from '../self-implement/graph-catalog.js';
9
13
  import { parseGraphTemplateYaml, type GraphEdgeSpec, type GraphNodeSpec } from '../self-implement/graph-yaml.js';
@@ -215,6 +219,17 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
215
219
  const parsed = parseGraphTemplateYaml(stringifyYaml(document), path);
216
220
  if (!parsed.template || parsed.errors.length) throw new Error(parsed.errors.map((e) => `${e.path}: ${e.message}`).join('\n'));
217
221
  const graph = parsed.template;
222
+ const pluginRoot = elanousStateRoot();
223
+ const graphFile = realpathSync(path);
224
+ const installed = listInstalledPlugins(pluginRoot);
225
+ const owner = installed.find(item => {
226
+ const graphsDir = join(item.path, 'graphs');
227
+ return existsSync(graphsDir) && graphFile.startsWith(`${realpathSync(graphsDir)}${sep}`);
228
+ });
229
+ const credentials = owner ? pluginEnv(owner.name, pluginRoot) : {};
230
+ const declaredEnv = [...new Set(installed.map(item => item.name))]
231
+ .flatMap(name => credentialStatus(name, pluginRoot).fields.map(field => field.env));
232
+ const credentialFields = owner ? credentialStatus(owner.name, pluginRoot).fields.filter(field => field.set).map(field => field.name) : [];
218
233
  for (const terminal of graph.terminalNodes) {
219
234
  if (terminal !== 'done' && terminal !== 'failed') {
220
235
  throw new Error(`unsupported terminal node: ${terminal} (expected done or failed)`);
@@ -390,17 +405,32 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
390
405
  mkdirSync(dirname(contextPath), { recursive: true });
391
406
  writeFileSync(contextPath, JSON.stringify({ graphId, runId, nodeId: current, input: state.input ?? null, outputs }, null, 2) + '\n');
392
407
  let code: number | null = null;
408
+ const redactCredentials = (text: string) => Object.values(credentials).filter(value => value.length > 0)
409
+ .reduce((safe, value) => safe.replaceAll(value, '[REDACTED]'), text);
393
410
  const runBash: BashRun = async (body, opts) => {
394
411
  const result = await (options.deps?.runBash ?? realRunBash)(body, opts);
395
412
  code = result.exitCode;
396
- return result;
413
+ return { ...result, stdout: redactCredentials(result.stdout), stderr: redactCredentials(result.stderr) };
397
414
  };
398
- const ctx = { arguments: '', artifactsDir: dirname(statePath), outputs: {}, resolvedProvider: undefined, resolvedModel: undefined, toolPolicy: {}, env: { ...process.env, ELANOUS_GRAPH_CONTEXT: contextPath, ELANOUS_GRAPH_DIR: dirname(resolve(path)) } } as NodeExecContext;
415
+ if (owner) debug.log('plugin.credentials', 'injected', { plugin: owner.name, fields: credentialFields, count: credentialFields.length });
416
+ const env = { ...process.env };
417
+ for (const key of declaredEnv) delete env[key];
418
+ Object.assign(env, credentials);
419
+ // 러너가 명시로 받은 우주(`--config-dir`)를 `cmd:` 자식에 못 박는다 — 자식이 코드 위치(소스 트리)로 시험 우주를 새로 고르던 구멍
420
+ // (09-30 🅢 스튜어드: 작업 트리 `graph run … --config-dir ~/.elanous` 의 자식 triage 가 «apiKey missing» · 0.2.5 컷 릴리스 루프도 같은 발사 줄).
421
+ if (getElanousConfigDirOverride()) {
422
+ env.ELANOUS_CONFIG_DIR = getElanousConfigDir();
423
+ env.ELANOUS_STATE_DIR = effectiveInstanceRoot();
424
+ debug.log('graph-runner', 'child-universe-pinned', { graphId, runId, node: current, configDir: env.ELANOUS_CONFIG_DIR, stateDir: env.ELANOUS_STATE_DIR });
425
+ }
426
+ env.ELANOUS_GRAPH_CONTEXT = contextPath;
427
+ env.ELANOUS_GRAPH_DIR = dirname(resolve(path));
428
+ const ctx = { arguments: '', artifactsDir: dirname(statePath), outputs: {}, resolvedProvider: undefined, resolvedModel: undefined, toolPolicy: {}, env } as NodeExecContext;
399
429
  const result = await executeBashNode({ id: current, type: 'bash', bash: command.command, ...(command.timeout_ms ? { idle_timeout: command.timeout_ms } : {}) } as BashNode, ctx, { runBash } as WorkflowDeps);
400
430
  output = result.output;
401
431
  ok = result.ok;
402
432
  exit = code;
403
- error = result.error;
433
+ error = result.error ? redactCredentials(result.error) : undefined;
404
434
  state.executed++;
405
435
  }
406
436
  if (resumingCompleted && completed) {
@@ -3,7 +3,7 @@
3
3
  * Pod 는 호스트 우주를 «직접» 읽지 않는다(4우주·최소 자격). 대신 호스트 `POST /v1/grounding/query` 에
4
4
  * «질의 → 인용»만 묻는다. 그 문을 여는 것이 이 토큰이다.
5
5
  * - 모양: `base64url(payload).base64url(HMAC-SHA256)` · payload = `{v:1, runId, job, scope, exp}`.
6
- * scope 는 `'grounding'` 또는 `'llm-credential'` — 같은 서명·검증·회수. 문은 scope 를 엄격히 가른다.
6
+ * scope 는 `'grounding'` · `'llm-credential'` · `'gh-credential'` — 같은 서명·검증·회수. 문은 scope 를 엄격히 가른다.
7
7
  * - 상태 없는 검증 — 엔드포인트는 `verifyGroundingToken` 하나만 부른다.
8
8
  * - 키: 비밀 백엔드 `grounding/hmac`(없으면 처음 발급 때 무작위 32바이트로 만든다) — 호스트 밖으로 안 나간다.
9
9
  * - 수명: `exp`(Job 기한) ⊕ 조기 회수 `revokeGroundingRun(runId)`(Job 이 끝나면 호스트가 부른다).
@@ -19,13 +19,15 @@ export const GROUNDING_TOKEN_ENV = 'ELANOUS_GROUNDING_TOKEN';
19
19
  export const GROUNDING_URL_ENV = 'ELANOUS_GROUNDING_URL';
20
20
  export const GROUNDING_HMAC_SECRET_ID = 'grounding/hmac';
21
21
 
22
- export const GROUNDING_TOKEN_SCOPES = ['grounding', 'llm-credential'] as const;
22
+ export const GROUNDING_TOKEN_SCOPES = ['grounding', 'llm-credential', 'gh-credential'] as const;
23
23
  export type GroundingTokenScope = (typeof GROUNDING_TOKEN_SCOPES)[number];
24
24
 
25
25
  export interface GroundingClaims {
26
26
  readonly runId: string;
27
27
  readonly job: string;
28
28
  readonly scope: GroundingTokenScope;
29
+ /** GitHub credential relay only: full owner/repository bound by the host at issuance. */
30
+ readonly repository?: string;
29
31
  readonly exp: number;
30
32
  }
31
33
 
@@ -59,19 +61,22 @@ function sign(payload: string, key: string): string {
59
61
  }
60
62
 
61
63
  export async function mintGroundingToken(
62
- claims: { runId: string; job: string; ttlMs: number; scope?: GroundingTokenScope },
64
+ claims: { runId: string; job: string; ttlMs: number; scope?: GroundingTokenScope; repository?: string },
63
65
  deps: GroundingTokenDeps = {},
64
66
  ): Promise<{ token: string; exp: number }> {
65
67
  const key = await (deps.key ?? (() => readKey(true)))();
66
68
  if (!key) throw new Error('grounding: 서명 키를 만들 수 없다(비밀 백엔드)');
67
69
  const scope: GroundingTokenScope = claims.scope ?? 'grounding';
70
+ if (claims.repository !== undefined && (scope !== 'gh-credential' || !/^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(claims.repository) || claims.repository.split('/').some((part) => part === '.' || part === '..'))) {
71
+ throw new Error('grounding: invalid credential repository');
72
+ }
68
73
  const exp = (deps.now ?? Date.now)() + claims.ttlMs;
69
- const payload = Buffer.from(JSON.stringify({ v: 1, runId: claims.runId, job: claims.job, scope, exp })).toString('base64url');
74
+ const payload = Buffer.from(JSON.stringify({ v: 1, runId: claims.runId, job: claims.job, scope, ...(scope === 'gh-credential' ? { repository: claims.repository } : {}), exp })).toString('base64url');
70
75
  debug.log('grounding.token', 'minted', { runId: claims.runId, job: claims.job, scope, exp });
71
76
  return { token: `${payload}.${sign(payload, key)}`, exp };
72
77
  }
73
78
 
74
- /** 그라운딩 문은 scope `'grounding'` 만 받는다. `llm-credential` 은 `expectedScope` 로 따로 연다. */
79
+ /** 그라운딩 문은 scope `'grounding'` 만 받는다. 자격 증명 scope 는 `expectedScope` 로 따로 연다. */
75
80
  export async function verifyGroundingToken(
76
81
  token: string,
77
82
  deps: GroundingTokenDeps & { readonly expectedScope?: GroundingTokenScope } = {},
@@ -92,9 +97,10 @@ export async function verifyGroundingToken(
92
97
  if (claims.v !== 1 || typeof claims.runId !== 'string' || typeof claims.job !== 'string' || typeof claims.exp !== 'number') return reject('malformed');
93
98
  const expected: GroundingTokenScope = deps.expectedScope ?? 'grounding';
94
99
  if (claims.scope !== expected) return reject('scope', claims.runId);
100
+ if (expected === 'gh-credential' && claims.repository !== undefined && (typeof claims.repository !== 'string' || !/^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(claims.repository) || claims.repository.split('/').some((part) => part === '.' || part === '..'))) return reject('malformed', claims.runId);
95
101
  if ((deps.now ?? Date.now)() >= claims.exp) return reject('expired', claims.runId);
96
102
  if (isRevoked(claims.runId, deps)) return reject('revoked', claims.runId);
97
- return { ok: true, runId: claims.runId, job: claims.job, scope: expected, exp: claims.exp };
103
+ return { ok: true, runId: claims.runId, job: claims.job, scope: expected, ...(expected === 'gh-credential' && claims.repository !== undefined ? { repository: claims.repository } : {}), exp: claims.exp };
98
104
  }
99
105
 
100
106
  /** Job 이 끝나면 호스트가 부른다 — 그 런의 토큰은 기한 전이라도 더는 안 통한다. */
@@ -1,9 +1,10 @@
1
1
  import { execFileSync } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
2
3
  import { readFileSync } from 'node:fs';
3
4
  import { resolve } from 'node:path';
4
5
  import { Command, Option } from 'commander';
5
6
  import { runGitCommand } from '../git-fs/runner.js';
6
- import { GOAL_TYPES, parseGoalType, type GoalType } from '../self-implement/goal-author.js';
7
+ import { GOAL_TYPES, lintGoalFile, parseGoalId, parseGoalType, tracedPathReferences, type GoalType } from '../self-implement/goal-author.js';
7
8
  import { templateForGoalType } from '../self-implement/graph-templates.js';
8
9
  import { listRunLedgers, loadFederatedRunLedger, loadRunLedger, runLedgerDir, type RunLedgerMatch } from '../self-implement/run-ledger.js';
9
10
  import { resolveChildLlmEffort, resolveImplementationChildModel } from '../self-dev/dev-cli.js';
@@ -12,6 +13,7 @@ import { queryRunningRuns } from '../self-implement/running-runs.js';
12
13
  import { DevPipelineError } from '../self-dev/dev-pipeline.js';
13
14
  import { runDraftSweep, type DraftSweepAdapters, type DraftSweepResult, type SweepDraft, type SweepMergedPr } from '../self-dev/draft-sweep.js';
14
15
  import { debug } from '../debug/log.js';
16
+ import { dispatchTask, type DispatchTaskInput, type DispatchTaskDeps } from '../execution-loop/dispatch-task.js';
15
17
  import { PR_LABELS } from '../github/pr-labels.js';
16
18
  import { decideBudget, readBudgetInputsLive, type BudgetDecision, type BudgetInputs } from '../self-implement/budget-gate.js';
17
19
  import { installDeliverableVerifyCliCommand, type InstallDeliverableVerifyCliDeps } from './deliverable-verify-cli.js';
@@ -198,13 +200,16 @@ function printHarnessAskMarkerDryRun(goalPath?: string): void {
198
200
  const warnings = inspectHarnessAskMarkerWarnings(ask);
199
201
  if (warnings.length === 0) {
200
202
  console.log('[dry-run] ✅ ask 마커 — 경고 없음');
201
- return;
203
+ } else {
204
+ for (const warning of warnings) console.log(`[dry-run] ⚠️ ask 마커 — ${warning}`);
202
205
  }
203
- for (const warning of warnings) console.log(`[dry-run] ⚠️ ask 마커 — ${warning}`);
204
206
  } catch (error) {
205
207
  const message = error instanceof Error ? error.message.split('\n')[0] : String(error);
206
208
  console.log(`[dry-run] ⚠️ ask 마커 — 검사 실패: ${message}`);
207
209
  }
210
+ for (const finding of lintGoalFile(ask, 'main').filter(({ tag }) => tag === 'release-note')) {
211
+ console.log(`[dry-run] ⚠️ 릴리스 노트 — ${finding.message}`);
212
+ }
208
213
  }
209
214
 
210
215
  function printHarnessLaunchDryRun(preview: {
@@ -218,7 +223,9 @@ function printHarnessLaunchDryRun(preview: {
218
223
  console.log(`[dry-run] 입력: ${preview.input}`);
219
224
  console.log(`[dry-run] 입구: ${preview.entrance}`);
220
225
  console.log(`[dry-run] 시작 예정: ${preview.wouldStart}`);
221
- console.log('[dry-run] 전제 검사: ask 마커만 돌렸다 · 원격 조회(열린 PR·런 원장)는 돌리지 않음');
226
+ console.log(preview.goalPath
227
+ ? '[dry-run] 전제 검사: ask 마커·릴리스 노트 린트만 돌렸다 · 원격 조회(열린 PR·런 원장)는 돌리지 않음'
228
+ : '[dry-run] 전제 검사: ask 마커만 돌렸다 · 원격 조회(열린 PR·런 원장)는 돌리지 않음');
222
229
  printHarnessAskMarkerDryRun(preview.goalPath);
223
230
  if (preview.goalPath) console.log(renderGoalTemplateDryRun(preview.goalPath, preview.goalType));
224
231
  if (preview.target !== undefined) {
@@ -315,7 +322,8 @@ function resolveLaunchSubstrate(opts: HarnessSubstrateOpts): ResolvedHarnessSubs
315
322
  return resolved;
316
323
  }
317
324
  /** ⭐ 런 계약의 실행 칸 = pod — 호스트는 그래프를 안 돌리고 Pod 로 보낸다(harness-pod-dispatch.ts). */
318
- async function onPod(opts: unknown, entrance: 'cli-harness-ask' | 'cli-harness-say', input: string, pool: string): Promise<void> {
325
+ async function onPod(opts: unknown, entrance: 'cli-harness-ask' | 'cli-harness-say', input: string, pool: string,
326
+ recordDispatch: (input: DispatchTaskInput, deps?: DispatchTaskDeps) => ReturnType<typeof dispatchTask> = dispatchTask): Promise<void> {
319
327
  const o = opts as HarnessSubstrateOpts;
320
328
  if (o.target !== undefined) {
321
329
  debug.log('harness.pod', 'target-refused', { target: o.target });
@@ -324,10 +332,59 @@ async function onPod(opts: unknown, entrance: 'cli-harness-ask' | 'cli-harness-s
324
332
  process.exitCode = 2;
325
333
  return;
326
334
  }
335
+ let dispatchRecorded = false;
336
+ try {
337
+ const goalText = entrance === 'cli-harness-ask' ? readFileSync(input, 'utf8') : input;
338
+ await recordDispatch({
339
+ goalId: (entrance === 'cli-harness-ask' ? parseGoalId(goalText) : null)
340
+ ?? `request-${createHash('sha256').update(goalText).digest('hex').slice(0, 32)}`,
341
+ title: goalText.split(/\r?\n/, 1)[0]?.trim() || 'Untitled request',
342
+ goalText,
343
+ targetPaths: entrance === 'cli-harness-ask' ? tracedPathReferences(goalText).map((reference) => reference.path) : [],
344
+ spec: { input: entrance === 'cli-harness-ask' ? { file: input } : { text: input }, humanReadableOutput: false },
345
+ });
346
+ dispatchRecorded = true;
347
+ } catch (error) {
348
+ try { debug.log('execution-loop.gate', 'dispatch-unavailable', { reason: String(error) }); } catch { /* fail-soft */ }
349
+ }
327
350
  const { dispatchHarnessOnPod } = await import('./harness-pod-dispatch.js');
328
- const status = await dispatchHarnessOnPod({ entrance, input, podPool: pool, ...(o.podMemory ? { podMemory: o.podMemory } : {}), ...(o.autoMerge === false ? { autoMerge: false } : {}), ...(o.base ? { base: o.base } : {}), ...(o.json ? { json: true } : {}), ...(o.source ? { source: o.source } : {}) });
351
+ let output = '';
352
+ const status = await dispatchHarnessOnPod({ entrance, input, podPool: pool, ...(dispatchRecorded ? { dispatchRecorded: true } : {}), ...(o.podMemory ? { podMemory: o.podMemory } : {}), ...(o.autoMerge === false ? { autoMerge: false } : {}), ...(o.base ? { base: o.base } : {}), ...(o.json ? { json: true } : {}), ...(o.source ? { source: o.source } : {}) },
353
+ entrance === 'cli-harness-ask' ? { onOutput: (text) => { output = (output + text).slice(-16_000); } } : {});
329
354
  if (status !== 0) {
330
- console.error('Pod 실행에 닿지 못했다 — 풀·컨텍스트·SSH 연결을 확인하거나 `--substrate local` 로 명시하라');
355
+ let childError: string | undefined;
356
+ let podFailure: string | undefined;
357
+ let podJobFailed = false;
358
+ if (entrance === 'cli-harness-ask') {
359
+ for (const line of output.split('\n').reverse()) {
360
+ if (!line.trimStart().startsWith('[{') || !line.includes('"error"')) continue;
361
+ try {
362
+ const results: unknown = JSON.parse(line);
363
+ if (!Array.isArray(results)) continue;
364
+ for (const result of results) {
365
+ if (!result || typeof result !== 'object') continue;
366
+ const error = (result as { error?: { code?: string; message?: string } }).error;
367
+ if (typeof error?.code !== 'string' || typeof error.message !== 'string' || !error.code.startsWith('pod-')) continue;
368
+ if (error.code === 'pod-job-failed') {
369
+ podJobFailed = true;
370
+ childError = error.message.split('childError=')[1]?.split('\n', 1)[0];
371
+ }
372
+ podFailure = error.message;
373
+ break;
374
+ }
375
+ } catch { /* Non-result stdout cannot establish that a Pod child ran. */ }
376
+ if (podFailure) break;
377
+ }
378
+ if (!podFailure) {
379
+ const summary = output.split('\n').find((line) => /\s❌\sfailed · .* — pod-[\w-]+: /.test(line));
380
+ podFailure = summary?.split(/\s❌\sfailed · .* — pod-[\w-]+: /)[1];
381
+ podJobFailed = summary?.includes(' — pod-job-failed: ') ?? false;
382
+ if (podJobFailed) childError = podFailure?.split('childError=')[1]?.split('\n', 1)[0];
383
+ }
384
+ }
385
+ if (podJobFailed && childError && childError !== 'no-result-line') console.error(`Pod 안 자식이 실패했다 — ${childError}`);
386
+ else if (podFailure) console.error(`Pod 실행이 실패했다 — ${podFailure.split('\n', 1)[0]}`);
387
+ else console.error('Pod 실행에 닿지 못했다 — 풀·컨텍스트·SSH 연결을 확인하거나 `--substrate local` 로 명시하라');
331
388
  process.exitCode = status;
332
389
  }
333
390
  }
@@ -1502,6 +1559,8 @@ export interface HarnessCliCommandDeps {
1502
1559
  draftSweep?: HarnessDraftSweepDeps;
1503
1560
  ask?: HarnessAskHandler;
1504
1561
  say?: HarnessSayHandler;
1562
+ /** Observe-only Pod host dispatch seam. */
1563
+ podDispatchTask?: (input: DispatchTaskInput, deps?: DispatchTaskDeps) => ReturnType<typeof dispatchTask>;
1505
1564
  plan?: HarnessPlanHandler;
1506
1565
  /** ⭐ `plan` 주입이 «없을 때» 가는 기본 분기. 시험이 이 자리로 «CLI 의 기본 배선»을 문다
1507
1566
  * (⛔ 주입된 `plan` 이 파일을 스스로 만들면 그 시험은 배선을 «못 답한다» — 무인 리뷰 GOODHART 지적). */
@@ -1540,7 +1599,7 @@ export function installHarnessCliCommand(program: Command, deps: HarnessCliComma
1540
1599
  process.exitCode = 2;
1541
1600
  return Promise.resolve();
1542
1601
  }
1543
- return resolved.substrate === 'pod' ? onPod(opts, 'cli-harness-ask', goalPath, resolved.pool!) : ask(goalPath, normalizeHarnessAskSayOptions(opts));
1602
+ return resolved.substrate === 'pod' ? onPod(opts, 'cli-harness-ask', goalPath, resolved.pool!, deps.podDispatchTask) : ask(goalPath, normalizeHarnessAskSayOptions(opts));
1544
1603
  },
1545
1604
  );
1546
1605
  });
@@ -1564,7 +1623,7 @@ export function installHarnessCliCommand(program: Command, deps: HarnessCliComma
1564
1623
  process.exitCode = 2;
1565
1624
  return Promise.resolve();
1566
1625
  }
1567
- return resolved.substrate === 'pod' ? onPod(opts, 'cli-harness-say', sentence.join(' '), resolved.pool!) : say(sentence, normalizeHarnessAskSayOptions(opts));
1626
+ return resolved.substrate === 'pod' ? onPod(opts, 'cli-harness-say', sentence.join(' '), resolved.pool!, deps.podDispatchTask) : say(sentence, normalizeHarnessAskSayOptions(opts));
1568
1627
  },
1569
1628
  );
1570
1629
  });
@@ -19,6 +19,8 @@ export interface HarnessPodDispatchInput {
19
19
  /** say 의 문장, 또는 ask 의 골 문서 경로. */
20
20
  readonly input: string;
21
21
  readonly podPool?: string;
22
+ /** Host already recorded the observe-only dispatch; suppress a second decision in the Pod. */
23
+ readonly dispatchRecorded?: boolean;
22
24
  /** Pod 메모리 등급(standard|high) — 오케스트레이터 환경 `ELANOUS_POD_MEMORY_TIER` 로 Job 까지 간다. */
23
25
  readonly podMemory?: string;
24
26
  readonly autoMerge?: boolean;
@@ -49,7 +51,7 @@ export function podOrchestrateArgs(input: HarnessPodDispatchInput, goal: string)
49
51
  ];
50
52
  }
51
53
 
52
- type PodDispatchDeps = { run?: (cmd: string, args: readonly string[], env: NodeJS.ProcessEnv) => number | null; readFile?: (path: string) => string; cwd?: string; spawnChild?: typeof spawn };
54
+ type PodDispatchDeps = { run?: (cmd: string, args: readonly string[], env: NodeJS.ProcessEnv) => number | null; readFile?: (path: string) => string; cwd?: string; spawnChild?: typeof spawn; onOutput?: (text: string) => void };
53
55
 
54
56
  export function dispatchHarnessOnPod(input: HarnessPodDispatchInput, deps: PodDispatchDeps & { run: NonNullable<PodDispatchDeps['run']> }): number;
55
57
  export function dispatchHarnessOnPod(input: HarnessPodDispatchInput, deps?: PodDispatchDeps): number | Promise<number>;
@@ -68,6 +70,8 @@ export function dispatchHarnessOnPod(input: HarnessPodDispatchInput, deps: PodDi
68
70
  const args = podOrchestrateArgs(input, goal);
69
71
  const env = { ...process.env };
70
72
  delete env.ELANOUS_POD_GOAL_DOC;
73
+ delete env.ELANOUS_DISPATCH_RECORDED;
74
+ if (input.dispatchRecorded) env.ELANOUS_DISPATCH_RECORDED = '1';
71
75
  if (input.podMemory) env.ELANOUS_POD_MEMORY_TIER = input.podMemory;
72
76
  if (input.entrance === 'cli-harness-ask') {
73
77
  const root = findGitDir(cwd)?.root;
@@ -84,7 +88,9 @@ export function dispatchHarnessOnPod(input: HarnessPodDispatchInput, deps: PodDi
84
88
  goalChars: goal.length, ...(input.entrance === 'cli-harness-ask' ? { goalPath: input.input } : {}),
85
89
  });
86
90
  const run = deps.run ?? ((cmd: string, a: readonly string[], childEnv: NodeJS.ProcessEnv) => new Promise<number>((resolveStatus) => {
87
- const child = (deps.spawnChild ?? spawn)(cmd, [...a], { stdio: 'inherit', env: childEnv });
91
+ const child = (deps.spawnChild ?? spawn)(cmd, [...a], { stdio: deps.onOutput ? ['inherit', 'pipe', 'inherit'] : 'inherit', env: childEnv });
92
+ child.stdout?.setEncoding('utf8');
93
+ child.stdout?.on('data', (chunk: string) => { process.stdout.write(chunk); deps.onOutput?.(chunk); });
88
94
  let forwarded: NodeJS.Signals | undefined;
89
95
  const forward = (signal: NodeJS.Signals) => {
90
96
  forwarded = signal;