elanous 0.2.20 → 0.2.22

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 (456) 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/aBi6EyMtLsrl7J8eH4kF7/_buildManifest.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/088d8e8a.8069543d082bec12.js +160 -0
  5. package/apps/pwa/out/_next/static/chunks/1109.78a7a3288c0259d9.js +148 -0
  6. package/apps/pwa/out/_next/static/chunks/115.1c6690b8604f81c4.js +32 -0
  7. package/apps/pwa/out/_next/static/chunks/119fc678.6f591cdb84ca5791.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/1262.76a3f08e67c5f816.js +309 -0
  9. package/apps/pwa/out/_next/static/chunks/1307-08b716476f1d50af.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/1358d292.5c87f12cad6eaa91.js +122 -0
  11. package/apps/pwa/out/_next/static/chunks/1438.27a1a7b8f2308004.js +149 -0
  12. package/apps/pwa/out/_next/static/chunks/1442.02d85cd88f4c1eac.js +79 -0
  13. package/apps/pwa/out/_next/static/chunks/1501-1aad662d82ba43f7.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/1584.9e2dd53da6e6d4b1.js +24 -0
  15. package/apps/pwa/out/_next/static/chunks/1586.b989b0614afb77e6.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/1609.2ad355062870151f.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/1612-f6df0fc0eed7157b.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/1923.37f1d5fe7ebe3860.js +27 -0
  19. package/apps/pwa/out/_next/static/chunks/1948.5f35b39eef222a94.js +5 -0
  20. package/apps/pwa/out/_next/static/chunks/1993.3fd074a2a246234e.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/2140.d2daf9ee96e295d1.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/2212.0359851457473efc.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/224.583e562365a52f74.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/2328.0f37465646bafa7d.js +36 -0
  25. package/apps/pwa/out/_next/static/chunks/2372.bf71372184991937.js +286 -0
  26. package/apps/pwa/out/_next/static/chunks/{2489-ff55a35b202c0f94.js → 2489-d238d3e2eb5b2bf5.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/2549-a8515c86f9e8cb96.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/2655.72cc6a97276c5acc.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/2743-ef6e8c3ced74cce5.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/2840-5458ad50c49cac6a.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/2856.f75d5baef7f49b74.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/3344.0de4b0ec5a454b7a.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/335.a13f339e97893437.js +94 -0
  34. package/apps/pwa/out/_next/static/chunks/3394.646348f34b2500be.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/3443.6276909b53d72d53.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/3495.abd4254d808bf2dc.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/3522-1de1319d028ac89c.js +25 -0
  38. package/apps/pwa/out/_next/static/chunks/3721.ba5b82db0d9a5572.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/3857-5985236d9307f676.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/3919.9ff7dc25afefee33.js +74 -0
  41. package/apps/pwa/out/_next/static/chunks/3998.bd9a69d9b50e9bd2.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/4003.ab6db1dc266576c9.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/4216.e8ac94b9fe6b96ab.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/4327.c44dc2b55cb43ee5.js +34 -0
  45. package/apps/pwa/out/_next/static/chunks/4401.94257d497b6d26bc.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/4559.17ca2cdbaa8d414f.js +115 -0
  47. package/apps/pwa/out/_next/static/chunks/4588.ad3364cb7fb97ada.js +86 -0
  48. package/apps/pwa/out/_next/static/chunks/4706.97e734bd4c9816ca.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/4982.6e44352f751b9a1b.js +77 -0
  50. package/apps/pwa/out/_next/static/chunks/5342.49c5c5a530e62b50.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/5360-08a80abdf3a4da99.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/5431.66c58ab1327ee0b7.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/5633-d6852385000738ef.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/5717.bcfcb96e701b82e0.js +90 -0
  55. package/apps/pwa/out/_next/static/chunks/5755-30d102586789bf7f.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/5957.3f1a4012388dd962.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/5977-5b9e8a45a5b0c823.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/602.9bf02ac8e9502598.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/6020.3a1b347e2d2dfacc.js +8 -0
  60. package/apps/pwa/out/_next/static/chunks/{6158.af2e93b7adff709b.js → 6158.566c890adc06a459.js} +1 -1
  61. package/apps/pwa/out/_next/static/chunks/6375.fce1d174ba6d0324.js +64 -0
  62. package/apps/pwa/out/_next/static/chunks/6546.92a828aef441271f.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/658.957fdb0a8c6c6c8e.js +287 -0
  64. package/apps/pwa/out/_next/static/chunks/6706.323590aabb8b1293.js +226 -0
  65. package/apps/pwa/out/_next/static/chunks/6747.b67947f293a4c02d.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/6767.8e6c5f6e744fcd9f.js +38 -0
  67. package/apps/pwa/out/_next/static/chunks/6868.000b24a3045c5574.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/6946.bba74e9c96050f30.js +1 -0
  69. package/apps/pwa/out/_next/static/chunks/7033.a558f1b2f6912a53.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/7065.46029c26c53eac50.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/7087.6fae577d09249bc1.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/7175-350f4ce2873bc038.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/722.de8d9801e0f6d414.js +41 -0
  74. package/apps/pwa/out/_next/static/chunks/7497.57981e9d7a75a52a.js +24 -0
  75. package/apps/pwa/out/_next/static/chunks/7630.2b9a68659053ff4e.js +193 -0
  76. package/apps/pwa/out/_next/static/chunks/7672.4af122ad892117c7.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/771.6ab889a233e7dae7.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/7899.597af4a9996c907f.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/79b78388.663188133449d707.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/8096.f7da0776cfde6300.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/8194.484f51e273d9abd2.js +217 -0
  82. package/apps/pwa/out/_next/static/chunks/8769.815ca69879f608b4.js +104 -0
  83. package/apps/pwa/out/_next/static/chunks/8857-aa1cc4ac90af4d40.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/88729267.9c6c000157c83f2f.js +62 -0
  85. package/apps/pwa/out/_next/static/chunks/8f835b4f.048eeb02299e3e12.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/9068.5f2764fccf86c1b2.js +93 -0
  87. package/apps/pwa/out/_next/static/chunks/9247-55bcfbb0e8057f59.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/9451.ebfb0dcc9c02250f.js +23 -0
  89. package/apps/pwa/out/_next/static/chunks/9559.e648369b1d57cf9a.js +62 -0
  90. package/apps/pwa/out/_next/static/chunks/9832.2ab2e818013574c7.js +1 -0
  91. package/apps/pwa/out/_next/static/chunks/a9f2b54d-40c16bd0efdb04b8.js +1 -0
  92. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-c855c24de7db74fe.js → page-88459e0903da2803.js} +1 -1
  93. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-85b316bb4235980b.js → page-d85c43f46f58716b.js} +1 -1
  94. package/apps/pwa/out/_next/static/chunks/app/board/{page-9b2b26a33cb2cd42.js → page-4e645600579ae952.js} +1 -1
  95. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-c9955fde67b1bcb5.js → page-87edbfd19117823b.js} +1 -1
  96. package/apps/pwa/out/_next/static/chunks/app/bots/page-7f0c2b7801ac6a54.js +1 -0
  97. package/apps/pwa/out/_next/static/chunks/app/ceo/page-bf1f030a9d1adffd.js +1 -0
  98. package/apps/pwa/out/_next/static/chunks/app/chat/{page-4ba4abe096b6c2a8.js → page-cbaf66b1e1804adf.js} +1 -1
  99. package/apps/pwa/out/_next/static/chunks/app/consult/{page-575d4a8b4f7becb2.js → page-80ed511e02c81412.js} +1 -1
  100. package/apps/pwa/out/_next/static/chunks/app/control/{page-f5d56ff5f45ad164.js → page-9f5610caaa06178c.js} +1 -1
  101. package/apps/pwa/out/_next/static/chunks/app/decisions/{page-2260e9eef0357421.js → page-ec8f3575e339de74.js} +1 -1
  102. package/apps/pwa/out/_next/static/chunks/app/design-check/page-86d753e460f1a989.js +1 -0
  103. package/apps/pwa/out/_next/static/chunks/app/editor/page-4166a7d51cf0a57d.js +1 -0
  104. package/apps/pwa/out/_next/static/chunks/app/exec/{page-466bf0ee16ad1432.js → page-2a19c5cd3b562580.js} +1 -1
  105. package/apps/pwa/out/_next/static/chunks/app/field/{page-a23bc71713bcc08f.js → page-b6a09cde2e07521a.js} +1 -1
  106. package/apps/pwa/out/_next/static/chunks/app/inside/page-f7eae1518ca0c353.js +1 -0
  107. package/apps/pwa/out/_next/static/chunks/app/intake/page-456a8573ad811381.js +1 -0
  108. package/apps/pwa/out/_next/static/chunks/app/layout-8462d6a543cbb2d9.js +1 -0
  109. package/apps/pwa/out/_next/static/chunks/app/live/page-2974cdfb87c0f9c9.js +1 -0
  110. package/apps/pwa/out/_next/static/chunks/app/live-run/page-f927ffa25f3c86d1.js +1 -0
  111. package/apps/pwa/out/_next/static/chunks/app/loops/{page-35bfec2e631d3eae.js → page-abd86cd832aaa011.js} +1 -1
  112. package/apps/pwa/out/_next/static/chunks/app/market/page-79daf68f6256adaa.js +1 -0
  113. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-f9ccb1b1adbea14d.js → page-8c3f168fc3f91939.js} +1 -1
  114. package/apps/pwa/out/_next/static/chunks/app/morning/{page-ec102c9b8b691e37.js → page-786fb4504d0f2fa9.js} +1 -1
  115. package/apps/pwa/out/_next/static/chunks/app/not-found-75209f14597dcb8f.js +1 -0
  116. package/apps/pwa/out/_next/static/chunks/app/observatory/page-bf30affe3a85af7e.js +1 -0
  117. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-1187a44b7515434b.js +1 -0
  118. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-3d0910185a66ed54.js +1 -0
  119. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-f401ee1dd5a26e25.js +1 -0
  120. package/apps/pwa/out/_next/static/chunks/app/outputs/page-5dfe7604fd842784.js +1 -0
  121. package/apps/pwa/out/_next/static/chunks/app/page-76cf838050a8d681.js +1 -0
  122. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-db03ae3589a81c9e.js → page-7ec8e289b4c14df6.js} +1 -1
  123. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-4a6463b97f0ad94f.js → page-8c8ac17d52be2a79.js} +1 -1
  124. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-75bffecc9aa1004e.js → page-d359ab048d63a9d9.js} +1 -1
  125. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-b8b345c880bf78d1.js +1 -0
  126. package/apps/pwa/out/_next/static/chunks/app/settings/{page-31d5b875760bb62d.js → page-f151cc10ad026940.js} +1 -1
  127. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-9875f103057da433.js +1 -0
  128. package/apps/pwa/out/_next/static/chunks/app/setup/page-9789328af9a9aa62.js +1 -0
  129. package/apps/pwa/out/_next/static/chunks/app/share/{page-46d5201d2207a511.js → page-bf495a1e284f4e00.js} +1 -1
  130. package/apps/pwa/out/_next/static/chunks/app/showroom/page-a263d94bca4be22a.js +1 -0
  131. package/apps/pwa/out/_next/static/chunks/app/tasks/page-3962eab8ec58d16e.js +1 -0
  132. package/apps/pwa/out/_next/static/chunks/app/term/{page-cf0a4b4abead71ea.js → page-9abcfac07e02a542.js} +1 -1
  133. package/apps/pwa/out/_next/static/chunks/app/today/page-bee49f927c333abb.js +1 -0
  134. package/apps/pwa/out/_next/static/chunks/app/trace/page-bac2d56dcb02d955.js +1 -0
  135. package/apps/pwa/out/_next/static/chunks/app/vault/page-937cbcc13176295c.js +1 -0
  136. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d09495594f7f480d.js +1 -0
  137. package/apps/pwa/out/_next/static/chunks/app/workflows/{page-0acc8e198f7b0dfb.js → page-69cb93d1a2e57e0e.js} +1 -1
  138. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-34882c84f84021c5.js → page-72ec543928d67c3e.js} +1 -1
  139. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-82906eeda0186a7b.js +1 -0
  140. package/apps/pwa/out/_next/static/chunks/d70879db.c66c56a95a91efc6.js +66 -0
  141. package/apps/pwa/out/_next/static/chunks/f8626309.a4e5d362b1d55e59.js +1 -0
  142. package/apps/pwa/out/_next/static/chunks/webpack-f2b4016b9b981bc8.js +1 -0
  143. package/apps/pwa/out/_next/static/css/{d3427996da11e480.css → b9d8151e48998417.css} +1 -1
  144. package/apps/pwa/out/_next/static/css/f2bd5257e6af88d8.css +1 -0
  145. package/apps/pwa/out/approvals/index.html +1 -1
  146. package/apps/pwa/out/approvals/index.txt +14 -14
  147. package/apps/pwa/out/autopilot/index.html +1 -1
  148. package/apps/pwa/out/autopilot/index.txt +14 -14
  149. package/apps/pwa/out/board/index.html +1 -1
  150. package/apps/pwa/out/board/index.txt +14 -14
  151. package/apps/pwa/out/botlab/index.html +1 -1
  152. package/apps/pwa/out/botlab/index.txt +14 -14
  153. package/apps/pwa/out/bots/index.html +1 -1
  154. package/apps/pwa/out/bots/index.txt +14 -14
  155. package/apps/pwa/out/ceo/index.html +1 -1
  156. package/apps/pwa/out/ceo/index.txt +14 -14
  157. package/apps/pwa/out/chat/index.html +1 -1
  158. package/apps/pwa/out/chat/index.txt +14 -14
  159. package/apps/pwa/out/consult/index.html +1 -1
  160. package/apps/pwa/out/consult/index.txt +14 -14
  161. package/apps/pwa/out/control/index.html +1 -1
  162. package/apps/pwa/out/control/index.txt +14 -14
  163. package/apps/pwa/out/decisions/index.html +1 -1
  164. package/apps/pwa/out/decisions/index.txt +14 -14
  165. package/apps/pwa/out/design-check/index.html +1 -1
  166. package/apps/pwa/out/design-check/index.txt +14 -14
  167. package/apps/pwa/out/editor/index.html +1 -1
  168. package/apps/pwa/out/editor/index.txt +15 -15
  169. package/apps/pwa/out/exec/index.html +1 -1
  170. package/apps/pwa/out/exec/index.txt +14 -14
  171. package/apps/pwa/out/field/index.html +1 -1
  172. package/apps/pwa/out/field/index.txt +14 -14
  173. package/apps/pwa/out/healthz +1 -1
  174. package/apps/pwa/out/index.html +1 -1
  175. package/apps/pwa/out/index.txt +14 -14
  176. package/apps/pwa/out/inside/index.html +1 -1
  177. package/apps/pwa/out/inside/index.txt +15 -15
  178. package/apps/pwa/out/intake/index.html +1 -1
  179. package/apps/pwa/out/intake/index.txt +14 -14
  180. package/apps/pwa/out/live/index.html +1 -1
  181. package/apps/pwa/out/live/index.txt +14 -14
  182. package/apps/pwa/out/live-run/index.html +1 -0
  183. package/apps/pwa/out/live-run/index.txt +35 -0
  184. package/apps/pwa/out/loops/index.html +1 -1
  185. package/apps/pwa/out/loops/index.txt +14 -14
  186. package/apps/pwa/out/market/index.html +1 -1
  187. package/apps/pwa/out/market/index.txt +14 -14
  188. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  189. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  190. package/apps/pwa/out/morning/index.html +1 -1
  191. package/apps/pwa/out/morning/index.txt +14 -14
  192. package/apps/pwa/out/observatory/index.html +1 -1
  193. package/apps/pwa/out/observatory/index.txt +14 -14
  194. package/apps/pwa/out/ops/checklist/index.html +1 -1
  195. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  196. package/apps/pwa/out/ops/release/index.html +1 -1
  197. package/apps/pwa/out/ops/release/index.txt +14 -14
  198. package/apps/pwa/out/ops/seats/index.html +1 -1
  199. package/apps/pwa/out/ops/seats/index.txt +14 -14
  200. package/apps/pwa/out/outputs/index.html +1 -1
  201. package/apps/pwa/out/outputs/index.txt +14 -14
  202. package/apps/pwa/out/reflection/index.html +1 -1
  203. package/apps/pwa/out/reflection/index.txt +15 -15
  204. package/apps/pwa/out/scheduler/index.html +1 -1
  205. package/apps/pwa/out/scheduler/index.txt +14 -14
  206. package/apps/pwa/out/sessions/index.html +1 -1
  207. package/apps/pwa/out/sessions/index.txt +14 -14
  208. package/apps/pwa/out/settings/devices/index.html +1 -1
  209. package/apps/pwa/out/settings/devices/index.txt +14 -14
  210. package/apps/pwa/out/settings/index.html +1 -1
  211. package/apps/pwa/out/settings/index.txt +14 -14
  212. package/apps/pwa/out/setup/done/index.html +1 -1
  213. package/apps/pwa/out/setup/done/index.txt +14 -14
  214. package/apps/pwa/out/setup/index.html +1 -1
  215. package/apps/pwa/out/setup/index.txt +14 -14
  216. package/apps/pwa/out/share/index.html +1 -1
  217. package/apps/pwa/out/share/index.txt +14 -14
  218. package/apps/pwa/out/showroom/index.html +1 -1
  219. package/apps/pwa/out/showroom/index.txt +14 -14
  220. package/apps/pwa/out/tasks/index.html +1 -1
  221. package/apps/pwa/out/tasks/index.txt +14 -14
  222. package/apps/pwa/out/term/index.html +1 -1
  223. package/apps/pwa/out/term/index.txt +14 -14
  224. package/apps/pwa/out/today/index.html +1 -1
  225. package/apps/pwa/out/today/index.txt +14 -14
  226. package/apps/pwa/out/trace/index.html +1 -1
  227. package/apps/pwa/out/trace/index.txt +14 -14
  228. package/apps/pwa/out/vault/index.html +1 -1
  229. package/apps/pwa/out/vault/index.txt +14 -14
  230. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  231. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  232. package/apps/pwa/out/workflows/index.html +1 -1
  233. package/apps/pwa/out/workflows/index.txt +14 -14
  234. package/apps/pwa/out/workspace/index.html +1 -1
  235. package/apps/pwa/out/workspace/index.txt +14 -14
  236. package/apps/pwa/out/worktrees/index.html +1 -1
  237. package/apps/pwa/out/worktrees/index.txt +14 -14
  238. package/graphs/catalog/catalog.yaml +1 -1
  239. package/graphs/drafts/recipes.yaml +6 -6
  240. package/graphs/implement-loop.yaml +277 -5
  241. package/graphs/test-diet/recipes.yaml +3 -0
  242. package/graphs/test-diet/test-diet.yaml +3 -1
  243. package/package.json +1 -1
  244. package/scripts/coord-channel-watch.sh +31 -4
  245. package/scripts/docs-lint.ts +4 -1
  246. package/scripts/f-session-monitor.ts +69 -0
  247. package/scripts/hq/standby-snapshot.ts +7 -3
  248. package/scripts/knowledge-compress.ts +11 -0
  249. package/scripts/marketing/generate-ax-brief.ts +18 -1
  250. package/scripts/measure-nl-routing-corpus.ts +11 -3
  251. package/scripts/nightly-docops.ts +90 -3
  252. package/scripts/release-loop/gate-node.ts +5 -5
  253. package/scripts/release-loop/shard-plan.ts +50 -5
  254. package/scripts/research-coo-admin-dates.ts +39 -0
  255. package/scripts/rhythm/daily.ts +2 -1
  256. package/scripts/skills/public-check.ts +7 -2
  257. package/scripts/smart-merge/resync-shadow.ts +5 -3
  258. package/scripts/test-diet/lib.ts +81 -1
  259. package/scripts/test-diet/node.ts +48 -2
  260. package/scripts/verify-coo-admin-evidence.ts +83 -0
  261. package/src/acp/client.ts +4 -2
  262. package/src/agent/run-context.ts +4 -0
  263. package/src/agent-env/profile.ts +25 -5
  264. package/src/agent-mission/driver.ts +26 -12
  265. package/src/agent-mission/supervisor-metrics.ts +17 -2
  266. package/src/agent-substrate/acp-reviewer.ts +12 -5
  267. package/src/agent-substrate/pr-reviewer.ts +11 -4
  268. package/src/agent-substrate/review-intent.ts +102 -3
  269. package/src/autopilot/build/llm-conflict-merge.ts +22 -8
  270. package/src/autopilot/pr-manager.ts +18 -8
  271. package/src/away/release-watch.ts +47 -8
  272. package/src/briefing/brief-items.ts +104 -11
  273. package/src/browser-cdp/client.ts +10 -5
  274. package/src/chat/index.ts +1 -0
  275. package/src/cli/brief-cli.ts +20 -2
  276. package/src/cli/config-classify.ts +83 -0
  277. package/src/cli/control-cli.ts +14 -0
  278. package/src/cli/decisions-cli.ts +26 -16
  279. package/src/cli/logs-model-input.ts +1 -1
  280. package/src/cli/ops-cli.ts +11 -0
  281. package/src/cli/ops-overview-cli.ts +50 -0
  282. package/src/cli/pod-cli.ts +1 -0
  283. package/src/cli/pr-cli.ts +65 -8
  284. package/src/cli/release-cli.ts +21 -4
  285. package/src/cli/tasks-cli.ts +214 -1
  286. package/src/connectors/types.ts +6 -6
  287. package/src/context-bus/context-now-surfaces.ts +18 -0
  288. package/src/context-bus/coord-events.ts +32 -4
  289. package/src/context-bus/long-term-memory.ts +23 -6
  290. package/src/context-bus/nightly-condense.ts +169 -21
  291. package/src/control/control-status.ts +87 -0
  292. package/src/control-plane/light-member.ts +5 -3
  293. package/src/control-plane/member.ts +172 -7
  294. package/src/core-turn/run-goal-loop.ts +22 -1
  295. package/src/dashboard/index.ts +40 -4
  296. package/src/dashboard/slash-runtime/dashboard-handlers.ts +9 -0
  297. package/src/decisions/decision-cards.ts +20 -1
  298. package/src/decisions/decision-ledger.ts +11 -8
  299. package/src/decisions/web-push-decision-cards.ts +19 -3
  300. package/src/discord-self-message.ts +2 -2
  301. package/src/domains/coo-admin-dates.evidence.json +876 -0
  302. package/src/domains/coo-admin-tool.ts +48 -14
  303. package/src/domains/ops-status.ts +53 -0
  304. package/src/domains/outbound-alert.ts +24 -1
  305. package/src/esc-abort-gate.ts +3 -1
  306. package/src/exec-requests/planner.ts +6 -0
  307. package/src/graph-kinds/registry.ts +6 -6
  308. package/src/graph-runner/graph-cli.ts +91 -2
  309. package/src/graph-runner/graph-run-api.ts +20 -0
  310. package/src/graph-runner/graph-tick.ts +73 -12
  311. package/src/graph-runner/runner.ts +43 -11
  312. package/src/graph-unify/wf-to-graph.ts +79 -0
  313. package/src/graph-wizard/archetypes.ts +88 -0
  314. package/src/graph-wizard/generate.ts +577 -0
  315. package/src/graph-wizard/saved-steps.ts +120 -0
  316. package/src/graph-wizard/steps.ts +206 -0
  317. package/src/harness/harness-cli-command.ts +186 -32
  318. package/src/harness/harness-queue.ts +58 -14
  319. package/src/harness/launch-advice.ts +23 -0
  320. package/src/index.ts +43 -11
  321. package/src/intake-plane/digest.ts +18 -11
  322. package/src/knowledge/conclusion-compress.ts +93 -0
  323. package/src/knowledge/know-find.ts +9 -2
  324. package/src/knowledge/query.ts +20 -0
  325. package/src/llm.ts +26 -16
  326. package/src/loops/neighbor-runtime.ts +88 -0
  327. package/src/loops/orchestrator/author-depth.ts +29 -11
  328. package/src/loops/orchestrator/overlap.ts +7 -1
  329. package/src/loops/orchestrator/resync-shadow.ts +1 -1
  330. package/src/loops/orchestrator/tick.ts +35 -2
  331. package/src/loops/orchestrator/traffic.ts +2 -0
  332. package/src/loops/registry.ts +5 -0
  333. package/src/market/publish.ts +17 -1
  334. package/src/maturity/feature-maturity.ts +4 -2
  335. package/src/mss/logging/redaction.ts +17 -0
  336. package/src/nexus/api/draft-metrics.ts +128 -0
  337. package/src/nexus/api/graph-peer-edit.ts +77 -0
  338. package/src/nexus/api/graph-wizard-api.ts +61 -0
  339. package/src/nexus/api/graphs-api.ts +193 -17
  340. package/src/nexus/api/harness-traversal.ts +205 -0
  341. package/src/nexus/api/http-server.ts +122 -6
  342. package/src/nexus/api/plugins-market.ts +24 -0
  343. package/src/nexus/index.ts +3 -2
  344. package/src/nexus/outbound/router.ts +28 -8
  345. package/src/oauth/claude-pty-maturity.ts +24 -0
  346. package/src/oauth/fallback-chain.ts +27 -14
  347. package/src/onboarding/first-run.ts +3 -0
  348. package/src/ops/overview-collect.ts +345 -0
  349. package/src/ops/overview-seat-era.ts +90 -0
  350. package/src/ops/overview.ts +223 -0
  351. package/src/plugins/maker/market-bundle.ts +9 -0
  352. package/src/plugins/maker/plugin-maker.ts +9 -3
  353. package/src/plugins/maker/wizard-generate.ts +73 -9
  354. package/src/policy/llm-policy.ts +11 -6
  355. package/src/release-loop/checklist-board.ts +27 -0
  356. package/src/release-loop/checklist.ts +38 -0
  357. package/src/release-loop/gate-shards.ts +7 -2
  358. package/src/seat-address/seat-address.ts +1 -0
  359. package/src/seat-loop/seat-loop.ts +174 -20
  360. package/src/seat-loop/seat-report.ts +29 -1
  361. package/src/self-dev/ask-launch-flow.ts +4 -2
  362. package/src/self-dev/ask-launch-io.ts +3 -1
  363. package/src/self-dev/dev-cli.ts +90 -8
  364. package/src/self-dev/dev-pipeline.ts +20 -1
  365. package/src/self-dev/draft-sweep.ts +45 -8
  366. package/src/self-dev/graph-journey-nodes.ts +157 -0
  367. package/src/self-dev/graph-journey-route.ts +105 -0
  368. package/src/self-dev/launch-preflight.ts +43 -3
  369. package/src/self-dev/orchestrate.ts +3 -0
  370. package/src/self-dev/release-path-guard.ts +7 -4
  371. package/src/self-dev/review-provider-fallback.ts +83 -0
  372. package/src/self-dev/run-supervisor.ts +5 -1
  373. package/src/self-implement/frozen-merges.ts +23 -0
  374. package/src/self-implement/goal-author.ts +14 -0
  375. package/src/self-implement/harness-policy-gates.ts +7 -5
  376. package/src/self-implement/heal-intake.ts +3 -0
  377. package/src/self-implement/host-regate.ts +28 -14
  378. package/src/self-implement/l8-merge-queue.ts +80 -8
  379. package/src/self-implement/orchestrator.ts +84 -28
  380. package/src/self-implement/seams.ts +8 -3
  381. package/src/self-implement/self-implement-cli.ts +4 -0
  382. package/src/self-implement/self-implement-runtime.ts +12 -0
  383. package/src/session-runtime/index.ts +5 -3
  384. package/src/shell-env-bootstrap.ts +35 -6
  385. package/src/steward/triage.ts +25 -8
  386. package/src/storage/file-lock.ts +33 -4
  387. package/src/task-agent/card-evidence.ts +6 -0
  388. package/src/task-agent/completion-evidence.ts +31 -3
  389. package/src/task-agent/cover.ts +61 -0
  390. package/src/task-agent/live-moves.ts +126 -3
  391. package/src/task-agent/parent-host.ts +315 -0
  392. package/src/task-agent/shadow.ts +19 -4
  393. package/src/task-agent/task-hand.ts +11 -2
  394. package/src/task-orchestrator/surfaces/pod-command-job.ts +41 -6
  395. package/src/task-orchestrator/surfaces/pod-gate-deps.ts +123 -0
  396. package/src/task-orchestrator/surfaces/pod-pool.ts +138 -15
  397. package/src/task-orchestrator/surfaces/self-implement-pod.ts +39 -6
  398. package/src/task-orchestrator/surfaces/self-implement.ts +7 -0
  399. package/src/telegram-commands.ts +8 -0
  400. package/src/telegram-project-command.ts +6 -0
  401. package/src/telegram.ts +2 -2
  402. package/src/tool-runtime/browser-runtime.ts +87 -3
  403. package/src/tool-runtime/index.ts +1 -0
  404. package/src/typecheck-ratchet.ts +2 -1
  405. package/src/user-config.ts +55 -8
  406. package/src/version/packed-revision.json +1 -1
  407. package/src/workflow-runtime/executor.ts +24 -2
  408. package/src/workflow-runtime/triggers/registry.ts +27 -0
  409. package/src/workflow-runtime/triggers/schedule-source.ts +30 -3
  410. package/apps/pwa/out/_next/static/Bam1YziK2iH9bN4K0utGk/_buildManifest.js +0 -1
  411. package/apps/pwa/out/_next/static/chunks/1307-77fad80cf54477c6.js +0 -1
  412. package/apps/pwa/out/_next/static/chunks/1612-f60c0c93ec773f81.js +0 -1
  413. package/apps/pwa/out/_next/static/chunks/1844-9ec4b1d12f6e80eb.js +0 -1
  414. package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +0 -1
  415. package/apps/pwa/out/_next/static/chunks/1948.15f356b15a625597.js +0 -5
  416. package/apps/pwa/out/_next/static/chunks/2116-6429c188eb846ab8.js +0 -1
  417. package/apps/pwa/out/_next/static/chunks/224-e7be8f762fe74986.js +0 -1
  418. package/apps/pwa/out/_next/static/chunks/2549-89c518a4c8ed4d00.js +0 -1
  419. package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +0 -1
  420. package/apps/pwa/out/_next/static/chunks/3857-995ea9f5ef33d71e.js +0 -1
  421. package/apps/pwa/out/_next/static/chunks/5360-be1f1d8f9fcc9755.js +0 -1
  422. package/apps/pwa/out/_next/static/chunks/5755-ea7ed88eb956f47b.js +0 -1
  423. package/apps/pwa/out/_next/static/chunks/5977-94ed6f78e9814b2b.js +0 -1
  424. package/apps/pwa/out/_next/static/chunks/7748-6e51bfe927019f5c.js +0 -25
  425. package/apps/pwa/out/_next/static/chunks/9247-c1753a059e7bac08.js +0 -1
  426. package/apps/pwa/out/_next/static/chunks/a9f2b54d-33c0689b70ed8cf3.js +0 -1
  427. package/apps/pwa/out/_next/static/chunks/app/bots/page-7a852956c74015a3.js +0 -1
  428. package/apps/pwa/out/_next/static/chunks/app/ceo/page-c5a5c621be0e04ae.js +0 -1
  429. package/apps/pwa/out/_next/static/chunks/app/design-check/page-7c058069c5e7fa63.js +0 -1
  430. package/apps/pwa/out/_next/static/chunks/app/editor/page-dc5da0b8a2edb8a4.js +0 -1
  431. package/apps/pwa/out/_next/static/chunks/app/inside/page-ed4a6f799f005d09.js +0 -1
  432. package/apps/pwa/out/_next/static/chunks/app/intake/page-9cd565005b93aca6.js +0 -1
  433. package/apps/pwa/out/_next/static/chunks/app/layout-55f0f40c6db95f15.js +0 -1
  434. package/apps/pwa/out/_next/static/chunks/app/live/page-474cb5888ea0e8f3.js +0 -1
  435. package/apps/pwa/out/_next/static/chunks/app/market/page-9ca21f9ad3208a61.js +0 -1
  436. package/apps/pwa/out/_next/static/chunks/app/not-found-a9c2cfa8dda922a5.js +0 -1
  437. package/apps/pwa/out/_next/static/chunks/app/observatory/page-6da35144878feb6d.js +0 -1
  438. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-f6055495e9ed7ea8.js +0 -1
  439. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-25204694fa9984a5.js +0 -1
  440. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a60e035f151d84d3.js +0 -1
  441. package/apps/pwa/out/_next/static/chunks/app/outputs/page-189c587f36874538.js +0 -1
  442. package/apps/pwa/out/_next/static/chunks/app/page-c56634bde705a65d.js +0 -1
  443. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-a8d2fe0c669e0de4.js +0 -1
  444. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-c26fa10aac6c21c4.js +0 -1
  445. package/apps/pwa/out/_next/static/chunks/app/setup/page-68793c6e10331d66.js +0 -1
  446. package/apps/pwa/out/_next/static/chunks/app/showroom/page-d81c07a7b4f618ae.js +0 -1
  447. package/apps/pwa/out/_next/static/chunks/app/tasks/page-be1c4e8697f22360.js +0 -1
  448. package/apps/pwa/out/_next/static/chunks/app/today/page-a4717e2c4bf7a28f.js +0 -1
  449. package/apps/pwa/out/_next/static/chunks/app/trace/page-0fd895fab9b62c1b.js +0 -1
  450. package/apps/pwa/out/_next/static/chunks/app/vault/page-8b6c1454a8e8f306.js +0 -1
  451. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-c6e4364d9de8f717.js +0 -1
  452. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-0506a1e0e4bdb2f5.js +0 -1
  453. package/apps/pwa/out/_next/static/chunks/webpack-c260571513bf43c2.js +0 -1
  454. package/apps/pwa/out/_next/static/css/c23d959d3ee15e57.css +0 -1
  455. /package/apps/pwa/out/_next/static/{Bam1YziK2iH9bN4K0utGk → aBi6EyMtLsrl7J8eH4kF7}/_ssgManifest.js +0 -0
  456. /package/skills/project-onboarding/{skill.md → SKILL.md} +0 -0
@@ -4,10 +4,23 @@ import { getUserConfig } from '../user-config.js';
4
4
  import { getSecretAsync } from '../nexus/config/secrets/index.js';
5
5
  import { fetchLinearProjectIssues, type LinearProjectIssue } from '../connectors/linear.js';
6
6
  import { formatElanousCard } from './elanous-card.js';
7
+ import recordedDateEvidence from './coo-admin-dates.evidence.json';
8
+
9
+ type CooDateEvidence = {
10
+ project: string;
11
+ verifiedIssues: Array<{
12
+ identifier: string;
13
+ title: string;
14
+ url: string;
15
+ officialDeadline: string | null;
16
+ preparationPeriod: string | { start: string } | null;
17
+ representativeActionDate: string | null;
18
+ }>;
19
+ };
7
20
 
8
21
  export const COO_ADMIN_SPEC: LLMToolSpec = {
9
22
  name: 'coo_admin',
10
- description: '읽기 전용 Linear COO 행정 조회 — «행정 뭐 남았어 · COO 할 일 · 마감 다가오는 행정» 질문에 사용. 외부 행정 프로젝트의 열린 일을 마감 순으로 읽는다.',
23
+ description: '읽기 전용 Linear COO 행정 조회 — «행정 뭐 남았어 · COO 할 일 · 마감 다가오는 행정» 질문에 사용. 외부 행정 프로젝트의 열린 일을 우선순위와 확인된 날짜 근거로 브리핑한다.',
11
24
  parameters: { type: 'object', properties: {}, required: [] },
12
25
  };
13
26
 
@@ -16,6 +29,7 @@ export interface CooAdminDeps {
16
29
  fetch?: typeof fetch;
17
30
  project?: string;
18
31
  now?: Date;
32
+ dateEvidence?: CooDateEvidence;
19
33
  }
20
34
 
21
35
  /** Reads the configured Linear project without changing any Linear issue. */
@@ -32,27 +46,47 @@ export async function dispatchCooAdmin(_args: Record<string, unknown>, deps: Coo
32
46
  const issues = await fetchLinearProjectIssues({ apiKey, project, fetch: deps.fetch ?? fetch });
33
47
  const today = deps.now ?? new Date();
34
48
  const todayUtc = Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate());
35
- const daysUntil = (date: string) => Math.round((Date.parse(`${date.slice(0, 10)}T00:00:00Z`) - todayUtc) / 86_400_000);
49
+ const daysUntil = (date: string) => Math.round((Date.parse(`${date}T00:00:00Z`) - todayUtc) / 86_400_000);
50
+ const dateEvidence = deps.dateEvidence ?? (recordedDateEvidence as CooDateEvidence);
51
+ const verifiedDate = (date: string | null | undefined): string | null => {
52
+ if (!date || !/^\d{4}-\d{2}-\d{2}$/.test(date)) return null;
53
+ const time = Date.parse(`${date}T00:00:00Z`);
54
+ return Number.isFinite(time) && new Date(time).toISOString().slice(0, 10) === date ? date : null;
55
+ };
56
+ const evidenceFor = (item: LinearProjectIssue) => dateEvidence.project === project
57
+ ? dateEvidence.verifiedIssues.find(row => row.identifier === item.identifier && row.url === item.url && row.title === item.title)
58
+ : undefined;
59
+ const deadlineFor = (item: LinearProjectIssue) => verifiedDate(evidenceFor(item)?.officialDeadline);
60
+ const rank = (item: LinearProjectIssue) => item.priority === 0 ? 5 : item.priority;
36
61
  issues.sort((a, b) => {
37
- const deadlineOrder = a.dueDate == null ? (b.dueDate == null ? 0 : 1)
38
- : b.dueDate == null ? -1 : daysUntil(a.dueDate) - daysUntil(b.dueDate);
39
- return deadlineOrder || (a.priority === 0 ? 5 : a.priority) - (b.priority === 0 ? 5 : b.priority);
62
+ const aDue = deadlineFor(a);
63
+ const bDue = deadlineFor(b);
64
+ return rank(a) - rank(b) || (aDue === null ? (bDue === null ? 0 : 1)
65
+ : bDue === null ? -1 : daysUntil(aDue) - daysUntil(bDue));
40
66
  });
41
67
  count = issues.length;
42
68
  const lines = issues.map((item: LinearProjectIssue) => {
43
- const days = item.dueDate ? daysUntil(item.dueDate) : null;
69
+ const evidence = evidenceFor(item);
70
+ const due = deadlineFor(item);
71
+ const days = due ? daysUntil(due) : null;
44
72
  if (days !== null && days < 0) overdue++;
45
- const deadline = days === null ? '마감 없음' : days < 0 ? `지남 ${-days}일` : `D-${days}`;
46
- return `${deadline} · ${item.title} · ${item.state.name} · ${item.assignee?.name ?? '미배정'}\n${item.url}`;
73
+ const deadline = due ? `확인된 마감일 ${due} (${days !== null && days < 0 ? `지남 ${-days}일` : `D-${days}`})` : '기한 미확인 · 확인 예정일 미정';
74
+ const preparation = verifiedDate(typeof evidence?.preparationPeriod === 'string'
75
+ ? evidence.preparationPeriod : evidence?.preparationPeriod?.start);
76
+ const representative = verifiedDate(evidence?.representativeActionDate);
77
+ const dates = [preparation && `준비 시작일 ${preparation}`, representative && `대표 손이 필요한 날 ${representative}`].filter(Boolean);
78
+ return `${deadline} · 우선순위 ${item.priority || '미지정'} · ${item.title} · ${item.state.name} · ${item.assignee?.name ?? '미배정'}${dates.length ? ` · ${dates.join(' · ')}` : ''}\n${item.url}`;
47
79
  });
48
80
  debug.log('coo.admin', 'read', { count, overdue, reason: 'ok' });
49
- const scope = issues.truncated ? '최대 250건 중 조회한 항목만 마감 순으로 표시 (이후 항목은 포함되지 않음)\n' : '';
81
+ const scope = issues.truncated ? '최대 250건 중 조회한 항목만 우선순위·확인된 마감 순으로 표시 (이후 항목은 포함되지 않음)\n' : '';
50
82
  const prose = lines.length ? `${scope}${lines.join('\n')}` : '남은 행정 0건';
51
- const card = formatElanousCard({ kind: 'coo-admin', items: issues.map(item => ({
52
- title: item.title, due: item.dueDate?.slice(0, 10) ?? null,
53
- daysLeft: item.dueDate ? daysUntil(item.dueDate) : null,
54
- state: item.state.name, owner: item.assignee?.name ?? '미배정', url: item.url,
55
- })) });
83
+ const card = formatElanousCard({ kind: 'coo-admin', items: issues.map(item => {
84
+ const due = deadlineFor(item);
85
+ return {
86
+ title: item.title, due, daysLeft: due ? daysUntil(due) : null,
87
+ state: item.state.name, owner: item.assignee?.name ?? '미배정', url: item.url,
88
+ };
89
+ }) });
56
90
  return `${prose}\n${card}`;
57
91
  } catch (error) {
58
92
  const reason = error instanceof Error ? error.message : String(error);
@@ -26,6 +26,7 @@ import { loadMandate, type TradeMandate } from './trade-mandate.js';
26
26
  import { debug, redactSecretText } from '../debug/log.js';
27
27
  import { LogStore, logsDbPath, type LogStoreRow } from '../mss/logging/log-store.js';
28
28
  import { detectRepeatedFailures, explainFailure } from './repeated-failure.js';
29
+ import { listLoops, loopRecentRuns, type LoopRun } from '../loops/registry.js';
29
30
 
30
31
  /** 예약 레지스트리 조회 관측 — 실패가 0으로 접히지 않게 남기는 창구. */
31
32
  export const OPS_STATUS_LOG_CATEGORY = 'ops.status';
@@ -34,6 +35,29 @@ export const OPS_SCHEDULES_LOOKUP_FAILED_EVENT = 'schedules.lookup-failed';
34
35
  /** 예약 레지스트리 행 조회 seam — 미지정 시 실 레지스트리 reader. */
35
36
  export type ListScheduleRows = () => ScheduleRow[];
36
37
 
38
+ /** Graph-loop health input — run history is newest first. */
39
+ export type GraphLoopRuns = () => Array<{
40
+ id: string;
41
+ enabled: boolean;
42
+ runs: Array<Pick<LoopRun, 'at' | 'status' | 'failedNodes'>>;
43
+ }>;
44
+
45
+ /** Bounded window per loop: a streak whose oldest read run is already >24h old is flagged; a longer
46
+ * all-failed history than this window can only under-report (never over-report) the streak length. */
47
+ export const GRAPH_LOOP_HEALTH_RUN_WINDOW = 50;
48
+
49
+ function defaultGraphLoopRuns(): ReturnType<GraphLoopRuns> {
50
+ const out: ReturnType<GraphLoopRuns> = [];
51
+ for (const loop of listLoops().filter(entry => entry.enabled)) {
52
+ // One unreadable run history must not hide the other loops.
53
+ try { out.push({ id: loop.id, enabled: loop.enabled, runs: loopRecentRuns(loop.id, {}, GRAPH_LOOP_HEALTH_RUN_WINDOW) }); }
54
+ catch (err) {
55
+ debug.log('ops.health', 'graph-loop-failure-skipped', { loop: loop.id, reason: redactSecretText(err instanceof Error ? err.message : String(err)) });
56
+ }
57
+ }
58
+ return out;
59
+ }
60
+
37
61
  /** 조회 seam(테스트) — 미지정 시 실 스토어. */
38
62
  export interface OpsStatusOpts {
39
63
  now?: Date;
@@ -47,6 +71,8 @@ export interface OpsStatusOpts {
47
71
  planDraftFor?: (missionId: string) => string;
48
72
  /** Health-only log reader seam; the default reads the current instance's last ten minutes read-only. */
49
73
  recentLogs?: () => LogStoreRow[];
74
+ /** Health-only graph-loop registry reader; injection avoids the real state directory. */
75
+ graphLoopRuns?: GraphLoopRuns;
50
76
  }
51
77
 
52
78
  export interface CountByStatus { [status: string]: number }
@@ -341,6 +367,33 @@ export function opsHealth(opts: OpsStatusOpts = {}): OpsHealthReport {
341
367
  }
342
368
  }
343
369
 
370
+ // Graph-run history is independent of ops_events loops and of log-store failures.
371
+ try {
372
+ const loops = (opts.graphLoopRuns ?? defaultGraphLoopRuns)();
373
+ const nowMs = (opts.now ?? new Date()).getTime();
374
+ let flagged = 0;
375
+ for (const loop of loops) {
376
+ if (!loop.enabled) continue;
377
+ const runs = [...loop.runs].sort((a, b) => Date.parse(b.at) - Date.parse(a.at));
378
+ const failures = [] as typeof runs;
379
+ for (const run of runs) {
380
+ if (run.status !== 'failed') break;
381
+ failures.push(run);
382
+ }
383
+ const first = failures.at(-1);
384
+ if (failures.length < 2 || !first || !Number.isFinite(Date.parse(first.at)) || nowMs - Date.parse(first.at) <= 24 * 3600_000) continue;
385
+ anomalies.push({
386
+ kind: 'repeated_failure', entity: `loop:${loop.id}`,
387
+ detail: `그래프 루프 ${loop.id} 연속 실패 ${failures.length}회 · ${first.at} 부터 · 마지막 실패 노드 ${failures[0]!.failedNodes.join(', ')}`,
388
+ since: first.at,
389
+ });
390
+ flagged++;
391
+ }
392
+ debug.log('ops.health', 'graph-loop-failure', { loops: loops.length, flagged });
393
+ } catch (err) {
394
+ debug.log('ops.health', 'graph-loop-failure-skipped', { reason: redactSecretText(err instanceof Error ? err.message : String(err)) });
395
+ }
396
+
344
397
  // Independent log check: an unreadable log store must not hide task/loop/schedule anomalies.
345
398
  try {
346
399
  const recentLogs = opts.recentLogs ?? (() => {
@@ -17,6 +17,7 @@ import { getUserConfig, type UserConfig } from '../user-config.js';
17
17
  import { resolveChannelBotToken } from '../channel-bot-token.js';
18
18
  import { explainReportRoute, isOperationalKind, kindRouteTarget, logKindRouteFallback, mainHomeTarget } from './telegram-kind-route.js';
19
19
  import { debug } from '../debug/log.js';
20
+ import type { AddBriefItem } from '../briefing/brief-items.js';
20
21
  import { registerLogStoreSink, setLogInstanceName } from '../mss/logging/log-store.js';
21
22
  import type { LogSink } from '../mss/logging/sink.js';
22
23
  import { resolveDaemonEndpoint } from '../nexus/daemon-endpoint.js';
@@ -633,6 +634,13 @@ export function deliver(text: string, kind = 'alert', onBot?: (bot: string) => v
633
634
  return false;
634
635
  }
635
636
 
637
+ type UndeliverableRecorder = (item: AddBriefItem) => void;
638
+ let undeliverableRecorder: UndeliverableRecorder | null = null;
639
+ /** Replace the ledger writer in tests; null restores the production ledger. */
640
+ export function setUndeliverableRecorder(recorder: UndeliverableRecorder | null): void {
641
+ undeliverableRecorder = recorder;
642
+ }
643
+
636
644
  /** OB8b — both paths failed: say so loudly, with the universe this process resolved, instead of a silent false.
637
645
  * The usual cause (10-01 · MK): an ad-hoc `bun -e` or a script from a source tree resolves a cwd-derived test
638
646
  * universe, so it finds neither the production daemon nor the production bot token. */
@@ -647,7 +655,22 @@ function reportUndeliverable(kind: string, daemonPath: DaemonPathClass | 'not-fo
647
655
  let route: ReturnType<typeof explainReportRoute> | null = null;
648
656
  try { if (cfg) route = explainReportRoute(cfg, kind); } catch { /* the report still goes out */ }
649
657
  ensureOutboundLogSink();
650
- try { debug.log('outbound.send', 'undeliverable', { kind: safeObservationLabel(kind), daemonPath, universe, root, route: route?.reason ?? 'unknown' }); } catch { /* fail-soft */ }
658
+ const reason = route?.reason ?? 'unknown';
659
+ try { debug.log('outbound.send', 'undeliverable', { kind: safeObservationLabel(kind), daemonPath, universe, root, route: reason }); } catch { /* fail-soft */ }
660
+ try {
661
+ const record = undeliverableRecorder ?? ((item: AddBriefItem) => {
662
+ const { BriefItemsLedger } = require('../briefing/brief-items.js') as typeof import('../briefing/brief-items.js');
663
+ new BriefItemsLedger().add(item);
664
+ });
665
+ record({
666
+ domain: '운영', priority: 'P1', source: 'outbound.undeliverable',
667
+ text: `미전달: kind ${kind} · 데몬 ${daemonPath} · 경로 ${reason} · 우주 ${universe}`,
668
+ evidence: 'elanous logs --category outbound.send --event undeliverable',
669
+ dedupeKey: `${kind} · ${reason}`, dedupeWithinMs: 60 * 60_000,
670
+ });
671
+ } catch (error) {
672
+ try { debug.log('outbound.send', 'undeliverable-record-failed', { kind: safeObservationLabel(kind), route: reason, error: error instanceof Error ? error.message : String(error) }); } catch { /* fail-soft */ }
673
+ }
651
674
  const hint = universe === 'prod'
652
675
  ? '운영 데몬이 떠 있는지 확인: elanous nexus show'
653
676
  : '운영으로 보내려면 설치본 elanous 로 실행하거나 ELANOUS_STATE_DIR=~/.elanous 와 --config-dir ~/.elanous 를 준다';
@@ -55,6 +55,8 @@ export interface EscAbortGateDeps {
55
55
  getTheme?: () => ThemeTokens | null | undefined;
56
56
  /** Names shown/logged while a turn abort request is waiting for running work to unwind. */
57
57
  getWaitingTargetNames?: () => readonly string[];
58
+ /** Only an identified foreground harness run may bypass the sub-agent confirmation. */
59
+ shouldAbortImmediately?: () => boolean;
58
60
  /** Called before abortTurnOnly so the UI can paint a visible pending state immediately. */
59
61
  onAbortPending?: (event: { running: number; repeat: number; targets: readonly string[] }) => void;
60
62
  /** Called when another Escape arrives after the first abort request. */
@@ -211,7 +213,7 @@ export function createEscAbortGate(deps: EscAbortGateDeps): EscAbortGate {
211
213
  return;
212
214
  }
213
215
  const running = deps.getRunningCount();
214
- if (running <= 0) {
216
+ if (running <= 0 || deps.shouldAbortImmediately?.()) {
215
217
  debug.log('esc.abort', 'abort-immediately', { running, repeat: Math.max(0, escapePresses - 1) });
216
218
  abortPendingTurn(running);
217
219
  return;
@@ -7,6 +7,7 @@ import { defaultGraphsDir } from '../self-implement/graph-templates.js';
7
7
  import { getElanousConfigDir } from '../elanous-config-dir.js';
8
8
  import type { ExecAttachment } from './store.js';
9
9
  import { debug } from '../debug/log.js';
10
+ import { peerEditRunGate } from '../nexus/api/graph-peer-edit.js';
10
11
  import { OUTPUT_KINDS, pickDefaultOutput, type OutputKind } from './default-outputs.js';
11
12
 
12
13
  export interface InstalledGraph {
@@ -105,6 +106,11 @@ async function coreAndMineGraphs(coreDir: string, mineDir: string): Promise<Inst
105
106
  const id = doc?.graph_id;
106
107
  if (typeof id !== 'string' || !/^[a-z0-9-]+$/.test(id) || seen.has(id)) continue;
107
108
  if (core && doc?.loop?.exec_request !== true) continue;
109
+ // W9c — a «mine» graph a peer changed is not runnable until the owner approves it (fail closed).
110
+ if (!core) {
111
+ const gate = peerEditRunGate(dir, id);
112
+ if (!gate.ok) { debug.log('exec-requests', 'skipped-peer-edit', { id, error: gate.error }); continue; }
113
+ }
108
114
  const inputs = doc?.loop?.inputs;
109
115
  const inputKeys = core && Array.isArray(inputs)
110
116
  ? inputs.filter((key): key is string => typeof key === 'string')
@@ -20,13 +20,13 @@ export interface NodeKindEntry {
20
20
  run?: NodeKindRun;
21
21
  }
22
22
 
23
- export const HARNESS_CORE_KINDS = ['agent', 'gate', 'git', 'judge', 'observe', 'hitl', 'subgraph'] as const;
23
+ export const HARNESS_CORE_KINDS = ['agent', 'gate', 'git', 'judge', 'observe', 'hitl', 'subgraph', 'prompt'] as const;
24
24
 
25
25
  // Workflow validation, the palette, and executor variant labels read the same keys.
26
26
  export const WORKFLOW_CORE_KINDS = ['prompt', 'bash', 'skill', 'cft', 'approval', 'if', 'switch', 'iteration', 'classify', 'extract', 'set', 'filter', 'template', 'http', 'showroom', 'task', 'scheduleTrigger', 'webhookTrigger', 'discordTrigger', 'telegramTrigger', 'manualTrigger', 'chatTrigger', 'subworkflow', 'knowledge'] as const;
27
27
 
28
28
  const descriptions: Record<GraphKind, readonly string[]> = {
29
- harness: ['Run an agent', 'Check an execution gate', 'Perform git operations', 'Judge an outcome', 'Observe a result', 'Request human input', 'Run a child graph'],
29
+ harness: ['Run an agent', 'Check an execution gate', 'Perform git operations', 'Judge an outcome', 'Observe a result', 'Request human input', 'Run a child graph', 'Run an LLM prompt'],
30
30
  workflow: ['Run an LLM prompt', 'Run a shell command', 'Invoke a skill', 'Invoke a CFT method', 'Request approval', 'Branch on a condition', 'Select a case', 'Iterate items', 'Classify input', 'Extract structured data', 'Assign variables', 'Filter items', 'Render a template', 'Make an HTTP request', 'Run a showroom', 'Create a task', 'Start on a schedule', 'Start on a webhook', 'Start on Discord', 'Start on Telegram', 'Start manually', 'Start on chat', 'Call another workflow', 'Search the vault for context'],
31
31
  };
32
32
 
@@ -93,21 +93,21 @@ export function getNodeKind(graph: GraphKind, kind: string): NodeKindEntry | und
93
93
 
94
94
  function isNodeKindRun(run: NodeKindRun): boolean {
95
95
  if (!run || typeof run !== 'object' || Array.isArray(run)) return false;
96
- if ('bash' in run) return typeof run.bash === 'string' && run.bash.trim().length > 0 && !('http' in run) && !('skill' in run) && !('mcp' in run);
96
+ if ('bash' in run) return typeof run.bash === 'string' && run.bash.trim().length > 0 && !('prompt' in run) && !('http' in run) && !('skill' in run) && !('mcp' in run);
97
97
  if ('http' in run) {
98
- return !('skill' in run) && !('mcp' in run)
98
+ return !('prompt' in run) && !('skill' in run) && !('mcp' in run)
99
99
  && typeof run.http === 'object' && run.http !== null
100
100
  && typeof run.http.method === 'string' && run.http.method.trim().length > 0
101
101
  && typeof run.http.url === 'string' && run.http.url.trim().length > 0
102
102
  && (run.http.body === undefined || typeof run.http.body === 'string');
103
103
  }
104
- if ('skill' in run) return !('mcp' in run)
104
+ if ('skill' in run) return !('prompt' in run) && !('mcp' in run)
105
105
  && typeof run.skill === 'object' && run.skill !== null
106
106
  && typeof run.skill.name === 'string' && run.skill.name.trim().length > 0
107
107
  && (run.skill.prompt === undefined || typeof run.skill.prompt === 'string');
108
108
  if ('mcp' in run) {
109
109
  const spec = run.mcp;
110
- return typeof spec === 'object' && spec !== null && !Array.isArray(spec)
110
+ return !('prompt' in run) && typeof spec === 'object' && spec !== null && !Array.isArray(spec)
111
111
  && typeof spec.server === 'string' && /^[a-z0-9_-]+$/.test(spec.server)
112
112
  && typeof spec.tool === 'string' && /^[a-z0-9_-]+$/.test(spec.tool)
113
113
  && (spec.args === undefined || (typeof spec.args === 'object' && spec.args !== null && !Array.isArray(spec.args)));
@@ -94,13 +94,72 @@ export function registerGraphCommands(program: Command, runsDeps: { root?: strin
94
94
  process.exitCode = 1;
95
95
  }
96
96
  });
97
+ graph.command('wizard <prompt>')
98
+ .description('말 한 줄로 그래프 YAML 을 짓고 같은 검증기 ⊕ graph run --dry-run 으로 확인한다(저장하지 않는다) — --from 이면 그 그래프를 고친다')
99
+ .option('--kind <kind>', 'harness | workflow', 'harness')
100
+ .option('--from <file>', '고칠 그래프 YAML (편집 경로)')
101
+ .option('--out <file>', 'YAML 을 이 파일에 쓰고, 하니스면 같은 폴더에 recipes.yaml 도 쓴다(있으면 덮지 않는다)')
102
+ .option('--json', '결과를 JSON 으로')
103
+ .option('--run', '생성된 그래프의 dry-run 경로를 보여 준다(--dry-run 과 함께만)')
104
+ .option('--dry-run', '--run 과 함께: 명령을 실행하지 않고 경로만 걷는다')
105
+ .action(async (prompt: string, opts: { kind: string; from?: string; out?: string; json?: boolean; run?: boolean; dryRun?: boolean }) => {
106
+ try {
107
+ if (opts.kind !== 'harness' && opts.kind !== 'workflow') throw new Error('--kind must be harness or workflow');
108
+ if (opts.run && !opts.dryRun) throw new Error('--run is only supported with --dry-run (run the saved graph with `elanous graph run <file>`)');
109
+ if (opts.run && opts.kind !== 'harness') throw new Error('--run --dry-run walks harness graphs only');
110
+ await (await import('../domains/standalone-log-sink.js')).registerStandaloneLogSink('graph');
111
+ const { generateGraphFromPrompt } = await import('../graph-wizard/generate.js');
112
+ const currentYaml = opts.from ? readFileSync(opts.from, 'utf8') : undefined;
113
+ const started = Date.now();
114
+ const result = await generateGraphFromPrompt({ prompt, kind: opts.kind, ...(currentYaml ? { currentYaml } : {}) });
115
+ const ms = Date.now() - started;
116
+ let recipesNote: string | undefined;
117
+ if (opts.out) {
118
+ const { writeFileSync } = await import('node:fs');
119
+ const { dirname } = await import('node:path');
120
+ writeFileSync(opts.out, result.yaml);
121
+ if (result.recipes) {
122
+ const recipesPath = join(dirname(resolve(opts.out)), 'recipes.yaml');
123
+ if (existsSync(recipesPath) && readFileSync(recipesPath, 'utf8') !== result.recipes) recipesNote = `recipes.yaml 이 이미 있어 덮지 않았다: ${recipesPath}`;
124
+ else { writeFileSync(recipesPath, result.recipes); recipesNote = `recipes ${recipesPath}`; }
125
+ }
126
+ }
127
+ if (opts.json) {
128
+ await writeStdoutJson(JSON.stringify({ ...result, ms, ...(recipesNote ? { recipesNote } : {}) }) + '\n');
129
+ } else {
130
+ if (!opts.out) process.stdout.write(result.yaml);
131
+ console.log(`# ${result.ok ? 'ok' : 'invalid'} · id ${result.id} · base ${result.base ?? '-'} · attempts ${result.attempts} · ${(ms / 1000).toFixed(1)}s${opts.out ? ` · saved ${opts.out}` : ''}`);
132
+ console.log(`# ${result.summary}`);
133
+ if (recipesNote) console.log(`# ${recipesNote}`);
134
+ for (const issue of result.issues) console.log(`# issue: ${issue}`);
135
+ if (result.dryRun && (opts.run || !result.ok)) console.log(`# dry-run ${result.dryRun.status}: ${result.dryRun.path.join(' → ')}`);
136
+ }
137
+ if (!result.ok) process.exitCode = 1;
138
+ } catch (error) {
139
+ console.error(`graph wizard: ${error instanceof Error ? error.message : String(error)}`);
140
+ process.exitCode = 1;
141
+ }
142
+ });
143
+ graph.command('step <step>')
144
+ .description('그래프 마법사 단계 하나를 실행한다(graph run 의 cmd 노드가 부른다 · 마지막 줄 JSON {outcome})')
145
+ .option('--arg <text>', '단계 인자')
146
+ .option('--retries <n>', '실패하면 이 횟수만큼 다시 시도', '0')
147
+ .action(async (step: string, opts: { arg?: string; retries: string }) => {
148
+ const { runWizardStepWithRetries } = await import('../graph-wizard/steps.js');
149
+ const result = runWizardStepWithRetries(step, opts.arg, Number.parseInt(opts.retries, 10) || 0);
150
+ if (result.error) console.error(result.error);
151
+ console.log(JSON.stringify({ outcome: result.outcome, tries: result.tries, ...(result.text ? { text: result.text.slice(0, 20_000) } : {}), ...(result.error ? { error: result.error } : {}) }));
152
+ if (result.outcome === 'fail') process.exitCode = 1;
153
+ });
97
154
  graph.command('tick <file>')
98
155
  .description('Advance one graph run, resuming a pending decision or starting only when requested')
99
156
  .option('--start', 'Start a new run when the graph is idle')
157
+ .option('--schedule', 'Subscribe graph YAML triggers.schedule to the workflow scheduler and tick on each fire')
100
158
  .option('--input <json>', 'JSON object passed to a newly started run')
101
159
  .option('--json', 'Print the tick result as JSON')
102
- .action(async (file: string, opts: { start?: boolean; input?: string; json?: boolean }) => {
160
+ .action(async (file: string, opts: { start?: boolean; schedule?: boolean; input?: string; json?: boolean }) => {
103
161
  try {
162
+ if (opts.schedule && (opts.start || opts.input !== undefined || opts.json)) throw new Error('--schedule cannot be combined with --start, --input or --json');
104
163
  if (opts.input !== undefined && !opts.start) throw new Error('--input requires --start');
105
164
  let input: Record<string, unknown> | undefined;
106
165
  if (opts.input !== undefined) {
@@ -112,11 +171,41 @@ export function registerGraphCommands(program: Command, runsDeps: { root?: strin
112
171
  }
113
172
  await (await import('../domains/standalone-log-sink.js')).registerStandaloneLogSink('graph');
114
173
  const { graphTick } = await import('./graph-tick.js');
174
+ if (opts.schedule) {
175
+ const { createScheduleSource } = await import('../workflow-runtime/triggers/schedule-source.js');
176
+ const graphFile = resolve(file);
177
+ const source = createScheduleSource();
178
+ // The CLI tick callback is the graph counterpart of the workflow scheduler's onEmit.
179
+ const onScheduledTick = async () => {
180
+ try {
181
+ const { notifyGraphEvents } = await import('./graph-notify.js');
182
+ const result = await graphTick(graphFile, { startIfIdle: true, deps: { notify: async () => { await notifyGraphEvents(); } } });
183
+ if (result.status === 'refused') console.error(`graph tick: refused: ${result.reason}`);
184
+ else if (result.status === 'failed') console.error(`graph tick: ${result.runId}: failed`);
185
+ } catch (error) {
186
+ debug.log('graph.runner', 'schedule-tick-failed', { file: graphFile, error: error instanceof Error ? error.message : String(error) });
187
+ console.error(`graph tick: ${error instanceof Error ? error.message : String(error)}`);
188
+ }
189
+ };
190
+ source.subscribeGraph(graphFile, onScheduledTick);
191
+ if (!source.subscriptions().length) throw new Error('graph triggers.schedule is missing');
192
+ await source.start();
193
+ if (source.handle()?.skipped.length) {
194
+ const reasons = source.handle()!.skipped.map(item => item.reason).join('; ');
195
+ await source.stop();
196
+ throw new Error(`graph schedule skipped: ${reasons}`);
197
+ }
198
+ console.log(`graph schedule: ${source.subscriptions().length} active`);
199
+ const shutdown = () => { void source.stop().then(() => { process.exitCode = 0; }); };
200
+ process.once('SIGINT', shutdown);
201
+ process.once('SIGTERM', shutdown);
202
+ return;
203
+ }
115
204
  const result = await graphTick(file, { startIfIdle: opts.start, ...(input === undefined ? {} : { input }),
116
205
  deps: { notify: async () => { const { notifyGraphEvents } = await import('./graph-notify.js'); await notifyGraphEvents(); } } });
117
206
  if (opts.json) await writeStdoutJson(JSON.stringify(result) + '\n');
118
207
  else console.log(`graph tick: ${result.action}${result.runId ? ` ${result.runId}: ${result.status}` : ''}`);
119
- if (result.status === 'failed') process.exitCode = 1;
208
+ if (result.status === 'failed' || result.status === 'refused') process.exitCode = 1;
120
209
  } catch (error) {
121
210
  console.error(`graph tick: ${error instanceof Error ? error.message : String(error)}`);
122
211
  process.exitCode = 1;
@@ -11,8 +11,11 @@ import { elanousStateRoot } from '../autopilot/state-paths.js';
11
11
  import { debug } from '../debug/log.js';
12
12
  import { effectiveInstanceRoot } from '../instance/resolve.js';
13
13
  import { jsonResponse } from '../nexus/api/json-response.js';
14
+ import { peerEditRefusalBody, peerEditRunGate } from '../nexus/api/graph-peer-edit.js';
14
15
  import { defaultGraphsDir } from '../self-implement/graph-templates.js';
15
16
  import { runGraph, type GraphRunState } from './runner.js';
17
+ import { readWizardSteps, wizardRecipesFor } from '../graph-wizard/saved-steps.js';
18
+ import { stringify as stringifyYaml } from 'yaml';
16
19
 
17
20
  const GRAPH_ID = /^[a-z0-9-]+$/;
18
21
  const RUN_ID = /^ed-[a-z0-9]+-[a-f0-9]{6}$/;
@@ -141,6 +144,12 @@ export function handleGraphRunStart(rawId: string, deps: GraphRunApiDeps = {}):
141
144
  if (!id) return jsonResponse({ error: 'bad_request', reason: 'invalid graph id' }, 400);
142
145
  const mine = findMine(mineDirOf(deps), id);
143
146
  if (!mine) return jsonResponse({ error: 'not-found', id, reason: '내 그래프에만 실행이 있다 — 핵심 그래프는 복제해서 돌린다' }, 404);
147
+ // W9c — a peer «edit» save runs nothing on this machine until the owner approves it. Unreadable marker = refused.
148
+ const peerGate = peerEditRunGate(mineDirOf(deps), id);
149
+ if (!peerGate.ok) {
150
+ debug.log('graphs.run', 'refused-peer-edit', { id, error: peerGate.error }, { level: 'warn' });
151
+ return jsonResponse(peerEditRefusalBody(id, peerGate), 409);
152
+ }
144
153
  const recipesFile = deps.recipesFile ?? join(defaultGraphsDir(), 'demo', 'editor-recipes.yaml');
145
154
  let recipeText: string;
146
155
  let allowed: Set<string>;
@@ -148,6 +157,17 @@ export function handleGraphRunStart(rawId: string, deps: GraphRunApiDeps = {}):
148
157
  recipeText = readFileSync(recipesFile, 'utf8');
149
158
  const parsed: unknown = parseYaml(recipeText);
150
159
  allowed = new Set(parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? Object.keys(parsed) : []);
160
+ // GRAPH-WIZARD-SAVE-RECIPES — a wizard graph's own steps (saved as a validated sidecar) become recipes here,
161
+ // generated by the server from the step library; they take precedence for the nodes that carry them.
162
+ const steps = readWizardSteps(mineDirOf(deps), id, readFileSync(mine.file, 'utf8'));
163
+ const generated = steps ? wizardRecipesFor(readFileSync(mine.file, 'utf8'), steps) : {};
164
+ if (Object.keys(generated).length) {
165
+ const base = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as Record<string, unknown> : {};
166
+ const merged = { ...base, ...generated };
167
+ recipeText = stringifyYaml(merged);
168
+ allowed = new Set(Object.keys(merged));
169
+ debug.log('graphs.run', 'wizard-recipes', { id, nodes: Object.keys(generated) });
170
+ }
151
171
  } catch {
152
172
  return jsonResponse({ error: 'recipes-unavailable' }, 503);
153
173
  }
@@ -1,8 +1,10 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { closeSync, mkdirSync, openSync, readFileSync, rmdirSync, unlinkSync, writeFileSync } from 'node:fs';
3
- import { join } from 'node:path';
2
+ import { closeSync, mkdirSync, openSync, readFileSync, realpathSync, rmdirSync, unlinkSync, writeFileSync } from 'node:fs';
3
+ import { basename, isAbsolute, join, relative, resolve, sep } from 'node:path';
4
4
  import { parse as parseYaml } from 'yaml';
5
+ import { elanousStateRoot } from '../autopilot/state-paths.js';
5
6
  import { debug } from '../debug/log.js';
7
+ import { peerEditRunGate } from '../nexus/api/graph-peer-edit.js';
6
8
  import { observeModelInputTokens } from '../harness/model-input-observation.js';
7
9
  import { effectiveInstanceRoot } from '../instance/resolve.js';
8
10
  import { DecisionLedger } from '../decisions/decision-ledger.js';
@@ -11,23 +13,74 @@ import { latestGraphRun, runGraph, type GraphRunOptions, type GraphRunState } fr
11
13
  export interface GraphTickOptions {
12
14
  startIfIdle?: boolean;
13
15
  input?: unknown;
16
+ mineDir?: string;
14
17
  deps?: GraphRunOptions['deps'] & { notify?: () => void | Promise<void>; isAlive?: (pid: number) => boolean };
15
18
  }
16
19
 
17
20
  export interface GraphTickResult {
18
- action: 'waiting' | 'resumed' | 'started' | 'idle';
21
+ action: 'waiting' | 'resumed' | 'started' | 'idle' | 'refused';
19
22
  runId?: string;
20
- status?: GraphRunState['status'];
23
+ status?: GraphRunState['status'] | 'refused';
24
+ reason?: 'peer-edit-unapproved' | 'peer-edit-unreadable';
21
25
  }
22
26
 
23
27
  /** One tick owns the graph from the latest-state read through notification and the chosen action. */
24
28
  export async function graphTick(path: string, options: GraphTickOptions = {}): Promise<GraphTickResult> {
25
- const document: unknown = parseYaml(readFileSync(path, 'utf8'));
26
- const graphId = document && typeof document === 'object' && !Array.isArray(document)
27
- ? (document as Record<string, unknown>).graph_id : undefined;
28
- if (typeof graphId !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(graphId) || graphId === '.' || graphId === '..') {
29
- throw new Error(`invalid graph id: ${String(graphId)}`);
30
- }
29
+ const mineDir = options.mineDir ?? join(elanousStateRoot(), 'graphs');
30
+ const inside = (dir: string, file: string): boolean => {
31
+ const contained = relative(dir, file);
32
+ return contained !== '' && contained !== '..' && !contained.startsWith(`..${sep}`) && !isAbsolute(contained);
33
+ };
34
+ const inspect = (): { source: string; graphId: string; ids: string[] } => {
35
+ const source = readFileSync(path, 'utf8');
36
+ const document: unknown = parseYaml(source);
37
+ const graphId = document && typeof document === 'object' && !Array.isArray(document)
38
+ ? (document as Record<string, unknown>).graph_id : undefined;
39
+ if (typeof graphId !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(graphId) || graphId === '.' || graphId === '..') {
40
+ throw new Error(`invalid graph id: ${String(graphId)}`);
41
+ }
42
+ const ids: string[] = [];
43
+ const file = resolve(path);
44
+ const realFile = realpathSync(path);
45
+ let realMine: string | undefined;
46
+ try { realMine = realpathSync(mineDir); }
47
+ catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; }
48
+ for (const candidate of [file, realFile]) {
49
+ if (inside(resolve(mineDir), candidate) || (realMine && inside(realMine, candidate))) {
50
+ const name = basename(candidate);
51
+ if (/\.ya?ml$/.test(name)) ids.push(name.replace(/\.ya?ml$/, ''));
52
+ }
53
+ }
54
+ if (ids.length) ids.push(graphId);
55
+ return { source, graphId, ids: [...new Set(ids)] };
56
+ };
57
+ const initial = inspect();
58
+ const graphId = initial.graphId;
59
+ const peerRefusal = (ids: string[]): GraphTickResult | null => {
60
+ for (const id of ids) {
61
+ const gate = peerEditRunGate(mineDir, id);
62
+ if (gate.ok) continue;
63
+ debug.log('graph.runner', 'tick-peer-edit-refused', {
64
+ id,
65
+ editedBy: gate.error === 'peer-edit-unapproved' ? gate.record.editedBy : undefined,
66
+ version: gate.error === 'peer-edit-unapproved' ? gate.record.version : undefined,
67
+ });
68
+ return { action: 'refused', status: 'refused', reason: gate.error };
69
+ }
70
+ return null;
71
+ };
72
+ const refused = peerRefusal(initial.ids);
73
+ if (refused) return refused;
74
+ const peerEditRefused = Symbol('tick-peer-edit-refused');
75
+ let nodeRefusal: GraphTickResult | null = null;
76
+ const runDeps: GraphRunOptions['deps'] = {
77
+ ...options.deps,
78
+ beforeNode: () => {
79
+ options.deps?.beforeNode?.();
80
+ nodeRefusal = peerRefusal([...new Set([...initial.ids, ...inspect().ids])]);
81
+ if (nodeRefusal) throw peerEditRefused;
82
+ },
83
+ };
31
84
  const root = options.deps?.root ?? effectiveInstanceRoot();
32
85
  const lock = join(root, 'graph-runs', graphId, '.tick.lock');
33
86
  mkdirSync(join(root, 'graph-runs', graphId), { recursive: true });
@@ -38,6 +91,11 @@ export async function graphTick(path: string, options: GraphTickOptions = {}): P
38
91
  acquireTickLock(lock, graphId, options.deps?.isAlive ?? processAlive);
39
92
  acquired = true;
40
93
  await options.deps?.notify?.();
94
+ const current = inspect();
95
+ const refusedAfterNotify = peerRefusal([...new Set([...initial.ids, ...current.ids])]);
96
+ if (refusedAfterNotify) return refusedAfterNotify;
97
+ // The run identity and executed bytes must agree with the ledger and the gate.
98
+ if (current.graphId !== graphId) throw new Error(`graph id changed during tick: ${graphId} -> ${current.graphId}`);
41
99
  const latest = latestGraphRun(graphId, root);
42
100
  const growthCard = latest?.growthPark
43
101
  ? (options.deps?.growthDecision?.list ?? ((filters: { status: 'all' }) => new DecisionLedger({ stateDir: root }).list(filters)))({ status: 'all' })
@@ -49,10 +107,10 @@ export async function graphTick(path: string, options: GraphTickOptions = {}): P
49
107
  if (latest?.status === 'awaiting-approval' && !latest.pending?.decision && !growthDecided) {
50
108
  result = { action: 'waiting', runId: latest.runId, status: latest.status };
51
109
  } else if (latest?.status === 'running' || (latest?.status === 'awaiting-approval' && (latest.pending?.decision || growthDecided))) {
52
- const state = await runGraph(path, { resumeRunId: latest.runId, deps: { ...options.deps, root } });
110
+ const state = await runGraph(path, { resumeRunId: latest.runId, graphSource: current.source, deps: { ...runDeps, root } });
53
111
  result = { action: 'resumed', runId: state.runId, status: state.status };
54
112
  } else if (options.startIfIdle) {
55
- const state = await runGraph(path, { input: options.input, deps: { ...options.deps, root } });
113
+ const state = await runGraph(path, { input: options.input, graphSource: current.source, deps: { ...runDeps, root } });
56
114
  result = { action: 'started', runId: state.runId, status: state.status };
57
115
  } else {
58
116
  result = { action: 'idle' };
@@ -60,6 +118,9 @@ export async function graphTick(path: string, options: GraphTickOptions = {}): P
60
118
  (options.deps?.log ?? ((event: string, data: Record<string, unknown>) => debug.log('graph.runner', event, data)))
61
119
  ('tick', { graphId, action: result.action, ...(result.runId ? { runId: result.runId } : {}) });
62
120
  return result;
121
+ } catch (error) {
122
+ if (error === peerEditRefused && nodeRefusal) return nodeRefusal;
123
+ throw error;
63
124
  } finally {
64
125
  try {
65
126
  if (acquired) {