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
@@ -8,7 +8,7 @@ import { intakeOutboxDir, kstDay } from './route.js';
8
8
 
9
9
  export type DigestLensVerdict = '대체 후보' | '보강' | '경쟁 대조';
10
10
  export interface DigestEntry { id: string; sources: string[]; url?: string; note?: string; noteName?: string; oneLiner?: string; axis: string; impact?: { verdict: DigestLensVerdict; why: string; target: string } }
11
- export interface IntakeDigest { day: string; absorbed: DigestEntry[]; goals: { fact: string; url?: string }[]; review?: { fact: string; note?: string }[]; grounding: number; release: number; manual: number; savedSilence?: { days: number; lastNewAt: string } }
11
+ export interface IntakeDigest { day: string; absorbed: DigestEntry[]; goals: { fact: string; url?: string }[]; news?: { title: string; url: string; summary: string[]; implication: string[] }[]; shadowSuggestions?: { fact: string; url?: string; verdict: string }[]; review?: { fact: string; note?: string }[]; grounding: number; release: number; manual: number; savedSilence?: { days: number; lastNewAt: string } }
12
12
 
13
13
  const AXIS_LABEL: Record<string, string> = {
14
14
  video_automation: '영상 자동화', agent_basics: '에이전트 기본', agent_applied: '에이전트 응용', unrelated: '그 밖',
@@ -82,12 +82,24 @@ export function buildIntakeDigest(root: string, day: string, readFile: (p: strin
82
82
  };
83
83
  });
84
84
  const goals = readJsonl(join(out, 'goals', `${day}.jsonl`)).map((g) => ({ fact: String(g.fact ?? ''), ...(g.url ? { url: String(g.url) } : {}) }));
85
+ const lensRows = readJsonl(join(out, 'lens', `${day}.jsonl`));
86
+ const news = lensRows.flatMap((row) =>
87
+ typeof row.title === 'string' && typeof row.url === 'string' && Array.isArray(row.summary) && Array.isArray(row.implication)
88
+ ? [{ title: row.title, url: row.url, summary: row.summary.filter((s): s is string => typeof s === 'string'), implication: row.implication.filter((s): s is string => typeof s === 'string') }]
89
+ : []);
90
+ const shadowSuggestions = lensRows.flatMap((row) =>
91
+ Array.isArray(row.suggestions) ? row.suggestions.flatMap((raw: unknown) => {
92
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return [];
93
+ const suggestion = raw as Record<string, unknown>;
94
+ if (suggestion.status !== '제안' || typeof suggestion.fact !== 'string' || !suggestion.fact.trim()) return [];
95
+ return [{ fact: suggestion.fact, ...(typeof row.url === 'string' ? { url: row.url } : {}), verdict: typeof suggestion.verdict === 'string' ? suggestion.verdict : '판단 필요' }];
96
+ }) : []);
85
97
  const review = readJsonl(join(out, 'review', `${day}.jsonl`)).map((r) => ({ fact: String(r.fact ?? ''), ...(r.note ? { note: String(r.note) } : {}) }));
86
98
  const grounding = readJsonl(join(out, 'grounding.jsonl')).filter((g) => typeof g.at === 'string' && kstDay(g.at) === day).length;
87
99
  const lastNewAt = readSavedCursorAt(root);
88
100
  const silentMs = lastNewAt ? now.getTime() - Date.parse(lastNewAt) : NaN;
89
101
  return {
90
- day, absorbed, goals, review, grounding,
102
+ day, absorbed, goals, ...(news.length ? { news } : {}), ...(shadowSuggestions.length ? { shadowSuggestions } : {}), review, grounding,
91
103
  release: readJsonl(join(out, 'release', `${day}.jsonl`)).length,
92
104
  manual: readJsonl(join(out, 'manual', `${day}.jsonl`)).length,
93
105
  ...(lastNewAt && silentMs > SAVED_SILENCE_MS ? { savedSilence: { days: Math.floor(silentMs / 86_400_000), lastNewAt } } : {}),
@@ -98,7 +110,8 @@ export function buildIntakeDigest(root: string, day: string, readFile: (p: strin
98
110
  export function renderDigestMarkdown(d: IntakeDigest): string {
99
111
  const L: string[] = [`## 📰 오늘의 흡수 요약 (${d.absorbed.length})`, ''];
100
112
  if (d.savedSilence) L.push(`> ⚠️ 텔레그램 «저장된 메시지»에서 새 글을 ${d.savedSilence.days}일째 못 받았다(마지막 새 글 수집 ${kstDay(d.savedSilence.lastNewAt)}) — 저장했는데 안 들어왔다면 저장한 곳(본인 «저장된 메시지»인지)을 확인한다.`, '');
101
- if (!d.absorbed.length) { L.push('오늘 흡수한 것이 없다.', ''); return L.join('\n'); }
113
+ if (!d.absorbed.length) L.push('오늘 흡수한 것이 없다.', '');
114
+ if (!d.absorbed.length && !d.news?.length) return L.join('\n');
102
115
  const byAxis = new Map<string, DigestEntry[]>();
103
116
  for (const e of d.absorbed) (byAxis.get(e.axis) ?? byAxis.set(e.axis, []).get(e.axis)!).push(e);
104
117
  for (const [axis, rows] of byAxis) {
@@ -106,11 +119,25 @@ export function renderDigestMarkdown(d: IntakeDigest): string {
106
119
  for (const e of rows) L.push(`- ${e.noteName ? `[[${e.noteName}]]` : e.url ?? e.id} — ${e.oneLiner ?? '(요약 줄 없음)'}`);
107
120
  L.push('');
108
121
  }
122
+ if (d.news?.length) {
123
+ L.push(`### 📰 관심 뉴스 (${d.news.length})`, '');
124
+ for (const article of d.news.slice(0, 10)) {
125
+ L.push(`- [${article.title}](${article.url})`);
126
+ for (const line of article.summary) L.push(` - ${line}`);
127
+ for (const line of article.implication) L.push(` - 엘라누스 함의: ${line}`);
128
+ }
129
+ L.push('');
130
+ }
109
131
  if (d.goals.length) {
110
132
  L.push(`### 🔴 엘라누스에 없는 것 — 골 후보 (${d.goals.length})`, '');
111
133
  for (const g of d.goals.slice(0, 10)) L.push(`- ${g.fact}${g.url ? ` · [원본](${g.url})` : ''}`);
112
134
  L.push('');
113
135
  }
136
+ if (d.shadowSuggestions?.length) {
137
+ L.push(`### 📰 뉴스 칸 제안 — 그림자, 판 미등록 (${d.shadowSuggestions.length})`, '');
138
+ for (const s of d.shadowSuggestions.slice(0, 10)) L.push(`- [${s.verdict}] ${s.fact}${s.url ? ` · [원본](${s.url})` : ''}`);
139
+ L.push('');
140
+ }
114
141
  const undecided = d.absorbed.filter((e) => !e.impact).length;
115
142
  if (undecided) L.push(`렌즈 판정 못 함 ${undecided} — 원장 \`elanous intake items\``, '');
116
143
  L.push(`> 매뉴얼 후보 ${d.manual} · 릴리스 노트 맥락 ${d.release} · 그라운딩 후보 ${d.grounding} — 흡수 원장 \`elanous intake items\` · 큐 \`<인스턴스>/intake/outbox/\``, '');
@@ -122,7 +149,15 @@ export function renderDigestTelegram(d: IntakeDigest, _opts: { vaultRoot?: strin
122
149
  // «Touching» is decided by the lens verdict alone; a missing note summary only changes how S reads (ACP must-fix).
123
150
  const touching = d.absorbed.filter((e) => e.impact);
124
151
  const L = [`흡수 ${d.absorbed.length} → 우리에게 닿는 것 ${touching.length}`];
125
- if (!touching.length) return L[0];
152
+ // 관심 뉴스는 최대 셋 — 기사마다 S(요약 첫 줄) · A(엘라누스 함의 첫 줄) · 링크를 한 덩어리로(NEWS-INTAKE).
153
+ for (const article of (d.news ?? []).slice(0, 3)) {
154
+ L.push(`📰 ${article.title}`);
155
+ if (article.summary[0]) L.push(`S: ${article.summary[0]}`);
156
+ if (article.implication[0]) L.push(`A: ${article.implication[0]}`);
157
+ L.push(article.url);
158
+ }
159
+ if (d.shadowSuggestions?.length) L.push(`뉴스 칸 제안 ${d.shadowSuggestions.length}건 (그림자·판 미등록): ${d.shadowSuggestions.slice(0, 3).map((s) => `[${s.verdict}] ${s.fact}${s.url ? ` ${s.url}` : ''}`).join(' · ')}`);
160
+ if (!touching.length) return L.join('\n');
126
161
  // At most three items are shown, each as its own S·C·A·link block so context and source stay paired;
127
162
  // a later item sharing (verdict, target) says «위와 같은 행동» instead of repeating the A sentence (ACP must-fix).
128
163
  // S must be the note's own summary: items without one still count as touching but are not shown (ACP must-fix).
@@ -5,7 +5,7 @@ import { awaitGlobalPersonaLoad, getGlobalPersonaRegistry } from '../persona/glo
5
5
  import type { PersonaSource } from '../persona/mention-parser.js';
6
6
  import { dispatchCeoTask, type CeoCommandDeps } from '../seat-dispatch/ceo-commands.js';
7
7
  import { ceoTaskDeps, classifyCeoIntent } from '../seat-dispatch/ceo-intent.js';
8
- import { askSeat, parseSeatAsk, type SeatAskDeps } from '../seat-dispatch/seat-ask.js';
8
+ import { askSeat, askSeatAs, parseSeatAsk, type SeatAskDeps } from '../seat-dispatch/seat-ask.js';
9
9
  import { parseSeatAddress, resolveSeat } from '../seat-address/seat-address.js';
10
10
  import { getUserConfig, type UserConfig } from '../user-config.js';
11
11
  import { answerAsSeat } from './seat-answer.js';
@@ -110,7 +110,13 @@ export async function handleDiscordSeatWork(
110
110
  const seatId = canonicalSeatId(seats[0].seat.id);
111
111
  if (auth && ['OP', 'TC', 'MK', 'UX'].includes(seatId)) {
112
112
  const intent = classifyCeoIntent(body);
113
- if (intent === 'task') return sendTask(seatId, body, auth.cfg, auth.id);
113
+ if (intent === 'task') {
114
+ if (deps.askDeps) return askSeatAs(seatId as 'OP' | 'TC' | 'MK' | 'UX', body,
115
+ { channel: 'discord', channelId: msg.channelId, messageId: msg.messageId,
116
+ ...(msg.threadId ? { threadId: msg.threadId } : {}) },
117
+ deps.commandDeps ?? ceoTaskDeps(auth.cfg, auth.id), deps.askDeps);
118
+ return sendTask(seatId, body, auth.cfg, auth.id);
119
+ }
114
120
  const answer = await (deps.answer ?? answerAsSeat)(seats[0].name, body);
115
121
  debug.log('seat.dispatch', 'intent', { seat: seatId, intent, via: 'discord', outcome: answer ? 'answered' : 'intake' });
116
122
  if (answer) return answer.text;
@@ -0,0 +1,316 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { elanousStateRoot } from '../autopilot/state-paths.js';
5
+ import { normalizeClaim, type AddBriefItem } from '../briefing/brief-items.js';
6
+ import { createRequire } from 'node:module';
7
+ import { debug } from '../debug/log.js';
8
+ import { listChecklist, setItem, type ChecklistItem } from '../release-loop/checklist.js';
9
+ import { placeCell, type PlacementDeps } from '../release-loop/placement.js';
10
+ import { listSchedules } from '../release-loop/release-schedule.js';
11
+ import { getUserConfig } from '../user-config.js';
12
+ import { dateKey } from '../time/format.js';
13
+ import { cardIndexPath, CardStore, type TaskCard } from '../task-cards/card-store.js';
14
+ import { loadIntakeLedger, type IntakeItem } from './items.js';
15
+ import { parseRubric, rubricGrade, rubricScore } from '../release-loop/rubric.js';
16
+
17
+ export type RequirementSource = 'directive' | 'intake' | 'grounding' | 'coordination' | 'linear';
18
+ export interface RequirementCandidate {
19
+ id: string;
20
+ source: RequirementSource;
21
+ text: string;
22
+ link: string;
23
+ receivedAt: string;
24
+ priority?: 'P0' | 'P1' | 'P2';
25
+ risk?: number;
26
+ cost?: number;
27
+ security?: number;
28
+ license?: string;
29
+ patentPrivatePath?: boolean;
30
+ owner?: string;
31
+ deadlineVersion?: string;
32
+ }
33
+ export type IncomingRequirement = Omit<RequirementCandidate, 'source'>;
34
+ export interface RequirementSources {
35
+ directives?: readonly DirectiveRow[];
36
+ intake?: readonly IntakeItem[];
37
+ grounding?: readonly IncomingRequirement[];
38
+ coordination?: readonly IncomingRequirement[];
39
+ linear?: readonly IncomingRequirement[];
40
+ }
41
+ export interface RequirementDecision {
42
+ candidate: RequirementCandidate;
43
+ sources: RequirementCandidate[];
44
+ status: 'adopted' | 'would-adopt' | 'proposal';
45
+ cellId?: string;
46
+ version?: string;
47
+ rubric: string;
48
+ reason?: string;
49
+ }
50
+ export interface RequirementFunnelResult {
51
+ counts: Record<RequirementSource, number>;
52
+ candidates: number;
53
+ decisions: RequirementDecision[];
54
+ unlinkedDirectives: RequirementCandidate[];
55
+ briefItems: AddBriefItem[];
56
+ }
57
+ export interface FunnelOptions {
58
+ sources: RequirementSources;
59
+ /** Older directives are only checked for unlinked P0 backlog, not counted as today's arrivals. */
60
+ historicalDirectives?: readonly DirectiveRow[];
61
+ cells: readonly { version: string; item: ChecklistItem }[];
62
+ now: Date;
63
+ mode?: 'shadow' | 'live';
64
+ /** Required for live external adoption; the unresolved daily limit has no implicit value. */
65
+ dailyCap?: number;
66
+ adoptedToday?: number;
67
+ allowedLicenses?: readonly string[];
68
+ placement?: PlacementDeps;
69
+ adopt?: (candidate: RequirementCandidate, version: string, cellId: string, evidence: string) => void;
70
+ }
71
+
72
+ /** How many of the last 24 hours' unlinked directives the single P0 brief line names (the full backlog is one count). */
73
+ export const UNLINKED_DIRECTIVES_SHOWN = 5;
74
+ /** ABSORB-ADOPT daily limit when config sets none (OP 10-06 22:59 · the cell's own number). */
75
+ export const DEFAULT_DAILY_CAP = 5;
76
+ /** Directive row fields the funnel reads — kept local because `src/directives/directive-index` is excluded from the public export. */
77
+ export interface DirectiveRow { ts: string; track: string | null; text: string; source_file: string; line_no: number }
78
+
79
+ /** Loads the private directive index at run time only (same pattern as decisions/proact-meter), so public builds bundle cleanly. */
80
+ function openDirectiveIndex(stateDir: string): { dailyRequirements(since: string, until: string): DirectiveRow[]; close(): void } {
81
+ const spec = ['..', 'directives', 'directive-index.js'].join('/');
82
+ const mod = createRequire(import.meta.url)(spec) as { DirectiveIndex: new (o: { stateDir?: string }) => ReturnType<typeof openDirectiveIndex> };
83
+ return new mod.DirectiveIndex({ stateDir });
84
+ }
85
+
86
+ const oneLine = (value: string) => value.replace(/\s+/g, ' ').trim();
87
+ const idFor = (source: RequirementSource, key: string) => createHash('sha256').update(`${source}:${key}`).digest('hex').slice(0, 16);
88
+
89
+ export function directiveCandidates(rows: readonly DirectiveRow[]): RequirementCandidate[] {
90
+ return rows.map(row => ({ id: idFor('directive', `${row.source_file}:${row.line_no}`), source: 'directive',
91
+ text: oneLine(row.text), link: `${row.source_file}#L${row.line_no}`, receivedAt: row.ts,
92
+ priority: 'P2', owner: row.track && /^[A-Z]{2,8}$/.test(row.track) ? row.track : 'OP' }));
93
+ }
94
+ export function intakeCandidates(rows: readonly IntakeItem[]): RequirementCandidate[] {
95
+ return rows.map(row => ({ id: row.id, source: 'intake', text: oneLine(row.text || row.title || ''),
96
+ link: row.url || `intake:${row.id}`, receivedAt: row.observedAt }));
97
+ }
98
+ export function suppliedCandidates(source: 'grounding' | 'coordination' | 'linear', rows: readonly IncomingRequirement[]): RequirementCandidate[] {
99
+ return rows.map(row => ({ ...row, source, text: oneLine(row.text) }));
100
+ }
101
+
102
+ function linkFor(candidate: RequirementCandidate, cells: FunnelOptions['cells']): FunnelOptions['cells'][number] | undefined {
103
+ return cells.find(({ item }) => !!candidate.link && !!item.evidence && item.evidence.includes(candidate.link));
104
+ }
105
+ function gate(candidate: RequirementCandidate, options: FunnelOptions, used: number): string | null {
106
+ if (candidate.priority !== 'P1' && candidate.priority !== 'P2') return 'P1/P2 판정 없음';
107
+ if (candidate.priority === 'P1' && !candidate.deadlineVersion) return 'P1 마감 판 미확인';
108
+ if (candidate.risk === undefined || !Number.isFinite(candidate.risk) || candidate.risk > 1 || candidate.risk < 0) return '위험≤1 미확인';
109
+ if (candidate.cost !== 0) return '돈 0 미확인';
110
+ if (candidate.security !== 0) return '보안 0 미확인';
111
+ if (!candidate.license || !options.allowedLicenses?.includes(candidate.license)) return '허용 라이선스 미확인';
112
+ if (candidate.patentPrivatePath !== false) return '특허 비공개 경로 무접촉 미확인';
113
+ if (options.dailyCap === undefined || !Number.isSafeInteger(options.dailyCap) || options.dailyCap < 0) return '하루 상한 미설정';
114
+ if (used >= options.dailyCap) return '하루 상한 도달';
115
+ return null;
116
+ }
117
+
118
+ /** RUBRIC-PLACE: use the release placement engine's priority, deadline, capacity and predecessor checks, not an invented score. */
119
+ export function runRequirementFunnel(options: FunnelOptions): RequirementFunnelResult {
120
+ const { sources, cells, now } = options;
121
+ const arrivals = [
122
+ ...directiveCandidates(sources.directives ?? []), ...intakeCandidates(sources.intake ?? []),
123
+ ...suppliedCandidates('grounding', sources.grounding ?? []),
124
+ ...suppliedCandidates('coordination', sources.coordination ?? []),
125
+ ...suppliedCandidates('linear', sources.linear ?? []),
126
+ ].filter(candidate => candidate.text && Number.isFinite(Date.parse(candidate.receivedAt)) && Date.parse(candidate.receivedAt) <= now.getTime());
127
+ const counts: RequirementFunnelResult['counts'] = { directive: 0, intake: 0, grounding: 0, coordination: 0, linear: 0 };
128
+ for (const candidate of arrivals) counts[candidate.source]++;
129
+ const groups = new Map<string, RequirementCandidate[]>();
130
+ for (const candidate of arrivals) {
131
+ const key = normalizeClaim(candidate.text);
132
+ groups.set(key, [...(groups.get(key) ?? []), candidate]);
133
+ }
134
+ const decisions: RequirementDecision[] = [];
135
+ let used = options.adoptedToday ?? 0;
136
+ for (const members of groups.values()) {
137
+ members.sort((a, b) => a.receivedAt.localeCompare(b.receivedAt));
138
+ const candidate = members.find(member => member.source === 'directive') ?? members[0]!;
139
+ const linked = members.map(member => linkFor(member, cells)).find(Boolean);
140
+ const evidence = members.map(member => `${member.link} (${member.receivedAt})`).join(' · ');
141
+ if (linked) {
142
+ decisions.push({ candidate, sources: members, status: 'adopted', cellId: linked.item.id, version: linked.version,
143
+ rubric: `RUBRIC-PLACE ${linked.item.priority ?? '미지정'} · ${linked.version}` });
144
+ continue;
145
+ }
146
+ const isDirective = members.some(member => member.source === 'directive');
147
+ const reason = isDirective ? null : gate(candidate, options, used);
148
+ if (reason) {
149
+ decisions.push({ candidate, sources: members, status: 'proposal', reason, rubric: `ABSORB-ADOPT · ${reason}` });
150
+ continue;
151
+ }
152
+ const cellId = candidate.id;
153
+ const priority = candidate.priority ?? 'P1';
154
+ const rubricInput = { id: cellId, title: candidate.text, owner: candidate.owner ?? 'OP', priority,
155
+ predecessors: [], ...(priority === 'P1' && candidate.deadlineVersion ? { deadlineVersion: candidate.deadlineVersion } : {}) };
156
+ let placement: ReturnType<typeof placeCell>;
157
+ try { placement = placeCell(rubricInput, { ...options.placement, now, dryRun: true }); }
158
+ catch (error) {
159
+ const reason = error instanceof Error ? error.message : String(error);
160
+ decisions.push({ candidate, sources: members, status: isDirective ? 'would-adopt' : 'proposal', reason,
161
+ rubric: `RUBRIC-PLACE · 판 배치 보류: ${reason}` });
162
+ continue;
163
+ }
164
+ const rubric = `RUBRIC-PLACE ${priority} · ${placement.version}: ${placement.reason}`;
165
+ if (options.mode === 'live') {
166
+ if (!options.adopt) throw new Error('편입 기록자가 설정되지 않았다');
167
+ options.adopt(candidate, placement.version, cellId, evidence);
168
+ }
169
+ decisions.push({ candidate, sources: members, status: options.mode === 'live' ? 'adopted' : 'would-adopt',
170
+ cellId, version: placement.version, rubric });
171
+ if (!isDirective) used++;
172
+ }
173
+ const unlinkedDirectives = [...directiveCandidates(options.historicalDirectives ?? []).filter(candidate =>
174
+ candidate.text && Number.isFinite(Date.parse(candidate.receivedAt)) && Date.parse(candidate.receivedAt) <= now.getTime()
175
+ && !linkFor(candidate, cells) && !arrivals.some(arrival => arrival.id === candidate.id)),
176
+ ...decisions.filter(decision => decision.status !== 'adopted')
177
+ .flatMap(decision => decision.sources.filter(source => source.source === 'directive'))]
178
+ .sort((a, b) => a.receivedAt.localeCompare(b.receivedAt));
179
+ const briefItems: AddBriefItem[] = [];
180
+ const summary = `들어온 요구 ${groups.size}(문별 지시 ${counts.directive} · 흡수 ${counts.intake} · 그라운딩 ${counts.grounding} · 조율 ${counts.coordination} · Linear ${counts.linear}) · 편입 ${decisions.filter(d => d.status === 'adopted').length} · 했을 것 ${decisions.filter(d => d.status === 'would-adopt').length} · 제안 ${decisions.filter(d => d.status === 'proposal').length}`;
181
+ if (arrivals.length || unlinkedDirectives.length) briefItems.push({ text: summary, domain: '흡수', priority: 'P1', source: 'requirement-funnel' });
182
+ for (const decision of decisions) {
183
+ const { candidate } = decision;
184
+ const status = decision.status === 'adopted' ? `편입 ${decision.cellId} · 판 ${decision.version}`
185
+ : decision.status === 'would-adopt' ? decision.version ? `했을 것 ${decision.cellId} · 판 ${decision.version}` : '편입 대상 · 판 배치 대기'
186
+ : `제안: ${decision.reason}`;
187
+ briefItems.push({ text: `${status} · ${candidate.text} · ${decision.rubric}`, domain: decision.status === 'proposal' ? '흡수' : '판',
188
+ priority: 'P1', source: 'requirement-funnel', evidence: decision.sources.map(member => `${member.link} (${member.receivedAt})`).join(' · ') });
189
+ }
190
+ // OP 10-06 22:59: the backlog can hold the whole directive history, so only the last 24 hours get named (one P0 line);
191
+ // the full backlog is a single count. Nothing recent → the count alone, at P1, so history cannot flood the brief.
192
+ if (unlinkedDirectives.length) {
193
+ const recent = unlinkedDirectives.filter(directive => now.getTime() - Date.parse(directive.receivedAt) <= 24 * 3_600_000);
194
+ const named = recent.slice(0, UNLINKED_DIRECTIVES_SHOWN);
195
+ const rest = recent.length - named.length;
196
+ const total = `전체 누계 ${unlinkedDirectives.length}`;
197
+ briefItems.push(recent.length
198
+ ? { text: `대표 지시 중 아직 칸 없는 것 — 최근 24시간 ${recent.length}: ${named.map(directive => `${directive.text} (${directive.receivedAt})`).join(' · ')}${rest > 0 ? ` · 외 ${rest}` : ''} · ${total}`,
199
+ domain: '판', priority: 'P0', source: 'requirement-funnel', evidence: named.map(directive => directive.link).join(' · ') }
200
+ : { text: `대표 지시 중 아직 칸 없는 것 — 최근 24시간 0 · ${total}`, domain: '판', priority: 'P1', source: 'requirement-funnel' });
201
+ }
202
+ return { counts, candidates: groups.size, decisions, unlinkedDirectives, briefItems };
203
+ }
204
+
205
+ /** Existing ledgers only; no source collector or sender is started here. */
206
+ export function readRequirementFunnel(options: { stateDir?: string; now?: Date; mode?: 'shadow' | 'live'; dailyCap?: number;
207
+ allowedLicenses?: readonly string[]; grounding?: readonly IncomingRequirement[]; coordination?: readonly IncomingRequirement[];
208
+ linear?: readonly IncomingRequirement[]; placement?: PlacementDeps } = {}): RequirementFunnelResult {
209
+ const stateDir = options.stateDir ?? elanousStateRoot();
210
+ const now = options.now ?? new Date();
211
+ const config = (getUserConfig().raw.intake as { requirementFunnel?: { mode?: 'shadow' | 'live'; dailyCap?: number; allowedLicenses?: string[] } } | undefined)?.requirementFunnel;
212
+ const mode = options.mode ?? config?.mode ?? 'shadow';
213
+ if (mode !== 'shadow' && mode !== 'live') throw new Error(`알 수 없는 요구 깔때기 모드: ${mode}`);
214
+ const dailyCap = options.dailyCap ?? config?.dailyCap ?? DEFAULT_DAILY_CAP;
215
+ const allowedLicenses = options.allowedLicenses ?? config?.allowedLicenses;
216
+ let directives: DirectiveRow[] = [];
217
+ let historicalDirectives: DirectiveRow[] = [];
218
+ const since = new Date(Date.parse(`${dateKey(now, { timeZone: 'Asia/Seoul' })}T00:00:00+09:00`)).toISOString();
219
+ try {
220
+ const index = openDirectiveIndex(stateDir);
221
+ try {
222
+ directives = index.dailyRequirements(since, now.toISOString());
223
+ historicalDirectives = index.dailyRequirements('1970-01-01T00:00:00.000Z', since);
224
+ } finally { index.close(); }
225
+ } catch (error) {
226
+ // Public builds have no directive index; the other doors still report.
227
+ debug.log('intake.requirement-funnel', 'directives-unavailable', { error: error instanceof Error ? error.message : String(error) });
228
+ }
229
+ const schedules = listSchedules();
230
+ const cells = schedules.flatMap(schedule => listChecklist(schedule.version).items.map(item => ({ version: schedule.version, item })));
231
+ const ledger = loadIntakeLedger(stateDir);
232
+ const day = dateKey(now, { timeZone: 'Asia/Seoul' });
233
+ const todayCount = cells.filter(({ item }) => {
234
+ const adoptedAt = item.evidence?.match(/ABSORB-ADOPT@(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z)/)?.[1];
235
+ return adoptedAt && dateKey(adoptedAt, { timeZone: 'Asia/Seoul' }) === day;
236
+ }).length;
237
+ const cardRequests: Record<'coordination' | 'linear', IncomingRequirement[]> = { coordination: [], linear: [] };
238
+ if (existsSync(cardIndexPath(stateDir))) {
239
+ const cards = new CardStore(stateDir, true);
240
+ try {
241
+ for (const card of cards.listCards()) {
242
+ const section = card.sections.find(entry => entry.key === 'intake');
243
+ if (!section) continue;
244
+ let raw: unknown;
245
+ try { raw = JSON.parse(section.content); } catch { continue; }
246
+ if (!raw || typeof raw !== 'object') continue;
247
+ const input = raw as { source?: string; text?: string; ref?: string; replyTo?: { surface?: string } };
248
+ const source = input.source === 'linear' ? 'linear' : input.source === 'coordination' ? 'coordination' : null;
249
+ if (!source || !input.text) continue;
250
+ cardRequests[source].push({ id: card.goalId, text: oneLine(input.text.split('\n')[0] ?? ''),
251
+ link: input.ref || card.goalId, receivedAt: card.createdAt });
252
+ }
253
+ } finally { cards.close(); }
254
+ }
255
+ const fromQueue = (name: string): IncomingRequirement[] => {
256
+ const path = join(stateDir, 'intake', name);
257
+ if (!existsSync(path)) return [];
258
+ return readFileSync(path, 'utf8').split('\n').flatMap(line => {
259
+ if (!line.trim()) return [];
260
+ try {
261
+ const value: unknown = JSON.parse(line);
262
+ if (!value || typeof value !== 'object') return [];
263
+ const row = value as Partial<IncomingRequirement> & { path?: string };
264
+ const item = row.id ? ledger.items.get(row.id) : undefined;
265
+ const text = row.text ?? item?.text ?? item?.title;
266
+ const link = row.link ?? item?.url ?? row.path;
267
+ const receivedAt = row.receivedAt ?? (row as { at?: string }).at ?? item?.observedAt;
268
+ return typeof row.id === 'string' && typeof text === 'string' && typeof link === 'string' && typeof receivedAt === 'string'
269
+ ? [{ ...row, id: row.id, text, link, receivedAt }] : [];
270
+ } catch { return []; }
271
+ });
272
+ };
273
+ // The grounding queue and the card store are cumulative; only today's rows are today's arrivals.
274
+ const today = (rows: readonly IncomingRequirement[]) => rows.filter(row => Date.parse(row.receivedAt) >= Date.parse(since));
275
+ return runRequirementFunnel({ sources: { directives, intake: [...ledger.items.values()].filter(item => Date.parse(item.observedAt) >= Date.parse(since)),
276
+ grounding: today(options.grounding ?? fromQueue('outbox/grounding.jsonl')),
277
+ coordination: today(options.coordination ?? cardRequests.coordination), linear: today(options.linear ?? cardRequests.linear) }, historicalDirectives, cells, now, mode,
278
+ dailyCap, adoptedToday: todayCount, allowedLicenses, placement: { ...options.placement, schedules },
279
+ adopt: (candidate, version, cellId, evidence) => {
280
+ placeCell({ id: cellId, title: candidate.text, owner: candidate.owner ?? 'OP', priority: candidate.priority ?? 'P1',
281
+ predecessors: [], ...(candidate.deadlineVersion ? { deadlineVersion: candidate.deadlineVersion } : {}) }, { ...options.placement, now, schedules });
282
+ setItem(version, cellId, { evidence: `${evidence} · ABSORB-ADOPT@${now.toISOString()}` }, 'OP');
283
+ } });
284
+ }
285
+
286
+ // ── `intake candidates` — 읽기 전용 요구 후보 목록(지시·흡수·소원 · 명시 루브릭만 점수) ──
287
+ export interface FunnelCandidate {
288
+ source: 'directive' | 'intake' | 'wish';
289
+ id: string;
290
+ title: string;
291
+ score: number | null;
292
+ grade: 'P1' | 'P2' | 'P3' | 'P4' | null;
293
+ }
294
+ /** The candidate list keys directives by session — the funnel's local `DirectiveRow` plus the index's `session_id`. */
295
+ export type FunnelDirectiveRow = DirectiveRow & { session_id: string };
296
+
297
+ function funnelCandidate(source: FunnelCandidate['source'], id: string, title: string, original: string): FunnelCandidate {
298
+ const rubric = parseRubric(original);
299
+ const score = rubric === null ? null : rubricScore(rubric);
300
+ return { source, id, title: title.split(/\r?\n/, 1)[0]!.slice(0, 80), score, grade: score === null ? null : rubricGrade(score) };
301
+ }
302
+
303
+ export function collectFunnelCandidates({ directives, intakeItems, cards }: {
304
+ directives: readonly FunnelDirectiveRow[];
305
+ intakeItems: readonly IntakeItem[];
306
+ cards: readonly TaskCard[];
307
+ }): FunnelCandidate[] {
308
+ return [
309
+ ...directives.map((row) => funnelCandidate('directive', `${row.session_id}:${row.line_no}`, row.text, row.text)),
310
+ ...intakeItems.filter((item) => item.status !== 'discarded')
311
+ .map((item) => funnelCandidate('intake', item.id, item.title ?? item.text ?? item.url ?? '', [item.title, item.text, item.url].filter(Boolean).join('\n'))),
312
+ ...cards.filter((card) => card.status === 'open' && card.goalId.startsWith('wish:'))
313
+ .map((card) => funnelCandidate('wish', card.goalId, card.title, card.title)),
314
+ ].sort((a, b) => a.score === null && b.score === null ? 0
315
+ : a.score === null ? 1 : b.score === null ? -1 : b.score - a.score);
316
+ }
@@ -1,6 +1,6 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import { LessonLedger, type LessonRow } from '../lessons/lesson-ledger.js';
3
- import { DecisionLedger, type DecisionEntry } from '../decisions/decision-ledger.js';
3
+ import { DecisionLedger, type DecisionEntry, type SeatDecisionRecord } from '../decisions/decision-ledger.js';
4
4
  import { devVersion, listChecklist, type Checklist } from '../release-loop/checklist.js';
5
5
  import { debug } from '../debug/log.js';
6
6
 
@@ -31,6 +31,7 @@ export interface KnowResult {
31
31
  export interface KnowFindDeps {
32
32
  directives: (query: string) => DirectiveRow[];
33
33
  decisions: () => DecisionEntry[];
34
+ seatDecisions: () => SeatDecisionRecord[];
34
35
  checklist: (version: string) => Checklist;
35
36
  lessons: (query: string) => LessonRow[];
36
37
  versions: () => string[];
@@ -49,6 +50,7 @@ const defaults: KnowFindDeps = {
49
50
  try { return index.search(query); } finally { index.close(); }
50
51
  },
51
52
  decisions: () => new DecisionLedger().list({ status: 'all' }),
53
+ seatDecisions: () => new DecisionLedger().seatReport(),
52
54
  checklist: (version) => listChecklist(version),
53
55
  lessons: (query) => new LessonLedger().find(query),
54
56
  versions,
@@ -84,6 +86,13 @@ export function knowFind(query: string, deps: Partial<KnowFindDeps> = {}): KnowR
84
86
  ref: row.refs?.[0] ?? `decisions show ${row.id}` });
85
87
  }
86
88
  });
89
+ attempt('decision', () => {
90
+ for (const row of read.seatDecisions()) {
91
+ if (!matches([row.title, row.decision, row.delegation].join(' '), words)) continue;
92
+ rows.push({ source: 'decision', id: row.id, title: row.title, status: 'seat-posthoc',
93
+ at: row.decidedAt ?? row.recordedAt, current: false, ref: `decisions seat-report --seat ${row.seat}` });
94
+ }
95
+ });
87
96
  attempt('checklist', () => {
88
97
  for (const version of new Set(read.versions())) {
89
98
  for (const row of read.checklist(version).items) {
@@ -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
+ }