elanous 0.2.17 → 0.2.18

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 (309) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/P6YTVgzuZz5oTZhY4Din_/_buildManifest.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/{1307-aac7332576c4d6fc.js → 1307-30dff22c9e1f3073.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/195-fb1d7690bf30bab4.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/1986-33e13d648a9df765.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/2537-6c289376caa88ca1.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/2699-325cc923ba7b8631.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/4565-3d7e3fdeb43686a8.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/{6158.9c0449a9a8d0064b.js → 6158.af2e93b7adff709b.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/6877-74399f69c7da1433.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/7797-df5aa0381fdc078f.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/8695-846034ce44852c70.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-3143b07a817d1da9.js → page-d92f7ee3994e84c7.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-17ba086ef82d339b.js → page-4733c967b56a4277.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/board/{page-7eb661ff23af7da2.js → page-d22fd849cc22f104.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/botlab/page-bb742f995c6769e7.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/bots/{page-6e5c3a867557298b.js → page-02b073a2ac793b83.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/chat/page-8a9b3594bc23289b.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/consult/{page-5e77cd1802567c89.js → page-a0eb02638b1e6f31.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/control/page-308111abadf35670.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-4187666132fc98c4.js → page-5d97b2901e0d1ea6.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/editor/{page-09328d499162bae4.js → page-c39554c321576982.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/exec/{page-29ff3a8f84f03b6a.js → page-8c3d8e63ae2e1c98.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/field/{page-80ec7401bc83f511.js → page-9b72a07d424701c6.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/intake/{page-fc7d86467291ae38.js → page-a572f58cd3ca5864.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/live/page-be5ea965a07fbc41.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/loops/{page-0e2d6985fb42cd2f.js → page-c130b63862437814.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/market/{page-13e4108999f58d13.js → page-fa244bd146a00ae8.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-e8296ee284aa1293.js → page-44d56d6391965a6a.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/morning/{page-043a9111e74f6b24.js → page-83a700d4c754d802.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/{not-found-23a4601ceb7f452e.js → not-found-7b3170f374f77b9d.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-10d958605a77b3bd.js → page-a3c58f5c72a5f3f9.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/outputs/{page-7a1d85e121065cc8.js → page-e8e1ecaea7cd71d2.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-12916918988ac97e.js → page-1467047104141297.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-d8a3347a9d0c3f4a.js → page-2da6f9b8a38c6978.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/sessions/page-102310930791197a.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/settings/page-6ed0b73bae8351d8.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-1d824510df025d18.js → page-44d5f767491692bf.js} +1 -1
  53. package/apps/pwa/out/_next/static/chunks/app/setup/page-43336055451f6e37.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/share/{page-7f49fe645d5132f7.js → page-3cc47dca0922ab9a.js} +1 -1
  55. package/apps/pwa/out/_next/static/chunks/app/showroom/page-a77ee355a2304ff0.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-27d71f131acd9320.js → page-b17cd46cf0e9b8cf.js} +1 -1
  57. package/apps/pwa/out/_next/static/chunks/app/term/page-61553851aa606146.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/today/page-a1f585e154e61c59.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/trace/{page-424659bacfb8cdd7.js → page-7bf54c9bb9274af8.js} +1 -1
  60. package/apps/pwa/out/_next/static/chunks/app/vault/page-5150ef17b5383a57.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-5d4fef9ccd7d406b.js → page-134da468538baf48.js} +1 -1
  64. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/{webpack-539cde99554be20f.js → webpack-c2782b330447ffb6.js} +1 -1
  66. package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +1 -0
  67. package/apps/pwa/out/approvals/index.html +1 -1
  68. package/apps/pwa/out/approvals/index.txt +14 -14
  69. package/apps/pwa/out/autopilot/index.html +1 -1
  70. package/apps/pwa/out/autopilot/index.txt +14 -14
  71. package/apps/pwa/out/board/index.html +1 -1
  72. package/apps/pwa/out/board/index.txt +14 -14
  73. package/apps/pwa/out/botlab/index.html +1 -1
  74. package/apps/pwa/out/botlab/index.txt +14 -14
  75. package/apps/pwa/out/bots/index.html +1 -1
  76. package/apps/pwa/out/bots/index.txt +14 -14
  77. package/apps/pwa/out/ceo/index.html +1 -1
  78. package/apps/pwa/out/ceo/index.txt +14 -14
  79. package/apps/pwa/out/chat/index.html +1 -1
  80. package/apps/pwa/out/chat/index.txt +14 -14
  81. package/apps/pwa/out/consult/index.html +1 -1
  82. package/apps/pwa/out/consult/index.txt +14 -14
  83. package/apps/pwa/out/control/index.html +1 -1
  84. package/apps/pwa/out/control/index.txt +14 -14
  85. package/apps/pwa/out/decisions/index.html +1 -0
  86. package/apps/pwa/out/decisions/index.txt +30 -0
  87. package/apps/pwa/out/design-check/index.html +1 -1
  88. package/apps/pwa/out/design-check/index.txt +14 -14
  89. package/apps/pwa/out/editor/index.html +1 -1
  90. package/apps/pwa/out/editor/index.txt +14 -14
  91. package/apps/pwa/out/exec/index.html +1 -1
  92. package/apps/pwa/out/exec/index.txt +14 -14
  93. package/apps/pwa/out/field/index.html +1 -1
  94. package/apps/pwa/out/field/index.txt +14 -14
  95. package/apps/pwa/out/healthz +1 -1
  96. package/apps/pwa/out/index.html +1 -1
  97. package/apps/pwa/out/index.txt +14 -14
  98. package/apps/pwa/out/inside/index.html +1 -1
  99. package/apps/pwa/out/inside/index.txt +14 -14
  100. package/apps/pwa/out/intake/index.html +1 -1
  101. package/apps/pwa/out/intake/index.txt +14 -14
  102. package/apps/pwa/out/live/index.html +1 -1
  103. package/apps/pwa/out/live/index.txt +14 -14
  104. package/apps/pwa/out/loops/index.html +1 -1
  105. package/apps/pwa/out/loops/index.txt +14 -14
  106. package/apps/pwa/out/market/index.html +1 -1
  107. package/apps/pwa/out/market/index.txt +14 -14
  108. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  109. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  110. package/apps/pwa/out/morning/index.html +1 -1
  111. package/apps/pwa/out/morning/index.txt +14 -14
  112. package/apps/pwa/out/observatory/index.html +1 -1
  113. package/apps/pwa/out/observatory/index.txt +14 -14
  114. package/apps/pwa/out/ops/checklist/index.html +1 -1
  115. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  116. package/apps/pwa/out/ops/release/index.html +1 -1
  117. package/apps/pwa/out/ops/release/index.txt +14 -14
  118. package/apps/pwa/out/ops/seats/index.html +1 -1
  119. package/apps/pwa/out/ops/seats/index.txt +14 -14
  120. package/apps/pwa/out/outputs/index.html +1 -1
  121. package/apps/pwa/out/outputs/index.txt +14 -14
  122. package/apps/pwa/out/reflection/index.html +1 -1
  123. package/apps/pwa/out/reflection/index.txt +15 -15
  124. package/apps/pwa/out/scheduler/index.html +1 -1
  125. package/apps/pwa/out/scheduler/index.txt +14 -14
  126. package/apps/pwa/out/sessions/index.html +1 -1
  127. package/apps/pwa/out/sessions/index.txt +14 -14
  128. package/apps/pwa/out/settings/devices/index.html +1 -1
  129. package/apps/pwa/out/settings/devices/index.txt +14 -14
  130. package/apps/pwa/out/settings/index.html +1 -1
  131. package/apps/pwa/out/settings/index.txt +14 -14
  132. package/apps/pwa/out/setup/done/index.html +1 -1
  133. package/apps/pwa/out/setup/done/index.txt +14 -14
  134. package/apps/pwa/out/setup/index.html +1 -1
  135. package/apps/pwa/out/setup/index.txt +14 -14
  136. package/apps/pwa/out/share/index.html +1 -1
  137. package/apps/pwa/out/share/index.txt +14 -14
  138. package/apps/pwa/out/showroom/index.html +1 -1
  139. package/apps/pwa/out/showroom/index.txt +14 -14
  140. package/apps/pwa/out/tasks/index.html +1 -1
  141. package/apps/pwa/out/tasks/index.txt +14 -14
  142. package/apps/pwa/out/term/index.html +1 -1
  143. package/apps/pwa/out/term/index.txt +14 -14
  144. package/apps/pwa/out/today/index.html +1 -1
  145. package/apps/pwa/out/today/index.txt +14 -14
  146. package/apps/pwa/out/trace/index.html +1 -1
  147. package/apps/pwa/out/trace/index.txt +14 -14
  148. package/apps/pwa/out/vault/index.html +1 -1
  149. package/apps/pwa/out/vault/index.txt +14 -14
  150. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  151. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  152. package/apps/pwa/out/workflows/index.html +1 -1
  153. package/apps/pwa/out/workflows/index.txt +14 -14
  154. package/apps/pwa/out/workspace/index.html +1 -1
  155. package/apps/pwa/out/workspace/index.txt +14 -14
  156. package/apps/pwa/out/worktrees/index.html +1 -1
  157. package/apps/pwa/out/worktrees/index.txt +14 -14
  158. package/package.json +2 -1
  159. package/samples/workflows/templates/approval-gated-deploy.yaml +30 -0
  160. package/samples/workflows/templates/chat-driven-research.yaml +35 -0
  161. package/samples/workflows/templates/discord-listen-and-respond.yaml +29 -0
  162. package/samples/workflows/templates/http-poll-then-alert.yaml +30 -0
  163. package/samples/workflows/templates/manual-quickstart.yaml +21 -0
  164. package/samples/workflows/templates/scheduled-daily-digest.yaml +30 -0
  165. package/samples/workflows/templates/webhook-deploy-hook.yaml +27 -0
  166. package/scripts/ci-public-export-import-gate.ts +7 -2
  167. package/scripts/ci-typecheck-changed.ts +21 -6
  168. package/scripts/discord-seat-probe.ts +3 -2
  169. package/scripts/docs/generate-cli-anatomy.ts +162 -0
  170. package/scripts/goals/authoring-metrics.ts +73 -0
  171. package/scripts/landing-heal/must-fix-match.ts +7 -7
  172. package/scripts/landing-heal/run-mission.ts +3 -2
  173. package/scripts/public-export-test-run.ts +37 -2
  174. package/scripts/release-loop/checklist-gate-node.ts +13 -11
  175. package/scripts/release-loop/cut-branch.ts +2 -1
  176. package/scripts/release-loop/cutoff.ts +4 -2
  177. package/scripts/release-loop/docs-land-node.ts +8 -1
  178. package/scripts/release-loop/docs-node.ts +1 -1
  179. package/scripts/release-loop/gate-node.ts +99 -14
  180. package/scripts/release-loop/node-verdict.ts +2 -1
  181. package/scripts/release-loop/npm-publish-node.ts +11 -7
  182. package/scripts/release-loop/ops-upgrade-node.ts +8 -1
  183. package/scripts/release-loop/preflight.ts +246 -0
  184. package/scripts/release-loop/publish-node.ts +8 -1
  185. package/scripts/release-loop/release-readiness.ts +11 -6
  186. package/scripts/release-loop/release-version.ts +48 -0
  187. package/scripts/release-loop/resume-release.ts +115 -0
  188. package/scripts/release-loop/unattended-release.ts +43 -7
  189. package/scripts/release-loop/version-node.ts +145 -40
  190. package/scripts/rhythm/daily.ts +25 -4
  191. package/scripts/skills/public-check.ts +62 -12
  192. package/scripts/test-deterministic.ts +51 -33
  193. package/scripts/test-diet/effectiveness.ts +62 -1
  194. package/scripts/test-diet/lib.ts +1 -1
  195. package/scripts/test-diet/node.ts +8 -1
  196. package/src/agent-mission/review-loop.ts +4 -3
  197. package/src/agent-substrate/pr-comment-meta.ts +20 -0
  198. package/src/autopilot/build/llm-conflict-merge.ts +28 -8
  199. package/src/briefing/brief-items.ts +86 -17
  200. package/src/cli/autopilot-cli.ts +30 -11
  201. package/src/cli/brief-cli.ts +36 -2
  202. package/src/cli/freeze-cli.ts +3 -2
  203. package/src/cli/intake-cli.ts +73 -1
  204. package/src/cli/notify-cli.ts +75 -0
  205. package/src/cli/provider-cli.ts +12 -2
  206. package/src/cli/release-cli.ts +101 -17
  207. package/src/cli/tasks-cli.ts +309 -3
  208. package/src/context-bus/condense.ts +19 -3
  209. package/src/context-bus/context-day.ts +77 -24
  210. package/src/context-bus/context-first.ts +33 -0
  211. package/src/discord-self-message.ts +20 -2
  212. package/src/discord-slash-wire.ts +1 -0
  213. package/src/domains/outbound-alert.ts +200 -27
  214. package/src/domains/outbound-log-probe.ts +16 -1
  215. package/src/domains/release-tool.ts +2 -2
  216. package/src/execution-loop/gates.ts +11 -2
  217. package/src/flow-loop/split.ts +8 -4
  218. package/src/git-fs/worktree.ts +23 -3
  219. package/src/harness/harness-cli-command.ts +10 -2
  220. package/src/harness/harness-queue.ts +388 -33
  221. package/src/harness/helper-scan.ts +10 -3
  222. package/src/hq/seats.ts +369 -0
  223. package/src/index.ts +133 -15
  224. package/src/intake-plane/adopt-gate.ts +49 -0
  225. package/src/intake-plane/check.ts +3 -0
  226. package/src/intake-plane/collect-github.ts +57 -25
  227. package/src/intake-plane/discord-seat-work.ts +8 -2
  228. package/src/intake-plane/requirement-funnel.ts +316 -0
  229. package/src/knowledge/know-find.ts +10 -1
  230. package/src/loops/budget.ts +36 -0
  231. package/src/loops/orchestrator/finish-rate.ts +73 -1
  232. package/src/loops/orchestrator/grid-place.ts +11 -0
  233. package/src/loops/orchestrator/tick.ts +295 -20
  234. package/src/maturity/feature-maturity.ts +3 -2
  235. package/src/nexus/api/decisions-route.ts +11 -1
  236. package/src/nexus/api/loop-edges.ts +69 -13
  237. package/src/nexus/api/ops-seats-sources.ts +2 -2
  238. package/src/nexus/api/workflow-templates.ts +3 -0
  239. package/src/oauth/codex.ts +38 -0
  240. package/src/onboarding/first-chat-gate.ts +29 -0
  241. package/src/release-loop/checklist.ts +104 -6
  242. package/src/release-loop/feature-store.ts +10 -9
  243. package/src/release-loop/landing-freeze.ts +6 -4
  244. package/src/release-loop/placement.ts +13 -3
  245. package/src/release-loop/rubric.ts +80 -0
  246. package/src/seat-dispatch/seat-ask.ts +79 -16
  247. package/src/seat-loop/seat-loop.ts +12 -6
  248. package/src/self-dev/ask-launch-flow.ts +24 -3
  249. package/src/self-dev/ask-path-candidates.ts +68 -0
  250. package/src/self-dev/dev-pipeline.ts +4 -0
  251. package/src/self-dev/draft-triage-rules.ts +1 -1
  252. package/src/self-dev/orchestrate-cli.ts +22 -0
  253. package/src/self-dev/round-pr-supersede.ts +244 -0
  254. package/src/self-dev/run-supervisor.ts +14 -0
  255. package/src/self-implement/follow-up-goals.ts +2 -2
  256. package/src/self-implement/gate-remote.ts +427 -0
  257. package/src/self-implement/goal-author.ts +8 -3
  258. package/src/self-implement/l8-merge-queue.ts +29 -5
  259. package/src/self-implement/orchestrator.ts +59 -12
  260. package/src/self-implement/prior-draft-findings.ts +4 -3
  261. package/src/skills/skill-feedback.ts +76 -0
  262. package/src/task-agent/actions.ts +39 -6
  263. package/src/task-agent/judge.ts +31 -5
  264. package/src/task-agent/mission.ts +423 -0
  265. package/src/task-agent/shadow.ts +90 -0
  266. package/src/task-agent/task-hand.ts +235 -0
  267. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +10 -0
  268. package/src/task-orchestrator/surfaces/self-implement-pod.ts +86 -18
  269. package/src/telegram-loops-command.ts +1 -1
  270. package/src/telegram.ts +5 -20
  271. package/src/user-config.ts +63 -3
  272. package/src/version/packed-revision.json +1 -1
  273. package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +0 -1
  274. package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +0 -1
  275. package/apps/pwa/out/_next/static/chunks/4614-21403aac730d4586.js +0 -1
  276. package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +0 -1
  277. package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +0 -1
  278. package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/6877-fe94e76a5841ee9b.js +0 -1
  280. package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +0 -1
  281. package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +0 -1
  282. package/apps/pwa/out/_next/static/chunks/8401-4ce1c42c8d89903f.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/8949-ecb4128a0858b911.js +0 -1
  284. package/apps/pwa/out/_next/static/chunks/899-f41be85e8a680651.js +0 -1
  285. package/apps/pwa/out/_next/static/chunks/app/botlab/page-6b8444914da74ca6.js +0 -1
  286. package/apps/pwa/out/_next/static/chunks/app/ceo/page-7110f732f45bf944.js +0 -1
  287. package/apps/pwa/out/_next/static/chunks/app/chat/page-9a41d65355fc25b9.js +0 -1
  288. package/apps/pwa/out/_next/static/chunks/app/control/page-41afec0b255ed2bb.js +0 -1
  289. package/apps/pwa/out/_next/static/chunks/app/inside/page-ab93b240f687d69a.js +0 -1
  290. package/apps/pwa/out/_next/static/chunks/app/layout-de43a186e5ba5683.js +0 -1
  291. package/apps/pwa/out/_next/static/chunks/app/live/page-8b08cafad27759c9.js +0 -1
  292. package/apps/pwa/out/_next/static/chunks/app/observatory/page-1ed1324b1dc36754.js +0 -1
  293. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-166745e51ac0c7b0.js +0 -1
  294. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a04260b8ccc968d4.js +0 -1
  295. package/apps/pwa/out/_next/static/chunks/app/page-1f50e3d4e13435f4.js +0 -1
  296. package/apps/pwa/out/_next/static/chunks/app/sessions/page-a9117d2cd3093d36.js +0 -1
  297. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-5a64cb5208c6a8fd.js +0 -1
  298. package/apps/pwa/out/_next/static/chunks/app/settings/page-1407ec83ea64c1f9.js +0 -1
  299. package/apps/pwa/out/_next/static/chunks/app/setup/page-6e878ebbe1737d00.js +0 -1
  300. package/apps/pwa/out/_next/static/chunks/app/showroom/page-81a4e7f8dbfe74cb.js +0 -1
  301. package/apps/pwa/out/_next/static/chunks/app/term/page-3ab59d19913b200e.js +0 -1
  302. package/apps/pwa/out/_next/static/chunks/app/today/page-bf85b2f5bf077925.js +0 -1
  303. package/apps/pwa/out/_next/static/chunks/app/vault/page-ab32a8f522a60371.js +0 -1
  304. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-fc2df261f8a2c7df.js +0 -1
  305. package/apps/pwa/out/_next/static/chunks/app/workflows/page-09461366af82ef13.js +0 -1
  306. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ea90a8ac73ce2749.js +0 -1
  307. package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +0 -1
  308. package/apps/pwa/out/_next/static/yRA5ur6DcPl0YHSRheH2O/_buildManifest.js +0 -1
  309. /package/apps/pwa/out/_next/static/{yRA5ur6DcPl0YHSRheH2O → P6YTVgzuZz5oTZhY4Din_}/_ssgManifest.js +0 -0
@@ -61,3 +61,39 @@ export function decideSpawn({ seat, running, caps, gate, injectedCap }: SpawnBud
61
61
  ? { allow: true, reason: 'within-cap', cap }
62
62
  : { allow: false, reason: 'seat-cap', cap };
63
63
  }
64
+
65
+ /** SEAT-CAP-STALE: why a run or a queue row is left out of a seat's running count (counting only — nothing is stopped). */
66
+ export type SeatCapExclusionReason = 'stale' | 'soft-stopped' | 'unknown-seat' | 'dead-row';
67
+ export const SEAT_CAP_STALE_RUN_MINUTES = 30;
68
+ export const SEAT_CAP_UNKNOWN_SEAT_CAP = 2;
69
+ /** A just-launched row has no process yet; it keeps its seat for this long. */
70
+ export const SEAT_CAP_LAUNCH_GRACE_MINUTES = 5;
71
+
72
+ export type SeatCapSubject =
73
+ | { kind: 'run'; seat?: OrchestratorSeat; progressAt?: number; stopReason?: string }
74
+ | { kind: 'row'; status: string; receipt: 'started' | 'finished' | 'not-started' | null; live: boolean; launchedAt?: number };
75
+
76
+ export type SeatCapExclusion = { reason: SeatCapExclusionReason; idleMin: number | null } | null;
77
+
78
+ /**
79
+ * Pure judgement: null = the subject holds a seat. A run without a seat goes to the unknown-seat bucket; a run whose
80
+ * supervisor already stopped (awaiting harvest) or whose last progress is older than staleRunMinutes is excluded; a
81
+ * launched row with no live process past the launch grace is a dead row. A run with no progress signal still counts.
82
+ */
83
+ export function seatCapExclusion(subject: SeatCapSubject, now: number,
84
+ { staleRunMinutes = SEAT_CAP_STALE_RUN_MINUTES, launchGraceMinutes = SEAT_CAP_LAUNCH_GRACE_MINUTES }:
85
+ { staleRunMinutes?: number; launchGraceMinutes?: number } = {}): SeatCapExclusion {
86
+ const minutes = (at: number | undefined): number | null =>
87
+ at !== undefined && Number.isFinite(at) ? Math.max(0, Math.floor((now - at) / 60_000)) : null;
88
+ if (subject.kind === 'row') {
89
+ if (subject.status !== 'launched' && subject.status !== 'launching') return null;
90
+ if (subject.live || subject.receipt === 'finished' || subject.receipt === 'not-started') return null;
91
+ const idle = minutes(subject.launchedAt);
92
+ return idle !== null && now - subject.launchedAt! >= launchGraceMinutes * 60_000 ? { reason: 'dead-row', idleMin: idle } : null;
93
+ }
94
+ const idle = minutes(subject.progressAt);
95
+ if (!subject.seat) return { reason: 'unknown-seat', idleMin: idle };
96
+ if (subject.stopReason) return { reason: 'soft-stopped', idleMin: idle };
97
+ if (idle !== null && now - subject.progressAt! >= staleRunMinutes * 60_000) return { reason: 'stale', idleMin: idle };
98
+ return null;
99
+ }
@@ -1,8 +1,9 @@
1
1
  import { execFileSync } from 'node:child_process';
2
- import { readdirSync, realpathSync, statSync } from 'node:fs';
2
+ import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
3
3
  import { join, resolve } from 'node:path';
4
4
  import { debug } from '../../debug/log.js';
5
5
  import { loadRunLedger, resolveFederatedRunLedgerTargets } from '../../self-implement/run-ledger.js';
6
+ import { withFileLockSync } from '../../storage/file-lock.js';
6
7
  import type { LogTarget } from '../../cli/logs-cli.js';
7
8
 
8
9
  const DAY_MS = 24 * 60 * 60 * 1_000;
@@ -24,6 +25,77 @@ export type FinishMetrics = {
24
25
  export type FinishAdvice = { finishSlots: number; launchSlots: number; state: 'healthy' | 'backlogged' | 'unknown'; reasons: string[] };
25
26
  export type FinishThresholds = { landingRate?: number; staleDrafts?: number; conflictRatio?: number };
26
27
 
28
+ export type FinishTrend = 'worsening' | 'improving' | 'flat' | 'unknown';
29
+ export type FinishTrendInput = Pick<FinishMetrics, 'landingRate' | 'staleDrafts' | 'conflictRatio'>;
30
+ export type FinishHistoryRow = {
31
+ hour: string; at: string; launched: number | null; landed: number | null;
32
+ landingRate: number | null; staleDrafts: number | null; conflictRatio: number | null; trend: FinishTrend;
33
+ };
34
+
35
+ /** FINISH-RATE 추세 — landingRate 는 내려가면 나쁨 · staleDrafts·conflictRatio 는 올라가면 나쁨.
36
+ * 양쪽 다 유한한 숫자인 지표만 비교한다(못 잰 값은 좋음도 나쁨도 아니다) · 임의 임계 없음.
37
+ * 나쁨 수 > 좋음 수 = worsening · 작으면 improving · 같으면 flat · 비교한 지표가 0 이면 unknown. */
38
+ export function finishTrend(previous: FinishTrendInput | null, current: FinishTrendInput): FinishTrend {
39
+ if (!previous) return 'unknown';
40
+ let compared = 0, worse = 0, better = 0;
41
+ for (const key of ['landingRate', 'staleDrafts', 'conflictRatio'] as const) {
42
+ const before = previous[key], after = current[key];
43
+ if (typeof before !== 'number' || !Number.isFinite(before) || typeof after !== 'number' || !Number.isFinite(after)) continue;
44
+ compared++;
45
+ const delta = key === 'landingRate' ? before - after : after - before;
46
+ if (delta > 0) worse++;
47
+ else if (delta < 0) better++;
48
+ }
49
+ if (compared === 0) return 'unknown';
50
+ return worse > better ? 'worsening' : worse < better ? 'improving' : 'flat';
51
+ }
52
+
53
+ export function finishHistoryPath(root: string): string {
54
+ return join(root, 'harness', 'finish-history.jsonl');
55
+ }
56
+
57
+ /** «한 시간 한 줄» 이력 — 같은 UTC 시간 줄이 이미 있으면 안 쓴다(null) · trend 는 지금보다 이른 가장 최근 시간 줄 대비.
58
+ * 읽을 수 없는 줄은 건너뛴다(이력 한 줄이 깨졌다고 측정을 멈추지 않는다). 쓰기 실패는 호출자에게 던진다. */
59
+ export function recordFinishHistory(root: string, metrics: FinishMetrics, now: Date): FinishHistoryRow | null {
60
+ const path = finishHistoryPath(root);
61
+ const hour = now.toISOString().slice(0, 13);
62
+ mkdirSync(join(root, 'harness'), { recursive: true });
63
+ // 확인과 덧붙이기를 한 잠금 안에서 — 같은 root 를 쓰는 두 틱이 같은 시간에 둘 다 «없음»을 보고 두 줄을 쓰지 않게.
64
+ const row = withFileLockSync(`${path}.lock`, (): FinishHistoryRow | null => {
65
+ const rows: FinishHistoryRow[] = [];
66
+ let text = '';
67
+ if (existsSync(path)) {
68
+ text = readFileSync(path, 'utf8');
69
+ for (const line of text.split('\n')) {
70
+ if (!line.trim()) continue;
71
+ try {
72
+ const parsed = JSON.parse(line) as FinishHistoryRow;
73
+ if (parsed && typeof parsed.hour === 'string') rows.push(parsed);
74
+ } catch { /* a broken line is skipped */ }
75
+ }
76
+ }
77
+ if (rows.some((existing) => existing.hour === hour)) return null;
78
+ // 앞 시간 = 지금보다 이른 줄 중 «가장 최근 UTC 시간»(파일 순서가 아니다 — 늦게 덧붙은 옛 시간 줄이 있을 수 있다) · 같은 시간이면 마지막 줄.
79
+ let previous: FinishHistoryRow | null = null;
80
+ for (const existing of rows) if (existing.hour < hour && (!previous || existing.hour >= previous.hour)) previous = existing;
81
+ const next: FinishHistoryRow = {
82
+ hour, at: now.toISOString(), launched: metrics.launched, landed: metrics.landed,
83
+ landingRate: metrics.landingRate, staleDrafts: metrics.staleDrafts, conflictRatio: metrics.conflictRatio,
84
+ trend: finishTrend(previous, metrics),
85
+ };
86
+ // 부분 쓰기로 끝 개행이 없으면 먼저 보정한다 — 새 줄이 깨진 꼬리에 붙어 둘 다 못 읽게 되지 않게.
87
+ appendFileSync(path, `${text && !text.endsWith('\n') ? '\n' : ''}${JSON.stringify(next)}\n`);
88
+ return next;
89
+ });
90
+ if (!row) return null;
91
+ try {
92
+ debug.log('loop.orchestrator', 'finish-history', {
93
+ hour, trend: row.trend, landingRate: row.landingRate, staleDrafts: row.staleDrafts, conflictRatio: row.conflictRatio,
94
+ });
95
+ } catch { /* the row is written; observation is fail-soft */ }
96
+ return row;
97
+ }
98
+
27
99
  type Start = { runId: string; startedAt: string };
28
100
  type StartObservation = { starts: readonly Start[]; unreadable: readonly { runId: string; reason: string }[]; unreadableDirectories?: number };
29
101
  type Pr = { number: number; headRefName: string; mergedAt?: string; createdAt?: string; mergeable?: string; labels?: { name: string }[] };
@@ -0,0 +1,11 @@
1
+ interface GridCell { kind?: string }
2
+ export interface GridHost { name: string; capabilities: ReadonlySet<string>; available: boolean }
3
+
4
+ /** A proposal only: the caller owns the host inventory and the actual launch. */
5
+ export function placeOnGrid(cell: GridCell, hosts: readonly GridHost[]): { host: string; reason?: never } | { host: null; reason: string } {
6
+ const kind = cell.kind ?? 'unknown';
7
+ const host = hosts.find(candidate => candidate.available && candidate.capabilities.has(kind));
8
+ if (host) return { host: host.name };
9
+ return { host: null, reason: hosts.some(candidate => candidate.capabilities.has(kind))
10
+ ? `no available host for ${kind}` : `no host with ${kind} capability` };
11
+ }
@@ -5,30 +5,36 @@ import { CardStore, taskCardsDir, type TaskCard } from '../../task-cards/card-st
5
5
  import { Database } from 'bun:sqlite';
6
6
  import { type ChecklistItem } from '../../release-loop/checklist.js';
7
7
  import { effectiveInstanceRoot } from '../../instance/resolve.js';
8
- import { getUserConfig } from '../../user-config.js';
8
+ import { getUserConfig, type OrchestratorLoopConfig } from '../../user-config.js';
9
9
  import { debug } from '../../debug/log.js';
10
10
  import { listSelfDevRuns, runSummaryLine, selfDevRunsDir } from '../../self-dev/run-store.js';
11
- import { planAction, runSeatLoopOnce, type SeatItem } from '../../seat-loop/seat-loop.js';
12
- import { harnessQueueOutcome } from '../../harness/harness-queue.js';
11
+ import { planAction, runSeatLoopOnce, type SeatDeps, type SeatItem } from '../../seat-loop/seat-loop.js';
12
+ import { addHarnessQueue, harnessQueueOutcome, setQueuedPoolHint } from '../../harness/harness-queue.js';
13
+ import { placeOnGrid, type GridHost } from './grid-place.js';
14
+ import { checkPodPool, parsePodPool, resolvePodPoolSpec } from '../../task-orchestrator/surfaces/pod-pool.js';
15
+ import { leaseKubectl } from '../../task-orchestrator/surfaces/pod-lease.js';
16
+ import { readTaskAgentState, TASK_SEATS, type HandTaskOptions, type TaskCard as TaskAgentCard, type HandTaskResult, type TaskLauncher, type TaskSeat } from '../../task-agent/task-hand.js';
13
17
 
14
18
  export type Window = '08' | '12' | '18';
15
19
  export type Node = 'intake' | 'split' | 'place' | 'delegate' | 'launch' | 'reconcile' | 'report';
16
20
  export type Mode = 'off' | 'shadow' | 'live';
17
21
  const SEAT_LOOP_IDS: Readonly<Record<string, string>> = { OP: 'op-seat', MK: 'cmo-seat', TC: 'tc-seat', UX: 'ux-seat' };
18
- export interface Cell { id: string; title: string; seat?: string; host?: string; version?: string; cardId: string; origin: 'candidate' | 'flow1a'; priority?: 'P0' | 'P1' | 'P2'; predecessors?: string[] }
22
+ export interface Cell { id: string; title: string; kind?: string; seat?: string; host?: string; gridHost?: boolean; version?: string; cardId: string; origin: 'candidate' | 'flow1a'; priority?: 'P0' | 'P1' | 'P2'; predecessors?: string[] }
19
23
  export interface TickState {
20
24
  runId: string; window: Window; mode: Mode; day: string;
21
25
  cards: TaskCard[]; skippedCards: Array<{ id: string; reason: string }>; cells: Cell[]; placed: number; unplaced?: number; delegated: number; wouldDelegate: number; rebalanced?: number; rebalanceBlocked?: Array<{ id: string; reason: string }>;
22
26
  reconciled: { reached: number; progressing: number; blocked: number; unknown: number };
23
27
  runSummaries?: string[];
24
28
  launched?: Array<{ seat: string; status: string; key?: string; queueId?: string; runId?: string }>;
29
+ /** ORCH-TA-HAND — cells handed to TASK-AGENT this run (card id · mode · launched). */
30
+ handed?: Array<{ key: string; cell: string; cardId: string; mode: 'shadow' | 'live'; launched: boolean }>;
25
31
  queueOutcomes?: Array<{ queueId: string; key?: string; outcome: string }>;
26
32
  steps: Array<{ node: Node; reason: string; count: number; targetLoopId?: string }>;
27
33
  nodes: Partial<Record<Node, 'ok' | 'skipped'>>;
28
34
  missing: string[];
29
35
  }
30
36
  /** FLOW1a contract: with `{ shadow: true }` the splitter only proposes cells and writes nothing (cards, ledgers, LLM side effects excepted). */
31
- export type SplitAdapter = (card: TaskCard, opts?: { shadow?: boolean }) => Promise<Array<{ id: string; title: string; seat?: string; host?: string }>> | Array<{ id: string; title: string; seat?: string; host?: string }>;
37
+ export type SplitAdapter = (card: TaskCard, opts?: { shadow?: boolean }) => Promise<Array<{ id: string; title: string; kind?: string; seat?: string; host?: string }>> | Array<{ id: string; title: string; kind?: string; seat?: string; host?: string }>;
32
38
  /** RELPLAN1 `placeCell(input: PlacementCell)` — owner·priority·predecessors are required; the result carries no seat. */
33
39
  export type PlacementInput = { id: string; title: string; owner: string; priority: 'P0' | 'P1' | 'P2'; predecessors: string[] };
34
40
  export type PlaceAdapter = (cell: PlacementInput, deps?: { dryRun?: boolean; now?: Date }) => Promise<{ version?: string } | null> | { version?: string } | null;
@@ -36,12 +42,50 @@ export type RebalanceAdapter = (version: string) => Promise<{ decisions: unknown
36
42
  export interface TickDeps {
37
43
  root?: string; now?: Date; mode?: Mode; runId?: string; window?: Window;
38
44
  cards?: () => TaskCard[]; split?: SplitAdapter; placeCell?: PlaceAdapter;
39
- seatTurn?: (seat: string, root: string) => Promise<{ status: string; queueId?: string; runId?: string; item?: { source: string; id: string } }>;
45
+ gridHosts?: readonly GridHost[];
46
+ seatTurn?: (seat: string, root: string, host?: string, options?: Pick<SeatDeps, 'enqueue'>) => Promise<{ status: string; queueId?: string; runId?: string; item?: { source: string; id: string } }>;
40
47
  queueOutcome?: (queueId: string, root: string) => 'pending' | 'unknown' | 'retryable' | 'succeeded';
48
+ /** 발사 전 대기열 항목의 풀 힌트 갱신(주입 안 하면 harness-queue `setQueuedPoolHint`). */
49
+ setPoolHint?: (queueId: string, poolHint: string | undefined, root: string) => Promise<boolean>;
41
50
  loadAdapter?: (path: string, name: string) => Promise<unknown>;
42
51
  checklist?: (version: string) => Pick<ChecklistItem, 'id' | 'status'>[];
43
52
  observe?: (event: string, data: { loopId: string; runId: string; window: Window; node: Node; reason: string; count: number; targetLoopId?: string }) => void;
44
53
  print?: (line: string) => void;
54
+ /** ORCH-TA-HAND seams — absent = config `loops.orchestrator.handToTaskAgent(Cell)` · real `handTask` · `defaultTaskLauncher`. */
55
+ handToTaskAgent?: 'off' | 'shadow' | 'live';
56
+ handToTaskAgentCell?: string;
57
+ handTask?: (opts: HandTaskOptions) => Promise<HandTaskResult>;
58
+ taskLauncher?: TaskLauncher;
59
+ }
60
+
61
+ /** ORCH-TA-HAND ledger — one row per handed `orch:<card>:<cell>` key so a cell is handed once across ticks and processes. */
62
+ function handedLedger(root: string): string { return join(root, 'loop', 'orchestrator', 'handed.jsonl'); }
63
+ type HandedRow = { key: string; cell: string; cardId: string; mode: 'shadow' | 'live'; launched: boolean; runId?: string };
64
+ /** Completed hands (`status: 'handed'`) by ledger key — a re-entered run restores its `handed[]` from these. */
65
+ function handedRows(path: string): Map<string, HandedRow> {
66
+ const rows = new Map<string, HandedRow>();
67
+ if (!existsSync(path)) return rows;
68
+ for (const line of readFileSync(path, 'utf8').split('\n')) {
69
+ if (!line.trim()) continue;
70
+ let row: Partial<HandedRow> & { status?: unknown };
71
+ try { row = JSON.parse(line) as typeof row; } catch { throw new Error('invalid orchestrator handed ledger'); }
72
+ if (row.status === 'handed' && typeof row.key === 'string' && typeof row.cardId === 'string' && typeof row.cell === 'string'
73
+ && (row.mode === 'shadow' || row.mode === 'live')) rows.set(row.key, { key: row.key, cell: row.cell, cardId: row.cardId, mode: row.mode, launched: row.launched === true, ...(typeof row.runId === 'string' ? { runId: row.runId } : {}) });
74
+ }
75
+ return rows;
76
+ }
77
+ function handedKeys(path: string): Set<string> {
78
+ if (!existsSync(path)) return new Set();
79
+ const keys = new Set<string>();
80
+ for (const line of readFileSync(path, 'utf8').split('\n')) {
81
+ if (!line.trim()) continue;
82
+ let row: { key?: unknown; status?: unknown };
83
+ try { row = JSON.parse(line) as typeof row; } catch { throw new Error('invalid orchestrator handed ledger'); }
84
+ if (typeof row.key !== 'string') continue;
85
+ // A released claim (a shadow hand that failed before its card) frees the key again — the last row decides.
86
+ if (row.status === 'released') keys.delete(row.key); else keys.add(row.key);
87
+ }
88
+ return keys;
45
89
  }
46
90
 
47
91
  export function kstWindow(now: Date): Window {
@@ -122,10 +166,138 @@ function readChecklist(root: string, version: string): Pick<ChecklistItem, 'id'
122
166
  try { return db.query('SELECT feature_id AS id, status FROM assignments WHERE version = ?').all(version) as Pick<ChecklistItem, 'id' | 'status'>[]; }
123
167
  finally { db.close(); }
124
168
  }
169
+ /** 명시 인벤토리: 없으면 undefined, «있는데 못 읽으면» null — null 은 풀 명부로 대체하지 않는다. */
170
+ function readGridHosts(): readonly GridHost[] | null | undefined {
171
+ const supplied = process.env.ELANOUS_GRID_HOSTS;
172
+ if (supplied === undefined) return undefined;
173
+ try {
174
+ const inventory: unknown = JSON.parse(supplied);
175
+ if (!Array.isArray(inventory) || !inventory.every(row => row && typeof row === 'object' &&
176
+ typeof row.name === 'string' && row.name.trim() !== '' && Array.isArray(row.capabilities) &&
177
+ row.capabilities.every((capability: unknown) => typeof capability === 'string') &&
178
+ typeof row.available === 'boolean')) return null;
179
+ return inventory.map(row => ({ name: row.name as string, capabilities: new Set<string>(row.capabilities), available: row.available as boolean }));
180
+ } catch { return null; }
181
+ }
182
+ /** 주입 > 명시 인벤토리 > 풀 명부. 명시 인벤토리를 못 읽으면 제안하지 않는다(기존 경로 그대로). */
183
+ function resolveGridHosts(deps: TickDeps, observe: (event: string, reason: string, count: number) => void): readonly GridHost[] | undefined {
184
+ if (deps.gridHosts) return deps.gridHosts;
185
+ const supplied = readGridHosts();
186
+ if (supplied === null) { observe('node-missing', 'grid-inventory-unreadable', 0); return undefined; }
187
+ return supplied ?? gridHostsFromPodPool();
188
+ }
189
+ function preferredPoolSpec(host: string): string | undefined {
190
+ const config = getUserConfig();
191
+ const spec = resolvePodPoolSpec(undefined, process.env, () => config.harness?.podPool ?? config.pod?.pool);
192
+ if (!spec) return undefined;
193
+ try {
194
+ const members = parsePodPool(spec);
195
+ const member = members.find(candidate => (candidate.sshHost ?? candidate.context) === host);
196
+ if (!member) return undefined;
197
+ const parts = spec.split(',').map(part => part.trim());
198
+ const preferred = parts.find(part => part === member.context ||
199
+ ['@', ':', '#'].some(separator => part.startsWith(`${member.context}${separator}`)));
200
+ return preferred ? [preferred, ...parts.filter(part => part !== preferred)].join(',') : undefined;
201
+ } catch { return undefined; }
202
+ }
203
+ function gridHostsFromPodPool(): readonly GridHost[] | undefined {
204
+ try {
205
+ const config = getUserConfig();
206
+ const spec = resolvePodPoolSpec(undefined, process.env, () => config.harness?.podPool ?? config.pod?.pool);
207
+ if (!spec) return undefined;
208
+ const members = parsePodPool(spec);
209
+ const checked = checkPodPool(members, leaseKubectl);
210
+ const ready = new Set(checked.ready.map(member => member.context));
211
+ return members.map(member => ({
212
+ name: member.sshHost ?? member.context,
213
+ capabilities: new Set(['pod']),
214
+ available: ready.has(member.context),
215
+ }));
216
+ } catch { return undefined; }
217
+ }
125
218
  function journal(root: string): string { return join(root, 'seat-requests', 'requests.jsonl'); }
126
- function readRequests(root: string): Array<{ key: string; status: string }> {
219
+ function readRequests(root: string): Array<{ key: string; status: string; seat?: string; kind?: string; host?: string; gridHost?: boolean }> {
127
220
  const path = journal(root);
128
- return existsSync(path) ? readFileSync(path, 'utf8').split('\n').filter(Boolean).map(line => JSON.parse(line) as { key: string; status: string }) : [];
221
+ return existsSync(path) ? readFileSync(path, 'utf8').split('\n').filter(Boolean).map(line => JSON.parse(line) as { key: string; status: string; seat?: string; kind?: string; host?: string; gridHost?: boolean }) : [];
222
+ }
223
+ function refreshGridHosts(cells: Cell[], hosts: readonly GridHost[] | undefined, mode: Mode,
224
+ root: string, observe: (event: string, reason: string, count: number, targetLoopId?: string) => void): void {
225
+ if (hosts === undefined) return;
226
+ for (const cell of cells) {
227
+ if (mode !== 'live' || cell.origin !== 'flow1a' || !cell.seat || !SEAT_LOOP_IDS[cell.seat]) {
228
+ observe('exchange', `would-host:${cell.id}:${placeOnGrid(cell, hosts).host ?? 'null'}`, 1);
229
+ continue;
230
+ }
231
+ const path = journal(root);
232
+ const key = `orch:${cell.cardId}:${cell.id}`;
233
+ // 의뢰가 이미 대기열을 떠났으면(발사·완료) 제안만 관측하고 칸의 호스트는 그 행의 확정 호스트로 되돌린다.
234
+ const keepLaunched = (row: { host?: string; gridHost?: boolean }): void => {
235
+ if (row.host) cell.host = row.host; else delete cell.host;
236
+ if (row.gridHost) cell.gridHost = true; else delete cell.gridHost;
237
+ };
238
+ let changedFrom: string | undefined;
239
+ mkdirSync(dirname(path), { recursive: true });
240
+ // 상태 확인 · 칸 변경 · 행 갱신은 모두 같은 잠금 안의 한 결정이다 — 다른 틱이 그 사이 행을 만들거나 바꿔도 덮지 않는다.
241
+ withFileLockSync(`${path}.lock`, () => {
242
+ const current = readRequests(root).filter(row => row.key === key).at(-1);
243
+ // 호스트 결정도 잠금 안 — 상태 확인과 같은 순간의 명부로 고른다.
244
+ const choice = placeOnGrid(cell, hosts);
245
+ observe('exchange', `would-host:${cell.id}:${choice.host ?? 'null'}`, 1);
246
+ if (current && current.status !== 'queued') { keepLaunched(current); return; }
247
+ if (choice.host) { cell.host = choice.host; cell.gridHost = true; }
248
+ else if (cell.gridHost || current?.gridHost) { delete cell.host; delete cell.gridHost; }
249
+ if (!current || (current.host === cell.host && current.gridHost === cell.gridHost && (current.kind === cell.kind || !cell.kind))) return;
250
+ if (current.host !== cell.host) changedFrom = current.host;
251
+ const updated = { ...current };
252
+ delete updated.host;
253
+ delete updated.gridHost;
254
+ if (cell.host) updated.host = cell.host;
255
+ if (cell.gridHost) updated.gridHost = true;
256
+ if (cell.kind) updated.kind = cell.kind;
257
+ appendFileSync(path, `${JSON.stringify(updated)}\n`);
258
+ });
259
+ if (changedFrom !== undefined) observe('exchange', `rehost:${cell.id}:${changedFrom}→${cell.host ?? 'none'}`, 1, SEAT_LOOP_IDS[cell.seat]);
260
+ }
261
+ }
262
+ /**
263
+ * 재배치된 의뢰가 앞 틱에서 이미 대기열에 들어갔다면(아직 발사 전) 그 항목의 풀 힌트도 의뢰 행의 새 호스트로 맞춘다.
264
+ * 의뢰 키 ↔ 대기열 id 는 live 틱 상태들의 `launched` 에서 찾는다(날짜를 넘겨 대기 중인 항목도). 의뢰 행이 아직 queued 일 때만 옮긴다.
265
+ */
266
+ async function syncQueuedPoolHints(root: string, current: NonNullable<TickState['launched']>, deps: TickDeps,
267
+ observe: (event: string, reason: string, count: number) => void): Promise<void> {
268
+ const dir = join(root, 'loop', 'orchestrator');
269
+ const turns = [...current];
270
+ if (existsSync(dir)) for (const name of readdirSync(dir).filter(file => /^[a-zA-Z0-9._-]+\.json$/.test(file))) {
271
+ try {
272
+ const other = JSON.parse(readFileSync(join(dir, name), 'utf8')) as TickState;
273
+ if (other.mode === 'live') turns.push(...(other.launched ?? []));
274
+ } catch { /* 깨진 상태 파일은 건너뛴다 */ }
275
+ }
276
+ const latest = new Map(readRequests(root).map(row => [row.key, row]));
277
+ const seen = new Set<string>();
278
+ for (const turn of turns) {
279
+ if (!turn.queueId || !turn.key || seen.has(turn.queueId)) continue;
280
+ seen.add(turn.queueId);
281
+ const row = latest.get(turn.key);
282
+ if (!row || row.kind !== 'pod' || row.status !== 'queued') continue;
283
+ // GRID 가 고른 행이면 그 호스트 쪽 힌트로, GRID 가 호스트를 거둔 행(둘 다 막힘)이면 GRID 가 넣었던 힌트만 지운다.
284
+ // GRID 와 무관한 기존 pod 의뢰의 힌트(ORCH1·명시 풀)는 건드리지 않는다.
285
+ const gridChosen = !!(row.gridHost && row.host);
286
+ if (!gridChosen && row.host) continue;
287
+ const hint = gridChosen ? preferredPoolSpec(row.host!) : undefined;
288
+ if (gridChosen && !hint) continue;
289
+ const key = turn.key;
290
+ // 갱신 직전 대기열 잠금 안에서: 의뢰가 아직 queued 이고 행의 호스트가 계산 때와 같으며,
291
+ // 지우는 경우엔 지금 대기열 힌트가 GRID 가 만든 꼴일 때만.
292
+ const stillQueued = (item: { poolHint?: string; poolHintSource?: 'grid' }): boolean => {
293
+ const now = readRequests(root).filter(r => r.key === key).at(-1);
294
+ if (now?.status !== 'queued' || now.host !== row.host || now.gridHost !== row.gridHost) return false;
295
+ // 지우는 쪽은 GRID 가 소유한 힌트만(소유 기록 확인) — 같은 문자열이라도 사람이 넣은 힌트는 남긴다.
296
+ return gridChosen || item.poolHintSource === 'grid';
297
+ };
298
+ if (await (deps.setPoolHint ?? ((id: string, value: string | undefined, stateRoot: string) => setQueuedPoolHint(id, value, { root: stateRoot }, stillQueued)))(turn.queueId, hint, root))
299
+ observe('exchange', `rehost-queue:${turn.key}:${hint ?? 'none'}`, 1);
300
+ }
129
301
  }
130
302
  function stateFile(root: string, runId: string): string {
131
303
  if (!/^[a-zA-Z0-9._-]+$/.test(runId) || runId === '.' || runId === '..') throw new Error('Invalid orchestrator runId');
@@ -215,7 +387,7 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
215
387
  const cells = split ? await split(card, { shadow: mode === 'shadow' }) : [{ id: card.id, title: card.title }];
216
388
  for (const cell of cells) {
217
389
  if (!cell.id?.trim() || !cell.title?.trim()) throw new Error('invalid split cell');
218
- state.cells.push({ cardId: card.id, id: cell.id, title: cell.title, origin: split ? 'flow1a' : 'candidate', ...(cell.seat ? { seat: cell.seat } : {}), ...(cell.host !== undefined ? { host: cell.host } : {}) });
390
+ state.cells.push({ cardId: card.id, id: cell.id, title: cell.title, origin: split ? 'flow1a' : 'candidate', ...(cell.kind !== undefined ? { kind: cell.kind } : {}), ...(cell.seat ? { seat: cell.seat } : {}), ...(cell.host !== undefined ? { host: cell.host } : {}) });
219
391
  }
220
392
  observe('exchange', `split:${card.id}`, cells.length);
221
393
  if (split && cells.length && mode === 'live') {
@@ -226,6 +398,9 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
226
398
  }
227
399
  }
228
400
  } else if (node === 'place') {
401
+ const gridHosts = resolveGridHosts(deps, observe);
402
+ refreshGridHosts(state.cells, gridHosts, mode, root, observe);
403
+ if (mode === 'live') await syncQueuedPoolHints(root, state.launched ?? [], deps, observe);
229
404
  const place = deps.placeCell ?? await adapter<PlaceAdapter>('../../release-loop/placement.js', 'placeCell');
230
405
  const available = typeof place === 'function';
231
406
  if (!available) { state.missing.push('relplan1-absent'); observe('node-missing', 'relplan1-absent', state.cells.length); }
@@ -251,13 +426,94 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
251
426
  }
252
427
  } else if (node === 'delegate') {
253
428
  const path = journal(root);
429
+ // 모드와 칸 필터는 따로 푼다 — 주입으로 모드만 줘도 설정의 칸 필터는 그대로 먹는다.
430
+ const handConfig: Partial<OrchestratorLoopConfig> = deps.handToTaskAgent !== undefined && deps.handToTaskAgentCell !== undefined ? {} : getUserConfig().loops?.orchestrator ?? {};
431
+ // live 로 넘긴 칸은 지금 설정(off·칸 필터)과 무관하게 자리 경로로 다시 안 간다 — 발사 주체가 둘이 되지 않게.
432
+ // ⭐ 넘김 claim 과 자리 저널 기록은 «같은 잠금»(자리 저널 잠금) 아래에서 서로를 다시 읽는다 — 동시 틱이 낡은 값을 보지 않게.
433
+ const ledger = handedLedger(root);
434
+ const liveHanded = handedKeys(ledger);
254
435
  const keys = new Set(readRequests(root).map(row => row.key));
436
+ // 넘김 뒤 상태 저장 전에 죽은 같은 run 이 다시 돌면 원장에서 handed[] 를 되살린다.
437
+ const ledgerHanded = handedRows(ledger);
438
+ const restoreHanded = (handKey: string, key: string, rows: Map<string, HandedRow> = handedRows(ledger)): void => {
439
+ const row = rows.get(handKey);
440
+ // 이 run 이 넘긴 것만 되살린다 — 다른 run 의 넘김을 «이 run 에서 넘겼다»로 적지 않게.
441
+ if (!row || row.runId !== runId || (state.handed ?? []).some(entry => entry.key === key && entry.mode === row.mode)) return;
442
+ (state.handed ??= []).push({ key, cell: row.cell, cardId: row.cardId, mode: row.mode, launched: row.launched });
443
+ };
444
+ const journalHas = (key: string): boolean => {
445
+ if (!existsSync(path)) return false;
446
+ return readFileSync(path, 'utf8').split('\n').some(line => {
447
+ if (!line) return false;
448
+ try { return (JSON.parse(line) as { key?: string }).key === key; } catch { throw new Error('invalid seat request journal'); }
449
+ });
450
+ };
255
451
  for (const cell of state.cells) {
256
452
  if (cell.origin !== 'flow1a' || !cell.seat) continue;
257
453
  const targetLoopId = SEAT_LOOP_IDS[cell.seat];
258
454
  if (!targetLoopId) continue;
259
455
  const key = `orch:${cell.cardId}:${cell.id}`;
456
+ // 저널 키로 건너뛰기 «전에» 원장에서 복구한다 — shadow 넘김 ⊕ 자리 기록 뒤 상태 저장 전에 죽은 run 도 handed[] 를 되찾게.
457
+ restoreHanded(key, key, ledgerHanded);
458
+ restoreHanded(`shadow:${key}`, key, ledgerHanded);
260
459
  if (keys.has(key)) continue;
460
+ if (liveHanded.has(key)) { keys.add(key); observe('exchange', `handed-to-task-agent:${key}`, 1, 'task-agent'); continue; }
461
+ // ORCH-TA-HAND: hand the picked cell to TASK-AGENT. shadow = card only (the seat path below is unchanged);
462
+ // live (tick live ⊕ hand live) = launch through TASK-AGENT and skip the seat journal so the cell launches once.
463
+ const handMode = deps.handToTaskAgent ?? handConfig.handToTaskAgent ?? 'off';
464
+ const handCell = deps.handToTaskAgentCell ?? handConfig.handToTaskAgentCell;
465
+ if (handMode !== 'off' && (!handCell || handCell === cell.id) && (TASK_SEATS as readonly string[]).includes(cell.seat)) {
466
+ // live 는 tick 도 live 이고 배치(유효한 판)가 된 칸만 — 나머지는 카드만(shadow · 배치 전 칸도 표본이 된다).
467
+ const handLive = handMode === 'live' && mode === 'live' && validVersion(cell.version);
468
+ // shadow 표본은 따로 센다 — 자리 경로로 아직 안 간 칸(shadow 틱·배치 전)이 나중에 live 로 바뀌면 그때 뜬다.
469
+ // ⚠️ 이미 자리 저널에 들어간 칸은 live 로 바꿔도 다시 안 넘긴다(keys.has 에서 끝) — 한 칸의 발사 주체는 하나다(의도).
470
+ const handKey = handLive ? key : `shadow:${key}`;
471
+ mkdirSync(dirname(ledger), { recursive: true });
472
+ mkdirSync(dirname(path), { recursive: true });
473
+ // Claim under the seat-journal lock — the same lock the seat append takes, so a live claim and a seat row never both land.
474
+ const claimed = withFileLockSync(`${path}.lock`, () => {
475
+ if (handedKeys(ledger).has(handKey)) return false;
476
+ if (handLive && journalHas(key)) return false;
477
+ appendFileSync(ledger, `${JSON.stringify({ key: handKey, cell: cell.id, seat: cell.seat, mode: handLive ? 'live' : 'shadow', status: 'claimed', runId, at: now.toISOString() })}\n`);
478
+ return true;
479
+ });
480
+ if (claimed) {
481
+ try {
482
+ const result = await (deps.handTask ?? (async (opts: HandTaskOptions) => (await import('../../task-agent/task-hand.js')).handTask(opts)))({
483
+ text: cell.title, seat: cell.seat as TaskSeat, checklistId: cell.id, live: handLive, now: () => now,
484
+ statePath: join(root, 'task-agent-actions.json'),
485
+ ...(handLive ? { launcher: deps.taskLauncher ?? (async (args: string[]) => (await import('../../cli/tasks-cli.js')).defaultTaskLauncher(args)) } : {}),
486
+ });
487
+ withFileLockSync(`${path}.lock`, () => appendFileSync(ledger, `${JSON.stringify({ key: handKey, cell: cell.id, cardId: result.card.id, mode: result.mode, launched: result.launched, status: 'handed', runId, at: now.toISOString() })}\n`));
488
+ (state.handed ??= []).push({ key, cell: cell.id, cardId: result.card.id, mode: result.mode, launched: result.launched });
489
+ observe('hand-to-task-agent', `${key}:${result.card.id}:${result.mode}${result.launched ? ':launched' : ''}`, 1, 'task-agent');
490
+ if (handLive) { keys.add(key); continue; }
491
+ } catch (error) {
492
+ observe('hand-to-task-agent-failed', `${key}:${String(error).slice(0, 160)}`, 1, 'task-agent');
493
+ // A failed live hand stays claimed (no relaunch loop) and does not fall back to the seat path — one launcher per cell.
494
+ if (handLive) { keys.add(key); continue; }
495
+ // A failed shadow hand launched nothing — release the claim so a later tick can write the card,
496
+ // but only when no card for this cell was written (a failure after the card must not make a second one).
497
+ let cardWritten = true;
498
+ try {
499
+ const storePath = join(root, 'task-agent-actions.json');
500
+ if (!existsSync(storePath)) cardWritten = false;
501
+ else {
502
+ // 상태를 믿을 수 있을 때만 «카드 0장»으로 읽는다 — tasks 가 없거나 꼴이 틀리면 claim 을 남긴다.
503
+ const tasks = readTaskAgentState<{ tasks?: unknown }>(storePath).tasks;
504
+ if (tasks && typeof tasks === 'object' && !Array.isArray(tasks)
505
+ && Object.values(tasks).every(card => card && typeof card === 'object' && !Array.isArray(card))) {
506
+ cardWritten = Object.values(tasks as Record<string, TaskAgentCard>).some(card => card.checklistId === cell.id && card.text === cell.title);
507
+ }
508
+ }
509
+ } catch { cardWritten = true; /* unreadable store → keep the claim (never risk a duplicate) */ }
510
+ if (!cardWritten) withFileLockSync(`${path}.lock`, () => appendFileSync(ledger, `${JSON.stringify({ key: handKey, cell: cell.id, status: 'released', runId, reason: String(error).slice(0, 160), at: now.toISOString() })}\n`));
511
+ }
512
+ } else {
513
+ restoreHanded(handKey, key);
514
+ if (handLive) { keys.add(key); continue; }
515
+ }
516
+ }
261
517
  if (!validVersion(cell.version)) {
262
518
  if (mode === 'shadow') { keys.add(key); state.wouldDelegate++; observe('would-delegate', `${key} (unplaced)`, 1, targetLoopId); }
263
519
  continue;
@@ -267,22 +523,23 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
267
523
  mkdirSync(dirname(path), { recursive: true });
268
524
  // TC review must-fix ①: the seat-requests journal contract needs receiptId, and appends are serialized under the
269
525
  // same lock as the traffic node — an invalid row stops request intake for every seat.
270
- const appended = withFileLockSync(`${path}.lock`, () => {
271
- const existing = existsSync(path) ? readFileSync(path, 'utf8') : '';
272
- if (existing.split('\n').some(line => {
273
- if (!line) return false;
274
- try { return (JSON.parse(line) as { key?: string }).key === key; } catch { throw new Error('invalid seat request journal'); }
275
- })) return false;
276
- appendFileSync(path, `${JSON.stringify({ key, receiptId: key, seat: cell.seat, ...(cell.host !== undefined ? { host: cell.host } : {}), loopId: targetLoopId, text: cell.title, status: 'queued', queuedAt: now.toISOString(), source: 'orchestrator', cell: cell.id, version: cell.version })}\n`);
526
+ const appended = withFileLockSync(`${path}.lock`, (): boolean | 'handed' => {
527
+ if (journalHas(key)) return false;
528
+ // 잠금 안에서 다시 본다 — 동시 틱이 이 칸을 방금 live 로 넘겼으면 자리 경로로 보내지 않는다.
529
+ if (handedKeys(ledger).has(key)) return 'handed';
530
+ appendFileSync(path, `${JSON.stringify({ key, receiptId: key, seat: cell.seat, ...(cell.kind !== undefined ? { kind: cell.kind } : {}), ...(cell.host !== undefined ? { host: cell.host } : {}), ...(cell.gridHost ? { gridHost: true } : {}), loopId: targetLoopId, text: cell.title, status: 'queued', queuedAt: now.toISOString(), source: 'orchestrator', cell: cell.id, version: cell.version })}\n`);
277
531
  return true;
278
532
  });
533
+ if (appended === 'handed') { observe('exchange', `seat-suppressed-handed:${key}`, 1, 'task-agent'); continue; }
279
534
  if (!appended) continue;
280
535
  state.delegated++;
281
536
  observe('exchange', 'queued', 1, targetLoopId);
282
537
  }
283
538
  } else if (node === 'launch') {
539
+ // shadow 는 저널을 읽지 않는다 — 깨진 live 저널이 관측 전용 틱을 넘어뜨리지 않게.
540
+ const latestRequests = new Map(mode === 'live' ? readRequests(root).map(row => [row.key, row]) : []);
284
541
  const eligible = state.cells.filter(cell => cell.origin === 'flow1a' && cell.seat && SEAT_LOOP_IDS[cell.seat]
285
- && validVersion(cell.version) && (mode === 'shadow' || readRequests(root).some(row => row.key === `orch:${cell.cardId}:${cell.id}` && row.status === 'queued')));
542
+ && validVersion(cell.version) && (mode === 'shadow' || latestRequests.get(`orch:${cell.cardId}:${cell.id}`)?.status === 'queued'));
286
543
  const seats = [...new Set(eligible.map(cell => cell.seat!))];
287
544
  state.launched = [];
288
545
  for (const seat of seats) {
@@ -293,9 +550,23 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
293
550
  state.launched.push({ seat, key: item.id, status: action.kind === 'harness' ? 'would-launch' : `would-${action.kind}` });
294
551
  observe('exchange', `would-${action.kind}:${seat}`, 1, SEAT_LOOP_IDS[seat]);
295
552
  } else {
296
- const turn = await (deps.seatTurn ?? ((assigned: string, stateRoot: string) => runSeatLoopOnce(assigned, {
297
- root: stateRoot, config: { ...getUserConfig().loops?.seat, mode: 'live-safe', seats: [assigned] },
298
- })))(seat, root);
553
+ const selected = eligible.find(cell => cell.seat === seat && latestRequests.get(`orch:${cell.cardId}:${cell.id}`)?.status === 'queued');
554
+ const host = selected && latestRequests.get(`orch:${selected.cardId}:${selected.id}`)?.host;
555
+ const pools = new Map<string, string>([...latestRequests].flatMap(([key, row]) => {
556
+ if (row.status !== 'queued' || row.seat !== seat || row.kind !== 'pod' || !row.gridHost || !row.host || !key.startsWith('orch:')) return [];
557
+ const pool = preferredPoolSpec(row.host);
558
+ // 풀 힌트는 «설정된 풀»의 순서 바꾸기다 — 고른 호스트가 풀에 없으면 힌트를 지어내지 않고 그 사실을 남긴다.
559
+ if (!pool) observe('exchange', `grid-host-unmapped:${key}:${row.host}`, 1, SEAT_LOOP_IDS[seat]);
560
+ return pool ? [[key, pool] as const] : [];
561
+ }));
562
+ const enqueue: SeatDeps['enqueue'] | undefined = pools.size
563
+ ? (queuedSeat, text, queuedRoot, idempotencyKey, item) => addHarnessQueue({ seat: queuedSeat, say: text, idempotencyKey,
564
+ ...(item.source === 'request' && pools.has(item.id) ? { poolHint: pools.get(item.id), poolHintSource: 'grid' as const } : {}) }, { root: queuedRoot })
565
+ : undefined;
566
+ const options = enqueue ? { enqueue } : undefined;
567
+ const turn = await (deps.seatTurn ?? ((assigned: string, stateRoot: string, _preferred?: string, overrides?: Pick<SeatDeps, 'enqueue'>) =>
568
+ runSeatLoopOnce(assigned, { root: stateRoot,
569
+ config: { ...getUserConfig().loops?.seat, mode: 'live-safe', seats: [assigned] }, ...overrides })))(seat, root, host, options);
299
570
  // 자리 턴이 «이 틱이 넣은 의뢰»를 처리했을 때만 칸에 잇는다 — 대기열의 다른 의뢰는 이 카드의 발사가 아니다.
300
571
  const targets = new Set(eligible.filter(row => row.seat === seat).map(row => `orch:${row.cardId}:${row.id}`));
301
572
  const handled = 'item' in turn && turn.item?.source === 'request' ? turn.item.id : undefined;
@@ -318,6 +589,10 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
318
589
  state.cells = merge(previous.cells, state.cells, cell => `${cell.cardId}:${cell.id}`);
319
590
  state.launched = merge(previous.launched ?? [], state.launched ?? [], turn => turn.queueId ?? `${turn.seat}:${turn.key ?? ''}`);
320
591
  }
592
+ if (mode === 'live') {
593
+ refreshGridHosts(state.cells, resolveGridHosts(deps, observe), mode, root, observe);
594
+ await syncQueuedPoolHints(root, state.launched ?? [], deps, observe);
595
+ }
321
596
  }
322
597
  state.queueOutcomes = (state.launched ?? []).filter(turn => turn.queueId).map(turn => {
323
598
  const outcome = (deps.queueOutcome ?? ((id: string, stateRoot: string) => harnessQueueOutcome(id, { root: stateRoot })))(turn.queueId!, root);
@@ -46,6 +46,7 @@ export const FEATURE_MATURITY = {
46
46
  '/ops/seats': { pwa: 'ops' },
47
47
  '/ops/checklist': { pwa: 'ops' },
48
48
  '/ceo': { pwa: 'ops' },
49
+ '/decisions': { pwa: 'ops' },
49
50
  '/dashboard': { pwa: 'ops' },
50
51
  '/bots': { pwa: 'ops' },
51
52
  '/botlab': { pwa: 'ops' },
@@ -75,7 +76,7 @@ export const FEATURE_MATURITY = {
75
76
  schedule: 'beta', intake: 'beta', logs: 'stable', ad: 'beta', docs: 'beta',
76
77
  decisions: 'beta', directives: 'beta', claims: 'beta', lesson: 'beta', fleet: 'beta', ops: 'stable', outputs: 'beta', publish: 'beta',
77
78
  env: 'beta', 'agent-mission': 'beta', dev: 'beta', keys: 'beta', finance: 'beta',
78
- config: 'stable', tasks: 'stable', brief: 'beta', labels: 'beta', msg: 'beta', ask: 'stable',
79
+ config: 'stable', tasks: 'stable', brief: 'beta', notify: 'beta', labels: 'beta', msg: 'beta', ask: 'stable',
79
80
  onboarding: 'beta', login: 'stable', session: 'beta', chat: 'stable', repl: 'stable',
80
81
  agent: 'stable', registry: 'beta', tier: 'stable', local: 'beta', scheduler: 'beta',
81
82
  attach: 'beta', wf: 'beta', nexus: 'stable', phone: 'beta', token: 'beta',
@@ -166,7 +167,7 @@ export const FEATURE_MATURITY = {
166
167
  },
167
168
  discordCommand: {
168
169
  status: 'stable', sessions: 'stable', new: 'stable', fork: 'stable',
169
- persona: 'beta', poll: 'beta', attach: 'beta', now: 'beta',
170
+ persona: 'beta', poll: 'beta', attach: 'beta', now: 'beta', loops: 'beta',
170
171
  brain: 'tool', cc: 'tool', cdx: 'tool', gem: 'tool',
171
172
  relay: 'ops', showroom: 'ops', bots: 'ops', bot: 'ops', screen: 'ops', chart: 'ops',
172
173
  routines: 'ops', botsay: 'ops', 'voice-join': 'ops', 'voice-leave': 'ops', 'voice-status': 'ops',
@@ -1,5 +1,6 @@
1
1
  import { debug } from '../../debug/log.js';
2
2
  import { DecisionLedger, type DecisionEntry } from '../../decisions/decision-ledger.js';
3
+ import { isIrreversible } from '../../decisions/decision-cards.js';
3
4
  import { jsonResponse } from './json-response.js';
4
5
 
5
6
  export const DECISIONS_PATH = '/v1/decisions';
@@ -27,10 +28,19 @@ export async function handleDecisions(req: Request, deps: DecisionsRouteDeps = {
27
28
  id: entry.id,
28
29
  title: entry.title,
29
30
  situation: entry.scqa.s.slice(0, 200),
30
- options: entry.options.map(option => ({ id: option.key, label: option.label })),
31
+ options: entry.options.map(option => ({ id: option.key, label: option.label, consequence: option.consequence })),
31
32
  recommendation: entry.recommendation,
32
33
  raisedAt: entry.raisedAt,
33
34
  dueAt: entry.dueAt,
35
+ scqa: entry.scqa,
36
+ category: entry.category,
37
+ irreversible: isIrreversible(entry),
38
+ raisedBy: { agent: entry.raisedBy.agent, ...(entry.raisedBy.track ? { track: entry.raisedBy.track } : {}) },
39
+ alternative: entry.alternative,
40
+ dissent: entry.dissent,
41
+ crossCheck: entry.crossCheck,
42
+ crossCheckSkipped: entry.crossCheckSkipped,
43
+ pendingQuestion: entry.pendingQuestion,
34
44
  }));
35
45
  debug.log('decisions.api', 'listed', { count: decisions.length });
36
46
  return jsonResponse({ decisions });