elanous 0.2.12 → 0.2.14

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 (534) 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/chunks/{7334.a6679d9168f37aa8.js → 1268.0840ff91e083fcbd.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/1307-6e7e0377c1db0bdc.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/{9656-c1540088f5859c80.js → 1844-9ec4b1d12f6e80eb.js} +1 -1
  7. package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/{390.c373e31b3b9d0c41.js → 1948.15f356b15a625597.js} +2 -2
  9. package/apps/pwa/out/_next/static/chunks/211-9c9090c59e2c5907.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/311.e685738f92e6e2ec.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/3194-807b212fb8eae065.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/3324-d1d2979821dfa845.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/3558-bbb5ab553a0471d3.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/4562-071d8bac44a01069.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-2df489622a52a33a.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/{9906-482c6619b3fdda20.js → 8060-cc008cc6fedfa818.js} +1 -1
  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/9070.067c106528ec1ae6.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/9103-7a3ae7f60af3b99f.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/{3869-3cd46526879f454c.js → 9247-29d889b809288e65.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/935.4ddaa99fc1d1126c.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/{79f8be9e-957d68913b590b08.js → a9f2b54d-7865aba0f4d2ee49.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-c1a96669158471ec.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/approvals/page-74383f890f075511.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-7b2b4f378354869a.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/board/page-c496bb16da85385f.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/botlab/page-1abfe3aec3be6bdc.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/bots/page-1512d91c7491ed62.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/chat/page-a627db94ec6904a0.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/consult/page-fb260b9283dbf646.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/control/page-e6b84ab07d3567d5.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/design-check/page-61ec1d2e92bdd50a.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/editor/page-fed69f9759d248d0.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/exec/{page-a67e11ba962337b5.js → page-15374909d8f1aebd.js} +1 -1
  60. package/apps/pwa/out/_next/static/chunks/app/field/page-012a742f81218b01.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/healthz/route-c1a96669158471ec.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/inside/page-fd7e200d4d83fbd7.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/intake/page-99effd9d893a43dc.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/layout-938de57ae0315e46.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/live/page-d47266766766d227.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/loops/page-160089123a40dd91.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/market/page-629e1ede482ec6eb.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-793fa536b3a6ef37.js → page-2ce5471926761fdd.js} +1 -1
  69. package/apps/pwa/out/_next/static/chunks/app/morning/page-27e99b4e54a849d4.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/not-found-e995d30f6800b6bf.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/observatory/page-39f89c53038a51d4.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-32cde713be47cddb.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e7e10cd3386222e5.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-5a753bb4ec0cc511.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/outputs/page-04250f0e2c0d03df.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/page-341480ed8e3c1ea8.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/reflection/page-e64caff084f7ce21.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-db07eaffba0df7e3.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/sessions/page-8ac8d71c2adf9c8a.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e1e47d8d940aba3d.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/settings/page-86f91034bd5cabe5.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-94dc383d3d54db1a.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/setup/layout-c1a96669158471ec.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/app/setup/page-b3759cc14da80908.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/app/share/page-ce4d8a1215ce53a3.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/app/showroom/page-58043db78ba519ae.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/app/tasks/page-357750dfb91bf960.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/app/term/{page-e297866051595d81.js → page-274703bc41b71333.js} +1 -1
  89. package/apps/pwa/out/_next/static/chunks/app/today/page-194dce4a83143fce.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/app/trace/page-35bdeae2e2c17147.js +1 -0
  91. package/apps/pwa/out/_next/static/chunks/app/vault/page-3b7e821dc492bdfd.js +1 -0
  92. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-16bef3da05a900c3.js +1 -0
  93. package/apps/pwa/out/_next/static/chunks/app/workflows/page-f8da5dc6f3752276.js +1 -0
  94. package/apps/pwa/out/_next/static/chunks/app/workspace/page-2f810b18d89ecba6.js +1 -0
  95. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-134c1bc762aee62c.js +1 -0
  96. package/apps/pwa/out/_next/static/chunks/{5cd7e3a9-404fd6b8d0ecd44c.js → f6231284-45726550aba08fca.js} +1 -1
  97. package/apps/pwa/out/_next/static/chunks/{f48170cc-d05d28bf87a43bf4.js → f6b7c9ba.64a895b8b2fedbdc.js} +1 -1
  98. package/apps/pwa/out/_next/static/chunks/framework-5c169757551134ed.js +1 -0
  99. package/apps/pwa/out/_next/static/chunks/main-59fe95fed40c7a01.js +1 -0
  100. package/apps/pwa/out/_next/static/chunks/main-app-4ca74dbbee7057c6.js +1 -0
  101. package/apps/pwa/out/_next/static/chunks/pages/_app-13a0a886df4d97a1.js +1 -0
  102. package/apps/pwa/out/_next/static/chunks/pages/_error-6af8591cd3a3f038.js +1 -0
  103. package/apps/pwa/out/_next/static/chunks/webpack-539cde99554be20f.js +1 -0
  104. package/apps/pwa/out/_next/static/css/2e33713c444d5b39.css +1 -0
  105. package/apps/pwa/out/_next/static/x6mjI6vqOkVVu0jFuDkz1/_buildManifest.js +1 -0
  106. package/apps/pwa/out/approvals/index.html +1 -1
  107. package/apps/pwa/out/approvals/index.txt +23 -23
  108. package/apps/pwa/out/autopilot/index.html +1 -1
  109. package/apps/pwa/out/autopilot/index.txt +23 -23
  110. package/apps/pwa/out/board/index.html +1 -1
  111. package/apps/pwa/out/board/index.txt +23 -23
  112. package/apps/pwa/out/botlab/index.html +1 -1
  113. package/apps/pwa/out/botlab/index.txt +22 -22
  114. package/apps/pwa/out/bots/index.html +1 -1
  115. package/apps/pwa/out/bots/index.txt +22 -22
  116. package/apps/pwa/out/chat/index.html +1 -1
  117. package/apps/pwa/out/chat/index.txt +22 -22
  118. package/apps/pwa/out/consult/index.html +1 -1
  119. package/apps/pwa/out/consult/index.txt +23 -23
  120. package/apps/pwa/out/control/index.html +1 -1
  121. package/apps/pwa/out/control/index.txt +23 -23
  122. package/apps/pwa/out/design-check/index.html +1 -1
  123. package/apps/pwa/out/design-check/index.txt +23 -23
  124. package/apps/pwa/out/editor/index.html +1 -1
  125. package/apps/pwa/out/editor/index.txt +23 -23
  126. package/apps/pwa/out/exec/index.html +1 -1
  127. package/apps/pwa/out/exec/index.txt +23 -23
  128. package/apps/pwa/out/field/index.html +1 -1
  129. package/apps/pwa/out/field/index.txt +23 -23
  130. package/apps/pwa/out/healthz +1 -1
  131. package/apps/pwa/out/index.html +1 -1
  132. package/apps/pwa/out/index.txt +22 -22
  133. package/apps/pwa/out/inside/index.html +1 -1
  134. package/apps/pwa/out/inside/index.txt +22 -22
  135. package/apps/pwa/out/intake/index.html +1 -1
  136. package/apps/pwa/out/intake/index.txt +23 -23
  137. package/apps/pwa/out/live/index.html +1 -1
  138. package/apps/pwa/out/live/index.txt +23 -23
  139. package/apps/pwa/out/loops/index.html +1 -0
  140. package/apps/pwa/out/loops/index.txt +30 -0
  141. package/apps/pwa/out/market/index.html +1 -1
  142. package/apps/pwa/out/market/index.txt +22 -22
  143. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  144. package/apps/pwa/out/missions/placeholder/index.txt +22 -22
  145. package/apps/pwa/out/morning/index.html +1 -1
  146. package/apps/pwa/out/morning/index.txt +23 -23
  147. package/apps/pwa/out/observatory/index.html +1 -1
  148. package/apps/pwa/out/observatory/index.txt +23 -23
  149. package/apps/pwa/out/ops/checklist/index.html +1 -1
  150. package/apps/pwa/out/ops/checklist/index.txt +22 -22
  151. package/apps/pwa/out/ops/release/index.html +1 -1
  152. package/apps/pwa/out/ops/release/index.txt +22 -22
  153. package/apps/pwa/out/ops/seats/index.html +1 -1
  154. package/apps/pwa/out/ops/seats/index.txt +22 -22
  155. package/apps/pwa/out/outputs/index.html +1 -1
  156. package/apps/pwa/out/outputs/index.txt +22 -22
  157. package/apps/pwa/out/reflection/index.html +1 -1
  158. package/apps/pwa/out/reflection/index.txt +23 -23
  159. package/apps/pwa/out/scheduler/index.html +1 -1
  160. package/apps/pwa/out/scheduler/index.txt +23 -23
  161. package/apps/pwa/out/sessions/index.html +1 -1
  162. package/apps/pwa/out/sessions/index.txt +22 -22
  163. package/apps/pwa/out/settings/devices/index.html +1 -1
  164. package/apps/pwa/out/settings/devices/index.txt +23 -23
  165. package/apps/pwa/out/settings/index.html +1 -1
  166. package/apps/pwa/out/settings/index.txt +23 -23
  167. package/apps/pwa/out/setup/done/index.html +1 -1
  168. package/apps/pwa/out/setup/done/index.txt +23 -23
  169. package/apps/pwa/out/setup/index.html +1 -1
  170. package/apps/pwa/out/setup/index.txt +23 -23
  171. package/apps/pwa/out/share/index.html +1 -1
  172. package/apps/pwa/out/share/index.txt +23 -23
  173. package/apps/pwa/out/showroom/index.html +1 -1
  174. package/apps/pwa/out/showroom/index.txt +22 -22
  175. package/apps/pwa/out/tasks/index.html +1 -1
  176. package/apps/pwa/out/tasks/index.txt +23 -23
  177. package/apps/pwa/out/term/index.html +1 -1
  178. package/apps/pwa/out/term/index.txt +23 -23
  179. package/apps/pwa/out/today/index.html +1 -1
  180. package/apps/pwa/out/today/index.txt +23 -23
  181. package/apps/pwa/out/trace/index.html +1 -1
  182. package/apps/pwa/out/trace/index.txt +23 -23
  183. package/apps/pwa/out/vault/index.html +1 -1
  184. package/apps/pwa/out/vault/index.txt +23 -23
  185. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  186. package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
  187. package/apps/pwa/out/workflows/index.html +1 -1
  188. package/apps/pwa/out/workflows/index.txt +23 -23
  189. package/apps/pwa/out/workspace/index.html +1 -1
  190. package/apps/pwa/out/workspace/index.txt +23 -23
  191. package/apps/pwa/out/worktrees/index.html +1 -1
  192. package/apps/pwa/out/worktrees/index.txt +23 -23
  193. package/examples/teams/personas/backend.yaml +4 -0
  194. package/examples/teams/personas/pm.yaml +4 -0
  195. package/examples/teams/personas/qa.yaml +4 -0
  196. package/examples/teams/small-product.yaml +20 -0
  197. package/graphs/lecture/lecture-note.yaml +28 -0
  198. package/graphs/lecture/recipes.yaml +17 -0
  199. package/graphs/orchestrator/orchestrator.yaml +59 -0
  200. package/graphs/orchestrator/recipes.yaml +18 -0
  201. package/graphs/release/recipes.yaml +3 -0
  202. package/graphs/release/release-loop.yaml +4 -1
  203. package/graphs/rhythm/daily.yaml +38 -0
  204. package/graphs/rhythm/recipes.yaml +18 -0
  205. package/graphs/rhythm/weekly.yaml +39 -0
  206. package/graphs/video/recipes.yaml +3 -0
  207. package/graphs/video/youtube-summary.yaml +17 -0
  208. package/package.json +2 -1
  209. package/scripts/audit-test-state-writes.ts +72 -17
  210. package/scripts/brand/check.ts +186 -0
  211. package/scripts/context-hooks/emit.ts +36 -14
  212. package/scripts/coord-channel-watch.sh +1 -0
  213. package/scripts/coord-post.sh +11 -1
  214. package/scripts/discord-seat-probe.ts +144 -0
  215. package/scripts/docs-cli-check.ts +23 -6
  216. package/scripts/expose-rubric-check.ts +118 -0
  217. package/scripts/expose-rubric-suggest.ts +163 -0
  218. package/scripts/heal/run-role.ts +14 -0
  219. package/scripts/hq/hq-standby.sh +65 -0
  220. package/scripts/hq/standby-promote.ts +173 -0
  221. package/scripts/hq/standby-role.ts +59 -0
  222. package/scripts/hq/standby-snapshot.ts +226 -0
  223. package/scripts/hq/standby-verify.ts +223 -0
  224. package/scripts/lecture-note/node.ts +256 -0
  225. package/scripts/lessons/handbook-page.ts +85 -0
  226. package/scripts/release-loop/gate-node.ts +62 -15
  227. package/scripts/release-loop/ops-upgrade-node.ts +17 -5
  228. package/scripts/release-loop/publish-node.ts +14 -1
  229. package/scripts/release-loop/unattended-release.ts +23 -1
  230. package/scripts/release-loop/vault-note-node.ts +196 -0
  231. package/scripts/release-story/draft.ts +172 -0
  232. package/scripts/rhythm/daily.ts +383 -0
  233. package/scripts/rhythm/weekly.ts +294 -0
  234. package/scripts/skills/public-check.ts +169 -0
  235. package/scripts/test-deterministic.ts +57 -11
  236. package/scripts/test-diet/effectiveness.ts +195 -0
  237. package/scripts/test-diet/lib.ts +16 -8
  238. package/scripts/test-diet/node.ts +77 -9
  239. package/scripts/ux/demo-rehearsal.ts +2 -2
  240. package/scripts/ux/lib/rehearsal-verdict.ts +1 -0
  241. package/scripts/video/youtube-summary.ts +133 -0
  242. package/skills/diagram-master/SKILL.md +42 -49
  243. package/skills/diagram-master/references/pyproject.toml +4 -1
  244. package/skills/diagram-master/references/routing-guide.md +4 -2
  245. package/skills/grill-me/SKILL.md +1 -0
  246. package/skills/omni-crawl/SKILL.md +16 -16
  247. package/skills/omni-crawl/package-lock.json +1 -544
  248. package/skills/omni-crawl/package.json +3 -4
  249. package/skills/omni-crawl/scripts/main.ts +13 -13
  250. package/skills/omni-crawl/scripts/monitor.ts +8 -8
  251. package/skills/omni-crawl/src/apify.ts +13 -2
  252. package/skills/omni-crawl/src/capture.ts +6 -0
  253. package/skills/omni-crawl/src/registry.ts +2 -2
  254. package/skills/omni-crawl/src/stdout-json.ts +4 -0
  255. package/skills/omni-digest/SKILL.md +10 -11
  256. package/skills/omni-digest/package-lock.json +0 -545
  257. package/skills/omni-digest/package.json +2 -5
  258. package/skills/omni-digest/scripts/main.ts +3 -3
  259. package/skills/omni-digest/src/delegate.ts +5 -5
  260. package/skills/omni-digest/src/fetch.ts +2 -2
  261. package/skills/omni-digest/src/media.ts +25 -11
  262. package/skills/omni-digest/src/visual.ts +3 -2
  263. package/skills/omni-market/scripts/main.ts +1 -1
  264. package/skills/omni-market/scripts/stdout-json.ts +4 -0
  265. package/skills/youtube-master/SKILL.md +13 -13
  266. package/skills/youtube-master/package-lock.json +0 -545
  267. package/skills/youtube-master/package.json +2 -5
  268. package/skills/youtube-master/scripts/main.ts +2 -2
  269. package/skills/youtube-master/scripts/transcribe-local.ts +1 -1
  270. package/skills/youtube-master/src/util.ts +30 -7
  271. package/src/agent/identity-env.ts +11 -0
  272. package/src/agent-mission/driver.ts +15 -10
  273. package/src/agent-mission/duo.ts +94 -0
  274. package/src/agent-mission/pty-decision.ts +1 -1
  275. package/src/autopilot/build/llm-conflict-merge.ts +45 -8
  276. package/src/autopilot/build/merge-intent.ts +53 -0
  277. package/src/autopilot/track-agent-shadow.ts +2 -1
  278. package/src/autopilot/track-agent.ts +4 -1
  279. package/src/boot/daemon-prompt-request.ts +3 -0
  280. package/src/boot/daemon-prompt-turn.ts +6 -2
  281. package/src/boot/daemon-runtime.ts +48 -16
  282. package/src/chat/index.ts +2 -0
  283. package/src/claims/claims-ledger.ts +232 -0
  284. package/src/claims/claims-render.ts +157 -0
  285. package/src/cli/claims-cli.ts +94 -0
  286. package/src/cli/decisions-cli.ts +84 -20
  287. package/src/cli/docs-cli.ts +2 -0
  288. package/src/cli/flow-cli.ts +11 -0
  289. package/src/cli/freeze-cli.ts +45 -0
  290. package/src/cli/lesson-cli.ts +108 -0
  291. package/src/cli/logs-cli.ts +104 -2
  292. package/src/cli/logs-model-input.ts +131 -0
  293. package/src/cli/outputs-cli.ts +92 -0
  294. package/src/cli/persona-cli.ts +29 -0
  295. package/src/cli/pod-cli.ts +10 -3
  296. package/src/cli/pr-cli.ts +44 -2
  297. package/src/cli/private-ledger-commands.ts +54 -4
  298. package/src/cli/pty-drive-verdict.ts +11 -2
  299. package/src/cli/release-cli.ts +248 -45
  300. package/src/cli/rfc-status.ts +91 -0
  301. package/src/connectors/linear.ts +5 -3
  302. package/src/context-bus/condense.ts +189 -0
  303. package/src/context-bus/context-day.ts +82 -0
  304. package/src/context-bus/context-now-public.ts +86 -0
  305. package/src/context-bus/context-now-surfaces.ts +70 -4
  306. package/src/context-bus/context-now.ts +4 -3
  307. package/src/context-bus/long-term-memory.ts +201 -24
  308. package/src/context-bus/nightly-condense.ts +1 -1
  309. package/src/dashboard/index.ts +75 -22
  310. package/src/dashboard/slash-runtime/chat-persona.ts +66 -0
  311. package/src/dashboard/slash-runtime/dashboard-handlers.ts +99 -0
  312. package/src/dashboard/slash-runtime/loops-table.ts +77 -0
  313. package/src/decisions/decision-cards.ts +13 -7
  314. package/src/decisions/decision-ledger.ts +83 -18
  315. package/src/discord/sprint21-runtime.ts +16 -0
  316. package/src/discord-self-message.ts +8 -0
  317. package/src/discord-slash-wire.ts +31 -2
  318. package/src/domains/outbound-alert.ts +1 -1
  319. package/src/elanous-config-dir.ts +29 -4
  320. package/src/execution-loop/launch-gate.ts +29 -14
  321. package/src/flow/cards.ts +239 -0
  322. package/src/graph-kinds/registry.ts +2 -2
  323. package/src/graph-runner/graph-approval-card.ts +2 -2
  324. package/src/graph-runner/graph-grow.ts +64 -11
  325. package/src/graph-runner/graph-tick.ts +13 -2
  326. package/src/graph-runner/runner.ts +111 -22
  327. package/src/harness/harness-cli-command.ts +178 -27
  328. package/src/harness/harness-incidents.ts +43 -12
  329. package/src/harness/harness-pod-dispatch.ts +2 -0
  330. package/src/harness/harness-queue-child.ts +37 -0
  331. package/src/harness/harness-queue.ts +494 -0
  332. package/src/harness/helper-scan.ts +139 -0
  333. package/src/harness/model-input-observation.ts +37 -0
  334. package/src/hooks/github-shadow.ts +72 -2
  335. package/src/hooks/receiver.ts +4 -2
  336. package/src/hq/hq.ts +318 -0
  337. package/src/hq/lease.ts +210 -0
  338. package/src/hq/ledger-write-target.ts +41 -0
  339. package/src/hq/promote.ts +301 -0
  340. package/src/index.ts +281 -27
  341. package/src/intake-plane/discord-seat-work.ts +13 -6
  342. package/src/intake-plane/linear-wish.ts +72 -0
  343. package/src/intake-plane/persona-answer.ts +5 -0
  344. package/src/intake-plane/telegram-seat-work.ts +13 -6
  345. package/src/intake-plane/wish-benchmark-ledger.ts +129 -0
  346. package/src/intake-plane/wish-card.ts +53 -2
  347. package/src/intake-plane/wish-folder.ts +5 -2
  348. package/src/intake-plane/wish-reply.ts +72 -0
  349. package/src/lessons/lesson-import.ts +106 -0
  350. package/src/lessons/lesson-ledger.ts +206 -0
  351. package/src/loops/activity.ts +203 -0
  352. package/src/loops/budget.ts +32 -0
  353. package/src/loops/checker.ts +180 -0
  354. package/src/loops/contract/examples/orchestrator.loop.yaml +1 -1
  355. package/src/loops/loop-cli.ts +72 -6
  356. package/src/loops/neighbors.ts +40 -0
  357. package/src/loops/observe.ts +59 -0
  358. package/src/loops/orchestrator/author-depth.ts +164 -0
  359. package/src/loops/orchestrator/author-input-check.ts +77 -0
  360. package/src/loops/orchestrator/author-ledger.ts +230 -0
  361. package/src/loops/orchestrator/finish-rate.ts +175 -0
  362. package/src/loops/orchestrator/overlap.ts +180 -0
  363. package/src/loops/orchestrator/resync-shadow.ts +227 -0
  364. package/src/loops/orchestrator/tick.ts +302 -0
  365. package/src/loops/orchestrator/traffic.ts +165 -0
  366. package/src/loops/package/verify.ts +125 -0
  367. package/src/loops/registry.ts +454 -9
  368. package/src/loops/status-rows.ts +118 -0
  369. package/src/loops/verdict.ts +34 -0
  370. package/src/maturity/bot-slash-catalog.ts +66 -0
  371. package/src/maturity/feature-maturity.ts +8 -5
  372. package/src/memory.ts +37 -4
  373. package/src/nexus/api/card-followup-route.ts +118 -0
  374. package/src/nexus/api/dashboard.ts +6 -1
  375. package/src/nexus/api/decisions-route.ts +68 -0
  376. package/src/nexus/api/http-server.ts +65 -2
  377. package/src/nexus/api/merge-approvals-gate.ts +1 -1
  378. package/src/nexus/api/personas.ts +2 -1
  379. package/src/nexus/api/schedules-read.ts +5 -1
  380. package/src/nexus/api/skill-exec-route.ts +97 -0
  381. package/src/nexus/api/task-cards-api.ts +44 -5
  382. package/src/nexus/api/workflows.ts +2 -1
  383. package/src/persona/loader.ts +18 -0
  384. package/src/persona/persona-todo.ts +14 -9
  385. package/src/persona/presets.ts +2 -1
  386. package/src/persona/team.ts +105 -0
  387. package/src/persona/types.ts +4 -0
  388. package/src/pod-lease/host-lease.ts +130 -20
  389. package/src/release-loop/auto-start.ts +48 -0
  390. package/src/release-loop/checklist.ts +13 -4
  391. package/src/release-loop/feature-store.ts +28 -16
  392. package/src/release-loop/landed-but-yellow.ts +12 -0
  393. package/src/release-loop/landing-freeze.ts +121 -0
  394. package/src/release-loop/placement.ts +242 -0
  395. package/src/release-loop/release-schedule.ts +3 -1
  396. package/src/seat-dispatch/ceo-commands.ts +2 -2
  397. package/src/seat-dispatch/seat-ask.ts +73 -15
  398. package/src/seat-dispatch/seat-questions.ts +58 -0
  399. package/src/seat-loop/seat-loop.ts +495 -46
  400. package/src/seat-loop/seat-pack.ts +127 -0
  401. package/src/self-dev/ask-launch-flow.ts +3 -0
  402. package/src/self-dev/dev-pipeline.ts +6 -2
  403. package/src/self-dev/draft-sweep.ts +76 -0
  404. package/src/self-dev/run-store.ts +73 -1
  405. package/src/self-dev/run-supervisor.ts +8 -0
  406. package/src/self-dev/self-orchestrate-runtime.ts +5 -1
  407. package/src/self-implement/budget-gate.ts +29 -0
  408. package/src/self-implement/expose-gate.ts +47 -0
  409. package/src/self-implement/frozen-merges.ts +294 -0
  410. package/src/self-implement/gate-cli.ts +10 -4
  411. package/src/self-implement/gate-scope.ts +46 -9
  412. package/src/self-implement/goal-archive.ts +222 -0
  413. package/src/self-implement/goal-lookup.ts +147 -0
  414. package/src/self-implement/heal-intake.ts +79 -0
  415. package/src/self-implement/host-regate.ts +25 -2
  416. package/src/self-implement/l8-merge-queue.ts +157 -1
  417. package/src/self-implement/orchestrator.ts +114 -10
  418. package/src/self-implement/revert-guard.ts +62 -0
  419. package/src/self-implement/run-ledger.ts +1 -1
  420. package/src/self-implement/seams.ts +28 -8
  421. package/src/session/index.ts +2 -0
  422. package/src/steward/launch.ts +5 -1
  423. package/src/steward/readiness.ts +125 -0
  424. package/src/steward/rescue.ts +75 -0
  425. package/src/steward/triage.ts +48 -9
  426. package/src/task-cards/card-cli.ts +86 -0
  427. package/src/task-cards/card-store.ts +10 -6
  428. package/src/task-orchestrator/surfaces/pod-lease.ts +23 -8
  429. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +46 -6
  430. package/src/task-orchestrator/surfaces/pod-pool.ts +27 -10
  431. package/src/task-orchestrator/surfaces/self-implement-pod.ts +112 -18
  432. package/src/telegram-commands.ts +27 -3
  433. package/src/telegram-dispatch-forward.ts +1 -1
  434. package/src/telegram-fabric-plan.ts +90 -0
  435. package/src/telegram-loops-command.ts +44 -0
  436. package/src/telegram-project-command.ts +10 -4
  437. package/src/telegram.ts +5 -0
  438. package/src/tool-runtime/elanous-control-runtimes.ts +2 -1
  439. package/src/user-config.ts +204 -16
  440. package/src/version/packed-revision.json +1 -1
  441. package/src/workflow-runtime/executor.ts +4 -0
  442. package/src/workflow-runtime/node-catalog.ts +13 -0
  443. package/src/workflow-runtime/nodes/knowledge.ts +38 -0
  444. package/src/workflow-runtime/schema.ts +17 -1
  445. package/src/workflow-runtime/storage.ts +31 -13
  446. package/src/workflow-runtime/types.ts +13 -1
  447. package/apps/pwa/out/_next/static/-6XrAoD3eU7NyMHAE4xPS/_buildManifest.js +0 -1
  448. package/apps/pwa/out/_next/static/chunks/1147-466ec15afacc1a65.js +0 -1
  449. package/apps/pwa/out/_next/static/chunks/1159-0c5ef8bc21cb6afb.js +0 -1
  450. package/apps/pwa/out/_next/static/chunks/1394-ee3795b112907122.js +0 -1
  451. package/apps/pwa/out/_next/static/chunks/2176-c379e3ebce874151.js +0 -1
  452. package/apps/pwa/out/_next/static/chunks/2426.8c6ee0c668d53def.js +0 -1
  453. package/apps/pwa/out/_next/static/chunks/3170-7ec60a56fa0e4728.js +0 -1
  454. package/apps/pwa/out/_next/static/chunks/3278-ae6ef084769f6195.js +0 -1
  455. package/apps/pwa/out/_next/static/chunks/3454-988a53dac60a1763.js +0 -1
  456. package/apps/pwa/out/_next/static/chunks/3638-aa80b4641ea439db.js +0 -1
  457. package/apps/pwa/out/_next/static/chunks/3735-fe3402553ac1c9c6.js +0 -1
  458. package/apps/pwa/out/_next/static/chunks/3992-2e522a8ac5f2afde.js +0 -25
  459. package/apps/pwa/out/_next/static/chunks/4215-c9d91dd644ec1719.js +0 -1
  460. package/apps/pwa/out/_next/static/chunks/4508-5d0161d1e724d735.js +0 -1
  461. package/apps/pwa/out/_next/static/chunks/4733-623a79345b171e1c.js +0 -1
  462. package/apps/pwa/out/_next/static/chunks/4883.3bd969ea1d3aaaff.js +0 -2
  463. package/apps/pwa/out/_next/static/chunks/5532.6899a12958a89b72.js +0 -1
  464. package/apps/pwa/out/_next/static/chunks/5845-e6c003ce8d5fd36a.js +0 -1
  465. package/apps/pwa/out/_next/static/chunks/5978-b27004c677119115.js +0 -1
  466. package/apps/pwa/out/_next/static/chunks/6048-4c5c33579bc4ef78.js +0 -1
  467. package/apps/pwa/out/_next/static/chunks/6303-422dbf0f67c72aac.js +0 -1
  468. package/apps/pwa/out/_next/static/chunks/6751-bb3595948b2e311b.js +0 -1
  469. package/apps/pwa/out/_next/static/chunks/7274-19b7279f18b47ab3.js +0 -1
  470. package/apps/pwa/out/_next/static/chunks/7277-0b2fc7c16ba2d64b.js +0 -1
  471. package/apps/pwa/out/_next/static/chunks/7551-a0e0707b79164ddd.js +0 -1
  472. package/apps/pwa/out/_next/static/chunks/7758-0b018a9e4fc56200.js +0 -1
  473. package/apps/pwa/out/_next/static/chunks/7777-79f845045aa1ed86.js +0 -1
  474. package/apps/pwa/out/_next/static/chunks/8025-cc24ee5d8f2fb6f8.js +0 -1
  475. package/apps/pwa/out/_next/static/chunks/8206-c6684dcde12be721.js +0 -1
  476. package/apps/pwa/out/_next/static/chunks/8605-0259994816604c49.js +0 -1
  477. package/apps/pwa/out/_next/static/chunks/8725.bf46bb1395dd0fa2.js +0 -1
  478. package/apps/pwa/out/_next/static/chunks/8734-252d511cd276c0dc.js +0 -1
  479. package/apps/pwa/out/_next/static/chunks/8834-ca1cb09c1cb1a6bf.js +0 -1
  480. package/apps/pwa/out/_next/static/chunks/9269.d2a4795bc6812717.js +0 -1
  481. package/apps/pwa/out/_next/static/chunks/9538-2816b90923606459.js +0 -1
  482. package/apps/pwa/out/_next/static/chunks/9920-5cfdc5e8fd209825.js +0 -1
  483. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-d16993889b125c42.js +0 -1
  484. package/apps/pwa/out/_next/static/chunks/app/approvals/page-cb169620945d1952.js +0 -1
  485. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-cefe8169e36cf62a.js +0 -1
  486. package/apps/pwa/out/_next/static/chunks/app/board/page-d2e35228bc497119.js +0 -1
  487. package/apps/pwa/out/_next/static/chunks/app/botlab/page-5c05b0da4bc1392c.js +0 -1
  488. package/apps/pwa/out/_next/static/chunks/app/bots/page-e0ccdb576812b523.js +0 -1
  489. package/apps/pwa/out/_next/static/chunks/app/chat/page-813c4a52c9d05ca8.js +0 -1
  490. package/apps/pwa/out/_next/static/chunks/app/consult/page-4731f2c215358937.js +0 -1
  491. package/apps/pwa/out/_next/static/chunks/app/control/page-a022b257950f39e6.js +0 -1
  492. package/apps/pwa/out/_next/static/chunks/app/design-check/page-a2ffc717e04b8b0a.js +0 -1
  493. package/apps/pwa/out/_next/static/chunks/app/editor/page-b59ee4e70fc18fcf.js +0 -1
  494. package/apps/pwa/out/_next/static/chunks/app/field/page-c172dabd796eb7a3.js +0 -1
  495. package/apps/pwa/out/_next/static/chunks/app/healthz/route-d16993889b125c42.js +0 -1
  496. package/apps/pwa/out/_next/static/chunks/app/inside/page-1efd0cb9e0ed9d13.js +0 -1
  497. package/apps/pwa/out/_next/static/chunks/app/intake/page-d5dc7ff69a37b4cf.js +0 -1
  498. package/apps/pwa/out/_next/static/chunks/app/layout-e71c84189e8c683e.js +0 -1
  499. package/apps/pwa/out/_next/static/chunks/app/live/page-1d6a533ce5a6efcb.js +0 -1
  500. package/apps/pwa/out/_next/static/chunks/app/market/page-dfca9b7d2de0b135.js +0 -1
  501. package/apps/pwa/out/_next/static/chunks/app/morning/page-6dd3bfbbde2a5c2c.js +0 -1
  502. package/apps/pwa/out/_next/static/chunks/app/not-found-490653e8d4cc8852.js +0 -1
  503. package/apps/pwa/out/_next/static/chunks/app/observatory/page-77447356baad75d2.js +0 -1
  504. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-29e961191f6057df.js +0 -1
  505. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-c822f55b884d9104.js +0 -1
  506. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fa4ccfc432424e52.js +0 -1
  507. package/apps/pwa/out/_next/static/chunks/app/outputs/page-6d604227e1664b71.js +0 -1
  508. package/apps/pwa/out/_next/static/chunks/app/page-9c7e8f744a765daf.js +0 -1
  509. package/apps/pwa/out/_next/static/chunks/app/reflection/page-4048543b4b7ad5e0.js +0 -1
  510. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4c3a3126abed551a.js +0 -1
  511. package/apps/pwa/out/_next/static/chunks/app/sessions/page-64b0aecb1ca75e75.js +0 -1
  512. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-4292d0149020e8a7.js +0 -1
  513. package/apps/pwa/out/_next/static/chunks/app/settings/page-db14d0be7f1c460c.js +0 -1
  514. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-e5d8072cd7cfef73.js +0 -1
  515. package/apps/pwa/out/_next/static/chunks/app/setup/layout-d16993889b125c42.js +0 -1
  516. package/apps/pwa/out/_next/static/chunks/app/setup/page-9547b5cec7cf76bf.js +0 -1
  517. package/apps/pwa/out/_next/static/chunks/app/share/page-e6dcb83265530636.js +0 -1
  518. package/apps/pwa/out/_next/static/chunks/app/showroom/page-45248f08d6aecae5.js +0 -1
  519. package/apps/pwa/out/_next/static/chunks/app/tasks/page-2cddeb9e2aeedc55.js +0 -1
  520. package/apps/pwa/out/_next/static/chunks/app/today/page-baba2e047872fb0f.js +0 -1
  521. package/apps/pwa/out/_next/static/chunks/app/trace/page-e895da4ec9cac345.js +0 -1
  522. package/apps/pwa/out/_next/static/chunks/app/vault/page-4344faf20216233b.js +0 -1
  523. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-698bb29f11864b89.js +0 -1
  524. package/apps/pwa/out/_next/static/chunks/app/workflows/page-bd6a6dc1ddc273f2.js +0 -1
  525. package/apps/pwa/out/_next/static/chunks/app/workspace/page-2e9e2fb01c9da1c9.js +0 -1
  526. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-d62ceb13411ebba4.js +0 -1
  527. package/apps/pwa/out/_next/static/chunks/framework-172630e6aa5e2d7f.js +0 -1
  528. package/apps/pwa/out/_next/static/chunks/main-app-f0fe2d7477845181.js +0 -1
  529. package/apps/pwa/out/_next/static/chunks/main-fc98ac70b44eccb0.js +0 -1
  530. package/apps/pwa/out/_next/static/chunks/pages/_app-5bd7f10271f72413.js +0 -1
  531. package/apps/pwa/out/_next/static/chunks/pages/_error-97d0d85ea05fef9b.js +0 -1
  532. package/apps/pwa/out/_next/static/chunks/webpack-9e4d02d567b98240.js +0 -1
  533. package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +0 -1
  534. /package/apps/pwa/out/_next/static/{-6XrAoD3eU7NyMHAE4xPS → x6mjI6vqOkVVu0jFuDkz1}/_ssgManifest.js +0 -0
@@ -0,0 +1,302 @@
1
+ import { withFileLockSync } from '../../storage/file-lock.js';
2
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { CardStore, taskCardsDir, type TaskCard } from '../../task-cards/card-store.js';
5
+ import { Database } from 'bun:sqlite';
6
+ import { type ChecklistItem } from '../../release-loop/checklist.js';
7
+ import { effectiveInstanceRoot } from '../../instance/resolve.js';
8
+ import { getUserConfig } from '../../user-config.js';
9
+ import { debug } from '../../debug/log.js';
10
+ import { listSelfDevRuns, runSummaryLine, selfDevRunsDir } from '../../self-dev/run-store.js';
11
+
12
+ export type Window = '08' | '12' | '18';
13
+ export type Node = 'intake' | 'split' | 'place' | 'delegate' | 'reconcile' | 'report';
14
+ export type Mode = 'off' | 'shadow' | 'live';
15
+ const SEAT_LOOP_IDS: Readonly<Record<string, string>> = { OP: 'op-seat', MK: 'cmo-seat', TC: 'tc-seat', UX: 'ux-seat' };
16
+ export interface Cell { id: string; title: string; seat?: string; host?: string; version?: string; cardId: string; origin: 'candidate' | 'flow1a'; priority?: 'P0' | 'P1' | 'P2'; predecessors?: string[] }
17
+ export interface TickState {
18
+ runId: string; window: Window; mode: Mode; day: string;
19
+ cards: TaskCard[]; skippedCards: Array<{ id: string; reason: string }>; cells: Cell[]; placed: number; unplaced?: number; delegated: number; wouldDelegate: number; rebalanced?: number;
20
+ reconciled: { reached: number; progressing: number; blocked: number; unknown: number };
21
+ runSummaries?: string[];
22
+ nodes: Partial<Record<Node, 'ok' | 'skipped'>>;
23
+ missing: string[];
24
+ }
25
+ /** FLOW1a contract: with `{ shadow: true }` the splitter only proposes cells and writes nothing (cards, ledgers, LLM side effects excepted). */
26
+ export type SplitAdapter = (card: TaskCard, opts?: { shadow?: boolean }) => Promise<Array<{ id: string; title: string; seat?: string; host?: string }>> | Array<{ id: string; title: string; seat?: string; host?: string }>;
27
+ /** RELPLAN1 `placeCell(input: PlacementCell)` — owner·priority·predecessors are required; the result carries no seat. */
28
+ export type PlacementInput = { id: string; title: string; owner: string; priority: 'P0' | 'P1' | 'P2'; predecessors: string[] };
29
+ export type PlaceAdapter = (cell: PlacementInput) => Promise<{ version?: string } | null> | { version?: string } | null;
30
+ export type RebalanceAdapter = (version: string) => Promise<unknown[]> | unknown[];
31
+ export interface TickDeps {
32
+ root?: string; now?: Date; mode?: Mode; runId?: string; window?: Window;
33
+ cards?: () => TaskCard[]; split?: SplitAdapter; placeCell?: PlaceAdapter;
34
+ loadAdapter?: (path: string, name: string) => Promise<unknown>;
35
+ checklist?: (version: string) => Pick<ChecklistItem, 'id' | 'status'>[];
36
+ observe?: (event: string, data: { loopId: string; runId: string; window: Window; node: Node; reason: string; count: number; targetLoopId?: string }) => void;
37
+ print?: (line: string) => void;
38
+ }
39
+
40
+ export function kstWindow(now: Date): Window {
41
+ const hour = Number(new Intl.DateTimeFormat('en-US', { timeZone: 'Asia/Seoul', hour: '2-digit', hourCycle: 'h23' }).format(now));
42
+ return hour < 12 ? '08' : hour < 18 ? '12' : '18';
43
+ }
44
+ function kstDay(now: Date): string {
45
+ return new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Seoul', year: 'numeric', month: '2-digit', day: '2-digit' }).format(now);
46
+ }
47
+ export function activeNode(node: Node, window: Window): boolean {
48
+ return node === 'report' || (window === '08' ? node !== 'reconcile' : node === 'reconcile');
49
+ }
50
+ function validVersion(version: unknown): version is string {
51
+ return typeof version === 'string' && /^\d+\.\d+\.\d+(?:-(?:rc|alpha|beta)\.\d+)?$/.test(version);
52
+ }
53
+ export function wishCards(cards: readonly TaskCard[]): TaskCard[] {
54
+ return cards.filter(card => card.status === 'open' && card.goalId.startsWith('wish:') &&
55
+ !card.sections.some(section => section.key === 'orch:split' || section.key.startsWith('orch:split:')));
56
+ }
57
+ export function reconciliation(cells: readonly Cell[], requests: readonly { key: string; status: string }[], checklist: (version: string) => Pick<ChecklistItem, 'id' | 'status'>[]): TickState['reconciled'] {
58
+ const result = { reached: 0, progressing: 0, blocked: 0, unknown: 0 };
59
+ const byVersion = new Map<string, Pick<ChecklistItem, 'id' | 'status'>[]>();
60
+ const keys = new Set(requests.filter(row => row.status === 'queued').map(row => row.key));
61
+ for (const cell of cells) {
62
+ if (!keys.has(`orch:${cell.cardId}:${cell.id}`) || !cell.version) { result.unknown++; continue; }
63
+ if (!byVersion.has(cell.version)) byVersion.set(cell.version, checklist(cell.version));
64
+ const status = byVersion.get(cell.version)!.find(item => item.id === cell.id)?.status;
65
+ if (status === 'green' || status === 'done') result.reached++;
66
+ else if (status === 'yellow') result.progressing++;
67
+ else if (status === 'red') result.blocked++;
68
+ else result.unknown++;
69
+ }
70
+ return result;
71
+ }
72
+ export function reportLine(state: TickState): string {
73
+ const r = state.reconciled;
74
+ const line = `orchestrator ${state.window} cards=${state.cards.length} cells=${state.cells.length} placed=${state.missing.includes('relplan1-absent') ? 'missing' : state.placed} delegated=${state.mode === 'shadow' ? `would ${state.wouldDelegate}` : state.delegated} reconciled=${r.reached}/${r.progressing}/${r.blocked}/${r.unknown} mode=${state.mode}`;
75
+ return state.runSummaries?.length ? `${line}\n${state.runSummaries.join('\n')}` : line;
76
+ }
77
+ function readCardSnapshot(root: string): { cards: TaskCard[]; skippedCards: TickState['skippedCards'] } {
78
+ const dir = taskCardsDir(root);
79
+ if (!existsSync(dir)) return { cards: [], skippedCards: [] };
80
+ const cards: TaskCard[] = [];
81
+ const skippedCards: TickState['skippedCards'] = [];
82
+ for (const name of readdirSync(dir).filter(name => /^[a-zA-Z0-9_-]+\.jsonl$/.test(name))) {
83
+ const id = name.slice(0, -'.jsonl'.length);
84
+ try {
85
+ let card: TaskCard | undefined;
86
+ const raw = readFileSync(join(dir, name), 'utf8');
87
+ if (raw && !raw.endsWith('\n')) throw new Error(`incomplete card journal: ${name}`);
88
+ for (const line of raw.split('\n').filter(Boolean)) {
89
+ const event = JSON.parse(line) as { type: string; id?: string; goalId?: string; title?: string; createdAt: string; key?: string; owner?: string; content?: string };
90
+ if (event.type === 'created' && event.id && event.goalId && event.title) card = { id: event.id, goalId: event.goalId, title: event.title, createdAt: event.createdAt, status: 'open', sections: [] };
91
+ else if (event.type === 'section' && card && event.key && event.owner && event.content) card.sections.push({ key: event.key, owner: event.owner, content: event.content, createdAt: event.createdAt });
92
+ else if (event.type === 'closed' && card) card.status = 'closed';
93
+ }
94
+ if (card) {
95
+ if (card.id !== id) throw new Error(`card journal id mismatch: ${name}`);
96
+ cards.push(card);
97
+ }
98
+ } catch (error) { skippedCards.push({ id, reason: String(error) }); }
99
+ }
100
+ return { cards: cards.sort((a, b) => b.createdAt.localeCompare(a.createdAt) || b.id.localeCompare(a.id)), skippedCards };
101
+ }
102
+ function readChecklist(root: string, version: string): Pick<ChecklistItem, 'id' | 'status'>[] {
103
+ const path = join(root, 'release', 'features.sqlite');
104
+ if (!existsSync(path)) return [];
105
+ const db = new Database(path, { readonly: true, strict: true });
106
+ try { return db.query('SELECT feature_id AS id, status FROM assignments WHERE version = ?').all(version) as Pick<ChecklistItem, 'id' | 'status'>[]; }
107
+ finally { db.close(); }
108
+ }
109
+ function journal(root: string): string { return join(root, 'seat-requests', 'requests.jsonl'); }
110
+ function readRequests(root: string): Array<{ key: string; status: string }> {
111
+ const path = journal(root);
112
+ return existsSync(path) ? readFileSync(path, 'utf8').split('\n').filter(Boolean).map(line => JSON.parse(line) as { key: string; status: string }) : [];
113
+ }
114
+ function stateFile(root: string, runId: string): string {
115
+ if (!/^[a-zA-Z0-9._-]+$/.test(runId) || runId === '.' || runId === '..') throw new Error('Invalid orchestrator runId');
116
+ return join(root, 'loop', 'orchestrator', `${runId}.json`);
117
+ }
118
+ function restorePrevious(root: string, day: string, mode: Mode): Pick<TickState, 'cards' | 'cells'> | null {
119
+ const dir = join(root, 'loop', 'orchestrator');
120
+ if (!existsSync(dir)) return null;
121
+ const states = readdirSync(dir).filter(name => /^[a-zA-Z0-9._-]+\.json$/.test(name))
122
+ .map(name => ({ name, state: JSON.parse(readFileSync(join(dir, name), 'utf8')) as TickState }))
123
+ .filter(({ state }) => state.day === day && state.mode === mode && state.window === '08' && state.nodes.delegate === 'ok');
124
+ if (!states.length) return null;
125
+ states.sort((a, b) => a.name.localeCompare(b.name));
126
+ const cards = new Map<string, TaskCard>();
127
+ const cells = new Map<string, Cell>();
128
+ for (const { state } of states) {
129
+ for (const card of state.cards) cards.set(card.id, card);
130
+ for (const cell of state.cells) cells.set(`${cell.cardId}:${cell.id}`, cell);
131
+ }
132
+ return { cards: [...cards.values()], cells: [...cells.values()] };
133
+ }
134
+ function initial(runId: string, window: Window, mode: Mode, day: string): TickState {
135
+ return { runId, window, mode, day, cards: [], skippedCards: [], cells: [], placed: 0, delegated: 0, wouldDelegate: 0,
136
+ reconciled: { reached: 0, progressing: 0, blocked: 0, unknown: 0 }, nodes: {}, missing: [] };
137
+ }
138
+ function graphRunId(): string | undefined {
139
+ const context = process.env.ELANOUS_GRAPH_CONTEXT;
140
+ if (!context) return undefined;
141
+ try {
142
+ const value = JSON.parse(readFileSync(context, 'utf8')) as { graphId?: string; runId?: string };
143
+ if (value.graphId !== 'orchestrator' || typeof value.runId !== 'string') throw new Error('invalid orchestrator graph context');
144
+ return value.runId;
145
+ } catch (error) { throw new Error(`orchestrator graph context unavailable: ${String(error)}`); }
146
+ }
147
+ async function optionalAdapter<T>(path: string, name: string): Promise<T | undefined> {
148
+ try { return (await import(path) as Record<string, unknown>)[name] as T | undefined; }
149
+ catch (error) {
150
+ if ((error as { code?: string }).code === 'ERR_MODULE_NOT_FOUND' || (error as { code?: string }).code === 'MODULE_NOT_FOUND') return undefined;
151
+ throw error;
152
+ }
153
+ }
154
+
155
+ /** A graph command invokes one node; only the per-run file carries data across processes. */
156
+ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Promise<TickState> {
157
+ const now = deps.now ?? new Date();
158
+ const window = deps.window ?? kstWindow(now);
159
+ const root = deps.root ?? effectiveInstanceRoot();
160
+ const runId = deps.runId ?? graphRunId() ?? `${kstDay(now)}-${window}`;
161
+ const path = stateFile(root, runId);
162
+ const configured = deps.mode ?? getUserConfig().loops?.orchestrator?.mode;
163
+ const mode: Mode = configured === 'off' || configured === 'live' ? configured : 'shadow';
164
+ const state: TickState = existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) as TickState : initial(runId, window, mode, kstDay(now));
165
+ if (state.window !== window || state.mode !== mode) throw new Error('orchestrator run window/mode changed');
166
+ const predecessor: Partial<Record<Node, Node>> = { split: 'intake', place: 'split', delegate: 'place', reconcile: 'delegate', report: 'reconcile' };
167
+ const preceding = predecessor[node];
168
+ if (preceding && !state.nodes[preceding]) throw new Error(`orchestrator ${node}: missing ${preceding} result`);
169
+ const loadAdapter = deps.loadAdapter ?? optionalAdapter;
170
+ const adapter = async <T>(module: string, name: string): Promise<T | undefined> => {
171
+ const value = await loadAdapter(module, name);
172
+ return typeof value === 'function' ? value as T : undefined;
173
+ };
174
+ const observe = (event: string, reason = '', count = 0, targetLoopId?: string): void => {
175
+ const data = { loopId: 'orchestrator', runId, window, node, reason, count, ...(targetLoopId ? { targetLoopId } : {}) };
176
+ try { (deps.observe ?? ((name, payload) => debug.log('loop.orchestrator', name, payload)))(event, data); }
177
+ catch { /* observation must not change the run */ }
178
+ };
179
+ if (mode !== 'off' && activeNode(node, window) && state.nodes[node] !== 'ok') {
180
+ if (node === 'intake') {
181
+ if (mode === 'live' && deps.cards) { state.cards = wishCards(deps.cards()); state.skippedCards = []; }
182
+ else {
183
+ const snapshot = readCardSnapshot(root);
184
+ state.cards = wishCards(snapshot.cards);
185
+ state.skippedCards = snapshot.skippedCards;
186
+ state.skippedCards.forEach(() => observe('exchange', 'card-unreadable', 1));
187
+ }
188
+ observe('exchange', 'wish-cards', state.cards.length);
189
+ } else if (node === 'split') {
190
+ // Shadow also asks the splitter (it only proposes cells); only live records `orch:split` on the card.
191
+ const split = deps.split ?? await adapter<SplitAdapter>('../../flow-loop/split.js', 'splitCard');
192
+ if (!split) { state.missing.push('flow1a-absent'); observe('node-missing', 'flow1a-absent', state.cards.length); }
193
+ state.cells = [];
194
+ for (const card of state.cards) {
195
+ const cells = split ? await split(card, { shadow: mode === 'shadow' }) : [{ id: card.id, title: card.title }];
196
+ for (const cell of cells) {
197
+ if (!cell.id?.trim() || !cell.title?.trim()) throw new Error('invalid split cell');
198
+ state.cells.push({ cardId: card.id, id: cell.id, title: cell.title, origin: split ? 'flow1a' : 'candidate', ...(cell.seat ? { seat: cell.seat } : {}), ...(cell.host !== undefined ? { host: cell.host } : {}) });
199
+ }
200
+ if (split && cells.length && mode === 'live') {
201
+ const store = new CardStore(root);
202
+ try {
203
+ store.appendSection(card.id, { key: 'orch:split', owner: 'orchestrator', content: JSON.stringify(cells) });
204
+ } finally { store.close(); }
205
+ }
206
+ }
207
+ } else if (node === 'place') {
208
+ const place = deps.placeCell ?? await adapter<PlaceAdapter>('../../release-loop/placement.js', 'placeCell');
209
+ const available = typeof place === 'function';
210
+ if (!available) { state.missing.push('relplan1-absent'); observe('node-missing', 'relplan1-absent', state.cells.length); }
211
+ else if (mode === 'shadow') observe('exchange', 'relplan1-shadow-not-invoked', state.cells.length);
212
+ if (mode === 'live' && available) for (const cell of state.cells) {
213
+ if (cell.origin !== 'flow1a') continue;
214
+ // TC review must-fix ②: map to the placer's required shape; a cell without an owner is unplaced, not a crash.
215
+ if (!cell.seat) { state.unplaced = (state.unplaced ?? 0) + 1; observe('exchange', 'unplaced-no-owner', 1); continue; }
216
+ const placed = await place({ id: cell.id, title: cell.title, owner: cell.seat, priority: cell.priority ?? 'P2', predecessors: cell.predecessors ?? [] });
217
+ if (validVersion(placed?.version)) {
218
+ cell.version = placed.version;
219
+ state.placed++;
220
+ }
221
+ }
222
+ } else if (node === 'delegate') {
223
+ const path = journal(root);
224
+ const keys = new Set(readRequests(root).map(row => row.key));
225
+ for (const cell of state.cells) {
226
+ if (cell.origin !== 'flow1a' || !cell.seat) continue;
227
+ const targetLoopId = SEAT_LOOP_IDS[cell.seat];
228
+ if (!targetLoopId) continue;
229
+ const key = `orch:${cell.cardId}:${cell.id}`;
230
+ if (keys.has(key)) continue;
231
+ // Shadow shows what it would hand over even before a release is chosen (placement is not invoked in shadow).
232
+ if (mode === 'shadow') { keys.add(key); state.wouldDelegate++; observe('would-delegate', validVersion(cell.version) ? key : `${key} (unplaced)`, 1, targetLoopId); continue; }
233
+ if (!validVersion(cell.version)) continue;
234
+ keys.add(key);
235
+ mkdirSync(dirname(path), { recursive: true });
236
+ // TC review must-fix ①: the seat-requests journal contract needs receiptId, and appends are serialized under the
237
+ // same lock as the traffic node — an invalid row stops request intake for every seat.
238
+ const appended = withFileLockSync(`${path}.lock`, () => {
239
+ const existing = existsSync(path) ? readFileSync(path, 'utf8') : '';
240
+ if (existing.split('\n').some(line => {
241
+ if (!line) return false;
242
+ try { return (JSON.parse(line) as { key?: string }).key === key; } catch { throw new Error('invalid seat request journal'); }
243
+ })) return false;
244
+ appendFileSync(path, `${JSON.stringify({ key, receiptId: key, seat: cell.seat, ...(cell.host !== undefined ? { host: cell.host } : {}), loopId: targetLoopId, text: cell.title, status: 'queued', queuedAt: now.toISOString(), source: 'orchestrator', cell: cell.id, version: cell.version })}\n`);
245
+ return true;
246
+ });
247
+ if (!appended) continue;
248
+ state.delegated++;
249
+ observe('exchange', 'queued', 1, targetLoopId);
250
+ }
251
+ } else if (node === 'reconcile') {
252
+ if (window !== '08' && state.cells.length === 0) {
253
+ const previous = restorePrevious(root, state.day, mode);
254
+ if (previous) { state.cards = previous.cards; state.cells = previous.cells; }
255
+ }
256
+ const requests = readRequests(root);
257
+ state.reconciled = reconciliation(state.cells, requests, mode === 'shadow' ? version => readChecklist(root, version) : (deps.checklist ?? (version => readChecklist(root, version))));
258
+ const from = new Date(`${state.day}T00:00:00+09:00`).getTime();
259
+ state.runSummaries = listSelfDevRuns(selfDevRunsDir(root))
260
+ .filter(run => run.updatedAt >= from && run.updatedAt < from + 86_400_000)
261
+ .map(run => runSummaryLine(run));
262
+ observe('exchange', 'reconciled', state.cells.length);
263
+ if (window === '18') {
264
+ const rebalance = await adapter<RebalanceAdapter>('../../release-loop/placement.js', 'rebalance');
265
+ if (!rebalance) { state.missing.push('k1b-absent'); observe('node-missing', 'k1b-absent', 1); }
266
+ else {
267
+ const versions = [...new Set(state.cells.map(cell => cell.version).filter(validVersion))];
268
+ if (mode === 'shadow') observe('exchange', 'rebalance-shadow-not-invoked', versions.length);
269
+ else {
270
+ state.rebalanced = 0;
271
+ for (const version of versions) state.rebalanced += (await rebalance(version)).length;
272
+ }
273
+ }
274
+ }
275
+ } else if (node === 'report') (deps.print ?? console.log)(reportLine(state));
276
+ state.nodes[node] = 'ok';
277
+ } else if (state.nodes[node] !== 'ok') {
278
+ // A rerun of a node that already completed keeps its result (review round 3): only never-run nodes become skipped.
279
+ state.nodes[node] = 'skipped';
280
+ }
281
+ observe('tick', state.nodes[node], state.cells.length);
282
+ mkdirSync(dirname(path), { recursive: true });
283
+ writeFileSync(path, JSON.stringify(state));
284
+ return state;
285
+ }
286
+
287
+ if (import.meta.main) {
288
+ const node = process.argv[2];
289
+ const index = process.argv.indexOf('--window');
290
+ const window = index < 0 ? undefined : process.argv[index + 1];
291
+ if (!['intake', 'split', 'place', 'delegate', 'reconcile', 'report'].includes(node ?? '') ||
292
+ (window !== undefined && !['08', '12', '18'].includes(window))) {
293
+ console.error('orchestrator: expected <intake|split|place|delegate|reconcile|report> [--window 08|12|18]');
294
+ process.exitCode = 2;
295
+ } else {
296
+ void (async () => {
297
+ try { await (await import('../../domains/standalone-log-sink.js')).registerStandaloneLogSink('orchestrator'); }
298
+ catch { /* local execution can still use the per-run file */ }
299
+ await runOrchestratorNode(node as Node, { window: window as Window | undefined });
300
+ })().catch(error => { console.error(`orchestrator ${node}: ${String(error)}`); process.exitCode = 1; });
301
+ }
302
+ }
@@ -0,0 +1,165 @@
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from 'node:fs';
2
+ import { dirname, isAbsolute, join, resolve, sep } from 'node:path';
3
+ import { debug } from '../../debug/log.js';
4
+ import { defaultListHarnessProcesses, type HarnessProcessRecord } from '../../harness/harness-cli-command.js';
5
+ import { effectiveInstanceRoot } from '../../instance/resolve.js';
6
+ import { devVersion, listChecklist, type ChecklistItem } from '../../release-loop/checklist.js';
7
+ import { withFileLockSync } from '../../storage/file-lock.js';
8
+ import { getUserConfig, ORCHESTRATOR_DEFAULTS, type OrchestratorLoopConfig, type OrchestratorSeat } from '../../user-config.js';
9
+ import { finishAdvice, measureFinish, type FinishAdvice, type FinishMetrics, type MeasureFinishDeps } from './finish-rate.js';
10
+
11
+ export const TRAFFIC_SEATS = ['OP', 'TC', 'MK', 'UX'] as const;
12
+ export type TrafficCell = Pick<ChecklistItem, 'id' | 'title' | 'owner' | 'status'>;
13
+ export type TrafficProcess = Pick<HarnessProcessRecord, 'command' | 'elapsedSeconds' | 'cwd'> & { seat: OrchestratorSeat | null };
14
+ export type TrafficSeatRow = { seat: OrchestratorSeat; running: number; cap: number; idleFor: number | null; idle: boolean; nextCell: TrafficCell | null };
15
+ export type TrafficResult = { seats: TrafficSeatRow[]; unassigned: number; now: Date; idleMinutes: number; finish?: { metrics: FinishMetrics; advice: FinishAdvice } };
16
+
17
+ /** Same bounded walk as the CTX1 hook: a marker at the git root is checked before stopping. */
18
+ export function seatOfTree(cwd: string | undefined, cfg: Pick<OrchestratorLoopConfig, 'seatTrees'>): OrchestratorSeat | null {
19
+ if (!cwd || !isAbsolute(cwd)) return null;
20
+ let dir = resolve(cwd);
21
+ for (let level = 0; level <= 6; level++) {
22
+ const marker = join(dir, '.claude', 'seat');
23
+ if (existsSync(marker)) {
24
+ const seat = readFileSync(marker, 'utf8').trim();
25
+ return TRAFFIC_SEATS.some(id => id === seat) ? seat as OrchestratorSeat : null;
26
+ }
27
+ if (existsSync(join(dir, '.git'))) break;
28
+ const parent = dirname(dir);
29
+ if (parent === dir) break;
30
+ dir = parent;
31
+ }
32
+ // Compare real paths: on macOS a process cwd under /var reports as /private/var (10-04 MK: UX trees under $TMPDIR
33
+ // never matched their configured /var/folders/... paths).
34
+ const path = realOrResolved(cwd);
35
+ for (const seat of TRAFFIC_SEATS) {
36
+ if (cfg.seatTrees[seat]?.some(tree => {
37
+ if (!isAbsolute(tree)) return false;
38
+ const root = realOrResolved(tree);
39
+ return path === root || path.startsWith(root + sep);
40
+ })) return seat;
41
+ }
42
+ return null;
43
+ }
44
+
45
+ function realOrResolved(path: string): string {
46
+ try { return realpathSync(path); } catch { return resolve(path); }
47
+ }
48
+
49
+ const IS_LAUNCH = /(?:^|\s)(?:\S*\/)?elanous\.mjs\s+(?:(?:--test|--config-dir\s+\S+)\s+)?harness\s+(?:ask|say)(?=\s|$)/;
50
+
51
+ /** Process-to-seat resolution occurs at the boundary; this decision does no IO. */
52
+ export function trafficTick({ processes, now, caps, lastLaunchAt, openCells, idleMinutes = 30 }: {
53
+ processes: readonly TrafficProcess[];
54
+ now: Date;
55
+ caps: Readonly<Record<OrchestratorSeat, number>>;
56
+ lastLaunchAt?: Partial<Record<OrchestratorSeat, Date | null>>;
57
+ openCells: readonly TrafficCell[];
58
+ idleMinutes?: number;
59
+ }): TrafficResult {
60
+ const launches = processes.filter(process => IS_LAUNCH.test(process.command));
61
+ const seats = TRAFFIC_SEATS.map((seat): TrafficSeatRow => {
62
+ const owned = launches.filter(process => process.seat === seat);
63
+ const latest = owned.reduce<number | null>((current, process) => {
64
+ if (!Number.isFinite(process.elapsedSeconds) || process.elapsedSeconds < 0) return current;
65
+ const start = now.getTime() - process.elapsedSeconds * 1000;
66
+ return current === null ? start : Math.max(current, start);
67
+ }, null);
68
+ const fallback = owned.length ? lastLaunchAt?.[seat]?.getTime() : undefined;
69
+ const last = latest ?? (fallback !== undefined && Number.isFinite(fallback) ? fallback : null);
70
+ const idleFor = last === null ? null : Math.max(0, Math.floor((now.getTime() - last) / 60_000));
71
+ const idle = owned.length < caps[seat] && last !== null && now.getTime() - last > idleMinutes * 60_000;
72
+ return { seat, running: owned.length, cap: caps[seat], idleFor, idle,
73
+ nextCell: idle ? openCells.find(cell => cell.status !== 'done' && cell.owner?.split('/')[0] === seat) ?? null : null };
74
+ });
75
+ return { seats, unassigned: launches.filter(process => process.seat === null).length, now, idleMinutes };
76
+ }
77
+
78
+ export function withFinishAdvice(result: TrafficResult, deps: MeasureFinishDeps = {}): TrafficResult {
79
+ const metrics = measureFinish(deps, result.now);
80
+ const totalSlots = result.seats.reduce((total, row) => total + row.cap, 0);
81
+ return { ...result, finish: { metrics, advice: finishAdvice(metrics, totalSlots) } };
82
+ }
83
+
84
+ export type TrafficRequest = { key: string; receiptId: string; seat: OrchestratorSeat; text: string; status: 'queued'; queuedAt: string; source: 'orchestrator-traffic' };
85
+ export interface TrafficApplyDeps {
86
+ mode?: 'shadow' | 'live';
87
+ root?: string;
88
+ log?: (category: string, event: string, data: Record<string, unknown>) => void;
89
+ }
90
+
91
+ /** Only the live branch opens the journal. The lock serializes hourly-key checks across CLI processes. */
92
+ export function applyTraffic(result: TrafficResult, deps: TrafficApplyDeps = {}): number {
93
+ const log = deps.log ?? ((category: string, event: string, data: Record<string, unknown>) => debug.log(category, event, data));
94
+ const mode = deps.mode ?? 'shadow';
95
+ log('loop.orchestrator', 'tick', { mode, seats: result.seats.map(({ seat, running, cap, idleFor, idle }) => ({ seat, running, cap, idleFor, idle })), unassigned: result.unassigned });
96
+ if (result.finish) {
97
+ const { finishSlots, launchSlots, state, reasons } = result.finish.advice;
98
+ log('loop.orchestrator', 'would-rebalance', { finishSlots, launchSlots, state, reasons });
99
+ }
100
+ let queued = 0;
101
+ for (const row of result.seats) {
102
+ if (!row.idle) continue;
103
+ const data = { seat: row.seat, running: row.running, cap: row.cap, idleMinutes: row.idleFor, nextCell: row.nextCell };
104
+ if (mode !== 'live') {
105
+ log('loop.orchestrator', 'would-nudge', data);
106
+ continue;
107
+ }
108
+ if (!row.nextCell) {
109
+ log('loop.orchestrator', 'exchange', { ...data, outcome: 'no-cell' });
110
+ continue;
111
+ }
112
+ const path = join(deps.root ?? effectiveInstanceRoot(), 'seat-requests', 'requests.jsonl');
113
+ const key = `orch-traffic:${row.seat}:${result.now.toISOString().slice(0, 13)}`;
114
+ mkdirSync(dirname(path), { recursive: true });
115
+ const appended = withFileLockSync(`${path}.lock`, () => {
116
+ const previous = existsSync(path) ? readFileSync(path, 'utf8') : '';
117
+ if (previous.split('\n').some(line => {
118
+ if (!line) return false;
119
+ try { return (JSON.parse(line) as { key?: string }).key === key; } catch { throw new Error('invalid seat request journal'); }
120
+ })) return false;
121
+ const request: TrafficRequest = { key, receiptId: key, seat: row.seat,
122
+ text: `다음 칸 쏘라: ${row.nextCell!.id} ${row.nextCell!.title.slice(0, 60)} (지금 ${row.running}/${row.cap} · ${row.idleFor}분 놂)`,
123
+ status: 'queued', queuedAt: result.now.toISOString(), source: 'orchestrator-traffic' };
124
+ appendFileSync(path, JSON.stringify(request) + '\n');
125
+ return true;
126
+ });
127
+ if (appended) queued++;
128
+ log('loop.orchestrator', 'exchange', { ...data, key, outcome: appended ? 'queued' : 'duplicate' });
129
+ }
130
+ return queued;
131
+ }
132
+
133
+ export function trafficLine(result: TrafficResult, mode: 'shadow' | 'live'): string {
134
+ const parts = result.seats.map(row => `${row.seat} ${row.running}/${row.cap}`);
135
+ const idle = result.seats.filter(row => row.idle).map(row => row.seat);
136
+ const prefix = `traffic ${parts.join(' · ')} · idle=${idle.length ? idle.join(',') : '없음'} · mode=${mode}`;
137
+ if (!result.finish) return prefix;
138
+ const { metrics, advice } = result.finish;
139
+ const rate = (value: number | null) => value === null ? '?' : value.toFixed(2);
140
+ return `${prefix} · finish rate=${rate(metrics.landingRate)} stale=${metrics.staleDrafts ?? '?'} conflict=${rate(metrics.conflictRatio)} → finish=${advice.finishSlots}/${advice.finishSlots + advice.launchSlots}`;
141
+ }
142
+
143
+ if (import.meta.main) {
144
+ try {
145
+ const args = process.argv.slice(2);
146
+ const versionFlag = args.indexOf('--version');
147
+ const version = versionFlag < 0 ? devVersion().replace(/-dev\.\d+$/, '') : args[versionFlag + 1];
148
+ if (!version || version.startsWith('--')
149
+ || args.some((arg, i) => arg !== '--json' && i !== versionFlag && (versionFlag < 0 || i !== versionFlag + 1))) {
150
+ throw new Error('usage: traffic.ts [--version <판>] [--json]');
151
+ }
152
+ const cfg = getUserConfig().loops?.orchestrator ?? ORCHESTRATOR_DEFAULTS;
153
+ const observation = defaultListHarnessProcesses();
154
+ if (observation.status !== 'ok') throw new Error(`process observation ${observation.status}`);
155
+ const now = new Date();
156
+ const processes = observation.records.map(process => ({ ...process, seat: seatOfTree(process.cwdStatus === 'unknown' ? undefined : process.cwd, cfg) }));
157
+ const result = withFinishAdvice(trafficTick({ processes, now, caps: cfg.seatCaps, openCells: listChecklist(version).items }));
158
+ const queued = applyTraffic(result, { mode: cfg.trafficMode });
159
+ if (args.includes('--json')) console.log(JSON.stringify({ ...result, mode: cfg.trafficMode, queued, version }));
160
+ else console.log(trafficLine(result, cfg.trafficMode));
161
+ } catch (error) {
162
+ console.error(`traffic: ${error instanceof Error ? error.message : String(error)}`);
163
+ process.exitCode = 1;
164
+ }
165
+ }
@@ -0,0 +1,125 @@
1
+ import { readFileSync, realpathSync, statSync } from 'node:fs';
2
+ import { isAbsolute, relative, resolve, sep } from 'node:path';
3
+ import { parse as parseYaml } from 'yaml';
4
+ import { debug } from '../../debug/log.js';
5
+ import { loopAgentContractErrors } from '../contract/validate.js';
6
+
7
+ const contractSchema = JSON.parse(readFileSync(resolve(import.meta.dir, '../contract/loop-agent.schema.json'), 'utf8')) as { $id: string };
8
+ const allowedOverlayFields = new Set(['neighbors', 'cadence', 'grounding', 'budget']);
9
+ type RecordValue = Record<string, unknown>;
10
+
11
+ function record(value: unknown): value is RecordValue {
12
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
13
+ }
14
+
15
+ function schemaMajor(value: string): number | null {
16
+ const match = /^(?:https:\/\/elanous\.ai\/schemas\/loop-agent\/)?(\d+)\.\d+\.\d+$/.exec(value);
17
+ return match ? Number(match[1]) : null;
18
+ }
19
+
20
+ /** The package version is independent of the declared contract schema version. */
21
+ export function contractMajor(manifest: RecordValue): { ok: boolean; error?: string } {
22
+ const declared = manifest.schema ?? manifest.$schema ?? contractSchema.$id;
23
+ const expected = schemaMajor(contractSchema.$id);
24
+ const actual = typeof declared === 'string' ? schemaMajor(declared) : null;
25
+ const other = manifest.schema !== undefined && manifest.$schema !== undefined ? manifest.$schema : undefined;
26
+ if (actual !== null && expected !== null && actual === expected &&
27
+ (other === undefined || typeof other === 'string' && schemaMajor(other) === expected)) return { ok: true };
28
+ return { ok: false, error: `contract major mismatch: declared ${String(declared)}${other === undefined ? '' : ` / ${String(other)}`}, expected ${contractSchema.$id}` };
29
+ }
30
+
31
+ /** Replace only allowlisted top-level fields; refused fields never enter the merged manifest. */
32
+ export function mergeOverlay(manifest: RecordValue, overlay: unknown): { merged: RecordValue; refused: string[] } {
33
+ const merged = { ...manifest };
34
+ const refused: string[] = [];
35
+ if (!record(overlay)) return { merged, refused: ['overlay'] };
36
+ for (const [field, value] of Object.entries(overlay)) {
37
+ if (!allowedOverlayFields.has(field)) refused.push(field);
38
+ else merged[field] = value;
39
+ }
40
+ return { merged, refused };
41
+ }
42
+
43
+ function inside(root: string, path: string): boolean {
44
+ const part = relative(root, path);
45
+ return part !== '..' && !part.startsWith(`..${sep}`) && !isAbsolute(part);
46
+ }
47
+
48
+ function readReference(root: string, name: string, path: unknown, errors: string[]): void {
49
+ if (typeof path !== 'string' || !path || isAbsolute(path) || /^[a-zA-Z]:[\\/]/.test(path) || path.split(/[\\/]/).includes('..')) {
50
+ errors.push(`${name} path escape: ${String(path)}`);
51
+ return;
52
+ }
53
+ const candidate = resolve(root, path);
54
+ if (!inside(root, candidate)) {
55
+ errors.push(`${name} path escape: ${path}`);
56
+ return;
57
+ }
58
+ try {
59
+ const actual = realpathSync(candidate);
60
+ if (!inside(root, actual)) errors.push(`${name} path escape: ${path}`);
61
+ else if (!statSync(actual).isFile()) errors.push(`${name} file missing: ${path}`);
62
+ } catch {
63
+ errors.push(`${name} file missing: ${path}`);
64
+ }
65
+ }
66
+
67
+ export function readLoopPackage(dir: string): { manifest: RecordValue | null; overlay?: unknown; errors: string[] } {
68
+ const errors: string[] = [];
69
+ let manifest: RecordValue | null = null;
70
+ let overlay: unknown;
71
+ const root = realpathSync(dir);
72
+ if (!statSync(root).isDirectory()) throw new Error(`${dir} is not a directory`);
73
+ const readYaml = (name: string): unknown => {
74
+ const path = resolve(root, name);
75
+ const actual = realpathSync(path);
76
+ if (!inside(root, actual) || !statSync(actual).isFile()) throw new Error(`${name} path escape or not a file`);
77
+ return parseYaml(readFileSync(actual, 'utf8'));
78
+ };
79
+ try {
80
+ const parsed = readYaml('loop.yaml');
81
+ if (!record(parsed)) errors.push('loop.yaml must contain a manifest object');
82
+ else manifest = parsed;
83
+ } catch (error) { errors.push(`loop.yaml: ${error instanceof Error ? error.message : String(error)}`); }
84
+ try { overlay = readYaml('overlay.yaml'); }
85
+ catch (error) {
86
+ if (!(record(error) && error.code === 'ENOENT')) errors.push(`overlay.yaml: ${error instanceof Error ? error.message : String(error)}`);
87
+ }
88
+ if (manifest) {
89
+ readReference(root, 'graph', manifest.graph, errors);
90
+ readReference(root, 'role', manifest.role, errors);
91
+ }
92
+ return { manifest, overlay, errors };
93
+ }
94
+
95
+ export function verifyLoopPackage(dir: string): {
96
+ ok: boolean; errors: string[]; refusedOverlayFields: string[]; merged: RecordValue | null;
97
+ } {
98
+ let manifest: RecordValue | null = null;
99
+ let overlay: unknown;
100
+ const errors: string[] = [];
101
+ try {
102
+ const read = readLoopPackage(dir);
103
+ manifest = read.manifest;
104
+ overlay = read.overlay;
105
+ errors.push(...read.errors);
106
+ } catch (error) { errors.push(`package directory: ${error instanceof Error ? error.message : String(error)}`); }
107
+ let merged: RecordValue | null = null;
108
+ let refusedOverlayFields: string[] = [];
109
+ if (manifest) {
110
+ const major = contractMajor(manifest);
111
+ if (major.error) errors.push(major.error);
112
+ const result = overlay === undefined ? { merged: { ...manifest }, refused: [] } : mergeOverlay(manifest, overlay);
113
+ refusedOverlayFields = result.refused;
114
+ merged = result.merged;
115
+ if (refusedOverlayFields.length) errors.push(`overlay refused: ${refusedOverlayFields.join(', ')}`);
116
+ // Schema declaration is package metadata, not a field in the loop-agent contract.
117
+ const { schema: _schema, $schema: _dollarSchema, ...contractInput } = merged;
118
+ errors.push(...loopAgentContractErrors(contractInput));
119
+ }
120
+ const ok = errors.length === 0;
121
+ debug.log('loop.package', ok ? 'verified' : 'refused', {
122
+ id: manifest?.id, version: manifest?.version, errors: errors.length, refusedOverlayFields,
123
+ });
124
+ return { ok, errors, refusedOverlayFields, merged };
125
+ }