elanous 0.2.16 → 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 (354) 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-b46d45b705e8c9ae.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/9247-269e4464cc4b491c.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/app/approvals/page-d92f7ee3994e84c7.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-3cb52e5ea8fc2fa5.js → page-4733c967b56a4277.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/board/page-d22fd849cc22f104.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-c1e20d7d338713f1.js → page-bb742f995c6769e7.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/bots/{page-482c5dbde699947c.js → page-02b073a2ac793b83.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/chat/page-8a9b3594bc23289b.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/consult/page-a0eb02638b1e6f31.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/control/{page-a0e2a6d1bfb52fc5.js → page-308111abadf35670.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-c53598cfce6156f2.js → page-5d97b2901e0d1ea6.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/editor/{page-13a63953f53c6a23.js → page-c39554c321576982.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/exec/{page-5d3c8429d04f3831.js → page-8c3d8e63ae2e1c98.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/field/{page-3193bdc0bbab047a.js → page-9b72a07d424701c6.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/intake/{page-6107ca8ddd5e3bdd.js → page-a572f58cd3ca5864.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/live/page-be5ea965a07fbc41.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/loops/page-c130b63862437814.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/app/market/page-fa244bd146a00ae8.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-44d56d6391965a6a.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/app/morning/{page-4167348049166852.js → page-83a700d4c754d802.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/{not-found-de52e8fdf9095f7a.js → not-found-7b3170f374f77b9d.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-ca3952cd78a83ca2.js → page-a3c58f5c72a5f3f9.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/outputs/page-e8e1ecaea7cd71d2.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-1ebfe049c640e171.js → page-1467047104141297.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-2da6f9b8a38c6978.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/sessions/page-102310930791197a.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/settings/page-6ed0b73bae8351d8.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-abe8e3b74bea1ffc.js → page-44d5f767491692bf.js} +1 -1
  54. package/apps/pwa/out/_next/static/chunks/app/setup/page-43336055451f6e37.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/share/{page-d28573d6792b4275.js → page-3cc47dca0922ab9a.js} +1 -1
  56. package/apps/pwa/out/_next/static/chunks/app/showroom/page-a77ee355a2304ff0.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-d751de5f8062ff5e.js → page-b17cd46cf0e9b8cf.js} +1 -1
  58. package/apps/pwa/out/_next/static/chunks/app/term/{page-fe1759f94003b32b.js → page-61553851aa606146.js} +1 -1
  59. package/apps/pwa/out/_next/static/chunks/app/today/{page-d64c984e855c8c68.js → page-a1f585e154e61c59.js} +1 -1
  60. package/apps/pwa/out/_next/static/chunks/app/trace/page-7bf54c9bb9274af8.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/vault/{page-9afdee66b820c76e.js → page-5150ef17b5383a57.js} +1 -1
  62. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-3544f154c87b43ee.js → page-134da468538baf48.js} +1 -1
  65. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/{webpack-539cde99554be20f.js → webpack-c2782b330447ffb6.js} +1 -1
  67. package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +1 -0
  68. package/apps/pwa/out/approvals/index.html +1 -1
  69. package/apps/pwa/out/approvals/index.txt +14 -14
  70. package/apps/pwa/out/autopilot/index.html +1 -1
  71. package/apps/pwa/out/autopilot/index.txt +14 -14
  72. package/apps/pwa/out/board/index.html +1 -1
  73. package/apps/pwa/out/board/index.txt +14 -14
  74. package/apps/pwa/out/botlab/index.html +1 -1
  75. package/apps/pwa/out/botlab/index.txt +14 -14
  76. package/apps/pwa/out/bots/index.html +1 -1
  77. package/apps/pwa/out/bots/index.txt +14 -14
  78. package/apps/pwa/out/ceo/index.html +1 -1
  79. package/apps/pwa/out/ceo/index.txt +14 -14
  80. package/apps/pwa/out/chat/index.html +1 -1
  81. package/apps/pwa/out/chat/index.txt +14 -14
  82. package/apps/pwa/out/consult/index.html +1 -1
  83. package/apps/pwa/out/consult/index.txt +14 -14
  84. package/apps/pwa/out/control/index.html +1 -1
  85. package/apps/pwa/out/control/index.txt +14 -14
  86. package/apps/pwa/out/decisions/index.html +1 -0
  87. package/apps/pwa/out/decisions/index.txt +30 -0
  88. package/apps/pwa/out/design-check/index.html +1 -1
  89. package/apps/pwa/out/design-check/index.txt +14 -14
  90. package/apps/pwa/out/editor/index.html +1 -1
  91. package/apps/pwa/out/editor/index.txt +14 -14
  92. package/apps/pwa/out/exec/index.html +1 -1
  93. package/apps/pwa/out/exec/index.txt +14 -14
  94. package/apps/pwa/out/field/index.html +1 -1
  95. package/apps/pwa/out/field/index.txt +14 -14
  96. package/apps/pwa/out/healthz +1 -1
  97. package/apps/pwa/out/index.html +1 -1
  98. package/apps/pwa/out/index.txt +14 -14
  99. package/apps/pwa/out/inside/index.html +1 -1
  100. package/apps/pwa/out/inside/index.txt +14 -14
  101. package/apps/pwa/out/intake/index.html +1 -1
  102. package/apps/pwa/out/intake/index.txt +14 -14
  103. package/apps/pwa/out/live/index.html +1 -1
  104. package/apps/pwa/out/live/index.txt +14 -14
  105. package/apps/pwa/out/loops/index.html +1 -1
  106. package/apps/pwa/out/loops/index.txt +14 -14
  107. package/apps/pwa/out/market/index.html +1 -1
  108. package/apps/pwa/out/market/index.txt +14 -14
  109. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  110. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  111. package/apps/pwa/out/morning/index.html +1 -1
  112. package/apps/pwa/out/morning/index.txt +14 -14
  113. package/apps/pwa/out/observatory/index.html +1 -1
  114. package/apps/pwa/out/observatory/index.txt +14 -14
  115. package/apps/pwa/out/ops/checklist/index.html +1 -1
  116. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  117. package/apps/pwa/out/ops/release/index.html +1 -1
  118. package/apps/pwa/out/ops/release/index.txt +14 -14
  119. package/apps/pwa/out/ops/seats/index.html +1 -1
  120. package/apps/pwa/out/ops/seats/index.txt +14 -14
  121. package/apps/pwa/out/outputs/index.html +1 -1
  122. package/apps/pwa/out/outputs/index.txt +14 -14
  123. package/apps/pwa/out/reflection/index.html +1 -1
  124. package/apps/pwa/out/reflection/index.txt +15 -15
  125. package/apps/pwa/out/scheduler/index.html +1 -1
  126. package/apps/pwa/out/scheduler/index.txt +14 -14
  127. package/apps/pwa/out/sessions/index.html +1 -1
  128. package/apps/pwa/out/sessions/index.txt +14 -14
  129. package/apps/pwa/out/settings/devices/index.html +1 -1
  130. package/apps/pwa/out/settings/devices/index.txt +14 -14
  131. package/apps/pwa/out/settings/index.html +1 -1
  132. package/apps/pwa/out/settings/index.txt +14 -14
  133. package/apps/pwa/out/setup/done/index.html +1 -1
  134. package/apps/pwa/out/setup/done/index.txt +14 -14
  135. package/apps/pwa/out/setup/index.html +1 -1
  136. package/apps/pwa/out/setup/index.txt +14 -14
  137. package/apps/pwa/out/share/index.html +1 -1
  138. package/apps/pwa/out/share/index.txt +14 -14
  139. package/apps/pwa/out/showroom/index.html +1 -1
  140. package/apps/pwa/out/showroom/index.txt +14 -14
  141. package/apps/pwa/out/tasks/index.html +1 -1
  142. package/apps/pwa/out/tasks/index.txt +14 -14
  143. package/apps/pwa/out/term/index.html +1 -1
  144. package/apps/pwa/out/term/index.txt +14 -14
  145. package/apps/pwa/out/today/index.html +1 -1
  146. package/apps/pwa/out/today/index.txt +14 -14
  147. package/apps/pwa/out/trace/index.html +1 -1
  148. package/apps/pwa/out/trace/index.txt +14 -14
  149. package/apps/pwa/out/vault/index.html +1 -1
  150. package/apps/pwa/out/vault/index.txt +14 -14
  151. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  152. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  153. package/apps/pwa/out/workflows/index.html +1 -1
  154. package/apps/pwa/out/workflows/index.txt +14 -14
  155. package/apps/pwa/out/workspace/index.html +1 -1
  156. package/apps/pwa/out/workspace/index.txt +14 -14
  157. package/apps/pwa/out/worktrees/index.html +1 -1
  158. package/apps/pwa/out/worktrees/index.txt +14 -14
  159. package/graphs/intake/intake-daily.yaml +9 -2
  160. package/graphs/intake/news-sources.yaml +4 -0
  161. package/graphs/intake/recipes.yaml +6 -1
  162. package/package.json +2 -1
  163. package/samples/workflows/templates/approval-gated-deploy.yaml +30 -0
  164. package/samples/workflows/templates/chat-driven-research.yaml +35 -0
  165. package/samples/workflows/templates/discord-listen-and-respond.yaml +29 -0
  166. package/samples/workflows/templates/http-poll-then-alert.yaml +30 -0
  167. package/samples/workflows/templates/manual-quickstart.yaml +21 -0
  168. package/samples/workflows/templates/scheduled-daily-digest.yaml +30 -0
  169. package/samples/workflows/templates/webhook-deploy-hook.yaml +27 -0
  170. package/scripts/__fixtures__/intake-news.xml +7 -0
  171. package/scripts/ci-public-export-import-gate.ts +7 -2
  172. package/scripts/ci-typecheck-changed.ts +21 -6
  173. package/scripts/discord-seat-probe.ts +3 -2
  174. package/scripts/docs/generate-cli-anatomy.ts +162 -0
  175. package/scripts/goals/authoring-metrics.ts +73 -0
  176. package/scripts/intake-news.ts +169 -0
  177. package/scripts/landing-heal/must-fix-match.ts +7 -7
  178. package/scripts/landing-heal/run-mission.ts +3 -2
  179. package/scripts/public-export-test-run.ts +37 -2
  180. package/scripts/release-loop/checklist-gate-node.ts +13 -11
  181. package/scripts/release-loop/cut-branch.ts +2 -1
  182. package/scripts/release-loop/cutoff.ts +4 -2
  183. package/scripts/release-loop/docs-land-node.ts +77 -11
  184. package/scripts/release-loop/docs-node.ts +1 -1
  185. package/scripts/release-loop/gate-node.ts +99 -14
  186. package/scripts/release-loop/node-verdict.ts +2 -1
  187. package/scripts/release-loop/npm-publish-node.ts +11 -7
  188. package/scripts/release-loop/ops-upgrade-node.ts +8 -1
  189. package/scripts/release-loop/preflight.ts +246 -0
  190. package/scripts/release-loop/publish-node.ts +10 -3
  191. package/scripts/release-loop/release-docs.ts +134 -0
  192. package/scripts/release-loop/release-readiness.ts +11 -6
  193. package/scripts/release-loop/release-version.ts +48 -0
  194. package/scripts/release-loop/resume-release.ts +115 -0
  195. package/scripts/release-loop/unattended-release.ts +43 -7
  196. package/scripts/release-loop/version-node.ts +149 -41
  197. package/scripts/release-story/draft.ts +42 -14
  198. package/scripts/rhythm/daily.ts +25 -4
  199. package/scripts/skills/public-check.ts +62 -12
  200. package/scripts/test-deterministic.ts +51 -33
  201. package/scripts/test-diet/effectiveness.ts +62 -1
  202. package/scripts/test-diet/lib.ts +1 -1
  203. package/scripts/test-diet/node.ts +8 -1
  204. package/src/agent-mission/driver.ts +53 -10
  205. package/src/agent-mission/review-loop.ts +4 -3
  206. package/src/agent-substrate/pr-comment-meta.ts +20 -0
  207. package/src/autopilot/build/llm-conflict-merge.ts +28 -8
  208. package/src/autopilot/track-agent.ts +5 -3
  209. package/src/briefing/brief-items.ts +86 -17
  210. package/src/cli/autopilot-cli.ts +86 -1
  211. package/src/cli/brief-cli.ts +36 -2
  212. package/src/cli/decisions-cli.ts +4 -1
  213. package/src/cli/freeze-cli.ts +3 -2
  214. package/src/cli/intake-cli.ts +74 -2
  215. package/src/cli/log-event-names.ts +1 -0
  216. package/src/cli/notify-cli.ts +75 -0
  217. package/src/cli/pr-cli.ts +12 -1
  218. package/src/cli/provider-cli.ts +24 -0
  219. package/src/cli/release-cli.ts +101 -17
  220. package/src/cli/self-update-canary.ts +46 -0
  221. package/src/cli/self-update.ts +200 -1
  222. package/src/cli/tasks-cli.ts +309 -3
  223. package/src/context-bus/condense.ts +19 -3
  224. package/src/context-bus/context-day.ts +77 -24
  225. package/src/context-bus/context-first.ts +33 -0
  226. package/src/context-card/index.ts +45 -0
  227. package/src/context-card/task.ts +76 -0
  228. package/src/dashboard/slash-runtime/dashboard-handlers.ts +2 -0
  229. package/src/decisions/decision-ledger.ts +10 -1
  230. package/src/discord-self-message.ts +20 -2
  231. package/src/discord-slash-wire.ts +1 -0
  232. package/src/domains/outbound-alert.ts +200 -27
  233. package/src/domains/outbound-log-probe.ts +16 -1
  234. package/src/domains/release-tool.ts +2 -2
  235. package/src/execution-loop/gates.ts +11 -2
  236. package/src/flow-loop/split.ts +8 -4
  237. package/src/git-fs/worktree.ts +23 -3
  238. package/src/harness/harness-cli-command.ts +151 -11
  239. package/src/harness/harness-pod-dispatch.ts +11 -0
  240. package/src/harness/harness-queue.ts +466 -32
  241. package/src/harness/helper-scan.ts +10 -3
  242. package/src/harness/nested-elanous-policy.ts +23 -4
  243. package/src/hq/fence-wrapper.ts +123 -0
  244. package/src/hq/hq.ts +8 -0
  245. package/src/hq/seats.ts +369 -0
  246. package/src/index.ts +190 -23
  247. package/src/intake-plane/adopt-gate.ts +49 -0
  248. package/src/intake-plane/check.ts +3 -0
  249. package/src/intake-plane/collect-github.ts +57 -25
  250. package/src/intake-plane/digest.ts +39 -4
  251. package/src/intake-plane/discord-seat-work.ts +8 -2
  252. package/src/intake-plane/requirement-funnel.ts +316 -0
  253. package/src/knowledge/know-find.ts +10 -1
  254. package/src/loops/budget.ts +36 -0
  255. package/src/loops/orchestrator/finish-rate.ts +73 -1
  256. package/src/loops/orchestrator/grid-place.ts +11 -0
  257. package/src/loops/orchestrator/tick.ts +295 -20
  258. package/src/maturity/feature-maturity.ts +3 -2
  259. package/src/nexus/api/decisions-route.ts +11 -1
  260. package/src/nexus/api/grid.ts +61 -0
  261. package/src/nexus/api/http-server.ts +19 -1
  262. package/src/nexus/api/loop-edges.ts +69 -13
  263. package/src/nexus/api/ops-seats-sources.ts +2 -2
  264. package/src/nexus/api/task-cards-api.ts +19 -0
  265. package/src/nexus/api/workflow-templates.ts +3 -0
  266. package/src/oauth/codex.ts +120 -2
  267. package/src/onboarding/first-chat-gate.ts +29 -0
  268. package/src/release-loop/checklist.ts +104 -6
  269. package/src/release-loop/feature-store.ts +10 -9
  270. package/src/release-loop/landing-freeze.ts +24 -14
  271. package/src/release-loop/placement.ts +13 -3
  272. package/src/release-loop/rubric.ts +80 -0
  273. package/src/seat-dispatch/seat-ask.ts +79 -16
  274. package/src/seat-loop/seat-loop.ts +107 -37
  275. package/src/self-dev/ask-launch-flow.ts +50 -10
  276. package/src/self-dev/ask-path-candidates.ts +68 -0
  277. package/src/self-dev/dev-pipeline.ts +4 -0
  278. package/src/self-dev/draft-triage-rules.ts +1 -1
  279. package/src/self-dev/orchestrate-cli.ts +22 -0
  280. package/src/self-dev/round-pr-supersede.ts +244 -0
  281. package/src/self-dev/run-store.ts +5 -1
  282. package/src/self-dev/run-supervisor.ts +21 -1
  283. package/src/self-dev/supervisor-verdict-edges.ts +46 -0
  284. package/src/self-implement/follow-up-goals.ts +2 -2
  285. package/src/self-implement/frozen-merges.ts +25 -20
  286. package/src/self-implement/gate-remote.ts +427 -0
  287. package/src/self-implement/goal-author-clarification.ts +81 -0
  288. package/src/self-implement/goal-author-cli.ts +33 -5
  289. package/src/self-implement/goal-author.ts +93 -12
  290. package/src/self-implement/goal-clarification-escalation.ts +37 -1
  291. package/src/self-implement/goal-document-write.ts +28 -0
  292. package/src/self-implement/heal-intake.ts +4 -0
  293. package/src/self-implement/host-regate.ts +3 -1
  294. package/src/self-implement/l8-merge-queue.ts +29 -5
  295. package/src/self-implement/orchestrator.ts +78 -13
  296. package/src/self-implement/prior-draft-findings.ts +4 -3
  297. package/src/skills/skill-feedback.ts +76 -0
  298. package/src/task-agent/actions.ts +151 -0
  299. package/src/task-agent/judge.ts +122 -0
  300. package/src/task-agent/mission.ts +423 -0
  301. package/src/task-agent/shadow.ts +90 -0
  302. package/src/task-agent/task-hand.ts +235 -0
  303. package/src/task-cards/card-store.ts +4 -1
  304. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +43 -4
  305. package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
  306. package/src/task-orchestrator/surfaces/pod-pool.ts +81 -13
  307. package/src/task-orchestrator/surfaces/self-implement-pod.ts +225 -35
  308. package/src/telegram-commands.ts +1 -1
  309. package/src/telegram-loops-command.ts +22 -15
  310. package/src/telegram.ts +5 -20
  311. package/src/user-config.ts +86 -4
  312. package/src/version/packed-revision.json +1 -1
  313. package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +0 -1
  314. package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +0 -1
  315. package/apps/pwa/out/_next/static/chunks/4614-21403aac730d4586.js +0 -1
  316. package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +0 -1
  317. package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +0 -1
  318. package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +0 -1
  319. package/apps/pwa/out/_next/static/chunks/6877-7c6208705c284124.js +0 -1
  320. package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +0 -1
  321. package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +0 -1
  322. package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +0 -1
  323. package/apps/pwa/out/_next/static/chunks/8949-ecb4128a0858b911.js +0 -1
  324. package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +0 -1
  325. package/apps/pwa/out/_next/static/chunks/9247-29d889b809288e65.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/app/chat/page-8c2d9526164f5d09.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/layout-dce686e582c23937.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/loops/page-1a61c7ca1ceb3ae7.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/market/page-51f811fa119308b6.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-dacff46049e1557e.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/observatory/page-36a7d994f4ca0299.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-5b042736aa113756.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-acb749f1e432bcf3.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-57924393e3e4e539.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-31fa1ce4c4aff9ae.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/app/settings/page-0f5cd57ca928f42b.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/app/showroom/page-59f03fda76c03d22.js +0 -1
  348. package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +0 -1
  349. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d4aef67abf264874.js +0 -1
  350. package/apps/pwa/out/_next/static/chunks/app/workflows/page-1ea64acadd704ace.js +0 -1
  351. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +0 -1
  352. package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +0 -1
  353. package/apps/pwa/out/_next/static/rv9cierV0jSOvDBm1g0XB/_buildManifest.js +0 -1
  354. /package/apps/pwa/out/_next/static/{rv9cierV0jSOvDBm1g0XB → P6YTVgzuZz5oTZhY4Din_}/_ssgManifest.js +0 -0
@@ -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 });
@@ -0,0 +1,61 @@
1
+ import { getUserConfig } from '../../user-config.js';
2
+ import { readHqLease, type HqDeps } from '../../hq/hq.js';
3
+ import { measurePoolLease, type PoolLeaseMeasure } from '../../task-orchestrator/surfaces/pod-lease.js';
4
+ import { parsePodPool, resolvePodPoolSpec, type PodPoolMember, type PoolKubectl } from '../../task-orchestrator/surfaces/pod-pool.js';
5
+
6
+ export interface GridData {
7
+ /** null means the arbiter could not be read, not that no lease exists. */
8
+ hq: { record: ReturnType<typeof readHqLease>['record']; ageSeconds: number | null; expired: boolean | null; reason: string | null };
9
+ /** Configured member order; capacity comes from measurePoolLease when available. occupied is running + pending, or null on measurement failure. */
10
+ members: Array<{ context: string; capacity: number; running: number | null; pending: number | null; occupied: number | null; reason: string | null }>;
11
+ poolReason: string | null;
12
+ }
13
+
14
+ export interface GridDeps {
15
+ hq?: HqDeps;
16
+ readHq?: () => ReturnType<typeof readHqLease>;
17
+ poolSpec?: () => string | null;
18
+ /** Optional read-only kubectl seam; never use a probe that creates Pods. */
19
+ kubectl?: PoolKubectl;
20
+ measure?: (members: readonly PodPoolMember[]) => PoolLeaseMeasure;
21
+ }
22
+
23
+ /** Independent, fail-soft read snapshots. No lease status/heartbeat, DNS probe Pod, host reservation or writes. */
24
+ export function buildGridData(deps: GridDeps = {}): GridData {
25
+ let hq: GridData['hq'];
26
+ try {
27
+ hq = { ...(deps.readHq ?? (() => readHqLease(deps.hq)))(), reason: null };
28
+ } catch (error) {
29
+ hq = { record: null, ageSeconds: null, expired: null, reason: String(error) };
30
+ }
31
+
32
+ let members: GridData['members'] = [];
33
+ let poolReason: string | null = null;
34
+ try {
35
+ const spec = (deps.poolSpec ?? (() => resolvePodPoolSpec(undefined, process.env, () => {
36
+ const config = getUserConfig();
37
+ return config.harness?.podPool ?? config.pod?.pool;
38
+ })))();
39
+ if (spec) {
40
+ const configured = parsePodPool(spec);
41
+ try {
42
+ const measured = (deps.measure ?? ((pool) => measurePoolLease(pool, { skipDnsProbe: true, ...(deps.kubectl ? { kubectl: deps.kubectl } : {}) })))(configured);
43
+ const byContext = new Map(measured.members.map((m) => [m.context, m]));
44
+ members = configured.map((member) => {
45
+ const row = byContext.get(member.context);
46
+ const running = row?.running ?? null;
47
+ const pending = row?.pending ?? null;
48
+ return { context: member.context, capacity: row?.capacity ?? member.capacity, running, pending,
49
+ occupied: running === null || pending === null ? null : running + pending,
50
+ reason: row?.reason ?? (row ? null : '측정 불가') };
51
+ });
52
+ } catch (error) {
53
+ poolReason = String(error);
54
+ members = configured.map(({ context, capacity }) => ({ context, capacity, running: null, pending: null, occupied: null, reason: poolReason }));
55
+ }
56
+ } else poolReason = 'pool not configured';
57
+ } catch (error) {
58
+ poolReason = String(error);
59
+ }
60
+ return { hq, members, poolReason };
61
+ }
@@ -22,6 +22,7 @@ import { spawnSync } from 'node:child_process';
22
22
  import { isAbsolute } from 'node:path';
23
23
  import { createProject, listProjects } from '../../project/project-store.js';
24
24
  import { handleOutputsGet, type OutputsDeps } from './outputs-route.js';
25
+ import { buildGridData, type GridDeps } from './grid.js';
25
26
  import { DECISIONS_PATH, handleDecisions, type DecisionsRouteDeps } from './decisions-route.js';
26
27
  import { CARD_FOLLOWUP_PATH, createCardFollowupJobs, type CardFollowupJobs } from './card-followup-route.js';
27
28
  import { SKILL_EXEC_PATH, createSkillExecJobs, type SkillExecJobs } from './skill-exec-route.js';
@@ -341,7 +342,7 @@ import {
341
342
  handleTasksList,
342
343
  handleTaskDetail,
343
344
  } from './tasks-scheduler.js';
344
- import { handleTaskCardsGet, handleTaskCardsWishPost } from './task-cards-api.js';
345
+ import { handleTaskCardsGet, handleTaskCardsWishPost, handleTaskCardsClosePost } from './task-cards-api.js';
345
346
  import { handleTaskCreatePost, handleTaskApprovePost } from './tasks-create.js';
346
347
  import { matchIngestAuthorization } from './ingest-token.js';
347
348
  import {
@@ -478,6 +479,8 @@ export interface NexusHttpServerOpts {
478
479
  /** Optional installer state root for isolated API consumers and tests. */
479
480
  pluginStateRoot?: string;
480
481
  outputs?: OutputsDeps;
482
+ /** Read-only grid measurement seam; omitted in production for live HQ and Pod pool reads. */
483
+ grid?: GridDeps;
481
484
  decisions?: Pick<DecisionsRouteDeps, 'ledger'>;
482
485
  cardFollowup?: CardFollowupJobs;
483
486
  skillExec?: SkillExecJobs;
@@ -1385,6 +1388,12 @@ export async function routeRequest(
1385
1388
  });
1386
1389
  }
1387
1390
 
1391
+ // routeRequest is the live GET-only caller of buildGridData; keep it behind the owner auth gate.
1392
+ if (pathname === '/v1/grid') {
1393
+ if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
1394
+ if (method !== 'GET') return jsonResponse({ error: 'method-not-allowed', method }, 405);
1395
+ return jsonResponse(buildGridData(opts.grid));
1396
+ }
1388
1397
  if (method === 'GET' && pathname === '/v1/outputs') {
1389
1398
  if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
1390
1399
  return handleOutputsGet(req, opts.outputs);
@@ -1457,6 +1466,15 @@ export async function routeRequest(
1457
1466
  const mutated = await handleGraphsMutation(pathname, req);
1458
1467
  if (mutated) return mutated;
1459
1468
  }
1469
+ const cardClose = /^\/v1\/task-cards\/([^/]+)\/close$/.exec(pathname);
1470
+ if (method === 'POST' && cardClose) {
1471
+ if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
1472
+ if (bearerCredential(req, opts.metaApi) !== 'bearer-match') return jsonResponse({ error: 'forbidden' }, 403);
1473
+ let id: string;
1474
+ try { id = decodeURIComponent(cardClose[1]!); }
1475
+ catch { return jsonResponse({ error: 'not_found' }, 404); }
1476
+ return handleTaskCardsClosePost(req, id);
1477
+ }
1460
1478
  if (method === 'POST' && pathname === '/v1/task-cards/wish') {
1461
1479
  if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
1462
1480
  return handleTaskCardsWishPost(req);