elanous 0.2.13 → 0.2.15

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 (524) 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/LZcstaO9_NXypPFmujolv/_buildManifest.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/{7334.a6679d9168f37aa8.js → 1268.0840ff91e083fcbd.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/1307-33ffcc49ab45123b.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/{9656-c1540088f5859c80.js → 1844-9ec4b1d12f6e80eb.js} +1 -1
  8. package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/{390.c373e31b3b9d0c41.js → 1948.15f356b15a625597.js} +2 -2
  10. package/apps/pwa/out/_next/static/chunks/211-9c9090c59e2c5907.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/311.e685738f92e6e2ec.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/3194-807b212fb8eae065.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/3324-d1d2979821dfa845.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/3558-bbb5ab553a0471d3.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/{4685-c3c1c625b71accd1.js → 4614-21403aac730d4586.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/{8039-df92e08fede5de8f.js → 4795-f372490b178583fc.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/5360-be1f1d8f9fcc9755.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/{1452.93700483b9d4fc61.js → 6110.1297d4093fdfbb0d.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/6158.9c0449a9a8d0064b.js +2 -0
  31. package/apps/pwa/out/_next/static/chunks/6877-7c6208705c284124.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/726.83532d69452ee24b.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/7290-f09c928a06c52e2d.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/{1829-b453a1b22d3999bb.js → 7327-2d69fb321ba73389.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/8060-cc008cc6fedfa818.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/8085-d065d0393c1760f5.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/8682-7061dd4fa7bd62ed.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +25 -0
  42. package/apps/pwa/out/_next/static/chunks/{8421-1c4acfb1395a0edf.js → 8949-ecb4128a0858b911.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/9070.067c106528ec1ae6.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/9103-7a3ae7f60af3b99f.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/{3869-3cd46526879f454c.js → 9247-29d889b809288e65.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/935.4ddaa99fc1d1126c.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/{79f8be9e-957d68913b590b08.js → a9f2b54d-7865aba0f4d2ee49.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-c1a96669158471ec.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/approvals/page-0e2c57fe6add4b2a.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-e05a77df7e005b04.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/botlab/page-5491c115b1f9a923.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/bots/page-b8c49f6b4fa2df91.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/chat/page-976813c91d7394ea.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/consult/page-28b9448091776c06.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/control/page-1d4901eb7c2d8822.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/design-check/page-72d3e7ff35a8c954.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/editor/page-60c7a8e1a4fe62ce.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/exec/page-2a52b316cb143d9c.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/field/page-c3ce483c8d5db370.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/healthz/route-c1a96669158471ec.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/intake/page-96f074f6538e8a2c.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/layout-e97be5334d83a9f5.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +1 -0
  69. package/apps/pwa/out/_next/static/chunks/app/market/page-955ec2da3be4c560.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-9f88fbc15d9058d1.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/morning/page-c1343f9499d86f3f.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/not-found-8739a2e0ae793130.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/observatory/page-c9fc5706b8abaa11.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-d23b12bb92242558.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c7fea475488201a8.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/outputs/page-e2ee2896811e98d0.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/reflection/page-5efbf81124c474ae.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-85f1a1fc0a82738c.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/sessions/page-da6c08aae0f92768.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-06ee266953d9c879.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/settings/page-153c2b3bcc89e8de.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-87a11e6a05f15199.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/app/setup/layout-c1a96669158471ec.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/app/setup/page-7d4182964407c09c.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/app/share/page-0d6d23b40263a7ea.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +1 -0
  89. package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/app/term/{page-63d14a4d8fe1e1c1.js → page-f5597e894c02cd33.js} +1 -1
  91. package/apps/pwa/out/_next/static/chunks/app/today/page-0337fdf7fb5e37b6.js +1 -0
  92. package/apps/pwa/out/_next/static/chunks/app/trace/page-73735b64bfe1ae67.js +1 -0
  93. package/apps/pwa/out/_next/static/chunks/app/vault/page-8fcec4c9ea2e7f70.js +1 -0
  94. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f7e631e9b4daf66e.js +1 -0
  95. package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +1 -0
  96. package/apps/pwa/out/_next/static/chunks/app/workspace/page-9817c7f9007466cc.js +1 -0
  97. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-97f9f43172a3a90e.js +1 -0
  98. package/apps/pwa/out/_next/static/chunks/{5cd7e3a9-404fd6b8d0ecd44c.js → f6231284-45726550aba08fca.js} +1 -1
  99. package/apps/pwa/out/_next/static/chunks/{f48170cc-d05d28bf87a43bf4.js → f6b7c9ba.64a895b8b2fedbdc.js} +1 -1
  100. package/apps/pwa/out/_next/static/chunks/framework-5c169757551134ed.js +1 -0
  101. package/apps/pwa/out/_next/static/chunks/main-59fe95fed40c7a01.js +1 -0
  102. package/apps/pwa/out/_next/static/chunks/main-app-4ca74dbbee7057c6.js +1 -0
  103. package/apps/pwa/out/_next/static/chunks/pages/_app-13a0a886df4d97a1.js +1 -0
  104. package/apps/pwa/out/_next/static/chunks/pages/_error-6af8591cd3a3f038.js +1 -0
  105. package/apps/pwa/out/_next/static/chunks/webpack-539cde99554be20f.js +1 -0
  106. package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +1 -0
  107. package/apps/pwa/out/approvals/index.html +1 -1
  108. package/apps/pwa/out/approvals/index.txt +23 -23
  109. package/apps/pwa/out/autopilot/index.html +1 -1
  110. package/apps/pwa/out/autopilot/index.txt +23 -23
  111. package/apps/pwa/out/board/index.html +1 -1
  112. package/apps/pwa/out/board/index.txt +23 -23
  113. package/apps/pwa/out/botlab/index.html +1 -1
  114. package/apps/pwa/out/botlab/index.txt +22 -22
  115. package/apps/pwa/out/bots/index.html +1 -1
  116. package/apps/pwa/out/bots/index.txt +22 -22
  117. package/apps/pwa/out/ceo/index.html +1 -0
  118. package/apps/pwa/out/ceo/index.txt +33 -0
  119. package/apps/pwa/out/chat/index.html +1 -1
  120. package/apps/pwa/out/chat/index.txt +22 -22
  121. package/apps/pwa/out/consult/index.html +1 -1
  122. package/apps/pwa/out/consult/index.txt +23 -23
  123. package/apps/pwa/out/control/index.html +1 -1
  124. package/apps/pwa/out/control/index.txt +23 -23
  125. package/apps/pwa/out/design-check/index.html +1 -1
  126. package/apps/pwa/out/design-check/index.txt +23 -23
  127. package/apps/pwa/out/editor/index.html +1 -1
  128. package/apps/pwa/out/editor/index.txt +23 -23
  129. package/apps/pwa/out/exec/index.html +1 -1
  130. package/apps/pwa/out/exec/index.txt +23 -23
  131. package/apps/pwa/out/field/index.html +1 -1
  132. package/apps/pwa/out/field/index.txt +23 -23
  133. package/apps/pwa/out/healthz +1 -1
  134. package/apps/pwa/out/index.html +1 -1
  135. package/apps/pwa/out/index.txt +22 -22
  136. package/apps/pwa/out/inside/index.html +1 -1
  137. package/apps/pwa/out/inside/index.txt +22 -22
  138. package/apps/pwa/out/intake/index.html +1 -1
  139. package/apps/pwa/out/intake/index.txt +23 -23
  140. package/apps/pwa/out/live/index.html +1 -1
  141. package/apps/pwa/out/live/index.txt +23 -23
  142. package/apps/pwa/out/loops/index.html +1 -0
  143. package/apps/pwa/out/loops/index.txt +30 -0
  144. package/apps/pwa/out/market/index.html +1 -1
  145. package/apps/pwa/out/market/index.txt +22 -22
  146. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  147. package/apps/pwa/out/missions/placeholder/index.txt +22 -22
  148. package/apps/pwa/out/morning/index.html +1 -1
  149. package/apps/pwa/out/morning/index.txt +23 -23
  150. package/apps/pwa/out/observatory/index.html +1 -1
  151. package/apps/pwa/out/observatory/index.txt +23 -23
  152. package/apps/pwa/out/ops/checklist/index.html +1 -1
  153. package/apps/pwa/out/ops/checklist/index.txt +22 -22
  154. package/apps/pwa/out/ops/release/index.html +1 -1
  155. package/apps/pwa/out/ops/release/index.txt +22 -22
  156. package/apps/pwa/out/ops/seats/index.html +1 -1
  157. package/apps/pwa/out/ops/seats/index.txt +22 -22
  158. package/apps/pwa/out/outputs/index.html +1 -1
  159. package/apps/pwa/out/outputs/index.txt +22 -22
  160. package/apps/pwa/out/reflection/index.html +1 -1
  161. package/apps/pwa/out/reflection/index.txt +23 -23
  162. package/apps/pwa/out/scheduler/index.html +1 -1
  163. package/apps/pwa/out/scheduler/index.txt +23 -23
  164. package/apps/pwa/out/sessions/index.html +1 -1
  165. package/apps/pwa/out/sessions/index.txt +22 -22
  166. package/apps/pwa/out/settings/devices/index.html +1 -1
  167. package/apps/pwa/out/settings/devices/index.txt +23 -23
  168. package/apps/pwa/out/settings/index.html +1 -1
  169. package/apps/pwa/out/settings/index.txt +23 -23
  170. package/apps/pwa/out/setup/done/index.html +1 -1
  171. package/apps/pwa/out/setup/done/index.txt +23 -23
  172. package/apps/pwa/out/setup/index.html +1 -1
  173. package/apps/pwa/out/setup/index.txt +23 -23
  174. package/apps/pwa/out/share/index.html +1 -1
  175. package/apps/pwa/out/share/index.txt +23 -23
  176. package/apps/pwa/out/showroom/index.html +1 -1
  177. package/apps/pwa/out/showroom/index.txt +22 -22
  178. package/apps/pwa/out/tasks/index.html +1 -1
  179. package/apps/pwa/out/tasks/index.txt +23 -23
  180. package/apps/pwa/out/term/index.html +1 -1
  181. package/apps/pwa/out/term/index.txt +23 -23
  182. package/apps/pwa/out/today/index.html +1 -1
  183. package/apps/pwa/out/today/index.txt +23 -23
  184. package/apps/pwa/out/trace/index.html +1 -1
  185. package/apps/pwa/out/trace/index.txt +23 -23
  186. package/apps/pwa/out/vault/index.html +1 -1
  187. package/apps/pwa/out/vault/index.txt +23 -23
  188. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  189. package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
  190. package/apps/pwa/out/workflows/index.html +1 -1
  191. package/apps/pwa/out/workflows/index.txt +23 -23
  192. package/apps/pwa/out/workspace/index.html +1 -1
  193. package/apps/pwa/out/workspace/index.txt +23 -23
  194. package/apps/pwa/out/worktrees/index.html +1 -1
  195. package/apps/pwa/out/worktrees/index.txt +23 -23
  196. package/graphs/heal/heal-loop.yaml +1 -1
  197. package/graphs/implement-loop.yaml +1 -1
  198. package/graphs/orchestrator/orchestrator.yaml +59 -0
  199. package/graphs/orchestrator/recipes.yaml +18 -0
  200. package/graphs/release/recipes.yaml +6 -0
  201. package/graphs/release/release-loop.yaml +6 -1
  202. package/graphs/rhythm/daily.yaml +38 -0
  203. package/graphs/rhythm/recipes.yaml +18 -0
  204. package/graphs/rhythm/weekly.yaml +39 -0
  205. package/graphs/video/recipes.yaml +3 -0
  206. package/graphs/video/youtube-summary.yaml +17 -0
  207. package/package.json +1 -1
  208. package/scripts/audit-test-state-writes.ts +72 -17
  209. package/scripts/brand/check.ts +188 -0
  210. package/scripts/ci-typecheck-changed.ts +50 -18
  211. package/scripts/coord-post.sh +11 -1
  212. package/scripts/docs/generate-graph-anatomy.ts +315 -0
  213. package/scripts/docs-cli-check.ts +23 -6
  214. package/scripts/expose-rubric-check.ts +118 -0
  215. package/scripts/expose-rubric-suggest.ts +169 -0
  216. package/scripts/heal/run-role.ts +14 -0
  217. package/scripts/hq/standby-verify.ts +11 -7
  218. package/scripts/lecture-note/node.ts +31 -11
  219. package/scripts/lessons/handbook-page.ts +112 -0
  220. package/scripts/marketing/generate-ax-brief.ts +140 -0
  221. package/scripts/marketing/generate-feature-map.ts +118 -0
  222. package/scripts/release-loop/checklist-gate-node.ts +53 -14
  223. package/scripts/release-loop/gate-node.ts +89 -14
  224. package/scripts/release-loop/publish-node.ts +14 -1
  225. package/scripts/release-loop/unattended-release.ts +23 -1
  226. package/scripts/release-loop/vault-note-node.ts +428 -0
  227. package/scripts/release-story/draft.ts +220 -0
  228. package/scripts/rhythm/daily.ts +570 -0
  229. package/scripts/rhythm/weekly.ts +315 -0
  230. package/scripts/skills/public-check.ts +169 -0
  231. package/scripts/smart-merge/resync-shadow.ts +147 -0
  232. package/scripts/test-deterministic.ts +4 -8
  233. package/scripts/test-diet/effectiveness.ts +195 -0
  234. package/scripts/test-diet/lib.ts +16 -8
  235. package/scripts/test-diet/node.ts +77 -9
  236. package/scripts/ux/demo-rehearsal.ts +2 -2
  237. package/scripts/ux/lib/rehearsal-verdict.ts +1 -0
  238. package/scripts/video/youtube-summary.ts +133 -0
  239. package/skills/diagram-master/SKILL.md +42 -49
  240. package/skills/diagram-master/references/pyproject.toml +4 -1
  241. package/skills/diagram-master/references/routing-guide.md +4 -2
  242. package/skills/grill-me/SKILL.md +1 -0
  243. package/skills/youtube-master/SKILL.md +13 -13
  244. package/skills/youtube-master/package-lock.json +0 -545
  245. package/skills/youtube-master/package.json +2 -5
  246. package/skills/youtube-master/scripts/main.ts +2 -2
  247. package/skills/youtube-master/scripts/transcribe-local.ts +1 -1
  248. package/skills/youtube-master/src/util.ts +30 -7
  249. package/src/agent-env/profile.ts +161 -0
  250. package/src/agent-mission/driver.ts +3 -3
  251. package/src/agent-substrate/pr-reviewer.ts +2 -0
  252. package/src/ask-user-question/pending-questions.ts +5 -0
  253. package/src/auth/github-app-token.ts +112 -13
  254. package/src/autopilot/build/llm-conflict-merge.ts +73 -10
  255. package/src/autopilot/build/merge-intent.ts +53 -0
  256. package/src/briefing/brief-items.ts +121 -0
  257. package/src/chat/index.ts +2 -0
  258. package/src/claims/claims-ledger.ts +232 -0
  259. package/src/claims/claims-render.ts +166 -0
  260. package/src/cli/agent-env-cli.ts +21 -0
  261. package/src/cli/claims-cli.ts +94 -0
  262. package/src/cli/decisions-cli.ts +44 -3
  263. package/src/cli/docs-cli.ts +2 -0
  264. package/src/cli/doctor-cli.ts +23 -1
  265. package/src/cli/doctor-node-modules.ts +147 -0
  266. package/src/cli/flow-cli.ts +11 -0
  267. package/src/cli/freeze-cli.ts +45 -0
  268. package/src/cli/intake-cli.ts +46 -17
  269. package/src/cli/lesson-cli.ts +108 -0
  270. package/src/cli/logs-cli.ts +104 -2
  271. package/src/cli/logs-model-input.ts +131 -0
  272. package/src/cli/pod-cli.ts +2 -0
  273. package/src/cli/pr-cli.ts +29 -1
  274. package/src/cli/private-ledger-commands.ts +54 -4
  275. package/src/cli/pty-drive-verdict.ts +11 -2
  276. package/src/cli/release-cli.ts +190 -33
  277. package/src/cli/rfc-status.ts +91 -0
  278. package/src/cli/seat-requests-cli.ts +52 -0
  279. package/src/connectors/linear.ts +5 -3
  280. package/src/context-bus/condense.ts +15 -20
  281. package/src/context-bus/context-now-public.ts +96 -0
  282. package/src/context-bus/context-now-surfaces.ts +55 -2
  283. package/src/context-bus/context-now.ts +6 -3
  284. package/src/context-bus/coord-events.ts +6 -4
  285. package/src/context-bus/long-term-memory.ts +92 -11
  286. package/src/dashboard/index.ts +75 -22
  287. package/src/dashboard/slash-runtime/chat-persona.ts +66 -0
  288. package/src/dashboard/slash-runtime/dashboard-handlers.ts +99 -0
  289. package/src/dashboard/slash-runtime/loops-table.ts +77 -0
  290. package/src/decisions/decision-cards.ts +33 -7
  291. package/src/decisions/decision-ledger.ts +40 -1
  292. package/src/decisions/proact-meter.ts +286 -0
  293. package/src/decisions/proact1-lite.ts +86 -0
  294. package/src/decisions/web-push-decision-cards.ts +1 -1
  295. package/src/discord/sprint21-runtime.ts +16 -0
  296. package/src/discord-self-message.ts +8 -0
  297. package/src/discord-slash-wire.ts +31 -2
  298. package/src/domains/alert-audit.ts +271 -0
  299. package/src/domains/core-tools.ts +4 -3
  300. package/src/domains/ops-facts-tool.ts +31 -1
  301. package/src/domains/outbound-alert.ts +1 -1
  302. package/src/elanous-config-dir.ts +29 -4
  303. package/src/execution-loop/launch-gate.ts +29 -14
  304. package/src/flow/cards.ts +239 -0
  305. package/src/graph-runner/graph-tick.ts +4 -0
  306. package/src/graph-runner/runner.ts +30 -5
  307. package/src/harness/harness-cli-command.ts +259 -38
  308. package/src/harness/harness-incidents.ts +45 -12
  309. package/src/harness/harness-pod-dispatch.ts +102 -11
  310. package/src/harness/harness-queue-child.ts +3 -3
  311. package/src/harness/harness-queue.ts +583 -65
  312. package/src/harness/helper-repair.ts +87 -0
  313. package/src/harness/helper-scan.ts +151 -0
  314. package/src/harness/launch-stamp.ts +236 -0
  315. package/src/harness/ledger-write-guard-child.fixture.ts +12 -0
  316. package/src/harness/ledger-write-guard.ts +35 -0
  317. package/src/harness/model-input-observation.ts +37 -0
  318. package/src/harness/pod-memory-policy.ts +15 -0
  319. package/src/harness/repository-name.ts +55 -0
  320. package/src/hq/fence-audit.ts +90 -0
  321. package/src/hq/hq.ts +63 -5
  322. package/src/hq/lease.ts +4 -4
  323. package/src/hq/move-plan.ts +126 -0
  324. package/src/hq/promote.ts +332 -0
  325. package/src/index.ts +159 -27
  326. package/src/intake-plane/check.ts +63 -0
  327. package/src/intake-plane/digest.ts +37 -15
  328. package/src/intake-plane/front-classifier.ts +6 -0
  329. package/src/intake-plane/items.ts +3 -2
  330. package/src/intake-plane/lens.ts +177 -0
  331. package/src/intake-plane/linear-wish.ts +39 -11
  332. package/src/intake-plane/route.ts +145 -1
  333. package/src/intake-plane/wish-benchmark-ledger.ts +129 -0
  334. package/src/intake-plane/wish-card.ts +53 -2
  335. package/src/intake-plane/wish-folder.ts +5 -2
  336. package/src/intake-plane/wish-reply.ts +72 -0
  337. package/src/launch-head/seat-goal-queue-lock.fixture.ts +15 -0
  338. package/src/launch-head/seat-goal-queue.ts +198 -0
  339. package/src/lessons/lesson-import.ts +106 -0
  340. package/src/lessons/lesson-ledger.ts +206 -0
  341. package/src/loops/activity.ts +203 -0
  342. package/src/loops/budget.ts +63 -0
  343. package/src/loops/checker.ts +5 -3
  344. package/src/loops/contract/examples/orchestrator.loop.yaml +1 -1
  345. package/src/loops/loop-cli.ts +36 -4
  346. package/src/loops/neighbors.ts +40 -0
  347. package/src/loops/observe.ts +10 -0
  348. package/src/loops/orchestrator/author-depth.ts +220 -0
  349. package/src/loops/orchestrator/author-input-check.ts +82 -0
  350. package/src/loops/orchestrator/author-ledger.ts +232 -0
  351. package/src/loops/orchestrator/finish-rate.ts +175 -0
  352. package/src/loops/orchestrator/overlap.ts +51 -12
  353. package/src/loops/orchestrator/resync-shadow.ts +227 -0
  354. package/src/loops/orchestrator/seat-share.ts +56 -0
  355. package/src/loops/orchestrator/tick.ts +309 -0
  356. package/src/loops/orchestrator/traffic.ts +134 -13
  357. package/src/loops/registry.ts +535 -9
  358. package/src/loops/status-rows.ts +118 -0
  359. package/src/loops/verdict.ts +17 -5
  360. package/src/maturity/bot-slash-catalog.ts +66 -0
  361. package/src/maturity/feature-maturity.ts +9 -5
  362. package/src/maturity/tui-slash-availability.ts +31 -0
  363. package/src/mss/logging/log-store.ts +35 -4
  364. package/src/nexus/api/dashboard.ts +6 -1
  365. package/src/nexus/api/http-server.ts +33 -3
  366. package/src/nexus/api/loop-edges.ts +173 -0
  367. package/src/nexus/api/merge-approvals-gate.ts +1 -1
  368. package/src/nexus/api/schedules-read.ts +18 -1
  369. package/src/nexus/api/seat-requests.ts +79 -56
  370. package/src/nexus/api/task-cards-api.ts +44 -5
  371. package/src/nexus/api/workflows.ts +2 -1
  372. package/src/prompt-enhance/enhance.ts +6 -1
  373. package/src/release-loop/auto-start.ts +51 -0
  374. package/src/release-loop/checklist.ts +22 -4
  375. package/src/release-loop/feature-store.ts +71 -32
  376. package/src/release-loop/landing-freeze.ts +121 -0
  377. package/src/release-loop/merged-pr-checklist.ts +125 -0
  378. package/src/release-loop/placement.ts +302 -0
  379. package/src/release-loop/release-schedule.ts +3 -1
  380. package/src/seat-dispatch/ceo-commands.ts +2 -2
  381. package/src/seat-dispatch/seat-ask.ts +16 -3
  382. package/src/seat-dispatch/seat-questions.ts +58 -0
  383. package/src/seat-dispatch/seat-request-eligibility.ts +19 -0
  384. package/src/seat-dispatch/seat-request-ledger.ts +133 -0
  385. package/src/seat-loop/seat-loop.ts +531 -40
  386. package/src/self-dev/dev-pipeline.ts +14 -6
  387. package/src/self-dev/draft-sweep.ts +162 -6
  388. package/src/self-dev/draft-triage-rules.ts +25 -8
  389. package/src/self-dev/orchestrate.ts +15 -1
  390. package/src/self-dev/review-provider-fallback.ts +6 -1
  391. package/src/self-dev/run-store.ts +73 -1
  392. package/src/self-dev/run-supervisor.ts +57 -2
  393. package/src/self-dev/self-orchestrate-runtime.ts +5 -1
  394. package/src/self-implement/budget-gate.ts +29 -0
  395. package/src/self-implement/expose-gate.ts +47 -0
  396. package/src/self-implement/frozen-merges.ts +295 -0
  397. package/src/self-implement/gate-cli.ts +10 -4
  398. package/src/self-implement/gate-scope.ts +46 -9
  399. package/src/self-implement/goal-archive.ts +222 -0
  400. package/src/self-implement/goal-author.ts +43 -5
  401. package/src/self-implement/goal-clarification-escalation.ts +5 -0
  402. package/src/self-implement/goal-lookup.ts +147 -0
  403. package/src/self-implement/heal-intake.ts +197 -0
  404. package/src/self-implement/host-regate.ts +32 -2
  405. package/src/self-implement/l8-merge-queue.ts +132 -0
  406. package/src/self-implement/orchestrator.ts +299 -48
  407. package/src/self-implement/pipeline-shape.ts +1 -1
  408. package/src/self-implement/revert-guard.ts +97 -0
  409. package/src/self-implement/run-ledger.ts +3 -1
  410. package/src/self-implement/run-status-mapping.ts +2 -0
  411. package/src/self-implement/seams.ts +29 -9
  412. package/src/self-implement/self-implement-cli.ts +1 -0
  413. package/src/self-implement/speed-claims.ts +338 -0
  414. package/src/session/index.ts +2 -0
  415. package/src/steward/triage.ts +74 -3
  416. package/src/steward/working-backwards.ts +4 -1
  417. package/src/task-cards/card-cli.ts +10 -2
  418. package/src/task-cards/card-store.ts +10 -6
  419. package/src/task-orchestrator/surfaces/pod-artifact-return.ts +63 -48
  420. package/src/task-orchestrator/surfaces/pod-command-job.ts +84 -9
  421. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +48 -0
  422. package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
  423. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +46 -6
  424. package/src/task-orchestrator/surfaces/pod-pool.ts +60 -8
  425. package/src/task-orchestrator/surfaces/self-implement-pod.ts +224 -49
  426. package/src/task-orchestrator/surfaces/self-implement.ts +5 -1
  427. package/src/telegram-commands.ts +27 -3
  428. package/src/telegram-loops-command.ts +44 -0
  429. package/src/telegram.ts +25 -4
  430. package/src/user-config.ts +146 -18
  431. package/src/version/packed-revision.json +1 -1
  432. package/src/workflow-runtime/nodes/bash.ts +9 -1
  433. package/src/workflow-runtime/storage.ts +31 -13
  434. package/apps/pwa/out/_next/static/D51l8jUBplJrIpWvENsDK/_buildManifest.js +0 -1
  435. package/apps/pwa/out/_next/static/chunks/1159-0c5ef8bc21cb6afb.js +0 -1
  436. package/apps/pwa/out/_next/static/chunks/1394-931c9827067b2276.js +0 -1
  437. package/apps/pwa/out/_next/static/chunks/2176-c379e3ebce874151.js +0 -1
  438. package/apps/pwa/out/_next/static/chunks/2426.8c6ee0c668d53def.js +0 -1
  439. package/apps/pwa/out/_next/static/chunks/3170-7ec60a56fa0e4728.js +0 -1
  440. package/apps/pwa/out/_next/static/chunks/3278-ae6ef084769f6195.js +0 -1
  441. package/apps/pwa/out/_next/static/chunks/3454-988a53dac60a1763.js +0 -1
  442. package/apps/pwa/out/_next/static/chunks/3638-aa80b4641ea439db.js +0 -1
  443. package/apps/pwa/out/_next/static/chunks/3735-fe3402553ac1c9c6.js +0 -1
  444. package/apps/pwa/out/_next/static/chunks/3992-fe5c06d6b31f8b25.js +0 -25
  445. package/apps/pwa/out/_next/static/chunks/4215-c9d91dd644ec1719.js +0 -1
  446. package/apps/pwa/out/_next/static/chunks/4508-5d0161d1e724d735.js +0 -1
  447. package/apps/pwa/out/_next/static/chunks/4733-623a79345b171e1c.js +0 -1
  448. package/apps/pwa/out/_next/static/chunks/4883.3bd969ea1d3aaaff.js +0 -2
  449. package/apps/pwa/out/_next/static/chunks/5250-c78ca295585bbf95.js +0 -1
  450. package/apps/pwa/out/_next/static/chunks/5532.6899a12958a89b72.js +0 -1
  451. package/apps/pwa/out/_next/static/chunks/5845-e6c003ce8d5fd36a.js +0 -1
  452. package/apps/pwa/out/_next/static/chunks/5978-b27004c677119115.js +0 -1
  453. package/apps/pwa/out/_next/static/chunks/6048-4c5c33579bc4ef78.js +0 -1
  454. package/apps/pwa/out/_next/static/chunks/6303-422dbf0f67c72aac.js +0 -1
  455. package/apps/pwa/out/_next/static/chunks/6511-ff8360be3d65aaa4.js +0 -1
  456. package/apps/pwa/out/_next/static/chunks/6588-ba380869b67c4fa7.js +0 -1
  457. package/apps/pwa/out/_next/static/chunks/6751-bb3595948b2e311b.js +0 -1
  458. package/apps/pwa/out/_next/static/chunks/7274-19b7279f18b47ab3.js +0 -1
  459. package/apps/pwa/out/_next/static/chunks/7551-a0e0707b79164ddd.js +0 -1
  460. package/apps/pwa/out/_next/static/chunks/7758-85c2b53f731da024.js +0 -1
  461. package/apps/pwa/out/_next/static/chunks/7777-79f845045aa1ed86.js +0 -1
  462. package/apps/pwa/out/_next/static/chunks/8025-cc24ee5d8f2fb6f8.js +0 -1
  463. package/apps/pwa/out/_next/static/chunks/8206-c6684dcde12be721.js +0 -1
  464. package/apps/pwa/out/_next/static/chunks/8605-9da4678f8a310d18.js +0 -1
  465. package/apps/pwa/out/_next/static/chunks/8725.bf46bb1395dd0fa2.js +0 -1
  466. package/apps/pwa/out/_next/static/chunks/8734-252d511cd276c0dc.js +0 -1
  467. package/apps/pwa/out/_next/static/chunks/8834-ca1cb09c1cb1a6bf.js +0 -1
  468. package/apps/pwa/out/_next/static/chunks/9269.d2a4795bc6812717.js +0 -1
  469. package/apps/pwa/out/_next/static/chunks/9538-2816b90923606459.js +0 -1
  470. package/apps/pwa/out/_next/static/chunks/9906-e7b8b5310a169dfe.js +0 -1
  471. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-d16993889b125c42.js +0 -1
  472. package/apps/pwa/out/_next/static/chunks/app/approvals/page-e5b4e1b256f41076.js +0 -1
  473. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-e49b9b76c565a0b1.js +0 -1
  474. package/apps/pwa/out/_next/static/chunks/app/board/page-b4f032c893b21cc9.js +0 -1
  475. package/apps/pwa/out/_next/static/chunks/app/botlab/page-f14987d5850a31e0.js +0 -1
  476. package/apps/pwa/out/_next/static/chunks/app/bots/page-b433308aad13ceb6.js +0 -1
  477. package/apps/pwa/out/_next/static/chunks/app/chat/page-4c1dbb21861beab2.js +0 -1
  478. package/apps/pwa/out/_next/static/chunks/app/consult/page-c85b816e3b269dbb.js +0 -1
  479. package/apps/pwa/out/_next/static/chunks/app/control/page-f6bc66b919b2ea7d.js +0 -1
  480. package/apps/pwa/out/_next/static/chunks/app/design-check/page-60603c1e056c9b25.js +0 -1
  481. package/apps/pwa/out/_next/static/chunks/app/editor/page-a0ea8f5a2341ee6b.js +0 -1
  482. package/apps/pwa/out/_next/static/chunks/app/exec/page-45c5bf04b4a605e6.js +0 -1
  483. package/apps/pwa/out/_next/static/chunks/app/field/page-36c6c4183353ac9e.js +0 -1
  484. package/apps/pwa/out/_next/static/chunks/app/healthz/route-d16993889b125c42.js +0 -1
  485. package/apps/pwa/out/_next/static/chunks/app/inside/page-cb2eb0fd809980da.js +0 -1
  486. package/apps/pwa/out/_next/static/chunks/app/intake/page-31012264da8a341f.js +0 -1
  487. package/apps/pwa/out/_next/static/chunks/app/layout-50309aeb0bdd554a.js +0 -1
  488. package/apps/pwa/out/_next/static/chunks/app/live/page-bc00e964e855f4df.js +0 -1
  489. package/apps/pwa/out/_next/static/chunks/app/market/page-0696ee3fac9fe56c.js +0 -1
  490. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-66c27608f651b866.js +0 -1
  491. package/apps/pwa/out/_next/static/chunks/app/morning/page-6ddd2109b7fc3a80.js +0 -1
  492. package/apps/pwa/out/_next/static/chunks/app/not-found-4e50b7298c1f69a5.js +0 -1
  493. package/apps/pwa/out/_next/static/chunks/app/observatory/page-7a2bf830038a2264.js +0 -1
  494. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-1f327f18694fabca.js +0 -1
  495. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e5cef2fe6e4028df.js +0 -1
  496. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cf596128d2b420ba.js +0 -1
  497. package/apps/pwa/out/_next/static/chunks/app/outputs/page-f672536f14f0969f.js +0 -1
  498. package/apps/pwa/out/_next/static/chunks/app/page-b52365662232db89.js +0 -1
  499. package/apps/pwa/out/_next/static/chunks/app/reflection/page-e7a6c80f288b1948.js +0 -1
  500. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-29a709616df9b050.js +0 -1
  501. package/apps/pwa/out/_next/static/chunks/app/sessions/page-fee842d7c2151129.js +0 -1
  502. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-78b40a344ed22f24.js +0 -1
  503. package/apps/pwa/out/_next/static/chunks/app/settings/page-75d199f448171014.js +0 -1
  504. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-a011dd5b0bff29c5.js +0 -1
  505. package/apps/pwa/out/_next/static/chunks/app/setup/layout-d16993889b125c42.js +0 -1
  506. package/apps/pwa/out/_next/static/chunks/app/setup/page-d92e32d51ab5157e.js +0 -1
  507. package/apps/pwa/out/_next/static/chunks/app/share/page-5f21aed6932ef375.js +0 -1
  508. package/apps/pwa/out/_next/static/chunks/app/showroom/page-1e1a53946804debf.js +0 -1
  509. package/apps/pwa/out/_next/static/chunks/app/tasks/page-a1833cd43d832601.js +0 -1
  510. package/apps/pwa/out/_next/static/chunks/app/today/page-2a511d320f762e06.js +0 -1
  511. package/apps/pwa/out/_next/static/chunks/app/trace/page-1c3e03f88dac1e21.js +0 -1
  512. package/apps/pwa/out/_next/static/chunks/app/vault/page-c4d5289c2ea97a89.js +0 -1
  513. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b91f8952deaeaf27.js +0 -1
  514. package/apps/pwa/out/_next/static/chunks/app/workflows/page-71b43a2588489987.js +0 -1
  515. package/apps/pwa/out/_next/static/chunks/app/workspace/page-2b256feed331546f.js +0 -1
  516. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-505bb96d69a8622c.js +0 -1
  517. package/apps/pwa/out/_next/static/chunks/framework-172630e6aa5e2d7f.js +0 -1
  518. package/apps/pwa/out/_next/static/chunks/main-app-f0fe2d7477845181.js +0 -1
  519. package/apps/pwa/out/_next/static/chunks/main-fc98ac70b44eccb0.js +0 -1
  520. package/apps/pwa/out/_next/static/chunks/pages/_app-5bd7f10271f72413.js +0 -1
  521. package/apps/pwa/out/_next/static/chunks/pages/_error-97d0d85ea05fef9b.js +0 -1
  522. package/apps/pwa/out/_next/static/chunks/webpack-9e4d02d567b98240.js +0 -1
  523. package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +0 -1
  524. /package/apps/pwa/out/_next/static/{D51l8jUBplJrIpWvENsDK → LZcstaO9_NXypPFmujolv}/_ssgManifest.js +0 -0
@@ -0,0 +1,82 @@
1
+ import { debug } from '../../debug/log.js';
2
+
3
+ export interface AuthorInput {
4
+ title: string;
5
+ text: string;
6
+ cellId: string;
7
+ version: string;
8
+ }
9
+
10
+ export interface AuthorInputSignal {
11
+ kind: 'path' | 'symbol' | 'command' | 'marker' | 'missing';
12
+ match: string;
13
+ field: 'title' | 'text';
14
+ reason: string;
15
+ }
16
+
17
+ export interface AuthorInputCheck {
18
+ verdict: 'approved' | 'resubmit' | 'confirm' | 'uncheckable';
19
+ signals: AuthorInputSignal[];
20
+ units: { checked: number; candidates: number };
21
+ implementationRatio: number | null;
22
+ }
23
+
24
+ const TOKEN_SHAPES: ReadonlyArray<{ kind: AuthorInputSignal['kind']; pattern: RegExp; reason: string }> = [
25
+ { kind: 'path', pattern: /[\w.@~-]+\/[\w./@~-]+|[\w-]+\.[A-Za-z][A-Za-z0-9]{0,7}/g,
26
+ reason: '경로 또는 파일 이름 꼴' },
27
+ { kind: 'symbol', pattern: /[A-Za-z_][\w.]*\(\)|`[A-Za-z_][\w.]*`/g,
28
+ reason: '함수 또는 심볼 이름 꼴' },
29
+ { kind: 'command', pattern: /\b(?:bun|npm|pnpm|yarn|npx|bunx|node|deno|python3?|pip|uv|make|git|gh|cargo|go|docker|kubectl|elanous|eln|bash|sh|zsh|curl)\s+[a-z][\w-]*\b|--[\w-]+/g,
30
+ reason: '실행 명령 또는 플래그 꼴' },
31
+ { kind: 'marker', pattern: /대상 경로:|경계:|불변식:|판정 신호:|관측\s*=|기대\s*=/g,
32
+ reason: '골 구조 마커 꼴' },
33
+ ];
34
+
35
+ const KOREAN_DIRECTIVE = /하라|해라|하세요|하십시오|해\s?주세요|해\s?줘|해야|바꿔|고쳐|넣어|빼|돌려|추가|수정|삭제|판정|실행|쓰라/;
36
+ const ENGLISH_DIRECTIVE = /\b(?:please|must|should)\b|^\s*(?:run|edit|change|fix|add|remove|delete|update|modify|execute|test|build|use|write|implement|create|set|check|verify)\b/i;
37
+
38
+ function sentences(value: string): string[] {
39
+ return value.split(/(?<=[.?])(?=\s|$)|\n+/).map(part => part.trim()).filter(Boolean);
40
+ }
41
+
42
+ export function checkAuthorInput({ title, text, cellId, version }: AuthorInput): AuthorInputCheck {
43
+ const validIdentity = [title, cellId, version].every(value => typeof value === 'string' && !!value.trim())
44
+ && /^\d+\.\d+\.\d+$/.test(version);
45
+ const missingText = typeof text !== 'string' || !text.trim();
46
+ const valid = validIdentity && !missingText;
47
+ const signals: AuthorInputSignal[] = [];
48
+ if (validIdentity && missingText) signals.push({ kind: 'missing', match: '', field: 'text', reason: 'missing cell text' });
49
+ let checked = 0;
50
+ let candidates = 0;
51
+ let directed = false;
52
+
53
+ if (validIdentity) {
54
+ for (const field of ['title', 'text'] as const) {
55
+ if (field === 'text' && missingText) continue;
56
+ for (const sentence of sentences(field === 'title' ? title : text)) {
57
+ checked++;
58
+ let found = false;
59
+ for (const { kind, pattern, reason } of TOKEN_SHAPES) {
60
+ for (const match of sentence.matchAll(pattern)) {
61
+ signals.push({ kind, match: match[0], field, reason });
62
+ found = true;
63
+ if (kind === 'marker' || kind === 'command') directed = true;
64
+ }
65
+ }
66
+ if (found) {
67
+ candidates++;
68
+ if (KOREAN_DIRECTIVE.test(sentence) || ENGLISH_DIRECTIVE.test(sentence)) directed = true;
69
+ }
70
+ }
71
+ }
72
+ }
73
+
74
+ const verdict: AuthorInputCheck['verdict'] = !valid ? 'uncheckable'
75
+ : signals.length === 0 ? 'approved' : directed ? 'resubmit' : 'confirm';
76
+ const implementationRatio = valid ? candidates / checked : null;
77
+ if (!valid) { checked = 0; candidates = 0; }
78
+ debug.log('author.par', 'input-checked', {
79
+ cellId, version, verdict, signals: signals.length, ratio: implementationRatio,
80
+ });
81
+ return { verdict, signals, units: { checked, candidates }, implementationRatio };
82
+ }
@@ -0,0 +1,232 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import { createHash } from 'node:crypto';
3
+ import { chmodSync, mkdirSync } from 'node:fs';
4
+ import { dirname, join } from 'node:path';
5
+ import { debug } from '../../debug/log.js';
6
+ import { effectiveInstanceRoot } from '../../instance/resolve.js';
7
+
8
+ export type AuthorVerdict = 'approved' | 'resubmit' | 'confirm' | 'uncheckable';
9
+ export type AuthorStatus = 'held' | 'queued-for-author' | 'authoring' | 'authored' | 'enqueued'
10
+ | 'failed' | 'cancelled' | 'superseded-by-cell-change';
11
+ export interface AuthorCheck { verdict: AuthorVerdict; signals: unknown[]; ratio: number | null }
12
+ export interface AuthorRequest { seat: string; cellId: string; version: string; title: string; text: string; check: AuthorCheck }
13
+ export interface AuthorDetail {
14
+ queueId?: string;
15
+ goalRef?: string;
16
+ supersededBy?: string;
17
+ textHash?: string;
18
+ reason?: string;
19
+ }
20
+ export interface AuthorHistory {
21
+ fromStatus: AuthorStatus | null;
22
+ toStatus: AuthorStatus;
23
+ at: string;
24
+ by: string;
25
+ detail: AuthorDetail | AuthorCheck | string;
26
+ }
27
+ export interface AuthorRow extends AuthorRequest {
28
+ id: string;
29
+ status: AuthorStatus;
30
+ queueId: string | null;
31
+ goalRef: string | null;
32
+ supersededBy: string | null;
33
+ textHash: string | null;
34
+ createdAt: string;
35
+ updatedAt: string;
36
+ }
37
+ export interface AuthorRecord extends AuthorRow { history: AuthorHistory[] }
38
+ export interface AuthorMetric { value: number | null; reason?: '못 쟀다(원장 결손)' }
39
+ export interface AuthorMetrics {
40
+ tps: AuthorMetric;
41
+ latencyMs: AuthorMetric;
42
+ depthBySeat: { value: Record<string, number> | null; reason?: '못 쟀다(원장 결손)' };
43
+ }
44
+ export interface AuthorLedgerOptions { path?: string; now?: () => Date }
45
+
46
+ type StoredRow = Omit<AuthorRow, 'check'> & { checkJson: string };
47
+ type StoredHistory = Omit<AuthorHistory, 'detail'> & { detailJson: string };
48
+ const unmeasured = { value: null, reason: '못 쟀다(원장 결손)' } as const;
49
+ // Terminal states never change again: a cancel/fail after a supersede would erase the recorded replacement (supersededBy · textHash).
50
+ const TERMINAL: ReadonlySet<AuthorStatus> = new Set(['failed', 'cancelled', 'superseded-by-cell-change']);
51
+ const successors: Partial<Record<AuthorStatus, AuthorStatus>> = {
52
+ 'queued-for-author': 'authoring', authoring: 'authored', authored: 'enqueued',
53
+ };
54
+
55
+ function hash(parts: string[]): string {
56
+ const digest = createHash('sha256');
57
+ for (const part of parts) {
58
+ const bytes = Buffer.from(part, 'utf8');
59
+ const length = Buffer.alloc(4);
60
+ length.writeUInt32BE(bytes.length);
61
+ digest.update(length).update(bytes);
62
+ }
63
+ return digest.digest('hex');
64
+ }
65
+
66
+ export class AuthorLedger {
67
+ readonly path: string;
68
+ private readonly now: () => Date;
69
+
70
+ constructor(options: AuthorLedgerOptions = {}) {
71
+ this.path = options.path ?? join(effectiveInstanceRoot(), 'orchestrator', 'author-ledger.sqlite');
72
+ this.now = options.now ?? (() => new Date());
73
+ }
74
+
75
+ private open(create: boolean): Database {
76
+ if (create) mkdirSync(dirname(this.path), { recursive: true });
77
+ const db = new Database(this.path, { create, strict: true });
78
+ try {
79
+ if (create) chmodSync(this.path, 0o600);
80
+ db.exec('PRAGMA busy_timeout = 10000');
81
+ db.exec('PRAGMA foreign_keys = ON');
82
+ if (create) db.exec(`CREATE TABLE IF NOT EXISTS requests (
83
+ id TEXT PRIMARY KEY, seat TEXT NOT NULL, cellId TEXT NOT NULL, version TEXT NOT NULL,
84
+ title TEXT NOT NULL, text TEXT NOT NULL, checkJson TEXT NOT NULL,
85
+ status TEXT NOT NULL, queueId TEXT, goalRef TEXT, supersededBy TEXT, textHash TEXT,
86
+ createdAt TEXT NOT NULL, updatedAt TEXT NOT NULL);
87
+ CREATE TABLE IF NOT EXISTS history (
88
+ requestId TEXT NOT NULL REFERENCES requests(id), fromStatus TEXT, toStatus TEXT NOT NULL,
89
+ at TEXT NOT NULL, by TEXT NOT NULL, detailJson TEXT NOT NULL);`);
90
+ return db;
91
+ } catch (error) { db.close(); throw error; }
92
+ }
93
+
94
+ private using<T>(work: (db: Database) => T): T {
95
+ const db = this.open(true);
96
+ try { return work(db); } finally { db.close(); }
97
+ }
98
+
99
+ private transaction<T>(db: Database, work: () => T): T {
100
+ db.exec('BEGIN IMMEDIATE');
101
+ try { const result = work(); db.exec('COMMIT'); return result; }
102
+ catch (error) { db.exec('ROLLBACK'); throw error; }
103
+ }
104
+
105
+ private row(db: Database, id: string): AuthorRow {
106
+ const row = db.query('SELECT * FROM requests WHERE id = ?').get(id) as StoredRow | null;
107
+ if (!row) throw new Error(`author request not found: ${id}`);
108
+ const { checkJson, ...rest } = row;
109
+ return { ...rest, check: JSON.parse(checkJson) as AuthorCheck };
110
+ }
111
+
112
+ private record(db: Database, id: string, from: AuthorStatus | null, to: AuthorStatus,
113
+ at: string, by: string, detail: AuthorDetail | AuthorCheck | string): void {
114
+ db.query('INSERT INTO history (requestId, fromStatus, toStatus, at, by, detailJson) VALUES (?, ?, ?, ?, ?, ?)')
115
+ .run(id, from, to, at, by, JSON.stringify(detail));
116
+ }
117
+
118
+ private log(event: string, row: AuthorRow): void {
119
+ debug.log('author.par', 'ledger', { event, seat: row.seat, cellId: row.cellId, version: row.version, status: row.status });
120
+ }
121
+
122
+ request(input: AuthorRequest): AuthorRow & { duplicate: boolean } {
123
+ for (const key of ['seat', 'cellId', 'version', 'title'] as const) {
124
+ if (typeof input[key] !== 'string' || !input[key].trim()) throw new Error(`${key} is required`);
125
+ }
126
+ if (typeof input.text !== 'string' || (!input.text.trim() && input.check?.verdict !== 'uncheckable'))
127
+ throw new Error('text is required unless uncheckable');
128
+ if (!['approved', 'resubmit', 'confirm', 'uncheckable'].includes(input.check?.verdict)
129
+ || !Array.isArray(input.check.signals)
130
+ || (input.check.ratio !== null && (typeof input.check.ratio !== 'number' || !Number.isFinite(input.check.ratio))))
131
+ throw new Error('invalid author input check');
132
+ const id = hash([input.seat, input.cellId, input.version, input.title, input.text]);
133
+ return this.using(db => this.transaction(db, () => {
134
+ if (db.query('SELECT 1 FROM requests WHERE id = ?').get(id)) {
135
+ const row = this.row(db, id);
136
+ this.log('duplicate', row);
137
+ return { ...row, duplicate: true };
138
+ }
139
+ const at = this.now().toISOString();
140
+ const status = input.check.verdict === 'approved' ? 'queued-for-author' : 'held';
141
+ db.query(`INSERT INTO requests (id, seat, cellId, version, title, text, checkJson, status, createdAt, updatedAt)
142
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, input.seat, input.cellId, input.version,
143
+ input.title, input.text, JSON.stringify(input.check), status, at, at);
144
+ this.record(db, id, null, status, at, 'system', input.check);
145
+ const row = this.row(db, id);
146
+ this.log('request', row);
147
+ return { ...row, duplicate: false };
148
+ }));
149
+ }
150
+
151
+ transition(id: string, to: AuthorStatus, input: { by: string; detail: AuthorDetail | string }): AuthorRow {
152
+ return this.using(db => this.transaction(db, () => {
153
+ const previous = this.row(db, id);
154
+ const allowed = !TERMINAL.has(previous.status) && (successors[previous.status] === to
155
+ || (to === 'superseded-by-cell-change' && (previous.status === 'authored' || previous.status === 'enqueued'))
156
+ || to === 'failed' || to === 'cancelled');
157
+ if (!allowed) throw new Error(`invalid author transition: ${previous.status} → ${to}`);
158
+ const detail = input.detail;
159
+ const fields = typeof detail === 'string' ? {} : detail;
160
+ if (!input.by?.trim()) throw new Error('by is required');
161
+ if (to === 'authored' && !fields.goalRef?.trim()) throw new Error('goalRef is required');
162
+ if (to === 'enqueued' && !fields.queueId?.trim()) throw new Error('queueId is required');
163
+ if (to === 'superseded-by-cell-change') {
164
+ if (!previous.goalRef) throw new Error('old goalRef is required');
165
+ if (!fields.supersededBy?.trim() || !/^[a-f0-9]{64}$/.test(fields.textHash ?? ''))
166
+ throw new Error('supersededBy and textHash are required');
167
+ const candidates = db.query(`SELECT id, text FROM requests
168
+ WHERE seat = ? AND cellId = ? AND version = ? AND goalRef = ? AND status IN ('authored', 'enqueued')`)
169
+ .all(previous.seat, previous.cellId, previous.version, fields.supersededBy) as Array<{ id: string; text: string }>;
170
+ if (candidates.length !== 1 || candidates[0]!.id === id || candidates[0]!.text === previous.text
171
+ || createHash('sha256').update(candidates[0]!.text, 'utf8').digest('hex') !== fields.textHash)
172
+ throw new Error('supersededBy and textHash must match a changed authored snapshot');
173
+ }
174
+ const at = this.now().toISOString();
175
+ const recordedDetail: AuthorDetail | string = typeof detail === 'string' ? detail :
176
+ to === 'authored' ? { goalRef: fields.goalRef } :
177
+ to === 'enqueued' ? { queueId: fields.queueId } :
178
+ to === 'superseded-by-cell-change' ? { supersededBy: fields.supersededBy, textHash: fields.textHash } :
179
+ to === 'failed' || to === 'cancelled' ? { reason: fields.reason } : {};
180
+ db.query(`UPDATE requests SET status = ?, queueId = COALESCE(?, queueId), goalRef = COALESCE(?, goalRef),
181
+ supersededBy = ?, textHash = ?, updatedAt = ? WHERE id = ?`).run(to,
182
+ to === 'enqueued' ? fields.queueId ?? null : null, to === 'authored' ? fields.goalRef ?? null : null,
183
+ to === 'superseded-by-cell-change' ? fields.supersededBy ?? null : null,
184
+ to === 'superseded-by-cell-change' ? fields.textHash ?? null : null, at, id);
185
+ this.record(db, id, previous.status, to, at, input.by, recordedDetail);
186
+ const row = this.row(db, id);
187
+ this.log('transition', row);
188
+ return row;
189
+ }));
190
+ }
191
+
192
+ get(id: string): AuthorRecord {
193
+ return this.using(db => ({ ...this.row(db, id), history: (db.query(
194
+ 'SELECT fromStatus, toStatus, at, by, detailJson FROM history WHERE requestId = ? ORDER BY rowid',
195
+ ).all(id) as StoredHistory[]).map(({ detailJson, ...item }) => ({ ...item, detail: JSON.parse(detailJson) as AuthorDetail | AuthorCheck | string })) }));
196
+ }
197
+
198
+ metrics({ since, until }: { since: Date; until: Date }): AuthorMetrics {
199
+ const duration = (until.getTime() - since.getTime()) / 1000;
200
+ if (!Number.isFinite(duration) || duration <= 0) throw new Error('metrics window must have positive duration');
201
+ let db: Database;
202
+ try { db = this.open(false); }
203
+ catch { return { tps: unmeasured, latencyMs: unmeasured, depthBySeat: unmeasured }; }
204
+ try {
205
+ const start = since.toISOString();
206
+ const end = until.toISOString();
207
+ const events = (db.query('SELECT count(*) AS n FROM history WHERE at >= ? AND at < ?').get(start, end) as { n: number }).n;
208
+ if (!events) return { tps: unmeasured, latencyMs: unmeasured, depthBySeat: unmeasured };
209
+ const completed = db.query(`SELECT r.seat, r.version, r.cellId, r.createdAt, h.at
210
+ FROM history h JOIN requests r ON r.id = h.requestId
211
+ WHERE h.toStatus = 'authored' AND h.at >= ? AND h.at < ?`).all(start, end) as
212
+ Array<{ seat: string; version: string; cellId: string; createdAt: string; at: string }>;
213
+ const distinctCells = new Set(completed.map(row => JSON.stringify([row.seat, row.version, row.cellId])));
214
+ const delays = completed.map(row => Date.parse(row.at) - Date.parse(row.createdAt));
215
+ const latencyMs = delays.length && delays.every(delay => Number.isFinite(delay) && delay >= 0)
216
+ ? { value: delays.reduce((sum, delay) => sum + delay, 0) / delays.length } : unmeasured;
217
+ const depthRows = db.query(`WITH last_status AS (
218
+ SELECT requestId, toStatus, ROW_NUMBER() OVER (
219
+ PARTITION BY requestId ORDER BY at DESC, rowid DESC
220
+ ) AS rank FROM history WHERE at < ?
221
+ )
222
+ SELECT seat, count(*) AS n FROM (
223
+ SELECT DISTINCT r.seat, r.version, r.cellId FROM last_status h
224
+ JOIN requests r ON r.id = h.requestId
225
+ WHERE h.rank = 1 AND h.toStatus IN ('queued-for-author', 'authoring')
226
+ ) GROUP BY seat`).all(end) as Array<{ seat: string; n: number }>;
227
+ return { tps: { value: distinctCells.size / duration }, latencyMs,
228
+ depthBySeat: { value: Object.fromEntries(depthRows.map(row => [row.seat, row.n])) } };
229
+ } catch { return { tps: unmeasured, latencyMs: unmeasured, depthBySeat: unmeasured }; }
230
+ finally { db.close(); }
231
+ }
232
+ }
@@ -0,0 +1,175 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { readdirSync, realpathSync, statSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
+ import { debug } from '../../debug/log.js';
5
+ import { loadRunLedger, resolveFederatedRunLedgerTargets } from '../../self-implement/run-ledger.js';
6
+ import type { LogTarget } from '../../cli/logs-cli.js';
7
+
8
+ const DAY_MS = 24 * 60 * 60 * 1_000;
9
+ const GH_LIMIT = 1000;
10
+
11
+ export type FinishMetric = 'launched' | 'landed' | 'landingRate' | 'staleDrafts' | 'conflictRatio' | 'unknownMergeable';
12
+ export type FinishMetrics = {
13
+ launched: number | null;
14
+ launchedUnreadable?: number;
15
+ landed: number | null;
16
+ landingRate: number | null;
17
+ staleDrafts: number | null;
18
+ conflictRatio: number | null;
19
+ unknownMergeable: number | null;
20
+ reasons: Partial<Record<FinishMetric, string>>;
21
+ };
22
+ export type FinishAdvice = { finishSlots: number; launchSlots: number; state: 'healthy' | 'backlogged' | 'unknown'; reasons: string[] };
23
+ export type FinishThresholds = { landingRate?: number; staleDrafts?: number; conflictRatio?: number };
24
+
25
+ type Start = { runId: string; startedAt: string };
26
+ type StartObservation = { starts: readonly Start[]; unreadable: readonly { runId: string; reason: string }[]; unreadableDirectories?: number };
27
+ type Pr = { number: number; headRefName: string; mergedAt?: string; createdAt?: string; mergeable?: string; labels?: { name: string }[] };
28
+ export interface MeasureFinishDeps {
29
+ listStarts?: (cutoff: number) => StartObservation;
30
+ ledgerTargets?: readonly LogTarget[];
31
+ runGh?: (args: string[]) => string;
32
+ }
33
+
34
+ function defaultListStarts(cutoff: number, targets?: readonly LogTarget[]): StartObservation {
35
+ const directories = new Set<string>();
36
+ const starts = new Map<string, Start>();
37
+ const unreadable = new Map<string, { runId: string; reason: string }>();
38
+ const childLedgers = new Set<string>();
39
+ let unreadableDirectories = 0;
40
+ let unresolvedDirectories = 0;
41
+ for (const { ledgerDirectory } of resolveFederatedRunLedgerTargets({ includeTest: true, ...(targets ? { targets } : {}) })) {
42
+ let dir: string;
43
+ try {
44
+ dir = realpathSync(ledgerDirectory);
45
+ } catch {
46
+ unreadableDirectories += 1;
47
+ unresolvedDirectories += 1;
48
+ continue;
49
+ }
50
+ if (directories.has(dir)) continue;
51
+ directories.add(dir);
52
+ let names: string[];
53
+ try {
54
+ names = readdirSync(dir);
55
+ } catch {
56
+ unreadableDirectories += 1;
57
+ continue;
58
+ }
59
+ for (const name of names) {
60
+ if (!/^run-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.jsonl$/.test(name)) continue;
61
+ const path = join(dir, name);
62
+ if (statSync(path).mtimeMs < cutoff) continue;
63
+ const runId = name.slice(0, -'.jsonl'.length);
64
+ const entries = loadRunLedger(runId, dir);
65
+ if (!entries) throw new Error(`run ledger disappeared: ${runId}`);
66
+ if (entries[0]?.event === 'pod-child-run') {
67
+ childLedgers.add(runId);
68
+ continue;
69
+ }
70
+ const start = entries.find(entry => entry.event === 'start');
71
+ if (!start?.timestamp) {
72
+ if (!starts.has(runId)) unreadable.set(runId, { runId, reason: entries[0]?.event ?? 'start missing' });
73
+ continue;
74
+ }
75
+ starts.set(runId, { runId, startedAt: start.timestamp });
76
+ unreadable.delete(runId);
77
+ }
78
+ }
79
+ debug.log('loop.orchestrator', 'finish-launch-population', {
80
+ directories: directories.size + unresolvedDirectories, launched: starts.size, childLedgers: childLedgers.size, unreadable: unreadable.size,
81
+ });
82
+ return { starts: [...starts.values()], unreadable: [...unreadable.values()], unreadableDirectories };
83
+ }
84
+
85
+ function defaultRunGh(args: string[]): string {
86
+ return execFileSync('gh', args, { cwd: resolve(import.meta.dir, '../../..'), encoding: 'utf8', timeout: 60_000, maxBuffer: 20 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] });
87
+ }
88
+
89
+ function readPrs(runGh: (args: string[]) => string, args: string[]): Pr[] {
90
+ const parsed: unknown = JSON.parse(runGh(['pr', 'list', ...args, '--limit', String(GH_LIMIT)]));
91
+ if (!Array.isArray(parsed) || parsed.length >= GH_LIMIT || parsed.some(pr => !pr || typeof pr !== 'object'
92
+ || typeof pr.number !== 'number' || typeof pr.headRefName !== 'string')) throw new Error('incomplete or invalid PR observation');
93
+ return parsed as Pr[];
94
+ }
95
+
96
+ function reason(error: unknown): string { return error instanceof Error ? error.message : String(error); }
97
+
98
+ /** Read each source independently; an unreadable source never becomes a measured zero. */
99
+ export function measureFinish(deps: MeasureFinishDeps = {}, now: Date = new Date()): FinishMetrics {
100
+ const reasons: FinishMetrics['reasons'] = {};
101
+ const cutoff = now.getTime() - DAY_MS;
102
+ let launched: number | null = null;
103
+ let launchedUnreadable = 0;
104
+ let landed: number | null = null;
105
+ let staleDrafts: number | null = null;
106
+ let conflictRatio: number | null = null;
107
+ let unknownMergeable: number | null = null;
108
+ const gh = deps.runGh ?? defaultRunGh;
109
+ try {
110
+ const observation = deps.listStarts ? deps.listStarts(cutoff) : defaultListStarts(cutoff, deps.ledgerTargets);
111
+ if (!observation || !Array.isArray(observation.starts) || !Array.isArray(observation.unreadable)
112
+ || observation.starts.some(start => !start || typeof start.runId !== 'string' || !Number.isFinite(Date.parse(start.startedAt)))
113
+ || observation.unreadable.some(entry => !entry || typeof entry.runId !== 'string' || typeof entry.reason !== 'string')) throw new Error('invalid run start observation');
114
+ launched = new Set(observation.starts.filter(start => Date.parse(start.startedAt) >= cutoff && Date.parse(start.startedAt) <= now.getTime()).map(start => start.runId)).size;
115
+ launchedUnreadable = observation.unreadable.length;
116
+ if (launchedUnreadable > 0) {
117
+ reasons.launched = `${launchedUnreadable}개 원장 시작 줄 없음(하한값)`;
118
+ debug.log('loop.orchestrator', 'finish-ledger-unreadable', { count: launchedUnreadable, sample: observation.unreadable.slice(0, 5) });
119
+ }
120
+ if (observation.unreadableDirectories) {
121
+ reasons.launched = [reasons.launched, `원장 폴더 ${observation.unreadableDirectories}개 못 읽음(하한값)`].filter(Boolean).join(' · ');
122
+ }
123
+ } catch (error) { reasons.launched = reason(error); }
124
+ try {
125
+ const prs = readPrs(gh, ['--state', 'merged', '--search', `merged:>=${new Date(cutoff).toISOString().replace(/\.\d{3}Z$/, 'Z')}`, '--json', 'number,headRefName,mergedAt']);
126
+ if (prs.some(pr => typeof pr.mergedAt !== 'string' || !Number.isFinite(Date.parse(pr.mergedAt)))) throw new Error('invalid merged PR timestamp');
127
+ landed = new Set(prs.filter(pr => pr.headRefName.startsWith('self-impl/') && Date.parse(pr.mergedAt!) >= cutoff
128
+ && Date.parse(pr.mergedAt!) <= now.getTime()).map(pr => pr.number)).size;
129
+ } catch (error) { reasons.landed = reason(error); }
130
+ try {
131
+ const drafts = readPrs(gh, ['--state', 'open', '--draft', '--json', 'number,headRefName,createdAt,mergeable,labels']);
132
+ if (drafts.some(pr => typeof pr.createdAt !== 'string' || !Number.isFinite(Date.parse(pr.createdAt)) || !Array.isArray(pr.labels)
133
+ || pr.labels.some(label => !label || typeof label.name !== 'string'))) throw new Error('invalid draft observation');
134
+ staleDrafts = drafts.filter(pr => pr.headRefName.startsWith('self-impl/') && Date.parse(pr.createdAt!) < cutoff
135
+ && !pr.labels!.some(label => label.name === 'elanous:superseded')).length;
136
+ } catch (error) { reasons.staleDrafts = reason(error); }
137
+ try {
138
+ const prs = readPrs(gh, ['--state', 'open', '--json', 'number,headRefName,mergeable']);
139
+ const harness = prs.filter(pr => pr.headRefName.startsWith('self-impl/'));
140
+ if (harness.some(pr => !['CONFLICTING', 'MERGEABLE', 'UNKNOWN'].includes(pr.mergeable ?? ''))) throw new Error('invalid mergeability observation');
141
+ unknownMergeable = harness.filter(pr => pr.mergeable === 'UNKNOWN').length;
142
+ const known = harness.length - unknownMergeable;
143
+ if (known === 0) reasons.conflictRatio = 'no open harness PRs with known mergeability';
144
+ else conflictRatio = harness.filter(pr => pr.mergeable === 'CONFLICTING').length / known;
145
+ } catch (error) { reasons.conflictRatio = reason(error); reasons.unknownMergeable = reason(error); unknownMergeable = null; }
146
+ let landingRate: number | null = null;
147
+ if (launched === null) reasons.landingRate = `launched unavailable: ${reasons.launched}`;
148
+ else if (landed === null) reasons.landingRate = `landed unavailable: ${reasons.landed}`;
149
+ else if (launched === 0) reasons.landingRate = reasons.launched
150
+ ? `launched lower bound is zero — launch population unknown: ${reasons.launched}`
151
+ : 'no harness runs launched in the last 24 hours';
152
+ else if (landed > launched) reasons.landingRate = `landed exceeds launched — population mismatch (launched=${launched}, landed=${landed})`;
153
+ else landingRate = landed / launched;
154
+ return { launched, ...(launchedUnreadable > 0 ? { launchedUnreadable } : {}), landed, landingRate, staleDrafts, conflictRatio, unknownMergeable, reasons };
155
+ }
156
+
157
+ /** Advice only: no launch cap, PR, or config is changed. */
158
+ export function finishAdvice(metrics: FinishMetrics, totalSlots: number, thresholds: FinishThresholds = {}): FinishAdvice {
159
+ const reasons: string[] = [];
160
+ const missing = (['launched', 'landed', 'landingRate', 'staleDrafts', 'conflictRatio', 'unknownMergeable'] as const)
161
+ .filter(key => metrics[key] === null).map(key => `${key}: ${metrics.reasons[key] ?? 'unavailable'}`);
162
+ let state: FinishAdvice['state'];
163
+ let desired: number;
164
+ if (missing.length) { state = 'unknown'; desired = 6; reasons.push(...missing); }
165
+ else {
166
+ if (metrics.landingRate! < (thresholds.landingRate ?? 0.5)) reasons.push('landingRate below threshold');
167
+ if (metrics.staleDrafts! > (thresholds.staleDrafts ?? 20)) reasons.push('staleDrafts above threshold');
168
+ if (metrics.conflictRatio! > (thresholds.conflictRatio ?? 0.3)) reasons.push('conflictRatio above threshold');
169
+ state = reasons.length ? 'backlogged' : 'healthy';
170
+ desired = state === 'backlogged' ? Math.max(6, Math.ceil(totalSlots * 0.3)) : 2;
171
+ }
172
+ if (metrics.launched !== null && metrics.reasons.launched) reasons.push(metrics.reasons.launched);
173
+ const finishSlots = Math.min(totalSlots, desired);
174
+ return { finishSlots, launchSlots: totalSlots - finishSlots, state, reasons };
175
+ }
@@ -1,6 +1,6 @@
1
1
  import { execFileSync } from 'node:child_process';
2
- import { readFileSync } from 'node:fs';
3
- import { basename, isAbsolute, resolve } from 'node:path';
2
+ import { readFileSync, readdirSync } from 'node:fs';
3
+ import { basename, isAbsolute, join, resolve } from 'node:path';
4
4
  import { debug } from '../../debug/log.js';
5
5
  import { defaultListHarnessProcesses, type HarnessProcessListObservation } from '../../harness/harness-cli-command.js';
6
6
  import { getUserConfig, ORCHESTRATOR_DEFAULTS, type OrchestratorLoopConfig, type OrchestratorSeat } from '../../user-config.js';
@@ -10,7 +10,8 @@ export type WorkItem = { kind: 'pr' | 'goal'; ref: string; seat: OrchestratorSea
10
10
  export type WorkRef = Pick<WorkItem, 'kind' | 'ref' | 'seat'>;
11
11
  export type Overlap = ({ type: 'file'; path: string } | { type: 'cell'; cell: string }) & { refs: WorkRef[]; crossSeat: boolean };
12
12
 
13
- type OpenPr = { number: number; title: string; body?: string | null; headRefName: string; files: Array<{ path: string }>; isDraft: boolean };
13
+ type OpenPr = { number: number; title: string; body?: string | null; headRefName: string; files: Array<{ path: string }>; isDraft: boolean; labels: Array<{ name: string }> };
14
+ export type CollectWorkResult = { work: WorkItem[]; excluded: { stalled: number; superseded: number; draft: number }; unreadableTrees: number; unreadableGoals: number };
14
15
  export interface CollectWorkDeps {
15
16
  runGh?: (args: string[]) => string;
16
17
  listProcesses?: () => HarnessProcessListObservation;
@@ -59,14 +60,51 @@ function goalFiles(text: string): string[] {
59
60
  return [...new Set(match[1]!.split(/\s*[·,]\s*/).map(file => file.trim().replace(/^`|`$/g, '')).filter(Boolean))];
60
61
  }
61
62
 
62
- export function collectWork(deps: CollectWorkDeps = {}): WorkItem[] {
63
- const prs = JSON.parse((deps.runGh ?? defaultRunGh)(['pr', 'list', '--state', 'open', '--limit', '1000', '--json', 'number,title,body,headRefName,files,isDraft'])) as OpenPr[];
63
+ export function collectWork(deps: CollectWorkDeps = {}): CollectWorkResult {
64
+ const prs = JSON.parse((deps.runGh ?? defaultRunGh)(['pr', 'list', '--state', 'open', '--limit', '1000', '--json', 'number,title,body,headRefName,files,isDraft,labels'])) as OpenPr[];
64
65
  if (!Array.isArray(prs)) throw new Error('PR observation is not a list');
65
66
  const observation = (deps.listProcesses ?? defaultListHarnessProcesses)();
66
67
  if (observation.status !== 'ok') throw new Error(`process observation ${observation.status}`);
67
68
  const cfg = deps.config ?? getUserConfig().loops?.orchestrator ?? ORCHESTRATOR_DEFAULTS;
68
- const work: WorkItem[] = prs.map(pr => ({ kind: 'pr', ref: `#${pr.number}`, seat: seatOfPr(pr.title),
69
- files: pr.files.map(file => file.path), cells: cellsOf(`${pr.title}\n${pr.body ?? ''}`) }));
69
+ const excluded = { stalled: 0, superseded: 0, draft: 0 };
70
+ const ready = prs.filter(pr => {
71
+ const labels = pr.labels ?? [];
72
+ if (labels.some(label => label.name === 'elanous:stalled')) { excluded.stalled++; return false; }
73
+ if (labels.some(label => label.name === 'elanous:superseded')) { excluded.superseded++; return false; }
74
+ if (pr.isDraft) { excluded.draft++; return false; }
75
+ return true;
76
+ });
77
+ const seatsByPr = new Map<number, Set<OrchestratorSeat>>();
78
+ let unreadableTrees = 0;
79
+ let unreadableGoals = 0;
80
+ for (const seat of TRAFFIC_SEATS) {
81
+ for (const tree of cfg.seatTrees[seat] ?? []) {
82
+ try {
83
+ const goals = join(tree, 'docs', 'goals');
84
+ const numbers = new Set<number>();
85
+ for (const entry of readdirSync(goals, { withFileTypes: true })) {
86
+ if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
87
+ // One unreadable goal file must not drop the whole tree's PR→seat mapping; count it instead.
88
+ let text: string;
89
+ try { text = (deps.readGoal ?? ((file: string) => readFileSync(file, 'utf8')))(join(goals, entry.name)); }
90
+ catch { unreadableGoals++; continue; }
91
+ for (const match of text.matchAll(/^[ \t]*prNumber:[ \t]*(\d+)[ \t]*\r?$/gm)) numbers.add(Number(match[1]));
92
+ }
93
+ for (const number of numbers) {
94
+ const seats = seatsByPr.get(number) ?? new Set<OrchestratorSeat>();
95
+ seats.add(seat);
96
+ seatsByPr.set(number, seats);
97
+ }
98
+ } catch {
99
+ unreadableTrees++;
100
+ }
101
+ }
102
+ }
103
+ const work: WorkItem[] = ready.map(pr => {
104
+ const matched = seatsByPr.get(pr.number);
105
+ return { kind: 'pr', ref: `#${pr.number}`, seat: seatOfPr(pr.title) ?? (matched?.size === 1 ? [...matched][0]! : null),
106
+ files: pr.files.map(file => file.path), cells: cellsOf(`${pr.title}\n${pr.body ?? ''}`) };
107
+ });
70
108
  for (const process of observation.records) {
71
109
  const path = goalPath(process.command);
72
110
  if (path === null) continue;
@@ -86,7 +124,7 @@ export function collectWork(deps: CollectWorkDeps = {}): WorkItem[] {
86
124
  }
87
125
  work.push({ kind: 'goal', ref, seat, files: goalFiles(text), cells: cellsOf(text) });
88
126
  }
89
- return work;
127
+ return { work, excluded, unreadableTrees, unreadableGoals };
90
128
  }
91
129
 
92
130
  export function findOverlaps(work: readonly WorkItem[], excludedFiles: readonly string[] = ['release/next.md']): Overlap[] {
@@ -119,16 +157,17 @@ export function renderOverlaps(overlaps: readonly Overlap[]): string[] {
119
157
  if (import.meta.main) {
120
158
  try {
121
159
  if (process.argv.slice(2).some(arg => arg !== '--json')) throw new Error('usage: overlap.ts [--json]');
122
- const work = collectWork();
160
+ const { work, excluded, unreadableTrees, unreadableGoals } = collectWork();
123
161
  const overlaps = findOverlaps(work);
124
162
  const files = overlaps.filter(overlap => overlap.type === 'file').length;
125
163
  const cells = overlaps.filter(overlap => overlap.type === 'cell').length;
126
164
  const crossSeat = overlaps.filter(overlap => overlap.crossSeat).length;
127
165
  const unreadable = work.filter(item => item.unreadable).length;
128
- debug.log('loop.orchestrator', 'exchange', { node: 'overlap', files, cells, crossSeat });
129
- const summary = `overlap work=${work.length} files=${files} cells=${cells} crossSeat=${crossSeat} unreadable=${unreadable}`;
166
+ const seatNull = work.filter(item => item.kind === 'pr' && item.seat === null).length;
167
+ debug.log('loop.orchestrator', 'exchange', { node: 'overlap', files, cells, crossSeat, excluded, seatNull, unreadableTrees, unreadableGoals });
168
+ const summary = `overlap work=${work.length} files=${files} cells=${cells} crossSeat=${crossSeat} unreadable=${unreadable} excluded=stalled:${excluded.stalled},superseded:${excluded.superseded},draft:${excluded.draft} seatNull=${seatNull} unreadableTrees=${unreadableTrees} unreadableGoals=${unreadableGoals}`;
130
169
  if (process.argv.includes('--json')) {
131
- console.log(JSON.stringify({ work, overlaps }));
170
+ console.log(JSON.stringify({ work, overlaps, excluded, seatNull, unreadableTrees, unreadableGoals }));
132
171
  console.error(summary);
133
172
  } else {
134
173
  for (const line of renderOverlaps(overlaps)) console.log(line);