elanous 0.2.18 → 0.2.20

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 (350) 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/chunks/{1307-30dff22c9e1f3073.js → 1307-77fad80cf54477c6.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/1612-f60c0c93ec773f81.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/2116-6429c188eb846ab8.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/224-e7be8f762fe74986.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/2489-ff55a35b202c0f94.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/2549-89c518a4c8ed4d00.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/3857-995ea9f5ef33d71e.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/4144-fad095b48a1862ee.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/5755-ea7ed88eb956f47b.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/5977-94ed6f78e9814b2b.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/7748-6e51bfe927019f5c.js +25 -0
  14. package/apps/pwa/out/_next/static/chunks/8401-35e94e9091f5eb8d.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/9247-c1753a059e7bac08.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/a9f2b54d-33c0689b70ed8cf3.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-d92f7ee3994e84c7.js → page-c855c24de7db74fe.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-4733c967b56a4277.js → page-85b316bb4235980b.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/board/{page-d22fd849cc22f104.js → page-9b2b26a33cb2cd42.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-bb742f995c6769e7.js → page-c9955fde67b1bcb5.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/bots/{page-02b073a2ac793b83.js → page-7a852956c74015a3.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/ceo/page-c5a5c621be0e04ae.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/app/chat/{page-8a9b3594bc23289b.js → page-4ba4abe096b6c2a8.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/consult/page-575d4a8b4f7becb2.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/control/{page-308111abadf35670.js → page-f5d56ff5f45ad164.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/decisions/page-2260e9eef0357421.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-5d97b2901e0d1ea6.js → page-7c058069c5e7fa63.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/editor/page-dc5da0b8a2edb8a4.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/exec/{page-8c3d8e63ae2e1c98.js → page-466bf0ee16ad1432.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/field/{page-9b72a07d424701c6.js → page-a23bc71713bcc08f.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/inside/page-ed4a6f799f005d09.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/intake/{page-a572f58cd3ca5864.js → page-9cd565005b93aca6.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/app/layout-55f0f40c6db95f15.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/live/{page-be5ea965a07fbc41.js → page-474cb5888ea0e8f3.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/loops/page-35bfec2e631d3eae.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/market/{page-fa244bd146a00ae8.js → page-9ca21f9ad3208a61.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-44d56d6391965a6a.js → page-f9ccb1b1adbea14d.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/morning/{page-83a700d4c754d802.js → page-ec102c9b8b691e37.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/{not-found-7b3170f374f77b9d.js → not-found-a9c2cfa8dda922a5.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/observatory/page-6da35144878feb6d.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-f6055495e9ed7ea8.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-25204694fa9984a5.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a60e035f151d84d3.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/outputs/page-189c587f36874538.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/page-c56634bde705a65d.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-1467047104141297.js → page-db03ae3589a81c9e.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4a6463b97f0ad94f.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-102310930791197a.js → page-75bffecc9aa1004e.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-a8d2fe0c669e0de4.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/settings/{page-6ed0b73bae8351d8.js → page-31d5b875760bb62d.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-c26fa10aac6c21c4.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/setup/{page-43336055451f6e37.js → page-68793c6e10331d66.js} +1 -1
  53. package/apps/pwa/out/_next/static/chunks/app/share/{page-3cc47dca0922ab9a.js → page-46d5201d2207a511.js} +1 -1
  54. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-a77ee355a2304ff0.js → page-d81c07a7b4f618ae.js} +1 -1
  55. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-b17cd46cf0e9b8cf.js → page-be1c4e8697f22360.js} +1 -1
  56. package/apps/pwa/out/_next/static/chunks/app/term/page-cf0a4b4abead71ea.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/today/page-a4717e2c4bf7a28f.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/trace/{page-7bf54c9bb9274af8.js → page-0fd895fab9b62c1b.js} +1 -1
  59. package/apps/pwa/out/_next/static/chunks/app/vault/page-8b6c1454a8e8f306.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-c6e4364d9de8f717.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/workflows/page-0acc8e198f7b0dfb.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-134da468538baf48.js → page-34882c84f84021c5.js} +1 -1
  63. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-0506a1e0e4bdb2f5.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/{webpack-c2782b330447ffb6.js → webpack-c260571513bf43c2.js} +1 -1
  65. package/apps/pwa/out/_next/static/css/{e99e3cff94ced78a.css → 599369d853c61df7.css} +1 -1
  66. package/apps/pwa/out/_next/static/css/c23d959d3ee15e57.css +1 -0
  67. package/apps/pwa/out/_next/static/css/d3427996da11e480.css +1 -0
  68. package/apps/pwa/out/approvals/index.html +1 -1
  69. package/apps/pwa/out/approvals/index.txt +14 -14
  70. package/apps/pwa/out/autopilot/index.html +1 -1
  71. package/apps/pwa/out/autopilot/index.txt +14 -14
  72. package/apps/pwa/out/board/index.html +1 -1
  73. package/apps/pwa/out/board/index.txt +14 -14
  74. package/apps/pwa/out/botlab/index.html +1 -1
  75. package/apps/pwa/out/botlab/index.txt +14 -14
  76. package/apps/pwa/out/bots/index.html +1 -1
  77. package/apps/pwa/out/bots/index.txt +14 -14
  78. package/apps/pwa/out/ceo/index.html +1 -1
  79. package/apps/pwa/out/ceo/index.txt +14 -14
  80. package/apps/pwa/out/chat/index.html +1 -1
  81. package/apps/pwa/out/chat/index.txt +14 -14
  82. package/apps/pwa/out/consult/index.html +1 -1
  83. package/apps/pwa/out/consult/index.txt +14 -14
  84. package/apps/pwa/out/control/index.html +1 -1
  85. package/apps/pwa/out/control/index.txt +14 -14
  86. package/apps/pwa/out/decisions/index.html +1 -1
  87. package/apps/pwa/out/decisions/index.txt +14 -14
  88. package/apps/pwa/out/design-check/index.html +1 -1
  89. package/apps/pwa/out/design-check/index.txt +14 -14
  90. package/apps/pwa/out/editor/index.html +1 -1
  91. package/apps/pwa/out/editor/index.txt +16 -15
  92. package/apps/pwa/out/exec/index.html +1 -1
  93. package/apps/pwa/out/exec/index.txt +14 -14
  94. package/apps/pwa/out/field/index.html +1 -1
  95. package/apps/pwa/out/field/index.txt +14 -14
  96. package/apps/pwa/out/healthz +1 -1
  97. package/apps/pwa/out/index.html +1 -1
  98. package/apps/pwa/out/index.txt +14 -14
  99. package/apps/pwa/out/inside/index.html +1 -1
  100. package/apps/pwa/out/inside/index.txt +16 -15
  101. package/apps/pwa/out/intake/index.html +1 -1
  102. package/apps/pwa/out/intake/index.txt +14 -14
  103. package/apps/pwa/out/live/index.html +1 -1
  104. package/apps/pwa/out/live/index.txt +14 -14
  105. package/apps/pwa/out/loops/index.html +1 -1
  106. package/apps/pwa/out/loops/index.txt +24 -23
  107. package/apps/pwa/out/market/index.html +1 -1
  108. package/apps/pwa/out/market/index.txt +14 -14
  109. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  110. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  111. package/apps/pwa/out/morning/index.html +1 -1
  112. package/apps/pwa/out/morning/index.txt +14 -14
  113. package/apps/pwa/out/observatory/index.html +1 -1
  114. package/apps/pwa/out/observatory/index.txt +14 -14
  115. package/apps/pwa/out/ops/checklist/index.html +1 -1
  116. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  117. package/apps/pwa/out/ops/release/index.html +1 -1
  118. package/apps/pwa/out/ops/release/index.txt +14 -14
  119. package/apps/pwa/out/ops/seats/index.html +1 -1
  120. package/apps/pwa/out/ops/seats/index.txt +14 -14
  121. package/apps/pwa/out/outputs/index.html +1 -1
  122. package/apps/pwa/out/outputs/index.txt +14 -14
  123. package/apps/pwa/out/reflection/index.html +1 -1
  124. package/apps/pwa/out/reflection/index.txt +15 -15
  125. package/apps/pwa/out/scheduler/index.html +1 -1
  126. package/apps/pwa/out/scheduler/index.txt +14 -14
  127. package/apps/pwa/out/sessions/index.html +1 -1
  128. package/apps/pwa/out/sessions/index.txt +14 -14
  129. package/apps/pwa/out/settings/devices/index.html +1 -1
  130. package/apps/pwa/out/settings/devices/index.txt +14 -14
  131. package/apps/pwa/out/settings/index.html +1 -1
  132. package/apps/pwa/out/settings/index.txt +14 -14
  133. package/apps/pwa/out/setup/done/index.html +1 -1
  134. package/apps/pwa/out/setup/done/index.txt +14 -14
  135. package/apps/pwa/out/setup/index.html +1 -1
  136. package/apps/pwa/out/setup/index.txt +14 -14
  137. package/apps/pwa/out/share/index.html +1 -1
  138. package/apps/pwa/out/share/index.txt +14 -14
  139. package/apps/pwa/out/showroom/index.html +1 -1
  140. package/apps/pwa/out/showroom/index.txt +14 -14
  141. package/apps/pwa/out/tasks/index.html +1 -1
  142. package/apps/pwa/out/tasks/index.txt +14 -14
  143. package/apps/pwa/out/term/index.html +1 -1
  144. package/apps/pwa/out/term/index.txt +14 -14
  145. package/apps/pwa/out/today/index.html +1 -1
  146. package/apps/pwa/out/today/index.txt +14 -14
  147. package/apps/pwa/out/trace/index.html +1 -1
  148. package/apps/pwa/out/trace/index.txt +14 -14
  149. package/apps/pwa/out/vault/index.html +1 -1
  150. package/apps/pwa/out/vault/index.txt +14 -14
  151. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  152. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  153. package/apps/pwa/out/workflows/index.html +1 -1
  154. package/apps/pwa/out/workflows/index.txt +14 -14
  155. package/apps/pwa/out/workspace/index.html +1 -1
  156. package/apps/pwa/out/workspace/index.txt +14 -14
  157. package/apps/pwa/out/worktrees/index.html +1 -1
  158. package/apps/pwa/out/worktrees/index.txt +14 -14
  159. package/graphs/demo/editor-recipes.yaml +22 -0
  160. package/graphs/ops/guardian.yaml +26 -0
  161. package/graphs/ops/recipes.yaml +3 -0
  162. package/graphs/retro/daily.yaml +25 -0
  163. package/graphs/retro/recipes.yaml +24 -0
  164. package/package.json +1 -1
  165. package/scripts/ask-marker-check.ts +3 -1
  166. package/scripts/coord-channel-watch.sh +2 -2
  167. package/scripts/discord-seat-probe.ts +3 -1
  168. package/scripts/land-global-checks.ts +89 -0
  169. package/scripts/lib/pch-regress-map.ts +24 -0
  170. package/scripts/morning-report.ts +18 -4
  171. package/scripts/pch-regress.ts +88 -0
  172. package/scripts/release-loop/gate-diff.ts +18 -0
  173. package/scripts/release-loop/gate-node.ts +628 -65
  174. package/scripts/release-loop/gate-partial.ts +106 -0
  175. package/scripts/release-loop/gate-timing.ts +229 -0
  176. package/scripts/release-loop/preflight.ts +6 -4
  177. package/scripts/release-loop/publish-window.ts +141 -0
  178. package/scripts/release-loop/release-docs.ts +1 -4
  179. package/scripts/release-loop/release-universe.ts +38 -0
  180. package/scripts/release-loop/resume-release.ts +67 -5
  181. package/scripts/release-loop/shard-plan.ts +65 -0
  182. package/scripts/release-loop/unattended-release.ts +12 -2
  183. package/scripts/release-story/draft.ts +5 -1
  184. package/scripts/retro/shadow-compare.ts +71 -0
  185. package/scripts/rhythm/daily.ts +6 -1
  186. package/scripts/video/scene-table.ts +159 -0
  187. package/src/acp/core-turn-bridge.ts +39 -2
  188. package/src/agent-mission/driver.ts +34 -6
  189. package/src/autopilot/build/llm-conflict-merge.ts +132 -7
  190. package/src/autopilot/build/merge-intent.ts +135 -7
  191. package/src/away/presence.ts +40 -0
  192. package/src/away/release-watch.ts +272 -0
  193. package/src/chat/index.ts +2 -1
  194. package/src/chat/short-question.ts +3 -40
  195. package/src/cli/away-cli.ts +68 -0
  196. package/src/cli/config-drift.ts +513 -0
  197. package/src/cli/guardian-cli.ts +49 -0
  198. package/src/cli/intake-cli.ts +13 -7
  199. package/src/cli/log-event-names.ts +5 -0
  200. package/src/cli/pod-cli.ts +5 -1
  201. package/src/cli/pr-cli.ts +20 -0
  202. package/src/cli/pty-drive-cli.ts +14 -3
  203. package/src/cli/release-cli.ts +52 -12
  204. package/src/cli/retro-cli.ts +78 -0
  205. package/src/cli/setup-cli.ts +31 -1
  206. package/src/cli/tasks-cli.ts +193 -14
  207. package/src/context-bus/context-now-public.ts +1 -1
  208. package/src/context-bus/context-now-surfaces.ts +16 -6
  209. package/src/context-bus/context-now.ts +97 -3
  210. package/src/core-turn/short-question.ts +100 -0
  211. package/src/dashboard/dashboard-status-lines.ts +24 -4
  212. package/src/dashboard/index.ts +50 -5
  213. package/src/dashboard/input/slash-executor.ts +6 -2
  214. package/src/dashboard/slash-runtime/dashboard-handlers.ts +68 -40
  215. package/src/dashboard/slash-runtime/decide-slash.ts +60 -0
  216. package/src/dashboard/slash-runtime/help-from-registry.ts +62 -23
  217. package/src/dashboard/slash-runtime/help-groups.ts +44 -0
  218. package/src/dashboard/slash-runtime/model-choices.ts +125 -0
  219. package/src/domains/guardian-tick.ts +138 -0
  220. package/src/domains/outbound-alert.ts +44 -13
  221. package/src/domains/outbound-log-probe.ts +6 -1
  222. package/src/domains/schedule-registry.ts +23 -0
  223. package/src/domains/telegram-kind-route.ts +39 -0
  224. package/src/domains/watch-default-scheduler.ts +95 -0
  225. package/src/esc-abort-gate.ts +44 -0
  226. package/src/graph-runner/graph-run-api.ts +205 -0
  227. package/src/graph-runner/runner.ts +36 -6
  228. package/src/harness/harness-cli-command.ts +420 -51
  229. package/src/harness/harness-queue.ts +315 -25
  230. package/src/harness/harness-salvage-cli.ts +140 -0
  231. package/src/harness/harness-worktree-add.ts +68 -0
  232. package/src/harness/ledger-write-guard.ts +6 -16
  233. package/src/hq/fence-outcomes.ts +63 -0
  234. package/src/hq/fence-wrapper.ts +33 -0
  235. package/src/hq/hq.ts +17 -2
  236. package/src/index.ts +118 -6
  237. package/src/instance/test-write-guard-spy-first.fixture.ts +13 -0
  238. package/src/instance/test-write-guard.ts +200 -0
  239. package/src/intake-plane/digest.ts +39 -15
  240. package/src/intake-plane/harness-target.ts +4 -3
  241. package/src/intake-plane/intake-sources.ts +1 -0
  242. package/src/intake-plane/items.ts +1 -0
  243. package/src/loops/orchestrator/degradation.ts +57 -0
  244. package/src/loops/orchestrator/finish-rate.ts +33 -3
  245. package/src/loops/orchestrator/tick.ts +166 -45
  246. package/src/maturity/feature-maturity.ts +6 -4
  247. package/src/maturity/tui-slash-maturity.ts +1 -0
  248. package/src/mss/logging/log-store.ts +3 -0
  249. package/src/nexus/api/graph-kinds.ts +10 -4
  250. package/src/nexus/api/graphs-api.ts +178 -6
  251. package/src/nexus/api/http-server.ts +54 -8
  252. package/src/nexus/api/loop-edges.ts +292 -17
  253. package/src/nexus/api/ops-api.ts +112 -29
  254. package/src/nexus/api/ops-board-route.ts +48 -0
  255. package/src/nexus/index.ts +7 -0
  256. package/src/onboarding/entry-hints.ts +5 -0
  257. package/src/onboarding.ts +24 -6
  258. package/src/release-loop/checklist.ts +2 -0
  259. package/src/release-loop/feature-store.ts +68 -18
  260. package/src/release-loop/gate-shards.ts +158 -0
  261. package/src/release-loop/light-rc.ts +173 -0
  262. package/src/release-loop/placement.ts +51 -17
  263. package/src/release-loop/release-schedule.ts +6 -2
  264. package/src/release-loop/rubric.ts +57 -1
  265. package/src/retro/daily.ts +372 -0
  266. package/src/seat-loop/op-decide.ts +173 -0
  267. package/src/seat-loop/seat-loop.ts +60 -3
  268. package/src/seat-loop/seat-report.ts +58 -3
  269. package/src/self-dev/ask-launch-flow.ts +5 -4
  270. package/src/self-dev/ask-launch-io.ts +49 -1
  271. package/src/self-dev/ask-path-candidates.ts +40 -2
  272. package/src/self-dev/dev-cli.ts +36 -0
  273. package/src/self-dev/draft-sweep.ts +371 -16
  274. package/src/self-dev/draft-triage-rules.ts +19 -4
  275. package/src/self-dev/orchestrate.ts +1 -0
  276. package/src/self-dev/pr-preview.ts +247 -0
  277. package/src/self-dev/shard-run.ts +23 -7
  278. package/src/self-implement/blocked-draft-policy.ts +101 -0
  279. package/src/self-implement/execution-clarification.ts +11 -0
  280. package/src/self-implement/gate-cli.ts +29 -8
  281. package/src/self-implement/gate-env-retry.ts +83 -0
  282. package/src/self-implement/goal-author-clarification.ts +88 -13
  283. package/src/self-implement/goal-clarification-escalation.ts +2 -0
  284. package/src/self-implement/orchestrator.ts +220 -6
  285. package/src/self-implement/run-stop.ts +123 -0
  286. package/src/self-implement/run-ttl.ts +258 -0
  287. package/src/self-implement/salvage-branches.ts +254 -0
  288. package/src/self-implement/salvage-retention.ts +123 -0
  289. package/src/self-implement/seams.ts +4 -0
  290. package/src/self-implement/self-implement-cli.ts +2 -0
  291. package/src/self-implement/sibling-resync.ts +223 -0
  292. package/src/self-implement/stop-autoheal.ts +204 -0
  293. package/src/task-agent/actions.ts +63 -2
  294. package/src/task-agent/board.ts +132 -0
  295. package/src/task-agent/card-evidence.ts +292 -0
  296. package/src/task-agent/completion-evidence.ts +126 -0
  297. package/src/task-agent/judge.ts +25 -0
  298. package/src/task-agent/live-moves.ts +276 -0
  299. package/src/task-agent/mission.ts +33 -7
  300. package/src/task-agent/shadow.ts +192 -15
  301. package/src/task-agent/task-hand.ts +124 -17
  302. package/src/task-orchestrator/surfaces/pod-command-job.ts +82 -8
  303. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +4 -0
  304. package/src/task-orchestrator/surfaces/pod-install-slots.ts +89 -0
  305. package/src/task-orchestrator/surfaces/pod-lease.ts +233 -21
  306. package/src/task-orchestrator/surfaces/pod-pool.ts +155 -34
  307. package/src/task-orchestrator/surfaces/self-implement-pod.ts +21 -5
  308. package/src/telegram-away-command.ts +38 -0
  309. package/src/telegram-commands.ts +52 -0
  310. package/src/telegram-report-log-probe.ts +21 -0
  311. package/src/telegram-report.ts +27 -3
  312. package/src/testing/real-cli-spawn.ts +106 -0
  313. package/src/tui-client/dashboard-session.ts +1 -0
  314. package/src/user-config.ts +190 -5
  315. package/src/version/packed-revision.json +1 -1
  316. package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +0 -1
  317. package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +0 -1
  318. package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +0 -1
  319. package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +0 -1
  320. package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +0 -1
  321. package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +0 -1
  322. package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +0 -1
  323. package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +0 -25
  324. package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +0 -1
  325. package/apps/pwa/out/_next/static/chunks/a9f2b54d-7865aba0f4d2ee49.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/app/consult/page-a0eb02638b1e6f31.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/app/editor/page-c39554c321576982.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/loops/page-c130b63862437814.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-a3c58f5c72a5f3f9.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/outputs/page-e8e1ecaea7cd71d2.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-2da6f9b8a38c6978.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-44d5f767491692bf.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/app/term/page-61553851aa606146.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/app/today/page-a1f585e154e61c59.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/app/vault/page-5150ef17b5383a57.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +0 -1
  348. package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +0 -1
  349. /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → Bam1YziK2iH9bN4K0utGk}/_buildManifest.js +0 -0
  350. /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → Bam1YziK2iH9bN4K0utGk}/_ssgManifest.js +0 -0
@@ -9,15 +9,60 @@ 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
+ /** launch 간선만 — 그 카드에 묶인 런 원장 runId(ID 정규식). 못 묶으면 생략(PWA 가 10분 뒤 «유령»으로 본다). */
35
+ run?: string;
36
+ }
37
+
38
+ /** 노드 접두 — 조율 루프는 레지스트리 행 `loop:orchestrator` 그대로 쓴다. `queue:` 는 대기열 대체 간선 자리(조각 A 밖). */
39
+ export const TASK_AGENT_NODE = 'agent:task-agent';
40
+ export const ORCHESTRATOR_NODE = 'loop:orchestrator';
41
+
42
+ /** 한 런 원장에서 투영한 사실 — 문면은 feature(연결 비교 전용 · 응답에 싣지 않는다)뿐. */
43
+ export interface RunFact {
44
+ runId: string;
45
+ startAt?: string;
46
+ seat?: string;
47
+ feature?: string;
48
+ goalId?: string;
49
+ mergedAt?: string;
50
+ prs: Array<{ at: string; number: number }>;
51
+ children: string[];
19
52
  }
20
53
 
54
+ /** handed.jsonl 행 투영 — status 가 없으면 handed 로 읽는다(조각 A 주입 호환). claimed·released 는 끊긴 간선 판정에만 쓴다. */
55
+ export interface HandedRow {
56
+ at: string;
57
+ cardId?: string;
58
+ cell?: string;
59
+ status?: 'claimed' | 'handed' | 'released';
60
+ key?: string;
61
+ mode?: 'shadow' | 'live';
62
+ runId?: string;
63
+ }
64
+ export interface PodFinish { at: string; childRunId: string; prNumber: number }
65
+
21
66
  export interface LoopEdgesDeps {
22
67
  listCoord?: typeof listCoordEvents;
23
68
  cardRoot?: string;
@@ -28,6 +73,16 @@ export interface LoopEdgesDeps {
28
73
  listOrchestratorLaunches?: () => Array<{ key: string; runId: string }>;
29
74
  listLoopOwners?: () => Array<{ id: string; owner?: string | null }>;
30
75
  seatIds?: () => string[];
76
+ /** 기본 = `<root>/loop/orchestrator/handed.jsonl` · root = cardRoot ?? effectiveInstanceRoot(). */
77
+ listHanded?: () => HandedRow[];
78
+ /** live claimed 가 handed·released 없이 이만큼 지나면 끊긴 hand 간선(기본 10분). */
79
+ handBrokenAfterMs?: number;
80
+ /** 기본 = `<root>/task-agent-actions.json` 의 tasks(readTaskAgentState). */
81
+ listTaskCards?: () => TaskAgentCard[];
82
+ /** 기본 = ledgerDir(또는 연합 원장)에서 투영. listRunStarts 를 주입하고 이것을 안 주면 [] 다. */
83
+ listRunFacts?: () => RunFact[];
84
+ /** 대체 원천 — logs `self-implement.pod` `job-finished` 의 childRunId·prUrl(PR 번호만). */
85
+ listPodFinishes?: (since: string) => PodFinish[];
31
86
  }
32
87
 
33
88
  const ID = /^[a-zA-Z0-9_-]{1,128}$/;
@@ -100,9 +155,9 @@ function cardEdges(card: TaskCard, since: number, now: number, seats: ReadonlySe
100
155
  return edges;
101
156
  }
102
157
 
103
- function runStarts(since: string, dir: string, owners: ReadonlyMap<string, string>, seats: ReadonlySet<string>): Array<{ runId: string; at: string; seat: string; mergedAt?: string }> {
158
+ function runFacts(dir: string, owners: ReadonlyMap<string, string>, seats: ReadonlySet<string>): RunFact[] {
104
159
  if (!existsSync(dir)) return [];
105
- const result: Array<{ runId: string; at: string; seat: string; mergedAt?: string }> = [];
160
+ const result: RunFact[] = [];
106
161
  let names: string[];
107
162
  try { names = readdirSync(dir); } catch { return []; }
108
163
  for (const name of names) {
@@ -113,20 +168,84 @@ function runStarts(since: string, dir: string, owners: ReadonlyMap<string, strin
113
168
  const runId = name.slice(0, -6);
114
169
  let entries: RunLedgerEntry[] | null;
115
170
  try { entries = loadRunLedger(runId, dir); } catch { continue; }
116
- const start = entries?.find(entry => entry.event === 'start' && entry.runId === runId);
171
+ if (!entries) continue;
172
+ const start = entries.find(entry => entry.event === 'start' && entry.runId === runId);
117
173
  const feature = typeof start?.data.feature === 'string' ? start.data.feature : '';
118
174
  const launchSeat = start?.data.launchSeat ?? start?.data.seat ?? owners.get(feature)
119
175
  ?? 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
- }
176
+ const merged = entries.find(entry => entry.event === 'merged' && entry.data.merged === true && typeof entry.timestamp === 'string');
177
+ const goalId = [start, ...entries].map(entry => entry?.goalId ?? (typeof entry?.data.goalId === 'string' ? entry.data.goalId : undefined))
178
+ .find((value): value is string => typeof value === 'string' && /^[a-f0-9]{16}$/.test(value));
179
+ // pr-opened data = {url, number, branch, …} (TC 실측 10-07) — 번호는 data.number(양의 정수)만 읽는다. 못 읽으면 간선 0.
180
+ const prs = entries.filter(entry => entry.event === 'pr-opened' && typeof entry.timestamp === 'string'
181
+ && Number.isSafeInteger(entry.data.number) && (entry.data.number as number) > 0)
182
+ .map(entry => ({ at: entry.timestamp!, number: entry.data.number as number }));
183
+ // `pod-ledger-incomplete` 첫 줄 원장은 시작이 없어도 버리지 않는다 — pr 근거로만 쓰고 run 단계는 만들지 않는다.
184
+ const children = [...new Set(entries.filter(entry => entry.event === 'pod-child-run' && typeof entry.data.childRunId === 'string'
185
+ && ID.test(entry.data.childRunId) && entry.data.childRunId !== runId).map(entry => entry.data.childRunId as string))];
186
+ result.push({
187
+ runId, prs, children,
188
+ ...(start?.timestamp ? { startAt: start.timestamp } : {}),
189
+ ...(start?.timestamp && seat(launchSeat, seats) ? { seat: launchSeat } : {}),
190
+ ...(start?.timestamp && feature ? { feature } : {}),
191
+ ...(goalId ? { goalId } : {}),
192
+ ...(merged?.timestamp ? { mergedAt: merged.timestamp } : {}),
193
+ });
126
194
  }
127
195
  return result;
128
196
  }
129
197
 
198
+ function runStartsFrom(facts: readonly RunFact[], since: string): Array<{ runId: string; at: string; seat: string; mergedAt?: string }> {
199
+ return facts.filter(fact => fact.startAt !== undefined && fact.seat !== undefined
200
+ && (fact.startAt >= since || (fact.mergedAt !== undefined && fact.mergedAt >= since)))
201
+ .map(fact => ({ runId: fact.runId, at: fact.startAt!, seat: fact.seat!, ...(fact.mergedAt ? { mergedAt: fact.mergedAt } : {}) }));
202
+ }
203
+
204
+ /** handed.jsonl(`tick.ts` ORCH-TA-HAND) — 행의 at·status·key·cardId·cell·mode·runId 만. 넘김은 handed 뿐이고 claimed·released 는 끊긴 간선 판정용이다. */
205
+ function handedRows(root: string): HandedRow[] {
206
+ const path = join(root, 'loop', 'orchestrator', 'handed.jsonl');
207
+ try { if (!existsSync(path) || statSync(path).size > 5 * 1024 * 1024) return []; } catch { return []; }
208
+ let raw: string;
209
+ try { raw = readFileSync(path, 'utf8'); } catch { return []; }
210
+ const rows: HandedRow[] = [];
211
+ for (const line of raw.split('\n')) {
212
+ const row = line.trim() ? parse(line) : null;
213
+ const status = row?.status;
214
+ if ((status !== 'handed' && status !== 'claimed' && status !== 'released') || typeof row!.at !== 'string') continue;
215
+ if (status === 'handed' && typeof row!.cardId !== 'string') continue;
216
+ rows.push({
217
+ at: row!.at as string, status,
218
+ ...(typeof row!.cardId === 'string' ? { cardId: row!.cardId } : {}),
219
+ ...(typeof row!.cell === 'string' ? { cell: row!.cell } : {}),
220
+ ...(typeof row!.key === 'string' ? { key: row!.key } : {}),
221
+ ...(row!.mode === 'shadow' || row!.mode === 'live' ? { mode: row!.mode } : {}),
222
+ ...(typeof row!.runId === 'string' ? { runId: row!.runId } : {}),
223
+ });
224
+ }
225
+ return rows;
226
+ }
227
+
228
+ function taskCards(root: string): TaskAgentCard[] {
229
+ const tasks = object(readTaskAgentState<{ tasks?: unknown }>(join(root, 'task-agent-actions.json')).tasks);
230
+ return tasks ? Object.values(tasks).filter((card): card is TaskAgentCard => object(card) !== null) : [];
231
+ }
232
+
233
+ /** 대체 원천: Pod 런은 도는 동안 원장이 Pod 안에만 있다 — 호스트 logs 의 job-finished 에서 childRunId·PR 번호만 읽는다. */
234
+ function podFinishes(since: string): PodFinish[] {
235
+ const path = logsDbPath();
236
+ if (!existsSync(path)) return [];
237
+ const store = LogStore.openReadOnly(path);
238
+ try {
239
+ const result: PodFinish[] = [];
240
+ for (const row of store.queryAll({ exactCategories: ['self-implement.pod'], events: ['job-finished'], sinceMs: Date.parse(since) })) {
241
+ const data = row.data ? parse(row.data) : null;
242
+ const pr = typeof data?.prUrl === 'string' ? /^https:\/\/github\.com\/[\w.-]+\/[\w.-]+\/pull\/(\d+)$/.exec(data.prUrl) : null;
243
+ if (typeof data?.childRunId === 'string' && pr) result.push({ at: row.ts, childRunId: data.childRunId, prNumber: Number(pr[1]) });
244
+ }
245
+ return result;
246
+ } finally { store.close(); }
247
+ }
248
+
130
249
  /** Project only the key and runId of orchestrator launches, never their card snapshots or prose. */
131
250
  function orchestratorLaunches(root: string): Array<{ key: string; runId: string }> {
132
251
  const dir = join(root, 'loop', 'orchestrator');
@@ -152,17 +271,166 @@ function orchestratorLaunches(root: string): Array<{ key: string; runId: string
152
271
  return launches;
153
272
  }
154
273
 
274
+ type JourneyLink = { from: string; to: string; both: boolean };
275
+ type Journey = { links: JourneyLink[]; pieces: Map<string, string[]> };
276
+
277
+ /** 런 원장 start.feature 는 `boundReadableText(feature, 120)` 로 잘린다(orchestrator.ts RUN_START_FEATURE_MAX_CHARS) —
278
+ * 꼬리 표식 ` [truncated; originalChars=N]`·`[truncated]`·`…`(⊕ 손으로 쓴 `...`)을 떼고 공백을 정규화한다. */
279
+ const FEATURE_TRUNCATION_TAIL = /(?:\s*\[truncated(?:; originalChars=\d+)?\]|…|\.\.\.)$/;
280
+ const FEATURE_PREFIX_MIN = 40;
281
+ function normalizeText(text: string): string { return text.replace(/\s+/g, ' ').trim(); }
282
+ /** 잘린 feature 의 비교 접두 — 표식이 없으면 undefined(완전 일치만). 끝의 짝 잃은 서로게이트도 뗀다(UTF-16 절단). */
283
+ function featurePrefix(feature: string): string | undefined {
284
+ if (!FEATURE_TRUNCATION_TAIL.test(feature)) return undefined;
285
+ const prefix = normalizeText(feature.replace(FEATURE_TRUNCATION_TAIL, '').replace(/[\ud800-\udbff]$/, ''));
286
+ return prefix.length >= FEATURE_PREFIX_MIN ? prefix : undefined;
287
+ }
288
+ /** 카드 text ↔ 런 feature — 완전 일치, 또는 잘린 feature(≥40자)가 카드 text(공백 정규화)의 접두. */
289
+ function featureMatches(feature: string | undefined, text: string): boolean {
290
+ if (!feature || !text) return false;
291
+ if (feature === text) return true;
292
+ const prefix = featurePrefix(feature);
293
+ return prefix !== undefined && normalizeText(text).startsWith(prefix);
294
+ }
295
+
296
+ /** 카드 → 런 연결: 카드 runId(있으면 최우선) → goalId 일치 → feature 일치(완전 또는 잘린 접두) ∧ start ≥ launch.at — 뒤 둘은 «유일할 때만»
297
+ * (같은 feature 에 닿는 발사 카드가 둘이면 잇지 않는다). 자식 런(pod-child-run)은 부모로 닿는다. */
298
+ function linkedRun(card: TaskAgentCard, launchAt: string | undefined, launchedTexts: readonly string[], facts: readonly RunFact[], childIds: ReadonlySet<string>): string | undefined {
299
+ const explicitRun = (card as { runId?: unknown }).runId;
300
+ if (typeof explicitRun === 'string' && ID.test(explicitRun)) return explicitRun;
301
+ const pick = (candidates: RunFact[]): string | undefined => {
302
+ const top = candidates.filter(fact => !childIds.has(fact.runId));
303
+ return top.length === 1 ? top[0]!.runId : candidates.length === 1 ? candidates[0]!.runId : undefined;
304
+ };
305
+ if (typeof card.goalId === 'string' && card.goalId) {
306
+ const byGoal = pick(facts.filter(fact => fact.goalId === card.goalId));
307
+ if (byGoal) return byGoal;
308
+ }
309
+ const launchMs = launchAt === undefined ? NaN : Date.parse(launchAt);
310
+ if (!Number.isFinite(launchMs) || typeof card.text !== 'string' || !card.text) return undefined;
311
+ const runId = pick(facts.filter(fact => featureMatches(fact.feature, card.text) && fact.startAt !== undefined && Date.parse(fact.startAt) >= launchMs));
312
+ if (!runId) return undefined;
313
+ const feature = facts.find(fact => fact.runId === runId)!.feature;
314
+ return launchedTexts.filter(text => featureMatches(feature, text)).length === 1 ? runId : undefined;
315
+ }
316
+
317
+ /** hand 행의 mode — 행의 mode 필드가 우선, 없으면 key 접두(`shadow:orch:` = shadow · `orch:` = live). */
318
+ function handMode(row: HandedRow): 'shadow' | 'live' | undefined {
319
+ if (row.mode === 'shadow' || row.mode === 'live') return row.mode;
320
+ if (typeof row.key !== 'string') return undefined;
321
+ if (row.key.startsWith('shadow:orch:')) return 'shadow';
322
+ if (row.key.startsWith('orch:')) return 'live';
323
+ return undefined;
324
+ }
325
+
326
+ /** 넘김(hand)·발사(launch)·런→PR·green 제안(move) 간선을 더하고, ?ref= 여정에 쓸 id 연결을 돌려준다. 문면은 싣지 않는다. */
327
+ function taskAgentEdges(deps: LoopEdgesDeps, facts: readonly RunFact[], seats: ReadonlySet<string>, since: number, now: number, sinceIso: string, edges: LoopEdge[]): Journey {
328
+ // 주입된 카드 목록(시험)에 뿌리가 없으면 운영 뿌리를 읽지 않는다 — 공개본엔 bunfig preload 격리가 없다.
329
+ const root = deps.cardRoot ?? (deps.listCards ? undefined : effectiveInstanceRoot());
330
+ const journey: Journey = { links: [], pieces: new Map() };
331
+ let handed: HandedRow[] = [];
332
+ try { handed = (deps.listHanded ?? (() => root === undefined ? [] : handedRows(root)))(); } catch { /* An unreadable hand ledger hands nothing. */ }
333
+ const settled = new Set<string>();
334
+ for (const row of handed) if (row.key !== undefined && (row.status ?? 'handed') !== 'claimed') settled.add(row.key);
335
+ const brokenAfter = deps.handBrokenAfterMs ?? 10 * 60 * 1000;
336
+ const brokenKeys = new Set<string>();
337
+ for (const row of handed) {
338
+ const status = row.status ?? 'handed';
339
+ const mode = handMode(row);
340
+ const tick = typeof row.runId === 'string' && ID.test(row.runId) ? { tick: row.runId } : {};
341
+ if (status === 'handed') {
342
+ if (typeof row.cardId !== 'string' || !ID.test(row.cardId) || !inWindow(row.at, since, now)) continue;
343
+ edges.push({ at: row.at, kind: 'hand', from: ORCHESTRATOR_NODE, to: TASK_AGENT_NODE, ref: row.cardId,
344
+ ...(row.cell && ID.test(row.cell) ? { cell: row.cell } : {}), ...(mode ? { mode } : {}), ...tick });
345
+ // 넘김 행의 cardId 는 TASK-AGENT 카드다 — 소원(wish) 카드는 key(`orch:<wish>:<칸>`)에만 있다. 둘을 잇지 않으면
346
+ // `?ref=<wish>` 여정이 넘김에서 끊겨 데모 띠가 1단계 뒤로 «남음»에 멈춘다(10-07 리허설 2 실측 · JOURNEY-HAND-LINK).
347
+ const wish = row.key === undefined ? undefined : /^orch:([^:]+):([^:]+)$/.exec(row.key)?.[1];
348
+ if (wish !== undefined && ID.test(wish) && wish !== row.cardId) journey.links.push({ from: wish, to: row.cardId, both: true });
349
+ continue;
350
+ }
351
+ // live claimed 만(handed·released 없음 · N분 지남) = 발사 실패 — 끊긴 간선 하나. shadow claimed→released 는 간선 0.
352
+ if (status !== 'claimed' || mode !== 'live' || row.key === undefined || settled.has(row.key) || brokenKeys.has(row.key)) continue;
353
+ const claimedMs = Date.parse(row.at);
354
+ if (!Number.isFinite(claimedMs) || now - claimedMs < brokenAfter || !inWindow(row.at, since, now)) continue;
355
+ const parts = /^orch:([^:]+):([^:]+)$/.exec(row.key);
356
+ if (!parts || !ID.test(parts[1]!) || !ID.test(parts[2]!)) continue;
357
+ brokenKeys.add(row.key);
358
+ edges.push({ at: row.at, kind: 'hand', from: ORCHESTRATOR_NODE, to: TASK_AGENT_NODE, ref: parts[1]!, cell: parts[2]!, mode, ...tick, broken: true });
359
+ }
360
+ let cards: TaskAgentCard[] = [];
361
+ try { cards = (deps.listTaskCards ?? (() => root === undefined ? [] : taskCards(root)))().filter(card => typeof card?.id === 'string' && ID.test(card.id)); }
362
+ catch { /* An unreadable task-agent state launches nothing. */ }
363
+ const launchesOf = (card: TaskAgentCard) => (Array.isArray(card.history) ? card.history : [])
364
+ .filter(event => event?.event === 'launch' && typeof event.at === 'string');
365
+ const launchedTexts: string[] = [];
366
+ for (const card of cards) {
367
+ if (launchesOf(card).length && typeof card.text === 'string') launchedTexts.push(card.text);
368
+ }
369
+ const childIds = new Set(facts.flatMap(fact => fact.children));
370
+ for (const card of cards) {
371
+ if (typeof card.mission === 'string' && ID.test(card.mission)) journey.links.push({ from: card.id, to: card.mission, both: false });
372
+ if (Array.isArray(card.pieces)) journey.pieces.set(card.id, card.pieces.filter(piece => typeof piece === 'string' && ID.test(piece)));
373
+ const launches = launchesOf(card);
374
+ const runId = linkedRun(card, launches[0]?.at, launchedTexts, facts, childIds);
375
+ for (const launch of launches) {
376
+ 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, ...(runId ? { run: runId } : {}) });
377
+ }
378
+ if (runId) journey.links.push({ from: card.id, to: runId, both: true });
379
+ const green = object(card.greenProposal);
380
+ if (green && typeof green.at === 'string' && typeof green.checklistId === 'string' && ID.test(green.checklistId) && inWindow(green.at, since, now)) {
381
+ edges.push({ at: green.at, kind: 'move', from: TASK_AGENT_NODE, to: `release:${green.checklistId}`, ref: card.id, move: 'green-proposal' });
382
+ }
383
+ }
384
+ const prSeen = new Set<string>();
385
+ const prEdge = (runId: string, at: string, number: number) => {
386
+ const key = `${runId}:${number}`;
387
+ if (!ID.test(runId) || !Number.isSafeInteger(number) || number < 1 || prSeen.has(key) || !inWindow(at, since, now)) return;
388
+ prSeen.add(key);
389
+ edges.push({ at, kind: 'run', from: `loop:${runId}`, to: `pr:${number}`, ref: runId });
390
+ };
391
+ for (const fact of facts) {
392
+ for (const child of fact.children) journey.links.push({ from: fact.runId, to: child, both: true });
393
+ for (const pr of fact.prs) prEdge(fact.runId, pr.at, pr.number);
394
+ }
395
+ let pods: PodFinish[] = [];
396
+ // 대체 원천은 연합 원장(운영 기본)과 짝이다 — 원장을 주입했으면(시험·한 디렉터리) logs 를 읽지 않는다.
397
+ const injectedLedger = deps.ledgerDir !== undefined || deps.listRunStarts !== undefined || deps.listRunFacts !== undefined;
398
+ try { pods = (deps.listPodFinishes ?? (injectedLedger ? () => [] : podFinishes))(sinceIso); } catch { /* logs outage — the run ledger stays the primary source. */ }
399
+ for (const pod of pods) prEdge(pod.childRunId, pod.at, pod.prNumber);
400
+ return journey;
401
+ }
402
+
403
+ /** ?ref= 의 여정 id 집합 — 카드↔런·런↔자식 런은 양방향, 조각→미션은 위로만(형제 조각은 미션을 ref 로 줄 때만). */
404
+ function journeyIds(ref: string, journey: Journey): Set<string> {
405
+ const ids = new Set([ref, ...(journey.pieces.get(ref) ?? [])]);
406
+ const queue = [...ids];
407
+ while (queue.length) {
408
+ const id = queue.shift()!;
409
+ for (const link of journey.links) {
410
+ const next = link.from === id ? link.to : link.both && link.to === id ? link.from : undefined;
411
+ if (next !== undefined && !ids.has(next)) { ids.add(next); queue.push(next); }
412
+ }
413
+ }
414
+ return ids;
415
+ }
416
+
155
417
  /** GET only; project an allowlist of fields from the ledgers. Never serialize ledger records. */
156
418
  export function handleLoopEdgesGet(req: Request, deps: LoopEdgesDeps = {}): Response {
157
419
  const params = new URL(req.url).searchParams;
158
420
  const rawSince = params.get('since');
159
- const since = rawSince === null ? Date.now() - 60 * 60 * 1000 : Date.parse(rawSince);
421
+ const journeyRef = params.get('ref');
422
+ // ?mode=live|shadow — hand 간선만 그 mode 로 거른다(다른 종류 간선은 그대로).
423
+ const handModeFilter = params.get('mode');
424
+ // ?ref= 는 한 카드·런의 여정 — 60분 창과 limit 에 잘리지 않게 since 를 안 주면 처음부터 본다.
425
+ const since = rawSince === null ? (journeyRef === null ? Date.now() - 60 * 60 * 1000 : 0) : Date.parse(rawSince);
160
426
  const rawLimit = params.get('limit');
161
427
  const limit = rawLimit === null ? 100 : Number(rawLimit);
162
428
  const now = Date.now();
163
429
  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);
430
+ || !Number.isFinite(since) || since > now || !Number.isSafeInteger(limit) || limit < 1 || limit > 500
431
+ || (journeyRef !== null && !ID.test(journeyRef))
432
+ || (handModeFilter !== null && handModeFilter !== 'live' && handModeFilter !== 'shadow')) {
433
+ return jsonResponse({ error: 'usage: GET /v1/loops/edges?since=<ISO>&limit=<1..500>&ref=<card|run|mission id>' }, 400);
166
434
  }
167
435
  const sinceIso = new Date(since).toISOString();
168
436
  const seats = new Set((deps.seatIds ?? (() => [...SEATS.map(s => s.seat), ...listSubSeatCharters().map(s => s.id)]))());
@@ -189,7 +457,7 @@ export function handleLoopEdgesGet(req: Request, deps: LoopEdgesDeps = {}): Resp
189
457
  finally { store.close(); }
190
458
  }
191
459
  }
192
- const readStarts = deps.listRunStarts ?? ((from: string) => {
460
+ const readFacts = deps.listRunFacts ?? (deps.listRunStarts ? () => [] : () => {
193
461
  const owners = new Map<string, string>();
194
462
  try {
195
463
  for (const entry of (deps.listLoopOwners ?? (() => listLoops()))()) {
@@ -197,8 +465,11 @@ export function handleLoopEdgesGet(req: Request, deps: LoopEdgesDeps = {}): Resp
197
465
  }
198
466
  } catch { /* A registry outage does not erase explicit launch-seat evidence. */ }
199
467
  return (deps.ledgerDir ? [deps.ledgerDir] : resolveFederatedRunLedgerDirectories({ includeTest: false }))
200
- .flatMap(dir => runStarts(from, dir, owners, seats));
468
+ .flatMap(dir => runFacts(dir, owners, seats));
201
469
  });
470
+ let facts: RunFact[] = [];
471
+ try { facts = readFacts().filter(fact => ID.test(fact.runId)); } catch { /* An unreadable ledger set links nothing. */ }
472
+ const readStarts = deps.listRunStarts ?? ((from: string) => runStartsFrom(facts, from));
202
473
  const runs = readStarts(sinceIso).filter(run => seat(run.seat, seats) && ID.test(run.runId)
203
474
  && (inWindow(run.at, since, now) || (run.mergedAt !== undefined && inWindow(run.mergedAt, since, now))));
204
475
  for (const run of runs) {
@@ -224,6 +495,10 @@ export function handleLoopEdgesGet(req: Request, deps: LoopEdgesDeps = {}): Resp
224
495
  }
225
496
  }
226
497
  }
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) });
498
+ const links = taskAgentEdges(deps, facts, seats, since, now, sinceIso, edges);
499
+ const journey = journeyRef === null ? null : journeyIds(journeyRef, links);
500
+ const out = edges.filter(edge => (!journey || journey.has(edge.ref))
501
+ && (handModeFilter === null || edge.kind !== 'hand' || edge.mode === handModeFilter));
502
+ out.sort((a, b) => b.at.localeCompare(a.at) || a.kind.localeCompare(b.kind) || a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
503
+ return jsonResponse({ edges: out.slice(0, limit) });
229
504
  }
@@ -1,7 +1,8 @@
1
1
  import { lstatSync, readFileSync, readdirSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { debug, redactSecretText } from '../../debug/log.js';
4
- import { effectiveInstanceRoot } from '../../instance/resolve.js';
4
+ import { effectiveInstanceRoot, releaseLedgerRoot } from '../../instance/resolve.js';
5
+ import { gateShardsPath, readGateShards, summarizeShards, type GateShardsFile, type GateShardsSummary } from '../../release-loop/gate-shards.js';
5
6
  import { listChecklist } from '../../release-loop/checklist.js';
6
7
  import { getSchedule } from '../../release-loop/release-schedule.js';
7
8
  import { jsonResponse } from './json-response.js';
@@ -18,13 +19,35 @@ let seatsCache: ReturnType<typeof createSeatsCache> | null = null;
18
19
  export function setSeatsCacheForTest(cache: ReturnType<typeof createSeatsCache> | null): void { seatsCache = cache; }
19
20
  const LOG_PATH = /^\/v1\/ops\/release\/runs\/([^/]+)\/nodes\/([^/]+)\/log$/;
20
21
 
21
- interface RunNode { nodeId: string; ok?: unknown; output?: unknown }
22
+ interface RunNode { nodeId: string; ok?: unknown; output?: unknown; startedAt?: unknown; endedAt?: unknown }
22
23
  interface RunRecord {
23
24
  status: string;
24
25
  path: string[];
25
26
  startedAt: string;
26
27
  input?: unknown;
27
28
  nodes: RunNode[];
29
+ currentNode?: { nodeId?: unknown; startedAt?: unknown };
30
+ }
31
+
32
+ /** Node times come only from the ledger (GRAPH-NODE-TIMES); older ledgers have none and the screen says so. */
33
+ const ISO_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
34
+ function isoTime(value: unknown): string | undefined {
35
+ return typeof value === 'string' && ISO_TIME.test(value) && Number.isFinite(Date.parse(value)) ? value : undefined;
36
+ }
37
+
38
+ function nodeTimes(node: RunNode): { startedAt?: string; endedAt?: string } {
39
+ const startedAt = isoTime(node.startedAt);
40
+ const endedAt = isoTime(node.endedAt);
41
+ return { ...(startedAt ? { startedAt } : {}), ...(endedAt ? { endedAt } : {}) };
42
+ }
43
+
44
+ /** The running node has no record yet — show it with its start time when the ledger names it as the path tail. */
45
+ function runningNode(run: RunRecord, recorded: number): Array<{ nodeId: string; ok: null; summary: string; startedAt: string }> {
46
+ const current = run.currentNode;
47
+ const startedAt = isoTime(current?.startedAt);
48
+ if (run.status !== 'running' || !startedAt || typeof current?.nodeId !== 'string' ||
49
+ run.path.length !== recorded + 1 || run.path.at(-1) !== current.nodeId) return [];
50
+ return [{ nodeId: redactSecretText(current.nodeId), ok: null, summary: '', startedAt }];
28
51
  }
29
52
 
30
53
  function recordAt(path: string): RunRecord | null {
@@ -66,6 +89,91 @@ function runVersion(input: unknown): string | null {
66
89
  return typeof version === 'string' && VERSION.test(version) ? version : null;
67
90
  }
68
91
 
92
+ /** Facts away mode prints for a node — only built when asked, so the HTTP shape stays as it was. */
93
+ export interface ReleaseNodeFacts { verdict?: string; introduced?: number; preexisting?: number; waiver?: string }
94
+ export interface ReleaseRunNodeView {
95
+ nodeId: string; ok: boolean | null; summary: string; startedAt?: string; endedAt?: string; facts?: ReleaseNodeFacts;
96
+ }
97
+
98
+ function parsedOutput(output: unknown): Record<string, unknown> | null {
99
+ let value = output;
100
+ if (typeof value === 'string') {
101
+ const text = value;
102
+ try { value = JSON.parse(text) as unknown; }
103
+ catch {
104
+ const last = text.split('\n').map((line) => line.trim()).filter(Boolean).at(-1);
105
+ try { value = JSON.parse(last ?? '') as unknown; } catch { return null; }
106
+ }
107
+ }
108
+ return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null;
109
+ }
110
+
111
+ export function releaseNodeFacts(output: unknown): ReleaseNodeFacts {
112
+ const value = parsedOutput(output);
113
+ if (!value) return {};
114
+ const verdict = typeof value.verdict === 'string' ? value.verdict : typeof value.outcome === 'string' ? value.outcome : undefined;
115
+ const introduced = Array.isArray(value.introduced) ? value.introduced.length : typeof value.introduced === 'number' ? value.introduced : undefined;
116
+ const preexisting = typeof value.preexisting === 'number' ? value.preexisting : undefined;
117
+ const waiver = typeof value.waiver === 'string' && value.waiver.trim() ? redactSecretText(value.waiver).slice(0, 200) : undefined;
118
+ return {
119
+ ...(verdict ? { verdict: redactSecretText(verdict).slice(0, 40) } : {}),
120
+ ...(introduced !== undefined ? { introduced } : {}),
121
+ ...(preexisting !== undefined ? { preexisting } : {}),
122
+ ...(waiver ? { waiver } : {}),
123
+ };
124
+ }
125
+ export interface ReleaseRunView {
126
+ runId: string; status: string; startedAt: string; version: string | null; path: string[]; nodes: ReleaseRunNodeView[];
127
+ /** GATE-LIVE-OBS — 지금 gate 노드에 있는 런에만 싣는다(발행 원장 `gate-logs/cut/shards.json`). 파일이 없으면 생략. */
128
+ gateShards?: GateShardsFile & { summary: GateShardsSummary };
129
+ }
130
+
131
+ /** 도는 gate 노드의 조각 표 — 다른 판의 파일을 붙이지 않게 version 이 같을 때만. */
132
+ function gateShardsFor(version: string | null, status: string, nodes: ReleaseRunNodeView[], ledgerRoot: string, now: number):
133
+ ReleaseRunView['gateShards'] | undefined {
134
+ if (!version || status !== 'running' || !nodes.some((node) => node.nodeId === 'gate' && node.ok === null)) return undefined;
135
+ const file = readGateShards(gateShardsPath(version, ledgerRoot));
136
+ if (!file || file.version !== version) return undefined;
137
+ return { ...file, shards: file.shards.map((shard) => shard.waitReason ? { ...shard, waitReason: redactSecretText(shard.waitReason) } : shard),
138
+ summary: summarizeShards(file, now) };
139
+ }
140
+
141
+ /** The release run ledger as `/v1/ops/release/runs` serves it (newest 20) — shared with Telegram /release and away mode
142
+ * so every surface reads the same verdicts. `'unavailable'` = the directory exists but cannot be read. */
143
+ export function readReleaseRuns(version: string | null = null,
144
+ dir: string = join(effectiveInstanceRoot(), 'graph-runs', 'release-loop'),
145
+ opts: { facts?: boolean; ledgerRoot?: string; now?: number } = {}): ReleaseRunView[] | 'unavailable' {
146
+ let filenames: string[];
147
+ try {
148
+ filenames = readdirSync(dir, { withFileTypes: true })
149
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.json')
150
+ && IDENTIFIER.test(entry.name.slice(0, -5)))
151
+ .map((entry) => entry.name);
152
+ }
153
+ catch (error) {
154
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return [];
155
+ return 'unavailable';
156
+ }
157
+ return filenames.flatMap((filename) => {
158
+ const run = recordAt(join(dir, filename));
159
+ if (!run) return [];
160
+ const v = runVersion(run.input);
161
+ if (version !== null && v !== version) return [];
162
+ const nodes: ReleaseRunNodeView[] = [...run.nodes.filter((node) => node && typeof node.nodeId === 'string').map((node) => ({
163
+ nodeId: redactSecretText(node.nodeId), ok: typeof node.ok === 'boolean' ? node.ok : null,
164
+ summary: nodeSummary(node.output), ...nodeTimes(node),
165
+ ...(opts.facts ? { facts: releaseNodeFacts(node.output) } : {}),
166
+ })), ...runningNode(run, run.nodes.length)];
167
+ const gateShards = gateShardsFor(v, run.status, nodes, opts.ledgerRoot ?? releaseLedgerRoot(), opts.now ?? Date.now());
168
+ return [{
169
+ runId: redactSecretText(filename.slice(0, -5)), status: redactSecretText(run.status),
170
+ startedAt: redactSecretText(run.startedAt), version: v,
171
+ path: run.path.filter((step): step is string => typeof step === 'string').map(redactSecretText),
172
+ nodes, ...(gateShards ? { gateShards } : {}),
173
+ }];
174
+ }).sort((a, b) => b.startedAt.localeCompare(a.startedAt)).slice(0, 20);
175
+ }
176
+
69
177
  /** The sole data gate for every read-only /v1/ops endpoint. */
70
178
  export function handleOpsApi(req: Request, metaApi: MetaApiOpts | undefined): Response | Promise<Response> {
71
179
  const url = new URL(req.url);
@@ -103,33 +211,8 @@ export function handleOpsApi(req: Request, metaApi: MetaApiOpts | undefined): Re
103
211
  }
104
212
  if (pathname === '/v1/ops/release/runs') {
105
213
  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);
214
+ const runs = readReleaseRuns(version);
215
+ if (runs === 'unavailable') return jsonResponse({ error: 'runs-unavailable' }, 500);
133
216
  return served(jsonResponse(runs));
134
217
  }
135
218
  // WHATWG URL normalizes a literal runs/../nodes/... before routing.
@@ -0,0 +1,48 @@
1
+ import { canonicalSeatId, openMsgStore, type Message, type MsgStore } from '../../msg/msg-store.js';
2
+ import { jsonResponse } from './json-response.js';
3
+
4
+ function ownerNotices(store: Pick<MsgStore, 'list'>): Message[] {
5
+ const notices: Message[] = [];
6
+ let after = 0;
7
+ let batch: Message[];
8
+ do {
9
+ batch = store.list('CEO', after, 1000);
10
+ notices.push(...batch.filter(message => message.from !== 'CEO'));
11
+ if (batch.length) after = batch[batch.length - 1]!.id;
12
+ } while (batch.length === 1000);
13
+ return notices;
14
+ }
15
+
16
+ export const OPS_BOARD_PATH = '/v1/ops-board/notices';
17
+
18
+ export interface OpsBoardDeps {
19
+ authorize: (request: Request) => boolean;
20
+ openStore?: () => Pick<MsgStore, 'list' | 'post' | 'close'>;
21
+ }
22
+
23
+ /** Owner reads the CEO inbox without acknowledging it; replies are addressed to the original sender. */
24
+ export async function handleOpsBoard(req: Request, deps: OpsBoardDeps): Promise<Response> {
25
+ if (!deps.authorize(req)) return jsonResponse({ error: 'unauthorized' }, 401);
26
+ if (req.method !== 'GET' && req.method !== 'POST') return jsonResponse({ error: 'method-not-allowed' }, 405);
27
+ const store = (deps.openStore ?? openMsgStore)();
28
+ try {
29
+ if (req.method === 'GET') {
30
+ const notices = ownerNotices(store);
31
+ return jsonResponse({ notices });
32
+ }
33
+ let body: unknown;
34
+ try { body = await req.json(); } catch { return jsonResponse({ error: 'bad_request' }, 400); }
35
+ if (!body || typeof body !== 'object' || Array.isArray(body)) return jsonResponse({ error: 'bad_request' }, 400);
36
+ const input = body as Record<string, unknown>;
37
+ if (!Number.isSafeInteger(input.noticeId) || (input.noticeId as number) < 1
38
+ || typeof input.body !== 'string' || !input.body.trim() || input.body.length > 3000
39
+ || Object.keys(input).some(key => key !== 'noticeId' && key !== 'body')) return jsonResponse({ error: 'bad_request' }, 400);
40
+ const notice: Message | undefined = ownerNotices(store).find(message => message.id === input.noticeId);
41
+ if (!notice) return jsonResponse({ error: 'not-found' }, 404);
42
+ const recipient = canonicalSeatId(notice.from);
43
+ const reply = store.post({ from: 'CEO', to: recipient, body: input.body.trim(), kind: 'reply' });
44
+ return jsonResponse({ reply }, 201);
45
+ } finally {
46
+ store.close();
47
+ }
48
+ }
@@ -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
  }