elanous 0.2.10 → 0.2.11

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 (584) 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/Y4zKzL62sakG5momz3SoQ/_buildManifest.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/1159-0c5ef8bc21cb6afb.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/1394-556de6d71bbdb0fb.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/1452.93700483b9d4fc61.js +9 -0
  7. package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.js → 1829-b453a1b22d3999bb.js} +1 -1
  8. package/apps/pwa/out/_next/static/chunks/2176-c379e3ebce874151.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/2310-7a59f043f6db207e.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/2426.8c6ee0c668d53def.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/2548-6e316138d347cefe.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/3170-7ec60a56fa0e4728.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/3278-ae6ef084769f6195.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/3454-988a53dac60a1763.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/3638-aa80b4641ea439db.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/3735-fe3402553ac1c9c6.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/3869-a39d1d9921e10d43.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/390.c373e31b3b9d0c41.js +5 -0
  19. package/apps/pwa/out/_next/static/chunks/3992-2e522a8ac5f2afde.js +25 -0
  20. package/apps/pwa/out/_next/static/chunks/4215-c9d91dd644ec1719.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/4508-5d0161d1e724d735.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/4685-c3c1c625b71accd1.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/4733-623a79345b171e1c.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/4883.3bd969ea1d3aaaff.js +2 -0
  25. package/apps/pwa/out/_next/static/chunks/507-5acd98bd6dab2d8a.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/5445-745eb795c5963d79.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/5532.6899a12958a89b72.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/{8067-87ce253ec2f23697.js → 5845-e6c003ce8d5fd36a.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/5978-b27004c677119115.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/{8046cadf-211ca3c0e00f2b72.js → 5cd7e3a9-404fd6b8d0ecd44c.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/6048-4c5c33579bc4ef78.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/6303-422dbf0f67c72aac.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/6751-bb3595948b2e311b.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/7274-19b7279f18b47ab3.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/{4323.c5dfb6f503304775.js → 7334.a6679d9168f37aa8.js} +1 -1
  36. package/apps/pwa/out/_next/static/chunks/7551-a0e0707b79164ddd.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/7777-79f845045aa1ed86.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/79f8be9e-957d68913b590b08.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/8025-cc24ee5d8f2fb6f8.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/8039-df92e08fede5de8f.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/8206-c6684dcde12be721.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/8421-1c4acfb1395a0edf.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/8605-ea5bee9dfae512c7.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/8725.bf46bb1395dd0fa2.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/8734-252d511cd276c0dc.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/8834-ca1cb09c1cb1a6bf.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/{2415.ed6428ff334c7ce2.js → 9269.d2a4795bc6812717.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/9538-2816b90923606459.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/9656-c1540088f5859c80.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/9906-482c6619b3fdda20.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-d16993889b125c42.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/approvals/page-1dcdc58fc2de2454.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-6a061bd97f0b9398.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/board/page-cb95a7432c9ee744.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-54b32bd27726588d.js → page-b12b6d2f8d2d1aea.js} +1 -1
  56. package/apps/pwa/out/_next/static/chunks/app/bots/page-b9efe868bfd36255.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/chat/page-7c97dcdfe2e37a60.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/consult/page-8fd36984f549270a.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/control/page-5721537ca8a3598c.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/design-check/page-b654240a572e4c48.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/editor/page-b9750abc897508c3.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/exec/page-4cd1de371f8ad048.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/field/page-d99f89657828421c.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/healthz/route-d16993889b125c42.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/inside/page-50cb2a0eb15e6c83.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/intake/page-c8b1ccda6ba7ae98.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/layout-d6b1d156fb95e74b.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/live/page-2a9932ddb62d76bd.js +1 -0
  69. package/apps/pwa/out/_next/static/chunks/app/market/page-9688f5b4f1ff950f.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-6b15dd6456c5df09.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/morning/page-35414e0a007b4082.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/not-found-e7a529e0507ac86c.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/observatory/page-e001823c35c9dd67.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-371423d1a573d64b.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-4b764b125339ac17.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cdf8e4b01067bcf1.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/outputs/page-3a24072b35470157.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/page-94c82eb0ae5abb32.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/reflection/page-0be8d4b2ced75ba0.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-d37cb0d52f9b6bd5.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/sessions/page-686da0df9bf23e5c.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-4dc614f3ce043e63.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/settings/page-53f80edc6b8dc07c.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-d02c782fc792e2af.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/app/setup/layout-d16993889b125c42.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/app/setup/page-239c17b911b84e72.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/app/share/page-dc6a79fb003f746c.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/app/showroom/page-0fffb84cafdc9da6.js +1 -0
  89. package/apps/pwa/out/_next/static/chunks/app/tasks/page-1218753c82d585f6.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/app/term/page-4b5935e67531efb5.js +1 -0
  91. package/apps/pwa/out/_next/static/chunks/app/today/page-3285bc87caf1eaa5.js +1 -0
  92. package/apps/pwa/out/_next/static/chunks/app/trace/page-ad553b0e06505ecd.js +1 -0
  93. package/apps/pwa/out/_next/static/chunks/app/vault/page-fca096e3e860306f.js +1 -0
  94. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-9ebfa78f0d9885c3.js +1 -0
  95. package/apps/pwa/out/_next/static/chunks/app/workflows/page-3dc5f21f7e39622d.js +1 -0
  96. package/apps/pwa/out/_next/static/chunks/app/workspace/page-c6da2b468454532d.js +1 -0
  97. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-93bc70e36f595414.js +1 -0
  98. package/apps/pwa/out/_next/static/chunks/{2ea96958-e8c7e112de50b067.js → f48170cc-d05d28bf87a43bf4.js} +1 -1
  99. package/apps/pwa/out/_next/static/chunks/framework-172630e6aa5e2d7f.js +1 -0
  100. package/apps/pwa/out/_next/static/chunks/main-app-f0fe2d7477845181.js +1 -0
  101. package/apps/pwa/out/_next/static/chunks/main-fc98ac70b44eccb0.js +1 -0
  102. package/apps/pwa/out/_next/static/chunks/pages/_app-5bd7f10271f72413.js +1 -0
  103. package/apps/pwa/out/_next/static/chunks/pages/_error-97d0d85ea05fef9b.js +1 -0
  104. package/apps/pwa/out/_next/static/chunks/webpack-9e4d02d567b98240.js +1 -0
  105. package/apps/pwa/out/_next/static/css/543f56bcb1f79fca.css +1 -0
  106. package/apps/pwa/out/approvals/index.html +1 -1
  107. package/apps/pwa/out/approvals/index.txt +23 -23
  108. package/apps/pwa/out/autopilot/index.html +1 -1
  109. package/apps/pwa/out/autopilot/index.txt +23 -23
  110. package/apps/pwa/out/board/index.html +1 -1
  111. package/apps/pwa/out/board/index.txt +23 -23
  112. package/apps/pwa/out/botlab/index.html +1 -1
  113. package/apps/pwa/out/botlab/index.txt +22 -22
  114. package/apps/pwa/out/bots/index.html +1 -1
  115. package/apps/pwa/out/bots/index.txt +22 -22
  116. package/apps/pwa/out/chat/index.html +1 -1
  117. package/apps/pwa/out/chat/index.txt +22 -22
  118. package/apps/pwa/out/consult/index.html +1 -1
  119. package/apps/pwa/out/consult/index.txt +23 -23
  120. package/apps/pwa/out/control/index.html +1 -1
  121. package/apps/pwa/out/control/index.txt +23 -23
  122. package/apps/pwa/out/design-check/index.html +1 -1
  123. package/apps/pwa/out/design-check/index.txt +23 -23
  124. package/apps/pwa/out/editor/index.html +1 -1
  125. package/apps/pwa/out/editor/index.txt +23 -23
  126. package/apps/pwa/out/exec/index.html +1 -1
  127. package/apps/pwa/out/exec/index.txt +23 -23
  128. package/apps/pwa/out/field/index.html +1 -1
  129. package/apps/pwa/out/field/index.txt +23 -23
  130. package/apps/pwa/out/healthz +1 -1
  131. package/apps/pwa/out/index.html +1 -1
  132. package/apps/pwa/out/index.txt +22 -22
  133. package/apps/pwa/out/inside/index.html +1 -0
  134. package/apps/pwa/out/inside/index.txt +31 -0
  135. package/apps/pwa/out/intake/index.html +1 -1
  136. package/apps/pwa/out/intake/index.txt +23 -23
  137. package/apps/pwa/out/live/index.html +1 -1
  138. package/apps/pwa/out/live/index.txt +23 -23
  139. package/apps/pwa/out/market/index.html +1 -1
  140. package/apps/pwa/out/market/index.txt +22 -22
  141. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  142. package/apps/pwa/out/missions/placeholder/index.txt +22 -22
  143. package/apps/pwa/out/morning/index.html +1 -1
  144. package/apps/pwa/out/morning/index.txt +23 -23
  145. package/apps/pwa/out/observatory/index.html +1 -1
  146. package/apps/pwa/out/observatory/index.txt +23 -23
  147. package/apps/pwa/out/ops/checklist/index.html +1 -1
  148. package/apps/pwa/out/ops/checklist/index.txt +22 -22
  149. package/apps/pwa/out/ops/release/index.html +1 -1
  150. package/apps/pwa/out/ops/release/index.txt +22 -22
  151. package/apps/pwa/out/ops/seats/index.html +1 -1
  152. package/apps/pwa/out/ops/seats/index.txt +22 -22
  153. package/apps/pwa/out/outputs/index.html +1 -0
  154. package/apps/pwa/out/outputs/index.txt +30 -0
  155. package/apps/pwa/out/reflection/index.html +1 -1
  156. package/apps/pwa/out/reflection/index.txt +23 -23
  157. package/apps/pwa/out/scheduler/index.html +1 -1
  158. package/apps/pwa/out/scheduler/index.txt +23 -23
  159. package/apps/pwa/out/sessions/index.html +1 -1
  160. package/apps/pwa/out/sessions/index.txt +22 -22
  161. package/apps/pwa/out/settings/devices/index.html +1 -1
  162. package/apps/pwa/out/settings/devices/index.txt +23 -23
  163. package/apps/pwa/out/settings/index.html +1 -1
  164. package/apps/pwa/out/settings/index.txt +23 -23
  165. package/apps/pwa/out/setup/done/index.html +1 -1
  166. package/apps/pwa/out/setup/done/index.txt +23 -23
  167. package/apps/pwa/out/setup/index.html +1 -1
  168. package/apps/pwa/out/setup/index.txt +23 -23
  169. package/apps/pwa/out/share/index.html +1 -1
  170. package/apps/pwa/out/share/index.txt +23 -23
  171. package/apps/pwa/out/showroom/index.html +1 -1
  172. package/apps/pwa/out/showroom/index.txt +22 -22
  173. package/apps/pwa/out/tasks/index.html +1 -1
  174. package/apps/pwa/out/tasks/index.txt +23 -23
  175. package/apps/pwa/out/term/index.html +1 -1
  176. package/apps/pwa/out/term/index.txt +23 -24
  177. package/apps/pwa/out/today/index.html +1 -1
  178. package/apps/pwa/out/today/index.txt +23 -23
  179. package/apps/pwa/out/trace/index.html +1 -1
  180. package/apps/pwa/out/trace/index.txt +23 -23
  181. package/apps/pwa/out/vault/index.html +1 -1
  182. package/apps/pwa/out/vault/index.txt +23 -23
  183. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  184. package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
  185. package/apps/pwa/out/workflows/index.html +1 -1
  186. package/apps/pwa/out/workflows/index.txt +23 -23
  187. package/apps/pwa/out/workspace/index.html +1 -1
  188. package/apps/pwa/out/workspace/index.txt +23 -24
  189. package/apps/pwa/out/worktrees/index.html +1 -1
  190. package/apps/pwa/out/worktrees/index.txt +23 -23
  191. package/graphs/drafts/draft-cleanup.yaml +19 -0
  192. package/graphs/drafts/recipes.yaml +23 -0
  193. package/graphs/intake/intake-daily.yaml +2 -1
  194. package/graphs/ops/recipes.yaml +3 -0
  195. package/graphs/ops/secret-exposure.yaml +24 -0
  196. package/graphs/release/recipes.yaml +3 -0
  197. package/graphs/release/release-loop.yaml +4 -1
  198. package/graphs/video/explainer-line.yaml +2 -2
  199. package/package.json +2 -1
  200. package/plugins/elanous-hwp/README.md +25 -0
  201. package/plugins/elanous-hwp/cli.ts +22 -0
  202. package/plugins/elanous-hwp/converter.ts +237 -0
  203. package/plugins/elanous-hwp/fixtures/fixed-controls.hwpx +0 -0
  204. package/plugins/elanous-hwp/fixtures/fixed-table-controls.hwpx +0 -0
  205. package/plugins/elanous-hwp/fixtures/fixed.hwpx +0 -0
  206. package/plugins/elanous-hwp/plugin.json +16 -0
  207. package/plugins/elanous-hwp/plugin.ts +40 -0
  208. package/scripts/ci-public-export-leak-gate.ts +75 -14
  209. package/scripts/ci-public-leak-gate.ts +44 -9
  210. package/scripts/context-hooks/claude-stop.sh +4 -0
  211. package/scripts/context-hooks/emit.ts +24 -0
  212. package/scripts/context-hooks/post-tool-use.sh +4 -0
  213. package/scripts/coord-channel-watch.sh +12 -0
  214. package/scripts/coord-requests-bridge.ts +143 -0
  215. package/scripts/field-feed/lib.ts +4 -0
  216. package/scripts/fleet/node-setup.sh +20 -3
  217. package/scripts/homebrew-formula-bump.ts +69 -0
  218. package/scripts/marketing/measure-site-numbers.ts +169 -0
  219. package/scripts/measure-gate-shards.ts +309 -0
  220. package/scripts/mission-request-judge.ts +31 -1
  221. package/scripts/morning-report.ts +2 -2
  222. package/scripts/opportunity-scan.ts +1 -1
  223. package/scripts/release-loop/auto-approve-node.ts +21 -4
  224. package/scripts/release-loop/cut-branch.ts +84 -0
  225. package/scripts/release-loop/gate-node.ts +14 -13
  226. package/scripts/release-loop/mac-smoke-node.ts +229 -0
  227. package/scripts/release-loop/node-verdict.ts +3 -3
  228. package/scripts/release-loop/tui-node-checks.ts +42 -0
  229. package/scripts/release-loop/tui-sim-node.ts +32 -8
  230. package/scripts/release-loop/unattended-release.ts +5 -3
  231. package/scripts/release-loop/version-node.ts +61 -10
  232. package/scripts/signal-digest-cycle.ts +2 -7
  233. package/scripts/signal-digest-photo.ts +11 -0
  234. package/scripts/tech1-public-measure.ts +94 -0
  235. package/scripts/test-diet/node.ts +1 -1
  236. package/scripts/tui-regress.ts +2 -2
  237. package/scripts/ux/demo-rehearsal.ts +243 -0
  238. package/scripts/ux/lib/rehearsal-verdict.ts +73 -0
  239. package/skills/diagram-master/SKILL.md +366 -0
  240. package/skills/diagram-master/references/engines/excalidraw/color-palette.md +67 -0
  241. package/skills/diagram-master/references/engines/excalidraw/element-templates.md +182 -0
  242. package/skills/diagram-master/references/engines/excalidraw/json-schema.md +71 -0
  243. package/skills/diagram-master/references/engines/excalidraw/render_excalidraw.py +189 -0
  244. package/skills/diagram-master/references/engines/excalidraw/render_template.html +57 -0
  245. package/skills/diagram-master/references/engines/matplotlib/matplotlib-patterns.md +235 -0
  246. package/skills/diagram-master/references/engines/matplotlib/render_matplotlib.py +184 -0
  247. package/skills/diagram-master/references/engines/mermaid/domain-templates.md +435 -0
  248. package/skills/diagram-master/references/engines/mermaid/render_mermaid.py +116 -0
  249. package/skills/diagram-master/references/engines/mermaid/syntax-reference.md +2247 -0
  250. package/skills/diagram-master/references/engines/nanobannana/render_nanobannana2.py +168 -0
  251. package/skills/diagram-master/references/engines/svg/color-palette.md +188 -0
  252. package/skills/diagram-master/references/engines/svg/html-base-template.html +332 -0
  253. package/skills/diagram-master/references/engines/svg/interactivity-patterns.md +435 -0
  254. package/skills/diagram-master/references/engines/svg/render_interactive_svg.py +212 -0
  255. package/skills/diagram-master/references/engines/svg/svg-element-templates.md +349 -0
  256. package/skills/diagram-master/references/examples/README.md +69 -0
  257. package/skills/diagram-master/references/examples/ref_grid_architecture.png +0 -0
  258. package/skills/diagram-master/references/examples/ref_layered_flow.png +0 -0
  259. package/skills/diagram-master/references/pyproject.toml +16 -0
  260. package/skills/diagram-master/references/quality-checklist.md +33 -0
  261. package/skills/diagram-master/references/routing-guide.md +164 -0
  262. package/skills/diagram-master/references/uv.lock +1356 -0
  263. package/skills/google-workspace/SKILL.md +33 -0
  264. package/skills/google-workspace/scripts/gws-safe.sh +177 -0
  265. package/skills/grill-me/SKILL.md +1 -1
  266. package/skills/lecture-note-digitizer/SKILL.md +926 -0
  267. package/skills/lecture-note-digitizer/references/convert_to_pdf.py +30 -0
  268. package/skills/lecture-note-digitizer/references/extract_slide_diagram.py +73 -0
  269. package/skills/lecture-note-digitizer/references/html-template.md +167 -0
  270. package/skills/lecture-note-digitizer/references/katex-style-guide.md +165 -0
  271. package/skills/omni-crawl/SKILL.md +21 -11
  272. package/skills/omni-crawl/scripts/main.ts +6 -6
  273. package/skills/omni-crawl/src/router.ts +3 -3
  274. package/skills/omni-digest/SKILL.md +17 -9
  275. package/skills/omni-digest/digest.md +2 -2
  276. package/skills/omni-digest/src/fetch.ts +30 -3
  277. package/skills/pitch-deck/SKILL.md +2 -2
  278. package/skills/pitch-deck/template.html +2 -2
  279. package/skills/youtube-master/SKILL.md +163 -0
  280. package/skills/youtube-master/package-lock.json +558 -0
  281. package/skills/youtube-master/package.json +13 -0
  282. package/skills/youtube-master/references/study-note-template.md +124 -0
  283. package/skills/youtube-master/scripts/main.ts +435 -0
  284. package/skills/youtube-master/scripts/transcribe-local.ts +24 -0
  285. package/skills/youtube-master/src/audio.ts +63 -0
  286. package/skills/youtube-master/src/domain-staging.ts +50 -0
  287. package/skills/youtube-master/src/env.ts +36 -0
  288. package/skills/youtube-master/src/obsidian.ts +155 -0
  289. package/skills/youtube-master/src/router.ts +110 -0
  290. package/skills/youtube-master/src/stt.ts +199 -0
  291. package/skills/youtube-master/src/study-note.ts +175 -0
  292. package/skills/youtube-master/src/summarize.ts +358 -0
  293. package/skills/youtube-master/src/transcript.ts +76 -0
  294. package/skills/youtube-master/src/types.ts +91 -0
  295. package/skills/youtube-master/src/util.ts +93 -0
  296. package/skills/youtube-master/src/youtube.ts +70 -0
  297. package/skills/youtube-master/tsconfig.json +13 -0
  298. package/src/acp/server.ts +5 -2
  299. package/src/agent-mission/driver.ts +80 -10
  300. package/src/agent-mission/pty-decision.ts +71 -0
  301. package/src/agent-mission/review-loop.ts +30 -0
  302. package/src/autopilot/build/llm-conflict-merge.ts +16 -3
  303. package/src/autopilot/tool-registry.ts +2 -0
  304. package/src/boot/daemon-prompt-turn.ts +5 -0
  305. package/src/boot/daemon-runtime.ts +9 -3
  306. package/src/boot/daemon-tools/web-search.ts +3 -0
  307. package/src/card-followup/core.ts +109 -0
  308. package/src/chat/index.ts +1 -0
  309. package/src/cli/drive-input-jail.ts +101 -0
  310. package/src/cli/hooks-cli.ts +17 -3
  311. package/src/cli/make-cli.ts +43 -0
  312. package/src/cli/persona-cli.ts +90 -4
  313. package/src/cli/plugin-cli.ts +31 -22
  314. package/src/cli/pod-cli.ts +19 -9
  315. package/src/cli/pr-cli.ts +51 -15
  316. package/src/cli/pty-drive-cli.ts +41 -7
  317. package/src/cli/pty-drive-verdict.ts +40 -0
  318. package/src/cli/release-cli.ts +17 -4
  319. package/src/cli/setup-cli.ts +43 -1
  320. package/src/cli/skills-cli.ts +17 -1
  321. package/src/context-bus/claude-hooks-install.ts +10 -0
  322. package/src/context-bus/context-now-surfaces.ts +48 -0
  323. package/src/context-bus/context-now.ts +104 -0
  324. package/src/context-bus/coord-events.ts +2 -2
  325. package/src/context-bus/external-events.ts +52 -0
  326. package/src/context-bus/long-term-memory.ts +108 -0
  327. package/src/dashboard/index.ts +9 -4
  328. package/src/dashboard/slash-runtime/dashboard-handlers.ts +7 -1
  329. package/src/decisions/decision-cards.ts +10 -3
  330. package/src/decisions/decision-ledger.ts +13 -5
  331. package/src/discord/sprint21-runtime.ts +5 -1
  332. package/src/discord-self-message.ts +32 -0
  333. package/src/discord-slash-wire.ts +5 -1
  334. package/src/discord.ts +17 -1
  335. package/src/domains/core-tools.ts +3 -2
  336. package/src/domains/outbound-alert.ts +6 -1
  337. package/src/domains/telegram-kind-route.ts +51 -13
  338. package/src/exec-requests/default-outputs.ts +25 -0
  339. package/src/exec-requests/planner.ts +10 -2
  340. package/src/exec-requests/runner.ts +20 -6
  341. package/src/exec-requests/store.ts +3 -0
  342. package/src/execution-loop/launch-gate.ts +100 -0
  343. package/src/field/field-reel.ts +5 -1
  344. package/src/field-note/index.ts +201 -0
  345. package/src/field-note/transcribe.ts +248 -0
  346. package/src/graph-kinds/registry.ts +3 -3
  347. package/src/graph-runner/graph-cli.ts +61 -1
  348. package/src/graph-runner/graph-grow-llm.ts +37 -0
  349. package/src/graph-runner/graph-grow.ts +47 -0
  350. package/src/graph-runner/runner.ts +317 -25
  351. package/src/grok/agent-search.ts +8 -2
  352. package/src/guardian/secret-exposure-scan.ts +101 -0
  353. package/src/harness/harness-cli-command.ts +247 -7
  354. package/src/harness/harness-pod-dispatch.ts +43 -23
  355. package/src/harness/harness-seams.ts +38 -4
  356. package/src/harness/harness-stop.ts +23 -4
  357. package/src/harness/launch-registry.ts +91 -0
  358. package/src/harness/process-garbage.ts +5 -3
  359. package/src/hooks/dispatch.ts +82 -0
  360. package/src/hooks/expose.ts +254 -0
  361. package/src/hooks/github-shadow.ts +108 -0
  362. package/src/hooks/providers.ts +71 -5
  363. package/src/hooks/queue.ts +1 -1
  364. package/src/hooks/receiver.ts +21 -20
  365. package/src/index.ts +52 -14
  366. package/src/intake-plane/categorize.ts +18 -46
  367. package/src/intake-plane/discord-seat-work.ts +10 -0
  368. package/src/intake-plane/front-route-classifier.ts +33 -39
  369. package/src/intake-plane/persona-answer.ts +30 -0
  370. package/src/intake-plane/runtime-callables.ts +8 -1
  371. package/src/intake-plane/telegram-seat-work.ts +36 -2
  372. package/src/intake-plane/wish-folder.ts +67 -0
  373. package/src/intent-gate/gate.ts +30 -10
  374. package/src/live/detail-switch.ts +5 -0
  375. package/src/llm/difficulty.ts +57 -0
  376. package/src/llm/judge-layer.ts +101 -0
  377. package/src/llm/mission-router.ts +33 -19
  378. package/src/loops/contract/examples/cmo-seat.loop.yaml +51 -0
  379. package/src/loops/contract/examples/orchestrator.loop.yaml +57 -0
  380. package/src/loops/contract/examples/steward.loop.yaml +50 -0
  381. package/src/loops/contract/loop-agent.schema.json +138 -0
  382. package/src/loops/contract/validate.ts +64 -0
  383. package/src/loops/registry.ts +20 -6
  384. package/src/market/signed-index.ts +4 -0
  385. package/src/maturity/bot-command-maturity.ts +45 -0
  386. package/src/maturity/feature-maturity.ts +26 -4
  387. package/src/model-tier/index.ts +2 -2
  388. package/src/model-tier/tier-resolver.ts +20 -20
  389. package/src/model-tier/types.ts +6 -6
  390. package/src/mss/logging/log-store.ts +15 -0
  391. package/src/nexus/api/config-model-tier.ts +5 -4
  392. package/src/nexus/api/fabric-plans.ts +53 -0
  393. package/src/nexus/api/harness-api.ts +5 -1
  394. package/src/nexus/api/health.ts +2 -0
  395. package/src/nexus/api/http-server.ts +36 -0
  396. package/src/nexus/api/inside-events.ts +226 -0
  397. package/src/nexus/api/meta-api.ts +21 -8
  398. package/src/nexus/api/missions.ts +6 -1
  399. package/src/nexus/api/ops-seats.ts +33 -0
  400. package/src/nexus/api/outputs-route.ts +44 -0
  401. package/src/nexus/api/seat-requests.ts +44 -2
  402. package/src/nexus/api/sessions-store.ts +17 -38
  403. package/src/nexus/api/sub-seat-charters.ts +34 -0
  404. package/src/nexus/api/tabs-mutations.ts +3 -1
  405. package/src/nexus/api/trace.ts +18 -6
  406. package/src/nexus/api/workflow-run-event-bridge.ts +1 -0
  407. package/src/nexus/api/workflows.ts +80 -6
  408. package/src/nexus/index.ts +2 -0
  409. package/src/nexus/kinds/webterm.ts +2 -1
  410. package/src/nexus/webterm/pty.ts +6 -2
  411. package/src/onboarding/cli-auth-status.ts +80 -0
  412. package/src/onboarding/voice-ai.ts +3 -3
  413. package/src/org/seat-budget.ts +48 -0
  414. package/src/org/stall-escalation.ts +63 -0
  415. package/src/outputs/ledger.ts +120 -0
  416. package/src/persona/persona-todo.ts +79 -0
  417. package/src/persona/presets.ts +116 -2
  418. package/src/persona/team-set.ts +155 -0
  419. package/src/plugins/install/plugin-install.ts +14 -0
  420. package/src/plugins/maker/market-bundle.ts +109 -0
  421. package/src/plugins/maker/plugin-maker.ts +254 -11
  422. package/src/plugins/maker/wizard-generate.ts +103 -0
  423. package/src/pod-lease/admission.ts +150 -0
  424. package/src/pod-lease/dependency-state.ts +85 -0
  425. package/src/pod-lease/host-lease.ts +127 -0
  426. package/src/project/project-suggestion.ts +16 -0
  427. package/src/project/session-context.ts +24 -0
  428. package/src/release-loop/checklist.ts +2 -2
  429. package/src/release-loop/feature-store.ts +5 -3
  430. package/src/release-loop/next-md-merge.ts +58 -0
  431. package/src/release-loop/release-note.ts +59 -4
  432. package/src/seat-dispatch/seat-ask.ts +130 -0
  433. package/src/seat-dispatch/seat-questions.ts +100 -0
  434. package/src/seat-loop/seat-loop.ts +548 -29
  435. package/src/seat-loop/seat-report.ts +37 -2
  436. package/src/self-dev/dev-cli.ts +4 -1
  437. package/src/self-dev/dev-pipeline.ts +37 -1
  438. package/src/self-dev/draft-sweep.ts +38 -33
  439. package/src/self-dev/draft-triage-rules.ts +8 -5
  440. package/src/self-dev/fabric-plan-core.ts +297 -0
  441. package/src/self-dev/orchestrate.ts +36 -4
  442. package/src/self-dev/release-path-guard.ts +62 -0
  443. package/src/self-dev/shard-aggregate.ts +230 -0
  444. package/src/self-dev/shard-placement.ts +60 -0
  445. package/src/self-dev/shard-plan.ts +48 -0
  446. package/src/self-dev/shard-pod-run.ts +77 -0
  447. package/src/self-dev/shard-run.ts +105 -0
  448. package/src/self-implement/gate-baseline.ts +1 -1
  449. package/src/self-implement/gate-cli.ts +40 -1
  450. package/src/self-implement/gate-scope.ts +37 -7
  451. package/src/self-implement/graph-variant.ts +171 -0
  452. package/src/self-implement/harness-policy-gates.ts +1 -1
  453. package/src/self-implement/headless-elanous-driver.ts +22 -3
  454. package/src/self-implement/host-regate.ts +22 -1
  455. package/src/self-implement/importer-test-index.ts +2 -1
  456. package/src/self-implement/l8-merge-queue.ts +130 -0
  457. package/src/self-implement/orchestrator.ts +242 -9
  458. package/src/self-implement/route-consumer-test-index.ts +122 -0
  459. package/src/self-implement/seams.ts +72 -11
  460. package/src/self-implement/test-seams.ts +1 -0
  461. package/src/self-implement/uncommitted-work.ts +59 -0
  462. package/src/session/index.ts +36 -13
  463. package/src/skills/gift-install.ts +215 -0
  464. package/src/skills/tools/web-search.ts +4 -1
  465. package/src/steward/launch.ts +42 -26
  466. package/src/steward/steward-cards.ts +17 -9
  467. package/src/steward/triage.ts +46 -14
  468. package/src/task-cards/card-cli.ts +25 -1
  469. package/src/task-orchestrator/mission.ts +5 -0
  470. package/src/task-orchestrator/store.ts +12 -3
  471. package/src/task-orchestrator/surfaces/pod-command-job.ts +29 -7
  472. package/src/task-orchestrator/surfaces/pod-lease.ts +156 -19
  473. package/src/task-orchestrator/surfaces/pod-pool.ts +127 -26
  474. package/src/task-orchestrator/surfaces/self-implement-pod.ts +214 -40
  475. package/src/task-orchestrator/surfaces/self-implement.ts +15 -2
  476. package/src/task-orchestrator/types.ts +2 -0
  477. package/src/telegram-card-followup.ts +16 -96
  478. package/src/telegram-commands.ts +10 -1
  479. package/src/telegram-project-command.ts +71 -0
  480. package/src/telegram-report.ts +21 -17
  481. package/src/telegram.ts +29 -3
  482. package/src/tool-runtime/self-cognition-runtimes.ts +13 -3
  483. package/src/user-config.ts +102 -20
  484. package/src/version/packed-revision.json +1 -1
  485. package/src/web-search/grok.ts +7 -1
  486. package/src/web-search/index.ts +28 -1
  487. package/src/web-search/provider.ts +4 -0
  488. package/src/web-search/x-intent.ts +5 -0
  489. package/src/workflow-runtime/executor.ts +277 -47
  490. package/src/workflow-runtime/index.ts +1 -0
  491. package/src/workflow-runtime/nodes/subworkflow.ts +60 -0
  492. package/src/workflow-runtime/plugin-kind-node.ts +1 -1
  493. package/src/workflow-runtime/schema.ts +79 -4
  494. package/src/workflow-runtime/types.ts +35 -4
  495. package/apps/pwa/out/_next/static/2DsiDfDCks44g1cPUMhiW/_buildManifest.js +0 -1
  496. package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +0 -1
  497. package/apps/pwa/out/_next/static/chunks/1377-1efeffbbac0963ff.js +0 -1
  498. package/apps/pwa/out/_next/static/chunks/1470.98a6ab61ccb4131c.js +0 -1
  499. package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +0 -1
  500. package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +0 -1
  501. package/apps/pwa/out/_next/static/chunks/2375-cb6debea56cce4fb.js +0 -1
  502. package/apps/pwa/out/_next/static/chunks/24-e6d28380efab7faf.js +0 -1
  503. package/apps/pwa/out/_next/static/chunks/2977-e38442e86289f1d3.js +0 -1
  504. package/apps/pwa/out/_next/static/chunks/2982-2a2df543be3d2e5a.js +0 -25
  505. package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +0 -5
  506. package/apps/pwa/out/_next/static/chunks/3615-740dd270764ffa24.js +0 -1
  507. package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +0 -1
  508. package/apps/pwa/out/_next/static/chunks/3734-1015fe991eb9b7e7.js +0 -1
  509. package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +0 -1
  510. package/apps/pwa/out/_next/static/chunks/5012-a7fe6f3c8b6fe45a.js +0 -1
  511. package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +0 -1
  512. package/apps/pwa/out/_next/static/chunks/5783-ef40ebfc91d55230.js +0 -1
  513. package/apps/pwa/out/_next/static/chunks/5791-bb82743fa6299562.js +0 -1
  514. package/apps/pwa/out/_next/static/chunks/62-ff227c3edb8b248b.js +0 -1
  515. package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.js +0 -1
  516. package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +0 -1
  517. package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +0 -1
  518. package/apps/pwa/out/_next/static/chunks/7633-1b3709b0854c9412.js +0 -1
  519. package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.js +0 -1
  520. package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +0 -1
  521. package/apps/pwa/out/_next/static/chunks/7951-8b03dbb10b24cd06.js +0 -1
  522. package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +0 -1
  523. package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +0 -1
  524. package/apps/pwa/out/_next/static/chunks/8195-6fde3d7a39859206.js +0 -10
  525. package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +0 -1
  526. package/apps/pwa/out/_next/static/chunks/8894-b5fb36bab837cd29.js +0 -1
  527. package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +0 -1
  528. package/apps/pwa/out/_next/static/chunks/9454-84d0d5d77446bd92.js +0 -1
  529. package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +0 -1
  530. package/apps/pwa/out/_next/static/chunks/988-5f92e7ab7ccea3dd.js +0 -1
  531. package/apps/pwa/out/_next/static/chunks/9989-5582582f06429840.js +0 -1
  532. package/apps/pwa/out/_next/static/chunks/ae787e4c-f2998de7f1ba72a4.js +0 -1
  533. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +0 -1
  534. package/apps/pwa/out/_next/static/chunks/app/approvals/page-d2253d7e58ef1890.js +0 -1
  535. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-5441d4a9ab35e518.js +0 -1
  536. package/apps/pwa/out/_next/static/chunks/app/board/page-d9b12d680922d980.js +0 -1
  537. package/apps/pwa/out/_next/static/chunks/app/bots/page-e268d6d145827a22.js +0 -1
  538. package/apps/pwa/out/_next/static/chunks/app/chat/page-f8116559c7424da7.js +0 -1
  539. package/apps/pwa/out/_next/static/chunks/app/consult/page-1085399fd0f32112.js +0 -1
  540. package/apps/pwa/out/_next/static/chunks/app/control/page-1df686a14f65895a.js +0 -1
  541. package/apps/pwa/out/_next/static/chunks/app/design-check/page-54e1f966ab60a4dc.js +0 -1
  542. package/apps/pwa/out/_next/static/chunks/app/editor/page-7ed95f296f5ee8e9.js +0 -1
  543. package/apps/pwa/out/_next/static/chunks/app/exec/page-11edac668d77e5c3.js +0 -1
  544. package/apps/pwa/out/_next/static/chunks/app/field/page-59e2b9a22cd5a6d5.js +0 -1
  545. package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +0 -1
  546. package/apps/pwa/out/_next/static/chunks/app/intake/page-2976df21ad74ab48.js +0 -1
  547. package/apps/pwa/out/_next/static/chunks/app/layout-34d211a67f16d510.js +0 -1
  548. package/apps/pwa/out/_next/static/chunks/app/live/page-8ff652d5d950639b.js +0 -1
  549. package/apps/pwa/out/_next/static/chunks/app/market/page-b31610deda76337b.js +0 -1
  550. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-6cb0e1bbefe5fb3d.js +0 -1
  551. package/apps/pwa/out/_next/static/chunks/app/morning/page-a286a152cf584576.js +0 -1
  552. package/apps/pwa/out/_next/static/chunks/app/not-found-d9237581e7003bcb.js +0 -1
  553. package/apps/pwa/out/_next/static/chunks/app/observatory/page-83274e761643fdd2.js +0 -1
  554. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-ceee6cfb1d4e6d68.js +0 -1
  555. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-9c9f8bb3e4a64091.js +0 -1
  556. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c17217603ea3426a.js +0 -1
  557. package/apps/pwa/out/_next/static/chunks/app/page-efb6177cbf1e8a51.js +0 -1
  558. package/apps/pwa/out/_next/static/chunks/app/reflection/page-d307523703bc4df3.js +0 -1
  559. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-462f4e4bab45ce7d.js +0 -1
  560. package/apps/pwa/out/_next/static/chunks/app/sessions/page-8990e96f33af3837.js +0 -1
  561. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e168fa1030af0548.js +0 -1
  562. package/apps/pwa/out/_next/static/chunks/app/settings/page-1233e05a4068bc9c.js +0 -1
  563. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-13a1840b1fac337a.js +0 -1
  564. package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +0 -1
  565. package/apps/pwa/out/_next/static/chunks/app/setup/page-64a29fc80b11ab55.js +0 -1
  566. package/apps/pwa/out/_next/static/chunks/app/share/page-68e787339a16b3dc.js +0 -1
  567. package/apps/pwa/out/_next/static/chunks/app/showroom/page-4fddc90b23feb215.js +0 -1
  568. package/apps/pwa/out/_next/static/chunks/app/tasks/page-886ba8260dc79c79.js +0 -1
  569. package/apps/pwa/out/_next/static/chunks/app/term/page-0f26507a1722e40b.js +0 -1
  570. package/apps/pwa/out/_next/static/chunks/app/today/page-109d4ccd42259c9d.js +0 -1
  571. package/apps/pwa/out/_next/static/chunks/app/trace/page-f5c2a4e510726774.js +0 -1
  572. package/apps/pwa/out/_next/static/chunks/app/vault/page-b2e0039416f42522.js +0 -1
  573. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-26102fd7350f171e.js +0 -1
  574. package/apps/pwa/out/_next/static/chunks/app/workflows/page-708948bc8a802794.js +0 -1
  575. package/apps/pwa/out/_next/static/chunks/app/workspace/page-5e5611620f78fb0b.js +0 -1
  576. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-139ba5c367bbf60a.js +0 -1
  577. package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +0 -1
  578. package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +0 -1
  579. package/apps/pwa/out/_next/static/chunks/main-app-202b4e81f33895ea.js +0 -1
  580. package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +0 -1
  581. package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +0 -1
  582. package/apps/pwa/out/_next/static/chunks/webpack-e5bea36f183b25e6.js +0 -1
  583. package/apps/pwa/out/_next/static/css/43feca5fd387dcbd.css +0 -1
  584. /package/apps/pwa/out/_next/static/{2DsiDfDCks44g1cPUMhiW → Y4zKzL62sakG5momz3SoQ}/_ssgManifest.js +0 -0
@@ -0,0 +1,91 @@
1
+ /** Host-wide run → process binding for `harness stop`, independent of the launching worktree and its universe.
2
+ * A run launched from another worktree writes `pid.json` and its ledger under that tree's state dir, so a stop from
3
+ * any other tree found «no pid.json · goal path 0» while the launcher was alive (10-03 · 09-29). The binding below lives
4
+ * in a per-user directory under the OS temp dir — it only has to outlive the process it names, not a reboot. */
5
+ import { closeSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
6
+ import { tmpdir, userInfo } from 'node:os';
7
+ import { isAbsolute, join, resolve } from 'node:path';
8
+ import { randomUUID } from 'node:crypto';
9
+ import { debug } from '../debug/log.js';
10
+
11
+ export interface LaunchBinding {
12
+ readonly pid: number;
13
+ /** Same approximation as `pid.json` (`Date.now() − uptime`) — compared with `ps` start time before any signal. */
14
+ readonly startedAt: number;
15
+ readonly argv0?: string;
16
+ /** The launching process's working directory (absolute). */
17
+ readonly cwd: string;
18
+ /** Absolute goal document path when the launcher was given one (`--goal-file`/`--ask`/`--goal`). */
19
+ readonly goalPath?: string;
20
+ /** The `self-dev-runs` directory that holds this run's `pid.json` — tells a stop which universe launched it. */
21
+ readonly runsDir?: string;
22
+ }
23
+
24
+ const RUN_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
25
+
26
+ /** One directory per OS user: `$TMPDIR` is per-user on macOS, `/tmp` is shared on Linux. */
27
+ export function launchRegistryDir(): string {
28
+ let uid: string;
29
+ try { uid = String(userInfo().uid); } catch { uid = 'unknown'; }
30
+ return join(tmpdir(), `elanous-harness-launches-${uid}`);
31
+ }
32
+
33
+ /** The goal document named in an argv, resolved against `cwd` — the same option set `harness stop` already matches. */
34
+ export function goalPathFromArgv(argv: readonly string[], cwd: string): string | undefined {
35
+ for (let i = 0; i < argv.length; i++) {
36
+ const arg = argv[i]!;
37
+ let value: string | undefined;
38
+ for (const flag of ['--goal-file', '--ask', '--goal']) {
39
+ if (arg === flag) value = argv[i + 1];
40
+ else if (arg.startsWith(`${flag}=`)) value = arg.slice(flag.length + 1);
41
+ }
42
+ if (value?.trim()) return isAbsolute(value) ? resolve(value) : resolve(cwd, value);
43
+ }
44
+ return undefined;
45
+ }
46
+
47
+ export function writeLaunchBinding(runId: string, binding: LaunchBinding, dir = launchRegistryDir()): string | null {
48
+ if (!RUN_ID.test(runId)) return null;
49
+ try {
50
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
51
+ const path = join(dir, `${runId}.json`);
52
+ const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
53
+ const fd = openSync(temporary, 'wx', 0o600);
54
+ try { writeFileSync(fd, JSON.stringify(binding)); } finally { closeSync(fd); }
55
+ renameSync(temporary, path);
56
+ debug.log('harness.launch-registry', 'bound', { runId, pid: binding.pid, goalPath: binding.goalPath ?? null });
57
+ return path;
58
+ } catch (error) {
59
+ debug.log('harness.launch-registry', 'bind-failed', { runId, error: String(error) }, { level: 'warn' });
60
+ return null;
61
+ }
62
+ }
63
+
64
+ export function readLaunchBinding(runId: string, dir = launchRegistryDir()): LaunchBinding | null {
65
+ if (!RUN_ID.test(runId)) return null;
66
+ try {
67
+ const value = JSON.parse(readFileSync(join(dir, `${runId}.json`), 'utf8')) as Partial<LaunchBinding>;
68
+ if (!Number.isSafeInteger(value.pid) || (value.pid as number) <= 0 || !Number.isFinite(value.startedAt)) return null;
69
+ if (typeof value.cwd !== 'string' || !isAbsolute(value.cwd)) return null;
70
+ return {
71
+ pid: value.pid as number, startedAt: value.startedAt as number, cwd: value.cwd,
72
+ ...(typeof value.argv0 === 'string' ? { argv0: value.argv0 } : {}),
73
+ ...(typeof value.goalPath === 'string' && isAbsolute(value.goalPath) ? { goalPath: value.goalPath } : {}),
74
+ ...(typeof value.runsDir === 'string' && isAbsolute(value.runsDir) ? { runsDir: value.runsDir } : {}),
75
+ };
76
+ } catch { return null; }
77
+ }
78
+
79
+ export function removeLaunchBinding(runId: string, dir = launchRegistryDir()): void {
80
+ if (!RUN_ID.test(runId)) return;
81
+ try { unlinkSync(join(dir, `${runId}.json`)); }
82
+ catch (error) {
83
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') debug.log('harness.launch-registry', 'unbind-failed', { runId, error: String(error) }, { level: 'warn' });
84
+ }
85
+ }
86
+
87
+ /** Run IDs with a live-looking binding — used for stop's prefix resolution next to ledgers and `pid.json` names. */
88
+ export function launchBindingRunIds(dir = launchRegistryDir()): string[] {
89
+ try { return readdirSync(dir).filter((name) => name.endsWith('.json')).map((name) => name.slice(0, -5)).filter((id) => RUN_ID.test(id)); }
90
+ catch { return []; }
91
+ }
@@ -12,7 +12,9 @@ export interface GarbageProcess extends Pick<GarbageProcessRow, 'pid' | 'ppid' |
12
12
 
13
13
  const TEST_PATH = /elanous|repo\.worktrees|self-impl|\.elanous-test|gate-baseline/i;
14
14
  const TEST_DAEMON = /(?:^|[\s/])daemon\.ts(?:\s|$)/;
15
- const TEST_RUNNER = /\bbun\s+test\b|\bgate(?:[-\s]baseline|\b)/i;
15
+ const TEST_RUNNER = /^(?:\S*\/)?bun\s+test(?:\s|$)/i;
16
+ // Only the command itself counts — `bun [run|x] <…gate…>`; goal text later in argv must not (PROC1).
17
+ const GATE_RUNNER = /^(?:\S*\/)?bun\s+(?:(?:run|x)\s+)?\S*gate(?:[-\s]baseline|\b)/i;
16
18
  const NEXUS_RUN = /\bnexus\s+run\b/;
17
19
  const TEST_MARKER = /\.elanous-test|(?:^|\s)--test(?:\s|$)/;
18
20
 
@@ -20,7 +22,7 @@ const TEST_MARKER = /\.elanous-test|(?:^|\s)--test(?:\s|$)/;
20
22
  export function isGarbageProcessTarget(command: string): boolean {
21
23
  return command.includes('elanous.mjs')
22
24
  || (TEST_DAEMON.test(command) && /elanous-nexus-|\.elanous-test/.test(command))
23
- || (TEST_RUNNER.test(command) && TEST_PATH.test(command));
25
+ || ((TEST_RUNNER.test(command) || GATE_RUNNER.test(command)) && TEST_PATH.test(command));
24
26
  }
25
27
 
26
28
  export function classifyGarbage(
@@ -36,7 +38,7 @@ export function classifyGarbage(
36
38
  const reason: GarbageProcess['reason'] = TEST_DAEMON.test(row.command)
37
39
  && /elanous-nexus-|\.elanous-test/.test(row.command)
38
40
  ? 'orphan-test-daemon'
39
- : TEST_RUNNER.test(row.command) && TEST_PATH.test(row.command)
41
+ : (TEST_RUNNER.test(row.command) || GATE_RUNNER.test(row.command)) && TEST_PATH.test(row.command)
40
42
  ? 'orphan-test-runner'
41
43
  : 'orphan-elanous';
42
44
  garbage.push({ pid: row.pid, ppid: row.ppid, elapsedSeconds: row.elapsedSeconds, command: row.command, reason });
@@ -0,0 +1,82 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { join } from 'node:path';
3
+ import { debug } from '../debug/log.js';
4
+ import { runSeatLoopOnce } from '../seat-loop/seat-loop.js';
5
+ import { getUserConfig } from '../user-config.js';
6
+ import { openMsgStore } from '../msg/msg-store.js';
7
+ import type { EventsConfig } from '../user-config.js';
8
+ import type { QueuedHook } from './queue.js';
9
+
10
+ export type HookWake = (seat: string) => Promise<unknown>;
11
+
12
+ // A crashed wake can be reclaimed after the lease expires; a live wake renews it until completion.
13
+ const WAKE_LEASE_MS = 120_000;
14
+ const DEFAULT_SEAT_ROUTES = [
15
+ { source: 'linear', kind: 'Issue:update', seat: (event: QueuedHook, config: EventsConfig) =>
16
+ event.task.assigneeId ? config.linearAssignees[event.task.assigneeId] : undefined },
17
+ { source: 'github', kind: 'pull_request:review_requested', seat: 'TC' },
18
+ { source: 'github', kind: 'check_run:completed', seat: 'TC' },
19
+ { source: 'github', kind: 'check_suite:completed', seat: 'TC' },
20
+ ] as const;
21
+
22
+ /** Record a work card and inbox delivery atomically. Wake state: 0 = retryable, 2 = leased, 1 = done, 3 = seat loop off (terminal).
23
+ * `events.mode` decides only whether the card is delivered; the wake runs the seat loop under its own `loops.seat` mode.
24
+ * So a delivered card with the seat loop off is not retried (the card waits in the inbox), and a shadow loop's wake is
25
+ * recorded as `wake_status = 'shadow'` — visible as «not executed», not mistaken for a real run. */
26
+ export async function dispatchHook(event: QueuedHook, root: string, config: EventsConfig,
27
+ wake: HookWake = seat => runSeatLoopOnce(seat, { root, config: getUserConfig().loops?.seat ?? { mode: 'off' } }),
28
+ now: () => number = Date.now): Promise<void> {
29
+ const key = `${event.provider}:${event.eventId}`;
30
+ const route = config.routes.find(candidate => candidate.source === event.provider && candidate.kind === event.kind);
31
+ const defaultSeat = DEFAULT_SEAT_ROUTES.find(candidate => candidate.source === event.provider && candidate.kind === event.kind)?.seat;
32
+ const defaultResolvedSeat = typeof defaultSeat === 'function' ? defaultSeat(event, config) : defaultSeat;
33
+ if (event.provider === 'linear' && event.kind === 'Issue:update' && !route && !defaultResolvedSeat)
34
+ throw new Error('Linear issue update assignee is not mapped to a seat');
35
+ const seat = route?.seat ?? defaultResolvedSeat ?? 'OP';
36
+ const store = openMsgStore(join(root, 'msg', 'messages.db'));
37
+ const claim = randomUUID();
38
+ try {
39
+ store.db.exec(`CREATE TABLE IF NOT EXISTS hook_work_cards (
40
+ event_key TEXT PRIMARY KEY, source TEXT NOT NULL, kind TEXT NOT NULL,
41
+ seat TEXT NOT NULL, loop TEXT NOT NULL, mode TEXT NOT NULL, title TEXT NOT NULL,
42
+ message_id INTEGER, woken INTEGER NOT NULL DEFAULT 0,
43
+ wake_claim TEXT, wake_lease_until INTEGER
44
+ )`);
45
+ const columns = store.db.query('PRAGMA table_info(hook_work_cards)').all() as Array<{ name: string }>;
46
+ if (!columns.some(column => column.name === 'wake_claim')) store.db.exec('ALTER TABLE hook_work_cards ADD COLUMN wake_claim TEXT');
47
+ if (!columns.some(column => column.name === 'wake_lease_until')) store.db.exec('ALTER TABLE hook_work_cards ADD COLUMN wake_lease_until INTEGER');
48
+ if (!columns.some(column => column.name === 'wake_status')) store.db.exec('ALTER TABLE hook_work_cards ADD COLUMN wake_status TEXT');
49
+ store.db.transaction(() => {
50
+ const existing = store.db.query('SELECT event_key FROM hook_work_cards WHERE event_key = ?').get(key);
51
+ if (existing) return;
52
+ const live = config.mode === 'on';
53
+ let messageId: number | null = null;
54
+ if (live) {
55
+ const body = `[작업 카드 · ${key} · ${event.kind ?? 'unknown'} · loop: seat] ${event.task.title}\n${event.task.external.url ?? event.task.external.ref}`;
56
+ messageId = store.append({ from: 'HOOKS', to: seat, kind: 'hook-task', body }).id;
57
+ }
58
+ store.db.query(`INSERT INTO hook_work_cards(event_key, source, kind, seat, loop, mode, title, message_id)
59
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(key, event.provider, event.kind ?? 'unknown', seat, route?.loop ?? 'seat', live ? 'on' : 'shadow', event.task.title, messageId);
60
+ })();
61
+ const leaseAt = now();
62
+ const acquired = store.db.query(`UPDATE hook_work_cards SET woken = 2, wake_claim = ?, wake_lease_until = ?
63
+ WHERE event_key = ? AND message_id IS NOT NULL
64
+ AND (woken = 0 OR (woken = 2 AND (wake_lease_until IS NULL OR wake_lease_until <= ?)))`)
65
+ .run(claim, leaseAt + WAKE_LEASE_MS, key, leaseAt).changes === 1;
66
+ if (!acquired) return;
67
+ const renew = () => store.db.query(`UPDATE hook_work_cards SET wake_lease_until = ?
68
+ WHERE event_key = ? AND woken = 2 AND wake_claim = ?`).run(now() + WAKE_LEASE_MS, key, claim);
69
+ const heartbeat = setInterval(renew, WAKE_LEASE_MS / 4);
70
+ try {
71
+ const result = await wake(seat);
72
+ const status = result && typeof result === 'object' && 'status' in result && typeof result.status === 'string' ? result.status : 'woken';
73
+ // Seat loop off is a policy, not a transient failure: keep the card in the inbox and stop retrying.
74
+ const final = status === 'skipped-off' ? 3 : 1;
75
+ store.db.query('UPDATE hook_work_cards SET woken = ?, wake_status = ?, wake_claim = NULL, wake_lease_until = NULL WHERE event_key = ? AND woken = 2 AND wake_claim = ?').run(final, status, key, claim);
76
+ debug.log('hooks.dispatch', 'wake', { key, seat, status, retry: false });
77
+ } catch (error) {
78
+ store.db.query('UPDATE hook_work_cards SET woken = 0, wake_claim = NULL, wake_lease_until = NULL WHERE event_key = ? AND woken = 2 AND wake_claim = ?').run(key, claim);
79
+ throw error;
80
+ } finally { clearInterval(heartbeat); }
81
+ } finally { store.close(); }
82
+ }
@@ -0,0 +1,254 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
3
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { elanousStateRoot } from '../autopilot/state-paths.js';
6
+ import { debug } from '../debug/log.js';
7
+ import { DecisionLedger, type DecisionEntry } from '../decisions/decision-ledger.js';
8
+
9
+ export const PUBLIC_HOOK_PATH = '/hooks/linear';
10
+ export const PUBLIC_GITHUB_HOOK_PATH = '/hooks/github';
11
+ export const PUBLIC_HOOK_PORT = 31481;
12
+ const HTTPS_PORT = 8443;
13
+ export const HOOK_EXPOSURE_REF = 'hooks.expose:/hooks/linear';
14
+ const ON_ARGS = ['funnel', '--bg', `--https=${HTTPS_PORT}`, String(PUBLIC_HOOK_PORT)];
15
+ const OFF_ARGS = ['funnel', '--bg', `--https=${HTTPS_PORT}`, 'off'];
16
+ const PROBE_PATH = '/.hooks-ingress-identity';
17
+ const identityFile = (stateDir?: string) => join(stateDir ?? elanousStateRoot(), 'hooks', 'ingress-identity.json');
18
+ interface IngressIdentity { port: number; secret: string }
19
+
20
+ /** A fresh, private challenge key belongs to the listener that actually bound the port. */
21
+ function readIngressIdentity(stateDir?: string): IngressIdentity {
22
+ const identity = JSON.parse(readFileSync(identityFile(stateDir), 'utf8')) as IngressIdentity;
23
+ if (!Number.isInteger(identity.port) || identity.port < 1 || identity.port > 65535 ||
24
+ typeof identity.secret !== 'string' || !/^[a-f0-9]{64}$/.test(identity.secret)) throw new Error('invalid ingress identity');
25
+ return identity;
26
+ }
27
+
28
+ async function verifyIngress(stateDir?: string, port = PUBLIC_HOOK_PORT): Promise<boolean> {
29
+ let identity: IngressIdentity;
30
+ try { identity = readIngressIdentity(stateDir); } catch { return false; }
31
+ if (identity.port !== port) return false;
32
+ const nonce = randomBytes(32).toString('hex');
33
+ try {
34
+ const response = await fetch(`http://127.0.0.1:${port}${PROBE_PATH}`, {
35
+ headers: { 'x-hooks-ingress-challenge': nonce,
36
+ 'x-hooks-ingress-proof': createHmac('sha256', identity.secret).update(`request:${nonce}`).digest('hex') },
37
+ signal: AbortSignal.timeout(1500),
38
+ });
39
+ if (response.status !== 200) return false;
40
+ const expected = createHmac('sha256', identity.secret).update(`response:${nonce}`).digest();
41
+ const actual = await response.text();
42
+ return /^[a-f0-9]{64}$/.test(actual) && timingSafeEqual(Buffer.from(actual, 'hex'), expected);
43
+ } catch { return false; }
44
+ }
45
+
46
+ export interface HookExposureState { path: typeof PUBLIC_HOOK_PATH; decisionId: string; openedAt: string }
47
+ export interface HookExposureDeps {
48
+ stateDir?: string;
49
+ decision?: (id: string) => DecisionEntry;
50
+ execute?: (binary: string, args: readonly string[]) => void;
51
+ now?: () => Date;
52
+ ingressPort?: number;
53
+ /** Local port the 8443 Funnel currently proxies to, or null (none / unreadable). Default reads `tailscale funnel status --json`. */
54
+ funnelTarget?: () => number | null;
55
+ }
56
+
57
+ /** The loopback port a 8443 Funnel handler proxies to — the only evidence that an unrecorded Funnel is ours. */
58
+ export function parseFunnelTarget(statusJson: string): number | null {
59
+ let status: unknown;
60
+ try { status = JSON.parse(statusJson); } catch { return null; }
61
+ const web = (status as { Web?: Record<string, { Handlers?: Record<string, { Proxy?: unknown }> }> } | null)?.Web;
62
+ if (!web || typeof web !== 'object') return null;
63
+ for (const [host, entry] of Object.entries(web)) {
64
+ if (!host.endsWith(`:${HTTPS_PORT}`)) continue;
65
+ for (const handler of Object.values(entry?.Handlers ?? {})) {
66
+ const match = typeof handler?.Proxy === 'string' ? /^http:\/\/(?:127\.0\.0\.1|localhost):(\d+)\/?$/.exec(handler.Proxy) : null;
67
+ if (match) return Number(match[1]);
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+
73
+ function readFunnelTarget(): number | null {
74
+ const result = spawnSync('tailscale', ['funnel', 'status', '--json'], { encoding: 'utf8' });
75
+ if (result.error || result.status !== 0) return null;
76
+ return parseFunnelTarget(result.stdout);
77
+ }
78
+
79
+ function run(binary: string, args: readonly string[]): void {
80
+ const result = spawnSync(binary, [...args], { encoding: 'utf8' });
81
+ if (result.error || result.status !== 0) throw new Error(`tailscale command failed${result.error ? `: ${result.error.message}` : ''}`);
82
+ }
83
+
84
+ /** Funnel targets a dedicated loopback listener, never the multi-route hooks receiver or NEXUS. */
85
+ export function hookExposurePlan(): { path: string; local: string; decisionRef: string; on: string; off: string } {
86
+ const plan = { path: PUBLIC_HOOK_PATH, local: `http://127.0.0.1:${PUBLIC_HOOK_PORT}`,
87
+ decisionRef: HOOK_EXPOSURE_REF, on: `tailscale ${ON_ARGS.join(' ')}`, off: `tailscale ${OFF_ARGS.join(' ')}` };
88
+ debug.log('hooks.expose', 'plan', { path: plan.path, port: PUBLIC_HOOK_PORT });
89
+ return plan;
90
+ }
91
+
92
+ function stateFile(deps: HookExposureDeps): string { return join(deps.stateDir ?? elanousStateRoot(), 'hooks', 'exposure.json'); }
93
+
94
+ /** Portable cross-process lock (macOS has no `flock` binary): an atomic `mkdir` lock directory holding the owner pid.
95
+ * A lock whose owner pid is gone is stale and taken over; otherwise wait up to 30s. */
96
+ async function withExposureLock<T>(deps: HookExposureDeps, action: () => Promise<T> | T): Promise<T> {
97
+ const dir = join(deps.stateDir ?? elanousStateRoot(), 'hooks');
98
+ mkdirSync(dir, { recursive: true });
99
+ const lock = join(dir, 'exposure.lock.d');
100
+ const owner = join(lock, 'owner');
101
+ const deadline = Date.now() + 30_000;
102
+ for (;;) {
103
+ try {
104
+ mkdirSync(lock);
105
+ writeFileSync(owner, String(process.pid));
106
+ break;
107
+ } catch (error) {
108
+ if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
109
+ let holder = Number.NaN;
110
+ try { holder = Number(readFileSync(owner, 'utf8').trim()); } catch { /* owner not written yet */ }
111
+ const alive = Number.isInteger(holder) && holder > 0 && (() => { try { process.kill(holder, 0); return true; } catch { return false; } })();
112
+ if (Number.isInteger(holder) && holder > 0 && !alive) {
113
+ // Detach atomically, then re-check: only the dead holder's lock is removed — a lock another process just
114
+ // re-created after its own takeover is put back untouched.
115
+ const detached = `${lock}.stale.${process.pid}.${randomBytes(4).toString('hex')}`;
116
+ try { renameSync(lock, detached); } catch { continue; }
117
+ let detachedHolder = Number.NaN;
118
+ try { detachedHolder = Number(readFileSync(join(detached, 'owner'), 'utf8').trim()); } catch { /* unreadable */ }
119
+ if (detachedHolder === holder) rmSync(detached, { recursive: true, force: true });
120
+ else { try { renameSync(detached, lock); } catch { rmSync(detached, { recursive: true, force: true }); } }
121
+ continue;
122
+ }
123
+ if (Date.now() > deadline) throw new Error('hooks exposure lock unavailable (held for over 30s)');
124
+ await Bun.sleep(25);
125
+ }
126
+ }
127
+ try { return await action(); }
128
+ finally { rmSync(lock, { recursive: true, force: true }); }
129
+ }
130
+
131
+ /** Recorded exposure state; an out-of-band tailscale change is not represented by this file. */
132
+ export function hookExposureStatus(deps: HookExposureDeps = {}): HookExposureState | null {
133
+ const file = stateFile(deps);
134
+ if (!existsSync(file)) return null;
135
+ const raw: unknown = JSON.parse(readFileSync(file, 'utf8'));
136
+ if (raw === null) return null;
137
+ if (typeof raw !== 'object' || (raw as HookExposureState).path !== PUBLIC_HOOK_PATH ||
138
+ !/^D-\d{8}-\d+$/.test((raw as HookExposureState).decisionId) ||
139
+ !Number.isFinite(Date.parse((raw as HookExposureState).openedAt))) throw new Error('invalid hooks exposure state');
140
+ return raw as HookExposureState;
141
+ }
142
+
143
+ function save(deps: HookExposureDeps, state: HookExposureState | null): void {
144
+ const file = stateFile(deps);
145
+ mkdirSync(join(file, '..'), { recursive: true });
146
+ const temp = `${file}.${process.pid}.tmp`;
147
+ writeFileSync(temp, JSON.stringify(state) + '\n', { mode: 0o600 });
148
+ renameSync(temp, file);
149
+ }
150
+
151
+ export async function turnHookExposureOn(decisionId: string | undefined, deps: HookExposureDeps = {}): Promise<HookExposureState> {
152
+ const refuse = (reason: string): never => {
153
+ debug.log('hooks.expose', 'refused', { reason });
154
+ throw new Error(`hooks expose on refused: ${reason}`);
155
+ };
156
+ if (!decisionId || !/^D-\d{8}-\d+$/.test(decisionId)) return refuse('valid --decision <D-id> required');
157
+ return withExposureLock(deps, async () => {
158
+ if (hookExposureStatus(deps)) return refuse('already on; turn off first');
159
+ let entry: DecisionEntry;
160
+ try { entry = (deps.decision ?? (id => new DecisionLedger({ stateDir: deps.stateDir }).show(id)))(decisionId); }
161
+ catch { return refuse('decision not found or unreadable'); }
162
+ if (entry.id !== decisionId || entry.status !== 'decided') return refuse('decision is not decided');
163
+ if (entry.category !== 'security' || !entry.refs?.includes(HOOK_EXPOSURE_REF)) return refuse('decision is not for hooks exposure');
164
+ if (entry.decidedBy?.kind !== 'human') return refuse('owner decision required');
165
+ if (!entry.choice || entry.options.find(option => option.key === entry.choice)?.label !== '켬') return refuse('selected option is not 켬');
166
+ if (!await verifyIngress(deps.stateDir, deps.ingressPort)) return refuse('dedicated hooks ingress not running or identity mismatch');
167
+ const state: HookExposureState = { path: PUBLIC_HOOK_PATH, decisionId, openedAt: (deps.now ?? (() => new Date()))().toISOString() };
168
+ const execute = deps.execute ?? run;
169
+ execute('tailscale', ON_ARGS);
170
+ try { save(deps, state); }
171
+ catch (saveError) {
172
+ try { execute('tailscale', OFF_ARGS); }
173
+ catch (rollbackError) {
174
+ debug.log('hooks.expose', 'refused', { reason: 'state save and rollback failed', path: state.path, decisionId, openedAt: state.openedAt });
175
+ throw new AggregateError([saveError, rollbackError],
176
+ `hooks exposure state save and rollback failed; public exposure may still be open (decision ${decisionId}, path ${state.path}, opened ${state.openedAt}); run hooks expose off (or hooks expose off --force)`);
177
+ }
178
+ debug.log('hooks.expose', 'refused', { reason: 'state save failed; exposure rolled back', path: state.path, decisionId });
179
+ throw saveError;
180
+ }
181
+ debug.log('hooks.expose', 'on', { path: state.path, decisionId, openedAt: state.openedAt });
182
+ return state;
183
+ });
184
+ }
185
+
186
+ export interface HookExposureOffResult { changed: boolean; reason: 'recorded' | 'funnel-targets-hook-ingress' | 'forced' | 'not-ours' }
187
+
188
+ /** Turn off only a Funnel this tool owns: a recorded exposure, or a 8443 Funnel proxying to the dedicated hook ingress
189
+ * (recovery after a failed state save). Anything else is left alone unless `force` — an unrelated Funnel is not ours. */
190
+ export async function turnHookExposureOff(deps: HookExposureDeps = {}, opts: { force?: boolean } = {}): Promise<HookExposureOffResult> {
191
+ return withExposureLock(deps, () => offLocked(deps, opts));
192
+ }
193
+
194
+ function offLocked(deps: HookExposureDeps, opts: { force?: boolean }): HookExposureOffResult {
195
+ const recorded = hookExposureStatus(deps);
196
+ const reason: HookExposureOffResult['reason'] = recorded ? 'recorded'
197
+ : (deps.funnelTarget ?? readFunnelTarget)() === PUBLIC_HOOK_PORT ? 'funnel-targets-hook-ingress'
198
+ : opts.force ? 'forced' : 'not-ours';
199
+ if (reason === 'not-ours') {
200
+ debug.log('hooks.expose', 'off-skipped', { path: PUBLIC_HOOK_PATH, reason });
201
+ return { changed: false, reason };
202
+ }
203
+ (deps.execute ?? run)('tailscale', OFF_ARGS);
204
+ save(deps, null);
205
+ debug.log('hooks.expose', 'off', { path: PUBLIC_HOOK_PATH, reason });
206
+ return { changed: true, reason };
207
+ }
208
+
209
+ /** `hooks serve` shutdown: under the same lock as `on`, close a recorded exposure first, then the ingress — so a Funnel
210
+ * never outlives the listener it was verified against (another process could bind the port next). */
211
+ export async function closeIngressAndExposure(ingress: { stop: () => void } | undefined, deps: HookExposureDeps = {}): Promise<void> {
212
+ await withExposureLock(deps, () => {
213
+ try {
214
+ if (hookExposureStatus(deps)) offLocked(deps, {});
215
+ } catch (error) {
216
+ debug.log('hooks.expose', 'shutdown-off-failed', { path: PUBLIC_HOOK_PATH, error: String(error).slice(0, 200) });
217
+ console.error(`hooks: public exposure may still be open — run \`elanous hooks expose off\` (${error instanceof Error ? error.message : String(error)})`);
218
+ } finally {
219
+ ingress?.stop();
220
+ }
221
+ });
222
+ }
223
+
224
+ /** Only the two exact public hook paths are forwarded; the receiver verifies signatures and queues. */
225
+ export function startPublicHookIngress(receiverUrl: string, port = PUBLIC_HOOK_PORT, stateDir?: string): { url: string; stop: () => void } {
226
+ const receiver = new URL(receiverUrl);
227
+ if (receiver.protocol !== 'http:' || (receiver.hostname !== '127.0.0.1' && receiver.hostname !== 'localhost' && receiver.hostname !== '[::1]'))
228
+ throw new Error('public ingress requires loopback receiver');
229
+ const secret = randomBytes(32).toString('hex');
230
+ const server = Bun.serve({ hostname: '127.0.0.1', port, async fetch(request) {
231
+ const path = new URL(request.url).pathname;
232
+ if (path === PROBE_PATH && request.method === 'GET') {
233
+ const nonce = request.headers.get('x-hooks-ingress-challenge');
234
+ const proof = request.headers.get('x-hooks-ingress-proof');
235
+ if (!nonce || !/^[a-f0-9]{64}$/.test(nonce) || !proof || !/^[a-f0-9]{64}$/.test(proof))
236
+ return new Response('Not Found', { status: 404 });
237
+ const expected = createHmac('sha256', secret).update(`request:${nonce}`).digest();
238
+ if (!timingSafeEqual(Buffer.from(proof, 'hex'), expected)) return new Response('Not Found', { status: 404 });
239
+ return new Response(createHmac('sha256', secret).update(`response:${nonce}`).digest('hex'));
240
+ }
241
+ if (path !== PUBLIC_HOOK_PATH && path !== PUBLIC_GITHUB_HOOK_PATH) return new Response('Not Found', { status: 404 });
242
+ if (request.method !== 'POST') return new Response('Method Not Allowed', { status: 405 });
243
+ return fetch(new URL(path, receiver), { method: 'POST', headers: request.headers, body: request.body });
244
+ } });
245
+ const file = identityFile(stateDir);
246
+ try {
247
+ mkdirSync(join(file, '..'), { recursive: true });
248
+ writeFileSync(file, JSON.stringify({ port: server.port, secret }), { mode: 0o600 });
249
+ } catch (error) { server.stop(); throw error; }
250
+ return { url: server.url.toString(), stop: () => {
251
+ server.stop();
252
+ try { if (readIngressIdentity(stateDir).secret === secret) unlinkSync(file); } catch { /* identity already removed */ }
253
+ } };
254
+ }
@@ -0,0 +1,108 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { join } from 'node:path';
3
+ import { openMsgStore } from '../msg/msg-store.js';
4
+ import { CardStore, redactSecrets } from '../task-cards/card-store.js';
5
+ import { isGithubSpamText, PUBLIC_GITHUB_REPO } from './providers.js';
6
+ import type { QueuedHook } from './queue.js';
7
+
8
+ export type ShadowVerdict = 'accepted' | 'rejected' | 'human-check';
9
+ export interface ShadowJudgment { verdict: ShadowVerdict; reason: string }
10
+
11
+ const riskyPath = /(^|\/)(?:package\.json$|\.github\/(?:workflows\/|actions\/|CODEOWNERS$)|\.env(?:\.|$)|secrets?(?:\/|\.)|auth(?:\/|\.)|release(?:\/|\.)|scripts\/release-|src\/harness\/|src\/self-implement\/|src\/hooks\/)/i;
12
+
13
+ /** Webhook text is untrusted; a missing PR file list cannot be called safe. */
14
+ export function judgeGithubShadow(title: string, type: 'pull_request' | 'issue', files?: string[], spamSignal = false): ShadowJudgment {
15
+ if (spamSignal || isGithubSpamText(title)) return { verdict: 'rejected', reason: 'spam signal in title or body' };
16
+ if (type === 'pull_request' && (!files || !files.length || files.some(path => !path || riskyPath.test(path))))
17
+ return { verdict: 'human-check', reason: !files?.length ? 'PR file list unavailable' : 'sensitive or invalid file path' };
18
+ return { verdict: 'accepted', reason: 'public contribution candidate (shadow only)' };
19
+ }
20
+
21
+ /** Compare the current base tree against GitHub's PR test-merge tree, not the head tree.
22
+ * The test merge includes only changes contributed by this PR onto the current base. Read tree metadata only (no patches). */
23
+ export async function publicPrFileNames(number: number, fetchFn: typeof fetch = fetch): Promise<string[] | undefined> {
24
+ if (!Number.isSafeInteger(number) || number < 1) return undefined;
25
+ const read = async (path: string): Promise<Record<string, unknown> | undefined> => {
26
+ try {
27
+ const response = await fetchFn(`https://api.github.com/repos/${PUBLIC_GITHUB_REPO}${path}`, { method: 'GET',
28
+ headers: { accept: 'application/vnd.github+json' }, signal: AbortSignal.timeout(5000) });
29
+ if (!response.ok) return undefined;
30
+ return object(await response.json());
31
+ } catch { return undefined; }
32
+ };
33
+ const pull = await read(`/pulls/${number}`);
34
+ const base = object(pull?.base);
35
+ const head = object(pull?.head);
36
+ const baseSha = base?.sha;
37
+ const headSha = head?.sha;
38
+ const mergeSha = pull?.merge_commit_sha;
39
+ if (pull?.number !== number || object(base?.repo)?.full_name !== PUBLIC_GITHUB_REPO ||
40
+ !gitSha(baseSha) || !gitSha(headSha) || !gitSha(mergeSha)) return undefined;
41
+ const baseCommit = await read(`/git/commits/${baseSha}`);
42
+ const mergeCommit = await read(`/git/commits/${mergeSha}`);
43
+ const parents = mergeCommit?.parents;
44
+ if (mergeCommit?.sha !== mergeSha || !Array.isArray(parents) || parents.length !== 2 ||
45
+ object(parents[0])?.sha !== baseSha || object(parents[1])?.sha !== headSha) return undefined;
46
+ const baseTreeSha = object(baseCommit?.tree)?.sha;
47
+ const mergeTreeSha = object(mergeCommit?.tree)?.sha;
48
+ if (!gitSha(baseTreeSha) || !gitSha(mergeTreeSha)) return undefined;
49
+ const baseTree = await read(`/git/trees/${baseTreeSha}?recursive=1`);
50
+ const mergeTree = await read(`/git/trees/${mergeTreeSha}?recursive=1`);
51
+ const entries = (tree: Record<string, unknown> | undefined): Map<string, string> | undefined => {
52
+ if (!tree || tree.truncated !== false || !Array.isArray(tree.tree) || tree.tree.length > 100_000) return undefined;
53
+ const files = new Map<string, string>();
54
+ for (const row of tree.tree) {
55
+ const entry = object(row);
56
+ if (!entry || typeof entry.path !== 'string' || !gitSha(entry.sha) ||
57
+ !['blob', 'commit', 'tree'].includes(String(entry.type)) ||
58
+ typeof entry.mode !== 'string' || !/^[0-7]{6}$/.test(entry.mode)) return undefined;
59
+ if (entry.type === 'blob' || entry.type === 'commit')
60
+ files.set(entry.path, `${entry.type}:${entry.mode}:${entry.sha}`);
61
+ }
62
+ return files;
63
+ };
64
+ const before = entries(baseTree);
65
+ const after = entries(mergeTree);
66
+ if (!before || !after || baseTree?.sha !== baseTreeSha || mergeTree?.sha !== mergeTreeSha) return undefined;
67
+ return [...new Set([...before.keys(), ...after.keys()])].filter(path => before.get(path) !== after.get(path));
68
+ }
69
+
70
+ function object(value: unknown): Record<string, unknown> | undefined {
71
+ return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
72
+ }
73
+ function gitSha(value: unknown): value is string { return typeof value === 'string' && /^[0-9a-f]{40}$/i.test(value); }
74
+
75
+ /** 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> {
77
+ const github = event.task.github;
78
+ if (event.provider !== 'github' || !github || github.repository !== PUBLIC_GITHUB_REPO ||
79
+ !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(event.eventId) ||
80
+ !Number.isSafeInteger(github.number) || github.number < 1 || !['pull_request', 'issue'].includes(github.type))
81
+ throw new Error('invalid GitHub shadow event');
82
+ const files = github.type === 'pull_request' && !github.spamSignal && !isGithubSpamText(event.task.title)
83
+ ? await publicPrFileNames(github.number, fetchFn) : undefined;
84
+ const judgment = judgeGithubShadow(event.task.title, github.type, files, github.spamSignal);
85
+ const key = `github:${event.eventId}`;
86
+ const store = openMsgStore(join(root, 'msg', 'messages.db'));
87
+ try {
88
+ store.db.exec(`CREATE TABLE IF NOT EXISTS github_shadow_judgments (
89
+ event_key TEXT PRIMARY KEY, repository TEXT NOT NULL, item_type TEXT NOT NULL, item_number INTEGER NOT NULL,
90
+ verdict TEXT NOT NULL, reason TEXT NOT NULL, url TEXT NOT NULL
91
+ )`);
92
+ store.db.query(`INSERT OR IGNORE INTO github_shadow_judgments
93
+ (event_key, repository, item_type, item_number, verdict, reason, url) VALUES (?, ?, ?, ?, ?, ?, ?)`)
94
+ .run(key, github.repository, github.type, github.number, judgment.verdict, judgment.reason, event.task.external.url ?? '');
95
+ const recorded = store.db.query('SELECT verdict, reason FROM github_shadow_judgments WHERE event_key = ?')
96
+ .get(key) as ShadowJudgment;
97
+ const cards = new CardStore(root);
98
+ try {
99
+ const goalId = `github:${github.repository}:${github.type}:${github.number}`;
100
+ const card = cards.createCard({ goalId, title: redactSecrets(event.task.title).slice(0, 240) });
101
+ const content = JSON.stringify({ event: key, source: goalId, url: event.task.external.url, verdict: recorded.verdict,
102
+ reason: recorded.reason, candidate: recorded.verdict === 'rejected' ? 'intake-only' : 'decision-card' });
103
+ const sectionKey = `intake:${createHash('sha256').update(key).digest('hex')}`;
104
+ if (!card.sections.some(section => section.key === sectionKey))
105
+ cards.appendSection(card.id, { key: sectionKey, owner: 'steward', content: redactSecrets(content) });
106
+ } finally { cards.close(); }
107
+ } finally { store.close(); }
108
+ }