elanous 0.2.25 → 0.2.28

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 (337) 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/{1454-37fd16f479f5554a.js → 1454-29d1d773d5996a49.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{599-a914cc0468f56143.js → 599-56f2c0a5cc4a7295.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/app/approvals/page-5ab1e47978577c5e.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-24453f6a302816b4.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/app/board/page-6c131d30bb1cf871.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-b12149c51f7d04b1.js → page-fa46d4588e8442c2.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/app/bots/page-dc0772b11861af51.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/app/ceo/page-66ad46ed0279feb9.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/chat/page-5159dc8752c6cd29.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/app/consult/page-c5a2478861c08b87.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/control/page-f722a9dec084e08b.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/app/decisions/{page-305cd294e754906e.js → page-256ffd365d32e100.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-dea4b795741126fd.js → page-e0de78ae7b99f01c.js} +1 -1
  16. package/apps/pwa/out/_next/static/chunks/app/editor/page-cfbdc44b708d9801.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/exec/{page-45889e7070d11126.js → page-4c741eb760f2f77c.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/field/{page-16a2e893f83ae4d0.js → page-e9ba98f5f1440a21.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/inside/page-dafc901bb1b72b5f.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/app/intake/{page-56888bc744e08dac.js → page-2f6f303fa114ec24.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/layout-1634081c5272006c.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/live/{page-952c6ceec6d9de38.js → page-ced0bcfc6542b3fc.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/live-run/{page-6d0202af568f4c63.js → page-bb1f05cd51c38944.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/loops/page-646b50f63b6fe732.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/market/page-db069b48a59c8c00.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-4fb6fc34e20c07b7.js → page-f736324afa2117c1.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/morning/{page-e9502921f4f01b1f.js → page-6f131b51193a36f9.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/{not-found-b6169ea2f09cf287.js → not-found-435f5e2faf02bc01.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/observatory/{page-6a14333d1b1b3967.js → page-497e0eedf20e7890.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-8c91a9a66c9ec90a.js → page-a8d28fdf995e16ea.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8966452228753c5e.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fd5d18656cb432b6.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/outputs/page-5663779cb1a37609.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/{page-3d3fc73dd187ed52.js → page-66a67eb19ebd976b.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/reflection/page-3b492aa78cf3c4a6.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-9c04dac0831c2bbb.js → page-c71267e7681da982.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/sessions/page-a21d1e1797172692.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-04de9ca6d52a3893.js → page-cb249e81ed339318.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/settings/{page-62527677bbc2fdc4.js → page-781cfc5114e097da.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-9a2e09c4a311764c.js → page-478157214db6a023.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/setup/{page-bfeb35d69e8b24bb.js → page-86141f12cf090b7f.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/share/{page-89cb33bced824e4a.js → page-7256c73bb1c36751.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/app/showroom/page-5c738ca7adef3de5.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/tasks/page-0138c8091ec17224.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/term/page-cc2f24f16d85e735.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/today/{page-6445ac4cbb8dd869.js → page-979d318a534e92da.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/trace/page-182d394f7e6e096b.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/vault/page-21050928a697ed7a.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-44b9d807005a9030.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/{page-20162174b77f96cf.js → page-601fb77340e63c9a.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/workspace/page-75d3fb5d47af0edf.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-05a4b2c9b91034aa.js → page-11bf9e5725a72c22.js} +1 -1
  53. package/apps/pwa/out/approvals/index.html +1 -1
  54. package/apps/pwa/out/approvals/index.txt +13 -13
  55. package/apps/pwa/out/autopilot/index.html +1 -1
  56. package/apps/pwa/out/autopilot/index.txt +13 -13
  57. package/apps/pwa/out/board/index.html +1 -1
  58. package/apps/pwa/out/board/index.txt +13 -13
  59. package/apps/pwa/out/botlab/index.html +1 -1
  60. package/apps/pwa/out/botlab/index.txt +13 -13
  61. package/apps/pwa/out/bots/index.html +1 -1
  62. package/apps/pwa/out/bots/index.txt +13 -13
  63. package/apps/pwa/out/ceo/index.html +1 -1
  64. package/apps/pwa/out/ceo/index.txt +13 -13
  65. package/apps/pwa/out/chat/index.html +1 -1
  66. package/apps/pwa/out/chat/index.txt +13 -13
  67. package/apps/pwa/out/consult/index.html +1 -1
  68. package/apps/pwa/out/consult/index.txt +13 -13
  69. package/apps/pwa/out/control/index.html +1 -1
  70. package/apps/pwa/out/control/index.txt +13 -13
  71. package/apps/pwa/out/decisions/index.html +1 -1
  72. package/apps/pwa/out/decisions/index.txt +13 -13
  73. package/apps/pwa/out/design-check/index.html +1 -1
  74. package/apps/pwa/out/design-check/index.txt +13 -13
  75. package/apps/pwa/out/editor/index.html +1 -1
  76. package/apps/pwa/out/editor/index.txt +13 -13
  77. package/apps/pwa/out/exec/index.html +1 -1
  78. package/apps/pwa/out/exec/index.txt +13 -13
  79. package/apps/pwa/out/field/index.html +1 -1
  80. package/apps/pwa/out/field/index.txt +13 -13
  81. package/apps/pwa/out/healthz +1 -1
  82. package/apps/pwa/out/index.html +1 -1
  83. package/apps/pwa/out/index.txt +13 -13
  84. package/apps/pwa/out/inside/index.html +1 -1
  85. package/apps/pwa/out/inside/index.txt +13 -13
  86. package/apps/pwa/out/intake/index.html +1 -1
  87. package/apps/pwa/out/intake/index.txt +13 -13
  88. package/apps/pwa/out/live/index.html +1 -1
  89. package/apps/pwa/out/live/index.txt +13 -13
  90. package/apps/pwa/out/live-run/index.html +1 -1
  91. package/apps/pwa/out/live-run/index.txt +13 -13
  92. package/apps/pwa/out/loops/index.html +1 -1
  93. package/apps/pwa/out/loops/index.txt +13 -13
  94. package/apps/pwa/out/market/index.html +1 -1
  95. package/apps/pwa/out/market/index.txt +13 -13
  96. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  97. package/apps/pwa/out/missions/placeholder/index.txt +13 -13
  98. package/apps/pwa/out/morning/index.html +1 -1
  99. package/apps/pwa/out/morning/index.txt +13 -13
  100. package/apps/pwa/out/observatory/index.html +1 -1
  101. package/apps/pwa/out/observatory/index.txt +13 -13
  102. package/apps/pwa/out/ops/checklist/index.html +1 -1
  103. package/apps/pwa/out/ops/checklist/index.txt +13 -13
  104. package/apps/pwa/out/ops/release/index.html +1 -1
  105. package/apps/pwa/out/ops/release/index.txt +13 -13
  106. package/apps/pwa/out/ops/seats/index.html +1 -1
  107. package/apps/pwa/out/ops/seats/index.txt +13 -13
  108. package/apps/pwa/out/outputs/index.html +1 -1
  109. package/apps/pwa/out/outputs/index.txt +13 -13
  110. package/apps/pwa/out/reflection/index.html +1 -1
  111. package/apps/pwa/out/reflection/index.txt +14 -14
  112. package/apps/pwa/out/scheduler/index.html +1 -1
  113. package/apps/pwa/out/scheduler/index.txt +13 -13
  114. package/apps/pwa/out/sessions/index.html +1 -1
  115. package/apps/pwa/out/sessions/index.txt +13 -13
  116. package/apps/pwa/out/settings/devices/index.html +1 -1
  117. package/apps/pwa/out/settings/devices/index.txt +13 -13
  118. package/apps/pwa/out/settings/index.html +1 -1
  119. package/apps/pwa/out/settings/index.txt +13 -13
  120. package/apps/pwa/out/setup/done/index.html +1 -1
  121. package/apps/pwa/out/setup/done/index.txt +13 -13
  122. package/apps/pwa/out/setup/index.html +1 -1
  123. package/apps/pwa/out/setup/index.txt +13 -13
  124. package/apps/pwa/out/share/index.html +1 -1
  125. package/apps/pwa/out/share/index.txt +13 -13
  126. package/apps/pwa/out/showroom/index.html +1 -1
  127. package/apps/pwa/out/showroom/index.txt +13 -13
  128. package/apps/pwa/out/tasks/index.html +1 -1
  129. package/apps/pwa/out/tasks/index.txt +13 -13
  130. package/apps/pwa/out/term/index.html +1 -1
  131. package/apps/pwa/out/term/index.txt +13 -13
  132. package/apps/pwa/out/today/index.html +1 -1
  133. package/apps/pwa/out/today/index.txt +13 -13
  134. package/apps/pwa/out/trace/index.html +1 -1
  135. package/apps/pwa/out/trace/index.txt +13 -13
  136. package/apps/pwa/out/vault/index.html +1 -1
  137. package/apps/pwa/out/vault/index.txt +13 -13
  138. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  139. package/apps/pwa/out/workflows/chat-ui/index.txt +13 -13
  140. package/apps/pwa/out/workflows/index.html +1 -1
  141. package/apps/pwa/out/workflows/index.txt +13 -13
  142. package/apps/pwa/out/workspace/index.html +1 -1
  143. package/apps/pwa/out/workspace/index.txt +13 -13
  144. package/apps/pwa/out/worktrees/index.html +1 -1
  145. package/apps/pwa/out/worktrees/index.txt +13 -13
  146. package/graphs/implement-loop.yaml +38 -1
  147. package/graphs/steward/recipes.yaml +6 -0
  148. package/graphs/steward/release-day.yaml +23 -0
  149. package/graphs/steward/steward.yaml +9 -1
  150. package/graphs/work/code-pr.yaml +23 -0
  151. package/package.json +1 -1
  152. package/scripts/browser-observe-cycle.ts +76 -2
  153. package/scripts/ci-typecheck-changed.ts +119 -7
  154. package/scripts/coord-msg-bridge.ts +209 -0
  155. package/scripts/coord-post.sh +40 -7
  156. package/scripts/coord-requests-bridge.ts +10 -5
  157. package/scripts/detect-test-interference.ts +31 -1
  158. package/scripts/expose-rubric-suggest.ts +1 -1
  159. package/scripts/measure-tsc-affected-roots.ts +10 -1
  160. package/scripts/release-loop/docs-node.ts +51 -22
  161. package/scripts/release-loop/export-check-node.ts +8 -5
  162. package/scripts/release-loop/gate-node.ts +37 -4
  163. package/scripts/release-loop/node-verdict.ts +10 -3
  164. package/scripts/release-loop/publish-node.ts +3 -0
  165. package/scripts/release-loop/tui-node-checks.ts +5 -2
  166. package/scripts/skills/public-check.ts +5 -2
  167. package/scripts/test-deterministic.ts +520 -37
  168. package/scripts/ux-sim.ts +7 -1
  169. package/scripts/verify-kpack6-live.ts +230 -0
  170. package/src/acp/codex-plugins.ts +17 -0
  171. package/src/agent/task-notification.ts +32 -2
  172. package/src/agent-mission/in-agent-plugin.ts +8 -3
  173. package/src/agent-substrate/review-observation.ts +9 -2
  174. package/src/agent-substrate/self-review-cli.ts +62 -16
  175. package/src/boot/daemon-tools/self-implement.ts +72 -4
  176. package/src/briefing/brief-items.ts +151 -35
  177. package/src/budget/llm-cost.ts +30 -3
  178. package/src/budget/usage-outcomes.ts +32 -0
  179. package/src/claims/claims-ledger.ts +23 -0
  180. package/src/claims/claims-render.ts +22 -2
  181. package/src/cli/brief-cli.ts +131 -10
  182. package/src/cli/claims-cli.ts +24 -4
  183. package/src/cli/control-cli.ts +29 -0
  184. package/src/cli/doctor-cli.ts +90 -8
  185. package/src/cli/msg-cli.ts +177 -11
  186. package/src/cli/plugin-cli.ts +11 -3
  187. package/src/cli/pr-cli.ts +133 -18
  188. package/src/cli/pr-land-overlap-evidence.ts +18 -10
  189. package/src/cli/pr-land-refusal.ts +20 -0
  190. package/src/cli/release-cli.ts +34 -5
  191. package/src/cli/rfc-cli.ts +24 -0
  192. package/src/cli/seat-cli.ts +121 -0
  193. package/src/cli/self-utterances-cli.ts +140 -0
  194. package/src/cli/tailscale-serve.ts +5 -8
  195. package/src/cli/tasks-cli.ts +528 -31
  196. package/src/cli/usage-cli.ts +9 -4
  197. package/src/context-card/cell.ts +40 -0
  198. package/src/dashboard/boot-timing.ts +35 -0
  199. package/src/dashboard/first-screen-band.ts +63 -13
  200. package/src/dashboard/index.ts +94 -27
  201. package/src/dashboard/input/chat-main-plain-turn-runtime.ts +26 -0
  202. package/src/dashboard/skill-detection-runtime.ts +24 -4
  203. package/src/dashboard/slash-runtime/dashboard-handlers.ts +6 -0
  204. package/src/debug/log.ts +1 -1
  205. package/src/domains/morning-report.ts +3 -1
  206. package/src/domains/release-tool.ts +29 -12
  207. package/src/domains/surface-events.ts +16 -0
  208. package/src/elanous-config-dir.ts +2 -2
  209. package/src/fleet/host-doctor.ts +73 -2
  210. package/src/github/pr-labels.ts +10 -4
  211. package/src/graph-runner/runner.ts +104 -15
  212. package/src/harness/harness-clean.ts +21 -3
  213. package/src/harness/harness-cli-command.ts +68 -10
  214. package/src/harness/harness-pod-dispatch.ts +11 -4
  215. package/src/harness/harness-stop.ts +11 -7
  216. package/src/hq/seats.ts +17 -3
  217. package/src/index.ts +69 -9
  218. package/src/instance/current.ts +2 -6
  219. package/src/instance/resolve.ts +5 -6
  220. package/src/intelligence-map/model-bench-scores.ts +102 -0
  221. package/src/llm/openai-sampling-fields.ts +89 -0
  222. package/src/llm/runtime-model-family.ts +61 -0
  223. package/src/llm/zai-coding-plan.ts +27 -1
  224. package/src/llm.ts +13 -89
  225. package/src/loops/orchestrator/inbox.ts +73 -0
  226. package/src/loops/orchestrator/resync-shadow.ts +100 -12
  227. package/src/loops/orchestrator/tick.ts +73 -2
  228. package/src/machines/machine-ledger.ts +31 -3
  229. package/src/maturity/cli-maturity.ts +13 -3
  230. package/src/maturity/feature-maturity.ts +1 -1
  231. package/src/msg/msg-sse-server.ts +141 -0
  232. package/src/msg/msg-store.ts +149 -17
  233. package/src/ops/overview-collect.ts +3 -2
  234. package/src/ops/overview-owner.ts +29 -0
  235. package/src/patents/ledger.ts +128 -0
  236. package/src/plugin-hooks/shell-hook.ts +17 -2
  237. package/src/plugin-missions/evaluator-runner.ts +18 -2
  238. package/src/plugins/install/market-fetch.ts +32 -1
  239. package/src/plugins/install/plugin-install.ts +11 -1
  240. package/src/pod-lease/admission.ts +116 -41
  241. package/src/prompt-enhance/enhance.ts +11 -0
  242. package/src/release-loop/checklist.ts +52 -4
  243. package/src/release-loop/feature-store.ts +10 -9
  244. package/src/release-loop/merged-pr-checklist.ts +24 -5
  245. package/src/release-loop/rubric.ts +70 -0
  246. package/src/rfc-registry/registry.ts +88 -0
  247. package/src/seat-dispatch/seat-request-ledger.ts +4 -0
  248. package/src/seat-loop/knowhow-lint.ts +233 -0
  249. package/src/seat-loop/seat-loop.ts +5 -1
  250. package/src/seat-loop/seat-pack.ts +16 -4
  251. package/src/seat-loop/traffic-signal.ts +7 -0
  252. package/src/self-dev/ask-launch-flow.ts +48 -8
  253. package/src/self-dev/ask-path-candidates.ts +69 -14
  254. package/src/self-dev/decompose-proposal.ts +43 -1
  255. package/src/self-dev/draft-sweep.ts +23 -14
  256. package/src/self-dev/graph-journey-nodes.ts +2 -0
  257. package/src/self-dev/launch-preflight-text.ts +5 -1
  258. package/src/self-dev/launch-preflight.ts +143 -10
  259. package/src/self-dev/orchestrate.ts +6 -2
  260. package/src/self-dev/run-supervisor.ts +35 -1
  261. package/src/self-implement/gate-cli.ts +6 -2
  262. package/src/self-implement/goal-author.ts +5 -28
  263. package/src/self-implement/goal-command-execution.ts +26 -0
  264. package/src/self-implement/goal-new-paths.ts +4 -5
  265. package/src/self-implement/graph-authority.ts +17 -3
  266. package/src/self-implement/graph-yaml.ts +15 -7
  267. package/src/self-implement/headless-elanous-driver.ts +16 -1
  268. package/src/self-implement/host-regate.ts +44 -9
  269. package/src/self-implement/orchestrator.ts +5 -11
  270. package/src/self-implement/pwa-import-graph.ts +194 -7
  271. package/src/self-implement/rework-policy.ts +80 -2
  272. package/src/self-implement/self-implement-runtime.ts +126 -21
  273. package/src/self-implement/speed-claims.ts +145 -6
  274. package/src/session/index.ts +14 -4
  275. package/src/session-runtime/index.ts +55 -11
  276. package/src/skills/tools/persistent-grounding.ts +59 -4
  277. package/src/skills/tools/shell.ts +79 -1
  278. package/src/steward/rejudge-tick.ts +12 -1
  279. package/src/steward/release-day.ts +158 -0
  280. package/src/steward/resync-tick.ts +123 -0
  281. package/src/task-agent/actions.ts +1 -0
  282. package/src/task-agent/adopt-pr.ts +140 -0
  283. package/src/task-agent/board.ts +11 -1
  284. package/src/task-agent/card-evidence.ts +37 -5
  285. package/src/task-agent/cover.ts +105 -19
  286. package/src/task-agent/failure-notify.ts +61 -0
  287. package/src/task-agent/green-sweep.ts +170 -0
  288. package/src/task-agent/land-worktree.ts +52 -5
  289. package/src/task-agent/live-move-history.ts +1 -0
  290. package/src/task-agent/live-moves.ts +56 -17
  291. package/src/task-agent/parent-host.ts +1 -1
  292. package/src/task-agent/postland-card.ts +139 -0
  293. package/src/task-agent/rejudge-on-head.ts +321 -25
  294. package/src/task-agent/remote-rejudge-bind.ts +62 -0
  295. package/src/task-agent/repair-branch-cas.ts +182 -0
  296. package/src/task-agent/repair-cycle.ts +275 -0
  297. package/src/task-agent/repair-next-move.ts +31 -0
  298. package/src/task-agent/repair-review.ts +24 -0
  299. package/src/task-agent/repair-worker.ts +87 -0
  300. package/src/task-agent/review-result-capture.ts +66 -2
  301. package/src/task-agent/shadow.ts +150 -29
  302. package/src/task-agent/task-hand.ts +133 -8
  303. package/src/task-orchestrator/surfaces/pod-repair-commit.ts +96 -0
  304. package/src/task-orchestrator/surfaces/self-implement-pod.ts +71 -12
  305. package/src/telegram.ts +30 -1
  306. package/src/tool-runtime/tool-search-runtime.ts +25 -0
  307. package/src/tui.ts +20 -1
  308. package/src/user-config.ts +11 -60
  309. package/src/ux-sim/fault-sqlite-busy.ts +50 -0
  310. package/src/version/packed-revision.json +1 -1
  311. package/apps/pwa/out/_next/static/chunks/app/approvals/page-9360b1285b2a346e.js +0 -1
  312. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f08f6529d09ffae1.js +0 -1
  313. package/apps/pwa/out/_next/static/chunks/app/board/page-f000d54d258f9ef6.js +0 -1
  314. package/apps/pwa/out/_next/static/chunks/app/bots/page-22f674e7e4c2c9d5.js +0 -1
  315. package/apps/pwa/out/_next/static/chunks/app/ceo/page-8888733e40cea6d1.js +0 -1
  316. package/apps/pwa/out/_next/static/chunks/app/chat/page-9717ec6964b92276.js +0 -1
  317. package/apps/pwa/out/_next/static/chunks/app/consult/page-44eaad486bc98a45.js +0 -1
  318. package/apps/pwa/out/_next/static/chunks/app/control/page-97fa6cf3d34c7eb9.js +0 -1
  319. package/apps/pwa/out/_next/static/chunks/app/editor/page-35e05d7450461a67.js +0 -1
  320. package/apps/pwa/out/_next/static/chunks/app/inside/page-b2638013c0bf7dfd.js +0 -1
  321. package/apps/pwa/out/_next/static/chunks/app/layout-c44f93a41167536f.js +0 -1
  322. package/apps/pwa/out/_next/static/chunks/app/loops/page-1b810474507f3702.js +0 -1
  323. package/apps/pwa/out/_next/static/chunks/app/market/page-74366ae2693a7d95.js +0 -1
  324. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-4e634c5b98fd4e93.js +0 -1
  325. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-ed3070d637850548.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/app/outputs/page-b9cc8a10bc6078b4.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/app/reflection/page-128458c8d86fbb5f.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/app/sessions/page-cf0c2b0183991d09.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/app/showroom/page-b008aff96be56654.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/app/tasks/page-814394dcbae789b1.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/app/term/page-6a87325c87f09dd2.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/trace/page-852170303c6cd755.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/vault/page-087ceeb975ef272b.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-97c7361cc2a19319.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/workspace/page-8c6901833f897496.js +0 -1
  336. /package/apps/pwa/out/_next/static/{dRS6lEhSRRBr1abbzp7fJ → y1ltt7oCZxVpC6zVPhZ2O}/_buildManifest.js +0 -0
  337. /package/apps/pwa/out/_next/static/{dRS6lEhSRRBr1abbzp7fJ → y1ltt7oCZxVpC6zVPhZ2O}/_ssgManifest.js +0 -0
@@ -5,6 +5,9 @@ import { tmpdir } from 'node:os';
5
5
  import { dirname, isAbsolute, join, resolve } from 'node:path';
6
6
  import { debug } from '../../debug/log.js';
7
7
  import { effectiveInstanceRoot } from '../../instance/resolve.js';
8
+ import { PR_LABEL_NAMES } from '../../github/pr-labels.js';
9
+ import { listRunLedgers } from '../../self-implement/run-ledger.js';
10
+ import { queryRunningRuns } from '../../self-implement/running-runs.js';
8
11
  import {
9
12
  defaultGitMergeSeam, defaultLlmResolve, mergeMainWithLlmResolve,
10
13
  type LlmMergeOutcome, type MergeGitSeam,
@@ -16,6 +19,8 @@ interface ResyncShadowEntry {
16
19
  at: string;
17
20
  pr: number;
18
21
  head: string;
22
+ beforeHead?: string;
23
+ afterHead?: string;
19
24
  mode: ResyncShadowMode;
20
25
  pushed: boolean;
21
26
  labelRemoved: boolean;
@@ -27,7 +32,7 @@ interface ResyncShadowEntry {
27
32
  }
28
33
  export interface ResyncShadowSummary { candidates: number; tried: number; saved: number; savedRatio: number | null }
29
34
 
30
- type OpenPr = { number: number; headRefName: string; mergeable: string; isDraft: boolean; labels: { name: string }[] };
35
+ type OpenPr = { number: number; headRefName: string; headRefOid?: string; mergeable: string; isDraft: boolean; labels: { name: string }[] };
31
36
  export interface ResyncShadowDeps {
32
37
  repoRoot?: string;
33
38
  instanceRoot?: string;
@@ -37,6 +42,63 @@ export interface ResyncShadowDeps {
37
42
  gitSeam?: MergeGitSeam;
38
43
  runTests?: (cwd: string, files: string[]) => { ok: boolean; failedTests?: string[]; noTests?: boolean };
39
44
  now?: () => Date;
45
+ /** Run ownership is checked before touching a branch; null means the observation failed. */
46
+ activePrNumbers?: (candidatePrs: readonly number[]) => ReadonlySet<number> | null;
47
+ }
48
+
49
+ function activePrNumbers(candidatePrs: readonly OpenPr[], repoRoot: string, instanceRoot: string): ReadonlySet<number> | null {
50
+ const owners = new Map<number, { runId: string; branch: string; headCommit: string; terminal: boolean }[]>();
51
+ const branchRuns = new Map<string, Set<string>>();
52
+ const incompleteOwnerPrs = new Set<number>();
53
+ const candidateSet = new Set(candidatePrs.map(pr => pr.number));
54
+ try {
55
+ for (const match of listRunLedgers().matches) {
56
+ const latestStatus = match.entries.filter(entry => entry.event === 'run-status').at(-1)?.data.runStatus;
57
+ const terminal = latestStatus === 'completed' || latestStatus === 'failed' || latestStatus === 'cancelled';
58
+ for (const entry of match.entries) {
59
+ // A competing run can own the branch even when its PR number or worktree event is missing.
60
+ if ((entry.event === 'worktree' || entry.event === 'pr-opened') && typeof entry.data.branch === 'string') {
61
+ const ids = branchRuns.get(entry.data.branch) ?? new Set<string>();
62
+ ids.add(match.runId);
63
+ branchRuns.set(entry.data.branch, ids);
64
+ }
65
+ const number = entry.event === 'pr-opened' ? entry.data.number : undefined;
66
+ if (typeof number !== 'number' || !candidateSet.has(number)) continue;
67
+ const matches = owners.get(number) ?? [];
68
+ if (typeof entry.data.branch !== 'string' || typeof entry.data.headCommit !== 'string')
69
+ incompleteOwnerPrs.add(number);
70
+ else matches.push({ runId: match.runId, branch: entry.data.branch, headCommit: entry.data.headCommit, terminal });
71
+ owners.set(number, matches);
72
+ }
73
+ }
74
+ const runIds = [...new Set([...owners.values()].flatMap(rows => rows.map(row => row.runId)))];
75
+ if (runIds.length === 0) return new Set(candidatePrs.map(pr => pr.number));
76
+ const observed = queryRunningRuns({ runIds, caller: 'smart-merge.resync' });
77
+ if (observed.completeness !== 'complete') return null;
78
+ const active = new Set(observed.entries.filter(row => row.status === 'running' || row.status === 'probable-running'
79
+ || row.status === 'unknown' || row.status === 'ended-unclosed').map(row => row.runId));
80
+ const priorPath = join(instanceRoot, 'orchestrator', 'resync-shadow.jsonl');
81
+ const prior = existsSync(priorPath) ? readFileSync(priorPath, 'utf8').split('\n').filter(Boolean).map(line => JSON.parse(line) as ResyncShadowEntry) : [];
82
+ const excluded = new Set<number>();
83
+ for (const pr of candidatePrs) {
84
+ const matches = owners.get(pr.number) ?? [];
85
+ if (incompleteOwnerPrs.has(pr.number) || matches.length !== 1 || !/^[a-f0-9]{40}$/i.test(pr.headRefOid ?? '')) { excluded.add(pr.number); continue; }
86
+ const owner = matches[0]!;
87
+ if (owner.branch !== pr.headRefName || !owner.terminal || !/^[a-f0-9]{40}$/i.test(owner.headCommit)
88
+ || active.has(owner.runId) || [...(branchRuns.get(pr.headRefName) ?? [])].some(id => id !== owner.runId)) {
89
+ excluded.add(pr.number); continue;
90
+ }
91
+ const remote = command('git', ['ls-remote', 'origin', `refs/heads/${pr.headRefName}`], repoRoot).trim().split(/\s/)[0];
92
+ if (remote !== pr.headRefOid) { excluded.add(pr.number); continue; }
93
+ let proven = owner.headCommit;
94
+ for (const row of prior.filter(row => row.pr === pr.number && row.head === pr.headRefName && row.pushed)) {
95
+ if (row.beforeHead !== proven || !/^[a-f0-9]{40}$/i.test(row.afterHead ?? '')) { proven = ''; break; }
96
+ proven = row.afterHead!;
97
+ }
98
+ if (proven !== remote) excluded.add(pr.number);
99
+ }
100
+ return excluded;
101
+ } catch { return null; }
40
102
  }
41
103
 
42
104
  function command(file: string, args: string[], cwd: string, timeout = 60_000): string {
@@ -51,6 +113,15 @@ function changedTests(gh: (args: string[]) => string, number: number): string[]
51
113
  path.endsWith('.test.ts') && !isAbsolute(path) && path !== '' && !path.split('/').some(part => part === '..' || part === '.' || part === '')))];
52
114
  }
53
115
 
116
+ function listOpenPrs(gh: (args: string[]) => string): OpenPr[] {
117
+ const observed: unknown = JSON.parse(gh(['pr', 'list', '--state', 'open', '--json', 'number,headRefName,headRefOid,mergeable,isDraft,labels', '--limit', '1000']));
118
+ if (!Array.isArray(observed) || observed.length >= 1000 || observed.some(pr => !pr || !Number.isSafeInteger(pr.number)
119
+ || typeof pr.headRefName !== 'string' || typeof pr.mergeable !== 'string' || typeof pr.isDraft !== 'boolean'
120
+ || !Array.isArray(pr.labels) || pr.labels.some((label: unknown) => !label || typeof label !== 'object' || typeof (label as { name?: unknown }).name !== 'string')))
121
+ throw new Error('incomplete or invalid PR observation');
122
+ return observed as OpenPr[];
123
+ }
124
+
54
125
  // A PR test must not inherit credentials, reach the network or write outside its own worktree.
55
126
  // If the kernel does not provide the required isolation, fail closed before starting bun.
56
127
  const TEST_SANDBOX = `import ctypes, os, sys
@@ -145,16 +216,16 @@ export async function runResyncShadow(max = 3, deps: ResyncShadowDeps = {}, mode
145
216
  if (mode !== 'shadow' && mode !== 'live') throw new Error('mode must be shadow or live');
146
217
  const repoRoot = deps.repoRoot ?? resolve(import.meta.dir, '../../..');
147
218
  const gh = deps.runGh ?? ((args: string[]) => command('gh', args, repoRoot));
148
- const observed: unknown = JSON.parse(gh(['pr', 'list', '--state', 'open', '--json', 'number,headRefName,mergeable,isDraft,labels', '--limit', '1000']));
149
- if (!Array.isArray(observed) || observed.length >= 1000 || observed.some(pr => !pr || !Number.isSafeInteger(pr.number)
150
- || typeof pr.headRefName !== 'string' || typeof pr.mergeable !== 'string' || typeof pr.isDraft !== 'boolean'
151
- || !Array.isArray(pr.labels) || pr.labels.some((label: unknown) => !label || typeof label !== 'object' || typeof (label as { name?: unknown }).name !== 'string')))
152
- throw new Error('incomplete or invalid PR observation');
153
- const candidates = (observed as OpenPr[]).filter(pr => pr.headRefName.startsWith('self-impl/')
154
- && pr.mergeable === 'CONFLICTING'
155
- && (mode === 'shadow' || pr.labels.some(label => label.name === 'elanous:needs-rebase'))
156
- && !pr.labels.some(label => label.name === 'elanous:stalled' || label.name === 'elanous:superseded'))
157
- .sort((a, b) => a.number - b.number).slice(0, max);
219
+ const eligible = listOpenPrs(gh).filter(pr => pr.headRefName.startsWith('self-impl/')
220
+ && (mode === 'shadow' ? pr.mergeable === 'CONFLICTING' : pr.mergeable === 'MERGEABLE')
221
+ && (mode === 'shadow' || pr.labels.some(label => label.name === PR_LABEL_NAMES.needsRebase))
222
+ && !pr.labels.some(label => label.name === PR_LABEL_NAMES.running)
223
+ && !pr.labels.some(label => label.name === PR_LABEL_NAMES.stalled || label.name === PR_LABEL_NAMES.superseded));
224
+ const observeActive = deps.activePrNumbers ?? ((numbers: readonly number[]) => activePrNumbers(
225
+ eligible.filter(pr => numbers.includes(pr.number)), repoRoot, deps.instanceRoot ?? effectiveInstanceRoot()));
226
+ const active = mode === 'live' && eligible.length ? observeActive(eligible.map(pr => pr.number)) : new Set<number>();
227
+ if (active === null) throw new Error('active run observation unavailable — resync stays read-only');
228
+ const candidates = eligible.filter(pr => !active.has(pr.number)).sort((a, b) => a.number - b.number).slice(0, max);
158
229
  const ledger = join(deps.instanceRoot ?? effectiveInstanceRoot(), 'orchestrator', 'resync-shadow.jsonl');
159
230
  let saved = 0;
160
231
  let tried = 0;
@@ -169,6 +240,9 @@ export async function runResyncShadow(max = 3, deps: ResyncShadowDeps = {}, mode
169
240
  // Explicit destination refspecs make origin/<head> and origin/main fresh even without remote.origin.fetch.
170
241
  command('git', ['fetch', 'origin', `+refs/heads/${pr.headRefName}:refs/remotes/origin/${pr.headRefName}`,
171
242
  '+refs/heads/main:refs/remotes/origin/main'], repoRoot);
243
+ entry.beforeHead = command('git', ['rev-parse', `origin/${pr.headRefName}`], repoRoot).trim();
244
+ if (mode === 'live' && !deps.activePrNumbers && entry.beforeHead !== pr.headRefOid)
245
+ throw new Error('PR head changed after owner proof — resync stays read-only');
172
246
  entry.testFiles = changedTests(gh, pr.number);
173
247
  tmp = realpathSync(mkdtempSync(join(tmpdir(), 'resync-shadow-')));
174
248
  command('git', ['worktree', 'add', '--detach', tmp, `origin/${pr.headRefName}`], repoRoot);
@@ -189,10 +263,24 @@ export async function runResyncShadow(max = 3, deps: ResyncShadowDeps = {}, mode
189
263
  } else entry.verdict = outcome.status === 'conflict-unresolved' || outcome.status === 'up-to-date' ? 'unresolved' : 'error';
190
264
  if (mode === 'live' && entry.verdict === 'saved') {
191
265
  try {
266
+ const lateActive = observeActive([pr.number]);
267
+ if (lateActive === null || lateActive.has(pr.number))
268
+ throw new Error('PR ownership changed before push — resync stays read-only');
269
+ const current = listOpenPrs(gh).filter(row => row.number === pr.number);
270
+ if (current.length !== 1 || current[0]!.headRefName !== pr.headRefName
271
+ || current[0]!.headRefOid !== pr.headRefOid || current[0]!.mergeable !== 'MERGEABLE'
272
+ || !current[0]!.labels.some(label => label.name === PR_LABEL_NAMES.needsRebase)
273
+ || current[0]!.labels.some(label => label.name === PR_LABEL_NAMES.running
274
+ || label.name === PR_LABEL_NAMES.stalled || label.name === PR_LABEL_NAMES.superseded))
275
+ throw new Error('PR state changed before push — resync stays read-only');
276
+ const immediateHead = command('git', ['ls-remote', 'origin', `refs/heads/${pr.headRefName}`], repoRoot).trim().split(/\s/)[0];
277
+ if (immediateHead !== entry.beforeHead)
278
+ throw new Error('remote head changed before push — resync stays read-only');
192
279
  (deps.runGit ?? (args => { command('git', args, tmp!); }))(
193
280
  ['push', 'origin', `HEAD:refs/heads/${pr.headRefName}`]);
194
281
  entry.pushed = true;
195
- gh(['pr', 'edit', String(pr.number), '--remove-label', 'elanous:needs-rebase']);
282
+ entry.afterHead = command('git', ['rev-parse', 'HEAD'], tmp).trim();
283
+ gh(['pr', 'edit', String(pr.number), '--remove-label', PR_LABEL_NAMES.needsRebase]);
196
284
  entry.labelRemoved = true;
197
285
  } catch (error) {
198
286
  const message = error instanceof Error ? error.message : String(error);
@@ -1,5 +1,5 @@
1
1
  import { withFileLockSync } from '../../storage/file-lock.js';
2
- import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
2
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from 'node:fs';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { CardStore, taskCardsDir, type TaskCard } from '../../task-cards/card-store.js';
5
5
  import { Database } from 'bun:sqlite';
@@ -16,6 +16,7 @@ import { placeOnGrid, type GridHost } from './grid-place.js';
16
16
  import { checkPodPool, parsePodPool, resolvePodPoolSpec } from '../../task-orchestrator/surfaces/pod-pool.js';
17
17
  import { leaseKubectl } from '../../task-orchestrator/surfaces/pod-lease.js';
18
18
  import { readTaskAgentState, TASK_SEATS, type HandTaskOptions, type TaskCard as TaskAgentCard, type HandTaskResult, type TaskLauncher, type TaskSeat } from '../../task-agent/task-hand.js';
19
+ import { runCoordinatorInbox } from './inbox.js';
19
20
 
20
21
  export type Window = '08' | '12' | '18';
21
22
  export type Node = 'intake' | 'split' | 'place' | 'delegate' | 'launch' | 'reconcile' | 'report';
@@ -31,6 +32,10 @@ export interface TickState {
31
32
  /** The highest degradation actually applied in this run — the report keeps it even if the signal recovers before report. */
32
33
  degradationPeak?: { level: 1 | 2; reason: string };
33
34
  deferredCells?: Cell[];
35
+ /** A placement adapter rejected at least one cell; this run cannot claim full completion. */
36
+ placementFailed?: boolean;
37
+ /** Last confirmed live placement write in this run, carried across node processes. */
38
+ boardWrittenAt?: string;
34
39
  /** place ran at L2 (skipped) in this run — its unplaced cells are carried even if the board is back by delegate. */
35
40
  placeDegraded?: boolean;
36
41
  delegatedKeys?: string[];
@@ -67,6 +72,8 @@ export interface TickDeps {
67
72
  handToTaskAgentCell?: string;
68
73
  handTask?: (opts: HandTaskOptions) => Promise<HandTaskResult>;
69
74
  taskLauncher?: TaskLauncher;
75
+ /** Coordinator inbox seam; the real intake reads and replies on the durable message bus. */
76
+ coordinatorInbox?: () => { replied: number; acknowledged: number };
70
77
  }
71
78
 
72
79
  /** ORCH-TA-HAND ledger — one row per handed `orch:<card>:<cell>` key so a cell is handed once across ticks and processes. */
@@ -329,6 +336,41 @@ function stateFile(root: string, runId: string): string {
329
336
  if (!/^[a-zA-Z0-9._-]+$/.test(runId) || runId === '.' || runId === '..') throw new Error('Invalid orchestrator runId');
330
337
  return join(root, 'loop', 'orchestrator', `${runId}.json`);
331
338
  }
339
+ /** COORD-HA ③ 진행 하트비트 — the last tick that finished its work and the last confirmed placement write. The guardian watches this
340
+ * (RFC-loop-agent-map §A8b: «마지막 틱 완료 ⊕ 보드 마지막 쓰기 시각»), not process liveness — a held node keeps pulsing
341
+ * healthy while this record goes stale, which is exactly the stall it exists to expose. */
342
+ export interface CompletionHeartbeat {
343
+ runId: string;
344
+ node: Node;
345
+ window: Window;
346
+ mode: Mode;
347
+ /** last confirmed live placement write; null before the first such write */
348
+ boardWrittenAt: string | null;
349
+ /** last fully completed run, or null while only board writes have succeeded */
350
+ tickCompletedAt: string | null;
351
+ }
352
+ /** Its own subdirectory, so the run-state scanners (`restorePrevious` · `previousDelegations` · `syncQueuedPoolHints`) never parse it as a TickState. */
353
+ function completionHeartbeatPath(root: string): string { return join(root, 'loop', 'orchestrator', 'heartbeat', 'completion.json'); }
354
+ /** COORD-HA ③ — last completed run and board write, independently. Undefined means neither has been recorded. */
355
+ export function readCompletionHeartbeat(root: string): CompletionHeartbeat | undefined {
356
+ const path = completionHeartbeatPath(root);
357
+ if (!existsSync(path)) return undefined;
358
+ try {
359
+ const { runId, node, window, mode, boardWrittenAt, tickCompletedAt } = JSON.parse(readFileSync(path, 'utf8')) as Partial<CompletionHeartbeat>;
360
+ if (typeof runId !== 'string' || typeof node !== 'string' || typeof window !== 'string' || typeof mode !== 'string'
361
+ || (boardWrittenAt !== null && (typeof boardWrittenAt !== 'string' || !Number.isFinite(Date.parse(boardWrittenAt))))
362
+ || (tickCompletedAt !== null && (typeof tickCompletedAt !== 'string' || !Number.isFinite(Date.parse(tickCompletedAt))))) return undefined;
363
+ return { runId, node: node as Node, window: window as Window, mode: mode as Mode, boardWrittenAt, tickCompletedAt };
364
+ } catch { return undefined; }
365
+ }
366
+ /** Temp + rename, so a guardian in another process never reads a torn record. */
367
+ function recordCompletionHeartbeat(root: string, completion: CompletionHeartbeat): void {
368
+ const path = completionHeartbeatPath(root);
369
+ mkdirSync(dirname(path), { recursive: true });
370
+ const temp = `${path}.${process.pid}.tmp`;
371
+ writeFileSync(temp, JSON.stringify(completion));
372
+ renameSync(temp, path);
373
+ }
332
374
  function restorePrevious(root: string, day: string, mode: Mode): Pick<TickState, 'cards' | 'cells' | 'launched'> | null {
333
375
  const dir = join(root, 'loop', 'orchestrator');
334
376
  if (!existsSync(dir)) return null;
@@ -430,6 +472,15 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
430
472
  try { (deps.observe ?? ((name, payload) => debug.log('loop.orchestrator', name, payload)))(event, data); }
431
473
  catch { /* observation must not change the run */ }
432
474
  };
475
+ // A graph node may be replayed with the same runId, and 12/18 skip intake.
476
+ // Drain the bus on every tick command; the message transaction prevents duplicate answers.
477
+ if (mode !== 'off') {
478
+ const inbox = (deps.coordinatorInbox ?? (() => runCoordinatorInbox(undefined, 100, join(root, 'msg', 'messages.db'))))();
479
+ if (inbox.replied || inbox.acknowledged) {
480
+ observe('exchange', 'coordinator-inbox-replied', inbox.replied);
481
+ observe('exchange', 'coordinator-inbox-acknowledged', inbox.acknowledged);
482
+ }
483
+ }
433
484
  if (mode !== 'off' && activeNode(node, window) && state.nodes[node] !== 'ok') {
434
485
  if (node === 'intake') {
435
486
  if (mode === 'live' && deps.cards) { state.cards = wishCards(deps.cards()); state.skippedCards = []; }
@@ -495,11 +546,19 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
495
546
  if (validVersion(placed?.version)) {
496
547
  cell.version = placed.version;
497
548
  state.placed++;
549
+ // The live placer has returned from its board write. A shadow dry run or null result is not a write.
550
+ if (mode === 'live') state.boardWrittenAt = currentTime().toISOString();
498
551
  observe('exchange', mode === 'shadow' ? `would-place:${cell.id}:${cell.version}` : `placed:${cell.id}:${cell.version}`, 1);
499
- } else { state.unplaced = (state.unplaced ?? 0) + 1; observe('exchange', 'unplaced-no-release', 1); }
552
+ } else {
553
+ state.unplaced = (state.unplaced ?? 0) + 1;
554
+ if (mode === 'live') state.placementFailed = true;
555
+ observe('exchange', 'unplaced-no-release', 1);
556
+ }
500
557
  } catch (error) {
501
558
  state.unplaced = (state.unplaced ?? 0) + 1;
559
+ state.placementFailed = true;
502
560
  observe('exchange', `unplaced:${cell.id}:${String(error).slice(0, 160)}`, 1);
561
+ // Preserve the cell-by-cell recovery contract; the run completion record is withheld at report.
503
562
  }
504
563
  }
505
564
  }
@@ -762,8 +821,20 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
762
821
  state.nodes[node] = 'skipped';
763
822
  }
764
823
  observe('tick', state.nodes[node], state.cells.length);
824
+ if (mode === 'live' && node === 'place' && state.boardWrittenAt) {
825
+ // A board write can succeed even when a later cell fails. Advance its clock without claiming run completion.
826
+ const previous = readCompletionHeartbeat(root);
827
+ recordCompletionHeartbeat(root, previous
828
+ ? { ...previous, boardWrittenAt: state.boardWrittenAt }
829
+ : { runId, node, window, mode, boardWrittenAt: state.boardWrittenAt, tickCompletedAt: null });
830
+ }
765
831
  mkdirSync(dirname(path), { recursive: true });
766
832
  writeFileSync(path, JSON.stringify(state));
833
+ if (mode !== 'off' && node === 'report' && !state.placementFailed && !state.placeDegraded) {
834
+ // Only the last node can attest that the whole run completed. Carry the most recent confirmed board write.
835
+ const boardWrittenAt = state.boardWrittenAt ?? readCompletionHeartbeat(root)?.boardWrittenAt ?? null;
836
+ recordCompletionHeartbeat(root, { runId, node, window, mode, boardWrittenAt, tickCompletedAt: currentTime().toISOString() });
837
+ }
767
838
  return state;
768
839
  } catch (error) {
769
840
  nodeError = error;
@@ -10,6 +10,16 @@ export interface MachineRow {
10
10
  locationStatus: string;
11
11
  role: string;
12
12
  duties: string;
13
+ /** Fleet intent only. Live membership comes from Nexus machine resources. */
14
+ fleet?: MachineFleetIntent;
15
+ }
16
+
17
+ export interface MachineFleetIntent {
18
+ sshName: string;
19
+ role: 'worker' | 'hq';
20
+ platform: 'darwin' | 'linux' | 'wsl';
21
+ podPool: string;
22
+ cap: number;
13
23
  }
14
24
 
15
25
  export interface DeviceRow {
@@ -59,18 +69,36 @@ export function validateMachineLedger(value: unknown): MachineLedger {
59
69
  keys(data, ['title', 'policy', 'machines', 'devices', 'rules', 'changes'], 'ledger');
60
70
  if (!Array.isArray(data.machines) || !Array.isArray(data.devices)) throw new Error('invalid machines or devices');
61
71
  const ids = new Set<string>();
62
- const rows = <T extends { id: string }>(entries: unknown[], fields: string[], label: string): T[] => entries.map((entry, index) => {
72
+ const rows = <T extends { id: string }>(entries: unknown[], fields: string[], label: string, extraFields: string[] = []): T[] => entries.map((entry, index) => {
63
73
  const row = record(entry, `${label}[${index}]`);
64
- keys(row, fields, label);
74
+ keys(row, [...fields, ...extraFields], label);
65
75
  const result = Object.fromEntries(fields.map((field) => [field, text(row[field], `${label}.${field}`)])) as T;
66
76
  if (!idPattern.test(result.id) || ids.has(result.id)) throw new Error(`empty, duplicate or invalid id: ${result.id}`);
67
77
  ids.add(result.id);
68
78
  return result;
69
79
  });
80
+ const machines = rows<MachineRow>(data.machines, ['id', 'name', 'specs', 'locationStatus', 'role', 'duties'], 'machines', ['fleet']);
81
+ const sshNames = new Set<string>();
82
+ for (let index = 0; index < machines.length; index++) {
83
+ const raw = record(data.machines[index], `machines[${index}]`);
84
+ if (raw.fleet === undefined) continue;
85
+ const fleet = record(raw.fleet, `machines[${index}].fleet`);
86
+ keys(fleet, ['sshName', 'role', 'platform', 'podPool', 'cap'], 'fleet');
87
+ const sshName = text(fleet.sshName, 'fleet.sshName');
88
+ const role = text(fleet.role, 'fleet.role');
89
+ const platform = text(fleet.platform, 'fleet.platform');
90
+ const podPool = text(fleet.podPool, 'fleet.podPool');
91
+ if (!idPattern.test(sshName) || sshNames.has(sshName)) throw new Error(`duplicate or invalid fleet.sshName: ${sshName}`);
92
+ if (role !== 'worker' && role !== 'hq') throw new Error(`invalid fleet.role: ${role}`);
93
+ if (platform !== 'darwin' && platform !== 'linux' && platform !== 'wsl') throw new Error(`invalid fleet.platform: ${platform}`);
94
+ if (!Number.isSafeInteger(fleet.cap) || (fleet.cap as number) < 0) throw new Error('invalid fleet.cap');
95
+ sshNames.add(sshName);
96
+ machines[index]!.fleet = { sshName, role, platform, podPool, cap: fleet.cap as number };
97
+ }
70
98
  return {
71
99
  title: text(data.title, 'title'),
72
100
  policy: list(data.policy, 'policy'),
73
- machines: rows<MachineRow>(data.machines, ['id', 'name', 'specs', 'locationStatus', 'role', 'duties'], 'machines'),
101
+ machines,
74
102
  devices: rows<DeviceRow>(data.devices, ['id', 'name', 'status', 'purpose'], 'devices'),
75
103
  rules: list(data.rules, 'rules'),
76
104
  changes: list(data.changes, 'changes'),
@@ -18,14 +18,24 @@ export function cliRootVisibleFor(name: string, role: Role, { showAll }: { showA
18
18
  return grade === 'stable' || (role === 'contributor' && (grade === 'beta' || grade === 'tool'));
19
19
  }
20
20
 
21
- /** Only help is filtered: Commander still dispatches explicitly typed commands. */
22
- export function filterCliRootHelp(commands: readonly Command[], role: Role, showAll: boolean): { shown: number; hidden: number } {
21
+ /** Root --help first screen (SETUP-HELP-SLIM): the three stable onboarding commands. */
22
+ export const FIRST_SCREEN_COMMANDS: readonly string[] = ['start', 'setup', 'ask'];
23
+
24
+ /**
25
+ * Only help is filtered: Commander still dispatches explicitly typed commands.
26
+ * firstScreen narrows the visible set to FIRST_SCREEN_COMMANDS and showAll wins over it;
27
+ * the caller opts in per role — the wired default (deferred slice) enables it for the
28
+ * general role only, so owner/contributor keep today's list.
29
+ */
30
+ export function filterCliRootHelp(commands: readonly Command[], role: Role, showAll: boolean, firstScreen: boolean = false): { shown: number; hidden: number } {
31
+ const firstScreenActive = firstScreen && !showAll;
23
32
  let shown = 0;
24
33
  let hidden = 0;
25
34
  for (const command of commands) {
26
35
  const grade = cliRootMaturity(command.name(), commands);
27
36
  if (!grade) throw new Error(`cliRoot 에 등급 없는 명령: ${command.name()}`);
28
- const visible = cliRootVisibleFor(command.name(), role, { showAll }, commands);
37
+ const visible = cliRootVisibleFor(command.name(), role, { showAll }, commands)
38
+ && (!firstScreenActive || FIRST_SCREEN_COMMANDS.includes(command.name()));
29
39
  (command as Command & { _hidden: boolean })._hidden = !visible;
30
40
  if (visible) shown++;
31
41
  else hidden++;
@@ -62,7 +62,7 @@ export const FEATURE_MATURITY = {
62
62
  preview: 'beta', pty: 'beta', pr: 'beta', repo: 'beta', role: 'beta', machine: 'beta', image: 'beta', 'agent-env': 'beta',
63
63
  where: 'stable', bench: 'beta', shadow: 'beta', llm: 'beta', storage: 'beta', leader: 'beta',
64
64
  pod: 'beta', browser: 'beta', questions: 'beta', usage: 'stable', live: 'beta',
65
- research: 'beta', release: 'beta', know: 'beta', freeze: 'beta', flow: 'beta', seat: 'beta', hq: 'beta', steward: 'beta', coord: 'beta', context: 'beta', 'model-watch': 'beta', doctor: 'stable',
65
+ research: 'beta', release: 'beta', rfc: 'beta', know: 'beta', freeze: 'beta', flow: 'beta', seat: 'beta', hq: 'beta', steward: 'beta', coord: 'beta', context: 'beta', 'model-watch': 'beta', doctor: 'stable',
66
66
  control: 'beta', resources: 'beta', hooks: 'beta', setup: 'stable', start: 'stable',
67
67
  grounding: 'beta', graph: 'beta', loop: 'stable', resource: 'beta', card: 'stable', 'launch-head': 'beta',
68
68
  a2a: 'beta', plugin: 'stable', make: 'beta', skills: 'beta', connect: 'beta', import: 'beta',
@@ -0,0 +1,141 @@
1
+ import { timingSafeEqual } from 'node:crypto';
2
+ import { canonicalSeatId, type Message, type MsgStore } from './msg-store.js';
3
+
4
+ type Subscriber = { to: string; lastId: number; heartbeatAt: number; replaying: boolean; controller: ReadableStreamDefaultController<Uint8Array> };
5
+ const encoder = new TextEncoder();
6
+
7
+ function cursor(raw: string | null): number {
8
+ if (raw === null) return 0;
9
+ if (!/^(0|[1-9]\d*)$/.test(raw)) throw new Error('invalid message cursor');
10
+ const id = Number(raw);
11
+ if (!Number.isSafeInteger(id)) throw new Error('invalid message cursor');
12
+ return id;
13
+ }
14
+
15
+ function authorized(request: Request, token: string): boolean {
16
+ const value = request.headers.get('authorization');
17
+ if (!value?.startsWith('Bearer ')) return false;
18
+ const received = Buffer.from(value.slice(7));
19
+ const expected = Buffer.from(token);
20
+ return expected.length > 0 && received.length === expected.length && timingSafeEqual(received, expected);
21
+ }
22
+
23
+ function addressed(message: Message, to: string): boolean {
24
+ return message.to === to || (message.to === 'ALL' && message.from !== to);
25
+ }
26
+
27
+ /** The SQLite ledger remains authoritative; this independent process only accelerates delivery. */
28
+ export function startMsgSseServer(input: {
29
+ store: MsgStore;
30
+ token: string;
31
+ port: number;
32
+ hostname?: string;
33
+ pollMs?: number;
34
+ }): { port: number; stats: () => { subscribers: number; maxQueuedEvents: number }; stop: () => void } {
35
+ if (!input.token) throw new Error('message SSE token is required');
36
+ if (!Number.isSafeInteger(input.port) || input.port < 0 || input.port > 65535) throw new Error('invalid SSE port');
37
+ const hostname = input.hostname ?? '127.0.0.1';
38
+ if (!['127.0.0.1', 'localhost', '::1', '[::1]'].includes(hostname)) throw new Error('message SSE must bind to loopback');
39
+ const pollMs = input.pollMs ?? 250;
40
+ if (!Number.isSafeInteger(pollMs) || pollMs < 1 || pollMs > 500) throw new Error('SSE ledger poll must be 1–500ms');
41
+ const subscribers = new Set<Subscriber>();
42
+ let maxQueuedEvents = 0;
43
+ let scanned = input.store.latestId();
44
+ const send = (subscriber: Subscriber, message: Message) => {
45
+ if (message.id <= subscriber.lastId || !addressed(message, subscriber.to)) return;
46
+ if ((subscriber.controller.desiredSize ?? 0) < -256) throw new Error('slow SSE subscriber; reconnect for ledger replay');
47
+ subscriber.controller.enqueue(encoder.encode(`id: ${message.id}\ndata: ${JSON.stringify(message)}\n\n`));
48
+ maxQueuedEvents = Math.max(maxQueuedEvents, Math.max(0, 1 - (subscriber.controller.desiredSize ?? 0)));
49
+ subscriber.lastId = message.id;
50
+ };
51
+ const replay = (subscriber: Subscriber) => {
52
+ if (!subscriber.replaying) return;
53
+ try {
54
+ while ((subscriber.controller.desiredSize ?? 0) > 0) {
55
+ const batch = input.store.listByRecipient(subscriber.to, subscriber.lastId, 1000);
56
+ if (!batch.length) { subscriber.replaying = false; break; }
57
+ for (const message of batch) {
58
+ if ((subscriber.controller.desiredSize ?? 0) <= 0) break;
59
+ send(subscriber, message);
60
+ }
61
+ }
62
+ } catch (error) {
63
+ subscribers.delete(subscriber);
64
+ subscriber.controller.error(error);
65
+ }
66
+ };
67
+ const tick = () => {
68
+ try {
69
+ let batch: Message[];
70
+ let scans = 0;
71
+ do {
72
+ batch = input.store.listAfter(scanned);
73
+ for (const message of batch) {
74
+ scanned = message.id;
75
+ for (const subscriber of subscribers) {
76
+ if (subscriber.replaying) continue;
77
+ try { send(subscriber, message); } catch (error) {
78
+ subscribers.delete(subscriber);
79
+ try { subscriber.controller.error(error); } catch { /* already disconnected */ }
80
+ }
81
+ }
82
+ }
83
+ scans++;
84
+ } while (batch.length === 1000 && scans < 4);
85
+ const now = Date.now();
86
+ for (const subscriber of subscribers) {
87
+ if (subscriber.replaying || (subscriber.controller.desiredSize ?? 0) <= 0) continue;
88
+ if (now - subscriber.heartbeatAt < 1000) continue;
89
+ try {
90
+ subscriber.controller.enqueue(encoder.encode(': keepalive\n\n'));
91
+ subscriber.heartbeatAt = now;
92
+ } catch { subscribers.delete(subscriber); }
93
+ }
94
+ } catch (error) {
95
+ // The next tick retries from the last scanned ID; the durable ledger is unchanged.
96
+ console.error(`msg SSE scan failed: ${error instanceof Error ? error.message : String(error)}`);
97
+ }
98
+ };
99
+ const server = Bun.serve({
100
+ port: input.port,
101
+ hostname,
102
+ fetch(request) {
103
+ const url = new URL(request.url);
104
+ if (url.pathname !== '/stream' || request.method !== 'GET') return new Response('Not Found', { status: 404 });
105
+ if (!authorized(request, input.token)) return new Response('Unauthorized', { status: 401 });
106
+ let to: string;
107
+ let after: number;
108
+ try {
109
+ to = canonicalSeatId(url.searchParams.get('to') ?? '');
110
+ if (to === 'ALL') throw new Error('ALL is not a recipient');
111
+ after = Math.max(cursor(url.searchParams.get('since')), cursor(request.headers.get('last-event-id')));
112
+ } catch (error) { return new Response(error instanceof Error ? error.message : String(error), { status: 400 }); }
113
+ let subscriber: Subscriber;
114
+ const stream = new ReadableStream<Uint8Array>({
115
+ start(controller) {
116
+ subscriber = { to, lastId: after, heartbeatAt: Date.now(), replaying: true, controller };
117
+ subscribers.add(subscriber);
118
+ controller.enqueue(encoder.encode(': connected\n\n'));
119
+ },
120
+ pull() { replay(subscriber); },
121
+ cancel() { subscribers.delete(subscriber); },
122
+ });
123
+ request.signal.addEventListener('abort', () => subscribers.delete(subscriber), { once: true });
124
+ return new Response(stream, { headers: {
125
+ 'Content-Type': 'text/event-stream; charset=utf-8',
126
+ 'Cache-Control': 'no-cache, no-transform',
127
+ Connection: 'keep-alive',
128
+ } });
129
+ },
130
+ });
131
+ const timer = setInterval(tick, pollMs);
132
+ timer.unref();
133
+ return { port: server.port ?? input.port,
134
+ stats: () => ({ subscribers: subscribers.size, maxQueuedEvents }),
135
+ stop: () => {
136
+ clearInterval(timer);
137
+ for (const subscriber of subscribers) { try { subscriber.controller.close(); } catch { /* disconnected */ } }
138
+ subscribers.clear();
139
+ server.stop(true);
140
+ } };
141
+ }