elanous 0.2.18 → 0.2.19

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 (404) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → J5H7BvqgSI8DUjtRRcA_S}/_buildManifest.js +1 -1
  4. package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/1157-cba0e8553d35a980.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/{726.83532d69452ee24b.js → 1470.98a6ab61ccb4131c.js} +1 -1
  7. package/apps/pwa/out/_next/static/chunks/171-23a877ed11720de9.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/2336-e830c777eb6bce04.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/{9247-269e4464cc4b491c.js → 2375-e061bb24cd2b5cde.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/{211-9c9090c59e2c5907.js → 2977-e38442e86289f1d3.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/{f6b7c9ba.64a895b8b2fedbdc.js → 2ea96958.e8c7e112de50b067.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/3094.67339318c7f72a3a.js +2 -0
  16. package/apps/pwa/out/_next/static/chunks/338-ceb956612dcfaa86.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/3395-835e4ceaa6335ba4.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +5 -0
  19. package/apps/pwa/out/_next/static/chunks/3441-be82e3595aef3a20.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/3689-89905039bf3e7257.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/3822-6f268bc26c6a84ac.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/{1268.0840ff91e083fcbd.js → 4323.c5dfb6f503304775.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/4457-d7df81c90f9d0268.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/4551-77dadae76f6cbace.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/{6110.1297d4093fdfbb0d.js → 4847.56f5bc454fbcc827.js} +2 -2
  28. package/apps/pwa/out/_next/static/chunks/4978.6fd93b974441e87f.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/5012-f746f6549b869f91.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/5791-bb82743fa6299562.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/{7327-2d69fb321ba73389.js → 6062-73e68343a9814f6a.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/62-5bfed1e23790b745.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/6320-9bc8561fde416e07.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/649-ca96d1c5d09e02b8.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/6726-85a7db59b288c53f.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/6735-33267c4630d0d963.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/7633-141682015873beb6.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/{935.4ddaa99fc1d1126c.js → 7647.2fcd30d894d5ee30.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/{f6231284-45726550aba08fca.js → 8046cadf-211ca3c0e00f2b72.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/8067-143791231c99bd02.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/8430-a10cfba8ef282d4e.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/8870-9614a213b990b10b.js +25 -0
  49. package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/9454-12c2f30dea62291c.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/976-4a9b4ebdca7c739a.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/9989-15cdf568bafa9fc8.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/ae787e4c-e5b1ef00b2dbceca.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/approvals/page-deef5ee191830f14.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-b193da485f9a14b2.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/board/page-b6e1a79df0e214f4.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/botlab/page-e84df4d907f703c7.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/bots/page-a5fb9a5db89ae4ea.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/ceo/page-1499f481d7fc52d6.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/chat/page-c7cf753aeb648790.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/consult/page-4118469e514e10fd.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/control/page-e968f7630654332a.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/decisions/page-505915052f947022.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/design-check/page-c2f962fb4a3b06c6.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/editor/page-7b5ed0bd8a589ec4.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/exec/page-cf8193250dd42dd4.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/field/page-cf3f5dfa35dea8e9.js +1 -0
  69. package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/inside/page-5b29d9b06ceadade.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/intake/page-a15e7fcc283e9ca7.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/layout-d2915e7b70ff3d0e.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/live/page-b70cd005d9169e25.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/loops/page-2595f0eba29f57e1.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/market/page-c776322454156e36.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-77bbd17f4435aa10.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/morning/page-049689e611ecc936.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/not-found-85954a4fb09c44eb.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/observatory/page-e773e552e1068ee6.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-5633c4bfa7684b4a.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-d829b92bd01b4994.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-061c317fc771becf.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/outputs/page-3a45ece8865ed4b6.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/app/page-0be990618c0d56bd.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/app/reflection/page-bfea4865dc244b73.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-0d6ba8e337741484.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/app/sessions/page-76a88b55d816482e.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-bba1542df59f61ce.js +1 -0
  89. package/apps/pwa/out/_next/static/chunks/app/settings/page-4185c95689bb372e.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b699e1a8d5d34b8b.js +1 -0
  91. package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +1 -0
  92. package/apps/pwa/out/_next/static/chunks/app/setup/page-c225e338c31bf274.js +1 -0
  93. package/apps/pwa/out/_next/static/chunks/app/share/page-449353f70507ca1f.js +1 -0
  94. package/apps/pwa/out/_next/static/chunks/app/showroom/page-90b889714102476d.js +1 -0
  95. package/apps/pwa/out/_next/static/chunks/app/tasks/page-f1ee7fbeb7f96ddc.js +1 -0
  96. package/apps/pwa/out/_next/static/chunks/app/term/page-d59a95e1434dfaa0.js +1 -0
  97. package/apps/pwa/out/_next/static/chunks/app/today/page-71491a23414d1a47.js +1 -0
  98. package/apps/pwa/out/_next/static/chunks/app/trace/page-de7112bdb65fa4fe.js +1 -0
  99. package/apps/pwa/out/_next/static/chunks/app/vault/page-feb4d0d88079660d.js +1 -0
  100. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f17d887b40dd6b1d.js +1 -0
  101. package/apps/pwa/out/_next/static/chunks/app/workflows/page-8da77be2192bcd5d.js +1 -0
  102. package/apps/pwa/out/_next/static/chunks/app/workspace/page-72c3b9a5e9582b29.js +1 -0
  103. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3afe4c8d0c48c39.js +1 -0
  104. package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +1 -0
  105. package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +1 -0
  106. package/apps/pwa/out/_next/static/chunks/main-app-b9308f2d8a931531.js +1 -0
  107. package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +1 -0
  108. package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +1 -0
  109. package/apps/pwa/out/_next/static/chunks/webpack-6f35f9d892fa630b.js +1 -0
  110. package/apps/pwa/out/_next/static/css/{e99e3cff94ced78a.css → 599369d853c61df7.css} +1 -1
  111. package/apps/pwa/out/_next/static/css/85f53c6f35d11382.css +1 -0
  112. package/apps/pwa/out/_next/static/css/d3427996da11e480.css +1 -0
  113. package/apps/pwa/out/approvals/index.html +1 -1
  114. package/apps/pwa/out/approvals/index.txt +23 -23
  115. package/apps/pwa/out/autopilot/index.html +1 -1
  116. package/apps/pwa/out/autopilot/index.txt +23 -23
  117. package/apps/pwa/out/board/index.html +1 -1
  118. package/apps/pwa/out/board/index.txt +23 -23
  119. package/apps/pwa/out/botlab/index.html +1 -1
  120. package/apps/pwa/out/botlab/index.txt +22 -22
  121. package/apps/pwa/out/bots/index.html +1 -1
  122. package/apps/pwa/out/bots/index.txt +22 -22
  123. package/apps/pwa/out/ceo/index.html +1 -1
  124. package/apps/pwa/out/ceo/index.txt +23 -23
  125. package/apps/pwa/out/chat/index.html +1 -1
  126. package/apps/pwa/out/chat/index.txt +22 -22
  127. package/apps/pwa/out/consult/index.html +1 -1
  128. package/apps/pwa/out/consult/index.txt +23 -23
  129. package/apps/pwa/out/control/index.html +1 -1
  130. package/apps/pwa/out/control/index.txt +23 -23
  131. package/apps/pwa/out/decisions/index.html +1 -1
  132. package/apps/pwa/out/decisions/index.txt +22 -22
  133. package/apps/pwa/out/design-check/index.html +1 -1
  134. package/apps/pwa/out/design-check/index.txt +23 -23
  135. package/apps/pwa/out/editor/index.html +1 -1
  136. package/apps/pwa/out/editor/index.txt +25 -24
  137. package/apps/pwa/out/exec/index.html +1 -1
  138. package/apps/pwa/out/exec/index.txt +23 -23
  139. package/apps/pwa/out/field/index.html +1 -1
  140. package/apps/pwa/out/field/index.txt +23 -23
  141. package/apps/pwa/out/healthz +1 -1
  142. package/apps/pwa/out/index.html +1 -1
  143. package/apps/pwa/out/index.txt +22 -22
  144. package/apps/pwa/out/inside/index.html +1 -1
  145. package/apps/pwa/out/inside/index.txt +24 -23
  146. package/apps/pwa/out/intake/index.html +1 -1
  147. package/apps/pwa/out/intake/index.txt +23 -23
  148. package/apps/pwa/out/live/index.html +1 -1
  149. package/apps/pwa/out/live/index.txt +23 -23
  150. package/apps/pwa/out/loops/index.html +1 -1
  151. package/apps/pwa/out/loops/index.txt +28 -27
  152. package/apps/pwa/out/market/index.html +1 -1
  153. package/apps/pwa/out/market/index.txt +22 -22
  154. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  155. package/apps/pwa/out/missions/placeholder/index.txt +22 -22
  156. package/apps/pwa/out/morning/index.html +1 -1
  157. package/apps/pwa/out/morning/index.txt +23 -23
  158. package/apps/pwa/out/observatory/index.html +1 -1
  159. package/apps/pwa/out/observatory/index.txt +23 -23
  160. package/apps/pwa/out/ops/checklist/index.html +1 -1
  161. package/apps/pwa/out/ops/checklist/index.txt +22 -22
  162. package/apps/pwa/out/ops/release/index.html +1 -1
  163. package/apps/pwa/out/ops/release/index.txt +22 -22
  164. package/apps/pwa/out/ops/seats/index.html +1 -1
  165. package/apps/pwa/out/ops/seats/index.txt +22 -22
  166. package/apps/pwa/out/outputs/index.html +1 -1
  167. package/apps/pwa/out/outputs/index.txt +22 -22
  168. package/apps/pwa/out/reflection/index.html +1 -1
  169. package/apps/pwa/out/reflection/index.txt +23 -23
  170. package/apps/pwa/out/scheduler/index.html +1 -1
  171. package/apps/pwa/out/scheduler/index.txt +23 -23
  172. package/apps/pwa/out/sessions/index.html +1 -1
  173. package/apps/pwa/out/sessions/index.txt +22 -22
  174. package/apps/pwa/out/settings/devices/index.html +1 -1
  175. package/apps/pwa/out/settings/devices/index.txt +23 -23
  176. package/apps/pwa/out/settings/index.html +1 -1
  177. package/apps/pwa/out/settings/index.txt +23 -23
  178. package/apps/pwa/out/setup/done/index.html +1 -1
  179. package/apps/pwa/out/setup/done/index.txt +23 -23
  180. package/apps/pwa/out/setup/index.html +1 -1
  181. package/apps/pwa/out/setup/index.txt +23 -23
  182. package/apps/pwa/out/share/index.html +1 -1
  183. package/apps/pwa/out/share/index.txt +23 -23
  184. package/apps/pwa/out/showroom/index.html +1 -1
  185. package/apps/pwa/out/showroom/index.txt +22 -22
  186. package/apps/pwa/out/tasks/index.html +1 -1
  187. package/apps/pwa/out/tasks/index.txt +23 -23
  188. package/apps/pwa/out/term/index.html +1 -1
  189. package/apps/pwa/out/term/index.txt +23 -23
  190. package/apps/pwa/out/today/index.html +1 -1
  191. package/apps/pwa/out/today/index.txt +23 -23
  192. package/apps/pwa/out/trace/index.html +1 -1
  193. package/apps/pwa/out/trace/index.txt +23 -23
  194. package/apps/pwa/out/vault/index.html +1 -1
  195. package/apps/pwa/out/vault/index.txt +23 -23
  196. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  197. package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
  198. package/apps/pwa/out/workflows/index.html +1 -1
  199. package/apps/pwa/out/workflows/index.txt +23 -23
  200. package/apps/pwa/out/workspace/index.html +1 -1
  201. package/apps/pwa/out/workspace/index.txt +23 -23
  202. package/apps/pwa/out/worktrees/index.html +1 -1
  203. package/apps/pwa/out/worktrees/index.txt +23 -23
  204. package/graphs/ops/guardian.yaml +26 -0
  205. package/graphs/ops/recipes.yaml +3 -0
  206. package/package.json +1 -1
  207. package/scripts/coord-channel-watch.sh +2 -2
  208. package/scripts/discord-seat-probe.ts +3 -1
  209. package/scripts/lib/pch-regress-map.ts +23 -0
  210. package/scripts/morning-report.ts +18 -4
  211. package/scripts/pch-regress.ts +88 -0
  212. package/scripts/release-loop/gate-diff.ts +18 -0
  213. package/scripts/release-loop/gate-node.ts +338 -46
  214. package/scripts/release-loop/gate-partial.ts +106 -0
  215. package/scripts/release-loop/gate-timing.ts +120 -0
  216. package/scripts/release-loop/resume-release.ts +64 -5
  217. package/scripts/release-loop/shard-plan.ts +65 -0
  218. package/scripts/release-loop/unattended-release.ts +2 -0
  219. package/scripts/video/scene-table.ts +159 -0
  220. package/src/acp/core-turn-bridge.ts +39 -2
  221. package/src/away/presence.ts +40 -0
  222. package/src/away/release-watch.ts +255 -0
  223. package/src/chat/index.ts +2 -1
  224. package/src/chat/short-question.ts +3 -40
  225. package/src/cli/away-cli.ts +68 -0
  226. package/src/cli/guardian-cli.ts +49 -0
  227. package/src/cli/intake-cli.ts +13 -7
  228. package/src/cli/log-event-names.ts +1 -0
  229. package/src/cli/pod-cli.ts +5 -1
  230. package/src/cli/pty-drive-cli.ts +14 -3
  231. package/src/cli/release-cli.ts +32 -7
  232. package/src/cli/setup-cli.ts +31 -1
  233. package/src/cli/tasks-cli.ts +104 -11
  234. package/src/context-bus/context-now-public.ts +1 -1
  235. package/src/context-bus/context-now-surfaces.ts +16 -6
  236. package/src/context-bus/context-now.ts +97 -3
  237. package/src/core-turn/short-question.ts +100 -0
  238. package/src/dashboard/dashboard-status-lines.ts +24 -4
  239. package/src/dashboard/index.ts +50 -5
  240. package/src/dashboard/input/slash-executor.ts +6 -2
  241. package/src/dashboard/slash-runtime/dashboard-handlers.ts +63 -36
  242. package/src/dashboard/slash-runtime/decide-slash.ts +60 -0
  243. package/src/dashboard/slash-runtime/help-from-registry.ts +21 -8
  244. package/src/dashboard/slash-runtime/help-groups.ts +44 -0
  245. package/src/dashboard/slash-runtime/model-choices.ts +125 -0
  246. package/src/domains/guardian-tick.ts +138 -0
  247. package/src/domains/outbound-alert.ts +44 -13
  248. package/src/domains/schedule-registry.ts +23 -0
  249. package/src/domains/telegram-kind-route.ts +39 -0
  250. package/src/domains/watch-default-scheduler.ts +95 -0
  251. package/src/esc-abort-gate.ts +44 -0
  252. package/src/graph-runner/runner.ts +25 -5
  253. package/src/harness/harness-cli-command.ts +2 -0
  254. package/src/harness/harness-salvage-cli.ts +78 -0
  255. package/src/harness/harness-worktree-add.ts +68 -0
  256. package/src/harness/ledger-write-guard.ts +6 -16
  257. package/src/hq/hq.ts +6 -1
  258. package/src/index.ts +52 -3
  259. package/src/instance/test-write-guard-spy-first.fixture.ts +13 -0
  260. package/src/instance/test-write-guard.ts +200 -0
  261. package/src/intake-plane/digest.ts +39 -15
  262. package/src/intake-plane/harness-target.ts +4 -3
  263. package/src/intake-plane/intake-sources.ts +1 -0
  264. package/src/intake-plane/items.ts +1 -0
  265. package/src/loops/orchestrator/degradation.ts +57 -0
  266. package/src/loops/orchestrator/finish-rate.ts +33 -3
  267. package/src/loops/orchestrator/tick.ts +166 -45
  268. package/src/maturity/feature-maturity.ts +6 -4
  269. package/src/maturity/tui-slash-maturity.ts +1 -0
  270. package/src/mss/logging/log-store.ts +3 -0
  271. package/src/nexus/api/http-server.ts +16 -5
  272. package/src/nexus/api/loop-edges.ts +290 -17
  273. package/src/nexus/api/ops-api.ts +96 -28
  274. package/src/nexus/index.ts +7 -0
  275. package/src/release-loop/checklist.ts +2 -0
  276. package/src/release-loop/feature-store.ts +54 -7
  277. package/src/release-loop/light-rc.ts +173 -0
  278. package/src/release-loop/placement.ts +51 -17
  279. package/src/release-loop/rubric.ts +57 -1
  280. package/src/self-dev/dev-cli.ts +36 -0
  281. package/src/self-dev/orchestrate.ts +1 -0
  282. package/src/self-dev/pr-preview.ts +179 -0
  283. package/src/self-dev/shard-run.ts +23 -7
  284. package/src/self-implement/gate-cli.ts +29 -8
  285. package/src/self-implement/goal-author-clarification.ts +88 -13
  286. package/src/self-implement/goal-clarification-escalation.ts +2 -0
  287. package/src/self-implement/orchestrator.ts +2 -1
  288. package/src/self-implement/salvage-branches.ts +254 -0
  289. package/src/task-agent/actions.ts +63 -2
  290. package/src/task-agent/board.ts +132 -0
  291. package/src/task-agent/card-evidence.ts +203 -0
  292. package/src/task-agent/completion-evidence.ts +126 -0
  293. package/src/task-agent/judge.ts +18 -0
  294. package/src/task-agent/live-moves.ts +176 -0
  295. package/src/task-agent/mission.ts +32 -6
  296. package/src/task-agent/shadow.ts +183 -15
  297. package/src/task-agent/task-hand.ts +117 -15
  298. package/src/task-orchestrator/surfaces/pod-command-job.ts +64 -4
  299. package/src/task-orchestrator/surfaces/pod-lease.ts +142 -13
  300. package/src/task-orchestrator/surfaces/pod-pool.ts +13 -5
  301. package/src/task-orchestrator/surfaces/self-implement-pod.ts +2 -2
  302. package/src/telegram-away-command.ts +38 -0
  303. package/src/telegram-commands.ts +11 -0
  304. package/src/telegram-report-log-probe.ts +21 -0
  305. package/src/telegram-report.ts +27 -3
  306. package/src/tui-client/dashboard-session.ts +1 -0
  307. package/src/user-config.ts +134 -2
  308. package/src/version/packed-revision.json +1 -1
  309. package/apps/pwa/out/_next/static/chunks/1307-30dff22c9e1f3073.js +0 -1
  310. package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +0 -1
  311. package/apps/pwa/out/_next/static/chunks/1844-9ec4b1d12f6e80eb.js +0 -1
  312. package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +0 -1
  313. package/apps/pwa/out/_next/static/chunks/1948.15f356b15a625597.js +0 -5
  314. package/apps/pwa/out/_next/static/chunks/195-fb1d7690bf30bab4.js +0 -1
  315. package/apps/pwa/out/_next/static/chunks/1986-33e13d648a9df765.js +0 -1
  316. package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.js +0 -1
  317. package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +0 -1
  318. package/apps/pwa/out/_next/static/chunks/2537-6c289376caa88ca1.js +0 -1
  319. package/apps/pwa/out/_next/static/chunks/2699-325cc923ba7b8631.js +0 -1
  320. package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +0 -1
  321. package/apps/pwa/out/_next/static/chunks/311.e685738f92e6e2ec.js +0 -1
  322. package/apps/pwa/out/_next/static/chunks/3194-807b212fb8eae065.js +0 -1
  323. package/apps/pwa/out/_next/static/chunks/3324-d1d2979821dfa845.js +0 -1
  324. package/apps/pwa/out/_next/static/chunks/3558-bbb5ab553a0471d3.js +0 -1
  325. package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/4565-3d7e3fdeb43686a8.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/4795-f372490b178583fc.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/5360-be1f1d8f9fcc9755.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/6158.af2e93b7adff709b.js +0 -2
  335. package/apps/pwa/out/_next/static/chunks/6877-74399f69c7da1433.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/7290-f09c928a06c52e2d.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/7797-df5aa0381fdc078f.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/8060-cc008cc6fedfa818.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/8085-d065d0393c1760f5.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/8682-7061dd4fa7bd62ed.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/8695-846034ce44852c70.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +0 -25
  344. package/apps/pwa/out/_next/static/chunks/9070.067c106528ec1ae6.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/9103-60fbef33e66f0a46.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/a9f2b54d-7865aba0f4d2ee49.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-c1a96669158471ec.js +0 -1
  348. package/apps/pwa/out/_next/static/chunks/app/approvals/page-d92f7ee3994e84c7.js +0 -1
  349. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-4733c967b56a4277.js +0 -1
  350. package/apps/pwa/out/_next/static/chunks/app/board/page-d22fd849cc22f104.js +0 -1
  351. package/apps/pwa/out/_next/static/chunks/app/botlab/page-bb742f995c6769e7.js +0 -1
  352. package/apps/pwa/out/_next/static/chunks/app/bots/page-02b073a2ac793b83.js +0 -1
  353. package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +0 -1
  354. package/apps/pwa/out/_next/static/chunks/app/chat/page-8a9b3594bc23289b.js +0 -1
  355. package/apps/pwa/out/_next/static/chunks/app/consult/page-a0eb02638b1e6f31.js +0 -1
  356. package/apps/pwa/out/_next/static/chunks/app/control/page-308111abadf35670.js +0 -1
  357. package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +0 -1
  358. package/apps/pwa/out/_next/static/chunks/app/design-check/page-5d97b2901e0d1ea6.js +0 -1
  359. package/apps/pwa/out/_next/static/chunks/app/editor/page-c39554c321576982.js +0 -1
  360. package/apps/pwa/out/_next/static/chunks/app/exec/page-8c3d8e63ae2e1c98.js +0 -1
  361. package/apps/pwa/out/_next/static/chunks/app/field/page-9b72a07d424701c6.js +0 -1
  362. package/apps/pwa/out/_next/static/chunks/app/healthz/route-c1a96669158471ec.js +0 -1
  363. package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +0 -1
  364. package/apps/pwa/out/_next/static/chunks/app/intake/page-a572f58cd3ca5864.js +0 -1
  365. package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +0 -1
  366. package/apps/pwa/out/_next/static/chunks/app/live/page-be5ea965a07fbc41.js +0 -1
  367. package/apps/pwa/out/_next/static/chunks/app/loops/page-c130b63862437814.js +0 -1
  368. package/apps/pwa/out/_next/static/chunks/app/market/page-fa244bd146a00ae8.js +0 -1
  369. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-44d56d6391965a6a.js +0 -1
  370. package/apps/pwa/out/_next/static/chunks/app/morning/page-83a700d4c754d802.js +0 -1
  371. package/apps/pwa/out/_next/static/chunks/app/not-found-7b3170f374f77b9d.js +0 -1
  372. package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +0 -1
  373. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-a3c58f5c72a5f3f9.js +0 -1
  374. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +0 -1
  375. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +0 -1
  376. package/apps/pwa/out/_next/static/chunks/app/outputs/page-e8e1ecaea7cd71d2.js +0 -1
  377. package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +0 -1
  378. package/apps/pwa/out/_next/static/chunks/app/reflection/page-1467047104141297.js +0 -1
  379. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-2da6f9b8a38c6978.js +0 -1
  380. package/apps/pwa/out/_next/static/chunks/app/sessions/page-102310930791197a.js +0 -1
  381. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +0 -1
  382. package/apps/pwa/out/_next/static/chunks/app/settings/page-6ed0b73bae8351d8.js +0 -1
  383. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-44d5f767491692bf.js +0 -1
  384. package/apps/pwa/out/_next/static/chunks/app/setup/layout-c1a96669158471ec.js +0 -1
  385. package/apps/pwa/out/_next/static/chunks/app/setup/page-43336055451f6e37.js +0 -1
  386. package/apps/pwa/out/_next/static/chunks/app/share/page-3cc47dca0922ab9a.js +0 -1
  387. package/apps/pwa/out/_next/static/chunks/app/showroom/page-a77ee355a2304ff0.js +0 -1
  388. package/apps/pwa/out/_next/static/chunks/app/tasks/page-b17cd46cf0e9b8cf.js +0 -1
  389. package/apps/pwa/out/_next/static/chunks/app/term/page-61553851aa606146.js +0 -1
  390. package/apps/pwa/out/_next/static/chunks/app/today/page-a1f585e154e61c59.js +0 -1
  391. package/apps/pwa/out/_next/static/chunks/app/trace/page-7bf54c9bb9274af8.js +0 -1
  392. package/apps/pwa/out/_next/static/chunks/app/vault/page-5150ef17b5383a57.js +0 -1
  393. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +0 -1
  394. package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +0 -1
  395. package/apps/pwa/out/_next/static/chunks/app/workspace/page-134da468538baf48.js +0 -1
  396. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +0 -1
  397. package/apps/pwa/out/_next/static/chunks/framework-5c169757551134ed.js +0 -1
  398. package/apps/pwa/out/_next/static/chunks/main-59fe95fed40c7a01.js +0 -1
  399. package/apps/pwa/out/_next/static/chunks/main-app-4ca74dbbee7057c6.js +0 -1
  400. package/apps/pwa/out/_next/static/chunks/pages/_app-13a0a886df4d97a1.js +0 -1
  401. package/apps/pwa/out/_next/static/chunks/pages/_error-6af8591cd3a3f038.js +0 -1
  402. package/apps/pwa/out/_next/static/chunks/webpack-c2782b330447ffb6.js +0 -1
  403. package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +0 -1
  404. /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → J5H7BvqgSI8DUjtRRcA_S}/_ssgManifest.js +0 -0
@@ -9,15 +9,58 @@ import { listLoops } from '../../loops/registry.js';
9
9
  import { listSubSeatCharters } from './sub-seat-charters.js';
10
10
  import { SEATS } from './ops-seats.js';
11
11
  import { jsonResponse } from './json-response.js';
12
+ import { readTaskAgentState, type TaskCard as TaskAgentCard } from '../../task-agent/task-hand.js';
13
+ import { LogStore, logsDbPath } from '../../mss/logging/log-store.js';
14
+
15
+ /** `move` 칸에 실을 수 있는 값(allowlist) — 판단부 수 종류만, 문면은 싣지 않는다. */
16
+ export const LOOP_EDGE_MOVES = ['green-proposal'] as const;
12
17
 
13
18
  export interface LoopEdge {
14
19
  at: string;
15
- kind: 'request' | 'decision' | 'report' | 'card' | 'run';
20
+ kind: 'request' | 'decision' | 'report' | 'card' | 'run' | 'hand' | 'launch' | 'move';
16
21
  from: string;
17
22
  to: string;
18
23
  ref: string;
24
+ /** 체크리스트 칸 id(ID 정규식) — hand 간선만. */
25
+ cell?: string;
26
+ /** LOOP_EDGE_MOVES 중 하나 — move 간선만. */
27
+ move?: typeof LOOP_EDGE_MOVES[number];
28
+ /** hand 간선만 — 원장 행 mode, 없으면 key 접두(`shadow:orch:` = shadow · `orch:` = live). 못 정하면 생략. */
29
+ mode?: 'shadow' | 'live';
30
+ /** hand 간선만 — 그 행을 쓴 오케스트레이터 틱 runId(ID 정규식). 없으면 생략. */
31
+ tick?: string;
32
+ /** live claimed 뒤 handed·released 없이 N분 지남 = 발사 실패(끊긴 넘김). ref = 오케스트레이터 카드 id. */
33
+ broken?: true;
34
+ }
35
+
36
+ /** 노드 접두 — 조율 루프는 레지스트리 행 `loop:orchestrator` 그대로 쓴다. `queue:` 는 대기열 대체 간선 자리(조각 A 밖). */
37
+ export const TASK_AGENT_NODE = 'agent:task-agent';
38
+ export const ORCHESTRATOR_NODE = 'loop:orchestrator';
39
+
40
+ /** 한 런 원장에서 투영한 사실 — 문면은 feature(연결 비교 전용 · 응답에 싣지 않는다)뿐. */
41
+ export interface RunFact {
42
+ runId: string;
43
+ startAt?: string;
44
+ seat?: string;
45
+ feature?: string;
46
+ goalId?: string;
47
+ mergedAt?: string;
48
+ prs: Array<{ at: string; number: number }>;
49
+ children: string[];
19
50
  }
20
51
 
52
+ /** handed.jsonl 행 투영 — status 가 없으면 handed 로 읽는다(조각 A 주입 호환). claimed·released 는 끊긴 간선 판정에만 쓴다. */
53
+ export interface HandedRow {
54
+ at: string;
55
+ cardId?: string;
56
+ cell?: string;
57
+ status?: 'claimed' | 'handed' | 'released';
58
+ key?: string;
59
+ mode?: 'shadow' | 'live';
60
+ runId?: string;
61
+ }
62
+ export interface PodFinish { at: string; childRunId: string; prNumber: number }
63
+
21
64
  export interface LoopEdgesDeps {
22
65
  listCoord?: typeof listCoordEvents;
23
66
  cardRoot?: string;
@@ -28,6 +71,16 @@ export interface LoopEdgesDeps {
28
71
  listOrchestratorLaunches?: () => Array<{ key: string; runId: string }>;
29
72
  listLoopOwners?: () => Array<{ id: string; owner?: string | null }>;
30
73
  seatIds?: () => string[];
74
+ /** 기본 = `<root>/loop/orchestrator/handed.jsonl` · root = cardRoot ?? effectiveInstanceRoot(). */
75
+ listHanded?: () => HandedRow[];
76
+ /** live claimed 가 handed·released 없이 이만큼 지나면 끊긴 hand 간선(기본 10분). */
77
+ handBrokenAfterMs?: number;
78
+ /** 기본 = `<root>/task-agent-actions.json` 의 tasks(readTaskAgentState). */
79
+ listTaskCards?: () => TaskAgentCard[];
80
+ /** 기본 = ledgerDir(또는 연합 원장)에서 투영. listRunStarts 를 주입하고 이것을 안 주면 [] 다. */
81
+ listRunFacts?: () => RunFact[];
82
+ /** 대체 원천 — logs `self-implement.pod` `job-finished` 의 childRunId·prUrl(PR 번호만). */
83
+ listPodFinishes?: (since: string) => PodFinish[];
31
84
  }
32
85
 
33
86
  const ID = /^[a-zA-Z0-9_-]{1,128}$/;
@@ -100,9 +153,9 @@ function cardEdges(card: TaskCard, since: number, now: number, seats: ReadonlySe
100
153
  return edges;
101
154
  }
102
155
 
103
- function runStarts(since: string, dir: string, owners: ReadonlyMap<string, string>, seats: ReadonlySet<string>): Array<{ runId: string; at: string; seat: string; mergedAt?: string }> {
156
+ function runFacts(dir: string, owners: ReadonlyMap<string, string>, seats: ReadonlySet<string>): RunFact[] {
104
157
  if (!existsSync(dir)) return [];
105
- const result: Array<{ runId: string; at: string; seat: string; mergedAt?: string }> = [];
158
+ const result: RunFact[] = [];
106
159
  let names: string[];
107
160
  try { names = readdirSync(dir); } catch { return []; }
108
161
  for (const name of names) {
@@ -113,20 +166,84 @@ function runStarts(since: string, dir: string, owners: ReadonlyMap<string, strin
113
166
  const runId = name.slice(0, -6);
114
167
  let entries: RunLedgerEntry[] | null;
115
168
  try { entries = loadRunLedger(runId, dir); } catch { continue; }
116
- const start = entries?.find(entry => entry.event === 'start' && entry.runId === runId);
169
+ if (!entries) continue;
170
+ const start = entries.find(entry => entry.event === 'start' && entry.runId === runId);
117
171
  const feature = typeof start?.data.feature === 'string' ? start.data.feature : '';
118
172
  const launchSeat = start?.data.launchSeat ?? start?.data.seat ?? owners.get(feature)
119
173
  ?? start?.data.originAgent ?? start?.data.controller;
120
- if (start?.timestamp && seat(launchSeat, seats)) {
121
- const merged = entries?.find(entry => entry.event === 'merged' && entry.data.merged === true && typeof entry.timestamp === 'string');
122
- if (start.timestamp >= since || (merged?.timestamp && merged.timestamp >= since)) {
123
- result.push({ runId, at: start.timestamp, seat: launchSeat, ...(merged?.timestamp ? { mergedAt: merged.timestamp } : {}) });
124
- }
125
- }
174
+ const merged = entries.find(entry => entry.event === 'merged' && entry.data.merged === true && typeof entry.timestamp === 'string');
175
+ const goalId = [start, ...entries].map(entry => entry?.goalId ?? (typeof entry?.data.goalId === 'string' ? entry.data.goalId : undefined))
176
+ .find((value): value is string => typeof value === 'string' && /^[a-f0-9]{16}$/.test(value));
177
+ // pr-opened data = {url, number, branch, …} (TC 실측 10-07) — 번호는 data.number(양의 정수)만 읽는다. 못 읽으면 간선 0.
178
+ const prs = entries.filter(entry => entry.event === 'pr-opened' && typeof entry.timestamp === 'string'
179
+ && Number.isSafeInteger(entry.data.number) && (entry.data.number as number) > 0)
180
+ .map(entry => ({ at: entry.timestamp!, number: entry.data.number as number }));
181
+ // `pod-ledger-incomplete` 첫 줄 원장은 시작이 없어도 버리지 않는다 — pr 근거로만 쓰고 run 단계는 만들지 않는다.
182
+ const children = [...new Set(entries.filter(entry => entry.event === 'pod-child-run' && typeof entry.data.childRunId === 'string'
183
+ && ID.test(entry.data.childRunId) && entry.data.childRunId !== runId).map(entry => entry.data.childRunId as string))];
184
+ result.push({
185
+ runId, prs, children,
186
+ ...(start?.timestamp ? { startAt: start.timestamp } : {}),
187
+ ...(start?.timestamp && seat(launchSeat, seats) ? { seat: launchSeat } : {}),
188
+ ...(start?.timestamp && feature ? { feature } : {}),
189
+ ...(goalId ? { goalId } : {}),
190
+ ...(merged?.timestamp ? { mergedAt: merged.timestamp } : {}),
191
+ });
126
192
  }
127
193
  return result;
128
194
  }
129
195
 
196
+ function runStartsFrom(facts: readonly RunFact[], since: string): Array<{ runId: string; at: string; seat: string; mergedAt?: string }> {
197
+ return facts.filter(fact => fact.startAt !== undefined && fact.seat !== undefined
198
+ && (fact.startAt >= since || (fact.mergedAt !== undefined && fact.mergedAt >= since)))
199
+ .map(fact => ({ runId: fact.runId, at: fact.startAt!, seat: fact.seat!, ...(fact.mergedAt ? { mergedAt: fact.mergedAt } : {}) }));
200
+ }
201
+
202
+ /** handed.jsonl(`tick.ts` ORCH-TA-HAND) — 행의 at·status·key·cardId·cell·mode·runId 만. 넘김은 handed 뿐이고 claimed·released 는 끊긴 간선 판정용이다. */
203
+ function handedRows(root: string): HandedRow[] {
204
+ const path = join(root, 'loop', 'orchestrator', 'handed.jsonl');
205
+ try { if (!existsSync(path) || statSync(path).size > 5 * 1024 * 1024) return []; } catch { return []; }
206
+ let raw: string;
207
+ try { raw = readFileSync(path, 'utf8'); } catch { return []; }
208
+ const rows: HandedRow[] = [];
209
+ for (const line of raw.split('\n')) {
210
+ const row = line.trim() ? parse(line) : null;
211
+ const status = row?.status;
212
+ if ((status !== 'handed' && status !== 'claimed' && status !== 'released') || typeof row!.at !== 'string') continue;
213
+ if (status === 'handed' && typeof row!.cardId !== 'string') continue;
214
+ rows.push({
215
+ at: row!.at as string, status,
216
+ ...(typeof row!.cardId === 'string' ? { cardId: row!.cardId } : {}),
217
+ ...(typeof row!.cell === 'string' ? { cell: row!.cell } : {}),
218
+ ...(typeof row!.key === 'string' ? { key: row!.key } : {}),
219
+ ...(row!.mode === 'shadow' || row!.mode === 'live' ? { mode: row!.mode } : {}),
220
+ ...(typeof row!.runId === 'string' ? { runId: row!.runId } : {}),
221
+ });
222
+ }
223
+ return rows;
224
+ }
225
+
226
+ function taskCards(root: string): TaskAgentCard[] {
227
+ const tasks = object(readTaskAgentState<{ tasks?: unknown }>(join(root, 'task-agent-actions.json')).tasks);
228
+ return tasks ? Object.values(tasks).filter((card): card is TaskAgentCard => object(card) !== null) : [];
229
+ }
230
+
231
+ /** 대체 원천: Pod 런은 도는 동안 원장이 Pod 안에만 있다 — 호스트 logs 의 job-finished 에서 childRunId·PR 번호만 읽는다. */
232
+ function podFinishes(since: string): PodFinish[] {
233
+ const path = logsDbPath();
234
+ if (!existsSync(path)) return [];
235
+ const store = LogStore.openReadOnly(path);
236
+ try {
237
+ const result: PodFinish[] = [];
238
+ for (const row of store.queryAll({ exactCategories: ['self-implement.pod'], events: ['job-finished'], sinceMs: Date.parse(since) })) {
239
+ const data = row.data ? parse(row.data) : null;
240
+ const pr = typeof data?.prUrl === 'string' ? /^https:\/\/github\.com\/[\w.-]+\/[\w.-]+\/pull\/(\d+)$/.exec(data.prUrl) : null;
241
+ if (typeof data?.childRunId === 'string' && pr) result.push({ at: row.ts, childRunId: data.childRunId, prNumber: Number(pr[1]) });
242
+ }
243
+ return result;
244
+ } finally { store.close(); }
245
+ }
246
+
130
247
  /** Project only the key and runId of orchestrator launches, never their card snapshots or prose. */
131
248
  function orchestratorLaunches(root: string): Array<{ key: string; runId: string }> {
132
249
  const dir = join(root, 'loop', 'orchestrator');
@@ -152,17 +269,166 @@ function orchestratorLaunches(root: string): Array<{ key: string; runId: string
152
269
  return launches;
153
270
  }
154
271
 
272
+ type JourneyLink = { from: string; to: string; both: boolean };
273
+ type Journey = { links: JourneyLink[]; pieces: Map<string, string[]> };
274
+
275
+ /** 런 원장 start.feature 는 `boundReadableText(feature, 120)` 로 잘린다(orchestrator.ts RUN_START_FEATURE_MAX_CHARS) —
276
+ * 꼬리 표식 ` [truncated; originalChars=N]`·`[truncated]`·`…`(⊕ 손으로 쓴 `...`)을 떼고 공백을 정규화한다. */
277
+ const FEATURE_TRUNCATION_TAIL = /(?:\s*\[truncated(?:; originalChars=\d+)?\]|…|\.\.\.)$/;
278
+ const FEATURE_PREFIX_MIN = 40;
279
+ function normalizeText(text: string): string { return text.replace(/\s+/g, ' ').trim(); }
280
+ /** 잘린 feature 의 비교 접두 — 표식이 없으면 undefined(완전 일치만). 끝의 짝 잃은 서로게이트도 뗀다(UTF-16 절단). */
281
+ function featurePrefix(feature: string): string | undefined {
282
+ if (!FEATURE_TRUNCATION_TAIL.test(feature)) return undefined;
283
+ const prefix = normalizeText(feature.replace(FEATURE_TRUNCATION_TAIL, '').replace(/[\ud800-\udbff]$/, ''));
284
+ return prefix.length >= FEATURE_PREFIX_MIN ? prefix : undefined;
285
+ }
286
+ /** 카드 text ↔ 런 feature — 완전 일치, 또는 잘린 feature(≥40자)가 카드 text(공백 정규화)의 접두. */
287
+ function featureMatches(feature: string | undefined, text: string): boolean {
288
+ if (!feature || !text) return false;
289
+ if (feature === text) return true;
290
+ const prefix = featurePrefix(feature);
291
+ return prefix !== undefined && normalizeText(text).startsWith(prefix);
292
+ }
293
+
294
+ /** 카드 → 런 연결: 카드 runId(있으면 최우선) → goalId 일치 → feature 일치(완전 또는 잘린 접두) ∧ start ≥ launch.at — 뒤 둘은 «유일할 때만»
295
+ * (같은 feature 에 닿는 발사 카드가 둘이면 잇지 않는다). 자식 런(pod-child-run)은 부모로 닿는다. */
296
+ function linkedRun(card: TaskAgentCard, launchAt: string | undefined, launchedTexts: readonly string[], facts: readonly RunFact[], childIds: ReadonlySet<string>): string | undefined {
297
+ const explicitRun = (card as { runId?: unknown }).runId;
298
+ if (typeof explicitRun === 'string' && ID.test(explicitRun)) return explicitRun;
299
+ const pick = (candidates: RunFact[]): string | undefined => {
300
+ const top = candidates.filter(fact => !childIds.has(fact.runId));
301
+ return top.length === 1 ? top[0]!.runId : candidates.length === 1 ? candidates[0]!.runId : undefined;
302
+ };
303
+ if (typeof card.goalId === 'string' && card.goalId) {
304
+ const byGoal = pick(facts.filter(fact => fact.goalId === card.goalId));
305
+ if (byGoal) return byGoal;
306
+ }
307
+ const launchMs = launchAt === undefined ? NaN : Date.parse(launchAt);
308
+ if (!Number.isFinite(launchMs) || typeof card.text !== 'string' || !card.text) return undefined;
309
+ const runId = pick(facts.filter(fact => featureMatches(fact.feature, card.text) && fact.startAt !== undefined && Date.parse(fact.startAt) >= launchMs));
310
+ if (!runId) return undefined;
311
+ const feature = facts.find(fact => fact.runId === runId)!.feature;
312
+ return launchedTexts.filter(text => featureMatches(feature, text)).length === 1 ? runId : undefined;
313
+ }
314
+
315
+ /** hand 행의 mode — 행의 mode 필드가 우선, 없으면 key 접두(`shadow:orch:` = shadow · `orch:` = live). */
316
+ function handMode(row: HandedRow): 'shadow' | 'live' | undefined {
317
+ if (row.mode === 'shadow' || row.mode === 'live') return row.mode;
318
+ if (typeof row.key !== 'string') return undefined;
319
+ if (row.key.startsWith('shadow:orch:')) return 'shadow';
320
+ if (row.key.startsWith('orch:')) return 'live';
321
+ return undefined;
322
+ }
323
+
324
+ /** 넘김(hand)·발사(launch)·런→PR·green 제안(move) 간선을 더하고, ?ref= 여정에 쓸 id 연결을 돌려준다. 문면은 싣지 않는다. */
325
+ function taskAgentEdges(deps: LoopEdgesDeps, facts: readonly RunFact[], seats: ReadonlySet<string>, since: number, now: number, sinceIso: string, edges: LoopEdge[]): Journey {
326
+ // 주입된 카드 목록(시험)에 뿌리가 없으면 운영 뿌리를 읽지 않는다 — 공개본엔 bunfig preload 격리가 없다.
327
+ const root = deps.cardRoot ?? (deps.listCards ? undefined : effectiveInstanceRoot());
328
+ const journey: Journey = { links: [], pieces: new Map() };
329
+ let handed: HandedRow[] = [];
330
+ try { handed = (deps.listHanded ?? (() => root === undefined ? [] : handedRows(root)))(); } catch { /* An unreadable hand ledger hands nothing. */ }
331
+ const settled = new Set<string>();
332
+ for (const row of handed) if (row.key !== undefined && (row.status ?? 'handed') !== 'claimed') settled.add(row.key);
333
+ const brokenAfter = deps.handBrokenAfterMs ?? 10 * 60 * 1000;
334
+ const brokenKeys = new Set<string>();
335
+ for (const row of handed) {
336
+ const status = row.status ?? 'handed';
337
+ const mode = handMode(row);
338
+ const tick = typeof row.runId === 'string' && ID.test(row.runId) ? { tick: row.runId } : {};
339
+ if (status === 'handed') {
340
+ if (typeof row.cardId !== 'string' || !ID.test(row.cardId) || !inWindow(row.at, since, now)) continue;
341
+ edges.push({ at: row.at, kind: 'hand', from: ORCHESTRATOR_NODE, to: TASK_AGENT_NODE, ref: row.cardId,
342
+ ...(row.cell && ID.test(row.cell) ? { cell: row.cell } : {}), ...(mode ? { mode } : {}), ...tick });
343
+ // 넘김 행의 cardId 는 TASK-AGENT 카드다 — 소원(wish) 카드는 key(`orch:<wish>:<칸>`)에만 있다. 둘을 잇지 않으면
344
+ // `?ref=<wish>` 여정이 넘김에서 끊겨 데모 띠가 1단계 뒤로 «남음»에 멈춘다(10-07 리허설 2 실측 · JOURNEY-HAND-LINK).
345
+ const wish = row.key === undefined ? undefined : /^orch:([^:]+):([^:]+)$/.exec(row.key)?.[1];
346
+ if (wish !== undefined && ID.test(wish) && wish !== row.cardId) journey.links.push({ from: wish, to: row.cardId, both: true });
347
+ continue;
348
+ }
349
+ // live claimed 만(handed·released 없음 · N분 지남) = 발사 실패 — 끊긴 간선 하나. shadow claimed→released 는 간선 0.
350
+ if (status !== 'claimed' || mode !== 'live' || row.key === undefined || settled.has(row.key) || brokenKeys.has(row.key)) continue;
351
+ const claimedMs = Date.parse(row.at);
352
+ if (!Number.isFinite(claimedMs) || now - claimedMs < brokenAfter || !inWindow(row.at, since, now)) continue;
353
+ const parts = /^orch:([^:]+):([^:]+)$/.exec(row.key);
354
+ if (!parts || !ID.test(parts[1]!) || !ID.test(parts[2]!)) continue;
355
+ brokenKeys.add(row.key);
356
+ edges.push({ at: row.at, kind: 'hand', from: ORCHESTRATOR_NODE, to: TASK_AGENT_NODE, ref: parts[1]!, cell: parts[2]!, mode, ...tick, broken: true });
357
+ }
358
+ let cards: TaskAgentCard[] = [];
359
+ try { cards = (deps.listTaskCards ?? (() => root === undefined ? [] : taskCards(root)))().filter(card => typeof card?.id === 'string' && ID.test(card.id)); }
360
+ catch { /* An unreadable task-agent state launches nothing. */ }
361
+ const launchesOf = (card: TaskAgentCard) => (Array.isArray(card.history) ? card.history : [])
362
+ .filter(event => event?.event === 'launch' && typeof event.at === 'string');
363
+ const launchedTexts: string[] = [];
364
+ for (const card of cards) {
365
+ if (launchesOf(card).length && typeof card.text === 'string') launchedTexts.push(card.text);
366
+ }
367
+ const childIds = new Set(facts.flatMap(fact => fact.children));
368
+ for (const card of cards) {
369
+ if (typeof card.mission === 'string' && ID.test(card.mission)) journey.links.push({ from: card.id, to: card.mission, both: false });
370
+ if (Array.isArray(card.pieces)) journey.pieces.set(card.id, card.pieces.filter(piece => typeof piece === 'string' && ID.test(piece)));
371
+ const launches = launchesOf(card);
372
+ for (const launch of launches) {
373
+ if (seat(card.seat, seats) && inWindow(launch.at, since, now)) edges.push({ at: launch.at, kind: 'launch', from: TASK_AGENT_NODE, to: card.seat, ref: card.id });
374
+ }
375
+ const runId = linkedRun(card, launches[0]?.at, launchedTexts, facts, childIds);
376
+ if (runId) journey.links.push({ from: card.id, to: runId, both: true });
377
+ const green = object(card.greenProposal);
378
+ if (green && typeof green.at === 'string' && typeof green.checklistId === 'string' && ID.test(green.checklistId) && inWindow(green.at, since, now)) {
379
+ edges.push({ at: green.at, kind: 'move', from: TASK_AGENT_NODE, to: `release:${green.checklistId}`, ref: card.id, move: 'green-proposal' });
380
+ }
381
+ }
382
+ const prSeen = new Set<string>();
383
+ const prEdge = (runId: string, at: string, number: number) => {
384
+ const key = `${runId}:${number}`;
385
+ if (!ID.test(runId) || !Number.isSafeInteger(number) || number < 1 || prSeen.has(key) || !inWindow(at, since, now)) return;
386
+ prSeen.add(key);
387
+ edges.push({ at, kind: 'run', from: `loop:${runId}`, to: `pr:${number}`, ref: runId });
388
+ };
389
+ for (const fact of facts) {
390
+ for (const child of fact.children) journey.links.push({ from: fact.runId, to: child, both: true });
391
+ for (const pr of fact.prs) prEdge(fact.runId, pr.at, pr.number);
392
+ }
393
+ let pods: PodFinish[] = [];
394
+ // 대체 원천은 연합 원장(운영 기본)과 짝이다 — 원장을 주입했으면(시험·한 디렉터리) logs 를 읽지 않는다.
395
+ const injectedLedger = deps.ledgerDir !== undefined || deps.listRunStarts !== undefined || deps.listRunFacts !== undefined;
396
+ try { pods = (deps.listPodFinishes ?? (injectedLedger ? () => [] : podFinishes))(sinceIso); } catch { /* logs outage — the run ledger stays the primary source. */ }
397
+ for (const pod of pods) prEdge(pod.childRunId, pod.at, pod.prNumber);
398
+ return journey;
399
+ }
400
+
401
+ /** ?ref= 의 여정 id 집합 — 카드↔런·런↔자식 런은 양방향, 조각→미션은 위로만(형제 조각은 미션을 ref 로 줄 때만). */
402
+ function journeyIds(ref: string, journey: Journey): Set<string> {
403
+ const ids = new Set([ref, ...(journey.pieces.get(ref) ?? [])]);
404
+ const queue = [...ids];
405
+ while (queue.length) {
406
+ const id = queue.shift()!;
407
+ for (const link of journey.links) {
408
+ const next = link.from === id ? link.to : link.both && link.to === id ? link.from : undefined;
409
+ if (next !== undefined && !ids.has(next)) { ids.add(next); queue.push(next); }
410
+ }
411
+ }
412
+ return ids;
413
+ }
414
+
155
415
  /** GET only; project an allowlist of fields from the ledgers. Never serialize ledger records. */
156
416
  export function handleLoopEdgesGet(req: Request, deps: LoopEdgesDeps = {}): Response {
157
417
  const params = new URL(req.url).searchParams;
158
418
  const rawSince = params.get('since');
159
- const since = rawSince === null ? Date.now() - 60 * 60 * 1000 : Date.parse(rawSince);
419
+ const journeyRef = params.get('ref');
420
+ // ?mode=live|shadow — hand 간선만 그 mode 로 거른다(다른 종류 간선은 그대로).
421
+ const handModeFilter = params.get('mode');
422
+ // ?ref= 는 한 카드·런의 여정 — 60분 창과 limit 에 잘리지 않게 since 를 안 주면 처음부터 본다.
423
+ const since = rawSince === null ? (journeyRef === null ? Date.now() - 60 * 60 * 1000 : 0) : Date.parse(rawSince);
160
424
  const rawLimit = params.get('limit');
161
425
  const limit = rawLimit === null ? 100 : Number(rawLimit);
162
426
  const now = Date.now();
163
427
  if (rawSince !== null && !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?(?:Z|[+-]\d\d:\d\d)$/.test(rawSince)
164
- || !Number.isFinite(since) || since > now || !Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
165
- return jsonResponse({ error: 'usage: GET /v1/loops/edges?since=<ISO>&limit=<1..500>' }, 400);
428
+ || !Number.isFinite(since) || since > now || !Number.isSafeInteger(limit) || limit < 1 || limit > 500
429
+ || (journeyRef !== null && !ID.test(journeyRef))
430
+ || (handModeFilter !== null && handModeFilter !== 'live' && handModeFilter !== 'shadow')) {
431
+ return jsonResponse({ error: 'usage: GET /v1/loops/edges?since=<ISO>&limit=<1..500>&ref=<card|run|mission id>' }, 400);
166
432
  }
167
433
  const sinceIso = new Date(since).toISOString();
168
434
  const seats = new Set((deps.seatIds ?? (() => [...SEATS.map(s => s.seat), ...listSubSeatCharters().map(s => s.id)]))());
@@ -189,7 +455,7 @@ export function handleLoopEdgesGet(req: Request, deps: LoopEdgesDeps = {}): Resp
189
455
  finally { store.close(); }
190
456
  }
191
457
  }
192
- const readStarts = deps.listRunStarts ?? ((from: string) => {
458
+ const readFacts = deps.listRunFacts ?? (deps.listRunStarts ? () => [] : () => {
193
459
  const owners = new Map<string, string>();
194
460
  try {
195
461
  for (const entry of (deps.listLoopOwners ?? (() => listLoops()))()) {
@@ -197,8 +463,11 @@ export function handleLoopEdgesGet(req: Request, deps: LoopEdgesDeps = {}): Resp
197
463
  }
198
464
  } catch { /* A registry outage does not erase explicit launch-seat evidence. */ }
199
465
  return (deps.ledgerDir ? [deps.ledgerDir] : resolveFederatedRunLedgerDirectories({ includeTest: false }))
200
- .flatMap(dir => runStarts(from, dir, owners, seats));
466
+ .flatMap(dir => runFacts(dir, owners, seats));
201
467
  });
468
+ let facts: RunFact[] = [];
469
+ try { facts = readFacts().filter(fact => ID.test(fact.runId)); } catch { /* An unreadable ledger set links nothing. */ }
470
+ const readStarts = deps.listRunStarts ?? ((from: string) => runStartsFrom(facts, from));
202
471
  const runs = readStarts(sinceIso).filter(run => seat(run.seat, seats) && ID.test(run.runId)
203
472
  && (inWindow(run.at, since, now) || (run.mergedAt !== undefined && inWindow(run.mergedAt, since, now))));
204
473
  for (const run of runs) {
@@ -224,6 +493,10 @@ export function handleLoopEdgesGet(req: Request, deps: LoopEdgesDeps = {}): Resp
224
493
  }
225
494
  }
226
495
  }
227
- edges.sort((a, b) => b.at.localeCompare(a.at) || a.kind.localeCompare(b.kind) || a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
228
- return jsonResponse({ edges: edges.slice(0, limit) });
496
+ const links = taskAgentEdges(deps, facts, seats, since, now, sinceIso, edges);
497
+ const journey = journeyRef === null ? null : journeyIds(journeyRef, links);
498
+ const out = edges.filter(edge => (!journey || journey.has(edge.ref))
499
+ && (handModeFilter === null || edge.kind !== 'hand' || edge.mode === handModeFilter));
500
+ out.sort((a, b) => b.at.localeCompare(a.at) || a.kind.localeCompare(b.kind) || a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
501
+ return jsonResponse({ edges: out.slice(0, limit) });
229
502
  }
@@ -18,13 +18,35 @@ let seatsCache: ReturnType<typeof createSeatsCache> | null = null;
18
18
  export function setSeatsCacheForTest(cache: ReturnType<typeof createSeatsCache> | null): void { seatsCache = cache; }
19
19
  const LOG_PATH = /^\/v1\/ops\/release\/runs\/([^/]+)\/nodes\/([^/]+)\/log$/;
20
20
 
21
- interface RunNode { nodeId: string; ok?: unknown; output?: unknown }
21
+ interface RunNode { nodeId: string; ok?: unknown; output?: unknown; startedAt?: unknown; endedAt?: unknown }
22
22
  interface RunRecord {
23
23
  status: string;
24
24
  path: string[];
25
25
  startedAt: string;
26
26
  input?: unknown;
27
27
  nodes: RunNode[];
28
+ currentNode?: { nodeId?: unknown; startedAt?: unknown };
29
+ }
30
+
31
+ /** Node times come only from the ledger (GRAPH-NODE-TIMES); older ledgers have none and the screen says so. */
32
+ const ISO_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
33
+ function isoTime(value: unknown): string | undefined {
34
+ return typeof value === 'string' && ISO_TIME.test(value) && Number.isFinite(Date.parse(value)) ? value : undefined;
35
+ }
36
+
37
+ function nodeTimes(node: RunNode): { startedAt?: string; endedAt?: string } {
38
+ const startedAt = isoTime(node.startedAt);
39
+ const endedAt = isoTime(node.endedAt);
40
+ return { ...(startedAt ? { startedAt } : {}), ...(endedAt ? { endedAt } : {}) };
41
+ }
42
+
43
+ /** The running node has no record yet — show it with its start time when the ledger names it as the path tail. */
44
+ function runningNode(run: RunRecord, recorded: number): Array<{ nodeId: string; ok: null; summary: string; startedAt: string }> {
45
+ const current = run.currentNode;
46
+ const startedAt = isoTime(current?.startedAt);
47
+ if (run.status !== 'running' || !startedAt || typeof current?.nodeId !== 'string' ||
48
+ run.path.length !== recorded + 1 || run.path.at(-1) !== current.nodeId) return [];
49
+ return [{ nodeId: redactSecretText(current.nodeId), ok: null, summary: '', startedAt }];
28
50
  }
29
51
 
30
52
  function recordAt(path: string): RunRecord | null {
@@ -66,6 +88,77 @@ function runVersion(input: unknown): string | null {
66
88
  return typeof version === 'string' && VERSION.test(version) ? version : null;
67
89
  }
68
90
 
91
+ /** Facts away mode prints for a node — only built when asked, so the HTTP shape stays as it was. */
92
+ export interface ReleaseNodeFacts { verdict?: string; introduced?: number; preexisting?: number; waiver?: string }
93
+ export interface ReleaseRunNodeView {
94
+ nodeId: string; ok: boolean | null; summary: string; startedAt?: string; endedAt?: string; facts?: ReleaseNodeFacts;
95
+ }
96
+
97
+ function parsedOutput(output: unknown): Record<string, unknown> | null {
98
+ let value = output;
99
+ if (typeof value === 'string') {
100
+ const text = value;
101
+ try { value = JSON.parse(text) as unknown; }
102
+ catch {
103
+ const last = text.split('\n').map((line) => line.trim()).filter(Boolean).at(-1);
104
+ try { value = JSON.parse(last ?? '') as unknown; } catch { return null; }
105
+ }
106
+ }
107
+ return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null;
108
+ }
109
+
110
+ export function releaseNodeFacts(output: unknown): ReleaseNodeFacts {
111
+ const value = parsedOutput(output);
112
+ if (!value) return {};
113
+ const verdict = typeof value.verdict === 'string' ? value.verdict : typeof value.outcome === 'string' ? value.outcome : undefined;
114
+ const introduced = Array.isArray(value.introduced) ? value.introduced.length : typeof value.introduced === 'number' ? value.introduced : undefined;
115
+ const preexisting = typeof value.preexisting === 'number' ? value.preexisting : undefined;
116
+ const waiver = typeof value.waiver === 'string' && value.waiver.trim() ? redactSecretText(value.waiver).slice(0, 200) : undefined;
117
+ return {
118
+ ...(verdict ? { verdict: redactSecretText(verdict).slice(0, 40) } : {}),
119
+ ...(introduced !== undefined ? { introduced } : {}),
120
+ ...(preexisting !== undefined ? { preexisting } : {}),
121
+ ...(waiver ? { waiver } : {}),
122
+ };
123
+ }
124
+ export interface ReleaseRunView {
125
+ runId: string; status: string; startedAt: string; version: string | null; path: string[]; nodes: ReleaseRunNodeView[];
126
+ }
127
+
128
+ /** The release run ledger as `/v1/ops/release/runs` serves it (newest 20) — shared with Telegram /release and away mode
129
+ * so every surface reads the same verdicts. `'unavailable'` = the directory exists but cannot be read. */
130
+ export function readReleaseRuns(version: string | null = null,
131
+ dir: string = join(effectiveInstanceRoot(), 'graph-runs', 'release-loop'),
132
+ opts: { facts?: boolean } = {}): ReleaseRunView[] | 'unavailable' {
133
+ let filenames: string[];
134
+ try {
135
+ filenames = readdirSync(dir, { withFileTypes: true })
136
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.json')
137
+ && IDENTIFIER.test(entry.name.slice(0, -5)))
138
+ .map((entry) => entry.name);
139
+ }
140
+ catch (error) {
141
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return [];
142
+ return 'unavailable';
143
+ }
144
+ return filenames.flatMap((filename) => {
145
+ const run = recordAt(join(dir, filename));
146
+ if (!run) return [];
147
+ const v = runVersion(run.input);
148
+ if (version !== null && v !== version) return [];
149
+ return [{
150
+ runId: redactSecretText(filename.slice(0, -5)), status: redactSecretText(run.status),
151
+ startedAt: redactSecretText(run.startedAt), version: v,
152
+ path: run.path.filter((step): step is string => typeof step === 'string').map(redactSecretText),
153
+ nodes: [...run.nodes.filter((node) => node && typeof node.nodeId === 'string').map((node) => ({
154
+ nodeId: redactSecretText(node.nodeId), ok: typeof node.ok === 'boolean' ? node.ok : null,
155
+ summary: nodeSummary(node.output), ...nodeTimes(node),
156
+ ...(opts.facts ? { facts: releaseNodeFacts(node.output) } : {}),
157
+ })), ...runningNode(run, run.nodes.length)],
158
+ }];
159
+ }).sort((a, b) => b.startedAt.localeCompare(a.startedAt)).slice(0, 20);
160
+ }
161
+
69
162
  /** The sole data gate for every read-only /v1/ops endpoint. */
70
163
  export function handleOpsApi(req: Request, metaApi: MetaApiOpts | undefined): Response | Promise<Response> {
71
164
  const url = new URL(req.url);
@@ -103,33 +196,8 @@ export function handleOpsApi(req: Request, metaApi: MetaApiOpts | undefined): Re
103
196
  }
104
197
  if (pathname === '/v1/ops/release/runs') {
105
198
  if (version !== null && !VERSION.test(version)) return jsonResponse({ error: 'invalid-version' }, 400);
106
- const dir = join(effectiveInstanceRoot(), 'graph-runs', 'release-loop');
107
- let filenames: string[];
108
- try {
109
- filenames = readdirSync(dir, { withFileTypes: true })
110
- .filter((entry) => entry.isFile() && entry.name.endsWith('.json')
111
- && IDENTIFIER.test(entry.name.slice(0, -5)))
112
- .map((entry) => entry.name);
113
- }
114
- catch (error) {
115
- if ((error as NodeJS.ErrnoException).code === 'ENOENT') return served(jsonResponse([]));
116
- return jsonResponse({ error: 'runs-unavailable' }, 500);
117
- }
118
- const runs = filenames.flatMap((filename) => {
119
- const run = recordAt(join(dir, filename));
120
- if (!run) return [];
121
- const v = runVersion(run.input);
122
- if (version !== null && v !== version) return [];
123
- return [{
124
- runId: redactSecretText(filename.slice(0, -5)), status: redactSecretText(run.status),
125
- startedAt: redactSecretText(run.startedAt), version: v,
126
- path: run.path.filter((step): step is string => typeof step === 'string').map(redactSecretText),
127
- nodes: run.nodes.filter((node) => node && typeof node.nodeId === 'string').map((node) => ({
128
- nodeId: redactSecretText(node.nodeId), ok: typeof node.ok === 'boolean' ? node.ok : null,
129
- summary: nodeSummary(node.output),
130
- })),
131
- }];
132
- }).sort((a, b) => b.startedAt.localeCompare(a.startedAt)).slice(0, 20);
199
+ const runs = readReleaseRuns(version);
200
+ if (runs === 'unavailable') return jsonResponse({ error: 'runs-unavailable' }, 500);
133
201
  return served(jsonResponse(runs));
134
202
  }
135
203
  // WHATWG URL normalizes a literal runs/../nodes/... before routing.
@@ -136,6 +136,7 @@ import {
136
136
  type DailyReflectionSchedulerHandle,
137
137
  } from '../notes/daily-reflection-scheduler.js';
138
138
  import { type ScheduleRunnerHandle } from '../domains/schedule-runner.js';
139
+ import { startWatchDefaultScheduler } from '../domains/watch-default-scheduler.js';
139
140
  import { resolvePwaStaticDir } from './static-dir-resolve.js';
140
141
  import {
141
142
  createHitlPendingCallbacks,
@@ -1310,6 +1311,7 @@ export async function runNexus(opts: RunNexusOptions = {}): Promise<RunNexusHand
1310
1311
  // the daily-reflection scheduler regardless of which branch booted
1311
1312
  // it. Tests with detachForTesting set leave this undefined.
1312
1313
  let dailyReflectionScheduler: DailyReflectionSchedulerHandle | undefined;
1314
+ let watchDefaultScheduler: ReturnType<typeof startWatchDefaultScheduler> | undefined;
1313
1315
  // RFC #2161 FU A8 (2026-05-11) — discovery cron via NEXUS. Dormant
1314
1316
  // by default (opt-in via `ELANOUS_DISCOVERY_CRON_INTERVAL_MS`); when
1315
1317
  // configured, fires runDiscovery every interval and pushes the
@@ -3198,6 +3200,8 @@ export async function runNexus(opts: RunNexusOptions = {}): Promise<RunNexusHand
3198
3200
  // schedule migrate 로 이관.) 롤백: 아래 한 줄 복원.
3199
3201
  // try { scheduleRunner = startScheduleRunner(); } catch { /* fail-soft */ }
3200
3202
  void scheduleRunner; // 은퇴(undefined 유지)
3203
+ try { watchDefaultScheduler = startWatchDefaultScheduler(); }
3204
+ catch (error) { debug.log('watch.default', 'failed', { stage: 'boot', reason: error instanceof Error ? error.message : String(error) }); }
3201
3205
  }
3202
3206
  }
3203
3207
 
@@ -3335,6 +3339,9 @@ export async function runNexus(opts: RunNexusOptions = {}): Promise<RunNexusHand
3335
3339
  }
3336
3340
  // S2 (2026-07-07) · stop the schedule runner so its node-cron tasks +
3337
3341
  // reconcile interval don't keep firing/holding the loop past shutdown.
3342
+ if (watchDefaultScheduler) {
3343
+ try { watchDefaultScheduler.stop(); } catch { /* swallow */ }
3344
+ }
3338
3345
  if (scheduleRunner) {
3339
3346
  try { scheduleRunner.stop(); } catch { /* swallow */ }
3340
3347
  }
@@ -40,6 +40,7 @@ export interface ChecklistHistory {
40
40
  dev: string;
41
41
  force?: true;
42
42
  reason?: string;
43
+ generation?: number;
43
44
  }
44
45
  export interface Checklist {
45
46
  version: string;
@@ -101,6 +102,7 @@ export function statusChangesSince(history: readonly ChecklistHistory[], sinceIs
101
102
  }
102
103
 
103
104
  function mutate(v: string, apply: (data: Checklist, otherItems: (id: string) => store.ChecklistCollision[]) => boolean): Checklist {
105
+ // addItem/setItem/claimItem/removeItem/seedFromRoadmap reach the generation-fenced ledger write here.
104
106
  return decodeClaimHistory(store.mutate(v, store.releasedVersion(), devVersion(), apply));
105
107
  }
106
108