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
@@ -1,34 +1,126 @@
1
1
  import { spawn, spawnSync } from 'node:child_process';
2
- import { randomUUID } from 'node:crypto';
3
- import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readlinkSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
4
4
  import { join, resolve } from 'node:path';
5
5
  import { Database } from 'bun:sqlite';
6
6
  import { debug } from '../debug/log.js';
7
- import { effectiveInstanceRoot, prodInstanceRoot } from '../instance/resolve.js';
8
- import { getUserConfig } from '../user-config.js';
7
+ import { effectiveInstanceRoot, prodInstanceRoot, releaseLedgerRoot } from '../instance/resolve.js';
8
+ import { listChecklist, type ChecklistItem } from '../release-loop/checklist.js';
9
+ import { listSchedules } from '../release-loop/release-schedule.js';
10
+ import { withFileLockSync } from '../storage/file-lock.js';
11
+ import { trafficTick, TRAFFIC_SEATS, type TrafficCell } from '../loops/orchestrator/traffic.js';
12
+ import { decideSpawn, seatCapDetails, seatCapReason } from '../loops/budget.js';
13
+ import { listSelfDevRuns, processBirthId, selfDevRunsDir } from '../self-dev/run-store.js';
14
+ import { getUserConfig, userConfigPath, type OrchestratorLoopConfig } from '../user-config.js';
9
15
  import { hostLeaseCounts, leaseHasPendingPod } from '../pod-lease/host-lease.js';
10
16
  import { leaseKubectl, measurePoolLease } from '../task-orchestrator/surfaces/pod-lease.js';
11
17
  import { parsePodPool, podPoolHostLease, resolvePodPoolSpec } from '../task-orchestrator/surfaces/pod-pool.js';
12
18
  import { writeHarnessQueueReceipt } from './harness-queue-child.js';
19
+ import { finishAdvice, measureFinish, type FinishMetrics } from '../loops/orchestrator/finish-rate.js';
13
20
 
14
21
  export type QueueSeat = 'OP' | 'TC' | 'MK' | 'UX';
15
22
  export type QueueItem = {
16
23
  id: string; seat: QueueSeat; kind: 'say' | 'ask'; input: string; hold: boolean; heavy: boolean;
17
- at: string; status: 'queued' | 'launching' | 'launched' | 'finished'; pid?: number; launchId?: string;
24
+ at: string; status: 'queued' | 'launching' | 'launched' | 'finished'; pid?: number; launchId?: string; idempotencyKey?: string; waitingReason?: string;
18
25
  };
19
26
  export type QueuePool = { running: number; pending: number; reserved: number; limit: number };
20
27
  export type QueueTick = { outcome: 'launched' | 'waiting' | 'skipped'; item?: QueueItem; reason: string };
21
- export type QueueProcess = { pid: number; seat?: QueueSeat; launchId?: string };
28
+ export type QueueProcess = { pid: number; seat?: QueueSeat; launchId?: string; runId?: string; rootPid?: number };
29
+ type ProcessProbe = {
30
+ run?: typeof spawnSync; platform?: NodeJS.Platform;
31
+ cwd?: (pid: number) => string;
32
+ seatTrees?: Partial<Record<QueueSeat, string[]>>;
33
+ runsDir?: string;
34
+ environ?: (pid: number) => string;
35
+ birthId?: (pid: number) => string | undefined;
36
+ };
37
+ const queueSeatNames = ['OP', 'TC', 'MK', 'UX'] as const;
38
+
39
+ export function queueSeatForCwd(cwd: string, trees: Partial<Record<QueueSeat, string[]>> = {}): QueueSeat | undefined {
40
+ if (!cwd.startsWith('/')) return undefined;
41
+ const current = resolve(cwd);
42
+ const matches = queueSeatNames.filter((s) => trees[s]?.some((tree) => {
43
+ if (!tree.startsWith('/')) return false;
44
+ const prefix = resolve(tree);
45
+ return current === prefix || current.startsWith(`${prefix}/`);
46
+ }));
47
+ return matches.length === 1 ? matches[0] : undefined;
48
+ }
22
49
 
23
50
  export interface HarnessQueueDeps {
24
51
  root?: string;
52
+ configPath?: string;
25
53
  cap?: (seat: QueueSeat) => number;
26
54
  pool?: () => QueuePool;
27
55
  launch?: (item: QueueItem, args: string[], root: string) => Promise<number>;
28
56
  alive?: (pid: number) => boolean;
29
57
  processes?: () => readonly QueueProcess[];
30
58
  receipt?: (root: string, launchId: string) => 'started' | 'finished' | 'not-started' | null;
59
+ now?: () => Date;
60
+ authorShadow?: (root: string, now: Date) => void | Promise<void>;
61
+ idleRequest?: (root: string, now: Date, items: readonly QueueItem[], deps: HarnessQueueDeps) => void | Promise<void>;
62
+ idleCells?: (now: Date) => { current: readonly Pick<ChecklistItem, 'id' | 'title' | 'owner' | 'status' | 'evidence'>[];
63
+ next: readonly Pick<ChecklistItem, 'id' | 'title' | 'owner' | 'status' | 'evidence'>[] };
64
+ idleLog?: (category: string, event: string, data: Record<string, unknown>) => void;
31
65
  log?: (event: 'enqueued' | 'launched' | 'waiting' | 'skipped', data: Record<string, unknown>) => void;
66
+ /** FINISH-RATE metrics source; a test process without it skips the real measurement. */
67
+ finishMetrics?: (now: Date) => FinishMetrics | Promise<FinishMetrics>;
68
+ }
69
+
70
+ const FINISH_CACHE_MS = 10 * 60 * 1_000;
71
+ const FINISH_KEYS = ['launched', 'landed', 'landingRate', 'staleDrafts', 'conflictRatio', 'unknownMergeable'] as const;
72
+
73
+ /** A cache row missing any metric or its reasons map is re-measured, never fed to finishAdvice. */
74
+ function validFinishMetrics(value: unknown): value is FinishMetrics {
75
+ if (!value || typeof value !== 'object') return false;
76
+ const row = value as Record<string, unknown>;
77
+ return FINISH_KEYS.every((key) => row[key] === null || (typeof row[key] === 'number' && Number.isFinite(row[key])))
78
+ && !!row.reasons && typeof row.reasons === 'object' && !Array.isArray(row.reasons);
79
+ }
80
+
81
+ /** Reads finish metrics at most once per 10 minutes per root; unreadable or missing → null (never a hold). */
82
+ async function cachedFinishMetrics(root: string, now: Date, deps: HarnessQueueDeps): Promise<FinishMetrics | null> {
83
+ // Checked before the cache: a cached real measurement must not reach an uninjected test tick either.
84
+ if (!deps.finishMetrics && (process.env.NODE_ENV === 'test' || process.env.ELANOUS_TEST_HOME)) {
85
+ debug.log('loop.orchestrator', 'finish-skipped-test', {});
86
+ return null;
87
+ }
88
+ const path = join(root, 'harness', 'finish-metrics.json');
89
+ try {
90
+ if (existsSync(path)) {
91
+ const cached = JSON.parse(readFileSync(path, 'utf8')) as { at?: unknown; metrics?: unknown };
92
+ const at = typeof cached.at === 'string' ? Date.parse(cached.at) : NaN;
93
+ if (Number.isFinite(at) && now.getTime() - at >= 0 && now.getTime() - at < FINISH_CACHE_MS
94
+ && validFinishMetrics(cached.metrics)) return cached.metrics;
95
+ }
96
+ } catch { /* A broken cache is re-measured. */ }
97
+ const measure = deps.finishMetrics ?? (() => measureFinish());
98
+ const metrics = await measure(now);
99
+ try {
100
+ mkdirSync(join(root, 'harness'), { recursive: true });
101
+ writeFileSync(path, JSON.stringify({ at: now.toISOString(), metrics }));
102
+ } catch (error) { debug.log('loop.orchestrator', 'finish-cache-write-failed', { reason: String(error) }); }
103
+ return metrics;
104
+ }
105
+
106
+ /** FINISH-RATE: a backlogged board holds this launch when the pool already fills the launch share. */
107
+ async function finishGateReason(root: string, now: Date, mode: 'off' | 'shadow' | 'on', running: number, totalSlots: number,
108
+ deps: HarnessQueueDeps): Promise<string | null> {
109
+ if (mode === 'off') return null;
110
+ let metrics: FinishMetrics | null;
111
+ try { metrics = await cachedFinishMetrics(root, now, deps); }
112
+ catch (error) {
113
+ debug.log('loop.orchestrator', 'finish-measure-failed', { reason: String(error) });
114
+ return null;
115
+ }
116
+ if (!metrics) return null;
117
+ const advice = finishAdvice(metrics, totalSlots);
118
+ if (advice.state !== 'backlogged' || running < advice.launchSlots) return null;
119
+ debug.log('loop.orchestrator', 'finish-rebalanced', {
120
+ finishSlots: advice.finishSlots, launchSlots: advice.launchSlots, running, reasons: advice.reasons, mode,
121
+ });
122
+ if (mode === 'shadow') return null;
123
+ return `마무리 우선 — finish=${advice.finishSlots}/${totalSlots} · 사유 ${advice.reasons.join(', ')}`;
32
124
  }
33
125
 
34
126
  export function harnessQueuePath(root = effectiveInstanceRoot()): string {
@@ -49,7 +141,9 @@ function read(path: string): QueueItem[] {
49
141
  && typeof row.at === 'string' && typeof row.hold === 'boolean' && typeof row.heavy === 'boolean'
50
142
  && ['queued', 'launching', 'launched', 'finished'].includes(row.status)
51
143
  && (row.pid === undefined || Number.isSafeInteger(row.pid))
52
- && (row.launchId === undefined || typeof row.launchId === 'string'))) {
144
+ && (row.launchId === undefined || typeof row.launchId === 'string')
145
+ && (row.idempotencyKey === undefined || typeof row.idempotencyKey === 'string')
146
+ && (row.waitingReason === undefined || typeof row.waitingReason === 'string'))) {
53
147
  throw new Error('harness queue: invalid queue file (no launch)');
54
148
  }
55
149
  return value as QueueItem[];
@@ -66,6 +160,29 @@ function save(path: string, items: QueueItem[]): void {
66
160
  }
67
161
  }
68
162
 
163
+ function lastLaunchedSeat(path: string): QueueSeat | undefined {
164
+ const marker = `${path}.round-robin.json`;
165
+ if (!existsSync(marker)) return undefined;
166
+ const value: unknown = JSON.parse(readFileSync(marker, 'utf8'));
167
+ if (!value || typeof value !== 'object' || Array.isArray(value)
168
+ || !queueSeatNames.some((name) => name === (value as { lastSeat?: unknown }).lastSeat)) {
169
+ throw new Error('harness queue: invalid round-robin state (no launch)');
170
+ }
171
+ return (value as { lastSeat: QueueSeat }).lastSeat;
172
+ }
173
+
174
+ function saveLastLaunchedSeat(path: string, lastSeat: QueueSeat): void {
175
+ const marker = `${path}.round-robin.json`;
176
+ const temporary = `${marker}.${randomUUID()}.tmp`;
177
+ try {
178
+ writeFileSync(temporary, JSON.stringify({ lastSeat }), { flag: 'wx', mode: 0o600 });
179
+ renameSync(temporary, marker);
180
+ } catch (error) {
181
+ try { unlinkSync(temporary); } catch { /* write did not finish */ }
182
+ throw error;
183
+ }
184
+ }
185
+
69
186
  // SQLite OS locks release on process death, including death while awaiting a launch.
70
187
  async function locked<T>(root: string, action: (path: string) => Promise<T>): Promise<T> {
71
188
  const path = harnessQueuePath(root);
@@ -90,7 +207,7 @@ function seat(value: string): QueueSeat {
90
207
  return value as QueueSeat;
91
208
  }
92
209
 
93
- export async function addHarnessQueue(input: { seat: string; say?: string; ask?: string; hold?: boolean; heavy?: boolean }, deps: HarnessQueueDeps = {}): Promise<QueueItem> {
210
+ export async function addHarnessQueue(input: { seat: string; say?: string; ask?: string; hold?: boolean; heavy?: boolean; idempotencyKey?: string }, deps: HarnessQueueDeps = {}): Promise<QueueItem> {
94
211
  const assigned = seat(input.seat);
95
212
  if ((input.say === undefined) === (input.ask === undefined)) throw new Error('harness queue add: --say 또는 --ask 중 하나만 필요');
96
213
  const kind = input.ask === undefined ? 'say' : 'ask';
@@ -100,9 +217,21 @@ export async function addHarnessQueue(input: { seat: string; say?: string; ask?:
100
217
  if (kind === 'ask' && !existsSync(value)) throw new Error(`harness queue add: goal file not found: ${value}`);
101
218
  const root = deps.root ?? effectiveInstanceRoot();
102
219
  return locked(root, async (path) => {
220
+ const rows = read(path);
221
+ if (input.idempotencyKey !== undefined) {
222
+ if (!input.idempotencyKey.trim()) throw new Error('harness queue add: empty idempotency key');
223
+ const existing = rows.find((row) => row.idempotencyKey === input.idempotencyKey
224
+ && (row.status === 'queued' || row.status === 'launching' || row.status === 'launched')
225
+ && queueRowOutcome(row, root) !== 'retryable');
226
+ if (existing) {
227
+ if (existing.seat !== assigned || existing.kind !== kind || existing.input !== value) throw new Error('harness queue: idempotency key collision');
228
+ return existing;
229
+ }
230
+ }
103
231
  const item: QueueItem = { id: `hq-${randomUUID()}`, seat: assigned, kind, input: value,
104
- hold: input.hold === true, heavy: input.heavy === true, at: new Date().toISOString(), status: 'queued' };
105
- save(path, [...read(path), item]);
232
+ hold: input.hold === true, heavy: input.heavy === true, at: new Date().toISOString(), status: 'queued',
233
+ ...(input.idempotencyKey === undefined ? {} : { idempotencyKey: input.idempotencyKey }) };
234
+ save(path, [...rows, item]);
106
235
  observe('enqueued', { id: item.id, seat: item.seat, kind }, deps);
107
236
  return item;
108
237
  });
@@ -112,6 +241,60 @@ export function listHarnessQueue(deps: HarnessQueueDeps = {}): QueueItem[] {
112
241
  return read(harnessQueuePath(deps.root ?? effectiveInstanceRoot()));
113
242
  }
114
243
 
244
+ /** A missing row is not proof of failure unless the queue left a cancellation marker. */
245
+ export function harnessQueueOutcome(id: string, deps: HarnessQueueDeps = {}): 'pending' | 'succeeded' | 'retryable' | 'unknown' {
246
+ const root = deps.root ?? effectiveInstanceRoot();
247
+ const row = listHarnessQueue(deps).find((item) => item.id === id);
248
+ if (!row) {
249
+ const marker = join(root, 'harness', `${id}.outcome`);
250
+ if (!existsSync(marker)) return 'unknown';
251
+ const value = readFileSync(marker, 'utf8');
252
+ return value === 'succeeded' || value === 'retryable' ? value : 'unknown';
253
+ }
254
+ return queueRowOutcome(row, root);
255
+ }
256
+
257
+ const keyMarkerPath = (root: string, key: string) => join(root, 'harness', 'by-key', `${createHash('sha256').update(key).digest('hex')}.id`);
258
+
259
+ function recordQueueOutcome(root: string, id: string, outcome: 'succeeded' | 'retryable', key?: string): void {
260
+ const path = join(root, 'harness', `${id}.outcome`);
261
+ if (!existsSync(path)) writeFileSync(path, outcome, { flag: 'wx', mode: 0o600 });
262
+ // The row is about to leave the queue; keep «which id last held this key» so a caller that crashed before
263
+ // recording the id can still find the outcome (LOOP-LIVE1 · seat loop recovery).
264
+ if (key) {
265
+ mkdirSync(join(root, 'harness', 'by-key'), { recursive: true });
266
+ writeFileSync(keyMarkerPath(root, key), id, { mode: 0o600 });
267
+ }
268
+ }
269
+
270
+ /** Last queue id that settled under this idempotency key, or undefined when none was recorded. */
271
+ export function harnessQueueIdForKey(key: string, deps: HarnessQueueDeps = {}): string | undefined {
272
+ try {
273
+ const id = readFileSync(keyMarkerPath(deps.root ?? effectiveInstanceRoot(), key), 'utf8').trim();
274
+ return /^hq-[0-9a-f-]{36}$/i.test(id) ? id : undefined;
275
+ } catch { return undefined; }
276
+ }
277
+
278
+ function queueRowOutcome(row: QueueItem, root: string): 'pending' | 'succeeded' | 'retryable' | 'unknown' {
279
+ if (row.status === 'queued') return 'pending';
280
+ if (row.status === 'finished') {
281
+ const marker = join(root, 'harness', `${row.id}.outcome`);
282
+ if (existsSync(marker)) {
283
+ const value = readFileSync(marker, 'utf8');
284
+ if (value === 'succeeded' || value === 'retryable') return value;
285
+ }
286
+ }
287
+ if (!row.launchId) return 'unknown';
288
+ const receiptPath = harnessQueueReceiptPath(root, row.launchId);
289
+ if (!existsSync(receiptPath)) return 'pending';
290
+ const record: unknown = JSON.parse(readFileSync(receiptPath, 'utf8'));
291
+ if (!record || typeof record !== 'object') return 'unknown';
292
+ const { state, exitCode } = record as { state?: unknown; exitCode?: unknown };
293
+ if (state === 'not-started') return 'retryable';
294
+ if (state === 'finished') return exitCode === 0 ? 'succeeded' : typeof exitCode === 'number' ? 'retryable' : 'unknown';
295
+ return state === 'started' ? 'pending' : 'unknown';
296
+ }
297
+
115
298
  export async function removeHarnessQueue(id: string, deps: HarnessQueueDeps = {}): Promise<boolean> {
116
299
  const root = deps.root ?? effectiveInstanceRoot();
117
300
  return locked(root, async (path) => {
@@ -124,6 +307,10 @@ export async function removeHarnessQueue(id: string, deps: HarnessQueueDeps = {}
124
307
  if (state !== 'finished' && state !== 'not-started'
125
308
  || (deps.processes ?? readHarnessQueueProcesses)().some((row) => row.launchId === item.launchId)) return false;
126
309
  }
310
+ const outcome = item.status === 'queued' ? 'retryable' : queueRowOutcome(item, root);
311
+ if (item.idempotencyKey && (outcome === 'retryable' || outcome === 'succeeded')) {
312
+ recordQueueOutcome(root, id, outcome, item.idempotencyKey);
313
+ }
127
314
  save(path, rows.filter((row) => row.id !== id));
128
315
  observe('skipped', { id, seat: item.seat, reason: 'removed' }, deps);
129
316
  return true;
@@ -159,31 +346,109 @@ function alive(pid: number): boolean {
159
346
  }
160
347
 
161
348
  /** Host inventory includes harness authoring before it has a run ledger. Unattributed processes count for every seat. */
162
- export function readHarnessQueueProcesses(): readonly QueueProcess[] {
163
- const result = spawnSync('ps', ['-eo', 'pid=,args='], { encoding: 'utf8', timeout: 5_000, maxBuffer: 16 * 1024 * 1024 });
349
+ export function readHarnessQueueProcesses(probe: ProcessProbe = {}): readonly QueueProcess[] {
350
+ const run = probe.run ?? spawnSync;
351
+ const platform = probe.platform ?? process.platform;
352
+ const result = run('ps', ['-eo', 'pid=,ppid=,args='], { encoding: 'utf8', timeout: 5_000, maxBuffer: 16 * 1024 * 1024 });
164
353
  if (result.error || result.status !== 0 || typeof result.stdout !== 'string') throw new Error('harness process inventory unavailable');
165
- const rows: QueueProcess[] = [];
354
+ const candidates: { pid: number; ppid: number; command: string }[] = [];
166
355
  for (const line of result.stdout.split('\n')) {
167
- const match = /^\s*(\d+)\s+(.+)$/.exec(line);
168
- if (!match) continue;
169
- const command = match[2]!;
356
+ const match = /^\s*(\d+)\s+(\d+)\s+(.+)$/.exec(line);
357
+ if (!match) {
358
+ if (/(?:elanous\.mjs|harness-queue-child\.ts)/.test(line)) throw new Error('invalid harness process inventory row');
359
+ continue;
360
+ }
361
+ const command = match[3]!;
170
362
  if (!/^(?:\S*\/)?(?:bun|node)(?:\s|$)/.test(command)
171
363
  || !/(?:elanous\.mjs|harness-queue-child\.ts)/.test(command)
172
364
  || !/(?:\bharness\s+(?:say|ask)\b|\bdev\s+.*(?:--say|--ask|--file|--implement)\b|\bself\s+(?:implement|orchestrate)\b|harness-queue-child\.ts)/.test(command)) continue;
173
- const pid = Number(match[1]);
174
- if (!Number.isSafeInteger(pid) || pid <= 0) throw new Error('invalid harness pid');
175
- let envSeat: string | undefined;
176
- let envLaunch: string | undefined;
177
- if (process.platform === 'linux') {
365
+ const pid = Number(match[1]), ppid = Number(match[2]);
366
+ if (!Number.isSafeInteger(pid) || pid <= 0 || !Number.isSafeInteger(ppid) || ppid < 0) throw new Error('invalid harness pid');
367
+ candidates.push({ pid, ppid, command });
368
+ }
369
+ const byPid = new Map(candidates.map((row) => [row.pid, row]));
370
+ const children = new Map<number, typeof candidates>();
371
+ for (const candidate of candidates) {
372
+ const siblings = children.get(candidate.ppid) ?? [];
373
+ siblings.push(candidate);
374
+ children.set(candidate.ppid, siblings);
375
+ }
376
+ const trees = probe.seatTrees ?? getUserConfig().loops?.orchestrator?.seatTrees;
377
+ const ledgerSeats = new Map<number, QueueSeat>();
378
+ const ambiguousPids = new Set<number>();
379
+ const candidatePids = new Set(candidates.map(({ pid }) => pid));
380
+ const births = new Map<number, string | undefined>();
381
+ for (const record of listSelfDevRuns(probe.runsDir ?? selfDevRunsDir())) {
382
+ const pid = record.pid;
383
+ if (!pid || !candidatePids.has(pid) || !record.pidStart || !queueSeatNames.some((name) => name === record.seat)) continue;
384
+ if (!births.has(pid)) {
385
+ try { births.set(pid, (probe.birthId ?? ((id: number) => processBirthId(id, platform)))(pid)); }
386
+ catch { births.set(pid, undefined); }
387
+ }
388
+ if (births.get(pid) !== record.pidStart || ambiguousPids.has(pid)) continue;
389
+ const prior = ledgerSeats.get(pid);
390
+ if (prior && prior !== record.seat) { ledgerSeats.delete(pid); ambiguousPids.add(pid); }
391
+ else ledgerSeats.set(pid, record.seat!);
392
+ }
393
+ const rows: QueueProcess[] = [];
394
+ // Attribute a run after grouping: the orchestrator may switch working trees while its launcher stays in HQ.
395
+ for (const root of candidates.filter((row) => !byPid.has(row.ppid))) {
396
+ const members: { process: typeof root; depth: number }[] = [];
397
+ const visit = (process: typeof root, depth: number): void => {
398
+ members.push({ process, depth });
399
+ for (const child of children.get(process.pid) ?? []) visit(child, depth + 1);
400
+ };
401
+ visit(root, 0);
402
+ const attributed: { seat: QueueSeat; depth: number; source: 'env' | 'flag' | 'ledger' | 'cwd'; priority: number }[] = [];
403
+ const worktrees: { seat: QueueSeat; depth: number }[] = [];
404
+ let launchId: string | undefined;
405
+ let runId: string | undefined;
406
+ for (const { process: { pid, command }, depth } of members) {
407
+ let envSeat: string | undefined;
408
+ if (platform === 'linux') {
409
+ try {
410
+ const env = (probe.environ ?? ((id: number) => readFileSync(`/proc/${id}/environ`, 'utf8')))(pid).split('\0');
411
+ envSeat = env.find((entry) => entry.startsWith('ELANOUS_HARNESS_SEAT='))?.slice('ELANOUS_HARNESS_SEAT='.length);
412
+ launchId ??= env.find((entry) => entry.startsWith('ELANOUS_HARNESS_QUEUE_LAUNCH='))?.slice('ELANOUS_HARNESS_QUEUE_LAUNCH='.length);
413
+ runId ??= env.find((entry) => entry.startsWith('ELANOUS_RUN_ID='))?.slice('ELANOUS_RUN_ID='.length);
414
+ } catch { /* Unknown attribution is charged to all seats. */ }
415
+ }
416
+ const flag = /--seat[=\s]+(OP|TC|MK|UX)(?:\s|$)/.exec(command)?.[1] as QueueSeat | undefined;
417
+ const envAssigned = envSeat && /^(OP|TC|MK|UX)$/.test(envSeat) ? envSeat as QueueSeat : undefined;
418
+ if (envAssigned) attributed.push({ seat: envAssigned, depth, source: 'env', priority: 0 });
419
+ if (flag) attributed.push({ seat: flag, depth, source: 'flag', priority: 1 });
420
+ const fromLedger = ledgerSeats.get(pid);
421
+ if (fromLedger) attributed.push({ seat: fromLedger, depth, source: 'ledger', priority: 2 });
422
+ if (!trees || !queueSeatNames.some((s) => trees[s]?.length)) continue;
178
423
  try {
179
- const env = readFileSync(`/proc/${pid}/environ`, 'utf8').split('\0');
180
- envSeat = env.find((entry) => entry.startsWith('ELANOUS_HARNESS_SEAT='))?.slice('ELANOUS_HARNESS_SEAT='.length);
181
- envLaunch = env.find((entry) => entry.startsWith('ELANOUS_HARNESS_QUEUE_LAUNCH='))?.slice('ELANOUS_HARNESS_QUEUE_LAUNCH='.length);
424
+ let cwd: string;
425
+ if (probe.cwd) cwd = probe.cwd(pid);
426
+ else if (platform === 'linux') cwd = readlinkSync(`/proc/${pid}/cwd`);
427
+ else {
428
+ const lsof = run('lsof', ['-a', '-p', String(pid), '-d', 'cwd', '-Fn'], { encoding: 'utf8', timeout: 5_000 });
429
+ if (lsof.error || lsof.status !== 0 || typeof lsof.stdout !== 'string') throw new Error('cwd unavailable');
430
+ cwd = lsof.stdout.split('\n').find((line) => line.startsWith('n/'))?.slice(1) ?? '';
431
+ }
432
+ if (!cwd.startsWith('/')) continue;
433
+ const found = queueSeatForCwd(cwd, trees);
434
+ if (found) worktrees.push({ seat: found, depth });
182
435
  } catch { /* Unknown attribution is charged to all seats. */ }
183
436
  }
184
- const assigned = envSeat ?? /--seat[=\s]+(OP|TC|MK|UX)(?:\s|$)/.exec(command)?.[1];
185
- rows.push({ pid, ...(assigned && /^(OP|TC|MK|UX)$/.test(assigned) ? { seat: assigned as QueueSeat } : {}),
186
- ...(envLaunch ? { launchId: envLaunch } : {}) });
437
+ const deepest = Math.max(...members.map((entry) => entry.depth));
438
+ const inferred = new Set(worktrees.filter((entry) => entry.depth === deepest).map((entry) => entry.seat));
439
+ const complete = worktrees.filter((entry) => entry.depth === deepest).length
440
+ === members.filter((entry) => entry.depth === deepest).length;
441
+ if (complete && inferred.size === 1) attributed.push({ seat: [...inferred][0]!, depth: deepest, source: 'cwd', priority: 3 });
442
+ const priority = Math.min(...attributed.map((entry) => entry.priority));
443
+ const eligible = attributed.filter((entry) => entry.priority === priority);
444
+ const depth = Math.max(...eligible.map((entry) => entry.depth));
445
+ const top = eligible.filter((entry) => entry.depth === depth);
446
+ const seats = new Set(top.map((entry) => entry.seat));
447
+ const assigned = seats.size === 1 ? top[0] : undefined;
448
+ try { debug.log('harness.queue', 'attributed', { pid: root.pid, seat: assigned?.seat ?? null, source: assigned?.source ?? 'all' }); }
449
+ catch { /* Observation does not block inventory. */ }
450
+ rows.push({ pid: root.pid, ...(assigned ? { seat: assigned.seat } : {}), ...(launchId ? { launchId } : {}),
451
+ ...(runId ? { runId } : {}) });
187
452
  }
188
453
  return rows;
189
454
  }
@@ -242,6 +507,8 @@ export async function reconcileHarnessQueue(id: string, deps: HarnessQueueDeps =
242
507
  if (state !== 'finished' && state !== 'not-started') return 'unknown';
243
508
  // A finished receipt proves the child exited; a reused wrapper PID alone is not its identity.
244
509
  // Keep completed launches in the ledger, but never charge their seat again.
510
+ const outcome = state === 'not-started' ? 'retryable' : queueRowOutcome(item, root);
511
+ if (item.idempotencyKey && (outcome === 'succeeded' || outcome === 'retryable')) recordQueueOutcome(root, id, outcome, item.idempotencyKey);
245
512
  save(path, state === 'finished' && item.status === 'launched'
246
513
  ? rows.map((row) => row.id === id ? { ...row, status: 'finished' } : row)
247
514
  : rows.filter((row) => row.id !== id));
@@ -250,57 +517,308 @@ export async function reconcileHarnessQueue(id: string, deps: HarnessQueueDeps =
250
517
  });
251
518
  }
252
519
 
520
+ const warnedLegacySeatCapPaths = new Set<string>();
521
+
522
+ function warnLegacySeatCap(path: string): void {
523
+ if (warnedLegacySeatCapPaths.has(path)) return;
524
+ try {
525
+ const raw: unknown = JSON.parse(readFileSync(path, 'utf8'));
526
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return;
527
+ const harness = (raw as Record<string, unknown>).harness;
528
+ if (!harness || typeof harness !== 'object' || Array.isArray(harness)) return;
529
+ const queue = (harness as Record<string, unknown>).queue;
530
+ if (!queue || typeof queue !== 'object' || Array.isArray(queue)
531
+ || !Object.hasOwn(queue, 'seatCap')) return;
532
+ warnedLegacySeatCapPaths.add(path);
533
+ debug.log('harness.queue', 'legacy-seat-cap-ignored', {
534
+ key: 'harness.queue.seatCap', replacement: 'loops.orchestrator.seatCaps', path,
535
+ }, { level: 'warn' });
536
+ } catch { /* An unreadable config cannot affect dispatch or observation. */ }
537
+ }
538
+
539
+ function groupQueueRuns(processes: readonly QueueProcess[]): { seat?: QueueSeat; launchIds: Set<string> }[] {
540
+ const groups: { seats: Set<QueueSeat>; unknown: boolean; launchIds: Set<string>; keys: Set<string> }[] = [];
541
+ for (const row of processes) {
542
+ const keys = [row.runId && `run:${row.runId}`, `root:${row.rootPid ?? row.pid}`,
543
+ row.launchId && `launch:${row.launchId}`, `pid:${row.pid}`].filter((key): key is string => Boolean(key));
544
+ const matching = groups.filter((group) => keys.some((key) => group.keys.has(key)));
545
+ const group = matching[0] ?? { seats: new Set<QueueSeat>(), unknown: false,
546
+ launchIds: new Set<string>(), keys: new Set<string>() };
547
+ for (const other of matching.slice(1)) {
548
+ for (const key of other.keys) group.keys.add(key);
549
+ for (const seat of other.seats) group.seats.add(seat);
550
+ for (const id of other.launchIds) group.launchIds.add(id);
551
+ group.unknown ||= other.unknown;
552
+ groups.splice(groups.indexOf(other), 1);
553
+ }
554
+ for (const key of keys) group.keys.add(key);
555
+ if (row.seat) group.seats.add(row.seat);
556
+ else group.unknown = true;
557
+ if (row.launchId) group.launchIds.add(row.launchId);
558
+ if (!matching.length) groups.push(group);
559
+ }
560
+ return groups.map((group) => ({
561
+ ...(group.unknown || group.seats.size !== 1 ? {} : { seat: [...group.seats][0] }),
562
+ launchIds: group.launchIds,
563
+ }));
564
+ }
565
+
566
+ const IDLE_REQUEST_INTERVAL_MS = 15 * 60_000;
567
+ const IDLE_REQUEST_MINUTES = 30;
568
+ type IdleRequestState = { checkedAt: string; idleSince: Partial<Record<QueueSeat, string>>; notified?: string[] };
569
+
570
+ function idleRequestCells(now: Date, root: string): ReturnType<NonNullable<HarnessQueueDeps['idleCells']>> {
571
+ const ledger = root === effectiveInstanceRoot() ? releaseLedgerRoot() : root;
572
+ if (!existsSync(join(ledger, 'release', 'features.sqlite'))) throw new Error('release checklist ledger unavailable');
573
+ const upcoming = listSchedules(ledger).filter(row => Date.parse(row.cutAt) > now.getTime())
574
+ .sort((a, b) => Date.parse(a.cutAt) - Date.parse(b.cutAt));
575
+ if (!upcoming[0]) throw new Error('current release schedule unavailable');
576
+ return { current: listChecklist(upcoming[0].version, ledger).items,
577
+ next: upcoming[1] ? listChecklist(upcoming[1].version, ledger).items : [] };
578
+ }
579
+
580
+ function hasRunEvidence(cell: Pick<ChecklistItem, 'evidence'>): boolean {
581
+ return /(?:\brun[-_][a-z0-9-]+\b|\b(?:런|run)(?:\s*(?:근거|id|ID))?\s*[::#]\s*\S+)/i.test(cell.evidence ?? '');
582
+ }
583
+
584
+ /** The queue lock serializes the observation marker; request journal lock also covers external traffic writers. */
585
+ export function requestIdleSeats(root: string, now: Date, items: readonly QueueItem[], deps: HarnessQueueDeps = {}): void {
586
+ const config: OrchestratorLoopConfig | undefined = (deps.configPath ? getUserConfig(deps.configPath) : getUserConfig()).loops?.orchestrator;
587
+ const mode = config?.idleRequest ?? 'shadow';
588
+ if (mode === 'off') return;
589
+ if (!Number.isFinite(now.getTime())) throw new Error('invalid idle request clock');
590
+ const marker = join(root, 'orchestrator', 'idle-request.json');
591
+ const previous: IdleRequestState = existsSync(marker) ? JSON.parse(readFileSync(marker, 'utf8')) as IdleRequestState
592
+ : { checkedAt: new Date(now.getTime() - IDLE_REQUEST_INTERVAL_MS).toISOString(), idleSince: {} };
593
+ if (!previous || typeof previous !== 'object' || !previous.idleSince || typeof previous.idleSince !== 'object'
594
+ || !Number.isFinite(Date.parse(previous.checkedAt)) || (previous.notified !== undefined
595
+ && (!Array.isArray(previous.notified) || previous.notified.some(key => typeof key !== 'string')))
596
+ || Object.entries(previous.idleSince).some(([seat, value]) =>
597
+ !TRAFFIC_SEATS.some(name => name === seat) || typeof value !== 'string' || !Number.isFinite(Date.parse(value)))) {
598
+ throw new Error('invalid idle request marker');
599
+ }
600
+ const processes = (deps.processes ?? readHarnessQueueProcesses)();
601
+ const groups = groupQueueRuns(processes);
602
+ const reservations = items.filter(item => item.status === 'launching' || item.status === 'launched').filter(item => {
603
+ if (item.launchId && groups.some(group => group.launchIds.has(item.launchId!))) return false;
604
+ const state = item.launchId ? (deps.receipt ?? receipt)(root, item.launchId) : null;
605
+ return state !== 'finished' && state !== 'not-started';
606
+ });
607
+ const counts = Object.fromEntries(TRAFFIC_SEATS.map(seat => [seat,
608
+ groups.filter(group => !group.seat || group.seat === seat).length
609
+ + reservations.filter(item => item.seat === seat).length])) as Record<QueueSeat, number>;
610
+ const idleSince = { ...previous.idleSince };
611
+ for (const seat of TRAFFIC_SEATS) {
612
+ if (counts[seat] > 0) delete idleSince[seat];
613
+ else if (!idleSince[seat]) idleSince[seat] = now.toISOString();
614
+ }
615
+ const due = now.getTime() - Date.parse(previous.checkedAt) >= IDLE_REQUEST_INTERVAL_MS;
616
+ if (!due && Object.keys(idleSince).length === Object.keys(previous.idleSince).length) return;
617
+ const day = new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Seoul', year: 'numeric', month: '2-digit', day: '2-digit' }).format(now);
618
+ const state: IdleRequestState = { checkedAt: due ? now.toISOString() : previous.checkedAt, idleSince,
619
+ notified: (previous.notified ?? []).filter(key => key.startsWith('orch-idle:') && key.split(':')[2] === day) };
620
+ mkdirSync(join(root, 'orchestrator'), { recursive: true });
621
+ const temporary = `${marker}.${randomUUID()}.tmp`;
622
+ try {
623
+ writeFileSync(temporary, JSON.stringify(state), { flag: 'wx', mode: 0o600 });
624
+ renameSync(temporary, marker);
625
+ } catch (error) {
626
+ try { unlinkSync(temporary); } catch { /* no temporary file */ }
627
+ throw error;
628
+ }
629
+ if (!due) return;
630
+ const cells = (deps.idleCells ?? ((clock: Date) => idleRequestCells(clock, root)))(now);
631
+ const eligible = (rows: typeof cells.current, seat: QueueSeat) => rows.filter(cell =>
632
+ cell.owner?.split('/')[0] === seat && cell.status === 'yellow' && !hasRunEvidence(cell));
633
+ const candidates: TrafficCell[] = TRAFFIC_SEATS.flatMap(seat => {
634
+ const current = eligible(cells.current, seat);
635
+ return (current.length ? current : eligible(cells.next, seat)).slice(0, 1);
636
+ });
637
+ const measured = trafficTick({ now, processes: TRAFFIC_SEATS.flatMap(seat => Array.from({ length: counts[seat] }, () =>
638
+ ({ seat, command: 'bun bin/elanous.mjs harness say observed', elapsedSeconds: 0 }))),
639
+ caps: config?.seatCaps ?? { OP: 4, TC: 8, MK: 6, UX: 6 }, idleSince: Object.fromEntries(TRAFFIC_SEATS.map(seat =>
640
+ [seat, idleSince[seat] ? new Date(idleSince[seat]) : null])), openCells: candidates, idleMinutes: IDLE_REQUEST_MINUTES });
641
+ const log = deps.idleLog ?? ((category: string, event: string, data: Record<string, unknown>) => debug.log(category, event, data));
642
+ for (const row of measured.seats) {
643
+ if (!row.idle || row.running !== 0) continue;
644
+ const cell = row.nextCell ? `${row.nextCell.id} ${row.nextCell.title.replace(/\s+/g, ' ').trim()}` : '칸 없음';
645
+ let outcome: 'shadow' | 'queued' | 'duplicate' | 'no-cell' = mode === 'live' ? 'no-cell' : 'shadow';
646
+ const key = `orch-idle:${row.seat}:${day}:${row.nextCell?.id ?? 'no-cell'}`;
647
+ if (state.notified!.includes(key)) continue;
648
+ if (mode === 'live' && row.nextCell) {
649
+ const path = join(root, 'seat-requests', 'requests.jsonl');
650
+ mkdirSync(join(root, 'seat-requests'), { recursive: true });
651
+ outcome = withFileLockSync(`${path}.lock`, () => {
652
+ if (existsSync(path) && readFileSync(path, 'utf8').split('\n').some(line => {
653
+ if (!line) return false;
654
+ try { return (JSON.parse(line) as { key?: string }).key === key; }
655
+ catch { throw new Error('invalid seat request journal'); }
656
+ })) return 'duplicate';
657
+ appendFileSync(path, JSON.stringify({ key, receiptId: key, seat: row.seat,
658
+ text: `다음 칸: ${cell} — 30분 놀았다`, status: 'queued', queuedAt: now.toISOString(), source: 'orchestrator-idle' }) + '\n');
659
+ return 'queued';
660
+ });
661
+ }
662
+ if (outcome !== 'duplicate') {
663
+ state.notified!.push(key);
664
+ try { log('loop.orchestrator', 'idle-requested', {
665
+ seat: row.seat, idleMinutes: IDLE_REQUEST_MINUTES, cell, mode, outcome }); }
666
+ catch { /* Observation cannot change the request or queue decision. */ }
667
+ }
668
+ }
669
+ try {
670
+ writeFileSync(temporary, JSON.stringify(state), { flag: 'wx', mode: 0o600 });
671
+ renameSync(temporary, marker);
672
+ } catch (error) {
673
+ try { unlinkSync(temporary); } catch { /* no temporary file */ }
674
+ throw error;
675
+ }
676
+ }
677
+
253
678
  export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<QueueTick> {
254
679
  const root = deps.root ?? effectiveInstanceRoot();
255
680
  return locked(root, async (path) => {
681
+ try {
682
+ const now = (deps.now ?? (() => new Date()))();
683
+ // A test process must not run the real author shadow — it reads operational ledgers and blew the 5s test budget (10-05 P0).
684
+ if (!deps.authorShadow && process.env.ELANOUS_AUTHOR_SHADOW_LIVE !== '1' && (process.env.NODE_ENV === 'test' || process.env.ELANOUS_TEST_HOME)) {
685
+ debug.log('loops.author-depth', 'shadow-skipped-test', {});
686
+ } else {
687
+ const shadow = deps.authorShadow ?? (await import('../loops/orchestrator/author-depth.js')).runAuthorDepthShadow;
688
+ await shadow(root, now);
689
+ }
690
+ } catch (error) {
691
+ try { debug.log('loops.author-depth', 'shadow-failed', { reason: String(error) }); }
692
+ catch { /* Shadow observation cannot affect queue dispatch. */ }
693
+ }
256
694
  const items = read(path);
257
- const item = items.find((row) => row.status === 'queued');
258
- if (!item) { observe('skipped', { reason: 'empty' }, deps); return { outcome: 'skipped', reason: 'empty' }; }
259
- const cap = (deps.cap ?? ((s: QueueSeat) => getUserConfig().harness?.queue?.seatCap?.[s] ?? 8))(item.seat);
260
- if (!Number.isSafeInteger(cap) || cap < 1) throw new Error(`harness queue: invalid seat cap for ${item.seat}`);
261
- const wait = (reason: string, active: number): QueueTick => {
262
- observe('waiting', { id: item.id, seat: item.seat, reason, active, cap }, deps);
263
- return { outcome: 'waiting', item, reason };
695
+ try { await (deps.idleRequest ?? requestIdleSeats)(root, (deps.now ?? (() => new Date()))(), items, deps); }
696
+ catch (error) {
697
+ try { debug.log('loop.orchestrator', 'idle-request-failed', { reason: String(error) }); }
698
+ catch { /* Idle observation cannot affect queue dispatch. */ }
699
+ }
700
+ const heads = new Map<QueueSeat, QueueItem>();
701
+ for (const row of items) if (row.status === 'queued' && !heads.has(row.seat)) heads.set(row.seat, row);
702
+ if (!heads.size) { observe('skipped', { reason: 'empty' }, deps); return { outcome: 'skipped', reason: 'empty' }; }
703
+ const lastSeat = lastLaunchedSeat(path);
704
+ const first = lastSeat ? (queueSeatNames.indexOf(lastSeat) + 1) % queueSeatNames.length
705
+ : queueSeatNames.indexOf(items.find((row) => row.status === 'queued')!.seat);
706
+ const candidates = [...queueSeatNames.slice(first), ...queueSeatNames.slice(0, first)]
707
+ .flatMap((name) => heads.get(name) ? [heads.get(name)!] : []);
708
+ const config = deps.configPath === undefined ? getUserConfig() : getUserConfig(deps.configPath);
709
+ warnLegacySeatCap(deps.configPath ?? userConfigPath());
710
+ const caps = config.loops?.orchestrator?.seatCaps;
711
+ const gate = config.loops?.orchestrator?.releaseGate;
712
+ const attributed: Record<QueueSeat, number> = { OP: 0, TC: 0, MK: 0, UX: 0 };
713
+ let unattributed = 0;
714
+ const blocked = new Map<QueueSeat, { reason: string; active: number }>();
715
+ const waiting = (): QueueTick => {
716
+ const reason = candidates.map((item) => blocked.get(item.seat)!.reason).join('\n');
717
+ const updated = current.map((row) => {
718
+ const block = blocked.get(row.seat);
719
+ return heads.get(row.seat)?.id === row.id && block && row.waitingReason !== block.reason
720
+ ? { ...row, waitingReason: block.reason } : row;
721
+ });
722
+ if (updated.some((row, index) => row !== current[index])) save(path, updated);
723
+ for (const item of candidates) {
724
+ const { reason: seatReason, active } = blocked.get(item.seat)!;
725
+ const injectedCap = deps.cap?.(item.seat);
726
+ const details = seatCapDetails({ seat: item.seat, caps, gate, injectedCap });
727
+ observe('waiting', { id: item.id, seat: item.seat, reason: seatReason, active, cap: details.cap,
728
+ seatCaps: details.seatCaps, releaseGate: details.releaseGate,
729
+ ...(details.injectedCap === undefined ? {} : { injectedCap: details.injectedCap }), capKeys: details.winners,
730
+ attributed, unattributed }, deps);
731
+ }
732
+ return { outcome: 'waiting', item: { ...candidates[0]!, waitingReason: blocked.get(candidates[0]!.seat)!.reason }, reason };
264
733
  };
734
+ for (const item of candidates) {
735
+ const injectedCap = deps.cap?.(item.seat);
736
+ if (injectedCap !== undefined && (!Number.isSafeInteger(injectedCap) || injectedCap < 0)) throw new Error(`harness queue: invalid seat cap for ${item.seat}`);
737
+ }
738
+ let current = items;
265
739
  let processes: readonly QueueProcess[];
266
740
  try { processes = (deps.processes ?? readHarnessQueueProcesses)(); }
267
- catch (error) { return wait(`harness process inventory unavailable: ${String(error)}`, 0); }
268
- const current = items.flatMap((row) => {
741
+ catch (error) {
742
+ for (const item of candidates) {
743
+ const injectedCap = deps.cap?.(item.seat);
744
+ const budget = decideSpawn({ seat: item.seat, running: null, caps, gate, injectedCap });
745
+ blocked.set(item.seat, { reason: `${budget.reason}: harness process inventory unavailable: ${String(error)}`, active: 0 });
746
+ }
747
+ return waiting();
748
+ }
749
+ const runs = groupQueueRuns(processes);
750
+ for (const run of runs) {
751
+ if (run.seat) attributed[run.seat]++;
752
+ else unattributed++;
753
+ }
754
+ current = items.flatMap((row) => {
269
755
  if ((row.status !== 'launched' && row.status !== 'launching') || !row.launchId) return [row];
270
756
  const state = (deps.receipt ?? receipt)(root, row.launchId);
271
757
  if (state !== 'finished' && state !== 'not-started'
272
758
  || processes.some((process) => process.launchId === row.launchId)) return [row];
273
- return state === 'finished' && row.status === 'launched' ? [{ ...row, status: 'finished' as const }] : [];
759
+ if (state === 'finished' && row.status === 'launched') return [{ ...row, status: 'finished' as const }];
760
+ const outcome = state === 'not-started' ? 'retryable' : queueRowOutcome(row, root);
761
+ if (row.idempotencyKey && (outcome === 'succeeded' || outcome === 'retryable')) recordQueueOutcome(root, row.id, outcome, row.idempotencyKey);
762
+ return [];
274
763
  });
275
764
  if (current.length !== items.length || current.some((row, index) => row !== items[index])) save(path, current);
276
- const charged = processes.filter((row) => !row.seat || row.seat === item.seat);
277
- const launchIds = new Set(charged.map((row) => row.launchId));
278
- const active = new Set(charged.map((row) => row.launchId ?? `pid:${row.pid}`)).size + current.filter((row) => row.seat === item.seat &&
279
- (row.status === 'launching' || row.status === 'launched')
280
- && (!row.launchId || !launchIds.has(row.launchId))).length;
281
- if (active >= cap) return wait(`seat ${item.seat}: ${active}/${cap}`, active);
282
- let pool: QueuePool;
283
- try { pool = (deps.pool ?? readHarnessQueuePool)(); }
284
- catch (error) { return wait(`pod lease status unavailable: ${String(error)}`, active); }
285
- if (![pool.running, pool.pending, pool.reserved, pool.limit].every((n) => Number.isSafeInteger(n) && n >= 0) || pool.limit < 1) {
286
- return wait('pod lease status incomplete', active);
287
- }
288
- const queueLaunching = current.filter((row) => row.status === 'launching' || row.status === 'launched').length;
289
- if (pool.running + pool.pending + pool.reserved + queueLaunching >= pool.limit) {
290
- return wait(`pool: ${pool.running}+${pool.pending}+${pool.reserved}+${queueLaunching}/${pool.limit}`, active);
291
- }
292
- const launching: QueueItem = { ...item, status: 'launching', launchId: `hq-${randomUUID()}` };
293
- save(path, current.map((row) => row.id === item.id ? launching : row));
294
- try {
295
- const pid = await (deps.launch ?? launch)(launching, queueLaunchArgs(launching), root);
296
- if (!Number.isSafeInteger(pid) || pid < 1) throw new Error('launcher returned no pid');
297
- const launched: QueueItem = { ...launching, pid, status: 'launched' };
298
- save(path, current.map((row) => row.id === item.id ? launched : row));
299
- observe('launched', { id: item.id, seat: item.seat, pid, kind: item.kind, launchId: launched.launchId }, deps);
300
- return { outcome: 'launched', item: launched, reason: 'spawned' };
301
- } catch (error) {
302
- observe('skipped', { id: item.id, seat: item.seat, reason: `launch uncertain: ${String(error)}`, launchId: launching.launchId }, deps);
303
- throw error;
765
+ let pool: QueuePool | undefined;
766
+ let poolReason: string | undefined;
767
+ let finishHeld: string | null | undefined;
768
+ for (const item of candidates) {
769
+ const injectedCap = deps.cap?.(item.seat);
770
+ const details = seatCapDetails({ seat: item.seat, caps, gate, injectedCap });
771
+ const charged = runs.filter((run) => !run.seat || run.seat === item.seat);
772
+ const launchIds = new Set(charged.flatMap((run) => [...run.launchIds]));
773
+ const active = charged.length + current.filter((row) => row.seat === item.seat &&
774
+ (row.status === 'launching' || row.status === 'launched')
775
+ && (!row.launchId || !launchIds.has(row.launchId))).length;
776
+ if (active >= details.cap) { blocked.set(item.seat, { reason: seatCapReason(item.seat, active, details), active }); continue; }
777
+ if (!pool && !poolReason) {
778
+ try { pool = (deps.pool ?? readHarnessQueuePool)(); }
779
+ catch (error) { poolReason = `pod lease status unavailable: ${String(error)}`; }
780
+ if (pool && (![pool.running, pool.pending, pool.reserved, pool.limit].every((n) => Number.isSafeInteger(n) && n >= 0) || pool.limit < 1)) {
781
+ poolReason = 'pod lease status incomplete';
782
+ }
783
+ if (pool && !poolReason) {
784
+ const queueLaunching = current.filter((row) => row.status === 'launching' || row.status === 'launched').length;
785
+ if (pool.running + pool.pending + pool.reserved + queueLaunching >= pool.limit) {
786
+ poolReason = `pool: ${pool.running}+${pool.pending}+${pool.reserved}+${queueLaunching}/${pool.limit}`;
787
+ }
788
+ }
789
+ }
790
+ if (poolReason) { blocked.set(item.seat, { reason: poolReason, active }); continue; }
791
+ const budget = decideSpawn({ seat: item.seat, running: active, caps, gate, injectedCap });
792
+ if (!budget.allow) {
793
+ blocked.set(item.seat, { reason: `${budget.reason}: ${seatCapReason(item.seat, active, details)}`, active });
794
+ continue;
795
+ }
796
+ if (finishHeld === undefined) {
797
+ finishHeld = await finishGateReason(root, (deps.now ?? (() => new Date()))(), config.loops?.orchestrator?.finishGate ?? 'shadow',
798
+ pool!.running + pool!.pending + pool!.reserved
799
+ + current.filter((row) => row.status === 'launching' || row.status === 'launched').length, pool!.limit, deps);
800
+ }
801
+ if (finishHeld) { blocked.set(item.seat, { reason: finishHeld, active }); continue; }
802
+ const launching: QueueItem = { ...item, status: 'launching', launchId: `hq-${randomUUID()}` };
803
+ delete launching.waitingReason;
804
+ save(path, current.map((row) => row.id === item.id ? launching : row));
805
+ try {
806
+ const pid = await (deps.launch ?? launch)(launching, queueLaunchArgs(launching), root);
807
+ if (!Number.isSafeInteger(pid) || pid < 1) throw new Error('launcher returned no pid');
808
+ const launched: QueueItem = { ...launching, pid, status: 'launched' };
809
+ save(path, current.map((row) => row.id === item.id ? launched : row));
810
+ try { saveLastLaunchedSeat(path, item.seat); }
811
+ catch (error) {
812
+ try { debug.log('harness.queue', 'round-robin-save-failed', { id: item.id, seat: item.seat, reason: String(error) }, { level: 'warn' }); }
813
+ catch { /* Observation cannot change an already persisted launch. */ }
814
+ }
815
+ observe('launched', { id: item.id, seat: item.seat, pid, kind: item.kind, launchId: launched.launchId }, deps);
816
+ return { outcome: 'launched', item: launched, reason: 'spawned' };
817
+ } catch (error) {
818
+ observe('skipped', { id: item.id, seat: item.seat, reason: `launch uncertain: ${String(error)}`, launchId: launching.launchId }, deps);
819
+ throw error;
820
+ }
304
821
  }
822
+ return waiting();
305
823
  });
306
824
  }