elanous 0.2.12 → 0.2.13

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 (289) 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/{1394-ee3795b112907122.js → 1394-931c9827067b2276.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{3992-2e522a8ac5f2afde.js → 3992-fe5c06d6b31f8b25.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/5250-c78ca295585bbf95.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/6511-ff8360be3d65aaa4.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/6588-ba380869b67c4fa7.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/{7758-0b018a9e4fc56200.js → 7758-85c2b53f731da024.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/{8605-0259994816604c49.js → 8605-9da4678f8a310d18.js} +1 -1
  10. package/apps/pwa/out/_next/static/chunks/9906-e7b8b5310a169dfe.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-cb169620945d1952.js → page-e5b4e1b256f41076.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-cefe8169e36cf62a.js → page-e49b9b76c565a0b1.js} +1 -1
  13. package/apps/pwa/out/_next/static/chunks/app/board/page-b4f032c893b21cc9.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/app/botlab/page-f14987d5850a31e0.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/app/bots/page-b433308aad13ceb6.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/chat/{page-813c4a52c9d05ca8.js → page-4c1dbb21861beab2.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/app/consult/{page-4731f2c215358937.js → page-c85b816e3b269dbb.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/control/{page-a022b257950f39e6.js → page-f6bc66b919b2ea7d.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-a2ffc717e04b8b0a.js → page-60603c1e056c9b25.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/editor/{page-b59ee4e70fc18fcf.js → page-a0ea8f5a2341ee6b.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/exec/page-45c5bf04b4a605e6.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/field/{page-c172dabd796eb7a3.js → page-36c6c4183353ac9e.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/inside/{page-1efd0cb9e0ed9d13.js → page-cb2eb0fd809980da.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/intake/{page-d5dc7ff69a37b4cf.js → page-31012264da8a341f.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/layout-50309aeb0bdd554a.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/live/page-bc00e964e855f4df.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/market/{page-dfca9b7d2de0b135.js → page-0696ee3fac9fe56c.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-793fa536b3a6ef37.js → page-66c27608f651b866.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/morning/{page-6dd3bfbbde2a5c2c.js → page-6ddd2109b7fc3a80.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/not-found-4e50b7298c1f69a5.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/app/observatory/{page-77447356baad75d2.js → page-7a2bf830038a2264.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-1f327f18694fabca.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e5cef2fe6e4028df.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cf596128d2b420ba.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/outputs/{page-6d604227e1664b71.js → page-f672536f14f0969f.js} +1 -1
  36. package/apps/pwa/out/_next/static/chunks/app/page-b52365662232db89.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-4048543b4b7ad5e0.js → page-e7a6c80f288b1948.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-4c3a3126abed551a.js → page-29a709616df9b050.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-64b0aecb1ca75e75.js → page-fee842d7c2151129.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-78b40a344ed22f24.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/settings/page-75d199f448171014.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-a011dd5b0bff29c5.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/setup/{page-9547b5cec7cf76bf.js → page-d92e32d51ab5157e.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/share/page-5f21aed6932ef375.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/showroom/page-1e1a53946804debf.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-2cddeb9e2aeedc55.js → page-a1833cd43d832601.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/term/{page-e297866051595d81.js → page-63d14a4d8fe1e1c1.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/today/{page-baba2e047872fb0f.js → page-2a511d320f762e06.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/trace/{page-e895da4ec9cac345.js → page-1c3e03f88dac1e21.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/vault/page-c4d5289c2ea97a89.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b91f8952deaeaf27.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/workflows/page-71b43a2588489987.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2e9e2fb01c9da1c9.js → page-2b256feed331546f.js} +1 -1
  54. package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-d62ceb13411ebba4.js → page-505bb96d69a8622c.js} +1 -1
  55. package/apps/pwa/out/approvals/index.html +1 -1
  56. package/apps/pwa/out/approvals/index.txt +13 -13
  57. package/apps/pwa/out/autopilot/index.html +1 -1
  58. package/apps/pwa/out/autopilot/index.txt +13 -13
  59. package/apps/pwa/out/board/index.html +1 -1
  60. package/apps/pwa/out/board/index.txt +13 -13
  61. package/apps/pwa/out/botlab/index.html +1 -1
  62. package/apps/pwa/out/botlab/index.txt +13 -13
  63. package/apps/pwa/out/bots/index.html +1 -1
  64. package/apps/pwa/out/bots/index.txt +13 -13
  65. package/apps/pwa/out/chat/index.html +1 -1
  66. package/apps/pwa/out/chat/index.txt +13 -13
  67. package/apps/pwa/out/consult/index.html +1 -1
  68. package/apps/pwa/out/consult/index.txt +13 -13
  69. package/apps/pwa/out/control/index.html +1 -1
  70. package/apps/pwa/out/control/index.txt +13 -13
  71. package/apps/pwa/out/design-check/index.html +1 -1
  72. package/apps/pwa/out/design-check/index.txt +13 -13
  73. package/apps/pwa/out/editor/index.html +1 -1
  74. package/apps/pwa/out/editor/index.txt +13 -13
  75. package/apps/pwa/out/exec/index.html +1 -1
  76. package/apps/pwa/out/exec/index.txt +13 -13
  77. package/apps/pwa/out/field/index.html +1 -1
  78. package/apps/pwa/out/field/index.txt +13 -13
  79. package/apps/pwa/out/healthz +1 -1
  80. package/apps/pwa/out/index.html +1 -1
  81. package/apps/pwa/out/index.txt +13 -13
  82. package/apps/pwa/out/inside/index.html +1 -1
  83. package/apps/pwa/out/inside/index.txt +13 -13
  84. package/apps/pwa/out/intake/index.html +1 -1
  85. package/apps/pwa/out/intake/index.txt +13 -13
  86. package/apps/pwa/out/live/index.html +1 -1
  87. package/apps/pwa/out/live/index.txt +13 -13
  88. package/apps/pwa/out/market/index.html +1 -1
  89. package/apps/pwa/out/market/index.txt +13 -13
  90. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  91. package/apps/pwa/out/missions/placeholder/index.txt +13 -13
  92. package/apps/pwa/out/morning/index.html +1 -1
  93. package/apps/pwa/out/morning/index.txt +13 -13
  94. package/apps/pwa/out/observatory/index.html +1 -1
  95. package/apps/pwa/out/observatory/index.txt +13 -13
  96. package/apps/pwa/out/ops/checklist/index.html +1 -1
  97. package/apps/pwa/out/ops/checklist/index.txt +13 -13
  98. package/apps/pwa/out/ops/release/index.html +1 -1
  99. package/apps/pwa/out/ops/release/index.txt +13 -13
  100. package/apps/pwa/out/ops/seats/index.html +1 -1
  101. package/apps/pwa/out/ops/seats/index.txt +13 -13
  102. package/apps/pwa/out/outputs/index.html +1 -1
  103. package/apps/pwa/out/outputs/index.txt +13 -13
  104. package/apps/pwa/out/reflection/index.html +1 -1
  105. package/apps/pwa/out/reflection/index.txt +14 -14
  106. package/apps/pwa/out/scheduler/index.html +1 -1
  107. package/apps/pwa/out/scheduler/index.txt +13 -13
  108. package/apps/pwa/out/sessions/index.html +1 -1
  109. package/apps/pwa/out/sessions/index.txt +13 -13
  110. package/apps/pwa/out/settings/devices/index.html +1 -1
  111. package/apps/pwa/out/settings/devices/index.txt +13 -13
  112. package/apps/pwa/out/settings/index.html +1 -1
  113. package/apps/pwa/out/settings/index.txt +13 -13
  114. package/apps/pwa/out/setup/done/index.html +1 -1
  115. package/apps/pwa/out/setup/done/index.txt +13 -13
  116. package/apps/pwa/out/setup/index.html +1 -1
  117. package/apps/pwa/out/setup/index.txt +13 -13
  118. package/apps/pwa/out/share/index.html +1 -1
  119. package/apps/pwa/out/share/index.txt +13 -13
  120. package/apps/pwa/out/showroom/index.html +1 -1
  121. package/apps/pwa/out/showroom/index.txt +13 -13
  122. package/apps/pwa/out/tasks/index.html +1 -1
  123. package/apps/pwa/out/tasks/index.txt +13 -13
  124. package/apps/pwa/out/term/index.html +1 -1
  125. package/apps/pwa/out/term/index.txt +13 -13
  126. package/apps/pwa/out/today/index.html +1 -1
  127. package/apps/pwa/out/today/index.txt +13 -13
  128. package/apps/pwa/out/trace/index.html +1 -1
  129. package/apps/pwa/out/trace/index.txt +13 -13
  130. package/apps/pwa/out/vault/index.html +1 -1
  131. package/apps/pwa/out/vault/index.txt +13 -13
  132. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  133. package/apps/pwa/out/workflows/chat-ui/index.txt +13 -13
  134. package/apps/pwa/out/workflows/index.html +1 -1
  135. package/apps/pwa/out/workflows/index.txt +13 -13
  136. package/apps/pwa/out/workspace/index.html +1 -1
  137. package/apps/pwa/out/workspace/index.txt +13 -13
  138. package/apps/pwa/out/worktrees/index.html +1 -1
  139. package/apps/pwa/out/worktrees/index.txt +13 -13
  140. package/examples/teams/personas/backend.yaml +4 -0
  141. package/examples/teams/personas/pm.yaml +4 -0
  142. package/examples/teams/personas/qa.yaml +4 -0
  143. package/examples/teams/small-product.yaml +20 -0
  144. package/graphs/lecture/lecture-note.yaml +28 -0
  145. package/graphs/lecture/recipes.yaml +17 -0
  146. package/package.json +2 -1
  147. package/scripts/context-hooks/emit.ts +36 -14
  148. package/scripts/coord-channel-watch.sh +1 -0
  149. package/scripts/discord-seat-probe.ts +144 -0
  150. package/scripts/hq/hq-standby.sh +65 -0
  151. package/scripts/hq/standby-promote.ts +173 -0
  152. package/scripts/hq/standby-role.ts +59 -0
  153. package/scripts/hq/standby-snapshot.ts +226 -0
  154. package/scripts/hq/standby-verify.ts +219 -0
  155. package/scripts/lecture-note/node.ts +256 -0
  156. package/scripts/release-loop/gate-node.ts +53 -15
  157. package/scripts/release-loop/ops-upgrade-node.ts +17 -5
  158. package/scripts/test-deterministic.ts +53 -3
  159. package/skills/omni-crawl/SKILL.md +16 -16
  160. package/skills/omni-crawl/package-lock.json +1 -544
  161. package/skills/omni-crawl/package.json +3 -4
  162. package/skills/omni-crawl/scripts/main.ts +13 -13
  163. package/skills/omni-crawl/scripts/monitor.ts +8 -8
  164. package/skills/omni-crawl/src/apify.ts +13 -2
  165. package/skills/omni-crawl/src/capture.ts +6 -0
  166. package/skills/omni-crawl/src/registry.ts +2 -2
  167. package/skills/omni-crawl/src/stdout-json.ts +4 -0
  168. package/skills/omni-digest/SKILL.md +10 -11
  169. package/skills/omni-digest/package-lock.json +0 -545
  170. package/skills/omni-digest/package.json +2 -5
  171. package/skills/omni-digest/scripts/main.ts +3 -3
  172. package/skills/omni-digest/src/delegate.ts +5 -5
  173. package/skills/omni-digest/src/fetch.ts +2 -2
  174. package/skills/omni-digest/src/media.ts +25 -11
  175. package/skills/omni-digest/src/visual.ts +3 -2
  176. package/skills/omni-market/scripts/main.ts +1 -1
  177. package/skills/omni-market/scripts/stdout-json.ts +4 -0
  178. package/src/agent/identity-env.ts +11 -0
  179. package/src/agent-mission/driver.ts +12 -7
  180. package/src/agent-mission/duo.ts +94 -0
  181. package/src/agent-mission/pty-decision.ts +1 -1
  182. package/src/autopilot/track-agent-shadow.ts +2 -1
  183. package/src/autopilot/track-agent.ts +4 -1
  184. package/src/boot/daemon-prompt-request.ts +3 -0
  185. package/src/boot/daemon-prompt-turn.ts +6 -2
  186. package/src/boot/daemon-runtime.ts +48 -16
  187. package/src/cli/decisions-cli.ts +51 -19
  188. package/src/cli/outputs-cli.ts +92 -0
  189. package/src/cli/persona-cli.ts +29 -0
  190. package/src/cli/pod-cli.ts +10 -3
  191. package/src/cli/pr-cli.ts +15 -1
  192. package/src/cli/release-cli.ts +108 -28
  193. package/src/context-bus/condense.ts +194 -0
  194. package/src/context-bus/context-day.ts +82 -0
  195. package/src/context-bus/context-now-surfaces.ts +15 -2
  196. package/src/context-bus/long-term-memory.ts +118 -22
  197. package/src/context-bus/nightly-condense.ts +1 -1
  198. package/src/decisions/decision-cards.ts +13 -7
  199. package/src/decisions/decision-ledger.ts +43 -17
  200. package/src/graph-kinds/registry.ts +2 -2
  201. package/src/graph-runner/graph-approval-card.ts +2 -2
  202. package/src/graph-runner/graph-grow.ts +64 -11
  203. package/src/graph-runner/graph-tick.ts +9 -2
  204. package/src/graph-runner/runner.ts +81 -17
  205. package/src/harness/harness-cli-command.ts +27 -1
  206. package/src/harness/harness-queue-child.ts +37 -0
  207. package/src/harness/harness-queue.ts +306 -0
  208. package/src/hooks/github-shadow.ts +72 -2
  209. package/src/hooks/receiver.ts +4 -2
  210. package/src/hq/hq.ts +318 -0
  211. package/src/hq/lease.ts +210 -0
  212. package/src/hq/ledger-write-target.ts +41 -0
  213. package/src/index.ts +199 -7
  214. package/src/intake-plane/discord-seat-work.ts +13 -6
  215. package/src/intake-plane/linear-wish.ts +44 -0
  216. package/src/intake-plane/persona-answer.ts +5 -0
  217. package/src/intake-plane/telegram-seat-work.ts +13 -6
  218. package/src/intake-plane/wish-card.ts +1 -1
  219. package/src/loops/checker.ts +178 -0
  220. package/src/loops/loop-cli.ts +37 -3
  221. package/src/loops/observe.ts +49 -0
  222. package/src/loops/orchestrator/overlap.ts +141 -0
  223. package/src/loops/orchestrator/traffic.ts +150 -0
  224. package/src/loops/package/verify.ts +125 -0
  225. package/src/loops/verdict.ts +22 -0
  226. package/src/maturity/feature-maturity.ts +3 -3
  227. package/src/memory.ts +37 -4
  228. package/src/nexus/api/card-followup-route.ts +118 -0
  229. package/src/nexus/api/decisions-route.ts +68 -0
  230. package/src/nexus/api/http-server.ts +47 -0
  231. package/src/nexus/api/personas.ts +2 -1
  232. package/src/nexus/api/skill-exec-route.ts +97 -0
  233. package/src/persona/loader.ts +18 -0
  234. package/src/persona/persona-todo.ts +14 -9
  235. package/src/persona/presets.ts +2 -1
  236. package/src/persona/team.ts +105 -0
  237. package/src/persona/types.ts +4 -0
  238. package/src/pod-lease/host-lease.ts +130 -20
  239. package/src/release-loop/landed-but-yellow.ts +12 -0
  240. package/src/seat-dispatch/seat-ask.ts +57 -12
  241. package/src/seat-loop/seat-loop.ts +90 -16
  242. package/src/seat-loop/seat-pack.ts +127 -0
  243. package/src/self-dev/ask-launch-flow.ts +3 -0
  244. package/src/self-implement/l8-merge-queue.ts +157 -1
  245. package/src/steward/launch.ts +5 -1
  246. package/src/steward/readiness.ts +125 -0
  247. package/src/steward/rescue.ts +75 -0
  248. package/src/steward/triage.ts +48 -9
  249. package/src/task-cards/card-cli.ts +78 -0
  250. package/src/task-orchestrator/surfaces/pod-lease.ts +18 -6
  251. package/src/task-orchestrator/surfaces/pod-pool.ts +27 -10
  252. package/src/task-orchestrator/surfaces/self-implement-pod.ts +38 -8
  253. package/src/telegram-dispatch-forward.ts +1 -1
  254. package/src/telegram-fabric-plan.ts +90 -0
  255. package/src/telegram-project-command.ts +10 -4
  256. package/src/telegram.ts +5 -0
  257. package/src/tool-runtime/elanous-control-runtimes.ts +2 -1
  258. package/src/user-config.ts +94 -10
  259. package/src/version/packed-revision.json +1 -1
  260. package/src/workflow-runtime/executor.ts +4 -0
  261. package/src/workflow-runtime/node-catalog.ts +13 -0
  262. package/src/workflow-runtime/nodes/knowledge.ts +38 -0
  263. package/src/workflow-runtime/schema.ts +17 -1
  264. package/src/workflow-runtime/types.ts +13 -1
  265. package/apps/pwa/out/_next/static/chunks/1147-466ec15afacc1a65.js +0 -1
  266. package/apps/pwa/out/_next/static/chunks/7277-0b2fc7c16ba2d64b.js +0 -1
  267. package/apps/pwa/out/_next/static/chunks/9906-482c6619b3fdda20.js +0 -1
  268. package/apps/pwa/out/_next/static/chunks/9920-5cfdc5e8fd209825.js +0 -1
  269. package/apps/pwa/out/_next/static/chunks/app/board/page-d2e35228bc497119.js +0 -1
  270. package/apps/pwa/out/_next/static/chunks/app/botlab/page-5c05b0da4bc1392c.js +0 -1
  271. package/apps/pwa/out/_next/static/chunks/app/bots/page-e0ccdb576812b523.js +0 -1
  272. package/apps/pwa/out/_next/static/chunks/app/exec/page-a67e11ba962337b5.js +0 -1
  273. package/apps/pwa/out/_next/static/chunks/app/layout-e71c84189e8c683e.js +0 -1
  274. package/apps/pwa/out/_next/static/chunks/app/live/page-1d6a533ce5a6efcb.js +0 -1
  275. package/apps/pwa/out/_next/static/chunks/app/not-found-490653e8d4cc8852.js +0 -1
  276. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-29e961191f6057df.js +0 -1
  277. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-c822f55b884d9104.js +0 -1
  278. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fa4ccfc432424e52.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/app/page-9c7e8f744a765daf.js +0 -1
  280. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-4292d0149020e8a7.js +0 -1
  281. package/apps/pwa/out/_next/static/chunks/app/settings/page-db14d0be7f1c460c.js +0 -1
  282. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-e5d8072cd7cfef73.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/app/share/page-e6dcb83265530636.js +0 -1
  284. package/apps/pwa/out/_next/static/chunks/app/showroom/page-45248f08d6aecae5.js +0 -1
  285. package/apps/pwa/out/_next/static/chunks/app/vault/page-4344faf20216233b.js +0 -1
  286. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-698bb29f11864b89.js +0 -1
  287. package/apps/pwa/out/_next/static/chunks/app/workflows/page-bd6a6dc1ddc273f2.js +0 -1
  288. /package/apps/pwa/out/_next/static/{-6XrAoD3eU7NyMHAE4xPS → D51l8jUBplJrIpWvENsDK}/_buildManifest.js +0 -0
  289. /package/apps/pwa/out/_next/static/{-6XrAoD3eU7NyMHAE4xPS → D51l8jUBplJrIpWvENsDK}/_ssgManifest.js +0 -0
@@ -0,0 +1,306 @@
1
+ import { spawn, spawnSync } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
4
+ import { join, resolve } from 'node:path';
5
+ import { Database } from 'bun:sqlite';
6
+ import { debug } from '../debug/log.js';
7
+ import { effectiveInstanceRoot, prodInstanceRoot } from '../instance/resolve.js';
8
+ import { getUserConfig } from '../user-config.js';
9
+ import { hostLeaseCounts, leaseHasPendingPod } from '../pod-lease/host-lease.js';
10
+ import { leaseKubectl, measurePoolLease } from '../task-orchestrator/surfaces/pod-lease.js';
11
+ import { parsePodPool, podPoolHostLease, resolvePodPoolSpec } from '../task-orchestrator/surfaces/pod-pool.js';
12
+ import { writeHarnessQueueReceipt } from './harness-queue-child.js';
13
+
14
+ export type QueueSeat = 'OP' | 'TC' | 'MK' | 'UX';
15
+ export type QueueItem = {
16
+ id: string; seat: QueueSeat; kind: 'say' | 'ask'; input: string; hold: boolean; heavy: boolean;
17
+ at: string; status: 'queued' | 'launching' | 'launched' | 'finished'; pid?: number; launchId?: string;
18
+ };
19
+ export type QueuePool = { running: number; pending: number; reserved: number; limit: number };
20
+ export type QueueTick = { outcome: 'launched' | 'waiting' | 'skipped'; item?: QueueItem; reason: string };
21
+ export type QueueProcess = { pid: number; seat?: QueueSeat; launchId?: string };
22
+
23
+ export interface HarnessQueueDeps {
24
+ root?: string;
25
+ cap?: (seat: QueueSeat) => number;
26
+ pool?: () => QueuePool;
27
+ launch?: (item: QueueItem, args: string[], root: string) => Promise<number>;
28
+ alive?: (pid: number) => boolean;
29
+ processes?: () => readonly QueueProcess[];
30
+ receipt?: (root: string, launchId: string) => 'started' | 'finished' | 'not-started' | null;
31
+ log?: (event: 'enqueued' | 'launched' | 'waiting' | 'skipped', data: Record<string, unknown>) => void;
32
+ }
33
+
34
+ export function harnessQueuePath(root = effectiveInstanceRoot()): string {
35
+ return join(root, 'harness', 'queue.json');
36
+ }
37
+
38
+ function observe(event: 'enqueued' | 'launched' | 'waiting' | 'skipped', data: Record<string, unknown>, deps: HarnessQueueDeps): void {
39
+ try { (deps.log ?? ((name, payload) => debug.log('harness.queue', name, payload)))(event, data); }
40
+ catch { /* Logging cannot affect dispatch. */ }
41
+ }
42
+
43
+ function read(path: string): QueueItem[] {
44
+ if (!existsSync(path)) return [];
45
+ const value: unknown = JSON.parse(readFileSync(path, 'utf8'));
46
+ if (!Array.isArray(value) || !value.every((row) => row && typeof row === 'object'
47
+ && typeof row.id === 'string' && /^(OP|TC|MK|UX)$/.test(row.seat)
48
+ && (row.kind === 'say' || row.kind === 'ask') && typeof row.input === 'string'
49
+ && typeof row.at === 'string' && typeof row.hold === 'boolean' && typeof row.heavy === 'boolean'
50
+ && ['queued', 'launching', 'launched', 'finished'].includes(row.status)
51
+ && (row.pid === undefined || Number.isSafeInteger(row.pid))
52
+ && (row.launchId === undefined || typeof row.launchId === 'string'))) {
53
+ throw new Error('harness queue: invalid queue file (no launch)');
54
+ }
55
+ return value as QueueItem[];
56
+ }
57
+
58
+ function save(path: string, items: QueueItem[]): void {
59
+ const temporary = `${path}.${randomUUID()}.tmp`;
60
+ try {
61
+ writeFileSync(temporary, `${JSON.stringify(items, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
62
+ renameSync(temporary, path);
63
+ } catch (error) {
64
+ try { unlinkSync(temporary); } catch { /* write did not finish */ }
65
+ throw error;
66
+ }
67
+ }
68
+
69
+ // SQLite OS locks release on process death, including death while awaiting a launch.
70
+ async function locked<T>(root: string, action: (path: string) => Promise<T>): Promise<T> {
71
+ const path = harnessQueuePath(root);
72
+ mkdirSync(join(root, 'harness'), { recursive: true, mode: 0o700 });
73
+ const db = new Database(`${path}.mutex.sqlite`, { create: true, strict: true });
74
+ try {
75
+ const deadline = Date.now() + 30_000;
76
+ for (;;) {
77
+ try { db.exec('BEGIN IMMEDIATE'); break; }
78
+ catch (error) {
79
+ if ((error as { code?: string }).code !== 'SQLITE_BUSY' || Date.now() >= deadline) throw error;
80
+ await Bun.sleep(25);
81
+ }
82
+ }
83
+ try { return await action(path); }
84
+ finally { db.exec('ROLLBACK'); }
85
+ } finally { db.close(); }
86
+ }
87
+
88
+ function seat(value: string): QueueSeat {
89
+ if (!/^(OP|TC|MK|UX)$/.test(value)) throw new Error(`harness queue: unknown seat ${value}`);
90
+ return value as QueueSeat;
91
+ }
92
+
93
+ export async function addHarnessQueue(input: { seat: string; say?: string; ask?: string; hold?: boolean; heavy?: boolean }, deps: HarnessQueueDeps = {}): Promise<QueueItem> {
94
+ const assigned = seat(input.seat);
95
+ if ((input.say === undefined) === (input.ask === undefined)) throw new Error('harness queue add: --say 또는 --ask 중 하나만 필요');
96
+ const kind = input.ask === undefined ? 'say' : 'ask';
97
+ const text = kind === 'say' ? input.say! : input.ask!;
98
+ if (!text.trim()) throw new Error('harness queue add: 빈 입력');
99
+ const value = kind === 'ask' ? resolve(text) : text;
100
+ if (kind === 'ask' && !existsSync(value)) throw new Error(`harness queue add: goal file not found: ${value}`);
101
+ const root = deps.root ?? effectiveInstanceRoot();
102
+ return locked(root, async (path) => {
103
+ const item: QueueItem = { id: `hq-${randomUUID()}`, seat: assigned, kind, input: value,
104
+ hold: input.hold === true, heavy: input.heavy === true, at: new Date().toISOString(), status: 'queued' };
105
+ save(path, [...read(path), item]);
106
+ observe('enqueued', { id: item.id, seat: item.seat, kind }, deps);
107
+ return item;
108
+ });
109
+ }
110
+
111
+ export function listHarnessQueue(deps: HarnessQueueDeps = {}): QueueItem[] {
112
+ return read(harnessQueuePath(deps.root ?? effectiveInstanceRoot()));
113
+ }
114
+
115
+ export async function removeHarnessQueue(id: string, deps: HarnessQueueDeps = {}): Promise<boolean> {
116
+ const root = deps.root ?? effectiveInstanceRoot();
117
+ return locked(root, async (path) => {
118
+ const rows = read(path);
119
+ const item = rows.find((row) => row.id === id);
120
+ if (!item) return false;
121
+ if (item.status !== 'queued' && item.status !== 'finished') {
122
+ if ((item.status !== 'launched' && item.status !== 'launching') || !item.launchId) return false;
123
+ const state = (deps.receipt ?? receipt)(root, item.launchId);
124
+ if (state !== 'finished' && state !== 'not-started'
125
+ || (deps.processes ?? readHarnessQueueProcesses)().some((row) => row.launchId === item.launchId)) return false;
126
+ }
127
+ save(path, rows.filter((row) => row.id !== id));
128
+ observe('skipped', { id, seat: item.seat, reason: 'removed' }, deps);
129
+ return true;
130
+ });
131
+ }
132
+
133
+ export function queueLaunchArgs(item: QueueItem): string[] {
134
+ return ['harness', item.kind, item.input, '--substrate', 'pod',
135
+ ...(item.hold ? ['--no-auto-merge'] : []), ...(item.heavy ? ['--pod-memory', 'high'] : [])];
136
+ }
137
+
138
+ export function readHarnessQueuePool(): QueuePool {
139
+ const config = getUserConfig();
140
+ const spec = resolvePodPoolSpec(undefined, process.env, () => config.harness?.podPool ?? config.pod?.pool);
141
+ const current = spec ? null : leaseKubectl(['config', 'current-context']);
142
+ const context = spec ?? (current?.status === 0 ? current.stdout.trim() : '');
143
+ if (!context) throw new Error('pod lease status: no pool/context');
144
+ const members = parsePodPool(context);
145
+ const observed = measurePoolLease(members);
146
+ if (observed.members.some((member) => member.running === null || member.pending === null)) throw new Error('pod lease status: incomplete running/pending measurement');
147
+ const pendingJobs = observed.members.flatMap((member) => member.pendingJobs ?? []);
148
+ const records = podPoolHostLease(members).live();
149
+ return {
150
+ running: observed.members.reduce((sum, member) => sum + member.running!, 0),
151
+ pending: observed.members.reduce((sum, member) => sum + member.pending!, 0),
152
+ reserved: hostLeaseCounts(records).reserved + records.filter((row) => row.stage === 'job' && !leaseHasPendingPod(row, pendingJobs)).length,
153
+ limit: members.reduce((sum, member) => sum + member.capacity, 0),
154
+ };
155
+ }
156
+
157
+ function alive(pid: number): boolean {
158
+ try { process.kill(pid, 0); return true; } catch { return false; }
159
+ }
160
+
161
+ /** Host inventory includes harness authoring before it has a run ledger. Unattributed processes count for every seat. */
162
+ export function readHarnessQueueProcesses(): readonly QueueProcess[] {
163
+ const result = spawnSync('ps', ['-eo', 'pid=,args='], { encoding: 'utf8', timeout: 5_000, maxBuffer: 16 * 1024 * 1024 });
164
+ if (result.error || result.status !== 0 || typeof result.stdout !== 'string') throw new Error('harness process inventory unavailable');
165
+ const rows: QueueProcess[] = [];
166
+ for (const line of result.stdout.split('\n')) {
167
+ const match = /^\s*(\d+)\s+(.+)$/.exec(line);
168
+ if (!match) continue;
169
+ const command = match[2]!;
170
+ if (!/^(?:\S*\/)?(?:bun|node)(?:\s|$)/.test(command)
171
+ || !/(?:elanous\.mjs|harness-queue-child\.ts)/.test(command)
172
+ || !/(?:\bharness\s+(?:say|ask)\b|\bdev\s+.*(?:--say|--ask|--file|--implement)\b|\bself\s+(?:implement|orchestrate)\b|harness-queue-child\.ts)/.test(command)) continue;
173
+ const pid = Number(match[1]);
174
+ if (!Number.isSafeInteger(pid) || pid <= 0) throw new Error('invalid harness pid');
175
+ let envSeat: string | undefined;
176
+ let envLaunch: string | undefined;
177
+ if (process.platform === 'linux') {
178
+ try {
179
+ const env = readFileSync(`/proc/${pid}/environ`, 'utf8').split('\0');
180
+ envSeat = env.find((entry) => entry.startsWith('ELANOUS_HARNESS_SEAT='))?.slice('ELANOUS_HARNESS_SEAT='.length);
181
+ envLaunch = env.find((entry) => entry.startsWith('ELANOUS_HARNESS_QUEUE_LAUNCH='))?.slice('ELANOUS_HARNESS_QUEUE_LAUNCH='.length);
182
+ } catch { /* Unknown attribution is charged to all seats. */ }
183
+ }
184
+ const assigned = envSeat ?? /--seat[=\s]+(OP|TC|MK|UX)(?:\s|$)/.exec(command)?.[1];
185
+ rows.push({ pid, ...(assigned && /^(OP|TC|MK|UX)$/.test(assigned) ? { seat: assigned as QueueSeat } : {}),
186
+ ...(envLaunch ? { launchId: envLaunch } : {}) });
187
+ }
188
+ return rows;
189
+ }
190
+
191
+ export function harnessQueueReceiptPath(root: string, launchId: string): string {
192
+ if (!/^hq-[0-9a-f-]{36}$/.test(launchId)) throw new Error('invalid queue launch id');
193
+ return join(root, 'harness', `${launchId}.receipt.json`);
194
+ }
195
+
196
+ function receipt(root: string, launchId: string): 'started' | 'finished' | 'not-started' | null {
197
+ try {
198
+ const record = JSON.parse(readFileSync(harnessQueueReceiptPath(root, launchId), 'utf8')) as { state?: string };
199
+ return record.state === 'started' || record.state === 'finished' || record.state === 'not-started' ? record.state : null;
200
+ } catch (error) {
201
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
202
+ throw error;
203
+ }
204
+ }
205
+
206
+ async function launch(item: QueueItem, args: string[], root: string): Promise<number> {
207
+ const repo = resolve(import.meta.dir, '../..');
208
+ const logPath = join(root, 'harness', `${item.id}.log`);
209
+ const fd = openSync(logPath, 'a', 0o600);
210
+ try {
211
+ const child = spawn(process.execPath, [join(repo, 'src', 'harness', 'harness-queue-child.ts'),
212
+ harnessQueueReceiptPath(root, item.launchId!), join(repo, 'bin', 'elanous.mjs'),
213
+ ...(resolve(root) === prodInstanceRoot() ? [] : [`--test=${root}`]), ...args], {
214
+ cwd: repo, detached: true, stdio: ['ignore', fd, fd],
215
+ env: { ...process.env, ELANOUS_STATE_DIR: root, ELANOUS_HARNESS_SEAT: item.seat,
216
+ ELANOUS_HARNESS_QUEUE_LAUNCH: item.launchId! },
217
+ });
218
+ const pid = await new Promise<number>((done, reject) => {
219
+ child.once('spawn', () => done(child.pid!));
220
+ child.once('error', (error) => {
221
+ try { writeHarnessQueueReceipt(harnessQueueReceiptPath(root, item.launchId!), 'not-started'); }
222
+ catch { /* A failed receipt write leaves the reservation safely indeterminate. */ }
223
+ reject(error);
224
+ });
225
+ });
226
+ child.unref();
227
+ return pid;
228
+ } finally { closeSync(fd); }
229
+ }
230
+
231
+ /** Only a wrapper receipt proving no active child can clear an uncertain reservation. */
232
+ export async function reconcileHarnessQueue(id: string, deps: HarnessQueueDeps = {}): Promise<'released' | 'running' | 'unknown'> {
233
+ const root = deps.root ?? effectiveInstanceRoot();
234
+ return locked(root, async (path) => {
235
+ const rows = read(path);
236
+ const item = rows.find((row) => row.id === id);
237
+ if (!item || !['launching', 'launched'].includes(item.status) || !item.launchId) return 'unknown';
238
+ const state = (deps.receipt ?? receipt)(root, item.launchId);
239
+ const processes = (deps.processes ?? readHarnessQueueProcesses)();
240
+ if (processes.some((row) => row.launchId === item.launchId)
241
+ || (item.pid !== undefined && (deps.alive ?? alive)(item.pid) && state !== 'finished' && state !== 'not-started')) return 'running';
242
+ if (state !== 'finished' && state !== 'not-started') return 'unknown';
243
+ // A finished receipt proves the child exited; a reused wrapper PID alone is not its identity.
244
+ // Keep completed launches in the ledger, but never charge their seat again.
245
+ save(path, state === 'finished' && item.status === 'launched'
246
+ ? rows.map((row) => row.id === id ? { ...row, status: 'finished' } : row)
247
+ : rows.filter((row) => row.id !== id));
248
+ observe('skipped', { id, seat: item.seat, reason: `reconciled ${state}`, launchId: item.launchId }, deps);
249
+ return 'released';
250
+ });
251
+ }
252
+
253
+ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<QueueTick> {
254
+ const root = deps.root ?? effectiveInstanceRoot();
255
+ return locked(root, async (path) => {
256
+ const items = read(path);
257
+ const item = items.find((row) => row.status === 'queued');
258
+ if (!item) { observe('skipped', { reason: 'empty' }, deps); return { outcome: 'skipped', reason: 'empty' }; }
259
+ const cap = (deps.cap ?? ((s: QueueSeat) => getUserConfig().harness?.queue?.seatCap?.[s] ?? 8))(item.seat);
260
+ if (!Number.isSafeInteger(cap) || cap < 1) throw new Error(`harness queue: invalid seat cap for ${item.seat}`);
261
+ const wait = (reason: string, active: number): QueueTick => {
262
+ observe('waiting', { id: item.id, seat: item.seat, reason, active, cap }, deps);
263
+ return { outcome: 'waiting', item, reason };
264
+ };
265
+ let processes: readonly QueueProcess[];
266
+ try { processes = (deps.processes ?? readHarnessQueueProcesses)(); }
267
+ catch (error) { return wait(`harness process inventory unavailable: ${String(error)}`, 0); }
268
+ const current = items.flatMap((row) => {
269
+ if ((row.status !== 'launched' && row.status !== 'launching') || !row.launchId) return [row];
270
+ const state = (deps.receipt ?? receipt)(root, row.launchId);
271
+ if (state !== 'finished' && state !== 'not-started'
272
+ || processes.some((process) => process.launchId === row.launchId)) return [row];
273
+ return state === 'finished' && row.status === 'launched' ? [{ ...row, status: 'finished' as const }] : [];
274
+ });
275
+ if (current.length !== items.length || current.some((row, index) => row !== items[index])) save(path, current);
276
+ const charged = processes.filter((row) => !row.seat || row.seat === item.seat);
277
+ const launchIds = new Set(charged.map((row) => row.launchId));
278
+ const active = new Set(charged.map((row) => row.launchId ?? `pid:${row.pid}`)).size + current.filter((row) => row.seat === item.seat &&
279
+ (row.status === 'launching' || row.status === 'launched')
280
+ && (!row.launchId || !launchIds.has(row.launchId))).length;
281
+ if (active >= cap) return wait(`seat ${item.seat}: ${active}/${cap}`, active);
282
+ let pool: QueuePool;
283
+ try { pool = (deps.pool ?? readHarnessQueuePool)(); }
284
+ catch (error) { return wait(`pod lease status unavailable: ${String(error)}`, active); }
285
+ if (![pool.running, pool.pending, pool.reserved, pool.limit].every((n) => Number.isSafeInteger(n) && n >= 0) || pool.limit < 1) {
286
+ return wait('pod lease status incomplete', active);
287
+ }
288
+ const queueLaunching = current.filter((row) => row.status === 'launching' || row.status === 'launched').length;
289
+ if (pool.running + pool.pending + pool.reserved + queueLaunching >= pool.limit) {
290
+ return wait(`pool: ${pool.running}+${pool.pending}+${pool.reserved}+${queueLaunching}/${pool.limit}`, active);
291
+ }
292
+ const launching: QueueItem = { ...item, status: 'launching', launchId: `hq-${randomUUID()}` };
293
+ save(path, current.map((row) => row.id === item.id ? launching : row));
294
+ try {
295
+ const pid = await (deps.launch ?? launch)(launching, queueLaunchArgs(launching), root);
296
+ if (!Number.isSafeInteger(pid) || pid < 1) throw new Error('launcher returned no pid');
297
+ const launched: QueueItem = { ...launching, pid, status: 'launched' };
298
+ save(path, current.map((row) => row.id === item.id ? launched : row));
299
+ observe('launched', { id: item.id, seat: item.seat, pid, kind: item.kind, launchId: launched.launchId }, deps);
300
+ return { outcome: 'launched', item: launched, reason: 'spawned' };
301
+ } catch (error) {
302
+ observe('skipped', { id: item.id, seat: item.seat, reason: `launch uncertain: ${String(error)}`, launchId: launching.launchId }, deps);
303
+ throw error;
304
+ }
305
+ });
306
+ }
@@ -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>();