elanous 0.2.11 → 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 (342) 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-556de6d71bbdb0fb.js → 1394-931c9827067b2276.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/3869-3cd46526879f454c.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/{3992-2e522a8ac5f2afde.js → 3992-fe5c06d6b31f8b25.js} +1 -1
  6. package/apps/pwa/out/_next/static/chunks/5250-c78ca295585bbf95.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/6511-ff8360be3d65aaa4.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/6588-ba380869b67c4fa7.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/7758-85c2b53f731da024.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/{8605-ea5bee9dfae512c7.js → 8605-9da4678f8a310d18.js} +1 -1
  11. package/apps/pwa/out/_next/static/chunks/9906-e7b8b5310a169dfe.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/app/approvals/page-e5b4e1b256f41076.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-6a061bd97f0b9398.js → page-e49b9b76c565a0b1.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/app/board/page-b4f032c893b21cc9.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/app/botlab/page-f14987d5850a31e0.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/bots/page-b433308aad13ceb6.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/chat/{page-7c97dcdfe2e37a60.js → page-4c1dbb21861beab2.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/consult/page-c85b816e3b269dbb.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/app/control/{page-5721537ca8a3598c.js → page-f6bc66b919b2ea7d.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-b654240a572e4c48.js → page-60603c1e056c9b25.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/editor/{page-b9750abc897508c3.js → page-a0ea8f5a2341ee6b.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/exec/page-45c5bf04b4a605e6.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/app/field/page-36c6c4183353ac9e.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/inside/page-cb2eb0fd809980da.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/intake/{page-c8b1ccda6ba7ae98.js → page-31012264da8a341f.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/{layout-d6b1d156fb95e74b.js → layout-50309aeb0bdd554a.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/live/page-bc00e964e855f4df.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/app/market/{page-9688f5b4f1ff950f.js → page-0696ee3fac9fe56c.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-66c27608f651b866.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/morning/{page-35414e0a007b4082.js → page-6ddd2109b7fc3a80.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/not-found-4e50b7298c1f69a5.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/observatory/{page-e001823c35c9dd67.js → page-7a2bf830038a2264.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-371423d1a573d64b.js → page-1f327f18694fabca.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-4b764b125339ac17.js → page-e5cef2fe6e4028df.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-cdf8e4b01067bcf1.js → page-cf596128d2b420ba.js} +1 -1
  36. package/apps/pwa/out/_next/static/chunks/app/outputs/{page-3a24072b35470157.js → page-f672536f14f0969f.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/page-b52365662232db89.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/app/reflection/page-e7a6c80f288b1948.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-d37cb0d52f9b6bd5.js → page-29a709616df9b050.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-686da0df9bf23e5c.js → page-fee842d7c2151129.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-4dc614f3ce043e63.js → page-78b40a344ed22f24.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/settings/page-75d199f448171014.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-d02c782fc792e2af.js → page-a011dd5b0bff29c5.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/setup/{page-239c17b911b84e72.js → page-d92e32d51ab5157e.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/share/{page-dc6a79fb003f746c.js → page-5f21aed6932ef375.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-0fffb84cafdc9da6.js → page-1e1a53946804debf.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-1218753c82d585f6.js → page-a1833cd43d832601.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/term/{page-4b5935e67531efb5.js → page-63d14a4d8fe1e1c1.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/today/{page-3285bc87caf1eaa5.js → page-2a511d320f762e06.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/trace/{page-ad553b0e06505ecd.js → page-1c3e03f88dac1e21.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/vault/{page-fca096e3e860306f.js → page-c4d5289c2ea97a89.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-9ebfa78f0d9885c3.js → page-b91f8952deaeaf27.js} +1 -1
  53. package/apps/pwa/out/_next/static/chunks/app/workflows/{page-3dc5f21f7e39622d.js → page-71b43a2588489987.js} +1 -1
  54. package/apps/pwa/out/_next/static/chunks/app/workspace/page-2b256feed331546f.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-505bb96d69a8622c.js +1 -0
  56. package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +1 -0
  57. package/apps/pwa/out/approvals/index.html +1 -1
  58. package/apps/pwa/out/approvals/index.txt +14 -14
  59. package/apps/pwa/out/autopilot/index.html +1 -1
  60. package/apps/pwa/out/autopilot/index.txt +14 -14
  61. package/apps/pwa/out/board/index.html +1 -1
  62. package/apps/pwa/out/board/index.txt +14 -14
  63. package/apps/pwa/out/botlab/index.html +1 -1
  64. package/apps/pwa/out/botlab/index.txt +14 -14
  65. package/apps/pwa/out/bots/index.html +1 -1
  66. package/apps/pwa/out/bots/index.txt +14 -14
  67. package/apps/pwa/out/chat/index.html +1 -1
  68. package/apps/pwa/out/chat/index.txt +14 -14
  69. package/apps/pwa/out/consult/index.html +1 -1
  70. package/apps/pwa/out/consult/index.txt +14 -14
  71. package/apps/pwa/out/control/index.html +1 -1
  72. package/apps/pwa/out/control/index.txt +14 -14
  73. package/apps/pwa/out/design-check/index.html +1 -1
  74. package/apps/pwa/out/design-check/index.txt +14 -14
  75. package/apps/pwa/out/editor/index.html +1 -1
  76. package/apps/pwa/out/editor/index.txt +14 -14
  77. package/apps/pwa/out/exec/index.html +1 -1
  78. package/apps/pwa/out/exec/index.txt +14 -14
  79. package/apps/pwa/out/field/index.html +1 -1
  80. package/apps/pwa/out/field/index.txt +14 -14
  81. package/apps/pwa/out/healthz +1 -1
  82. package/apps/pwa/out/index.html +1 -1
  83. package/apps/pwa/out/index.txt +14 -14
  84. package/apps/pwa/out/inside/index.html +1 -1
  85. package/apps/pwa/out/inside/index.txt +14 -14
  86. package/apps/pwa/out/intake/index.html +1 -1
  87. package/apps/pwa/out/intake/index.txt +14 -14
  88. package/apps/pwa/out/live/index.html +1 -1
  89. package/apps/pwa/out/live/index.txt +14 -14
  90. package/apps/pwa/out/market/index.html +1 -1
  91. package/apps/pwa/out/market/index.txt +14 -14
  92. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  93. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  94. package/apps/pwa/out/morning/index.html +1 -1
  95. package/apps/pwa/out/morning/index.txt +14 -14
  96. package/apps/pwa/out/observatory/index.html +1 -1
  97. package/apps/pwa/out/observatory/index.txt +14 -14
  98. package/apps/pwa/out/ops/checklist/index.html +1 -1
  99. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  100. package/apps/pwa/out/ops/release/index.html +1 -1
  101. package/apps/pwa/out/ops/release/index.txt +14 -14
  102. package/apps/pwa/out/ops/seats/index.html +1 -1
  103. package/apps/pwa/out/ops/seats/index.txt +14 -14
  104. package/apps/pwa/out/outputs/index.html +1 -1
  105. package/apps/pwa/out/outputs/index.txt +14 -14
  106. package/apps/pwa/out/reflection/index.html +1 -1
  107. package/apps/pwa/out/reflection/index.txt +15 -15
  108. package/apps/pwa/out/scheduler/index.html +1 -1
  109. package/apps/pwa/out/scheduler/index.txt +14 -14
  110. package/apps/pwa/out/sessions/index.html +1 -1
  111. package/apps/pwa/out/sessions/index.txt +14 -14
  112. package/apps/pwa/out/settings/devices/index.html +1 -1
  113. package/apps/pwa/out/settings/devices/index.txt +14 -14
  114. package/apps/pwa/out/settings/index.html +1 -1
  115. package/apps/pwa/out/settings/index.txt +14 -14
  116. package/apps/pwa/out/setup/done/index.html +1 -1
  117. package/apps/pwa/out/setup/done/index.txt +14 -14
  118. package/apps/pwa/out/setup/index.html +1 -1
  119. package/apps/pwa/out/setup/index.txt +14 -14
  120. package/apps/pwa/out/share/index.html +1 -1
  121. package/apps/pwa/out/share/index.txt +14 -14
  122. package/apps/pwa/out/showroom/index.html +1 -1
  123. package/apps/pwa/out/showroom/index.txt +14 -14
  124. package/apps/pwa/out/tasks/index.html +1 -1
  125. package/apps/pwa/out/tasks/index.txt +14 -14
  126. package/apps/pwa/out/term/index.html +1 -1
  127. package/apps/pwa/out/term/index.txt +14 -14
  128. package/apps/pwa/out/today/index.html +1 -1
  129. package/apps/pwa/out/today/index.txt +14 -14
  130. package/apps/pwa/out/trace/index.html +1 -1
  131. package/apps/pwa/out/trace/index.txt +14 -14
  132. package/apps/pwa/out/vault/index.html +1 -1
  133. package/apps/pwa/out/vault/index.txt +14 -14
  134. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  135. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  136. package/apps/pwa/out/workflows/index.html +1 -1
  137. package/apps/pwa/out/workflows/index.txt +14 -14
  138. package/apps/pwa/out/workspace/index.html +1 -1
  139. package/apps/pwa/out/workspace/index.txt +14 -14
  140. package/apps/pwa/out/worktrees/index.html +1 -1
  141. package/apps/pwa/out/worktrees/index.txt +14 -14
  142. package/examples/teams/personas/backend.yaml +4 -0
  143. package/examples/teams/personas/pm.yaml +4 -0
  144. package/examples/teams/personas/qa.yaml +4 -0
  145. package/examples/teams/small-product.yaml +20 -0
  146. package/graphs/context/nightly-condense.yaml +21 -0
  147. package/graphs/context/recipes.yaml +12 -0
  148. package/graphs/demo/inside-seed.yaml +20 -0
  149. package/graphs/demo/recipes.yaml +12 -0
  150. package/graphs/lecture/lecture-note.yaml +28 -0
  151. package/graphs/lecture/recipes.yaml +17 -0
  152. package/package.json +2 -1
  153. package/plugins/card-followup/examples/sample-card.png +0 -0
  154. package/scripts/context-hooks/emit.ts +36 -14
  155. package/scripts/coord-channel-watch.sh +1 -0
  156. package/scripts/discord-seat-probe.ts +144 -0
  157. package/scripts/hq/hq-standby.sh +65 -0
  158. package/scripts/hq/standby-promote.ts +173 -0
  159. package/scripts/hq/standby-role.ts +59 -0
  160. package/scripts/hq/standby-snapshot.ts +226 -0
  161. package/scripts/hq/standby-verify.ts +219 -0
  162. package/scripts/lecture-note/node.ts +256 -0
  163. package/scripts/lib/telegram-inject-args.ts +31 -0
  164. package/scripts/measure-pod-memory-by-goal.ts +130 -0
  165. package/scripts/release-loop/auto-approve-node.ts +1 -1
  166. package/scripts/release-loop/checklist-gate-node.ts +2 -1
  167. package/scripts/release-loop/gate-node.ts +61 -17
  168. package/scripts/release-loop/ops-upgrade-node.ts +59 -6
  169. package/scripts/release-loop/publish-node.ts +4 -1
  170. package/scripts/release-loop/version-node.ts +13 -3
  171. package/scripts/telegram-inject.ts +14 -6
  172. package/scripts/test-deterministic.ts +53 -3
  173. package/scripts/test-diet/remeasure-list.ts +112 -0
  174. package/scripts/ux/demo-rehearsal.ts +17 -13
  175. package/scripts/ux/demo-seed.ts +108 -0
  176. package/scripts/ux/lib/rehearsal-verdict.ts +12 -5
  177. package/skills/omni-crawl/SKILL.md +16 -16
  178. package/skills/omni-crawl/package-lock.json +1 -544
  179. package/skills/omni-crawl/package.json +3 -4
  180. package/skills/omni-crawl/scripts/main.ts +13 -13
  181. package/skills/omni-crawl/scripts/monitor.ts +8 -8
  182. package/skills/omni-crawl/src/apify.ts +13 -2
  183. package/skills/omni-crawl/src/capture.ts +6 -0
  184. package/skills/omni-crawl/src/registry.ts +2 -2
  185. package/skills/omni-crawl/src/stdout-json.ts +4 -0
  186. package/skills/omni-digest/SKILL.md +10 -11
  187. package/skills/omni-digest/package-lock.json +0 -545
  188. package/skills/omni-digest/package.json +2 -5
  189. package/skills/omni-digest/scripts/main.ts +3 -3
  190. package/skills/omni-digest/src/delegate.ts +5 -5
  191. package/skills/omni-digest/src/fetch.ts +2 -2
  192. package/skills/omni-digest/src/media.ts +25 -11
  193. package/skills/omni-digest/src/visual.ts +3 -2
  194. package/skills/omni-market/scripts/main.ts +1 -1
  195. package/skills/omni-market/scripts/stdout-json.ts +4 -0
  196. package/src/agent/identity-env.ts +11 -0
  197. package/src/agent-mission/driver.ts +150 -23
  198. package/src/agent-mission/duo.ts +94 -0
  199. package/src/agent-mission/pty-decision.ts +2 -2
  200. package/src/autopilot/pty-control-loop.ts +4 -1
  201. package/src/autopilot/track-agent-shadow.ts +2 -1
  202. package/src/autopilot/track-agent.ts +4 -1
  203. package/src/boot/daemon-prompt-request.ts +3 -0
  204. package/src/boot/daemon-prompt-turn.ts +6 -2
  205. package/src/boot/daemon-runtime.ts +48 -16
  206. package/src/chat/index.ts +1 -0
  207. package/src/cli/decisions-cli.ts +51 -19
  208. package/src/cli/outputs-cli.ts +92 -0
  209. package/src/cli/persona-cli.ts +45 -13
  210. package/src/cli/pod-cli.ts +27 -3
  211. package/src/cli/pod-memory-advice.ts +35 -0
  212. package/src/cli/pr-cli.ts +15 -1
  213. package/src/cli/release-cli.ts +135 -15
  214. package/src/context-bus/condense.ts +194 -0
  215. package/src/context-bus/context-day.ts +82 -0
  216. package/src/context-bus/context-now-surfaces.ts +32 -2
  217. package/src/context-bus/context-now.ts +2 -2
  218. package/src/context-bus/coord-events.ts +2 -2
  219. package/src/context-bus/long-term-memory.ts +118 -22
  220. package/src/context-bus/nightly-condense.ts +153 -0
  221. package/src/context-bus/session-events.ts +35 -0
  222. package/src/dashboard/dashboard-status-lines.ts +2 -0
  223. package/src/dashboard/index.ts +8 -0
  224. package/src/dashboard/slash-runtime/dashboard-handlers.ts +24 -0
  225. package/src/decisions/decision-cards.ts +13 -7
  226. package/src/decisions/decision-ledger.ts +43 -17
  227. package/src/domains/outbound-alert.ts +99 -31
  228. package/src/domains/outbound-log-probe.ts +8 -0
  229. package/src/execution-loop/launch-gate.ts +17 -8
  230. package/src/field-note/telegram.ts +142 -0
  231. package/src/graph-kinds/registry.ts +2 -2
  232. package/src/graph-runner/graph-approval-card.ts +2 -2
  233. package/src/graph-runner/graph-grow-llm.ts +23 -13
  234. package/src/graph-runner/graph-grow.ts +69 -13
  235. package/src/graph-runner/graph-tick.ts +9 -2
  236. package/src/graph-runner/runner.ts +92 -24
  237. package/src/harness/harness-cli-command.ts +165 -17
  238. package/src/harness/harness-queue-child.ts +37 -0
  239. package/src/harness/harness-queue.ts +306 -0
  240. package/src/hooks/consult-intake.ts +153 -0
  241. package/src/hooks/github-shadow.ts +72 -2
  242. package/src/hooks/receiver.ts +82 -3
  243. package/src/hq/hq.ts +318 -0
  244. package/src/hq/lease.ts +210 -0
  245. package/src/hq/ledger-write-target.ts +41 -0
  246. package/src/index.ts +232 -16
  247. package/src/intake-plane/discord-seat-work.ts +36 -3
  248. package/src/intake-plane/linear-wish.ts +44 -0
  249. package/src/intake-plane/persona-answer.ts +5 -0
  250. package/src/intake-plane/telegram-seat-work.ts +13 -6
  251. package/src/intake-plane/wish-card.ts +33 -0
  252. package/src/llm/difficulty.ts +1 -1
  253. package/src/loops/checker.ts +178 -0
  254. package/src/loops/loop-cli.ts +37 -3
  255. package/src/loops/observe.ts +49 -0
  256. package/src/loops/orchestrator/overlap.ts +141 -0
  257. package/src/loops/orchestrator/traffic.ts +150 -0
  258. package/src/loops/package/verify.ts +125 -0
  259. package/src/loops/verdict.ts +22 -0
  260. package/src/maturity/feature-maturity.ts +5 -4
  261. package/src/memory.ts +37 -4
  262. package/src/nexus/api/card-followup-route.ts +118 -0
  263. package/src/nexus/api/decisions-route.ts +68 -0
  264. package/src/nexus/api/http-server.ts +78 -8
  265. package/src/nexus/api/outbound-report.ts +40 -7
  266. package/src/nexus/api/personas.ts +97 -54
  267. package/src/nexus/api/skill-exec-route.ts +97 -0
  268. package/src/nexus/api/task-cards-api.ts +19 -0
  269. package/src/nexus/api/workflow-history.ts +88 -0
  270. package/src/nexus/api/workflows.ts +52 -0
  271. package/src/nexus/outbound/delivery-ledger.ts +17 -8
  272. package/src/nexus/outbound/router.ts +8 -3
  273. package/src/persona/loader.ts +18 -0
  274. package/src/persona/persona-todo.ts +14 -9
  275. package/src/persona/presets.ts +10 -2
  276. package/src/persona/team.ts +105 -0
  277. package/src/persona/types.ts +4 -0
  278. package/src/plugins/maker/market-bundle.ts +2 -1
  279. package/src/pod-lease/host-lease.ts +130 -20
  280. package/src/release-loop/checklist.ts +15 -3
  281. package/src/release-loop/feature-store.ts +24 -10
  282. package/src/release-loop/landed-but-yellow.ts +60 -0
  283. package/src/seat-dispatch/seat-ask.ts +57 -12
  284. package/src/seat-loop/seat-loop.ts +253 -18
  285. package/src/seat-loop/seat-pack.ts +127 -0
  286. package/src/self-dev/ask-launch-flow.ts +3 -0
  287. package/src/self-dev/draft-sweep.ts +44 -10
  288. package/src/self-dev/draft-triage-rules.ts +54 -2
  289. package/src/self-implement/budget-gate.ts +29 -12
  290. package/src/self-implement/gate-candidate-ranking.ts +60 -0
  291. package/src/self-implement/gate-scope.ts +9 -8
  292. package/src/self-implement/headless-elanous-driver.ts +7 -2
  293. package/src/self-implement/l8-merge-queue.ts +157 -1
  294. package/src/self-implement/orchestrator.ts +15 -0
  295. package/src/self-implement/test-seams.ts +1 -0
  296. package/src/steward/launch.ts +5 -1
  297. package/src/steward/readiness.ts +125 -0
  298. package/src/steward/rescue.ts +75 -0
  299. package/src/steward/triage.ts +48 -9
  300. package/src/task-cards/card-cli.ts +78 -0
  301. package/src/task-orchestrator/surfaces/pod-command-job.ts +14 -10
  302. package/src/task-orchestrator/surfaces/pod-lease.ts +18 -6
  303. package/src/task-orchestrator/surfaces/pod-pool.ts +27 -10
  304. package/src/task-orchestrator/surfaces/self-implement-pod.ts +46 -8
  305. package/src/telegram-commands.ts +21 -0
  306. package/src/telegram-dispatch-forward.ts +1 -1
  307. package/src/telegram-fabric-plan.ts +90 -0
  308. package/src/telegram-project-command.ts +84 -4
  309. package/src/telegram.ts +37 -5
  310. package/src/tool-runtime/elanous-control-runtimes.ts +2 -1
  311. package/src/user-config.ts +101 -13
  312. package/src/version/packed-revision.json +1 -1
  313. package/src/workflow-runtime/executor.ts +4 -0
  314. package/src/workflow-runtime/node-catalog.ts +13 -0
  315. package/src/workflow-runtime/nodes/knowledge.ts +38 -0
  316. package/src/workflow-runtime/schema.ts +17 -1
  317. package/src/workflow-runtime/types.ts +13 -1
  318. package/apps/pwa/out/_next/static/chunks/2310-7a59f043f6db207e.js +0 -1
  319. package/apps/pwa/out/_next/static/chunks/2548-6e316138d347cefe.js +0 -1
  320. package/apps/pwa/out/_next/static/chunks/3869-a39d1d9921e10d43.js +0 -1
  321. package/apps/pwa/out/_next/static/chunks/507-5acd98bd6dab2d8a.js +0 -1
  322. package/apps/pwa/out/_next/static/chunks/5445-745eb795c5963d79.js +0 -1
  323. package/apps/pwa/out/_next/static/chunks/9906-482c6619b3fdda20.js +0 -1
  324. package/apps/pwa/out/_next/static/chunks/app/approvals/page-1dcdc58fc2de2454.js +0 -1
  325. package/apps/pwa/out/_next/static/chunks/app/board/page-cb95a7432c9ee744.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/app/botlab/page-b12b6d2f8d2d1aea.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/app/bots/page-b9efe868bfd36255.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/app/consult/page-8fd36984f549270a.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/app/exec/page-4cd1de371f8ad048.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/app/field/page-d99f89657828421c.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/app/inside/page-50cb2a0eb15e6c83.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/live/page-2a9932ddb62d76bd.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-6b15dd6456c5df09.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/not-found-e7a529e0507ac86c.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/page-94c82eb0ae5abb32.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/reflection/page-0be8d4b2ced75ba0.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/settings/page-53f80edc6b8dc07c.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/workspace/page-c6da2b468454532d.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-93bc70e36f595414.js +0 -1
  340. package/apps/pwa/out/_next/static/css/543f56bcb1f79fca.css +0 -1
  341. /package/apps/pwa/out/_next/static/{Y4zKzL62sakG5momz3SoQ → D51l8jUBplJrIpWvENsDK}/_buildManifest.js +0 -0
  342. /package/apps/pwa/out/_next/static/{Y4zKzL62sakG5momz3SoQ → D51l8jUBplJrIpWvENsDK}/_ssgManifest.js +0 -0
@@ -0,0 +1,153 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+
5
+ export const CONSULT_MAX_BYTES = 8 * 1024;
6
+ export const CONSULT_QUEUE_MAX_AGE_MS = 7 * 24 * 3600_000;
7
+
8
+ export interface ConsultInput {
9
+ name: string;
10
+ org?: string;
11
+ kind: 'company' | 'personal';
12
+ interest: 'A' | 'B';
13
+ contact: string;
14
+ consent: true;
15
+ }
16
+
17
+ export interface QueuedConsult { id: string; receivedAt: string; consult: ConsultInput }
18
+
19
+ const singleLine = (value: string): string => value.trim().replace(/[\r\n\u2028\u2029]+/g, ' ');
20
+
21
+ /** The Primary accepts optional org and requires nonblank name/contact, kind, interest and explicit consent. */
22
+ export function validateConsult(body: unknown): { ok: true; consult: ConsultInput } | { ok: false; field: string } {
23
+ if (!body || typeof body !== 'object' || Array.isArray(body)) return { ok: false, field: 'body' };
24
+ const b = body as Record<string, unknown>;
25
+ if (typeof b.name !== 'string' || !b.name.trim() || b.name.length > 256) return { ok: false, field: 'name' };
26
+ if (b.org !== undefined && (typeof b.org !== 'string' || !b.org.trim() || b.org.length > 256)) return { ok: false, field: 'org' };
27
+ if (b.kind !== 'company' && b.kind !== 'personal') return { ok: false, field: 'kind' };
28
+ if (b.interest !== 'A' && b.interest !== 'B') return { ok: false, field: 'interest' };
29
+ if (typeof b.contact !== 'string' || !b.contact.trim() || b.contact.length > 512) return { ok: false, field: 'contact' };
30
+ if (b.consent !== true) return { ok: false, field: 'consent' };
31
+ return { ok: true, consult: { name: singleLine(b.name), ...(typeof b.org === 'string' ? { org: singleLine(b.org) } : {}),
32
+ kind: b.kind, interest: b.interest, contact: b.contact.trim(), consent: true } };
33
+ }
34
+
35
+ /** Sliding windows; neither rejected hits nor invalid submissions consume quota. */
36
+ export class ConsultLimiter {
37
+ private readonly ips = new Map<string, number[]>();
38
+ private daily: number[] = [];
39
+ constructor(private readonly now: () => number = Date.now) {}
40
+
41
+ admit(ip: string): number | null {
42
+ const t = this.now();
43
+ const hits = (this.ips.get(ip) ?? []).filter(x => t - x < 600_000);
44
+ this.daily = this.daily.filter(x => t - x < 86_400_000);
45
+ const wait = hits.length >= 3 ? 600_000 - (t - hits[0]!) :
46
+ this.daily.length >= 200 ? 86_400_000 - (t - this.daily[0]!) : null;
47
+ if (wait !== null) { this.ips.set(ip, hits); return Math.max(1, Math.ceil(wait / 1000)); }
48
+ hits.push(t);
49
+ this.ips.set(ip, hits);
50
+ this.daily.push(t);
51
+ return null;
52
+ }
53
+ }
54
+
55
+ /** Atomic JSONL rewrite on every mutation, with a private file and directory. */
56
+ export class ConsultQueue {
57
+ readonly path: string;
58
+ constructor(root: string, private readonly log: (event: string, data: Record<string, unknown>) => void = () => {}) {
59
+ this.path = join(root, 'hooks', 'consult-queue.jsonl');
60
+ mkdirSync(dirname(this.path), { recursive: true, mode: 0o700 });
61
+ }
62
+
63
+ private read(): QueuedConsult[] {
64
+ try { return readFileSync(this.path, 'utf8').split('\n').filter(Boolean).map(line => JSON.parse(line) as QueuedConsult); }
65
+ catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return []; throw error; }
66
+ }
67
+
68
+ private write(items: QueuedConsult[]): void {
69
+ const temp = `${this.path}.${randomUUID()}.tmp`;
70
+ try {
71
+ const fd = openSync(temp, 'wx', 0o600);
72
+ try { writeSync(fd, items.map(item => JSON.stringify(item)).join('\n') + (items.length ? '\n' : '')); fsyncSync(fd); }
73
+ finally { closeSync(fd); }
74
+ renameSync(temp, this.path);
75
+ } finally { rmSync(temp, { force: true }); }
76
+ }
77
+
78
+ enqueue(item: QueuedConsult): void { this.write([...this.read(), item]); }
79
+ entries(now: number = Date.now()): QueuedConsult[] {
80
+ const items = this.read();
81
+ const kept = items.filter(item => Number.isFinite(Date.parse(item.receivedAt)) && now - Date.parse(item.receivedAt) <= CONSULT_QUEUE_MAX_AGE_MS);
82
+ if (kept.length !== items.length) { this.write(kept); this.log('dropped', { expired: items.length - kept.length }); }
83
+ return kept;
84
+ }
85
+ count(): number { return existsSync(this.path) ? this.read().length : 0; }
86
+ delivered(id: string): void { this.write(this.read().filter(item => item.id !== id)); }
87
+ }
88
+
89
+ /** Accept only a validated body; responses and logs never contain user-provided values. */
90
+ export async function handleConsultPost(request: Request, deps: {
91
+ limiter: ConsultLimiter; queue: ConsultQueue; now?: () => number; ip?: string;
92
+ log?: (event: string, data: Record<string, unknown>) => void; wake?: () => void;
93
+ }): Promise<Response> {
94
+ const log = deps.log ?? (() => {});
95
+ const declared = Number(request.headers.get('content-length') ?? '0');
96
+ if (declared > CONSULT_MAX_BYTES) { log('rejected', { reason: 'too-large' }); return Response.json({ error: 'too-large' }, { status: 413 }); }
97
+ const chunks: Uint8Array[] = [];
98
+ let size = 0;
99
+ const reader = request.body?.getReader();
100
+ try {
101
+ if (reader) {
102
+ while (true) {
103
+ const { done, value } = await reader.read();
104
+ if (done) break;
105
+ size += value.byteLength;
106
+ if (size > CONSULT_MAX_BYTES) {
107
+ await reader.cancel().catch(() => {});
108
+ log('rejected', { reason: 'too-large' });
109
+ return Response.json({ error: 'too-large' }, { status: 413 });
110
+ }
111
+ chunks.push(value);
112
+ }
113
+ }
114
+ } catch {
115
+ log('rejected', { reason: 'body' });
116
+ return Response.json({ error: 'invalid', field: 'body' }, { status: 400 });
117
+ } finally { reader?.releaseLock(); }
118
+ let body: unknown;
119
+ try {
120
+ const raw = new Uint8Array(size);
121
+ let offset = 0;
122
+ for (const chunk of chunks) { raw.set(chunk, offset); offset += chunk.byteLength; }
123
+ body = JSON.parse(new TextDecoder().decode(raw));
124
+ }
125
+ catch { log('rejected', { reason: 'body' }); return Response.json({ error: 'invalid', field: 'body' }, { status: 400 }); }
126
+ const checked = validateConsult(body);
127
+ if (!checked.ok) { log('rejected', { reason: checked.field }); return Response.json({ error: 'invalid', field: checked.field }, { status: 400 }); }
128
+ const ip = deps.ip ?? 'unknown';
129
+ const wait = deps.limiter.admit(ip);
130
+ if (wait !== null) {
131
+ log('rejected', { reason: 'rate-limited' });
132
+ return Response.json({ error: 'rate-limited', retryAfterSec: wait }, { status: 429, headers: { 'Retry-After': String(wait) } });
133
+ }
134
+ const id = randomUUID();
135
+ try { deps.queue.enqueue({ id, receivedAt: new Date((deps.now ?? Date.now)()).toISOString(), consult: checked.consult }); }
136
+ catch { log('rejected', { reason: 'queue-unavailable' }); return Response.json({ error: 'queue-unavailable' }, { status: 503 }); }
137
+ log('received', { queued: true, kind: checked.consult.kind, interest: checked.consult.interest,
138
+ nameLength: checked.consult.name.length, contactLength: checked.consult.contact.length, orgLength: checked.consult.org?.length ?? 0 });
139
+ deps.wake?.();
140
+ return Response.json({ id }, { status: 202 });
141
+ }
142
+
143
+ /** Forward in arrival order; a failure leaves that item and all following items queued. */
144
+ export async function drainConsults(queue: ConsultQueue, forward: (item: QueuedConsult) => Promise<boolean>, now: number = Date.now()): Promise<{ delivered: number; failed: number }> {
145
+ let delivered = 0;
146
+ let failed = 0;
147
+ for (const item of queue.entries(now)) {
148
+ let ok = false;
149
+ try { ok = await forward(item); } catch { ok = false; }
150
+ if (ok) { queue.delivered(item.id); delivered++; } else { failed++; break; }
151
+ }
152
+ return { delivered, failed };
153
+ }
@@ -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,9 +4,10 @@ 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
+ import { ConsultLimiter, ConsultQueue, drainConsults, handleConsultPost, type QueuedConsult } from './consult-intake.js';
10
11
  import { effectiveInstanceRoot } from '../instance/resolve.js';
11
12
 
12
13
  export interface HookSecrets {
@@ -27,8 +28,15 @@ export interface HookReceiverOptions {
27
28
  wakeSeat?: HookWake;
28
29
  /** ER2 — forward one queued error report to the Primary; default = POST /v1/reports/ingest over the tailnet. */
29
30
  forwardReport?: (item: ReceivedReport) => Promise<boolean>;
31
+ /** CS1 — forward a queued consultation to the Primary's consult ledger. */
32
+ forwardConsult?: (item: QueuedConsult) => Promise<boolean>;
33
+ /** CS1 — the public consult route is a write path from the open internet, so it stays closed (404) until
34
+ * `hooks.consult.enabled === true` in user config (대표 decision D-20261003-09). Tests pass it directly. */
35
+ consultEnabled?: boolean;
30
36
  /** GET-only public metadata lookup; failure forces human review. */
31
37
  githubFetch?: typeof fetch;
38
+ /** Test seam for the isolated read-only self review; never publishes externally. */
39
+ githubShadowReview?: (number: number) => Promise<ShadowReviewResult>;
32
40
  }
33
41
 
34
42
  /** `/v1/tasks` lives on the Primary's nexus API, not on the control plane — so the address is its own setting. */
@@ -53,11 +61,66 @@ async function forwardReportToPrimary(item: ReceivedReport): Promise<boolean> {
53
61
  return response.ok;
54
62
  }
55
63
 
64
+ async function forwardConsultToPrimary(item: QueuedConsult): Promise<boolean> {
65
+ const config = JSON.parse(readFileSync(userConfigPath(), 'utf8')) as { hooks?: { primaryTokenRef?: string; primaryUrl?: unknown } };
66
+ const ref = config.hooks?.primaryTokenRef;
67
+ if (!ref) throw new Error('primary token reference missing');
68
+ const token = await getSecretAsync(ref);
69
+ if (!token) throw new Error('primary token missing');
70
+ const response = await fetch(new URL('/v1/consult-requests', hooksPrimaryUrl(config)), {
71
+ method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` },
72
+ body: JSON.stringify(item.consult), signal: AbortSignal.timeout(10_000),
73
+ });
74
+ return response.ok;
75
+ }
76
+
77
+ /** Raw read like the report forwarder: only an explicit `hooks.consult.enabled: true` opens the route. */
78
+ function consultRouteEnabled(): boolean {
79
+ try {
80
+ const config = JSON.parse(readFileSync(userConfigPath(), 'utf8')) as { hooks?: { consult?: { enabled?: unknown } } };
81
+ return config.hooks?.consult?.enabled === true;
82
+ } catch {
83
+ return false;
84
+ }
85
+ }
86
+
87
+ function consultOrigin(origin: string | null): string | null {
88
+ if (!origin) return null;
89
+ try {
90
+ const url = new URL(origin);
91
+ if (url.protocol !== 'https:' || url.origin !== origin) return null;
92
+ return url.hostname === 'elanous.ai' || url.hostname === 'www.elanous.ai' ||
93
+ /^[a-z0-9-]+\.vercel\.app$/i.test(url.hostname) ? origin : null;
94
+ } catch { return null; }
95
+ }
96
+
56
97
  export function startHookReceiver(options: HookReceiverOptions): { url: string; stop: () => void; queue: HookQueue } {
57
98
  const queue = new HookQueue(options.root);
58
99
  const reportQueue = new ReportQueue(options.root ?? effectiveInstanceRoot(), (event, data) => debug.log('hooks.reports', event, data));
59
100
  const reportLimiter = new ReportLimiter(options.now ?? Date.now);
60
101
  const forwardReport = options.forwardReport ?? forwardReportToPrimary;
102
+ const consultEnabled = options.consultEnabled ?? consultRouteEnabled();
103
+ debug.log('hooks.consult', consultEnabled ? 'route-open' : 'route-closed', {});
104
+ const consultQueue = new ConsultQueue(options.root ?? effectiveInstanceRoot(), (_event, data) =>
105
+ debug.log('hooks.consult', 'rejected', { reason: 'expired', ...data }));
106
+ const consultLimiter = new ConsultLimiter(options.now ?? Date.now);
107
+ const forwardConsult = options.forwardConsult ?? forwardConsultToPrimary;
108
+ let consultFailures = 0;
109
+ let consultNextAt = 0;
110
+ let consultsDraining = false;
111
+ async function drainConsultQueue(): Promise<void> {
112
+ if (stopped || consultsDraining || Date.now() < consultNextAt) return;
113
+ consultsDraining = true;
114
+ try {
115
+ const result = await drainConsults(consultQueue, forwardConsult, (options.now ?? Date.now)());
116
+ if (result.delivered) debug.log('hooks.consult', 'forwarded', { queued: consultQueue.count(), delivered: result.delivered });
117
+ if (result.failed) {
118
+ consultFailures++;
119
+ consultNextAt = Date.now() + Math.min(300_000, Math.max(1, options.retryBaseMs ?? 1000) * 2 ** Math.min(consultFailures - 1, 20));
120
+ debug.log('hooks.consult', 'forward-failed', { queued: consultQueue.count(), reason: 'delivery-failed' });
121
+ } else consultFailures = 0;
122
+ } finally { consultsDraining = false; }
123
+ }
61
124
  let reportFailures = 0;
62
125
  let reportNextAt = 0;
63
126
  let reportsDraining = false;
@@ -79,7 +142,7 @@ export function startHookReceiver(options: HookReceiverOptions): { url: string;
79
142
  // L14: public-repository intake events take the shadow ledger path; every other event keeps the seat dispatch path.
80
143
  const dispatch = options.forward ?? ((event: QueuedHook) => dispatchHook(event, options.root ?? effectiveInstanceRoot(), events, options.wakeSeat).then(() => 204));
81
144
  const forward = (event: QueuedHook) => event.provider === 'github' && event.task.github
82
- ? recordGithubShadow(event, options.root ?? effectiveInstanceRoot(), options.githubFetch).then(() => 204)
145
+ ? recordGithubShadow(event, options.root ?? effectiveInstanceRoot(), options.githubFetch, options.githubShadowReview).then(() => 204)
83
146
  : dispatch(event);
84
147
  const attempts = new Map<string, { next: number; failures: number }>();
85
148
  const pending = new Set<string>();
@@ -111,18 +174,34 @@ export function startHookReceiver(options: HookReceiverOptions): { url: string;
111
174
  }
112
175
  } finally {
113
176
  void drainReportQueue();
177
+ void drainConsultQueue();
114
178
  draining = false;
115
179
  if (!stopped) timer = setTimeout(() => { void drain(); }, Math.min(1000, options.retryBaseMs ?? 1000));
116
180
  }
117
181
  }
118
182
  const server = Bun.serve({ hostname: options.host ?? '127.0.0.1', port: options.port,
119
- async fetch(request) {
183
+ async fetch(request, server) {
120
184
  const url = new URL(request.url);
121
185
  if (request.method === 'GET' && url.pathname === '/hooks/health')
122
186
  return Response.json({ ok: true, queued: queue.count(), reportsQueued: reportQueue.count() });
123
187
  if (request.method === 'POST' && url.pathname === '/v1/reports')
124
188
  return handleReportPost(request, { limiter: reportLimiter, queue: reportQueue, ...(options.now ? { now: options.now } : {}),
125
189
  log: (event, data) => debug.log('hooks.reports', event, data), wake: () => { reportNextAt = 0; void drainReportQueue(); } });
190
+ if (consultEnabled && url.pathname === '/v1/consult' && (request.method === 'POST' || request.method === 'OPTIONS')) {
191
+ const origin = consultOrigin(request.headers.get('origin'));
192
+ if (!origin) {
193
+ debug.log('hooks.consult', 'rejected', { reason: 'origin' });
194
+ return Response.json({ error: 'forbidden' }, { status: 403 });
195
+ }
196
+ const headers = { 'Access-Control-Allow-Origin': origin, Vary: 'Origin',
197
+ 'Access-Control-Allow-Methods': 'POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type' };
198
+ if (request.method === 'OPTIONS') return new Response(null, { status: 204, headers });
199
+ const response = await handleConsultPost(request, { limiter: consultLimiter, queue: consultQueue, ...(options.now ? { now: options.now } : {}),
200
+ ip: server.requestIP(request)?.address ?? 'unknown',
201
+ log: (event, data) => debug.log('hooks.consult', event, data), wake: () => { consultNextAt = 0; void drainConsultQueue(); } });
202
+ for (const [key, value] of Object.entries(headers)) response.headers.set(key, value);
203
+ return response;
204
+ }
126
205
  if (request.method !== 'POST' || !/^\/hooks\/(linear|asana|github)$/.test(url.pathname)) return new Response('Not Found', { status: 404 });
127
206
  const provider = url.pathname.slice('/hooks/'.length) as HookProvider;
128
207
  const reject = (reason: string) => {