elanous 0.2.17 → 0.2.19

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 (491) 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/J5H7BvqgSI8DUjtRRcA_S/_buildManifest.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/1157-cba0e8553d35a980.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/{726.83532d69452ee24b.js → 1470.98a6ab61ccb4131c.js} +1 -1
  7. package/apps/pwa/out/_next/static/chunks/171-23a877ed11720de9.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/2336-e830c777eb6bce04.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/{9247-269e4464cc4b491c.js → 2375-e061bb24cd2b5cde.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/{211-9c9090c59e2c5907.js → 2977-e38442e86289f1d3.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/{f6b7c9ba.64a895b8b2fedbdc.js → 2ea96958.e8c7e112de50b067.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/3094.67339318c7f72a3a.js +2 -0
  16. package/apps/pwa/out/_next/static/chunks/338-ceb956612dcfaa86.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/3395-835e4ceaa6335ba4.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +5 -0
  19. package/apps/pwa/out/_next/static/chunks/3441-be82e3595aef3a20.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/3689-89905039bf3e7257.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/3822-6f268bc26c6a84ac.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/{1268.0840ff91e083fcbd.js → 4323.c5dfb6f503304775.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/4457-d7df81c90f9d0268.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/4551-77dadae76f6cbace.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/{6110.1297d4093fdfbb0d.js → 4847.56f5bc454fbcc827.js} +2 -2
  28. package/apps/pwa/out/_next/static/chunks/4978.6fd93b974441e87f.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/5012-f746f6549b869f91.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/5791-bb82743fa6299562.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/{7327-2d69fb321ba73389.js → 6062-73e68343a9814f6a.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/62-5bfed1e23790b745.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/6320-9bc8561fde416e07.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/649-ca96d1c5d09e02b8.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/6726-85a7db59b288c53f.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/6735-33267c4630d0d963.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/7633-141682015873beb6.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/{935.4ddaa99fc1d1126c.js → 7647.2fcd30d894d5ee30.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/{f6231284-45726550aba08fca.js → 8046cadf-211ca3c0e00f2b72.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/8067-143791231c99bd02.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/8430-a10cfba8ef282d4e.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/8870-9614a213b990b10b.js +25 -0
  49. package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/9454-12c2f30dea62291c.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/976-4a9b4ebdca7c739a.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/9989-15cdf568bafa9fc8.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/ae787e4c-e5b1ef00b2dbceca.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/approvals/page-deef5ee191830f14.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-b193da485f9a14b2.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/board/page-b6e1a79df0e214f4.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/botlab/page-e84df4d907f703c7.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/bots/page-a5fb9a5db89ae4ea.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/ceo/page-1499f481d7fc52d6.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/chat/page-c7cf753aeb648790.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/consult/page-4118469e514e10fd.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/control/page-e968f7630654332a.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/decisions/page-505915052f947022.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/design-check/page-c2f962fb4a3b06c6.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/editor/page-7b5ed0bd8a589ec4.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/exec/page-cf8193250dd42dd4.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/field/page-cf3f5dfa35dea8e9.js +1 -0
  69. package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/inside/page-5b29d9b06ceadade.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/intake/page-a15e7fcc283e9ca7.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/layout-d2915e7b70ff3d0e.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/live/page-b70cd005d9169e25.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/loops/page-2595f0eba29f57e1.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/market/page-c776322454156e36.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-77bbd17f4435aa10.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/morning/page-049689e611ecc936.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/not-found-85954a4fb09c44eb.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/observatory/page-e773e552e1068ee6.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-5633c4bfa7684b4a.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-d829b92bd01b4994.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-061c317fc771becf.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/outputs/page-3a45ece8865ed4b6.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/app/page-0be990618c0d56bd.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/app/reflection/page-bfea4865dc244b73.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-0d6ba8e337741484.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/app/sessions/page-76a88b55d816482e.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-bba1542df59f61ce.js +1 -0
  89. package/apps/pwa/out/_next/static/chunks/app/settings/page-4185c95689bb372e.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b699e1a8d5d34b8b.js +1 -0
  91. package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +1 -0
  92. package/apps/pwa/out/_next/static/chunks/app/setup/page-c225e338c31bf274.js +1 -0
  93. package/apps/pwa/out/_next/static/chunks/app/share/page-449353f70507ca1f.js +1 -0
  94. package/apps/pwa/out/_next/static/chunks/app/showroom/page-90b889714102476d.js +1 -0
  95. package/apps/pwa/out/_next/static/chunks/app/tasks/page-f1ee7fbeb7f96ddc.js +1 -0
  96. package/apps/pwa/out/_next/static/chunks/app/term/page-d59a95e1434dfaa0.js +1 -0
  97. package/apps/pwa/out/_next/static/chunks/app/today/page-71491a23414d1a47.js +1 -0
  98. package/apps/pwa/out/_next/static/chunks/app/trace/page-de7112bdb65fa4fe.js +1 -0
  99. package/apps/pwa/out/_next/static/chunks/app/vault/page-feb4d0d88079660d.js +1 -0
  100. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f17d887b40dd6b1d.js +1 -0
  101. package/apps/pwa/out/_next/static/chunks/app/workflows/page-8da77be2192bcd5d.js +1 -0
  102. package/apps/pwa/out/_next/static/chunks/app/workspace/page-72c3b9a5e9582b29.js +1 -0
  103. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3afe4c8d0c48c39.js +1 -0
  104. package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +1 -0
  105. package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +1 -0
  106. package/apps/pwa/out/_next/static/chunks/main-app-b9308f2d8a931531.js +1 -0
  107. package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +1 -0
  108. package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +1 -0
  109. package/apps/pwa/out/_next/static/chunks/webpack-6f35f9d892fa630b.js +1 -0
  110. package/apps/pwa/out/_next/static/css/{e99e3cff94ced78a.css → 599369d853c61df7.css} +1 -1
  111. package/apps/pwa/out/_next/static/css/85f53c6f35d11382.css +1 -0
  112. package/apps/pwa/out/_next/static/css/d3427996da11e480.css +1 -0
  113. package/apps/pwa/out/approvals/index.html +1 -1
  114. package/apps/pwa/out/approvals/index.txt +23 -23
  115. package/apps/pwa/out/autopilot/index.html +1 -1
  116. package/apps/pwa/out/autopilot/index.txt +23 -23
  117. package/apps/pwa/out/board/index.html +1 -1
  118. package/apps/pwa/out/board/index.txt +23 -23
  119. package/apps/pwa/out/botlab/index.html +1 -1
  120. package/apps/pwa/out/botlab/index.txt +22 -22
  121. package/apps/pwa/out/bots/index.html +1 -1
  122. package/apps/pwa/out/bots/index.txt +22 -22
  123. package/apps/pwa/out/ceo/index.html +1 -1
  124. package/apps/pwa/out/ceo/index.txt +23 -23
  125. package/apps/pwa/out/chat/index.html +1 -1
  126. package/apps/pwa/out/chat/index.txt +22 -22
  127. package/apps/pwa/out/consult/index.html +1 -1
  128. package/apps/pwa/out/consult/index.txt +23 -23
  129. package/apps/pwa/out/control/index.html +1 -1
  130. package/apps/pwa/out/control/index.txt +23 -23
  131. package/apps/pwa/out/decisions/index.html +1 -0
  132. package/apps/pwa/out/decisions/index.txt +30 -0
  133. package/apps/pwa/out/design-check/index.html +1 -1
  134. package/apps/pwa/out/design-check/index.txt +23 -23
  135. package/apps/pwa/out/editor/index.html +1 -1
  136. package/apps/pwa/out/editor/index.txt +25 -24
  137. package/apps/pwa/out/exec/index.html +1 -1
  138. package/apps/pwa/out/exec/index.txt +23 -23
  139. package/apps/pwa/out/field/index.html +1 -1
  140. package/apps/pwa/out/field/index.txt +23 -23
  141. package/apps/pwa/out/healthz +1 -1
  142. package/apps/pwa/out/index.html +1 -1
  143. package/apps/pwa/out/index.txt +22 -22
  144. package/apps/pwa/out/inside/index.html +1 -1
  145. package/apps/pwa/out/inside/index.txt +24 -23
  146. package/apps/pwa/out/intake/index.html +1 -1
  147. package/apps/pwa/out/intake/index.txt +23 -23
  148. package/apps/pwa/out/live/index.html +1 -1
  149. package/apps/pwa/out/live/index.txt +23 -23
  150. package/apps/pwa/out/loops/index.html +1 -1
  151. package/apps/pwa/out/loops/index.txt +28 -27
  152. package/apps/pwa/out/market/index.html +1 -1
  153. package/apps/pwa/out/market/index.txt +22 -22
  154. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  155. package/apps/pwa/out/missions/placeholder/index.txt +22 -22
  156. package/apps/pwa/out/morning/index.html +1 -1
  157. package/apps/pwa/out/morning/index.txt +23 -23
  158. package/apps/pwa/out/observatory/index.html +1 -1
  159. package/apps/pwa/out/observatory/index.txt +23 -23
  160. package/apps/pwa/out/ops/checklist/index.html +1 -1
  161. package/apps/pwa/out/ops/checklist/index.txt +22 -22
  162. package/apps/pwa/out/ops/release/index.html +1 -1
  163. package/apps/pwa/out/ops/release/index.txt +22 -22
  164. package/apps/pwa/out/ops/seats/index.html +1 -1
  165. package/apps/pwa/out/ops/seats/index.txt +22 -22
  166. package/apps/pwa/out/outputs/index.html +1 -1
  167. package/apps/pwa/out/outputs/index.txt +22 -22
  168. package/apps/pwa/out/reflection/index.html +1 -1
  169. package/apps/pwa/out/reflection/index.txt +23 -23
  170. package/apps/pwa/out/scheduler/index.html +1 -1
  171. package/apps/pwa/out/scheduler/index.txt +23 -23
  172. package/apps/pwa/out/sessions/index.html +1 -1
  173. package/apps/pwa/out/sessions/index.txt +22 -22
  174. package/apps/pwa/out/settings/devices/index.html +1 -1
  175. package/apps/pwa/out/settings/devices/index.txt +23 -23
  176. package/apps/pwa/out/settings/index.html +1 -1
  177. package/apps/pwa/out/settings/index.txt +23 -23
  178. package/apps/pwa/out/setup/done/index.html +1 -1
  179. package/apps/pwa/out/setup/done/index.txt +23 -23
  180. package/apps/pwa/out/setup/index.html +1 -1
  181. package/apps/pwa/out/setup/index.txt +23 -23
  182. package/apps/pwa/out/share/index.html +1 -1
  183. package/apps/pwa/out/share/index.txt +23 -23
  184. package/apps/pwa/out/showroom/index.html +1 -1
  185. package/apps/pwa/out/showroom/index.txt +22 -22
  186. package/apps/pwa/out/tasks/index.html +1 -1
  187. package/apps/pwa/out/tasks/index.txt +23 -23
  188. package/apps/pwa/out/term/index.html +1 -1
  189. package/apps/pwa/out/term/index.txt +23 -23
  190. package/apps/pwa/out/today/index.html +1 -1
  191. package/apps/pwa/out/today/index.txt +23 -23
  192. package/apps/pwa/out/trace/index.html +1 -1
  193. package/apps/pwa/out/trace/index.txt +23 -23
  194. package/apps/pwa/out/vault/index.html +1 -1
  195. package/apps/pwa/out/vault/index.txt +23 -23
  196. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  197. package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
  198. package/apps/pwa/out/workflows/index.html +1 -1
  199. package/apps/pwa/out/workflows/index.txt +23 -23
  200. package/apps/pwa/out/workspace/index.html +1 -1
  201. package/apps/pwa/out/workspace/index.txt +23 -23
  202. package/apps/pwa/out/worktrees/index.html +1 -1
  203. package/apps/pwa/out/worktrees/index.txt +23 -23
  204. package/graphs/ops/guardian.yaml +26 -0
  205. package/graphs/ops/recipes.yaml +3 -0
  206. package/package.json +2 -1
  207. package/samples/workflows/templates/approval-gated-deploy.yaml +30 -0
  208. package/samples/workflows/templates/chat-driven-research.yaml +35 -0
  209. package/samples/workflows/templates/discord-listen-and-respond.yaml +29 -0
  210. package/samples/workflows/templates/http-poll-then-alert.yaml +30 -0
  211. package/samples/workflows/templates/manual-quickstart.yaml +21 -0
  212. package/samples/workflows/templates/scheduled-daily-digest.yaml +30 -0
  213. package/samples/workflows/templates/webhook-deploy-hook.yaml +27 -0
  214. package/scripts/ci-public-export-import-gate.ts +7 -2
  215. package/scripts/ci-typecheck-changed.ts +21 -6
  216. package/scripts/coord-channel-watch.sh +2 -2
  217. package/scripts/discord-seat-probe.ts +6 -3
  218. package/scripts/docs/generate-cli-anatomy.ts +162 -0
  219. package/scripts/goals/authoring-metrics.ts +73 -0
  220. package/scripts/landing-heal/must-fix-match.ts +7 -7
  221. package/scripts/landing-heal/run-mission.ts +3 -2
  222. package/scripts/lib/pch-regress-map.ts +23 -0
  223. package/scripts/morning-report.ts +18 -4
  224. package/scripts/pch-regress.ts +88 -0
  225. package/scripts/public-export-test-run.ts +37 -2
  226. package/scripts/release-loop/checklist-gate-node.ts +13 -11
  227. package/scripts/release-loop/cut-branch.ts +2 -1
  228. package/scripts/release-loop/cutoff.ts +4 -2
  229. package/scripts/release-loop/docs-land-node.ts +8 -1
  230. package/scripts/release-loop/docs-node.ts +1 -1
  231. package/scripts/release-loop/gate-diff.ts +18 -0
  232. package/scripts/release-loop/gate-node.ts +436 -59
  233. package/scripts/release-loop/gate-partial.ts +106 -0
  234. package/scripts/release-loop/gate-timing.ts +120 -0
  235. package/scripts/release-loop/node-verdict.ts +2 -1
  236. package/scripts/release-loop/npm-publish-node.ts +11 -7
  237. package/scripts/release-loop/ops-upgrade-node.ts +8 -1
  238. package/scripts/release-loop/preflight.ts +246 -0
  239. package/scripts/release-loop/publish-node.ts +8 -1
  240. package/scripts/release-loop/release-readiness.ts +11 -6
  241. package/scripts/release-loop/release-version.ts +48 -0
  242. package/scripts/release-loop/resume-release.ts +174 -0
  243. package/scripts/release-loop/shard-plan.ts +65 -0
  244. package/scripts/release-loop/unattended-release.ts +45 -7
  245. package/scripts/release-loop/version-node.ts +145 -40
  246. package/scripts/rhythm/daily.ts +25 -4
  247. package/scripts/skills/public-check.ts +62 -12
  248. package/scripts/test-deterministic.ts +51 -33
  249. package/scripts/test-diet/effectiveness.ts +62 -1
  250. package/scripts/test-diet/lib.ts +1 -1
  251. package/scripts/test-diet/node.ts +8 -1
  252. package/scripts/video/scene-table.ts +159 -0
  253. package/src/acp/core-turn-bridge.ts +39 -2
  254. package/src/agent-mission/review-loop.ts +4 -3
  255. package/src/agent-substrate/pr-comment-meta.ts +20 -0
  256. package/src/autopilot/build/llm-conflict-merge.ts +28 -8
  257. package/src/away/presence.ts +40 -0
  258. package/src/away/release-watch.ts +255 -0
  259. package/src/briefing/brief-items.ts +86 -17
  260. package/src/chat/index.ts +2 -1
  261. package/src/chat/short-question.ts +3 -40
  262. package/src/cli/autopilot-cli.ts +30 -11
  263. package/src/cli/away-cli.ts +68 -0
  264. package/src/cli/brief-cli.ts +36 -2
  265. package/src/cli/freeze-cli.ts +3 -2
  266. package/src/cli/guardian-cli.ts +49 -0
  267. package/src/cli/intake-cli.ts +85 -7
  268. package/src/cli/log-event-names.ts +1 -0
  269. package/src/cli/notify-cli.ts +75 -0
  270. package/src/cli/pod-cli.ts +5 -1
  271. package/src/cli/provider-cli.ts +12 -2
  272. package/src/cli/pty-drive-cli.ts +14 -3
  273. package/src/cli/release-cli.ts +128 -19
  274. package/src/cli/setup-cli.ts +31 -1
  275. package/src/cli/tasks-cli.ts +403 -4
  276. package/src/context-bus/condense.ts +19 -3
  277. package/src/context-bus/context-day.ts +77 -24
  278. package/src/context-bus/context-first.ts +33 -0
  279. package/src/context-bus/context-now-public.ts +1 -1
  280. package/src/context-bus/context-now-surfaces.ts +16 -6
  281. package/src/context-bus/context-now.ts +97 -3
  282. package/src/core-turn/short-question.ts +100 -0
  283. package/src/dashboard/dashboard-status-lines.ts +24 -4
  284. package/src/dashboard/index.ts +50 -5
  285. package/src/dashboard/input/slash-executor.ts +6 -2
  286. package/src/dashboard/slash-runtime/dashboard-handlers.ts +63 -36
  287. package/src/dashboard/slash-runtime/decide-slash.ts +60 -0
  288. package/src/dashboard/slash-runtime/help-from-registry.ts +21 -8
  289. package/src/dashboard/slash-runtime/help-groups.ts +44 -0
  290. package/src/dashboard/slash-runtime/model-choices.ts +125 -0
  291. package/src/discord-self-message.ts +20 -2
  292. package/src/discord-slash-wire.ts +1 -0
  293. package/src/domains/guardian-tick.ts +138 -0
  294. package/src/domains/outbound-alert.ts +244 -40
  295. package/src/domains/outbound-log-probe.ts +16 -1
  296. package/src/domains/release-tool.ts +2 -2
  297. package/src/domains/schedule-registry.ts +23 -0
  298. package/src/domains/telegram-kind-route.ts +39 -0
  299. package/src/domains/watch-default-scheduler.ts +95 -0
  300. package/src/esc-abort-gate.ts +44 -0
  301. package/src/execution-loop/gates.ts +11 -2
  302. package/src/flow-loop/split.ts +8 -4
  303. package/src/git-fs/worktree.ts +23 -3
  304. package/src/graph-runner/runner.ts +25 -5
  305. package/src/harness/harness-cli-command.ts +12 -2
  306. package/src/harness/harness-queue.ts +388 -33
  307. package/src/harness/harness-salvage-cli.ts +78 -0
  308. package/src/harness/harness-worktree-add.ts +68 -0
  309. package/src/harness/helper-scan.ts +10 -3
  310. package/src/harness/ledger-write-guard.ts +6 -16
  311. package/src/hq/hq.ts +6 -1
  312. package/src/hq/seats.ts +369 -0
  313. package/src/index.ts +185 -18
  314. package/src/instance/test-write-guard-spy-first.fixture.ts +13 -0
  315. package/src/instance/test-write-guard.ts +200 -0
  316. package/src/intake-plane/adopt-gate.ts +49 -0
  317. package/src/intake-plane/check.ts +3 -0
  318. package/src/intake-plane/collect-github.ts +57 -25
  319. package/src/intake-plane/digest.ts +39 -15
  320. package/src/intake-plane/discord-seat-work.ts +8 -2
  321. package/src/intake-plane/harness-target.ts +4 -3
  322. package/src/intake-plane/intake-sources.ts +1 -0
  323. package/src/intake-plane/items.ts +1 -0
  324. package/src/intake-plane/requirement-funnel.ts +316 -0
  325. package/src/knowledge/know-find.ts +10 -1
  326. package/src/loops/budget.ts +36 -0
  327. package/src/loops/orchestrator/degradation.ts +57 -0
  328. package/src/loops/orchestrator/finish-rate.ts +106 -4
  329. package/src/loops/orchestrator/grid-place.ts +11 -0
  330. package/src/loops/orchestrator/tick.ts +450 -54
  331. package/src/maturity/feature-maturity.ts +8 -5
  332. package/src/maturity/tui-slash-maturity.ts +1 -0
  333. package/src/mss/logging/log-store.ts +3 -0
  334. package/src/nexus/api/decisions-route.ts +11 -1
  335. package/src/nexus/api/http-server.ts +16 -5
  336. package/src/nexus/api/loop-edges.ts +352 -23
  337. package/src/nexus/api/ops-api.ts +96 -28
  338. package/src/nexus/api/ops-seats-sources.ts +2 -2
  339. package/src/nexus/api/workflow-templates.ts +3 -0
  340. package/src/nexus/index.ts +7 -0
  341. package/src/oauth/codex.ts +38 -0
  342. package/src/onboarding/first-chat-gate.ts +29 -0
  343. package/src/release-loop/checklist.ts +106 -6
  344. package/src/release-loop/feature-store.ts +64 -16
  345. package/src/release-loop/landing-freeze.ts +6 -4
  346. package/src/release-loop/light-rc.ts +173 -0
  347. package/src/release-loop/placement.ts +64 -20
  348. package/src/release-loop/rubric.ts +136 -0
  349. package/src/seat-dispatch/seat-ask.ts +79 -16
  350. package/src/seat-loop/seat-loop.ts +12 -6
  351. package/src/self-dev/ask-launch-flow.ts +24 -3
  352. package/src/self-dev/ask-path-candidates.ts +68 -0
  353. package/src/self-dev/dev-cli.ts +36 -0
  354. package/src/self-dev/dev-pipeline.ts +4 -0
  355. package/src/self-dev/draft-triage-rules.ts +1 -1
  356. package/src/self-dev/orchestrate-cli.ts +22 -0
  357. package/src/self-dev/orchestrate.ts +1 -0
  358. package/src/self-dev/pr-preview.ts +179 -0
  359. package/src/self-dev/round-pr-supersede.ts +244 -0
  360. package/src/self-dev/run-supervisor.ts +14 -0
  361. package/src/self-dev/shard-run.ts +23 -7
  362. package/src/self-implement/follow-up-goals.ts +2 -2
  363. package/src/self-implement/gate-cli.ts +29 -8
  364. package/src/self-implement/gate-remote.ts +427 -0
  365. package/src/self-implement/goal-author-clarification.ts +88 -13
  366. package/src/self-implement/goal-author.ts +8 -3
  367. package/src/self-implement/goal-clarification-escalation.ts +2 -0
  368. package/src/self-implement/l8-merge-queue.ts +29 -5
  369. package/src/self-implement/orchestrator.ts +61 -13
  370. package/src/self-implement/prior-draft-findings.ts +4 -3
  371. package/src/self-implement/salvage-branches.ts +254 -0
  372. package/src/skills/skill-feedback.ts +76 -0
  373. package/src/task-agent/actions.ts +101 -7
  374. package/src/task-agent/board.ts +132 -0
  375. package/src/task-agent/card-evidence.ts +203 -0
  376. package/src/task-agent/completion-evidence.ts +126 -0
  377. package/src/task-agent/judge.ts +49 -5
  378. package/src/task-agent/live-moves.ts +176 -0
  379. package/src/task-agent/mission.ts +449 -0
  380. package/src/task-agent/shadow.ts +258 -0
  381. package/src/task-agent/task-hand.ts +337 -0
  382. package/src/task-orchestrator/surfaces/pod-command-job.ts +64 -4
  383. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +10 -0
  384. package/src/task-orchestrator/surfaces/pod-lease.ts +142 -13
  385. package/src/task-orchestrator/surfaces/pod-pool.ts +13 -5
  386. package/src/task-orchestrator/surfaces/self-implement-pod.ts +88 -20
  387. package/src/telegram-away-command.ts +38 -0
  388. package/src/telegram-commands.ts +11 -0
  389. package/src/telegram-loops-command.ts +1 -1
  390. package/src/telegram-report-log-probe.ts +21 -0
  391. package/src/telegram-report.ts +27 -3
  392. package/src/telegram.ts +5 -20
  393. package/src/tui-client/dashboard-session.ts +1 -0
  394. package/src/user-config.ts +196 -4
  395. package/src/version/packed-revision.json +1 -1
  396. package/apps/pwa/out/_next/static/chunks/1307-aac7332576c4d6fc.js +0 -1
  397. package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +0 -1
  398. package/apps/pwa/out/_next/static/chunks/1844-9ec4b1d12f6e80eb.js +0 -1
  399. package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +0 -1
  400. package/apps/pwa/out/_next/static/chunks/1948.15f356b15a625597.js +0 -5
  401. package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.js +0 -1
  402. package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +0 -1
  403. package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +0 -1
  404. package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +0 -1
  405. package/apps/pwa/out/_next/static/chunks/311.e685738f92e6e2ec.js +0 -1
  406. package/apps/pwa/out/_next/static/chunks/3194-807b212fb8eae065.js +0 -1
  407. package/apps/pwa/out/_next/static/chunks/3324-d1d2979821dfa845.js +0 -1
  408. package/apps/pwa/out/_next/static/chunks/3558-bbb5ab553a0471d3.js +0 -1
  409. package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.js +0 -1
  410. package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +0 -1
  411. package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +0 -1
  412. package/apps/pwa/out/_next/static/chunks/4614-21403aac730d4586.js +0 -1
  413. package/apps/pwa/out/_next/static/chunks/4795-f372490b178583fc.js +0 -1
  414. package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +0 -1
  415. package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +0 -1
  416. package/apps/pwa/out/_next/static/chunks/5360-be1f1d8f9fcc9755.js +0 -1
  417. package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +0 -1
  418. package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +0 -1
  419. package/apps/pwa/out/_next/static/chunks/6158.9c0449a9a8d0064b.js +0 -2
  420. package/apps/pwa/out/_next/static/chunks/6877-fe94e76a5841ee9b.js +0 -1
  421. package/apps/pwa/out/_next/static/chunks/7290-f09c928a06c52e2d.js +0 -1
  422. package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +0 -1
  423. package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +0 -1
  424. package/apps/pwa/out/_next/static/chunks/8060-cc008cc6fedfa818.js +0 -1
  425. package/apps/pwa/out/_next/static/chunks/8085-d065d0393c1760f5.js +0 -1
  426. package/apps/pwa/out/_next/static/chunks/8401-4ce1c42c8d89903f.js +0 -1
  427. package/apps/pwa/out/_next/static/chunks/8682-7061dd4fa7bd62ed.js +0 -1
  428. package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +0 -25
  429. package/apps/pwa/out/_next/static/chunks/8949-ecb4128a0858b911.js +0 -1
  430. package/apps/pwa/out/_next/static/chunks/899-f41be85e8a680651.js +0 -1
  431. package/apps/pwa/out/_next/static/chunks/9070.067c106528ec1ae6.js +0 -1
  432. package/apps/pwa/out/_next/static/chunks/9103-60fbef33e66f0a46.js +0 -1
  433. package/apps/pwa/out/_next/static/chunks/a9f2b54d-7865aba0f4d2ee49.js +0 -1
  434. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-c1a96669158471ec.js +0 -1
  435. package/apps/pwa/out/_next/static/chunks/app/approvals/page-3143b07a817d1da9.js +0 -1
  436. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-17ba086ef82d339b.js +0 -1
  437. package/apps/pwa/out/_next/static/chunks/app/board/page-7eb661ff23af7da2.js +0 -1
  438. package/apps/pwa/out/_next/static/chunks/app/botlab/page-6b8444914da74ca6.js +0 -1
  439. package/apps/pwa/out/_next/static/chunks/app/bots/page-6e5c3a867557298b.js +0 -1
  440. package/apps/pwa/out/_next/static/chunks/app/ceo/page-7110f732f45bf944.js +0 -1
  441. package/apps/pwa/out/_next/static/chunks/app/chat/page-9a41d65355fc25b9.js +0 -1
  442. package/apps/pwa/out/_next/static/chunks/app/consult/page-5e77cd1802567c89.js +0 -1
  443. package/apps/pwa/out/_next/static/chunks/app/control/page-41afec0b255ed2bb.js +0 -1
  444. package/apps/pwa/out/_next/static/chunks/app/design-check/page-4187666132fc98c4.js +0 -1
  445. package/apps/pwa/out/_next/static/chunks/app/editor/page-09328d499162bae4.js +0 -1
  446. package/apps/pwa/out/_next/static/chunks/app/exec/page-29ff3a8f84f03b6a.js +0 -1
  447. package/apps/pwa/out/_next/static/chunks/app/field/page-80ec7401bc83f511.js +0 -1
  448. package/apps/pwa/out/_next/static/chunks/app/healthz/route-c1a96669158471ec.js +0 -1
  449. package/apps/pwa/out/_next/static/chunks/app/inside/page-ab93b240f687d69a.js +0 -1
  450. package/apps/pwa/out/_next/static/chunks/app/intake/page-fc7d86467291ae38.js +0 -1
  451. package/apps/pwa/out/_next/static/chunks/app/layout-de43a186e5ba5683.js +0 -1
  452. package/apps/pwa/out/_next/static/chunks/app/live/page-8b08cafad27759c9.js +0 -1
  453. package/apps/pwa/out/_next/static/chunks/app/loops/page-0e2d6985fb42cd2f.js +0 -1
  454. package/apps/pwa/out/_next/static/chunks/app/market/page-13e4108999f58d13.js +0 -1
  455. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-e8296ee284aa1293.js +0 -1
  456. package/apps/pwa/out/_next/static/chunks/app/morning/page-043a9111e74f6b24.js +0 -1
  457. package/apps/pwa/out/_next/static/chunks/app/not-found-23a4601ceb7f452e.js +0 -1
  458. package/apps/pwa/out/_next/static/chunks/app/observatory/page-1ed1324b1dc36754.js +0 -1
  459. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-10d958605a77b3bd.js +0 -1
  460. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-166745e51ac0c7b0.js +0 -1
  461. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a04260b8ccc968d4.js +0 -1
  462. package/apps/pwa/out/_next/static/chunks/app/outputs/page-7a1d85e121065cc8.js +0 -1
  463. package/apps/pwa/out/_next/static/chunks/app/page-1f50e3d4e13435f4.js +0 -1
  464. package/apps/pwa/out/_next/static/chunks/app/reflection/page-12916918988ac97e.js +0 -1
  465. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-d8a3347a9d0c3f4a.js +0 -1
  466. package/apps/pwa/out/_next/static/chunks/app/sessions/page-a9117d2cd3093d36.js +0 -1
  467. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-5a64cb5208c6a8fd.js +0 -1
  468. package/apps/pwa/out/_next/static/chunks/app/settings/page-1407ec83ea64c1f9.js +0 -1
  469. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-1d824510df025d18.js +0 -1
  470. package/apps/pwa/out/_next/static/chunks/app/setup/layout-c1a96669158471ec.js +0 -1
  471. package/apps/pwa/out/_next/static/chunks/app/setup/page-6e878ebbe1737d00.js +0 -1
  472. package/apps/pwa/out/_next/static/chunks/app/share/page-7f49fe645d5132f7.js +0 -1
  473. package/apps/pwa/out/_next/static/chunks/app/showroom/page-81a4e7f8dbfe74cb.js +0 -1
  474. package/apps/pwa/out/_next/static/chunks/app/tasks/page-27d71f131acd9320.js +0 -1
  475. package/apps/pwa/out/_next/static/chunks/app/term/page-3ab59d19913b200e.js +0 -1
  476. package/apps/pwa/out/_next/static/chunks/app/today/page-bf85b2f5bf077925.js +0 -1
  477. package/apps/pwa/out/_next/static/chunks/app/trace/page-424659bacfb8cdd7.js +0 -1
  478. package/apps/pwa/out/_next/static/chunks/app/vault/page-ab32a8f522a60371.js +0 -1
  479. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-fc2df261f8a2c7df.js +0 -1
  480. package/apps/pwa/out/_next/static/chunks/app/workflows/page-09461366af82ef13.js +0 -1
  481. package/apps/pwa/out/_next/static/chunks/app/workspace/page-5d4fef9ccd7d406b.js +0 -1
  482. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ea90a8ac73ce2749.js +0 -1
  483. package/apps/pwa/out/_next/static/chunks/framework-5c169757551134ed.js +0 -1
  484. package/apps/pwa/out/_next/static/chunks/main-59fe95fed40c7a01.js +0 -1
  485. package/apps/pwa/out/_next/static/chunks/main-app-4ca74dbbee7057c6.js +0 -1
  486. package/apps/pwa/out/_next/static/chunks/pages/_app-13a0a886df4d97a1.js +0 -1
  487. package/apps/pwa/out/_next/static/chunks/pages/_error-6af8591cd3a3f038.js +0 -1
  488. package/apps/pwa/out/_next/static/chunks/webpack-539cde99554be20f.js +0 -1
  489. package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +0 -1
  490. package/apps/pwa/out/_next/static/yRA5ur6DcPl0YHSRheH2O/_buildManifest.js +0 -1
  491. /package/apps/pwa/out/_next/static/{yRA5ur6DcPl0YHSRheH2O → J5H7BvqgSI8DUjtRRcA_S}/_ssgManifest.js +0 -0
@@ -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
+ }
@@ -0,0 +1,57 @@
1
+ // COORD-HA (0.2.19) — the signal the orchestrator tick reads for its degradation ladder (RFC loop map §A8b ④⑤):
2
+ // L1 = no usable LLM (rules only) · L2 = the task board cannot take a write (no new assignment). The tick already acts on
3
+ // `TickDeps.degradation`; until now nothing supplied it in production. Each probe answers «false» only on a measured
4
+ // failure — an unknown is not a degradation.
5
+ import { existsSync, mkdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ import { randomUUID } from 'node:crypto';
8
+ import { Database } from 'bun:sqlite';
9
+ import { cardIndexPath, taskCardsDir } from '../../task-cards/card-store.js';
10
+ import { resolveUsableLlm } from '../../llm/usable-llm.js';
11
+
12
+ export interface DegradationSignal { llm: boolean; board: boolean; why: string[] }
13
+
14
+ /** The task board takes a write: its folder is a directory a file can be created in (created and removed — a permission
15
+ * bit is not proof · review r1), and its index can take a write lock (busy 2 s · rolled back). */
16
+ export function probeBoardWritable(root: string): { ok: boolean; why?: string } {
17
+ const dir = taskCardsDir(root);
18
+ try {
19
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
20
+ if (!statSync(dir).isDirectory()) return { ok: false, why: 'board dir not writable: not a directory' };
21
+ // A fresh name each time — a leftover from a crashed probe can never make a writable board look full (review r2).
22
+ const probe = join(dir, `.degradation-probe-${randomUUID()}`);
23
+ try { writeFileSync(probe, '', { flag: 'wx', mode: 0o600 }); }
24
+ finally { rmSync(probe, { force: true }); }
25
+ } catch (error) { return { ok: false, why: `board dir not writable: ${(error as NodeJS.ErrnoException).code ?? String(error)}` }; }
26
+ const index = cardIndexPath(root);
27
+ // No index yet: the card store creates it on first open — a writable folder is enough.
28
+ if (!existsSync(index)) return { ok: true };
29
+ let db: Database | undefined;
30
+ try {
31
+ db = new Database(index, { readwrite: true, create: false });
32
+ db.exec('PRAGMA busy_timeout = 2000');
33
+ db.exec('BEGIN IMMEDIATE');
34
+ db.exec('ROLLBACK');
35
+ return { ok: true };
36
+ } catch (error) {
37
+ return { ok: false, why: `board index write lock: ${String(error instanceof Error ? error.message : error).slice(0, 160)}` };
38
+ } finally { try { db?.close(); } catch { /* closing a failed handle is best effort */ } }
39
+ }
40
+
41
+ export interface DegradationDeps {
42
+ usableLlm?: () => { usable: boolean; why: string };
43
+ board?: (root: string) => { ok: boolean; why?: string };
44
+ }
45
+
46
+ export function probeDegradation(root: string, deps: DegradationDeps = {}): DegradationSignal {
47
+ const why: string[] = [];
48
+ let llm = true;
49
+ try {
50
+ const usable = (deps.usableLlm ?? resolveUsableLlm)();
51
+ llm = usable.usable;
52
+ if (!llm) why.push(`llm: ${usable.why}`);
53
+ } catch (error) { why.push(`llm unknown: ${String(error).slice(0, 120)}`); }
54
+ const board = (deps.board ?? probeBoardWritable)(root);
55
+ if (!board.ok) why.push(board.why ?? 'board not writable');
56
+ return { llm, board: board.ok, why };
57
+ }
@@ -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;
@@ -14,6 +15,8 @@ export type FinishMetric = 'launched' | 'landed' | 'landingRate' | 'staleDrafts'
14
15
  export type FinishMetrics = {
15
16
  launched: number | null;
16
17
  launchedUnreadable?: number;
18
+ /** Launched Pod runs whose child ledger never came back (`pod-ledger-incomplete`) — counted in `launched`, result unknown. */
19
+ launchedIncomplete?: number;
17
20
  landed: number | null;
18
21
  landingRate: number | null;
19
22
  staleDrafts: number | null;
@@ -24,8 +27,81 @@ export type FinishMetrics = {
24
27
  export type FinishAdvice = { finishSlots: number; launchSlots: number; state: 'healthy' | 'backlogged' | 'unknown'; reasons: string[] };
25
28
  export type FinishThresholds = { landingRate?: number; staleDrafts?: number; conflictRatio?: number };
26
29
 
30
+ export type FinishTrend = 'worsening' | 'improving' | 'flat' | 'unknown';
31
+ export type FinishTrendInput = Pick<FinishMetrics, 'landingRate' | 'staleDrafts' | 'conflictRatio'>;
32
+ export type FinishHistoryRow = {
33
+ hour: string; at: string; launched: number | null; landed: number | null;
34
+ landingRate: number | null; staleDrafts: number | null; conflictRatio: number | null; trend: FinishTrend;
35
+ };
36
+
37
+ /** FINISH-RATE 추세 — landingRate 는 내려가면 나쁨 · staleDrafts·conflictRatio 는 올라가면 나쁨.
38
+ * 양쪽 다 유한한 숫자인 지표만 비교한다(못 잰 값은 좋음도 나쁨도 아니다) · 임의 임계 없음.
39
+ * 나쁨 수 > 좋음 수 = worsening · 작으면 improving · 같으면 flat · 비교한 지표가 0 이면 unknown. */
40
+ export function finishTrend(previous: FinishTrendInput | null, current: FinishTrendInput): FinishTrend {
41
+ if (!previous) return 'unknown';
42
+ let compared = 0, worse = 0, better = 0;
43
+ for (const key of ['landingRate', 'staleDrafts', 'conflictRatio'] as const) {
44
+ const before = previous[key], after = current[key];
45
+ if (typeof before !== 'number' || !Number.isFinite(before) || typeof after !== 'number' || !Number.isFinite(after)) continue;
46
+ compared++;
47
+ const delta = key === 'landingRate' ? before - after : after - before;
48
+ if (delta > 0) worse++;
49
+ else if (delta < 0) better++;
50
+ }
51
+ if (compared === 0) return 'unknown';
52
+ return worse > better ? 'worsening' : worse < better ? 'improving' : 'flat';
53
+ }
54
+
55
+ export function finishHistoryPath(root: string): string {
56
+ return join(root, 'harness', 'finish-history.jsonl');
57
+ }
58
+
59
+ /** «한 시간 한 줄» 이력 — 같은 UTC 시간 줄이 이미 있으면 안 쓴다(null) · trend 는 지금보다 이른 가장 최근 시간 줄 대비.
60
+ * 읽을 수 없는 줄은 건너뛴다(이력 한 줄이 깨졌다고 측정을 멈추지 않는다). 쓰기 실패는 호출자에게 던진다. */
61
+ export function recordFinishHistory(root: string, metrics: FinishMetrics, now: Date): FinishHistoryRow | null {
62
+ const path = finishHistoryPath(root);
63
+ const hour = now.toISOString().slice(0, 13);
64
+ mkdirSync(join(root, 'harness'), { recursive: true });
65
+ // 확인과 덧붙이기를 한 잠금 안에서 — 같은 root 를 쓰는 두 틱이 같은 시간에 둘 다 «없음»을 보고 두 줄을 쓰지 않게.
66
+ const row = withFileLockSync(`${path}.lock`, (): FinishHistoryRow | null => {
67
+ const rows: FinishHistoryRow[] = [];
68
+ let text = '';
69
+ if (existsSync(path)) {
70
+ text = readFileSync(path, 'utf8');
71
+ for (const line of text.split('\n')) {
72
+ if (!line.trim()) continue;
73
+ try {
74
+ const parsed = JSON.parse(line) as FinishHistoryRow;
75
+ if (parsed && typeof parsed.hour === 'string') rows.push(parsed);
76
+ } catch { /* a broken line is skipped */ }
77
+ }
78
+ }
79
+ if (rows.some((existing) => existing.hour === hour)) return null;
80
+ // 앞 시간 = 지금보다 이른 줄 중 «가장 최근 UTC 시간»(파일 순서가 아니다 — 늦게 덧붙은 옛 시간 줄이 있을 수 있다) · 같은 시간이면 마지막 줄.
81
+ let previous: FinishHistoryRow | null = null;
82
+ for (const existing of rows) if (existing.hour < hour && (!previous || existing.hour >= previous.hour)) previous = existing;
83
+ const next: FinishHistoryRow = {
84
+ hour, at: now.toISOString(), launched: metrics.launched, landed: metrics.landed,
85
+ landingRate: metrics.landingRate, staleDrafts: metrics.staleDrafts, conflictRatio: metrics.conflictRatio,
86
+ trend: finishTrend(previous, metrics),
87
+ };
88
+ // 부분 쓰기로 끝 개행이 없으면 먼저 보정한다 — 새 줄이 깨진 꼬리에 붙어 둘 다 못 읽게 되지 않게.
89
+ appendFileSync(path, `${text && !text.endsWith('\n') ? '\n' : ''}${JSON.stringify(next)}\n`);
90
+ return next;
91
+ });
92
+ if (!row) return null;
93
+ try {
94
+ debug.log('loop.orchestrator', 'finish-history', {
95
+ hour, trend: row.trend, landingRate: row.landingRate, staleDrafts: row.staleDrafts, conflictRatio: row.conflictRatio,
96
+ });
97
+ } catch { /* the row is written; observation is fail-soft */ }
98
+ return row;
99
+ }
100
+
27
101
  type Start = { runId: string; startedAt: string };
28
- type StartObservation = { starts: readonly Start[]; unreadable: readonly { runId: string; reason: string }[]; unreadableDirectories?: number };
102
+ type StartObservation = { starts: readonly Start[]; unreadable: readonly { runId: string; reason: string }[]; unreadableDirectories?: number;
103
+ /** Runs counted as launched although their child ledger never came back (reason = the host marker's data.reason). */
104
+ incomplete?: readonly { runId: string; reason: string }[] };
29
105
  type Pr = { number: number; headRefName: string; mergedAt?: string; createdAt?: string; mergeable?: string; labels?: { name: string }[] };
30
106
  export interface MeasureFinishDeps {
31
107
  listStarts?: (cutoff: number) => StartObservation;
@@ -41,6 +117,7 @@ function defaultListStarts(cutoff: number, targets?: readonly LogTarget[]): Star
41
117
  let unreadableDirectories = 0;
42
118
  let unresolvedDirectories = 0;
43
119
  const launchOnlyIds = new Set<string>();
120
+ const incomplete = new Map<string, { runId: string; reason: string }>();
44
121
  for (const { ledgerDirectory } of resolveFederatedRunLedgerTargets({ includeTest: true, ...(targets ? { targets } : {}) })) {
45
122
  let dir: string;
46
123
  try {
@@ -71,6 +148,20 @@ function defaultListStarts(cutoff: number, targets?: readonly LogTarget[]): Star
71
148
  continue;
72
149
  }
73
150
  const start = entries.find(entry => entry.event === 'start');
151
+ // ORCH-LIVE-1008 ①: the host writes `pod-ledger-incomplete` on a Pod child run's ledger when the child's own ledger
152
+ // never came back (OOM · logs unavailable · partial collection). The Job did run — it is a launch with an unknown
153
+ // result, not a ledger without a start. Its time is the marker's timestamp, else the ledger mtime.
154
+ const incompleteMarker = !start ? entries.find(entry => entry.event === 'pod-ledger-incomplete') : undefined;
155
+ if (incompleteMarker) {
156
+ // Only a real `start` (from any directory) outranks the marker; a host launch-only record does not — the same
157
+ // run read as launch-only first is still a launch whose result never came back.
158
+ if (starts.has(runId) && !launchOnlyIds.has(runId)) continue;
159
+ if (!starts.has(runId)) starts.set(runId, { runId, startedAt: incompleteMarker.timestamp ?? new Date(statSync(path).mtimeMs).toISOString() });
160
+ const why = (incompleteMarker.data as { reason?: unknown } | undefined)?.reason;
161
+ incomplete.set(runId, { runId, reason: typeof why === 'string' && why ? why : 'unknown' });
162
+ unreadable.delete(runId);
163
+ continue;
164
+ }
74
165
  // A Pod-dispatched launch leaves only a host launch record (launch-quota-policy · author-on-pod-receipt) — the run
75
166
  // itself starts inside the Pod. That is still a launch: its time is the record's timestamp, else the ledger mtime.
76
167
  const launchRecord = !start ? entries.find(entry => LAUNCH_ONLY_EVENTS.has(entry.event)) : undefined;
@@ -85,12 +176,14 @@ function defaultListStarts(cutoff: number, targets?: readonly LogTarget[]): Star
85
176
  if (launchRecord) launchOnlyIds.add(runId); else launchOnlyIds.delete(runId);
86
177
  starts.set(runId, { runId, startedAt });
87
178
  unreadable.delete(runId);
179
+ incomplete.delete(runId);
88
180
  }
89
181
  }
90
182
  debug.log('loop.orchestrator', 'finish-launch-population', {
91
183
  directories: directories.size + unresolvedDirectories, launched: starts.size, launchOnly: launchOnlyIds.size, childLedgers: childLedgers.size, unreadable: unreadable.size,
184
+ ...(incomplete.size > 0 ? { incomplete: incomplete.size } : {}),
92
185
  });
93
- return { starts: [...starts.values()], unreadable: [...unreadable.values()], unreadableDirectories };
186
+ return { starts: [...starts.values()], unreadable: [...unreadable.values()], unreadableDirectories, incomplete: [...incomplete.values()] };
94
187
  }
95
188
 
96
189
  function defaultRunGh(args: string[]): string {
@@ -112,6 +205,7 @@ export function measureFinish(deps: MeasureFinishDeps = {}, now: Date = new Date
112
205
  const cutoff = now.getTime() - DAY_MS;
113
206
  let launched: number | null = null;
114
207
  let launchedUnreadable = 0;
208
+ let launchedIncomplete = 0;
115
209
  let landed: number | null = null;
116
210
  let staleDrafts: number | null = null;
117
211
  let conflictRatio: number | null = null;
@@ -124,6 +218,14 @@ export function measureFinish(deps: MeasureFinishDeps = {}, now: Date = new Date
124
218
  || observation.unreadable.some(entry => !entry || typeof entry.runId !== 'string' || typeof entry.reason !== 'string')) throw new Error('invalid run start observation');
125
219
  launched = new Set(observation.starts.filter(start => Date.parse(start.startedAt) >= cutoff && Date.parse(start.startedAt) <= now.getTime()).map(start => start.runId)).size;
126
220
  launchedUnreadable = observation.unreadable.length;
221
+ const incompleteRuns = (observation.incomplete ?? []).filter(entry => observation.starts.some(start => start.runId === entry.runId
222
+ && Date.parse(start.startedAt) >= cutoff && Date.parse(start.startedAt) <= now.getTime()));
223
+ launchedIncomplete = incompleteRuns.length;
224
+ if (launchedIncomplete > 0) {
225
+ const byReason: Record<string, number> = {};
226
+ for (const entry of incompleteRuns) byReason[entry.reason] = (byReason[entry.reason] ?? 0) + 1;
227
+ debug.log('loop.orchestrator', 'finish-ledger-incomplete', { count: launchedIncomplete, byReason, sample: incompleteRuns.slice(0, 5) });
228
+ }
127
229
  if (launchedUnreadable > 0) {
128
230
  reasons.launched = `${launchedUnreadable}개 원장 시작 줄 없음(하한값)`;
129
231
  debug.log('loop.orchestrator', 'finish-ledger-unreadable', { count: launchedUnreadable, sample: observation.unreadable.slice(0, 5) });
@@ -162,7 +264,7 @@ export function measureFinish(deps: MeasureFinishDeps = {}, now: Date = new Date
162
264
  : 'no harness runs launched in the last 24 hours';
163
265
  else if (landed > launched) reasons.landingRate = `landed exceeds launched — population mismatch (launched=${launched}, landed=${landed})`;
164
266
  else landingRate = landed / launched;
165
- return { launched, ...(launchedUnreadable > 0 ? { launchedUnreadable } : {}), landed, landingRate, staleDrafts, conflictRatio, unknownMergeable, reasons };
267
+ return { launched, ...(launchedUnreadable > 0 ? { launchedUnreadable } : {}), ...(launchedIncomplete > 0 ? { launchedIncomplete } : {}), landed, landingRate, staleDrafts, conflictRatio, unknownMergeable, reasons };
166
268
  }
167
269
 
168
270
  /** Advice only: no launch cap, PR, or config is changed. */
@@ -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
+ }