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
@@ -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;
@@ -3,25 +3,106 @@
3
3
  // 🩸 2026-09-27: 멈추려면 사람이 PID 둘(harness ask 부모 · self orchestrate 자식)과 Job 이름을 손으로 찾아야 했고,
4
4
  // 그 사이 떠 있던 조각이 main 에 병합됐다(#21048). 부모를 죽이면 자식이 PPID 1 고아로 2h36m 더 돌았다(🅞 보고).
5
5
  // B 조각(#21171)이 오케스트레이터에 `self-dev-runs/<runId>/pid.json` ⊕ SIGTERM 정리(새 잡 멈춤 · 자기 Job 삭제 · cancelled)를 넣었다.
6
- // 이 파일(A 조각)은 그 기록을 «읽고», 소유가 확인될 때만 신호를 보내고, `elanous.run=<runId>` 라벨 Job 을 지운다.
6
+ // 이 파일은 그 기록과 런 원장·프로세스 표를 읽고, 시작 시각이 확인된 후보에만 신호를 보내며, `elanous.run=<runId>` 라벨 Job 을 지운다.
7
7
  //
8
8
  // ⛔ 불변식: 기록된 시작 시각과 실제 프로세스 시작 시각이 다르면 신호를 보내지 않는다(PID 재사용 방지).
9
9
  // ⛔ 불변식: `elanous.run=<runId>` 라벨이 붙은 Job 만 지운다.
10
10
 
11
- import { readFileSync } from 'node:fs';
12
- import { join } from 'node:path';
11
+ import { readFileSync, readlinkSync } from 'node:fs';
12
+ import { isAbsolute, join, resolve } from 'node:path';
13
13
  import { spawnSync } from 'node:child_process';
14
14
  import { debug } from '../debug/log.js';
15
15
  import { selfDevRunsDir } from '../self-dev/run-store.js';
16
+ import { loadRunLedger, runLedgerDir } from '../self-implement/run-ledger.js';
16
17
 
17
18
  const RUN_ID = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
18
19
  /** `pid.json` 의 `startedAt` 은 `Date.now() − uptime` 근삿값이다 — 이만큼의 차이는 같은 프로세스로 본다. */
19
20
  export const START_TOLERANCE_MS = 5_000;
20
21
 
21
22
  export interface PidRecord { pid: number; startedAt: number; argv0?: string }
23
+ export interface StopProcessCandidate { pid: number; startedAt: number; via: 'pid.json' | 'argv-runId' | 'argv-goal' }
24
+ export type StopProcessScan = { status: 'ok'; candidates: StopProcessCandidate[] } | { status: 'failed'; reason: string };
25
+
26
+ /** The start event, not later bookkeeping, owns the goal document path. */
27
+ export function stopGoalPathFromLedger(runId: string, dir = runLedgerDir()): string | null {
28
+ try {
29
+ const start = loadRunLedger(runId, dir)?.find((entry) => entry.event === 'start');
30
+ const goal = start?.data.goalFile;
31
+ const root = start?.data.targetRoot;
32
+ if (typeof goal !== 'string' || !goal.trim()) return null;
33
+ return isAbsolute(goal) ? resolve(goal) : typeof root === 'string' && isAbsolute(root) ? resolve(root, goal) : goal;
34
+ } catch { return null; }
35
+ }
36
+
37
+ /** Parse ps rows without treating a partial or failed measurement as an empty table. Relative goal arguments require the candidate's own cwd, never the stop caller's cwd. */
38
+ export function discoverStopProcesses(runId: string, goalPath: string | null, table: string, processCwd: (pid: number) => string | null = () => null): StopProcessScan {
39
+ const candidates: StopProcessCandidate[] = [];
40
+ for (const line of table.split(/\r?\n/)) {
41
+ if (!line.trim() || /^\s*PID\s/.test(line)) continue;
42
+ const match = /^\s*(\d+)\s+((?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+\w+\s+\d+\s+\d\d:\d\d:\d\d\s+\d{4})\s+(.+)$/.exec(line);
43
+ if (!match) return { status: 'failed', reason: 'unparseable process table row' };
44
+ const pid = Number(match[1]);
45
+ const startedAt = Date.parse(match[2]!);
46
+ if (!Number.isSafeInteger(pid) || !Number.isFinite(startedAt)) return { status: 'failed', reason: 'invalid process table row' };
47
+ if (pid <= 1) continue;
48
+ const args = match[3]!.match(/"[^"]*"|'[^']*'|\S+/g)?.map((part) => part.replace(/^(['"])(.*)\1$/, '$2')) ?? [];
49
+ const entry = args.findIndex((arg) => /(?:^|\/)elanous(?:\.mjs)?$/.test(arg));
50
+ const executable = args[0] ?? '';
51
+ const runtime = /(?:^|\/)(?:bun|node)$/.test(executable);
52
+ if (entry < 0 || (entry !== 0 && (!runtime || entry !== 1))) continue;
53
+ const command = args.slice(entry + 1);
54
+ const launcher = command[0] === 'harness' && ['ask', 'say'].includes(command[1] ?? '')
55
+ || command[0] === 'dev' && command.some((arg) => arg === '--ask' || arg === '--say' || arg.startsWith('--ask=') || arg.startsWith('--say='))
56
+ || command[0] === 'self' && command[1] === 'orchestrate';
57
+ if (!launcher) continue;
58
+ const namedRunIds = command.flatMap((arg, i) => arg === '--run-id' || arg === '--runId'
59
+ ? [command[i + 1] ?? '']
60
+ : arg.startsWith('--run-id=') || arg.startsWith('--runId=') ? [arg.slice(arg.indexOf('=') + 1)] : []);
61
+ const runMatch = namedRunIds.includes(runId) || command.includes(runId);
62
+ // An explicitly different run takes precedence over a shared goal document.
63
+ if (namedRunIds.some((id) => id !== runId)) continue;
64
+ // A goal path is a positional or option value, not text embedded in a prompt.
65
+ const goalArgs = command.flatMap((arg, i) => {
66
+ if (arg === '--ask' || arg === '--say' || arg === '--goal-file' || arg === '--goal') return [command[i + 1] ?? ''];
67
+ if (arg.startsWith('--ask=') || arg.startsWith('--say=') || arg.startsWith('--goal-file=') || arg.startsWith('--goal=')) return [arg.slice(arg.indexOf('=') + 1)];
68
+ if (i > 0 && (command[i - 1] === '--ask' || command[i - 1] === '--say' || command[i - 1] === '--goal-file' || command[i - 1] === '--goal')) return [];
69
+ if (command[0] === 'harness' && i === 2 && (command[1] === 'ask' || command[1] === 'say')) return [arg];
70
+ if (command[0] === 'self' && command[1] === 'orchestrate' && i === 2) return [arg];
71
+ return [];
72
+ });
73
+ const goalMatch = goalPath !== null && goalArgs.some((arg) => {
74
+ if (isAbsolute(arg)) return isAbsolute(goalPath) && resolve(arg) === resolve(goalPath);
75
+ if (!isAbsolute(goalPath)) return false;
76
+ let cwd: string | null;
77
+ try { cwd = processCwd(pid); } catch { return false; }
78
+ return cwd !== null && isAbsolute(cwd) && resolve(cwd, arg) === resolve(goalPath);
79
+ });
80
+ if (runMatch || goalMatch) candidates.push({ pid, startedAt, via: runMatch ? 'argv-runId' : 'argv-goal' });
81
+ }
82
+ return { status: 'ok', candidates };
83
+ }
84
+
85
+ function stopProcessCwd(pid: number): string | null {
86
+ try {
87
+ if (process.platform === 'linux') return readlinkSync(`/proc/${pid}/cwd`);
88
+ const r = spawnSync('lsof', ['-a', '-p', String(pid), '-d', 'cwd', '-Fn'], { encoding: 'utf8', timeout: 5_000 });
89
+ if (r.error || r.status !== 0 || typeof r.stdout !== 'string') return null;
90
+ return r.stdout.split(/\r?\n/).find((line) => line.startsWith('n/'))?.slice(1) ?? null;
91
+ } catch { return null; }
92
+ }
93
+
94
+ function scanStopProcessTable(runId: string, goalPath: string | null): StopProcessScan {
95
+ let r: ReturnType<typeof spawnSync>;
96
+ try { r = spawnSync('ps', ['-eo', 'pid=,lstart=,args='], { encoding: 'utf8', timeout: 5_000, maxBuffer: 16 * 1024 * 1024 }); }
97
+ catch (error) { return { status: 'failed', reason: String(error) }; }
98
+ if (r.error || r.status !== 0 || typeof r.stdout !== 'string') return { status: 'failed', reason: String(r.error ?? r.stderr ?? `ps status ${r.status}`) };
99
+ return discoverStopProcesses(runId, goalPath, r.stdout, stopProcessCwd);
100
+ }
22
101
 
23
102
  export interface HarnessStopDeps {
24
103
  readPidRecord: (runId: string) => PidRecord | null;
104
+ readGoalPath: (runId: string) => string | null;
105
+ scanProcesses: (runId: string, goalPath: string | null) => StopProcessScan;
25
106
  /** 살아 있는 프로세스의 시작 시각(ms) · 없으면 null. */
26
107
  processStartMs: (pid: number) => number | null;
27
108
  kill: (pid: number, signal: NodeJS.Signals) => void;
@@ -34,8 +115,12 @@ export interface HarnessStopDeps {
34
115
 
35
116
  export interface HarnessStopResult {
36
117
  runId: string;
37
- process: 'stopped' | 'killed' | 'absent' | 'owner-mismatch';
118
+ process: 'stopped' | 'killed' | 'absent' | 'owner-mismatch' | 'unmeasured' | 'dry-run';
38
119
  pid?: number;
120
+ candidates: StopProcessCandidate[];
121
+ scanned: boolean;
122
+ pidRecordFound: boolean;
123
+ dryRun: boolean;
39
124
  jobsDeleted: number;
40
125
  contexts: string[];
41
126
  }
@@ -69,6 +154,8 @@ export function defaultHarnessStopDeps(overrides: Partial<HarnessStopDeps> = {})
69
154
  };
70
155
  return {
71
156
  readPidRecord: (runId) => readPidRecordFrom(selfDevRunsDir(), runId),
157
+ readGoalPath: stopGoalPathFromLedger,
158
+ scanProcesses: scanStopProcessTable,
72
159
  processStartMs: psProcessStartMs,
73
160
  kill: (pid, signal) => { process.kill(pid, signal); },
74
161
  sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
@@ -83,49 +170,91 @@ export function defaultHarnessStopDeps(overrides: Partial<HarnessStopDeps> = {})
83
170
  };
84
171
  }
85
172
 
86
- export async function stopHarnessRun(runId: string, deps: HarnessStopDeps, contexts?: readonly string[]): Promise<HarnessStopResult> {
173
+ export async function stopHarnessRun(runId: string, deps: HarnessStopDeps, contexts?: readonly string[], dryRun = false): Promise<HarnessStopResult> {
87
174
  if (!RUN_ID.test(runId)) throw new Error(`invalid run id: ${runId}`);
88
175
  // ── ⓐ 프로세스 ────────────────────────────────────────────
89
176
  let processState: HarnessStopResult['process'] = 'absent';
90
177
  const record = deps.readPidRecord(runId);
178
+ let scan: StopProcessScan;
179
+ let goalPath: string | null = null;
180
+ try { goalPath = deps.readGoalPath(runId); }
181
+ catch { /* 원장 판독 실패 — runId 단독 대조 */ }
182
+ try { scan = deps.scanProcesses(runId, goalPath); }
183
+ catch (error) { scan = { status: 'failed', reason: String(error) }; }
184
+ const candidates: StopProcessCandidate[] = [];
185
+ const seen = new Set<number>();
186
+ const add = (candidate: StopProcessCandidate) => {
187
+ if (!seen.has(candidate.pid)) { seen.add(candidate.pid); candidates.push(candidate); }
188
+ };
189
+ let recordMismatch = false;
91
190
  if (record) {
92
- const actual = deps.processStartMs(record.pid);
93
- if (actual === null) processState = 'absent';
94
- else if (Math.abs(actual - record.startedAt) > START_TOLERANCE_MS) processState = 'owner-mismatch';
95
- else {
96
- try { deps.kill(record.pid, 'SIGTERM'); } catch { /* 방금 끝났다 */ }
191
+ const recordedStart = deps.processStartMs(record.pid);
192
+ if (recordedStart !== null && Math.abs(recordedStart - record.startedAt) <= START_TOLERANCE_MS) {
193
+ add({ pid: record.pid, startedAt: recordedStart, via: 'pid.json' });
194
+ } else if (recordedStart !== null) { processState = 'owner-mismatch'; recordMismatch = true; }
195
+ }
196
+ if (scan.status === 'ok') {
197
+ for (const candidate of scan.candidates) {
198
+ if (candidate.pid !== record?.pid || !recordMismatch) add(candidate);
199
+ }
200
+ }
201
+ debug.log('harness.stop', 'process-candidates', { runId, found: candidates.map(({ via, pid }) => ({ via, pid })), scanned: scan.status === 'ok' });
202
+ if (dryRun) processState = 'dry-run';
203
+ else {
204
+ const verified: StopProcessCandidate[] = [];
205
+ for (const candidate of candidates) {
206
+ if (candidate.via === 'pid.json') { verified.push(candidate); continue; }
207
+ const actual = deps.processStartMs(candidate.pid);
208
+ if (actual === null) continue;
209
+ if (Math.abs(actual - candidate.startedAt) > START_TOLERANCE_MS) { processState = 'owner-mismatch'; continue; }
210
+ verified.push(candidate);
211
+ }
212
+ for (const candidate of verified) {
213
+ try { deps.kill(candidate.pid, 'SIGTERM'); } catch { /* 방금 끝났다 */ }
214
+ }
215
+ for (const candidate of verified) {
97
216
  processState = 'stopped';
98
217
  const step = 250;
99
218
  for (let waited = 0; waited < deps.graceMs; waited += step) {
100
219
  await deps.sleep(step);
101
- if (deps.processStartMs(record.pid) === null) break;
220
+ if (deps.processStartMs(candidate.pid) === null) break;
102
221
  }
103
- const still = deps.processStartMs(record.pid);
104
- if (still !== null && Math.abs(still - record.startedAt) <= START_TOLERANCE_MS) {
105
- try { deps.kill(record.pid, 'SIGKILL'); processState = 'killed'; } catch { /* 끝났다 */ }
222
+ const still = deps.processStartMs(candidate.pid);
223
+ const expectedStart = candidate.via === 'pid.json' ? record!.startedAt : candidate.startedAt;
224
+ if (still !== null && Math.abs(still - expectedStart) <= START_TOLERANCE_MS) {
225
+ try { deps.kill(candidate.pid, 'SIGKILL'); processState = 'killed'; } catch { /* 끝났다 */ }
106
226
  }
107
227
  }
228
+ if (processState === 'absent' && (scan.status === 'failed' || candidates.length > 0)) processState = 'unmeasured';
108
229
  }
109
230
  // ── ⓑ Pod Job (라벨로만) ───────────────────────────────────
110
- const targets = contexts && contexts.length > 0 ? [...contexts] : deps.kubeContexts();
231
+ const targets = dryRun ? [] : contexts && contexts.length > 0 ? [...contexts] : deps.kubeContexts();
111
232
  let jobsDeleted = 0;
112
233
  for (const context of targets) {
113
234
  const r = deps.kubectl(['--context', context, '-n', deps.namespace, 'delete', 'job', '-l', `elanous.run=${runId}`, '--wait=false']);
114
235
  if (r.status === 0) jobsDeleted += r.stdout.split('\n').filter((line) => line.startsWith('job.batch/')).length;
115
236
  else debug.log('harness.stop', 'job-delete-failed', { runId, context, error: r.stderr.slice(0, 200) }, { level: 'warn' });
116
237
  }
117
- const result: HarnessStopResult = { runId, process: processState, ...(record ? { pid: record.pid } : {}), jobsDeleted, contexts: targets };
238
+ const result: HarnessStopResult = { runId, process: processState, ...(record ? { pid: record.pid } : {}), candidates, scanned: scan.status === 'ok', pidRecordFound: record !== null, dryRun, jobsDeleted, contexts: targets };
118
239
  debug.log('harness.stop', 'stopped', { runId, process: processState, jobsDeleted, contexts: targets.length });
119
240
  return result;
120
241
  }
121
242
 
122
243
  export function formatHarnessStop(r: HarnessStopResult): string {
123
244
  const label: Record<HarnessStopResult['process'], string> = {
124
- stopped: '멈춤(SIGTERM)', killed: '멈춤(SIGKILL — 제한 시간 안에 안 끝났다)', absent: '없음', 'owner-mismatch': '소유 불일치(건드리지 않음 — PID 가 다른 프로세스로 재사용됐다)',
245
+ stopped: '멈춤(SIGTERM)', killed: '멈춤(SIGKILL — 제한 시간 안에 안 끝났다)',
246
+ absent: `못 찾음(pid.json ${r.pidRecordFound ? '확인' : '없음'} · 프로세스 표에서 runId·골 경로 0)`,
247
+ unmeasured: '못 잼(프로세스 표 또는 후보 시작 시각 확인 실패 — 없음이 아님)',
248
+ 'dry-run': 'dry-run(신호·Pod Job 삭제 없음)',
249
+ 'owner-mismatch': '소유 불일치(건드리지 않음 — PID 가 다른 프로세스로 재사용됐다)',
125
250
  };
126
251
  return [
127
252
  `harness stop ${r.runId}`,
128
253
  ` 프로세스: ${label[r.process]}${r.pid ? ` · pid ${r.pid}` : ''}`,
129
- ` Pod Job: ${r.jobsDeleted}개 지움 (문맥 ${r.contexts.length}개 · 라벨 elanous.run=${r.runId})`,
254
+ ...(!r.scanned && r.process !== 'unmeasured' ? [' 프로세스 표: 못 잼(기록 밖 발사 프로세스는 확인하지 못함)'] : []),
255
+ ...r.candidates.map(({ pid, startedAt, via }) => ` 후보: pid ${pid} · 시작 ${new Date(startedAt).toISOString()} · ${via}`),
256
+ r.dryRun
257
+ ? ` Pod Job: dry-run(조회·삭제하지 않음 · 라벨 elanous.run=${r.runId})`
258
+ : ` Pod Job: ${r.jobsDeleted}개 지움 (문맥 ${r.contexts.length}개 · 라벨 elanous.run=${r.runId})`,
130
259
  ].join('\n');
131
260
  }
@@ -16,7 +16,7 @@ export interface SelfSendCandidateDisplay {
16
16
  readonly hiddenStaleCount: number;
17
17
  }
18
18
 
19
- const SELF_SEND_GOAL_ATTEMPT_SUFFIX = /^(.*)-[0-9a-f]{8}$/;
19
+ const SELF_SEND_GOAL_ATTEMPT_SUFFIX = /^(.*)-[0-9a-f]{8}(?:-r[a-z0-9]{6})?$/;
20
20
 
21
21
  function selfSendGoalPrefix(spaceId: string): string | undefined {
22
22
  return SELF_SEND_GOAL_ATTEMPT_SUFFIX.exec(spaceId)?.[1];
@@ -104,6 +104,7 @@ export interface SelfSendDecisionInput {
104
104
 
105
105
  export interface SelfSendDecisionDeps {
106
106
  resolveRunScreen(runId: string): SelfSendRunScreen;
107
+ podFragmentsForRun(runId: string): readonly { spaceId: string }[];
107
108
  resolveTarget(space: string): SelfSendTargetResolution;
108
109
  isPodFragment(spaceId: string): boolean;
109
110
  screens(): readonly SelfSendCandidate[];
@@ -122,26 +123,34 @@ export function decideSelfSend(input: SelfSendDecisionInput, deps: SelfSendDecis
122
123
  if (space !== undefined && opts.run !== undefined) return refuse('--run 과 space 는 함께 사용할 수 없습니다.\n', 2);
123
124
  if (opts.run !== undefined && opts.run.trim() === '') return refuse('--run 에 빈 runId 를 줄 수 없습니다.\n', 2);
124
125
  let requestedSpace = space;
126
+ let podRunSpace: string | undefined;
125
127
  if (opts.run !== undefined) {
126
128
  const resolved = deps.resolveRunScreen(opts.run);
127
- if (resolved.logStoreStatus !== 'read') {
129
+ if (resolved.logStoreStatus !== 'read' && resolved.screenKey) {
128
130
  return refuse(`run 화면 해석 불가: 로그 스토어 ${resolved.logStoreStatus} (${resolved.logStorePath})\n`, 1);
129
131
  }
130
132
  if (!resolved.screenKey) {
131
- const last = resolved.lastEvent;
132
- const lastDetail = last
133
- ? ` 마지막 이벤트: ${last.category}/${last.event} (${Number.isFinite(Date.parse(last.timestamp)) ? `${Math.max(0, Math.floor((now - Date.parse(last.timestamp)) / 60_000))}분 전` : '시각 알 수 없음'})`
134
- : ' 마지막 이벤트: 없음';
135
- const status = resolved.missingStatus;
136
- const guidance = status === 'awaiting-start'
137
- ? '아직 화면을 띄우기 전입니다. 되묻기에 답하거나 저작이 끝날 때까지 기다리세요.'
138
- : status === 'pipeline-failed' ? '파이프라인이 오류로 멈췄습니다. 해당 error를 읽어 원인을 수리하세요.'
139
- : status === 'cleaned' ? '하니스가 정리되어 화면이 없습니다. 필요하면 새 런을 시작하세요.'
140
- : status === 'not-found' ? '이 runId의 이벤트가 없습니다. runId와 인스턴스 우주를 확인하세요.'
141
- : '화면을 아직 분류할 수 없습니다. 마지막 이벤트를 조사하세요.';
142
- return refuse(`run 화면 해석 불가: ${guidance}${lastDetail} (${opts.run})\n`, 1);
143
- }
144
- requestedSpace = normalizeSpaceId(resolved.screenKey);
133
+ const fragments = deps.podFragmentsForRun(opts.run);
134
+ if (fragments.length === 1) requestedSpace = podRunSpace = fragments[0]!.spaceId;
135
+ else {
136
+ if (fragments.length > 1) return refuse(`run 의 Pod 조각이 여러 개입니다: ${opts.run}. space 이름으로 다시 보내세요:\n${fragments.map((fragment) => ` ${fragment.spaceId}`).join('\n')}\n`, 2);
137
+ if (resolved.logStoreStatus !== 'read') {
138
+ return refuse(`run 화면 해석 불가: 로그 스토어 ${resolved.logStoreStatus} (${resolved.logStorePath}) · Pod 조각 기록도 0\n`, 1);
139
+ }
140
+ const last = resolved.lastEvent;
141
+ const lastDetail = last
142
+ ? ` 마지막 이벤트: ${last.category}/${last.event} (${Number.isFinite(Date.parse(last.timestamp)) ? `${Math.max(0, Math.floor((now - Date.parse(last.timestamp)) / 60_000))}분 전` : '시각 알 수 없음'})`
143
+ : ' 마지막 이벤트: 없음';
144
+ const status = resolved.missingStatus;
145
+ const guidance = status === 'awaiting-start'
146
+ ? '아직 화면을 띄우기 전입니다. 되묻기에 답하거나 저작이 끝날 때까지 기다리세요.'
147
+ : status === 'pipeline-failed' ? '파이프라인이 오류로 멈췄습니다. 해당 error를 읽어 원인을 수리하세요.'
148
+ : status === 'cleaned' ? '하니스가 정리되어 화면이 없습니다. 필요하면 새 런을 시작하세요.'
149
+ : status === 'not-found' ? '이 runId의 이벤트가 없습니다. runId와 인스턴스 우주를 확인하세요.'
150
+ : '화면을 아직 분류할 수 없습니다. 마지막 이벤트를 조사하세요.';
151
+ return refuse(`run 화면 해석 불가: ${guidance}${lastDetail} (${opts.run}) · Pod 조각 기록도 0\n`, 1);
152
+ }
153
+ } else requestedSpace = normalizeSpaceId(resolved.screenKey);
145
154
  }
146
155
  const hasMemo = opts.memo !== undefined;
147
156
  if (opts.stop && hasMemo) return refuse('self send에서는 --stop 과 --memo를 함께 사용할 수 없습니다.\n', 2);
@@ -158,7 +167,7 @@ export function decideSelfSend(input: SelfSendDecisionInput, deps: SelfSendDecis
158
167
  }
159
168
  requestedSpace = resolution.spaceId;
160
169
  }
161
- if (requestedSpace !== undefined && deps.isPodFragment(requestedSpace)) {
170
+ if (requestedSpace !== undefined && (podRunSpace !== undefined || deps.isPodFragment(requestedSpace))) {
162
171
  return { kind: 'send', spaceId: requestedSpace, channel: 'pod', warnings: [] };
163
172
  }
164
173
  const screens = deps.screens();
@@ -1,12 +1,15 @@
1
- import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { dirname } from 'node:path';
4
+ import { normalizeRunId } from './harness-space.js';
4
5
  import { controlInboxPath, type ControlMemoPayload } from './control-inbox.js';
5
6
  import { sendPodControl } from './pod-control-send.js';
6
7
  import type { Kubectl } from '../task-orchestrator/surfaces/self-implement-pod.js';
7
8
 
8
9
  export interface PodFragmentRecord {
9
10
  spaceId: string;
11
+ runId?: string;
12
+ parentRunId?: string;
10
13
  context: string;
11
14
  namespace: string;
12
15
  job: string;
@@ -29,6 +32,24 @@ export function podFragmentFinished(spaceId: string, env?: NodeJS.ProcessEnv): b
29
32
  return existsSync(`${podRecordPath(spaceId, env)}.finished`);
30
33
  }
31
34
 
35
+ export function listPodFragmentsForRun(runId: string, env?: NodeJS.ProcessEnv): PodFragmentRecord[] {
36
+ if (!runId || runId !== normalizeRunId(runId)) return [];
37
+ const dir = dirname(podRecordPath('fragment', env));
38
+ let names: string[];
39
+ try { names = readdirSync(dir); } catch { return []; }
40
+ const suffix = '.inbox.pod.json';
41
+ const matches: PodFragmentRecord[] = [];
42
+ for (const name of names) {
43
+ if (!name.endsWith(suffix)) continue;
44
+ const spaceId = name.slice(0, -suffix.length);
45
+ if (podFragmentFinished(spaceId, env)) continue;
46
+ const record = readPodFragment(spaceId, env);
47
+ if (record && ((record.runId === runId && normalizeRunId(record.runId) === runId)
48
+ || (record.parentRunId === runId && normalizeRunId(record.parentRunId) === runId))) matches.push(record);
49
+ }
50
+ return matches;
51
+ }
52
+
32
53
  export function writePodFragment(record: PodFragmentRecord, env?: NodeJS.ProcessEnv): void {
33
54
  const path = podRecordPath(record.spaceId, env);
34
55
  mkdirSync(dirname(path), { recursive: true });
@@ -22,11 +22,12 @@ export function slugifyFeature(feature: string): string {
22
22
  /** 하니스가 만드는 `self-impl/<슬러그>-<8자리 16진수>` 브랜치 이름.
23
23
  * goalId 가 있으면 읽는 자(`branchGoalId`, `/(?:^|-)goalid-([^/-]+)-/`)와 호환되는
24
24
  * `goalid-<id>-` 구간을 슬러그 앞에 싣는다. 없으면 기존과 바이트 단위로 같다. */
25
- export function plannedSelfImplBranch(feature: string, goalId?: string): string {
25
+ export function plannedSelfImplBranch(feature: string, goalId?: string, runId?: string): string {
26
26
  const slug = slugifyFeature(feature);
27
27
  const id = goalId?.trim() ?? '';
28
- if (!id) return `${WORKTREE_BRANCH_PREFIX}${slug}`;
28
+ const runSuffix = runId === undefined ? '' : `-r${runId.replace(/^run-/i, '').replace(/[^a-z0-9]/gi, '').slice(0, 6).toLowerCase()}`;
29
+ if (!id) return `${WORKTREE_BRANCH_PREFIX}${slug}${runSuffix}`;
29
30
  const readable = slug.replace(/-[0-9a-f]{8}$/, '');
30
31
  const digest = createHash('sha256').update(id).digest('hex').slice(0, 8);
31
- return `${WORKTREE_BRANCH_PREFIX}goalid-${id}-${readable}-${digest}`;
32
+ return `${WORKTREE_BRANCH_PREFIX}goalid-${id}-${readable}-${digest}${runSuffix}`;
32
33
  }