harnery 0.0.1 → 0.2.1

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 (445) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +84 -2
  3. package/bin/agent-coord +42 -0
  4. package/bin/agent-hook +44 -0
  5. package/bin/harn +40 -0
  6. package/dist/cli.d.ts +9 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +18 -0
  9. package/dist/commander.d.ts +128 -0
  10. package/dist/commander.d.ts.map +1 -0
  11. package/dist/commander.js +126 -0
  12. package/dist/commands/agents.d.ts +18 -0
  13. package/dist/commands/agents.d.ts.map +1 -0
  14. package/dist/commands/agents.js +3946 -0
  15. package/dist/commands/backup.d.ts +22 -0
  16. package/dist/commands/backup.d.ts.map +1 -0
  17. package/dist/commands/backup.js +262 -0
  18. package/dist/commands/browse-ai.d.ts +4 -0
  19. package/dist/commands/browse-ai.d.ts.map +1 -0
  20. package/dist/commands/browse-ai.js +156 -0
  21. package/dist/commands/browse.d.ts +4 -0
  22. package/dist/commands/browse.d.ts.map +1 -0
  23. package/dist/commands/browse.js +590 -0
  24. package/dist/commands/callers.d.ts +4 -0
  25. package/dist/commands/callers.d.ts.map +1 -0
  26. package/dist/commands/callers.js +276 -0
  27. package/dist/commands/completion.d.ts +17 -0
  28. package/dist/commands/completion.d.ts.map +1 -0
  29. package/dist/commands/completion.js +158 -0
  30. package/dist/commands/config-get.d.ts +4 -0
  31. package/dist/commands/config-get.d.ts.map +1 -0
  32. package/dist/commands/config-get.js +131 -0
  33. package/dist/commands/context.d.ts +11 -0
  34. package/dist/commands/context.d.ts.map +1 -0
  35. package/dist/commands/context.js +185 -0
  36. package/dist/commands/cookies.d.ts +4 -0
  37. package/dist/commands/cookies.d.ts.map +1 -0
  38. package/dist/commands/cookies.js +140 -0
  39. package/dist/commands/docs.d.ts +4 -0
  40. package/dist/commands/docs.d.ts.map +1 -0
  41. package/dist/commands/docs.js +137 -0
  42. package/dist/commands/doctor.d.ts +25 -0
  43. package/dist/commands/doctor.d.ts.map +1 -0
  44. package/dist/commands/doctor.js +200 -0
  45. package/dist/commands/edit-batch.d.ts +18 -0
  46. package/dist/commands/edit-batch.d.ts.map +1 -0
  47. package/dist/commands/edit-batch.js +172 -0
  48. package/dist/commands/eml.d.ts +4 -0
  49. package/dist/commands/eml.d.ts.map +1 -0
  50. package/dist/commands/eml.js +428 -0
  51. package/dist/commands/env.d.ts +4 -0
  52. package/dist/commands/env.d.ts.map +1 -0
  53. package/dist/commands/env.js +201 -0
  54. package/dist/commands/fetch.d.ts +4 -0
  55. package/dist/commands/fetch.d.ts.map +1 -0
  56. package/dist/commands/fetch.js +99 -0
  57. package/dist/commands/file-history.d.ts +4 -0
  58. package/dist/commands/file-history.d.ts.map +1 -0
  59. package/dist/commands/file-history.js +152 -0
  60. package/dist/commands/grep.d.ts +4 -0
  61. package/dist/commands/grep.d.ts.map +1 -0
  62. package/dist/commands/grep.js +317 -0
  63. package/dist/commands/init.d.ts +82 -0
  64. package/dist/commands/init.d.ts.map +1 -0
  65. package/dist/commands/init.js +288 -0
  66. package/dist/commands/outline.d.ts +4 -0
  67. package/dist/commands/outline.d.ts.map +1 -0
  68. package/dist/commands/outline.js +509 -0
  69. package/dist/commands/presence.d.ts +12 -0
  70. package/dist/commands/presence.d.ts.map +1 -0
  71. package/dist/commands/presence.js +123 -0
  72. package/dist/commands/read.d.ts +7 -0
  73. package/dist/commands/read.d.ts.map +1 -0
  74. package/dist/commands/read.js +46 -0
  75. package/dist/commands/scratch.d.ts +4 -0
  76. package/dist/commands/scratch.d.ts.map +1 -0
  77. package/dist/commands/scratch.js +426 -0
  78. package/dist/commands/session.d.ts +4 -0
  79. package/dist/commands/session.d.ts.map +1 -0
  80. package/dist/commands/session.js +162 -0
  81. package/dist/commands/sync.d.ts +24 -0
  82. package/dist/commands/sync.d.ts.map +1 -0
  83. package/dist/commands/sync.js +275 -0
  84. package/dist/commands/toc.d.ts +5 -0
  85. package/dist/commands/toc.d.ts.map +1 -0
  86. package/dist/commands/toc.js +153 -0
  87. package/dist/commands/tokens.d.ts +4 -0
  88. package/dist/commands/tokens.d.ts.map +1 -0
  89. package/dist/commands/tokens.js +48 -0
  90. package/dist/commands/tunnel.d.ts +4 -0
  91. package/dist/commands/tunnel.d.ts.map +1 -0
  92. package/dist/commands/tunnel.js +513 -0
  93. package/dist/commands/uninstall.d.ts +22 -0
  94. package/dist/commands/uninstall.d.ts.map +1 -0
  95. package/dist/commands/uninstall.js +126 -0
  96. package/dist/commands/web.d.ts +4 -0
  97. package/dist/commands/web.d.ts.map +1 -0
  98. package/dist/commands/web.js +165 -0
  99. package/dist/core/agents/canonical-emit.d.ts +27 -0
  100. package/dist/core/agents/canonical-emit.d.ts.map +1 -0
  101. package/dist/core/agents/canonical-emit.js +72 -0
  102. package/dist/core/agents/cli-emit.d.ts +27 -0
  103. package/dist/core/agents/cli-emit.d.ts.map +1 -0
  104. package/dist/core/agents/cli-emit.js +57 -0
  105. package/dist/core/agents/cli.d.ts +10 -0
  106. package/dist/core/agents/cli.d.ts.map +1 -0
  107. package/dist/core/agents/cli.js +757 -0
  108. package/dist/core/agents/codex-replay.d.ts +29 -0
  109. package/dist/core/agents/codex-replay.d.ts.map +1 -0
  110. package/dist/core/agents/codex-replay.js +138 -0
  111. package/dist/core/agents/coord-client.d.ts +98 -0
  112. package/dist/core/agents/coord-client.d.ts.map +1 -0
  113. package/dist/core/agents/coord-client.js +212 -0
  114. package/dist/core/agents/events/consume.d.ts +59 -0
  115. package/dist/core/agents/events/consume.d.ts.map +1 -0
  116. package/dist/core/agents/events/consume.js +147 -0
  117. package/dist/core/agents/events/emit.d.ts +42 -0
  118. package/dist/core/agents/events/emit.d.ts.map +1 -0
  119. package/dist/core/agents/events/emit.js +70 -0
  120. package/dist/core/agents/events/ulid.d.ts +11 -0
  121. package/dist/core/agents/events/ulid.d.ts.map +1 -0
  122. package/dist/core/agents/events/ulid.js +47 -0
  123. package/dist/core/agents/index.d.ts +14 -0
  124. package/dist/core/agents/index.d.ts.map +1 -0
  125. package/dist/core/agents/index.js +13 -0
  126. package/dist/core/agents/paths.d.ts +6 -0
  127. package/dist/core/agents/paths.d.ts.map +1 -0
  128. package/dist/core/agents/paths.js +17 -0
  129. package/dist/core/agents/render/prompt-context.d.ts +43 -0
  130. package/dist/core/agents/render/prompt-context.d.ts.map +1 -0
  131. package/dist/core/agents/render/prompt-context.js +335 -0
  132. package/dist/core/agents/render/session-context.d.ts +39 -0
  133. package/dist/core/agents/render/session-context.d.ts.map +1 -0
  134. package/dist/core/agents/render/session-context.js +283 -0
  135. package/dist/core/agents/rules/claim-conflict.d.ts +35 -0
  136. package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -0
  137. package/dist/core/agents/rules/claim-conflict.js +244 -0
  138. package/dist/core/agents/rules/commit-conflict.d.ts +59 -0
  139. package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -0
  140. package/dist/core/agents/rules/commit-conflict.js +244 -0
  141. package/dist/core/agents/rules/stop-hook.d.ts +44 -0
  142. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -0
  143. package/dist/core/agents/rules/stop-hook.js +161 -0
  144. package/dist/core/agents/session-events.d.ts +41 -0
  145. package/dist/core/agents/session-events.d.ts.map +1 -0
  146. package/dist/core/agents/session-events.js +205 -0
  147. package/dist/core/agents/state/activity-log.d.ts +18 -0
  148. package/dist/core/agents/state/activity-log.d.ts.map +1 -0
  149. package/dist/core/agents/state/activity-log.js +34 -0
  150. package/dist/core/agents/state/council.d.ts +39 -0
  151. package/dist/core/agents/state/council.d.ts.map +1 -0
  152. package/dist/core/agents/state/council.js +216 -0
  153. package/dist/core/agents/state/heartbeat-projector.d.ts +59 -0
  154. package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -0
  155. package/dist/core/agents/state/heartbeat-projector.js +436 -0
  156. package/dist/core/agents/state/heartbeat-writer.d.ts +64 -0
  157. package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -0
  158. package/dist/core/agents/state/heartbeat-writer.js +271 -0
  159. package/dist/core/agents/state/names.d.ts +35 -0
  160. package/dist/core/agents/state/names.d.ts.map +1 -0
  161. package/dist/core/agents/state/names.js +376 -0
  162. package/dist/core/agents/state/pidmap.d.ts +11 -0
  163. package/dist/core/agents/state/pidmap.d.ts.map +1 -0
  164. package/dist/core/agents/state/pidmap.js +32 -0
  165. package/dist/core/agents/state/scratch.d.ts +27 -0
  166. package/dist/core/agents/state/scratch.d.ts.map +1 -0
  167. package/dist/core/agents/state/scratch.js +90 -0
  168. package/dist/core/agents/state/shell-mutation.d.ts +17 -0
  169. package/dist/core/agents/state/shell-mutation.d.ts.map +1 -0
  170. package/dist/core/agents/state/shell-mutation.js +41 -0
  171. package/dist/core/agents/state/stale-sweep.d.ts +16 -0
  172. package/dist/core/agents/state/stale-sweep.d.ts.map +1 -0
  173. package/dist/core/agents/state/stale-sweep.js +166 -0
  174. package/dist/core/config.d.ts +29 -0
  175. package/dist/core/config.d.ts.map +1 -0
  176. package/dist/core/config.js +108 -0
  177. package/dist/core/hooks/cli.d.ts +21 -0
  178. package/dist/core/hooks/cli.d.ts.map +1 -0
  179. package/dist/core/hooks/cli.js +1123 -0
  180. package/dist/core/hooks/effects/image-capture.d.ts +43 -0
  181. package/dist/core/hooks/effects/image-capture.d.ts.map +1 -0
  182. package/dist/core/hooks/effects/image-capture.js +288 -0
  183. package/dist/core/hooks/effects/index.d.ts +64 -0
  184. package/dist/core/hooks/effects/index.d.ts.map +1 -0
  185. package/dist/core/hooks/effects/index.js +197 -0
  186. package/dist/core/hooks/events/emit.d.ts +31 -0
  187. package/dist/core/hooks/events/emit.d.ts.map +1 -0
  188. package/dist/core/hooks/events/emit.js +89 -0
  189. package/dist/core/hooks/events/schema.d.ts +235 -0
  190. package/dist/core/hooks/events/schema.d.ts.map +1 -0
  191. package/dist/core/hooks/events/schema.js +12 -0
  192. package/dist/core/hooks/events/ulid.d.ts +10 -0
  193. package/dist/core/hooks/events/ulid.d.ts.map +1 -0
  194. package/dist/core/hooks/events/ulid.js +47 -0
  195. package/dist/core/hooks/harness/detect.d.ts +9 -0
  196. package/dist/core/hooks/harness/detect.d.ts.map +1 -0
  197. package/dist/core/hooks/harness/detect.js +29 -0
  198. package/dist/core/hooks/harness/events.d.ts +45 -0
  199. package/dist/core/hooks/harness/events.d.ts.map +1 -0
  200. package/dist/core/hooks/harness/events.js +71 -0
  201. package/dist/core/hooks/harness/output.d.ts +46 -0
  202. package/dist/core/hooks/harness/output.d.ts.map +1 -0
  203. package/dist/core/hooks/harness/output.js +87 -0
  204. package/dist/core/hooks/harness/parse.d.ts +67 -0
  205. package/dist/core/hooks/harness/parse.d.ts.map +1 -0
  206. package/dist/core/hooks/harness/parse.js +132 -0
  207. package/dist/core/hooks/index.d.ts +8 -0
  208. package/dist/core/hooks/index.d.ts.map +1 -0
  209. package/dist/core/hooks/index.js +7 -0
  210. package/dist/core/hooks/resolve/anchor.d.ts +37 -0
  211. package/dist/core/hooks/resolve/anchor.d.ts.map +1 -0
  212. package/dist/core/hooks/resolve/anchor.js +48 -0
  213. package/dist/core/hooks/resolve/coord-root.d.ts +6 -0
  214. package/dist/core/hooks/resolve/coord-root.d.ts.map +1 -0
  215. package/dist/core/hooks/resolve/coord-root.js +27 -0
  216. package/dist/core/hooks/resolve/intent.d.ts +33 -0
  217. package/dist/core/hooks/resolve/intent.d.ts.map +1 -0
  218. package/dist/core/hooks/resolve/intent.js +79 -0
  219. package/dist/core/hooks/resolve/owner.d.ts +42 -0
  220. package/dist/core/hooks/resolve/owner.d.ts.map +1 -0
  221. package/dist/core/hooks/resolve/owner.js +140 -0
  222. package/dist/core/hooks/resolve/transcript.d.ts +26 -0
  223. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -0
  224. package/dist/core/hooks/resolve/transcript.js +73 -0
  225. package/dist/index.d.ts +15 -0
  226. package/dist/index.d.ts.map +1 -0
  227. package/dist/index.js +13 -0
  228. package/dist/lib/agent-browser/client.d.ts +99 -0
  229. package/dist/lib/agent-browser/client.d.ts.map +1 -0
  230. package/dist/lib/agent-browser/client.js +177 -0
  231. package/dist/lib/agent-browser/index.d.ts +2 -0
  232. package/dist/lib/agent-browser/index.d.ts.map +1 -0
  233. package/dist/lib/agent-browser/index.js +1 -0
  234. package/dist/lib/browser/client.d.ts +193 -0
  235. package/dist/lib/browser/client.d.ts.map +1 -0
  236. package/dist/lib/browser/client.js +325 -0
  237. package/dist/lib/browser/dev-overlay.d.ts +23 -0
  238. package/dist/lib/browser/dev-overlay.d.ts.map +1 -0
  239. package/dist/lib/browser/dev-overlay.js +153 -0
  240. package/dist/lib/browser/index.d.ts +5 -0
  241. package/dist/lib/browser/index.d.ts.map +1 -0
  242. package/dist/lib/browser/index.js +2 -0
  243. package/dist/lib/browser/layout.d.ts +79 -0
  244. package/dist/lib/browser/layout.d.ts.map +1 -0
  245. package/dist/lib/browser/layout.js +220 -0
  246. package/dist/lib/browser/visibility.d.ts +86 -0
  247. package/dist/lib/browser/visibility.d.ts.map +1 -0
  248. package/dist/lib/browser/visibility.js +333 -0
  249. package/dist/lib/browser/visual-diff.d.ts +38 -0
  250. package/dist/lib/browser/visual-diff.d.ts.map +1 -0
  251. package/dist/lib/browser/visual-diff.js +107 -0
  252. package/dist/lib/completion/bash.d.ts +25 -0
  253. package/dist/lib/completion/bash.d.ts.map +1 -0
  254. package/dist/lib/completion/bash.js +284 -0
  255. package/dist/lib/completion/fish.d.ts +16 -0
  256. package/dist/lib/completion/fish.d.ts.map +1 -0
  257. package/dist/lib/completion/fish.js +118 -0
  258. package/dist/lib/completion/index.d.ts +5 -0
  259. package/dist/lib/completion/index.d.ts.map +1 -0
  260. package/dist/lib/completion/index.js +4 -0
  261. package/dist/lib/completion/walk.d.ts +68 -0
  262. package/dist/lib/completion/walk.d.ts.map +1 -0
  263. package/dist/lib/completion/walk.js +102 -0
  264. package/dist/lib/completion/zsh.d.ts +13 -0
  265. package/dist/lib/completion/zsh.d.ts.map +1 -0
  266. package/dist/lib/completion/zsh.js +249 -0
  267. package/dist/lib/context/index.d.ts +107 -0
  268. package/dist/lib/context/index.d.ts.map +1 -0
  269. package/dist/lib/context/index.js +275 -0
  270. package/dist/lib/cookies/client.d.ts +131 -0
  271. package/dist/lib/cookies/client.d.ts.map +1 -0
  272. package/dist/lib/cookies/client.js +239 -0
  273. package/dist/lib/cookies/index.d.ts +2 -0
  274. package/dist/lib/cookies/index.d.ts.map +1 -0
  275. package/dist/lib/cookies/index.js +1 -0
  276. package/dist/lib/council/index.d.ts +266 -0
  277. package/dist/lib/council/index.d.ts.map +1 -0
  278. package/dist/lib/council/index.js +674 -0
  279. package/dist/lib/docs-index.d.ts +28 -0
  280. package/dist/lib/docs-index.d.ts.map +1 -0
  281. package/dist/lib/docs-index.js +169 -0
  282. package/dist/lib/docs-lint.d.ts +26 -0
  283. package/dist/lib/docs-lint.d.ts.map +1 -0
  284. package/dist/lib/docs-lint.js +378 -0
  285. package/dist/lib/docs-sweep.d.ts +34 -0
  286. package/dist/lib/docs-sweep.d.ts.map +1 -0
  287. package/dist/lib/docs-sweep.js +304 -0
  288. package/dist/lib/docs.d.ts +27 -0
  289. package/dist/lib/docs.d.ts.map +1 -0
  290. package/dist/lib/docs.js +142 -0
  291. package/dist/lib/env.d.ts +11 -0
  292. package/dist/lib/env.d.ts.map +1 -0
  293. package/dist/lib/env.js +12 -0
  294. package/dist/lib/exec.d.ts +32 -0
  295. package/dist/lib/exec.d.ts.map +1 -0
  296. package/dist/lib/exec.js +54 -0
  297. package/dist/lib/format.d.ts +29 -0
  298. package/dist/lib/format.d.ts.map +1 -0
  299. package/dist/lib/format.js +139 -0
  300. package/dist/lib/http/client.d.ts +56 -0
  301. package/dist/lib/http/client.d.ts.map +1 -0
  302. package/dist/lib/http/client.js +160 -0
  303. package/dist/lib/http/index.d.ts +2 -0
  304. package/dist/lib/http/index.d.ts.map +1 -0
  305. package/dist/lib/http/index.js +1 -0
  306. package/dist/lib/identities/index.d.ts +77 -0
  307. package/dist/lib/identities/index.d.ts.map +1 -0
  308. package/dist/lib/identities/index.js +190 -0
  309. package/dist/lib/machine.d.ts +19 -0
  310. package/dist/lib/machine.d.ts.map +1 -0
  311. package/dist/lib/machine.js +61 -0
  312. package/dist/lib/presence.d.ts +48 -0
  313. package/dist/lib/presence.d.ts.map +1 -0
  314. package/dist/lib/presence.js +123 -0
  315. package/dist/lib/readability/client.d.ts +39 -0
  316. package/dist/lib/readability/client.d.ts.map +1 -0
  317. package/dist/lib/readability/client.js +121 -0
  318. package/dist/lib/readability/index.d.ts +2 -0
  319. package/dist/lib/readability/index.d.ts.map +1 -0
  320. package/dist/lib/readability/index.js +1 -0
  321. package/dist/lib/scratch/index.d.ts +74 -0
  322. package/dist/lib/scratch/index.d.ts.map +1 -0
  323. package/dist/lib/scratch/index.js +393 -0
  324. package/dist/lib/tunnel/gate.d.ts +12 -0
  325. package/dist/lib/tunnel/gate.d.ts.map +1 -0
  326. package/dist/lib/tunnel/gate.js +101 -0
  327. package/dist/lib/tunnel/state.d.ts +34 -0
  328. package/dist/lib/tunnel/state.d.ts.map +1 -0
  329. package/dist/lib/tunnel/state.js +132 -0
  330. package/package.json +160 -8
  331. package/schemas/.gitkeep +0 -0
  332. package/schemas/config.schema.json +109 -0
  333. package/src/cli.ts +22 -0
  334. package/src/commander.ts +242 -0
  335. package/src/commands/.gitkeep +0 -0
  336. package/src/commands/agents.ts +4567 -0
  337. package/src/commands/backup.ts +305 -0
  338. package/src/commands/browse-ai.ts +198 -0
  339. package/src/commands/browse.ts +849 -0
  340. package/src/commands/callers.ts +363 -0
  341. package/src/commands/completion.ts +193 -0
  342. package/src/commands/config-get.ts +161 -0
  343. package/src/commands/context.ts +209 -0
  344. package/src/commands/cookies.ts +198 -0
  345. package/src/commands/docs.ts +174 -0
  346. package/src/commands/doctor.ts +231 -0
  347. package/src/commands/edit-batch.ts +233 -0
  348. package/src/commands/eml.ts +519 -0
  349. package/src/commands/env.ts +254 -0
  350. package/src/commands/fetch.ts +136 -0
  351. package/src/commands/file-history.ts +202 -0
  352. package/src/commands/grep.ts +371 -0
  353. package/src/commands/init.ts +335 -0
  354. package/src/commands/outline.ts +583 -0
  355. package/src/commands/presence.ts +152 -0
  356. package/src/commands/read.ts +64 -0
  357. package/src/commands/scratch.ts +445 -0
  358. package/src/commands/session.ts +187 -0
  359. package/src/commands/sync.ts +306 -0
  360. package/src/commands/toc.ts +218 -0
  361. package/src/commands/tokens.ts +79 -0
  362. package/src/commands/tunnel.ts +633 -0
  363. package/src/commands/uninstall.ts +144 -0
  364. package/src/commands/web.ts +193 -0
  365. package/src/core/agents/canonical-emit.ts +77 -0
  366. package/src/core/agents/cli-emit.ts +64 -0
  367. package/src/core/agents/cli.ts +838 -0
  368. package/src/core/agents/codex-replay.ts +163 -0
  369. package/src/core/agents/coord-client.ts +249 -0
  370. package/src/core/agents/events/consume.ts +196 -0
  371. package/src/core/agents/events/emit.ts +108 -0
  372. package/src/core/agents/events/ulid.ts +51 -0
  373. package/src/core/agents/index.ts +14 -0
  374. package/src/core/agents/paths.ts +16 -0
  375. package/src/core/agents/render/prompt-context.ts +401 -0
  376. package/src/core/agents/render/session-context.ts +341 -0
  377. package/src/core/agents/rules/claim-conflict.ts +282 -0
  378. package/src/core/agents/rules/commit-conflict.ts +303 -0
  379. package/src/core/agents/rules/stop-hook.ts +229 -0
  380. package/src/core/agents/session-events.ts +228 -0
  381. package/src/core/agents/state/activity-log.ts +33 -0
  382. package/src/core/agents/state/council.ts +265 -0
  383. package/src/core/agents/state/heartbeat-projector.ts +488 -0
  384. package/src/core/agents/state/heartbeat-writer.ts +333 -0
  385. package/src/core/agents/state/names.ts +399 -0
  386. package/src/core/agents/state/pidmap.ts +38 -0
  387. package/src/core/agents/state/scratch.ts +121 -0
  388. package/src/core/agents/state/shell-mutation.ts +44 -0
  389. package/src/core/agents/state/stale-sweep.ts +190 -0
  390. package/src/core/config.ts +111 -0
  391. package/src/core/hooks/cli.ts +1247 -0
  392. package/src/core/hooks/effects/image-capture.ts +330 -0
  393. package/src/core/hooks/effects/index.ts +210 -0
  394. package/src/core/hooks/events/emit.ts +120 -0
  395. package/src/core/hooks/events/schema.ts +430 -0
  396. package/src/core/hooks/events/ulid.ts +51 -0
  397. package/src/core/hooks/harness/detect.ts +30 -0
  398. package/src/core/hooks/harness/events.ts +102 -0
  399. package/src/core/hooks/harness/output.ts +100 -0
  400. package/src/core/hooks/harness/parse.ts +180 -0
  401. package/src/core/hooks/index.ts +16 -0
  402. package/src/core/hooks/resolve/anchor.ts +51 -0
  403. package/src/core/hooks/resolve/coord-root.ts +25 -0
  404. package/src/core/hooks/resolve/intent.ts +89 -0
  405. package/src/core/hooks/resolve/owner.ts +140 -0
  406. package/src/core/hooks/resolve/transcript.ts +72 -0
  407. package/src/hooks/.gitkeep +0 -0
  408. package/src/index.ts +15 -0
  409. package/src/lib/agent-browser/client.ts +239 -0
  410. package/src/lib/agent-browser/index.ts +1 -0
  411. package/src/lib/browser/client.ts +449 -0
  412. package/src/lib/browser/dev-overlay.ts +207 -0
  413. package/src/lib/browser/index.ts +24 -0
  414. package/src/lib/browser/layout.ts +288 -0
  415. package/src/lib/browser/visibility.ts +419 -0
  416. package/src/lib/browser/visual-diff.ts +150 -0
  417. package/src/lib/completion/bash.ts +291 -0
  418. package/src/lib/completion/fish.ts +134 -0
  419. package/src/lib/completion/index.ts +10 -0
  420. package/src/lib/completion/walk.ts +184 -0
  421. package/src/lib/completion/zsh.ts +262 -0
  422. package/src/lib/context/index.ts +386 -0
  423. package/src/lib/cookies/client.ts +301 -0
  424. package/src/lib/cookies/index.ts +13 -0
  425. package/src/lib/council/index.ts +803 -0
  426. package/src/lib/docs-index.ts +216 -0
  427. package/src/lib/docs-lint.ts +413 -0
  428. package/src/lib/docs-sweep.ts +348 -0
  429. package/src/lib/docs.ts +199 -0
  430. package/src/lib/env.ts +12 -0
  431. package/src/lib/exec.ts +74 -0
  432. package/src/lib/format.ts +147 -0
  433. package/src/lib/http/client.ts +211 -0
  434. package/src/lib/http/index.ts +1 -0
  435. package/src/lib/identities/index.ts +210 -0
  436. package/src/lib/machine.ts +61 -0
  437. package/src/lib/presence.ts +154 -0
  438. package/src/lib/readability/client.ts +169 -0
  439. package/src/lib/readability/index.ts +5 -0
  440. package/src/lib/readability/turndown-plugin-gfm.d.ts +10 -0
  441. package/src/lib/scratch/index.ts +470 -0
  442. package/src/lib/tunnel/gate.ts +113 -0
  443. package/src/lib/tunnel/state.ts +167 -0
  444. package/src/web/.gitkeep +0 -0
  445. package/index.js +0 -1
@@ -0,0 +1,330 @@
1
+ /**
2
+ * Image-feed capture: a non-coordination side effect fired from the normalized
3
+ * `agent-hook` tool handlers (see effects/index.ts for the family rationale).
4
+ *
5
+ * When an agent VIEWS an image (Read tool on a `.png`/`.jpg`/…) or PRODUCES one
6
+ * (a Bash command writes an image file via `harn browse`, `harn image`, `--diff`, …),
7
+ * we content-address the bytes into `.harnery/images/<sha256>.<ext>` (dedup:
8
+ * identical bytes collapse to one blob) and emit an `image.captured` event into
9
+ * the canonical stream. The web image feed (`/images`) groups those events by
10
+ * hash and streams them live over the existing SSE infra.
11
+ *
12
+ * Why this lives at the hook layer: it's the single harness-agnostic chokepoint
13
+ * that sees every tool call across Claude Code / Cursor / Codex with the full
14
+ * `tool_input` (file paths) and `tool_response`. No per-command code needed.
15
+ *
16
+ * Everything here is best-effort and MUST NOT throw; callers wrap it in
17
+ * try/catch + logError, matching every other effect.
18
+ */
19
+
20
+ import { createHash } from "node:crypto";
21
+ import {
22
+ existsSync,
23
+ mkdirSync,
24
+ readdirSync,
25
+ readFileSync,
26
+ renameSync,
27
+ rmSync,
28
+ statSync,
29
+ writeFileSync,
30
+ } from "node:fs";
31
+ import { isAbsolute, join, resolve } from "node:path";
32
+ import { emit } from "../events/emit.ts";
33
+ import type { Harness } from "../events/schema.ts";
34
+ import type { ParsedPayload } from "../harness/parse.ts";
35
+
36
+ /** Raster + vector image extensions the feed accepts. PDF is intentionally
37
+ * excluded: book renders aren't an "image feed" and don't thumbnail inline. */
38
+ const IMAGE_EXTS = new Set(["png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"]);
39
+
40
+ /** Skip hashing files larger than this; screenshots are tens of KB; a huge
41
+ * file mentioned in passing isn't worth the read. */
42
+ const MAX_CAPTURE_BYTES = 25 * 1024 * 1024;
43
+
44
+ /** A produced image must have been written within this window of the command
45
+ * finishing, otherwise a path merely *mentioned* in output (an old baseline,
46
+ * a doc reference) would be captured. Viewed images skip this gate. */
47
+ const PRODUCED_MTIME_WINDOW_MS = 120_000;
48
+
49
+ /** Matches image-ish paths in a command string or its output. Deliberately
50
+ * permissive on the path body (`~`, `@`, `.`, `-`, `/`); existence + ext + the
51
+ * mtime gate do the real filtering. */
52
+ const IMAGE_PATH_RE = /[\w./~@+-]+\.(?:png|jpe?g|gif|webp|bmp|svg)\b/gi;
53
+
54
+ export interface CaptureContext {
55
+ eventType: "tool.pre_use" | "tool.post_use";
56
+ /** The built event data (tool_name, tool_input string, intent, tool_use_id). */
57
+ data: Record<string, unknown>;
58
+ /** The full parsed payload, used for the un-clamped command + tool_response. */
59
+ payload: ParsedPayload | null;
60
+ instanceId: string;
61
+ sessionId: string;
62
+ harness: Harness;
63
+ }
64
+
65
+ interface Candidate {
66
+ path: string; // resolved absolute path
67
+ role: "viewed" | "produced";
68
+ intent?: string;
69
+ commandHead?: string;
70
+ requireRecentMtime: boolean;
71
+ }
72
+
73
+ /**
74
+ * Inspect one tool event for image references and capture any that resolve to
75
+ * a real image on disk. Emits zero or more `image.captured` events.
76
+ */
77
+ export function captureImages(coordRoot: string, ctx: CaptureContext): void {
78
+ const imagesDir = join(coordRoot, ".harnery", "images");
79
+ const cwd = resolveCwd(ctx.payload);
80
+ const toolName = String(ctx.data.tool_name ?? "");
81
+
82
+ const candidates =
83
+ ctx.eventType === "tool.pre_use"
84
+ ? collectViewed(toolName, ctx.data, cwd)
85
+ : collectProduced(toolName, ctx.payload, cwd);
86
+
87
+ if (candidates.length === 0) return;
88
+
89
+ for (const cand of candidates) {
90
+ // Never re-capture our own blob store (would loop on Reads of the gallery).
91
+ if (cand.path.startsWith(`${imagesDir}/`)) continue;
92
+ const captured = captureOne(imagesDir, cand);
93
+ if (!captured) continue;
94
+ emit(coordRoot, {
95
+ event_type: "image.captured",
96
+ instance_id: ctx.instanceId,
97
+ session_id: ctx.sessionId,
98
+ harness: ctx.harness,
99
+ data: {
100
+ hash: captured.hash,
101
+ ext: captured.ext,
102
+ bytes: captured.bytes,
103
+ role: cand.role,
104
+ source_path: canonicalize(coordRoot, cand.path),
105
+ tool_name: toolName,
106
+ tool_use_id: ctx.data.tool_use_id as string | undefined,
107
+ ...(cand.intent ? { intent: cand.intent } : {}),
108
+ ...(cand.commandHead ? { command_head: cand.commandHead } : {}),
109
+ },
110
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
111
+ } as Parameters<typeof emit>[1]);
112
+ }
113
+ }
114
+
115
+ /** Read tool → the file_path it's about to show (the "agent saw this" signal). */
116
+ function collectViewed(toolName: string, data: Record<string, unknown>, cwd: string): Candidate[] {
117
+ if (toolName !== "Read") return [];
118
+ const input = parseToolInput(data.tool_input);
119
+ const filePath = input?.file_path;
120
+ if (typeof filePath !== "string" || !filePath) return [];
121
+ if (!hasImageExt(filePath)) return [];
122
+ return [
123
+ {
124
+ path: toAbsolute(filePath, cwd),
125
+ role: "viewed",
126
+ intent: typeof data.intent === "string" ? data.intent : undefined,
127
+ requireRecentMtime: false,
128
+ },
129
+ ];
130
+ }
131
+
132
+ /** Bash tool → scan the command + its output for freshly-written image files. */
133
+ function collectProduced(
134
+ toolName: string,
135
+ payload: ParsedPayload | null,
136
+ cwd: string,
137
+ ): Candidate[] {
138
+ if (toolName !== "Bash") return [];
139
+ const command = bashCommand(payload);
140
+ const responseText = stringifyResponse(payload?.tool_response);
141
+ const commandHead = command ? command.slice(0, 120) : undefined;
142
+
143
+ const seen = new Set<string>();
144
+ const out: Candidate[] = [];
145
+ for (const text of [command, responseText]) {
146
+ if (!text) continue;
147
+ for (const m of text.matchAll(IMAGE_PATH_RE)) {
148
+ const raw = m[0];
149
+ const abs = toAbsolute(raw, cwd);
150
+ if (seen.has(abs)) continue;
151
+ seen.add(abs);
152
+ out.push({ path: abs, role: "produced", commandHead, requireRecentMtime: true });
153
+ }
154
+ }
155
+ return out;
156
+ }
157
+
158
+ interface CapturedBlob {
159
+ hash: string;
160
+ ext: string;
161
+ bytes: number;
162
+ }
163
+
164
+ /**
165
+ * Validate the candidate on disk, hash it, and copy into the content-addressed
166
+ * store (skipping the copy when the blob already exists). Returns null when the
167
+ * candidate doesn't qualify.
168
+ */
169
+ function captureOne(imagesDir: string, cand: Candidate): CapturedBlob | null {
170
+ let st: ReturnType<typeof statSync>;
171
+ try {
172
+ st = statSync(cand.path);
173
+ } catch {
174
+ return null; // path doesn't exist (common for "produced" false-positives)
175
+ }
176
+ if (!st.isFile() || st.size === 0 || st.size > MAX_CAPTURE_BYTES) return null;
177
+ if (cand.requireRecentMtime && Date.now() - st.mtimeMs > PRODUCED_MTIME_WINDOW_MS) {
178
+ return null; // mentioned but not freshly produced by this command
179
+ }
180
+ const ext = extOf(cand.path);
181
+ if (!ext || !IMAGE_EXTS.has(ext)) return null;
182
+
183
+ let bytes: Buffer;
184
+ try {
185
+ bytes = readFileSync(cand.path);
186
+ } catch {
187
+ return null;
188
+ }
189
+ const hash = createHash("sha256").update(bytes).digest("hex");
190
+ const dest = join(imagesDir, `${hash}.${ext}`);
191
+ if (!existsSync(dest)) {
192
+ try {
193
+ mkdirSync(imagesDir, { recursive: true });
194
+ const tmp = `${dest}.tmp.${process.pid}`;
195
+ writeFileSync(tmp, bytes);
196
+ renameSync(tmp, dest);
197
+ } catch {
198
+ return null; // couldn't store, don't emit a dangling event
199
+ }
200
+ }
201
+ return { hash, ext, bytes: st.size };
202
+ }
203
+
204
+ /**
205
+ * Prune `.harnery/images/` past a size cap (default 2 GB) and an age cap
206
+ * (default 30 days), oldest-mtime-first. Fired on session.start next to
207
+ * scratchJanitor. Pure-fs, fail-soft. Orphaned `image.captured` events whose
208
+ * blob was pruned render as an "expired" placeholder in the gallery.
209
+ */
210
+ export function imageJanitor(coordRoot: string): void {
211
+ try {
212
+ const dir = join(coordRoot, ".harnery", "images");
213
+ if (!existsSync(dir)) return;
214
+ const maxBytes = envInt("HARNERY_IMAGES_MAX_BYTES", 2 * 1024 * 1024 * 1024);
215
+ const maxAgeMs = envInt("HARNERY_IMAGES_MAX_AGE_DAYS", 30) * 24 * 60 * 60 * 1000;
216
+ const now = Date.now();
217
+
218
+ type Entry = { path: string; size: number; mtimeMs: number };
219
+ const entries: Entry[] = [];
220
+ for (const name of readdirSync(dir)) {
221
+ if (name.endsWith(".tmp") || name.includes(".tmp.")) {
222
+ // Orphaned temp from a crashed copy; sweep it.
223
+ rmSync(join(dir, name), { force: true });
224
+ continue;
225
+ }
226
+ const full = join(dir, name);
227
+ let st: ReturnType<typeof statSync>;
228
+ try {
229
+ st = statSync(full);
230
+ } catch {
231
+ continue;
232
+ }
233
+ if (!st.isFile()) continue;
234
+ entries.push({ path: full, size: st.size, mtimeMs: st.mtimeMs });
235
+ }
236
+
237
+ // Age cap.
238
+ let total = 0;
239
+ const survivors: Entry[] = [];
240
+ for (const e of entries) {
241
+ if (now - e.mtimeMs > maxAgeMs) {
242
+ rmSync(e.path, { force: true });
243
+ continue;
244
+ }
245
+ total += e.size;
246
+ survivors.push(e);
247
+ }
248
+
249
+ // Size cap: drop oldest first until under.
250
+ if (total > maxBytes) {
251
+ survivors.sort((a, b) => a.mtimeMs - b.mtimeMs);
252
+ for (const e of survivors) {
253
+ if (total <= maxBytes) break;
254
+ rmSync(e.path, { force: true });
255
+ total -= e.size;
256
+ }
257
+ }
258
+ } catch {
259
+ // best-effort
260
+ }
261
+ }
262
+
263
+ /* ── helpers ─────────────────────────────────────────────────────────────── */
264
+
265
+ function envInt(name: string, fallback: number): number {
266
+ const v = process.env[name];
267
+ if (!v) return fallback;
268
+ const n = Number(v);
269
+ return Number.isFinite(n) && n > 0 ? n : fallback;
270
+ }
271
+
272
+ function parseToolInput(raw: unknown): Record<string, unknown> | null {
273
+ if (typeof raw !== "string") return null;
274
+ try {
275
+ const parsed = JSON.parse(raw) as Record<string, unknown>;
276
+ return parsed && typeof parsed === "object" ? parsed : null;
277
+ } catch {
278
+ return null;
279
+ }
280
+ }
281
+
282
+ /** The un-clamped Bash command from the raw payload (falls back to clamped). */
283
+ function bashCommand(payload: ParsedPayload | null): string {
284
+ const ti = payload?.raw?.tool_input as Record<string, unknown> | undefined;
285
+ const cmd = ti?.command;
286
+ return typeof cmd === "string" ? cmd : "";
287
+ }
288
+
289
+ function stringifyResponse(value: unknown): string {
290
+ if (value == null) return "";
291
+ if (typeof value === "string") return value;
292
+ try {
293
+ return JSON.stringify(value);
294
+ } catch {
295
+ return "";
296
+ }
297
+ }
298
+
299
+ function resolveCwd(payload: ParsedPayload | null): string {
300
+ const raw = payload?.raw as Record<string, unknown> | undefined;
301
+ const cwd = raw?.cwd;
302
+ return typeof cwd === "string" && cwd ? cwd : process.cwd();
303
+ }
304
+
305
+ function hasImageExt(p: string): boolean {
306
+ const ext = extOf(p);
307
+ return !!ext && IMAGE_EXTS.has(ext);
308
+ }
309
+
310
+ function extOf(p: string): string {
311
+ const clean = p.split(/[?#]/)[0] ?? p; // drop any query/fragment
312
+ const dot = clean.lastIndexOf(".");
313
+ if (dot < 0) return "";
314
+ return clean.slice(dot + 1).toLowerCase();
315
+ }
316
+
317
+ function toAbsolute(p: string, cwd: string): string {
318
+ let path = p;
319
+ if (path.startsWith("~/")) {
320
+ const home = process.env.HOME;
321
+ if (home) path = join(home, path.slice(2));
322
+ }
323
+ return isAbsolute(path) ? path : resolve(cwd, path);
324
+ }
325
+
326
+ /** Strip the coordRoot prefix so the feed shows repo-relative paths. */
327
+ function canonicalize(coordRoot: string, p: string): string {
328
+ if (p.startsWith(`${coordRoot}/`)) return p.slice(coordRoot.length + 1);
329
+ return p;
330
+ }
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Non-coordination side effects fired from the normalized `agent-hook` handlers.
3
+ *
4
+ * These are DELIBERATELY outside the agent-coord path: sounds, scratch
5
+ * lifecycle, session telemetry, presence detection. They used to live in
6
+ * per-harness bash adapters. Per the directive ("use the normalized hooks; if
7
+ * they aren't coordination, implement them outside of coordination") they move
8
+ * here so the harness configs reference only `agent-hook`, while staying a
9
+ * distinct concern from the coordination logic in cli.ts / agent-coord.
10
+ *
11
+ * Everything here is best-effort: it never throws and never blocks the hook on a
12
+ * slow dependency (telemetry runs detached).
13
+ */
14
+
15
+ import { spawn, spawnSync } from "node:child_process";
16
+ import { existsSync, readdirSync, rmSync } from "node:fs";
17
+ import os from "node:os";
18
+ import { join } from "node:path";
19
+ import { applyDetection } from "../../../lib/presence.ts";
20
+
21
+ export type { CaptureContext } from "./image-capture.ts";
22
+ export { captureImages, imageJanitor } from "./image-capture.ts";
23
+
24
+ /**
25
+ * Play a notification sound via the cross-platform utility
26
+ * (afplay on macOS, powershell.exe on WSL). The
27
+ * utility backgrounds the actual player, so this returns fast. Rate-limiting
28
+ * lives in the utility, keyed on CLAUDE_SOUND_SESSION_ID + the per-event count.
29
+ * Claude-Code-only today (Cursor has no sounds; Codex's never worked).
30
+ */
31
+ export function playSound(
32
+ repoRoot: string,
33
+ soundEvent: string,
34
+ sessionId: string,
35
+ maxPlays = 0,
36
+ ): void {
37
+ try {
38
+ const player = join(repoRoot, "scripts", "hooks", "play-sound.sh");
39
+ if (!existsSync(player)) return;
40
+ spawnSync("bash", [player, soundEvent, String(maxPlays)], {
41
+ env: { ...process.env, CLAUDE_SOUND_SESSION_ID: sessionId },
42
+ timeout: 4000,
43
+ stdio: "ignore",
44
+ });
45
+ } catch {
46
+ // best-effort
47
+ }
48
+ }
49
+
50
+ /** Map an agent-hook CLI event name → sound, or null if the event has no sound. */
51
+ export function soundForEvent(eventName: string): { sound: string; maxPlays: number } | null {
52
+ switch (eventName) {
53
+ case "stop":
54
+ return { sound: "stop", maxPlays: 0 };
55
+ case "stop-failure":
56
+ return { sound: "error", maxPlays: 0 };
57
+ case "sub-agent-start":
58
+ return { sound: "subagent-start", maxPlays: 3 };
59
+ default:
60
+ return null;
61
+ }
62
+ }
63
+
64
+ function harnBin(repoRoot: string): string | null {
65
+ const bin = join(repoRoot, "harnery", "bin", "harn");
66
+ return existsSync(bin) ? bin : null;
67
+ }
68
+
69
+ /** Prune stale scratch archives + sweep orphans (global, fast). Fire-and-forget. */
70
+ export function scratchJanitor(repoRoot: string): void {
71
+ try {
72
+ const bin = harnBin(repoRoot);
73
+ if (!bin) return;
74
+ spawnSync("bash", [bin, "scratch", "janitor", "--quiet"], {
75
+ env: { ...process.env, HARNERY_OUTPUT_SESSION_TEE: "0" },
76
+ timeout: 5000,
77
+ stdio: "ignore",
78
+ });
79
+ } catch {
80
+ // best-effort
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Return the one-line scratch recovery cue for SessionStart, or "" if none.
86
+ * The caller merges it into the session-start additionalContext (it used to be
87
+ * a standalone additionalContext emission from the previous scratch-on-start adapter).
88
+ */
89
+ export function scratchRecoveryCue(repoRoot: string): string {
90
+ try {
91
+ const bin = harnBin(repoRoot);
92
+ if (!bin) return "";
93
+ const r = spawnSync("bash", [bin, "scratch", "recovery-cue"], {
94
+ env: { ...process.env, HARNERY_OUTPUT_SESSION_TEE: "0" },
95
+ timeout: 5000,
96
+ encoding: "utf8",
97
+ });
98
+ return (r.stdout ?? "").trim();
99
+ } catch {
100
+ return "";
101
+ }
102
+ }
103
+
104
+ /** Archive the ending agent's scratchpad. Fire-and-forget. */
105
+ export function scratchArchive(repoRoot: string, owner: string): void {
106
+ try {
107
+ const bin = harnBin(repoRoot);
108
+ if (!bin || !owner) return;
109
+ spawnSync("bash", [bin, "scratch", "archive", "--owner", owner], {
110
+ env: { ...process.env, HARNERY_OUTPUT_SESSION_TEE: "0" },
111
+ timeout: 5000,
112
+ stdio: "ignore",
113
+ });
114
+ } catch {
115
+ // best-effort
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Sync Claude Code session JSONL → BigQuery (`harn claude-sessions sync`). Lives
121
+ * in the host CLI (parses Claude Code's own ~/.claude/projects/**\/*.jsonl), so harnery
122
+ * shells out to the harn binary rather than importing it. Detached + unref'd so a
123
+ * slow BigQuery round-trip never blocks the hook. Stop-path syncs are rate-
124
+ * limited inside `harn claude-sessions sync`; SessionEnd forces via env. Caller
125
+ * gates to the claude-code harness.
126
+ */
127
+ export function syncClaudeSessions(repoRoot: string, force: boolean): void {
128
+ try {
129
+ const bin = join(repoRoot, "bin", "bp");
130
+ if (!existsSync(bin)) return;
131
+ const env: Record<string, string | undefined> = {
132
+ ...process.env,
133
+ HARNERY_OUTPUT_SESSION_TEE: "0",
134
+ };
135
+ if (force) env.HARNERY_CLAUDE_SESSIONS_FORCE = "1";
136
+ const child = spawn("bash", [bin, "claude-sessions", "sync", "--quiet"], {
137
+ env,
138
+ detached: true,
139
+ stdio: "ignore",
140
+ });
141
+ child.unref();
142
+ } catch {
143
+ // best-effort
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Fire the turn-summary extension (Haiku auto-summary of the turn → heartbeat
149
+ * `turn_summary`). Detached + unref'd; it makes an Anthropic API call and must
150
+ * never block the Stop hook. Claude-Code-only. The
151
+ * script self-guards on ANTHROPIC_API_KEY / curl / jq / matching session.
152
+ */
153
+ export function runTurnSummary(
154
+ repoRoot: string,
155
+ owner: string,
156
+ sessionId: string,
157
+ transcriptPath: string | undefined,
158
+ ): void {
159
+ try {
160
+ if (!transcriptPath || !existsSync(transcriptPath)) return;
161
+ const script = join(
162
+ repoRoot,
163
+ "scripts",
164
+ "hooks",
165
+ "harness",
166
+ "claude_code",
167
+ "extensions",
168
+ "turn-summary.sh",
169
+ );
170
+ if (!existsSync(script)) return;
171
+ const child = spawn("bash", [script, owner, sessionId, transcriptPath], {
172
+ detached: true,
173
+ stdio: "ignore",
174
+ });
175
+ child.unref();
176
+ } catch {
177
+ // best-effort
178
+ }
179
+ }
180
+
181
+ /** Reset per-turn sound rate-limit counters at the start of a new turn. */
182
+ export function resetSoundCounters(sessionId: string): void {
183
+ try {
184
+ if (!sessionId) return;
185
+ const dir = os.tmpdir();
186
+ const prefix = `claude-sounds-${sessionId}-`;
187
+ for (const f of readdirSync(dir)) {
188
+ if (f.startsWith(prefix) && f.endsWith(".count")) {
189
+ rmSync(join(dir, f), { force: true });
190
+ }
191
+ }
192
+ } catch {
193
+ // best-effort
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Update the mobile-vs-office presence file from the user's prompt shape, using
199
+ * harnery's own presence library in-process: no external script, no host-path
200
+ * dependency. Claude-Code-only (the heuristic is tuned to CC's user-prompt
201
+ * payload; Cursor/Codex don't surface comparable prompt text). Fire-and-forget.
202
+ */
203
+ export function detectPresence(prompt: string): void {
204
+ try {
205
+ if (!prompt) return;
206
+ applyDetection(prompt);
207
+ } catch {
208
+ // best-effort
209
+ }
210
+ }
@@ -0,0 +1,120 @@
1
+ import { appendFileSync, closeSync, mkdirSync, openSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import {
4
+ type EventEnvelope,
5
+ type EventType,
6
+ type Harness,
7
+ SCHEMA_VERSION,
8
+ type Source,
9
+ } from "./schema.ts";
10
+ import { ulid } from "./ulid.ts";
11
+
12
+ const LOCK_FILE = ".harnery/events.ndjson.lock";
13
+ const STREAM_FILE = ".harnery/events.ndjson";
14
+
15
+ const MAX_LINE_BYTES = 64 * 1024;
16
+
17
+ export interface EmitInput<TType extends EventType, TData> {
18
+ event_type: TType;
19
+ instance_id: string;
20
+ session_id: string;
21
+ harness: Harness;
22
+ source?: Source;
23
+ parent_session_id?: string;
24
+ turn_id?: string;
25
+ parent_turn_id?: string;
26
+ ts?: string;
27
+ data: TData;
28
+ }
29
+
30
+ /**
31
+ * Build a canonical envelope around the caller's `data` payload. Pure;
32
+ * doesn't touch the filesystem. Useful for tests + replay fixtures.
33
+ */
34
+ export function buildEnvelope<TType extends EventType, TData>(
35
+ input: EmitInput<TType, TData>,
36
+ ): EventEnvelope<TType, TData> {
37
+ return {
38
+ schema_version: SCHEMA_VERSION,
39
+ event_id: ulid(),
40
+ event_type: input.event_type,
41
+ ts: input.ts ?? new Date().toISOString(),
42
+ instance_id: input.instance_id,
43
+ session_id: input.session_id,
44
+ parent_session_id: input.parent_session_id,
45
+ turn_id: input.turn_id,
46
+ parent_turn_id: input.parent_turn_id,
47
+ harness: input.harness,
48
+ source: input.source ?? "agent-hooks",
49
+ data: input.data,
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Append one canonical event to `.harnery/events.ndjson` under a file lock.
55
+ * Append-only, one JSON object per line, lock-serialized.
56
+ *
57
+ * Synchronous because hooks are short-lived processes: we want the append to
58
+ * land before the binary exits, and async/await across a tiny write is just
59
+ * latency for no benefit.
60
+ *
61
+ * Returns the envelope so callers can chain off it (debug logs, post-emit
62
+ * verdict requests).
63
+ */
64
+ export function emit<TType extends EventType, TData>(
65
+ coordRoot: string,
66
+ input: EmitInput<TType, TData>,
67
+ ): EventEnvelope<TType, TData> {
68
+ const envelope = buildEnvelope(input);
69
+ const streamPath = join(coordRoot, STREAM_FILE);
70
+ const lockPath = join(coordRoot, LOCK_FILE);
71
+
72
+ ensureDir(dirname(streamPath));
73
+ ensureFile(lockPath);
74
+
75
+ let line = `${JSON.stringify(envelope)}\n`;
76
+ if (Buffer.byteLength(line, "utf8") > MAX_LINE_BYTES) {
77
+ // lines >64KB are a schema bug. Phase 2 hard-truncates the `data`
78
+ // payload + sets `truncated: true` so we keep audit visibility without
79
+ // breaking downstream consumers.
80
+ const truncated = `${JSON.stringify({
81
+ ...envelope,
82
+ data: { __over_size_limit: true, original_bytes: Buffer.byteLength(line, "utf8") },
83
+ // eslint-disable-next-line @typescript-eslint/naming-convention
84
+ })}\n`;
85
+ line = truncated;
86
+ }
87
+
88
+ const lockFd = openSync(lockPath, "r+");
89
+ try {
90
+ // flock isn't directly exposed in node fs; use fcntl-like via shelling out
91
+ // would be slower than POSIX flock(2) inside the kernel. Bun exposes
92
+ // `Bun.flock` in recent builds, but a simple fs-level append is atomic
93
+ // for lines <PIPE_BUF (4KB on Linux) anyway. For larger lines we rely on
94
+ // append-mode flag (O_APPEND) which kernels serialize per-fd.
95
+ //
96
+ // The `events.ndjson.lock` file is kept around for forward
97
+ // compatibility: Phase 4+ helpers acquire it before multi-write batches.
98
+ appendFileSync(streamPath, line, { encoding: "utf8", flag: "a" });
99
+ } finally {
100
+ closeSync(lockFd);
101
+ }
102
+
103
+ return envelope;
104
+ }
105
+
106
+ function ensureDir(path: string): void {
107
+ try {
108
+ mkdirSync(path, { recursive: true });
109
+ } catch {
110
+ /* swallow */
111
+ }
112
+ }
113
+
114
+ function ensureFile(path: string): void {
115
+ try {
116
+ closeSync(openSync(path, "a"));
117
+ } catch {
118
+ /* swallow */
119
+ }
120
+ }