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
@@ -1,12 +1,15 @@
1
- import { createHash } from 'node:crypto';
1
+ import { createHash, randomUUID } from 'node:crypto';
2
2
  import { spawnSync } from 'node:child_process';
3
3
  import { closeSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { basename, dirname, isAbsolute, join, resolve, sep } from 'node:path';
6
6
  import { dlopen, FFIType } from 'bun:ffi';
7
7
  import { loadPluginManifestFromDir, type PluginManifest } from '../core/manifest.js';
8
+ import { loadPluginNodes } from '../../graph-kinds/plugin-nodes.js';
9
+ import { getNodeKindRegistration, listNodeKinds, unregisterPluginNodeKind } from '../../graph-kinds/registry.js';
8
10
  import { verifyIndex, type MarketplaceIndex, type VerifyIndexResult } from '../../market/signed-index.js';
9
11
  import { elanousStateRoot } from '../../autopilot/state-paths.js';
12
+ import { ensureMarketIndex, MarketFetchError, type MarketFetchOptions } from './market-fetch.js';
10
13
 
11
14
  const NAME = /^[a-z0-9][a-z0-9-]{1,39}$/;
12
15
  const VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][a-zA-Z0-9.-]+)?$/;
@@ -18,7 +21,7 @@ export type InstallEvent =
18
21
  | { event: 'verify'; signature: 'ok' | 'missing' | 'bad'; scan: 'safe' | 'caution' | 'dangerous' }
19
22
  | { event: 'consent'; capabilities: string[]; required: boolean }
20
23
  | { event: 'credentials'; connectors: Array<{ id: string; fields: Array<{ name: string; secret: boolean }> }> }
21
- | { event: 'registered'; kinds: string[]; graphs: string[]; skills: string[] }
24
+ | { event: 'registered'; /** Vocab file paths, not graph node kinds. */ kinds: string[]; nodes: string[]; nodeErrors: number; graphs: string[]; skills: string[] }
22
25
  | { event: 'done'; plugin: string; version: string; path: string };
23
26
 
24
27
  export interface InstalledPlugin {
@@ -27,6 +30,8 @@ export interface InstalledPlugin {
27
30
  market: string;
28
31
  path: string;
29
32
  sha256: string | null;
33
+ /** Absent for installations recorded before timestamps were introduced. */
34
+ installedAt?: string;
30
35
  }
31
36
 
32
37
  export interface InstallOptions {
@@ -34,12 +39,15 @@ export interface InstallOptions {
34
39
  marketDir?: string;
35
40
  cwd?: string;
36
41
  allowUnsigned?: boolean;
42
+ refresh?: boolean;
37
43
  yes?: boolean;
38
44
  trustedKeys?: ReadonlyArray<{ keyId: string; publicKey: string }>;
39
45
  verifySignature?: typeof verifyIndex;
40
46
  hashArtifact?: (bytes: Uint8Array) => string;
41
47
  onEvent?: (event: InstallEvent) => void;
42
48
  consent?: (capabilities: string[]) => boolean | Promise<boolean>;
49
+ fetcher?: MarketFetchOptions['fetcher'];
50
+ configPath?: string;
43
51
  }
44
52
 
45
53
  export class PluginInstallError extends Error {
@@ -115,28 +123,8 @@ function marketEntry(spec: string, opts: InstallOptions): { market: string; entr
115
123
  return { market, entry: matches[0]!, signature };
116
124
  }
117
125
 
118
- function unpackArchive(bytes: Uint8Array, target: string): void {
119
- const archive = join(dirname(target), 'artifact.tgz');
120
- writeFileSync(archive, bytes);
121
- try {
122
- const entries = child('tar', ['-tzf', archive]).split('\n');
123
- for (const item of entries) {
124
- if (item === '.' || item === './') continue;
125
- inside(target, item.replace(/^\.\//, ''));
126
- }
127
- // Links and special entries can write outside the destination before a post-extract scan.
128
- const types = child('tar', ['-tvzf', archive]).split('\n');
129
- if (types.some(line => line && !['-', 'd'].includes(line[0]!))) fail('scan', 'artifact contains links or special files');
130
- mkdirSync(target);
131
- child('tar', ['-xzf', archive, '-C', target, '--no-same-owner', '--no-same-permissions']);
132
- safeTree(target);
133
- } finally {
134
- rmSync(archive, { force: true });
135
- }
136
- }
137
-
138
- export function resolvePluginSource(spec: string, target: string, opts: InstallOptions = {}): { market: string; sha256: string | null; signature: 'ok' | 'missing'; expectedName?: string; expectedVersion?: string } {
139
- if (/^[a-z0-9][a-z0-9-]{1,39}@[a-z0-9][a-z0-9-]{1,39}$/.test(spec) && !existsSync(resolve(opts.cwd ?? process.cwd(), spec))) {
126
+ /** Offline: a signed index placed in `<markets>/<name>/` without a configured URL (pre-fetch behaviour — kept). */
127
+ function localMarketSource(spec: string, target: string, opts: InstallOptions): ResolvedPluginSource {
140
128
  const { market, entry, signature } = marketEntry(spec, opts);
141
129
  const source = entry.source;
142
130
  const sourceRoot = dirname(indexPath(market, opts));
@@ -159,7 +147,100 @@ export function resolvePluginSource(spec: string, target: string, opts: InstallO
159
147
  fail('io', `market entry has no verifiable artifact: ${entry.name}`);
160
148
  }
161
149
  return { market, sha256: entry.artifact?.sha256 ?? null, signature, expectedName: entry.name, expectedVersion: entry.version };
150
+ }
151
+
152
+ function hasLocalMarketIndex(market: string, opts: InstallOptions): boolean {
153
+ const base = resolve(opts.marketDir ?? join(opts.root ?? elanousStateRoot(), 'markets'));
154
+ return [join(base, market, 'marketplace.json'), join(base, market, '.agents', 'plugins', 'marketplace.json')].some(existsSync);
155
+ }
156
+
157
+ function unpackArchive(bytes: Uint8Array, target: string): void {
158
+ const archive = join(dirname(target), 'artifact.tgz');
159
+ writeFileSync(archive, bytes);
160
+ try {
161
+ const entries = child('tar', ['-tzf', archive]).split('\n');
162
+ for (const item of entries) {
163
+ if (item === '.' || item === './') continue;
164
+ inside(target, item.replace(/^\.\//, ''));
165
+ }
166
+ // Links and special entries can write outside the destination before a post-extract scan.
167
+ const types = child('tar', ['-tvzf', archive]).split('\n');
168
+ if (types.some(line => line && !['-', 'd'].includes(line[0]!))) fail('scan', 'artifact contains links or special files');
169
+ mkdirSync(target);
170
+ child('tar', ['-xzf', archive, '-C', target, '--no-same-owner', '--no-same-permissions']);
171
+ safeTree(target);
172
+ } finally {
173
+ rmSync(archive, { force: true });
174
+ }
175
+ }
176
+
177
+ type ResolvedPluginSource = { market: string; sha256: string | null; signature: 'ok' | 'missing'; expectedName?: string; expectedVersion?: string };
178
+
179
+ function marketSpec(spec: string, opts: InstallOptions): boolean {
180
+ return /^[a-z0-9][a-z0-9-]{1,39}@[a-z0-9][a-z0-9-]{1,39}$/.test(spec) && !existsSync(resolve(opts.cwd ?? process.cwd(), spec));
181
+ }
182
+
183
+ async function fetchedMarketSource(spec: string, target: string, opts: InstallOptions): Promise<ResolvedPluginSource> {
184
+ const [name, marketName] = spec.split('@') as [string, string];
185
+ let marketIndex: Awaited<ReturnType<typeof ensureMarketIndex>>;
186
+ try {
187
+ marketIndex = await ensureMarketIndex(marketName, {
188
+ root: opts.root, marketDir: opts.marketDir, configPath: opts.configPath, fetcher: opts.fetcher,
189
+ refresh: opts.refresh, verifySignature: opts.verifySignature, trustedKeys: opts.trustedKeys,
190
+ });
191
+ } catch (error) {
192
+ if (error instanceof MarketFetchError && /^market not configured: /.test(error.message) && hasLocalMarketIndex(marketName, opts)) {
193
+ return localMarketSource(spec, target, opts);
194
+ }
195
+ if (error instanceof MarketFetchError) throw new PluginInstallError(error.reason, error.message);
196
+ throw error;
197
+ }
198
+ const matches = marketIndex.index.plugins.filter(entry => entry.name === name);
199
+ if (matches.length !== 1) fail('io', `plugin not found or ambiguous in ${marketName}: ${name}`);
200
+ const entry = matches[0]!;
201
+ const artifact = entry.artifact;
202
+ if (!artifact || typeof artifact.key !== 'string' || !artifact.key || !HASH.test(artifact.sha256)
203
+ || !Number.isSafeInteger(artifact.bytes) || artifact.bytes < 0) fail('io', 'invalid artifact key, hash or size');
204
+ // Check the key as written, before URL resolution folds `..`/`.` or percent-encoded separators away.
205
+ if (!/^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(artifact.key) || artifact.key.split('/').some(part => part === '.' || part === '..')) fail('io', 'unsafe artifact key');
206
+ const base = new URL(marketIndex.market.url);
207
+ const url = new URL(artifact.key, base);
208
+ if (url.protocol !== 'https:' || url.origin !== base.origin || !url.pathname.startsWith(base.pathname)
209
+ || url.username || url.password || url.search || url.hash || /[\\?#]/.test(artifact.key)) fail('io', 'unsafe artifact key');
210
+ let response: Response;
211
+ // The URL checks above hold only if no redirect moves the download elsewhere.
212
+ try { response = await (opts.fetcher ?? fetch)(url.href, { redirect: 'error' }); }
213
+ catch { fail('io', 'artifact fetch failed: network error'); }
214
+ if (!response.ok || !response.body) fail('io', `artifact fetch failed: HTTP ${response.status}`);
215
+ if (response.redirected || (response.url && response.url !== url.href)) fail('io', 'artifact fetch was redirected');
216
+ const reader = response.body.getReader();
217
+ const chunks: Uint8Array[] = [];
218
+ let length = 0;
219
+ try {
220
+ for (;;) {
221
+ const { done, value } = await reader.read();
222
+ if (done) break;
223
+ length += value.byteLength;
224
+ if (length > artifact.bytes) fail('scan', 'artifact sha256 or size mismatch');
225
+ chunks.push(value);
226
+ }
227
+ } finally {
228
+ void reader.cancel().catch(() => {});
229
+ reader.releaseLock();
230
+ }
231
+ const bytes = new Uint8Array(length);
232
+ let offset = 0;
233
+ for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
234
+ if (length !== artifact.bytes || (opts.hashArtifact ?? (data => createHash('sha256').update(data).digest('hex')))(bytes).toLowerCase() !== artifact.sha256.toLowerCase()) {
235
+ fail('scan', 'artifact sha256 or size mismatch');
162
236
  }
237
+ unpackArchive(bytes, target);
238
+ return { market: marketName, sha256: artifact.sha256, signature: marketIndex.signature, expectedName: name, expectedVersion: entry.version };
239
+ }
240
+
241
+ export function resolvePluginSource(spec: string, target: string, opts: InstallOptions = {}): ResolvedPluginSource {
242
+ // Synchronous callers keep the offline contract: a signed index already placed under `<markets>/<name>/`.
243
+ if (marketSpec(spec, opts)) return localMarketSource(spec, target, opts);
163
244
  const local = resolve(opts.cwd ?? process.cwd(), spec);
164
245
  if (existsSync(local)) {
165
246
  copyPlugin(realpathSync(local), target);
@@ -187,19 +268,63 @@ function gitSource(url: string, destination: string, path?: unknown, sha?: unkno
187
268
  }
188
269
  }
189
270
 
271
+ function hasSkill(dir: string): boolean {
272
+ if (!existsSync(dir)) return false;
273
+ return readdirSync(dir, { withFileTypes: true }).some(entry => entry.isDirectory() && existsSync(join(dir, entry.name, 'SKILL.md')));
274
+ }
275
+
276
+ function inspectPluginNodes(packageRoot: string, manifest: PluginManifest): { nodes: string[]; nodeErrors: number } {
277
+ if (!manifest.contributes.nodes?.length) return { nodes: [], nodeErrors: 0 };
278
+ // Registration validates each staged definition. Use a private identity so existing installations
279
+ // cannot reject an otherwise valid kind as a duplicate; only this inspection's entries are removed.
280
+ const inspectionId = `inspect-${randomUUID().replaceAll('-', '')}`;
281
+ let nodeErrors = 0;
282
+ const nodes: string[] = [];
283
+ try {
284
+ nodeErrors = loadPluginNodes(packageRoot, { ...manifest, id: inspectionId }).errors.length;
285
+ } finally {
286
+ for (const entry of listNodeKinds()) {
287
+ if (entry.plugin !== inspectionId) continue;
288
+ nodes.push(`${manifest.id}:${entry.kind.slice(inspectionId.length + 1)}`);
289
+ const registered = getNodeKindRegistration(entry.graph, entry.kind);
290
+ if (registered) unregisterPluginNodeKind(entry.graph, entry.kind, inspectionId, registered);
291
+ }
292
+ }
293
+ return { nodes, nodeErrors };
294
+ }
295
+
296
+ function declaresMain(packageRoot: string): boolean {
297
+ for (const file of ['plugin.json', join('.codex-plugin', 'plugin.json')]) {
298
+ const path = join(packageRoot, file);
299
+ if (!existsSync(path)) continue;
300
+ try {
301
+ const raw: unknown = JSON.parse(readFileSync(path, 'utf8'));
302
+ if (raw && typeof raw === 'object' && 'main' in raw) return true;
303
+ } catch { return true; }
304
+ }
305
+ return !existsSync(join(packageRoot, 'plugin.json')) && !existsSync(join(packageRoot, '.codex-plugin', 'plugin.json'));
306
+ }
307
+
190
308
  export async function installPlugin(spec: string, opts: InstallOptions = {}): Promise<InstalledPlugin> {
191
309
  const root = resolve(opts.root ?? elanousStateRoot());
192
310
  const stageRoot = join(root, 'plugins', '.staging');
193
311
  mkdirSync(stageRoot, { recursive: true });
194
312
  const stage = mkdtempSync(join(stageRoot, 'plugin-'));
195
313
  try {
196
- const source = resolvePluginSource(spec, join(stage, 'package'), opts);
314
+ const source = marketSpec(spec, opts)
315
+ ? await fetchedMarketSource(spec, join(stage, 'package'), opts)
316
+ : resolvePluginSource(spec, join(stage, 'package'), opts);
197
317
  const manifest: PluginManifest = loadPluginManifestFromDir(join(stage, 'package'), { id: source.expectedName ?? basename(spec) }).manifest;
198
318
  if (!NAME.test(manifest.id) || !VERSION.test(manifest.version)) fail('io', 'invalid plugin name or version');
199
319
  if (source.expectedName && (manifest.id !== source.expectedName || manifest.version !== source.expectedVersion)) fail('conflict', 'market name or version differs from manifest');
200
320
  const packageRoot = join(stage, 'package');
201
321
  const mainPath = inside(packageRoot, manifest.main);
202
- if (!existsSync(mainPath) || !realpathSync(mainPath).startsWith(`${realpathSync(packageRoot)}${sep}`)) fail('io', `plugin main not found or unsafe: ${manifest.main}`);
322
+ // A skills/graphs-only pack (official `elanous-basics`) declares no `main` — the default `./plugin.ts` is not a promise.
323
+ // …but only a pack that ships something to install (skills or graphs) may omit it.
324
+ const shipsAssets = hasSkill(join(packageRoot, 'skills')) || (manifest.contributes.graphs?.length ?? 0) > 0;
325
+ if (existsSync(mainPath) || declaresMain(packageRoot) || !shipsAssets) {
326
+ if (!existsSync(mainPath) || !realpathSync(mainPath).startsWith(`${realpathSync(packageRoot)}${sep}`)) fail('io', `plugin main not found or unsafe: ${manifest.main}`);
327
+ }
203
328
  const name = manifest.id;
204
329
  const version = manifest.version;
205
330
  for (const asset of [...(manifest.contributes.graphs ?? []), ...(manifest.contributes.vocab ?? [])]) {
@@ -228,12 +353,13 @@ export async function installPlugin(spec: string, opts: InstallOptions = {}): Pr
228
353
  .map(entry => entry.name).sort()
229
354
  : []
230
355
  : manifest.contributes.skills.map(skill => skill.name ?? skill.id ?? '').filter(Boolean);
231
- emit({ event: 'registered', kinds, graphs, skills });
356
+ const { nodes, nodeErrors } = inspectPluginNodes(packageRoot, manifest);
357
+ emit({ event: 'registered', kinds, nodes, nodeErrors, graphs, skills });
232
358
  const installed = withLedgerLock(root, () => {
233
359
  if (existsSync(destination)) fail('conflict', `plugin already installed: ${name}@${version}`);
234
360
  mkdirSync(dirname(destination), { recursive: true });
235
361
  renameSync(join(stage, 'package'), destination);
236
- const item = { name, version, market: source.market, path: destination, sha256: source.sha256 };
362
+ const item = { name, version, market: source.market, path: destination, sha256: source.sha256, installedAt: new Date().toISOString() };
237
363
  try {
238
364
  writeLedger(root, [...listInstalledPlugins(root), item]);
239
365
  } catch (error) {
@@ -323,7 +449,8 @@ function writeLedger(root: string, entries: InstalledPlugin[]): void {
323
449
  const unique = new Map(entries.map(entry => [`${entry.market}/${entry.name}/${entry.version}`, entry]));
324
450
  const temp = `${ledger}.${process.pid}.tmp`;
325
451
  try {
326
- writeFileSync(temp, JSON.stringify([...unique.values()].map(({ name, version, market, sha256 }) => ({ name, version, market, sha256 })), null, 2));
452
+ writeFileSync(temp, JSON.stringify([...unique.values()].map(({ name, version, market, sha256, installedAt }) =>
453
+ ({ name, version, market, sha256, ...(installedAt === undefined ? {} : { installedAt }) })), null, 2));
327
454
  renameSync(temp, ledger);
328
455
  } finally {
329
456
  rmSync(temp, { force: true });
@@ -334,7 +461,7 @@ export function listInstalledPlugins(root = elanousStateRoot()): InstalledPlugin
334
461
  const base = join(root, 'plugins');
335
462
  if (!existsSync(base)) return [];
336
463
  const result: InstalledPlugin[] = [];
337
- let ledger: Array<Pick<InstalledPlugin, 'name' | 'version' | 'market' | 'sha256'>> = [];
464
+ let ledger: Array<Pick<InstalledPlugin, 'name' | 'version' | 'market' | 'sha256' | 'installedAt'>> = [];
338
465
  try {
339
466
  ledger = JSON.parse(readFileSync(join(base, 'installed.json'), 'utf8'));
340
467
  if (!Array.isArray(ledger)) ledger = [];
@@ -348,8 +475,12 @@ export function listInstalledPlugins(root = elanousStateRoot()): InstalledPlugin
348
475
  const path = join(base, market.name, name.name, version.name);
349
476
  try {
350
477
  const manifest = loadPluginManifestFromDir(path, { id: name.name }).manifest;
351
- if (manifest.id === name.name && manifest.version === version.name) result.push({ name: name.name, version: version.name, market: market.name, path,
352
- sha256: ledger.find(entry => entry.name === name.name && entry.version === version.name && entry.market === market.name)?.sha256 ?? null });
478
+ if (manifest.id === name.name && manifest.version === version.name) {
479
+ const recorded = ledger.find(entry => entry.name === name.name && entry.version === version.name && entry.market === market.name);
480
+ result.push({ name: name.name, version: version.name, market: market.name, path,
481
+ sha256: recorded?.sha256 ?? null,
482
+ ...(typeof recorded?.installedAt === 'string' ? { installedAt: recorded.installedAt } : {}) });
483
+ }
353
484
  } catch { /* malformed packages are not installed plugins */ }
354
485
  }
355
486
  }
@@ -59,6 +59,10 @@ export const PROJECT_ANCHOR_NON_CANDIDATE_FILENAMES = ['CLAUDE.md'] as const;
59
59
  export const PROJECT_TREE_MAX_CHARS = 8 * 1024;
60
60
  export const PROJECT_TREE_DIR_ENTRY_LIMIT = 20;
61
61
 
62
+ const NO_TOOLS_VERIFICATION_GUIDANCE = `## Chat without tools — verify rather than guess
63
+ This conversation has tools disabled. Do not guess repository or real-time facts that require verification, such as file counts, file contents, commits, or the current time. Say you cannot check them here. The project tree above is for navigation, not evidence for counts.
64
+ To check these facts, enable tools with \`elanous agent …\` or \`elanous chat --tools …\`.`;
65
+
62
66
  // P4 (2026-05-03) — Family-agnostic coding-agent lifecycle. Maps the
63
67
  // three user-facing pipelines to an explicit tool-batch sequence so every
64
68
  // model (not just codex) follows the same shape. Pattern source:
@@ -673,8 +677,7 @@ export function loadProjectAnchor(cwd: string): ProjectAnchorResult {
673
677
  return result;
674
678
  }
675
679
 
676
- /** Build the surface-agnostic preamble. Currently MVP — emits zero or
677
- * one system message (project anchor). Surfaces spread the result
680
+ /** Build the surface-agnostic preamble. Surfaces spread the result
678
681
  * into their own preamble; e.g.
679
682
  *
680
683
  * ```ts
@@ -729,6 +732,9 @@ export function buildUniversalPreamble(ctx: UniversalPreambleContext): LLMMessag
729
732
  content: `## Project Layout (top-level + 1 depth)\n\n\`\`\`\n${tree.content}\n\`\`\`\n\nUse this layout to pick directories directly instead of issuing broad project-root Grep/Glob calls. When AGENTS.md / CLAUDE.md describe a track or subsystem, this layout shows where its files actually live.`,
730
733
  });
731
734
  }
735
+ if (!ctx.enabledTools?.length) {
736
+ out.push({ role: 'system', content: NO_TOOLS_VERIFICATION_GUIDANCE });
737
+ }
732
738
  // P4 (2026-05-03) — Family-agnostic coding-agent lifecycle. Append
733
739
  // before family addendum so codex (which has its own discipline
734
740
  // overlay) sees both: lifecycle (what shape) + addendum (how to behave
@@ -0,0 +1,191 @@
1
+ import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { Database } from 'bun:sqlite';
3
+ import { dirname, join } from 'node:path';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { debug } from '../debug/log.js';
6
+ import { releaseLedgerRoot } from '../instance/resolve.js';
7
+
8
+ export type ChecklistStatus = 'green' | 'yellow' | 'red' | 'done';
9
+ export interface ChecklistItem {
10
+ id: string;
11
+ title: string;
12
+ status: ChecklistStatus;
13
+ owner?: string;
14
+ evidence?: string;
15
+ updatedAt: string;
16
+ updatedBy: string;
17
+ }
18
+ export interface ChecklistHistory {
19
+ at: string;
20
+ by: string;
21
+ id: string;
22
+ field: string;
23
+ from: unknown;
24
+ to: unknown;
25
+ released: string;
26
+ dev: string;
27
+ }
28
+ export interface Checklist {
29
+ version: string;
30
+ released: string;
31
+ dev: string;
32
+ items: ChecklistItem[];
33
+ history: ChecklistHistory[];
34
+ }
35
+
36
+ export function devVersion(): string {
37
+ return (JSON.parse(readFileSync(join(import.meta.dir, '..', '..', 'package.json'), 'utf8')) as { version: string }).version;
38
+ }
39
+
40
+ function releasedVersion(): string {
41
+ const dir = join(releaseLedgerRoot(), 'release');
42
+ if (!existsSync(dir)) return '';
43
+ return readdirSync(dir).filter((v) => /^\d+\.\d+\.\d+$/.test(v) && existsSync(join(dir, v, 'release.json')))
44
+ .filter((v) => {
45
+ try { const record = JSON.parse(readFileSync(join(dir, v, 'release.json'), 'utf8')) as { version?: string; publishedAt?: string }; return record.version === v && typeof record.publishedAt === 'string'; }
46
+ catch { return false; }
47
+ })
48
+ .sort((a, b) => {
49
+ const aa = a.split('.').map(Number), bb = b.split('.').map(Number);
50
+ return (bb[0]! - aa[0]!) || (bb[1]! - aa[1]!) || (bb[2]! - aa[2]!);
51
+ })[0] ?? '';
52
+ }
53
+
54
+ function pathFor(v: string): string {
55
+ if (!/^\d+\.\d+\.\d+(?:-(?:rc|alpha|beta)\.\d+)?$/.test(v)) throw new Error(`체크리스트 판이 아니다: ${v}`);
56
+ return join(releaseLedgerRoot(), 'release', v, 'checklist.json');
57
+ }
58
+
59
+ export function listChecklist(v: string): Checklist {
60
+ const path = pathFor(v);
61
+ if (existsSync(path)) {
62
+ const data = JSON.parse(readFileSync(path, 'utf8')) as Checklist;
63
+ refresh(data);
64
+ return data;
65
+ }
66
+ return { version: v, released: releasedVersion(), dev: devVersion(), items: [], history: [] };
67
+ }
68
+
69
+ // SQLite's OS-backed write lock is released when the owning process exits, even without a finally block.
70
+ // Hold it across the JSON read/modify/atomic-rename sequence so writers never use stale snapshots.
71
+ function mutate(v: string, apply: (data: Checklist) => boolean): Checklist {
72
+ const path = pathFor(v);
73
+ mkdirSync(dirname(path), { recursive: true });
74
+ const lock = `${path}.mutex.sqlite`;
75
+ const db = new Database(lock, { create: true, strict: true });
76
+ try {
77
+ chmodSync(lock, 0o600);
78
+ db.exec('PRAGMA busy_timeout = 10000');
79
+ db.exec('BEGIN IMMEDIATE');
80
+ try {
81
+ const data = listChecklist(v);
82
+ if (apply(data)) save(v, data);
83
+ db.exec('COMMIT');
84
+ return data;
85
+ } catch (error) {
86
+ db.exec('ROLLBACK');
87
+ throw error;
88
+ }
89
+ } finally { db.close(); }
90
+ }
91
+
92
+ function save(v: string, data: Checklist): void {
93
+ const path = pathFor(v);
94
+ mkdirSync(dirname(path), { recursive: true });
95
+ const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
96
+ try {
97
+ writeFileSync(temp, `${JSON.stringify(data, null, 2)}\n`, { mode: 0o600 });
98
+ chmodSync(temp, 0o600);
99
+ renameSync(temp, path);
100
+ } finally { if (existsSync(temp)) rmSync(temp); }
101
+ }
102
+
103
+ function change(data: Checklist, id: string, field: string, from: unknown, to: unknown, by: string, at: string): void {
104
+ data.history.push({ at, by, id, field, from: from ?? null, to: to ?? null, released: data.released, dev: data.dev });
105
+ debug.log('release-loop.checklist', 'change', { version: data.version, id, field, from: from ?? null, to: to ?? null, by });
106
+ }
107
+
108
+ function refresh(data: Checklist): void { data.released = releasedVersion(); data.dev = devVersion(); }
109
+
110
+ export function addItem(v: string, input: { id: string; title: string; owner?: string }): Checklist {
111
+ if (!input.id.trim()) throw new Error('칸 id 가 비었다');
112
+ if (!input.title.trim()) throw new Error('칸 제목이 비었다');
113
+ return mutate(v, (data) => {
114
+ if (data.items.some((item) => item.id === input.id)) throw new Error(`이미 있는 칸: ${input.id}`);
115
+ const by = process.env.ELANOUS_TRACK || 'cli';
116
+ const at = new Date().toISOString();
117
+ const item: ChecklistItem = { id: input.id, title: input.title, status: 'yellow', ...(input.owner !== undefined ? { owner: input.owner } : {}), updatedAt: at, updatedBy: by };
118
+ data.items.push(item);
119
+ change(data, item.id, 'add', null, item, by, at);
120
+ return true;
121
+ });
122
+ }
123
+
124
+ export function setItem(v: string, id: string, patch: { status?: ChecklistStatus; evidence?: string; owner?: string }, by: string): Checklist {
125
+ return mutate(v, (data) => {
126
+ const item = data.items.find((i) => i.id === id);
127
+ if (!item) throw new Error(`없는 칸: ${id}`);
128
+ if (patch.status !== undefined && !['green', 'yellow', 'red', 'done'].includes(patch.status)) throw new Error(`잘못된 상태: ${patch.status}`);
129
+ const fields = (['evidence', 'owner', 'status'] as const).filter((field) => patch[field] !== undefined && patch[field] !== item[field]);
130
+ if (fields.length === 0) return false;
131
+ const at = new Date().toISOString();
132
+ for (const field of fields) {
133
+ const from = item[field];
134
+ const to = patch[field]!;
135
+ (item as unknown as Record<string, unknown>)[field] = to;
136
+ item.updatedAt = at;
137
+ item.updatedBy = by;
138
+ change(data, id, field, from, to, by, at);
139
+ }
140
+ return true;
141
+ });
142
+ }
143
+
144
+ export function removeItem(v: string, id: string, by: string): Checklist {
145
+ return mutate(v, (data) => {
146
+ const index = data.items.findIndex((i) => i.id === id);
147
+ if (index < 0) throw new Error(`없는 칸: ${id}`);
148
+ const at = new Date().toISOString();
149
+ const [item] = data.items.splice(index, 1);
150
+ change(data, id, 'remove', item, null, by, at);
151
+ return true;
152
+ });
153
+ }
154
+
155
+ export function summarize(v: string): ChecklistSummary {
156
+ return summarizeChecklist(listChecklist(v));
157
+ }
158
+
159
+ export interface ChecklistSummary { green: number; yellow: number; red: number; done: number; blocked: string[]; byOwner: Record<string, number> }
160
+
161
+ export function summarizeChecklist(data: Checklist): ChecklistSummary {
162
+ const summary = { green: 0, yellow: 0, red: 0, done: 0, blocked: [] as string[], byOwner: Object.create(null) as Record<string, number> };
163
+ for (const item of data.items) {
164
+ summary[item.status]++;
165
+ if (item.status === 'red') summary.blocked.push(item.id);
166
+ const owner = item.owner ?? 'unassigned';
167
+ summary.byOwner[owner] = (summary.byOwner[owner] ?? 0) + 1;
168
+ }
169
+ return summary;
170
+ }
171
+
172
+ export function seedFromRoadmap(v: string, markdown: string): Checklist {
173
+ return mutate(v, (data) => {
174
+ let added = false;
175
+ for (const line of markdown.split('\n')) {
176
+ const match = /^\|\s*(K\d+[a-z]?[′']?)\s*\|\s*(.*?)\s*\|\s*(.*?)\s*\|/.exec(line);
177
+ if (!match || data.items.some((i) => i.id === match[1])) continue;
178
+ const id = match[1]!;
179
+ const title = match[2]!;
180
+ const icon = match[3]!.trim();
181
+ const status: ChecklistStatus = icon.startsWith('🟢') ? 'green' : icon.startsWith('🔴') ? 'red' : icon.startsWith('✅') ? 'done' : 'yellow';
182
+ const at = new Date().toISOString();
183
+ const by = process.env.ELANOUS_TRACK || 'cli';
184
+ const item: ChecklistItem = { id, title, status, updatedAt: at, updatedBy: by };
185
+ data.items.push(item);
186
+ change(data, id, 'add', null, item, by, at);
187
+ added = true;
188
+ }
189
+ return added;
190
+ });
191
+ }
@@ -30,9 +30,10 @@ function blankFencedCode(text: string): string {
30
30
 
31
31
  export function parseReleaseNoteSection(rawPrBody: string): { fragment?: Section; problems: string[] } {
32
32
  const prBody = blankFencedCode(rawPrBody);
33
- const start = /^## 릴리스 노트[ \t]*\r?$/m.exec(prBody);
33
+ const sections = [...prBody.matchAll(/^## 릴리스 노트[ \t]*\r?$/gm)];
34
+ const start = sections.at(-1);
34
35
  if (!start) return { problems: [] };
35
- const rest = prBody.slice(start.index + start[0].length).replace(/^\r?\n/, '');
36
+ const rest = prBody.slice(start.index! + start[0].length).replace(/^\r?\n/, '');
36
37
  const nextHeading = /^ {0,3}#{1,6}(?:[ \t]+|$)/m.exec(rest);
37
38
  const body = nextHeading ? rest.slice(0, nextHeading.index) : rest;
38
39
  const fields = new Map<string, string>();
@@ -51,7 +52,9 @@ export function parseReleaseNoteSection(rawPrBody: string): { fragment?: Section
51
52
  if (duplicates.has('종류') || !kind || !['fix', 'feat', 'security', 'internal'].includes(kind)) problems.push('종류');
52
53
  if (duplicates.has('문서') || !doc || (doc.startsWith('없음') ? !/^없음\([^\s()]+(?:[^()]*)\)$/.test(doc) : !/^\S+$/.test(doc))) problems.push('문서');
53
54
  if (duplicates.has('대상') || (target !== 'next' && target !== 'later')) problems.push('대상');
54
- if (problems.length) return { problems };
55
+ const invalid = problems.length > 0;
56
+ if (sections.length > 1) problems.push(`multiple release-note sections (${sections.length}); using the last`);
57
+ if (invalid) return { problems };
55
58
  return {
56
59
  fragment: {
57
60
  line: line!, kind: kind as Section['kind'],
@@ -45,6 +45,7 @@ import { getUserConfig, type UserConfig } from '../user-config.js';
45
45
  import { resolveChildLlmPreference, type ChildLlmPreferenceInput, type ResolvedChildLlmPreference } from '../self-implement/child-llm-preference.js';
46
46
  import { readCachedGrokQuota } from '../oauth/codex-account-store.js';
47
47
  import { queryAbandonedDraftPrs, type AbandonedDraftPr } from '../cli/logs-abandoned-draft-prs.js';
48
+ import { branchLineageSlug } from '../cli/pr-lineage.js';
48
49
  import { decideDraft, type DraftTriagePr } from './draft-triage-rules.js';
49
50
  import { resolveLogTargets } from '../cli/logs-cli.js';
50
51
  import { LogStore } from '../mss/logging/log-store.js';
@@ -196,12 +197,12 @@ export interface DevSelfRunExecution {
196
197
 
197
198
  interface DraftTriage {
198
199
  closed: number[];
199
- kept: number | null;
200
+ kept: number[] | null;
200
201
  closeFailed: number[];
201
202
  skippedNotDraft: number[];
202
203
  }
203
204
 
204
- export type DraftPrViewState = { isDraft: boolean; state: string } | null;
205
+ export type DraftPrViewState = { isDraft: boolean; state: string; headRefName?: string } | null;
205
206
 
206
207
  export interface DraftPrInventory {
207
208
  readonly open: readonly (DraftTriagePr & { url: string; openedAtMs: number; runId?: string | null })[];
@@ -294,11 +295,13 @@ function defaultViewDraftPr(prUrl: string, timeoutMs?: number): DraftPrViewState
294
295
  if (IN_TEST_PROCESS()) return null;
295
296
  const target = draftPrTargetFromUrl(prUrl);
296
297
  if (!target) return null;
297
- const view = spawnSync('gh', ['pr', 'view', String(target.number), '--repo', target.repository, '--json', 'isDraft,state'], { encoding: 'utf8', ...(timeoutMs === undefined ? {} : { timeout: timeoutMs }) });
298
+ const view = spawnSync('gh', ['pr', 'view', String(target.number), '--repo', target.repository, '--json', 'isDraft,state,headRefName'], { encoding: 'utf8', ...(timeoutMs === undefined ? {} : { timeout: timeoutMs }) });
298
299
  if (view.status !== 0) return null;
299
300
  try {
300
- const parsed = JSON.parse(view.stdout) as { isDraft?: unknown; state?: unknown };
301
- return typeof parsed.isDraft === 'boolean' && typeof parsed.state === 'string' ? { isDraft: parsed.isDraft, state: parsed.state } : null;
301
+ const parsed = JSON.parse(view.stdout) as { isDraft?: unknown; state?: unknown; headRefName?: unknown };
302
+ return typeof parsed.isDraft === 'boolean' && typeof parsed.state === 'string'
303
+ ? { isDraft: parsed.isDraft, state: parsed.state, ...(typeof parsed.headRefName === 'string' ? { headRefName: parsed.headRefName } : {}) }
304
+ : null;
302
305
  } catch {
303
306
  return null;
304
307
  }
@@ -946,41 +949,58 @@ export async function executeDevSelfRun(
946
949
  const view = supervise?.viewDraftPr ?? defaultViewDraftPr;
947
950
  const closeDraft = supervise?.closeDraftPr ?? defaultCloseDraftPr;
948
951
  const commentDraft = supervise?.commentDraftPr ?? defaultCommentDraftPr;
949
- const openDrafts: string[] = [];
952
+ const openDrafts: Array<{ url: string; lineage: string | null }> = [];
950
953
  const skippedNotDraft: number[] = [];
951
954
  for (const prUrl of drafts) {
952
955
  let live: DraftPrViewState = null;
953
956
  try { live = view(prUrl); } catch { live = null; }
954
- if (live?.isDraft === true && live.state === 'OPEN') openDrafts.push(prUrl);
955
- else {
957
+ if (live?.isDraft === true && live.state === 'OPEN') {
958
+ openDrafts.push({ url: prUrl, lineage: live.headRefName ? branchLineageSlug(live.headRefName) : null });
959
+ } else {
956
960
  const number = prNumberFromUrl(prUrl);
957
961
  if (number !== null) skippedNotDraft.push(number);
958
962
  }
959
963
  }
960
- const keptUrl = converged ? undefined : openDrafts.at(-1);
961
- const kept = keptUrl === undefined ? null : prNumberFromUrl(keptUrl);
964
+ const keptByLineage = new Map<string, string>();
965
+ if (!converged) {
966
+ for (const draft of openDrafts) {
967
+ if (draft.lineage !== null) keptByLineage.set(draft.lineage, draft.url);
968
+ }
969
+ }
970
+ const keptDrafts = converged ? [] : openDrafts.filter(({ url, lineage }) => lineage === null || keptByLineage.get(lineage) === url);
971
+ const kept = keptDrafts.length === 0 ? null : keptDrafts.flatMap(({ url }) => {
972
+ const number = prNumberFromUrl(url);
973
+ return number === null ? [] : [number];
974
+ });
962
975
  const closed: number[] = [];
963
976
  const closeFailed: number[] = [];
964
977
  const landed = mergedNumbers.map((n) => `#${n}`).join(' ') || '없음';
965
- for (const prUrl of openDrafts) {
966
- if (prUrl === keptUrl) continue;
967
- const number = prNumberFromUrl(prUrl);
978
+ for (const { url, lineage } of openDrafts) {
979
+ if (!converged && (lineage === null || keptByLineage.get(lineage) === url)) continue;
980
+ const number = prNumberFromUrl(url);
981
+ const keptInLineage = lineage === null ? null : prNumberFromUrl(keptByLineage.get(lineage)!);
968
982
  const comment = converged
969
983
  ? `하니스 종결 트리아지: 이 런이 수렴해 대체됨 — 착지 ${landed}`
970
- : `하니스 종결 트리아지: 최신 산출 #${kept ?? 'unknown'} 로 대체됨`;
984
+ : `하니스 종결 트리아지: 최신 산출 #${keptInLineage ?? 'unknown'} 로 대체됨`;
971
985
  let ok = false;
972
- try { ok = closeDraft(prUrl, comment); } catch { ok = false; }
986
+ try { ok = closeDraft(url, comment); } catch { ok = false; }
973
987
  if (number === null) continue;
974
988
  if (ok) closed.push(number);
975
989
  else closeFailed.push(number);
976
990
  }
977
- if (keptUrl !== undefined) {
991
+ for (const { url, lineage } of keptDrafts) {
992
+ const keptLineage = [...keptByLineage.keys()].find((other) => other !== lineage);
993
+ if (lineage !== null && keptLineage !== undefined) {
994
+ try { debug.log('self-dev.triage', 'kept-distinct-piece', { pr: prNumberFromUrl(url), lineage, keptLineage }); } catch { /* fail-open */ }
995
+ }
978
996
  const rounds = Math.max(1, executedRounds);
979
- const handoff = closeFailed.length === 0
980
- ? '이 draft 가 이 골의 유일한 사람 판단 대상'
981
- : `닫지 못한 draft 도 남아 있다: ${closeFailed.map((n) => `#${n}`).join(' ')}`;
997
+ const handoff = closeFailed.length > 0
998
+ ? `닫지 못한 draft 도 남아 있다: ${closeFailed.map((n) => `#${n}`).join(' ')}`
999
+ : lineage === null ? '브랜치 계보 미확인 — 사람 판단 대상'
1000
+ : keptDrafts.length === 1 ? '이 draft 가 이 골의 유일한 사람 판단 대상'
1001
+ : '이 draft 가 이 계보의 사람 판단 대상';
982
1002
  const comment = `하니스 종결 트리아지: 멈춘 사유 ${supervisorStopReason ?? 'unsupervised'} · 라운드 ${rounds} · 착지한 조각 PR ${landed} · ${handoff}`;
983
- try { commentDraft(keptUrl, comment); } catch { /* fail-open */ }
1003
+ try { commentDraft(url, comment); } catch { /* fail-open */ }
984
1004
  }
985
1005
  draftTriage = { closed, kept, closeFailed, skippedNotDraft };
986
1006
  };