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,37 @@
1
+ import { debug } from '../debug/log.js';
2
+
3
+ /** One observation per harness node execution or loop tick, not per LLM request. */
4
+ export type ModelInputObservationTarget = {
5
+ scope: 'harness-node' | 'loop-tick';
6
+ nodeKind: string;
7
+ graphId?: string;
8
+ runId?: string;
9
+ nodeId?: string;
10
+ tickId?: string;
11
+ };
12
+
13
+ export type ModelInputObservation = ModelInputObservationTarget & (
14
+ | { status: 'measured'; inputTokens: number }
15
+ | { status: 'unmeasured'; inputTokens: null; reason: 'usage-unavailable' | 'invalid-usage' }
16
+ );
17
+
18
+ /**
19
+ * Publish a single structured record at the execution boundary. Only provider-reported,
20
+ * nonnegative integer counts are measurements; missing/invalid usage is never a zero.
21
+ * The caller supplies the total across model calls for this execution, if available.
22
+ */
23
+ export function observeModelInputTokens(
24
+ target: ModelInputObservationTarget,
25
+ inputTokens?: number | null,
26
+ ): ModelInputObservation {
27
+ const observation: ModelInputObservation = inputTokens === null || inputTokens === undefined
28
+ ? { ...target, status: 'unmeasured', inputTokens: null, reason: 'usage-unavailable' }
29
+ : Number.isSafeInteger(inputTokens) && inputTokens >= 0
30
+ ? { ...target, status: 'measured', inputTokens }
31
+ : { ...target, status: 'unmeasured', inputTokens: null, reason: 'invalid-usage' };
32
+
33
+ try {
34
+ debug.log('harness.model-input', 'recorded', observation);
35
+ } catch { /* Logging must not interrupt the node or tick. */ }
36
+ return observation;
37
+ }
@@ -1,5 +1,9 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { join } from 'node:path';
2
+ import { spawn, spawnSync } from 'node:child_process';
3
+ import { existsSync, mkdtempSync, rmSync, symlinkSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join, resolve } from 'node:path';
6
+ import { debug } from '../debug/log.js';
3
7
  import { openMsgStore } from '../msg/msg-store.js';
4
8
  import { CardStore, redactSecrets } from '../task-cards/card-store.js';
5
9
  import { isGithubSpamText, PUBLIC_GITHUB_REPO } from './providers.js';
@@ -72,8 +76,57 @@ function object(value: unknown): Record<string, unknown> | undefined {
72
76
  }
73
77
  function gitSha(value: unknown): value is string { return typeof value === 'string' && /^[0-9a-f]{40}$/i.test(value); }
74
78
 
79
+ export interface ShadowReviewResult {
80
+ reviewed: boolean;
81
+ verdict: 'pass' | 'warn' | 'fail' | null;
82
+ summary: string;
83
+ }
84
+
85
+ /** Runs the read-only self-review command from a detached, disposable worktree of trusted HEAD.
86
+ * The PR is never checked out: only its remote diff enters the reviewer prompt. */
87
+ export async function runGithubShadowReview(number: number, repo = resolve(process.cwd())): Promise<ShadowReviewResult> {
88
+ if (!Number.isSafeInteger(number) || number < 1) throw new Error('invalid PR number');
89
+ const dir = mkdtempSync(join(tmpdir(), 'elanous-review-shadow-'));
90
+ const worktree = join(dir, 'repo');
91
+ let attached = false;
92
+ try {
93
+ const added = spawnSync('git', ['-C', repo, 'worktree', 'add', '--detach', worktree, 'HEAD'], { encoding: 'utf8', timeout: 30_000 });
94
+ if (added.status !== 0) throw new Error('isolated worktree unavailable');
95
+ attached = true;
96
+ if (existsSync(join(repo, 'node_modules'))) symlinkSync(join(repo, 'node_modules'), join(worktree, 'node_modules'), 'dir');
97
+ const output = await new Promise<string>((done, fail) => {
98
+ const child = spawn(process.execPath, [join(worktree, 'bin/elanous.mjs'), '--test', 'self', 'review', String(number), '--json'], {
99
+ cwd: worktree, env: { ...process.env, GH_REPO: PUBLIC_GITHUB_REPO }, stdio: ['ignore', 'pipe', 'pipe'],
100
+ });
101
+ let stdout = '';
102
+ let stderr = '';
103
+ const timer = setTimeout(() => child.kill(), 360_000);
104
+ child.stdout.on('data', (chunk: Buffer) => { stdout += chunk.toString(); if (stdout.length > 100_000) child.kill(); });
105
+ child.stderr.on('data', (chunk: Buffer) => { stderr += chunk.toString(); if (stderr.length > 100_000) child.kill(); });
106
+ child.on('error', error => { clearTimeout(timer); fail(error); });
107
+ child.on('close', code => { clearTimeout(timer); code === 0 ? done(stdout) : fail(new Error(`self review exited ${code}: ${stderr.slice(0, 200)}`)); });
108
+ });
109
+ const parsed: unknown = JSON.parse(output);
110
+ const result = object(parsed);
111
+ if (!result) throw new Error('self review returned no result');
112
+ const verdict = result.reviewed === true && ['pass', 'warn', 'fail'].includes(String(result.verdict))
113
+ ? result.verdict as 'pass' | 'warn' | 'fail' : null;
114
+ const findings = [...(Array.isArray(result.mustFix) ? result.mustFix : []), ...(Array.isArray(result.shouldFix) ? result.shouldFix : [])]
115
+ .filter((item): item is string => typeof item === 'string').slice(0, 8);
116
+ return { reviewed: result.reviewed === true && verdict !== null, verdict,
117
+ summary: redactSecrets(verdict ? `${verdict}: ${findings.join('; ') || 'no findings'}` : `review unavailable: ${String(result.error ?? 'not reviewed')}`).slice(0, 2000) };
118
+ } finally {
119
+ if (attached) {
120
+ rmSync(join(worktree, 'node_modules'), { force: true });
121
+ spawnSync('git', ['-C', repo, 'worktree', 'remove', '--force', worktree], { timeout: 30_000 });
122
+ }
123
+ rmSync(dir, { recursive: true, force: true });
124
+ }
125
+ }
126
+
75
127
  /** Separate shadow ledger/card path: never enter dispatchHook's seat wake or steward launch/report stages. */
76
- export async function recordGithubShadow(event: QueuedHook, root: string, fetchFn: typeof fetch = fetch): Promise<void> {
128
+ export async function recordGithubShadow(event: QueuedHook, root: string, fetchFn: typeof fetch = fetch,
129
+ review: (number: number) => Promise<ShadowReviewResult> = runGithubShadowReview): Promise<void> {
77
130
  const github = event.task.github;
78
131
  if (event.provider !== 'github' || !github || github.repository !== PUBLIC_GITHUB_REPO ||
79
132
  !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(event.eventId) ||
@@ -94,6 +147,7 @@ export async function recordGithubShadow(event: QueuedHook, root: string, fetchF
94
147
  .run(key, github.repository, github.type, github.number, judgment.verdict, judgment.reason, event.task.external.url ?? '');
95
148
  const recorded = store.db.query('SELECT verdict, reason FROM github_shadow_judgments WHERE event_key = ?')
96
149
  .get(key) as ShadowJudgment;
150
+ debug.log('steward.intake', 'judged', { eventKey: key, number: github.number, type: github.type, verdict: recorded.verdict, reason: recorded.reason });
97
151
  const cards = new CardStore(root);
98
152
  try {
99
153
  const goalId = `github:${github.repository}:${github.type}:${github.number}`;
@@ -104,5 +158,21 @@ export async function recordGithubShadow(event: QueuedHook, root: string, fetchF
104
158
  if (!card.sections.some(section => section.key === sectionKey))
105
159
  cards.appendSection(card.id, { key: sectionKey, owner: 'steward', content: redactSecrets(content) });
106
160
  } finally { cards.close(); }
161
+ if (github.type === 'pull_request' && recorded.verdict === 'accepted') {
162
+ store.db.exec(`CREATE TABLE IF NOT EXISTS github_shadow_reviews (
163
+ event_key TEXT PRIMARY KEY, repository TEXT NOT NULL, item_number INTEGER NOT NULL,
164
+ reviewed INTEGER NOT NULL, verdict TEXT, summary TEXT NOT NULL
165
+ )`);
166
+ if (!store.db.query('SELECT event_key FROM github_shadow_reviews WHERE event_key = ?').get(key)) {
167
+ let result: ShadowReviewResult;
168
+ try { result = await review(github.number); }
169
+ catch (error) { result = { reviewed: false, verdict: null, summary: `review unavailable: ${error instanceof Error ? error.message : String(error)}` }; }
170
+ store.db.query(`INSERT OR IGNORE INTO github_shadow_reviews
171
+ (event_key, repository, item_number, reviewed, verdict, summary) VALUES (?, ?, ?, ?, ?, ?)`)
172
+ .run(key, github.repository, github.number, result.reviewed ? 1 : 0, result.verdict,
173
+ redactSecrets(result.summary).slice(0, 2000));
174
+ debug.log('steward.review-shadow', 'recorded', { eventKey: key, number: github.number, reviewed: result.reviewed, verdict: result.verdict });
175
+ }
176
+ }
107
177
  } finally { store.close(); }
108
178
  }
@@ -4,7 +4,7 @@ import { getUserConfig, parseEventsConfig, userConfigPath, type EventsConfig } f
4
4
  import { dispatchHook, type HookWake } from './dispatch.js';
5
5
  import { getSecretAsync } from '../nexus/config/secrets/index.js';
6
6
  import { HookQueue, type QueuedHook } from './queue.js';
7
- import { recordGithubShadow } from './github-shadow.js';
7
+ import { recordGithubShadow, type ShadowReviewResult } from './github-shadow.js';
8
8
  import { toExternalTask, verifyWebhook, type HookProvider } from './providers.js';
9
9
  import { drainReports, handleReportPost, ReportLimiter, ReportQueue, type ReceivedReport } from './error-report.js';
10
10
  import { ConsultLimiter, ConsultQueue, drainConsults, handleConsultPost, type QueuedConsult } from './consult-intake.js';
@@ -35,6 +35,8 @@ export interface HookReceiverOptions {
35
35
  consultEnabled?: boolean;
36
36
  /** GET-only public metadata lookup; failure forces human review. */
37
37
  githubFetch?: typeof fetch;
38
+ /** Test seam for the isolated read-only self review; never publishes externally. */
39
+ githubShadowReview?: (number: number) => Promise<ShadowReviewResult>;
38
40
  }
39
41
 
40
42
  /** `/v1/tasks` lives on the Primary's nexus API, not on the control plane — so the address is its own setting. */
@@ -140,7 +142,7 @@ export function startHookReceiver(options: HookReceiverOptions): { url: string;
140
142
  // L14: public-repository intake events take the shadow ledger path; every other event keeps the seat dispatch path.
141
143
  const dispatch = options.forward ?? ((event: QueuedHook) => dispatchHook(event, options.root ?? effectiveInstanceRoot(), events, options.wakeSeat).then(() => 204));
142
144
  const forward = (event: QueuedHook) => event.provider === 'github' && event.task.github
143
- ? recordGithubShadow(event, options.root ?? effectiveInstanceRoot(), options.githubFetch).then(() => 204)
145
+ ? recordGithubShadow(event, options.root ?? effectiveInstanceRoot(), options.githubFetch, options.githubShadowReview).then(() => 204)
144
146
  : dispatch(event);
145
147
  const attempts = new Map<string, { next: number; failures: number }>();
146
148
  const pending = new Set<string>();
package/src/hq/hq.ts ADDED
@@ -0,0 +1,318 @@
1
+ // HQ lease operations — CLI-facing glue over the pure decisions in ./lease.ts (HQ-HB · HQ-FENCE · 10-04).
2
+ // Every outcome leaves one observation line (hq.lease / hq.fence) so the 12:00 drill is measurable from logs.
3
+ import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { homedir, hostname } from 'node:os';
5
+ import { dirname, join } from 'node:path';
6
+ import { spawnSync } from 'node:child_process';
7
+ import { debug } from '../debug/log.js';
8
+ import { DEFAULT_NEXUS_HTTP_PORT } from '../nexus/default-port.js';
9
+ import { getElanousConfigDir } from '../elanous-config-dir.js';
10
+ import { getUserConfig, type HqConfig, type HqFenceRole } from '../user-config.js';
11
+ import {
12
+ DEFAULT_TTL_SECONDS, decideAcquire, decideArbiterCheck, decideFence, decideRelease, decideRenew, defaultSshRunner,
13
+ fileLeaseStore, leaseExpired, parseLease, probeReachable, recordView, serializeLease, sshLeaseStore, sshReachable, localShellRunner,
14
+ type HostProbe, type LeaseRecord, type LeaseStore, type LocalHqState, type SshRunner,
15
+ } from './lease.js';
16
+
17
+ export const FENCE_ROLES: readonly HqFenceRole[] = ['telegram-poller', 'cron', 'seat-loop', 'release-run', 'conatus', 'git-push', 'ledger-cli'];
18
+
19
+ export interface HqDeps {
20
+ config?: HqConfig;
21
+ store?: LeaseStore;
22
+ ssh?: SshRunner;
23
+ /** Holder/standby views and the arbiter's check use this tailnet probe — never ssh (OP 10-04 09:04). */
24
+ probe?: HostProbe;
25
+ localPath?: string;
26
+ /** Host-local identity, deliberately outside the replicated config/state directory. */
27
+ hostPath?: string;
28
+ /** Seen-generation marker (default hq.seenGenerationFile · ~/.elanous-hq/seen-generation). */
29
+ seenPath?: string;
30
+ now?: () => number;
31
+ log?: typeof debug.log;
32
+ }
33
+
34
+ interface Resolved { me: string; arbiter: string; standby: string; ttl: number; failOpen: Set<HqFenceRole>; store: LeaseStore; ssh: SshRunner; probe: HostProbe; localPath: string; seenPath: string; now: () => number; log: typeof debug.log }
35
+ const defaultHostPath = () => join(homedir(), '.elanous-hq', 'host');
36
+
37
+ function readHost(path: string): string | undefined {
38
+ try {
39
+ lstatSync(path);
40
+ } catch (error) {
41
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined;
42
+ throw error;
43
+ }
44
+ const text = readFileSync(path, 'utf8');
45
+ const name = text.endsWith('\n') ? text.slice(0, -1) : text;
46
+ if (!name || name !== name.trim() || /[\r\n]/.test(name)) {
47
+ throw new Error(`hq host: invalid host file: ${path}`);
48
+ }
49
+ return name;
50
+ }
51
+
52
+ /** Set this machine's identity outside the replicable config.json and HQ state universe. */
53
+ export function hqHostSet(name: string, path = defaultHostPath()): string {
54
+ if (!name || name !== name.trim() || /[\r\n]/.test(name)) throw new Error('hq host set: name must be one nonempty line without surrounding whitespace');
55
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
56
+ const tempDir = mkdtempSync(`${path}.`);
57
+ try {
58
+ chmodSync(tempDir, 0o700);
59
+ const tmp = join(tempDir, 'host');
60
+ writeFileSync(tmp, `${name}\n`, { flag: 'wx', mode: 0o600 });
61
+ chmodSync(tmp, 0o600);
62
+ renameSync(tmp, path);
63
+ } finally {
64
+ rmSync(tempDir, { recursive: true, force: true });
65
+ }
66
+ return path;
67
+ }
68
+
69
+ function resolve(deps: HqDeps = {}): Resolved {
70
+ const config = deps.config ?? getUserConfig().hq ?? {};
71
+ const ssh = deps.ssh ?? defaultSshRunner;
72
+ const arbiter = config.arbiter ?? 'cloud-vm';
73
+ const local = readHost(deps.hostPath ?? defaultHostPath());
74
+ const me = local ?? config.hostName ?? hostname().replace(/\.local$/, '');
75
+ const log = deps.log ?? debug.log.bind(debug);
76
+ if (local && config.hostName && local !== config.hostName) {
77
+ try { log('hq.lease', 'hostname-mismatch', { local, config: config.hostName }); } catch { /* observation is fail-soft */ }
78
+ }
79
+ return {
80
+ me,
81
+ arbiter,
82
+ standby: config.standby ?? 'node-b',
83
+ ttl: config.ttlSeconds ?? DEFAULT_TTL_SECONDS,
84
+ failOpen: new Set(config.failOpenRoles ?? []),
85
+ store: deps.store ?? sshLeaseStore(arbiter, arbiter === 'local' ? localShellRunner : ssh),
86
+ ssh,
87
+ probe: deps.probe ?? tailnetProbe(config),
88
+ localPath: deps.localPath ?? join(getElanousConfigDir(), 'hq', 'local.json'),
89
+ seenPath: deps.seenPath ?? config.seenGenerationFile ?? (deps.localPath ? `${deps.localPath}.seen-generation` : join(homedir(), '.elanous-hq', 'seen-generation')),
90
+ now: deps.now ?? (() => Math.floor(Date.now() / 1000)),
91
+ log,
92
+ };
93
+ }
94
+
95
+ /** MagicDNS suffix from `tailscale status --json` (cached per process). */
96
+ let dnsSuffix: string | null | undefined;
97
+ function magicDnsSuffix(bin: string): string | undefined {
98
+ if (dnsSuffix !== undefined) return dnsSuffix ?? undefined;
99
+ const r = spawnSync(bin, ['status', '--json'], { encoding: 'utf8', timeout: 10_000 });
100
+ try { dnsSuffix = (JSON.parse(r.stdout || '{}') as { MagicDNSSuffix?: string }).MagicDNSSuffix?.replace(/\.$/, '') || null; } catch { dnsSuffix = null; }
101
+ return dnsSuffix ?? undefined;
102
+ }
103
+
104
+ /** Health URL of a remote host's nexus over the tailnet — its `tailscale serve` TLS mapping of the default nexus port
105
+ * (resolveDaemonEndpoint only knows this machine's daemon; the arbiter probes another host). */
106
+ export function healthUrlFor(host: string, config: HqConfig, suffix: () => string | undefined): string | undefined {
107
+ const pinned = config.healthUrls?.[host];
108
+ if (pinned) return pinned;
109
+ const domain = config.tailnetDomain ?? suffix();
110
+ return domain ? `https://${host}.${domain}:${DEFAULT_NEXUS_HTTP_PORT}/v1/health` : undefined;
111
+ }
112
+
113
+ /** Default probe: `tailscale ping -c 1` ⊕ curl of the nexus health (200 only). Both are tailnet-only, no ssh. */
114
+ export function tailnetProbe(config: HqConfig): HostProbe {
115
+ const bin = config.tailscaleBin ?? 'tailscale';
116
+ const timeout = config.probeTimeoutSeconds ?? 5;
117
+ return {
118
+ ping(host) {
119
+ const r = spawnSync(bin, ['ping', '-c', '1', '--timeout', `${timeout}s`, host], { encoding: 'utf8', timeout: (timeout + 5) * 1000 });
120
+ return r.status === 0 && /\bpong\b/.test(r.stdout ?? '');
121
+ },
122
+ health(host) {
123
+ const url = healthUrlFor(host, config, () => magicDnsSuffix(bin));
124
+ if (!url) return false;
125
+ const r = spawnSync('curl', ['-s', '-o', '/dev/null', '-m', String(timeout), '-w', '%{http_code}', url], { encoding: 'utf8', timeout: (timeout + 5) * 1000 });
126
+ return (r.stdout ?? '').trim() === '200';
127
+ },
128
+ };
129
+ }
130
+
131
+ export function readLocal(path: string): LocalHqState {
132
+ try { return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) as LocalHqState : {}; } catch { return {}; }
133
+ }
134
+ function writeLocal(path: string, state: LocalHqState): void {
135
+ mkdirSync(dirname(path), { recursive: true });
136
+ const tmp = `${path}.${process.pid}.tmp`;
137
+ writeFileSync(tmp, `${JSON.stringify(state)}\n`, { mode: 0o600 });
138
+ renameSync(tmp, path);
139
+ }
140
+
141
+ /** Highest lease generation this host ever observed (null = never). */
142
+ export function readSeenGeneration(path: string): number | null {
143
+ try {
144
+ if (!existsSync(path)) return null;
145
+ const n = Number(readFileSync(path, 'utf8').trim());
146
+ return Number.isSafeInteger(n) && n > 0 ? n : null;
147
+ } catch { return null; }
148
+ }
149
+ /** Keep the max generation seen (atomic · 600). Fail-soft: a lost write never blocks the lease operation. */
150
+ function noteSeen(r: Resolved, record: LeaseRecord | null | undefined): void {
151
+ if (!record || !Number.isSafeInteger(record.generation) || record.generation < 1) return;
152
+ try {
153
+ const prev = readSeenGeneration(r.seenPath);
154
+ if (prev !== null && prev >= record.generation) return;
155
+ mkdirSync(dirname(r.seenPath), { recursive: true, mode: 0o700 });
156
+ const tmp = `${r.seenPath}.${process.pid}.tmp`;
157
+ writeFileSync(tmp, `${record.generation}\n`, { mode: 0o600 });
158
+ renameSync(tmp, r.seenPath);
159
+ } catch (error) { observe(r, 'hq.lease', 'seen-write-failed', { error: String(error).slice(0, 200) }); }
160
+ }
161
+
162
+ /** `elanous hq seen` — wrappers use «never seen» (exit 3) to allow the pre-drill default-source path only. */
163
+ export function hqSeen(deps: HqDeps = {}): { seen: boolean; generation: number | null; path: string } {
164
+ const r = resolve(deps);
165
+ const generation = readSeenGeneration(r.seenPath);
166
+ return { seen: generation !== null, generation, path: r.seenPath };
167
+ }
168
+
169
+ function observe(r: Resolved, category: 'hq.lease' | 'hq.fence', event: string, data: Record<string, unknown>): void {
170
+ try { r.log(category, event, { host: r.me, ...data }); } catch { /* observation is fail-soft */ }
171
+ }
172
+
173
+ /** Read → decide → CAS, retried on a lost race (two hosts racing cannot both win: only one CAS sees the old bytes). */
174
+ function mutate(r: Resolved, decide: (record: LeaseRecord | null, now: number) => { ok: true; next: LeaseRecord } | { ok: false; reason: string }):
175
+ { ok: true; record: LeaseRecord } | { ok: false; reason: string; record: LeaseRecord | null } {
176
+ for (let attempt = 0; attempt < 5; attempt++) {
177
+ const { now, raw } = r.store.read();
178
+ const record = parseLease(raw);
179
+ const decided = decide(record, now);
180
+ if (!decided.ok) return { ok: false, reason: decided.reason, record };
181
+ if (r.store.cas(raw, serializeLease(decided.next))) return { ok: true, record: decided.next };
182
+ }
183
+ return { ok: false, reason: 'lost the race five times', record: null };
184
+ }
185
+
186
+ export type LeaseAction = 'acquire' | 'renew' | 'status' | 'release';
187
+ export function hqLease(action: LeaseAction, deps: HqDeps = {}, opts: { host?: string } = {}) {
188
+ const r0 = resolve(deps);
189
+ const r = opts.host ? { ...r0, me: opts.host } : r0;
190
+ if (action === 'status') {
191
+ const { now, raw } = r.store.read();
192
+ const record = parseLease(raw);
193
+ noteSeen(r, record);
194
+ return { ok: true as const, action, record, ageSeconds: record ? now - record.renewedAt : null, expired: record ? leaseExpired(record, now) : null };
195
+ }
196
+ const local = readLocal(r.localPath);
197
+ const result = mutate(r, (record, now) => action === 'acquire' ? decideAcquire(record, r.me, now, r.ttl)
198
+ : action === 'renew' ? decideRenew(record, r.me, local.generation, now) : decideRelease(record, r.me));
199
+ noteSeen(r, result.record);
200
+ if (result.ok && action !== 'release') writeLocal(r.localPath, { holder: result.record.holder, generation: result.record.generation, confirmedAt: r.now(), ttlSeconds: result.record.ttlSeconds });
201
+ if (result.ok && action === 'release') writeLocal(r.localPath, { ...local, holder: undefined, confirmedAt: undefined });
202
+ observe(r, 'hq.lease', result.ok ? (action === 'acquire' ? 'acquired' : action === 'renew' ? 'renewed' : 'released') : `${action}-refused`,
203
+ result.ok ? { generation: result.record.generation } : { reason: result.reason });
204
+ return { action, ...result };
205
+ }
206
+
207
+ /**
208
+ * Heartbeat (every 10 min on mbp and node-b). Holder → renew at the arbiter, or (arbiter down) confirm quorum through the standby.
209
+ * Non-holder → report its view of the holder to the arbiter (rule ②) and adopt a generation the arbiter granted it.
210
+ */
211
+ export function hqHeartbeat(deps: HqDeps = {}) {
212
+ const r = resolve(deps);
213
+ const local = readLocal(r.localPath);
214
+ let read: { now: number; raw: string | null };
215
+ try { read = r.store.read(); } catch (error) {
216
+ const standbyUp = local.holder === r.me ? sshReachable(r.standby, r.ssh) : false;
217
+ if (local.holder === r.me && standbyUp) writeLocal(r.localPath, { ...local, confirmedAt: r.now() });
218
+ const outcome = local.holder === r.me ? (standbyUp ? 'quorum-standby' : 'arbiter-unreachable') : 'arbiter-unreachable';
219
+ observe(r, 'hq.lease', outcome, { error: String(error).slice(0, 200), standbyReachable: standbyUp });
220
+ return { outcome };
221
+ }
222
+ const record = parseLease(read.raw);
223
+ noteSeen(r, record);
224
+ if (!record) { observe(r, 'hq.lease', 'no-lease', { seenGeneration: readSeenGeneration(r.seenPath) }); return { outcome: 'no-lease' as const }; }
225
+ if (record.holder === r.me) {
226
+ // The record names me as holder: a generation the arbiter granted me (promotion) is adopted, never refused.
227
+ const renewed = mutate(r, (current, now) => decideRenew(current, r.me, Math.max(local.generation ?? 0, current?.generation ?? 0), now));
228
+ if (renewed.ok && (local.generation ?? 0) < renewed.record.generation) observe(r, 'hq.lease', 'generation-adopted', { from: local.generation ?? null, to: renewed.record.generation });
229
+ if (renewed.ok) {
230
+ noteSeen(r, renewed.record);
231
+ writeLocal(r.localPath, { holder: r.me, generation: renewed.record.generation, confirmedAt: r.now(), ttlSeconds: renewed.record.ttlSeconds });
232
+ observe(r, 'hq.lease', 'renewed', { generation: renewed.record.generation });
233
+ return { outcome: 'renewed' as const, generation: renewed.record.generation };
234
+ }
235
+ observe(r, 'hq.lease', 'renew-refused', { reason: renewed.reason });
236
+ return { outcome: 'renew-refused' as const, reason: renewed.reason };
237
+ }
238
+ // Not the holder: report the view, keep the granted generation, stay standby.
239
+ const seen = probeReachable(record.holder, r.probe);
240
+ const reachable = seen.reachable;
241
+ mutate(r, (current) => current ? { ok: true, next: recordView(current, r.me, reachable, read.now) } : { ok: false, reason: 'no lease' });
242
+ if (leaseExpired(record, read.now)) observe(r, 'hq.lease', 'expired-seen', { holder: record.holder, generation: record.generation });
243
+ writeLocal(r.localPath, { holder: record.holder, generation: record.generation, ttlSeconds: record.ttlSeconds });
244
+ observe(r, 'hq.lease', 'standby', { holder: record.holder, generation: record.generation, holderReachable: reachable, ping: seen.ping, health: seen.health });
245
+ return { outcome: 'standby' as const, holder: record.holder, holderReachable: reachable };
246
+ }
247
+
248
+ /**
249
+ * Arbiter check (cron on cloud-vm every 10 min, next to the file): probe the holder over the tailnet
250
+ * (`tailscale ping` ⊕ nexus health · no ssh into home machines) and promote per rule ②.
251
+ */
252
+ export function hqArbiterCheck(deps: HqDeps & { leasePath?: string } = {}) {
253
+ const r0 = resolve(deps);
254
+ const r = deps.store ? r0 : { ...r0, store: fileLeaseStore(deps.leasePath ?? join(process.env.HOME ?? '', '.elanous-hq', 'lease.json')) };
255
+ let outcome: { promoted: boolean; streak: number; holder?: string; generation?: number; ping?: boolean; health?: boolean } = { promoted: false, streak: 0 };
256
+ const result = mutate(r, (record, now) => {
257
+ if (!record) return { ok: false, reason: 'no lease' };
258
+ const seen = probeReachable(record.holder, r.probe);
259
+ const checked = decideArbiterCheck(record, { arbiter: r.me, standby: r.standby, arbiterReachesHolder: seen.reachable, now });
260
+ outcome = { promoted: checked.promoted, streak: checked.streak, holder: checked.next.holder, generation: checked.next.generation, ping: seen.ping, health: seen.health };
261
+ return { ok: true, next: checked.next };
262
+ });
263
+ observe(r, 'hq.lease', outcome.promoted ? 'promoted' : 'arbiter-check', { ...outcome, ok: result.ok });
264
+ return { ok: result.ok, ...outcome };
265
+ }
266
+
267
+ /** Standby's own generation, asked over ssh (rule ③ when the arbiter is down). */
268
+ function standbyGeneration(r: Resolved): number | undefined {
269
+ const res = r.ssh(r.standby, 'cat "$HOME/.elanous/hq/local.json" 2>/dev/null || true');
270
+ try { const g = (JSON.parse(res.stdout || '{}') as LocalHqState).generation; return typeof g === 'number' ? g : undefined; } catch { return undefined; }
271
+ }
272
+
273
+ export function hqFenceDecision(role: HqFenceRole, deps: HqDeps = {}) {
274
+ const r = resolve(deps);
275
+ const local = readLocal(r.localPath);
276
+ let record: LeaseRecord | null | 'unreachable';
277
+ try { record = parseLease(r.store.read().raw); } catch { record = 'unreachable'; }
278
+ if (record !== 'unreachable') noteSeen(r, record);
279
+ const seenGeneration = readSeenGeneration(r.seenPath);
280
+ const standbyUp = record === 'unreachable' && local.holder === r.me ? sshReachable(r.standby, r.ssh) : undefined;
281
+ const decided = decideFence({
282
+ me: r.me, now: r.now(), local, record,
283
+ ...(standbyUp !== undefined ? { standbyReachable: standbyUp } : {}),
284
+ ...(standbyUp ? { standbyGeneration: standbyGeneration(r) } : {}),
285
+ failOpen: role !== 'ledger-cli' && r.failOpen.has(role),
286
+ ...(seenGeneration !== null ? { seenGeneration } : {}),
287
+ });
288
+ if (decided.run && record !== 'unreachable' && record) writeLocal(r.localPath, { holder: r.me, generation: record.generation, confirmedAt: r.now(), ttlSeconds: record.ttlSeconds });
289
+ observe(r, 'hq.fence', decided.run ? 'allowed' : 'skipped', { role, reason: decided.reason, holder: decided.holder, generation: decided.generation });
290
+ return decided;
291
+ }
292
+
293
+ /** CLI ledger writes fail closed after the first lease; bootstrap is allowed only before any generation was seen. */
294
+ export function hqCliWriteAllowed(command: string, override = false, deps: HqDeps = {}): boolean {
295
+ const decision = hqFenceDecision('ledger-cli', deps);
296
+ const allowed = decision.run || decision.reason === 'no-lease';
297
+ const r = resolve(deps);
298
+ if (override) {
299
+ observe(r, 'hq.fence', 'cli-override', { command, reason: decision.reason, holder: decision.holder, generation: decision.generation, allowed });
300
+ console.error(`hq override: ${command} — ${decision.reason} (holder ${decision.holder ?? 'unknown'} gen ${decision.generation ?? 'unknown'})`);
301
+ return true;
302
+ }
303
+ if (allowed) return true;
304
+ observe(r, 'hq.fence', 'cli-refused', { command, reason: decision.reason, holder: decision.holder, generation: decision.generation });
305
+ console.error(`본부는 ${decision.holder ?? 'unknown'} gen ${decision.generation ?? 'unknown'} — 원장 쓰기는 지금 본부로 보내라 (수동 우회: --hq-override, 관측)`);
306
+ process.exitCode = 4;
307
+ return false;
308
+ }
309
+
310
+ /** Run `command` only when this host may write; the child sees ELANOUS_HQ_GENERATION. Skips exit 0. */
311
+ export function hqFenceRun(role: HqFenceRole, command: string[], deps: HqDeps = {}): number {
312
+ const decided = hqFenceDecision(role, deps);
313
+ if (!decided.run) { console.error(`hq fence: skip ${role} — ${decided.reason}${decided.holder ? ` (holder ${decided.holder} gen ${decided.generation})` : ''}`); return 0; }
314
+ const [cmd, ...args] = command;
315
+ if (!cmd) throw new Error('hq fence: command required after --');
316
+ const r = spawnSync(cmd, args, { stdio: 'inherit', env: { ...process.env, ELANOUS_HQ_GENERATION: String(decided.generation ?? '') } });
317
+ return r.status ?? 1;
318
+ }